update
This commit is contained in:
parent
cc97f6aebb
commit
7404498d46
@ -17,13 +17,16 @@ App({
|
||||
|
||||
setUser(user) {
|
||||
const savedClass = wx.getStorageSync("active_class") || null;
|
||||
const savedClassValid = savedClass?.id && user.memberships?.some(
|
||||
const memberships = Array.isArray(user.memberships) ? user.memberships : [];
|
||||
const savedClassValid = savedClass && savedClass.id && memberships.some(
|
||||
(membership) => membership.class_id === savedClass.id
|
||||
);
|
||||
const activeMembership = user.active_membership || null;
|
||||
const firstMembership = memberships.length ? memberships[0] : null;
|
||||
this.globalData.user = user;
|
||||
this.globalData.activeClassId = savedClassValid
|
||||
? savedClass.id
|
||||
: user.active_membership?.class_id || user.memberships?.[0]?.class_id || null;
|
||||
: (activeMembership && activeMembership.class_id) || (firstMembership && firstMembership.class_id) || null;
|
||||
this.globalData.enabledModules = savedClassValid
|
||||
? savedClass.enabled_modules
|
||||
: user.enabled_modules || null;
|
||||
|
||||
@ -1,14 +1,15 @@
|
||||
{
|
||||
"pages": [
|
||||
"pages/bind/index",
|
||||
"pages/home/index",
|
||||
"pages/class/index",
|
||||
"pages/interact/index",
|
||||
"pages/mine/index",
|
||||
"pages/bind/index",
|
||||
"pages/module/index",
|
||||
"pages/manage/index",
|
||||
"pages/member-detail/index",
|
||||
"pages/schedule-detail/index",
|
||||
"pages/vote-detail/index",
|
||||
"pages/timeline-detail/index",
|
||||
"pages/timeline-create/index",
|
||||
"pages/profile-edit/index",
|
||||
|
||||
@ -230,6 +230,151 @@ page {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.fab-button {
|
||||
position: fixed;
|
||||
right: 34rpx;
|
||||
bottom: calc(44rpx + env(safe-area-inset-bottom));
|
||||
z-index: 40;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 108rpx;
|
||||
min-width: 108rpx;
|
||||
max-width: 108rpx;
|
||||
height: 108rpx;
|
||||
min-height: 108rpx;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 999rpx;
|
||||
background: #6b1f2b;
|
||||
box-shadow: 0 18rpx 42rpx rgba(107, 31, 43, 0.28);
|
||||
color: #fff8ed;
|
||||
font-size: 62rpx;
|
||||
font-weight: 420;
|
||||
line-height: 108rpx;
|
||||
}
|
||||
|
||||
.fab-button::after {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.fab-spacer {
|
||||
height: 144rpx;
|
||||
}
|
||||
|
||||
.form-page {
|
||||
padding-bottom: calc(140rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.form-header {
|
||||
margin-bottom: 26rpx;
|
||||
}
|
||||
|
||||
.form-kicker {
|
||||
color: #8b5a36;
|
||||
font-size: 22rpx;
|
||||
font-weight: 760;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.form-title {
|
||||
margin-top: 8rpx;
|
||||
color: #31211c;
|
||||
font-size: 44rpx;
|
||||
font-weight: 780;
|
||||
line-height: 1.18;
|
||||
}
|
||||
|
||||
.form-subtitle {
|
||||
margin-top: 10rpx;
|
||||
color: #8a7b70;
|
||||
font-size: 25rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
margin-bottom: 18rpx;
|
||||
border: 1rpx solid rgba(121, 84, 54, 0.11);
|
||||
border-radius: 28rpx;
|
||||
background: rgba(255, 252, 247, 0.96);
|
||||
padding: 28rpx;
|
||||
box-shadow: 0 14rpx 36rpx rgba(68, 39, 27, 0.06);
|
||||
}
|
||||
|
||||
.form-field {
|
||||
margin-top: 26rpx;
|
||||
}
|
||||
|
||||
.form-field:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12rpx;
|
||||
color: #3a221d;
|
||||
font-size: 25rpx;
|
||||
font-weight: 730;
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
color: #9b8b7f;
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-input,
|
||||
.form-textarea,
|
||||
.form-select {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
border: 1rpx solid rgba(121, 84, 54, 0.14);
|
||||
border-radius: 22rpx;
|
||||
background: #fffaf3;
|
||||
color: #30211c;
|
||||
font-size: 29rpx;
|
||||
}
|
||||
|
||||
.form-input,
|
||||
.form-select {
|
||||
min-height: 82rpx;
|
||||
padding: 0 24rpx;
|
||||
line-height: 82rpx;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
min-height: 190rpx;
|
||||
padding: 22rpx 24rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.form-switch-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 82rpx;
|
||||
}
|
||||
|
||||
.form-submit-bar {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 30;
|
||||
box-sizing: border-box;
|
||||
padding: 18rpx 32rpx calc(18rpx + env(safe-area-inset-bottom));
|
||||
background: rgba(250, 244, 235, 0.92);
|
||||
backdrop-filter: blur(18rpx);
|
||||
}
|
||||
|
||||
.form-submit-bar .button {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.empty {
|
||||
margin-top: 120rpx;
|
||||
text-align: center;
|
||||
@ -272,14 +417,30 @@ page {
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.pill.done {
|
||||
background: #dff3e8;
|
||||
color: #1f7a4d;
|
||||
}
|
||||
|
||||
.member-row,
|
||||
.schedule-row,
|
||||
.feed-head {
|
||||
.feed-head,
|
||||
.vote-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 22rpx;
|
||||
}
|
||||
|
||||
.vote-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10rpx 18rpx;
|
||||
margin-top: 14rpx;
|
||||
color: #8a7b70;
|
||||
font-size: 23rpx;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.more-dot {
|
||||
flex: none;
|
||||
min-width: 58rpx;
|
||||
@ -360,6 +521,94 @@ page {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.fund-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 14rpx;
|
||||
}
|
||||
|
||||
.fund-summary-item {
|
||||
min-height: 126rpx;
|
||||
box-sizing: border-box;
|
||||
padding: 20rpx 16rpx;
|
||||
border: 1rpx solid rgba(121, 84, 54, 0.1);
|
||||
border-radius: 26rpx;
|
||||
background: rgba(255, 252, 247, 0.96);
|
||||
box-shadow: 0 14rpx 34rpx rgba(68, 39, 27, 0.06);
|
||||
}
|
||||
|
||||
.fund-summary-item.strong {
|
||||
background: #6b1f2b;
|
||||
}
|
||||
|
||||
.fund-summary-item.strong .fund-label,
|
||||
.fund-summary-item.strong .fund-number {
|
||||
color: #fff8ed;
|
||||
}
|
||||
|
||||
.fund-label {
|
||||
color: #8a7b70;
|
||||
font-size: 22rpx;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.fund-number {
|
||||
margin-top: 14rpx;
|
||||
color: #30211c;
|
||||
font-size: 28rpx;
|
||||
font-weight: 780;
|
||||
}
|
||||
|
||||
.fund-number.income,
|
||||
.fund-amount.income {
|
||||
color: #1f7a4d;
|
||||
}
|
||||
|
||||
.fund-number.expense,
|
||||
.fund-amount.expense {
|
||||
color: #9a3a2f;
|
||||
}
|
||||
|
||||
.fund-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.fund-type-badge {
|
||||
flex: none;
|
||||
min-width: 72rpx;
|
||||
height: 48rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 23rpx;
|
||||
font-weight: 750;
|
||||
line-height: 48rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.fund-type-badge.income {
|
||||
background: #dff3e8;
|
||||
color: #1f7a4d;
|
||||
}
|
||||
|
||||
.fund-type-badge.expense {
|
||||
background: #f7e1dc;
|
||||
color: #9a3a2f;
|
||||
}
|
||||
|
||||
.fund-row-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 18rpx;
|
||||
}
|
||||
|
||||
.fund-amount {
|
||||
flex: none;
|
||||
font-size: 30rpx;
|
||||
font-weight: 780;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea,
|
||||
picker {
|
||||
|
||||
@ -20,6 +20,12 @@ Page({
|
||||
password: ""
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
if (wx.getStorageSync("auth_token")) {
|
||||
wx.switchTab({ url: "/pages/home/index" });
|
||||
}
|
||||
},
|
||||
|
||||
switchLoginMethod(event) {
|
||||
const method = event.currentTarget.dataset.method;
|
||||
this.setData({
|
||||
@ -102,7 +108,7 @@ Page({
|
||||
},
|
||||
|
||||
async bindWithPhone(event) {
|
||||
const phoneCode = event.detail?.code;
|
||||
const phoneCode = event.detail && event.detail.code;
|
||||
if (!phoneCode) {
|
||||
wx.showToast({ title: "需要授权手机号完成绑定", icon: "none" });
|
||||
return;
|
||||
@ -129,7 +135,7 @@ Page({
|
||||
},
|
||||
|
||||
bindCurrentWithPhone(event) {
|
||||
const phoneCode = event.detail?.code;
|
||||
const phoneCode = event.detail && event.detail.code;
|
||||
if (!phoneCode) {
|
||||
wx.showToast({ title: "需要授权手机号完成绑定", icon: "none" });
|
||||
return;
|
||||
|
||||
3
miniprogram/pages/bind/index.json
Normal file
3
miniprogram/pages/bind/index.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "登录"
|
||||
}
|
||||
@ -3,6 +3,30 @@ const { refreshMe, requireLogin } = require("../../utils/auth");
|
||||
const { isModuleEnabled, visibleModules } = require("../../utils/modules");
|
||||
const { getActiveClassId, getActiveClassName, getEnabledModules, showError } = require("../../utils/page-helpers");
|
||||
|
||||
function formatDateTime(value) {
|
||||
if (!value) return "";
|
||||
return String(value).replace("T", " ").slice(0, 16);
|
||||
}
|
||||
|
||||
function formatScheduleTime(item) {
|
||||
const start = formatDateTime(item.start_time);
|
||||
const end = formatDateTime(item.end_time);
|
||||
if (item.type === "deadline") return `截止 ${start}`;
|
||||
if (!end) return start;
|
||||
if (start.slice(0, 10) === end.slice(0, 10)) {
|
||||
return `${start} - ${end.slice(11)}`;
|
||||
}
|
||||
return `${start} - ${end}`;
|
||||
}
|
||||
|
||||
function scheduleTypeText(type) {
|
||||
return {
|
||||
course: "课程",
|
||||
deadline: "截止日",
|
||||
activity: "活动"
|
||||
}[type] || type || "排期";
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
className: "HKU ICB",
|
||||
@ -62,8 +86,20 @@ Page({
|
||||
const value = results[index];
|
||||
if (name === "unread") next.unreadCount = value.unread_count || 0;
|
||||
if (name === "announcements") next.announcements = value.items || [];
|
||||
if (name === "schedules") next.schedules = value || [];
|
||||
if (name === "votes") next.votes = value.items || [];
|
||||
if (name === "schedules") {
|
||||
next.schedules = (value || []).map((item) => ({
|
||||
...item,
|
||||
schedule_time_text: formatScheduleTime(item),
|
||||
schedule_type_text: scheduleTypeText(item.type)
|
||||
}));
|
||||
}
|
||||
if (name === "votes") {
|
||||
next.votes = (value.items || []).map((item) => ({
|
||||
...item,
|
||||
vote_type_text: item.vote_type === "multiple" ? "多选" : "单选",
|
||||
vote_action_text: item.has_voted ? "已参与" : "待参与"
|
||||
}));
|
||||
}
|
||||
if (name === "timelines") next.timelines = value.items || [];
|
||||
});
|
||||
this.setData(next);
|
||||
@ -83,6 +119,10 @@ Page({
|
||||
wx.navigateTo({ url: `/pages/schedule-detail/index?id=${event.currentTarget.dataset.id}` });
|
||||
},
|
||||
|
||||
openVote(event) {
|
||||
wx.navigateTo({ url: `/pages/vote-detail/index?id=${event.currentTarget.dataset.id}` });
|
||||
},
|
||||
|
||||
openTimeline(event) {
|
||||
wx.navigateTo({ url: `/pages/timeline-detail/index?id=${event.currentTarget.dataset.id}` });
|
||||
}
|
||||
|
||||
@ -60,8 +60,9 @@
|
||||
<view class="row-body">
|
||||
<view class="card-title">{{item.title}}</view>
|
||||
<view class="muted">{{item.location || "地点待定"}}</view>
|
||||
<view class="muted">{{item.schedule_time_text}}</view>
|
||||
</view>
|
||||
<view class="pill">{{item.type}}</view>
|
||||
<view class="pill">{{item.schedule_type_text}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@ -71,12 +72,12 @@
|
||||
<view class="section-title">班级投票</view>
|
||||
<view class="section-action" bindtap="openModule" data-key="votes">参与</view>
|
||||
</view>
|
||||
<view wx:for="{{votes}}" wx:key="id" class="card" bindtap="openModule" data-key="votes">
|
||||
<view wx:for="{{votes}}" wx:key="id" class="card" bindtap="openVote" 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.has_voted ? "已参与" : "待参与"}}</view>
|
||||
<view class="muted">{{item.vote_type_text}} · {{item.vote_action_text}} · {{item.total_voters}} 人参与</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@ -6,7 +6,7 @@ const { ensureModuleOpen, getActiveClassId, showError } = require("../../utils/p
|
||||
const FORM_DEFAULTS = {
|
||||
announcements: { title: "", content: "", is_pinned: false },
|
||||
votes: { title: "", description: "", options_text: "", vote_type: "single", is_anonymous: false },
|
||||
schedule: { title: "", type: "course", start_time: "", location: "", description: "" },
|
||||
schedule: { title: "", type: "course", start_time: "", end_time: "", location: "", description: "" },
|
||||
fund: { type: "expense", amount: "", category: "", description: "", record_date: "" }
|
||||
};
|
||||
|
||||
@ -19,8 +19,20 @@ Page({
|
||||
isVotes: false,
|
||||
isSchedule: false,
|
||||
isFund: false,
|
||||
scheduleTypes: ["course", "deadline", "activity"],
|
||||
isScheduleDeadline: false,
|
||||
scheduleTimeLabel: "开始时间",
|
||||
isFundIncome: false,
|
||||
isFundExpense: true,
|
||||
fundIncomeClass: "",
|
||||
fundExpenseClass: "active expense",
|
||||
scheduleTypes: ["课程", "截止日", "活动"],
|
||||
scheduleTypeValues: ["course", "deadline", "activity"],
|
||||
scheduleTypeLabel: "课程",
|
||||
fundTypes: ["income", "expense"],
|
||||
fundTypeLabels: ["入账", "出账"],
|
||||
fundIncomeCategories: ["班费收取", "活动赞助", "其他收入"],
|
||||
fundExpenseCategories: ["聚餐", "活动物资", "场地费", "交通费", "礼品", "其他支出"],
|
||||
fundCategories: ["聚餐", "活动物资", "场地费", "交通费", "礼品", "其他支出"],
|
||||
loading: false
|
||||
},
|
||||
|
||||
@ -30,19 +42,37 @@ Page({
|
||||
const classId = getActiveClassId();
|
||||
const user = getApp().globalData.user;
|
||||
if (!module || !ensureModuleOpen(moduleKey) || !hasManagePermission(user, classId, moduleKey)) {
|
||||
const unavailableTitle = module ? module.title : "功能";
|
||||
wx.redirectTo({
|
||||
url: `/pages/module-unavailable/index?title=${encodeURIComponent(module?.title || "功能")}`
|
||||
url: `/pages/module-unavailable/index?title=${encodeURIComponent(unavailableTitle)}`
|
||||
});
|
||||
return;
|
||||
}
|
||||
const form = Object.assign({}, FORM_DEFAULTS[moduleKey] || {});
|
||||
if (moduleKey === "fund" && !form.record_date) {
|
||||
form.record_date = new Date().toISOString().slice(0, 10);
|
||||
}
|
||||
if (moduleKey === "fund" && !form.category) {
|
||||
form.category = form.type === "income" ? "班费收取" : "聚餐";
|
||||
}
|
||||
this.setData({
|
||||
moduleKey,
|
||||
title: `新增${module.title}`,
|
||||
form: Object.assign({}, FORM_DEFAULTS[moduleKey] || {}),
|
||||
form,
|
||||
isAnnouncements: moduleKey === "announcements",
|
||||
isVotes: moduleKey === "votes",
|
||||
isSchedule: moduleKey === "schedule",
|
||||
isFund: moduleKey === "fund"
|
||||
isScheduleDeadline: moduleKey === "schedule" && form.type === "deadline",
|
||||
scheduleTimeLabel: form.type === "deadline" ? "截止时间" : "开始时间",
|
||||
scheduleTypeLabel: moduleKey === "schedule" ? this.scheduleTypeText(form.type) : "课程",
|
||||
isFund: moduleKey === "fund",
|
||||
isFundIncome: form.type === "income",
|
||||
isFundExpense: form.type === "expense",
|
||||
fundIncomeClass: form.type === "income" ? "active income" : "",
|
||||
fundExpenseClass: form.type === "expense" ? "active expense" : "",
|
||||
fundCategories: form.type === "income"
|
||||
? this.data.fundIncomeCategories
|
||||
: this.data.fundExpenseCategories
|
||||
});
|
||||
wx.setNavigationBarTitle({ title: `新增${module.title}` });
|
||||
},
|
||||
@ -60,7 +90,62 @@ Page({
|
||||
onPicker(event) {
|
||||
const field = event.currentTarget.dataset.field;
|
||||
const value = event.currentTarget.dataset.values.split(",")[Number(event.detail.value)];
|
||||
this.setData({ [`form.${field}`]: value });
|
||||
const nextData = { [`form.${field}`]: value };
|
||||
if (this.data.moduleKey === "schedule" && field === "type") {
|
||||
nextData.isScheduleDeadline = value === "deadline";
|
||||
if (value === "deadline") {
|
||||
nextData["form.end_time"] = "";
|
||||
}
|
||||
}
|
||||
this.setData(nextData);
|
||||
},
|
||||
|
||||
scheduleTypeText(type) {
|
||||
return {
|
||||
course: "课程",
|
||||
deadline: "截止日",
|
||||
activity: "活动"
|
||||
}[type] || type || "课程";
|
||||
},
|
||||
|
||||
onScheduleTypeChange(event) {
|
||||
const index = Number(event.detail.value);
|
||||
const value = this.data.scheduleTypeValues[index];
|
||||
const nextData = {
|
||||
"form.type": value,
|
||||
scheduleTypeLabel: this.scheduleTypeText(value),
|
||||
isScheduleDeadline: value === "deadline",
|
||||
scheduleTimeLabel: value === "deadline" ? "截止时间" : "开始时间"
|
||||
};
|
||||
if (value === "deadline") {
|
||||
nextData["form.end_time"] = "";
|
||||
}
|
||||
this.setData(nextData);
|
||||
},
|
||||
|
||||
setFundType(event) {
|
||||
const type = event.currentTarget.dataset.type;
|
||||
const categories = type === "income"
|
||||
? this.data.fundIncomeCategories
|
||||
: this.data.fundExpenseCategories;
|
||||
this.setData({
|
||||
"form.type": type,
|
||||
"form.category": categories[0],
|
||||
isFundIncome: type === "income",
|
||||
isFundExpense: type === "expense",
|
||||
fundIncomeClass: type === "income" ? "active income" : "",
|
||||
fundExpenseClass: type === "expense" ? "active expense" : "",
|
||||
fundCategories: categories
|
||||
});
|
||||
},
|
||||
|
||||
onDateChange(event) {
|
||||
this.setData({ "form.record_date": event.detail.value });
|
||||
},
|
||||
|
||||
onFundCategoryChange(event) {
|
||||
const index = Number(event.detail.value);
|
||||
this.setData({ "form.category": this.data.fundCategories[index] });
|
||||
},
|
||||
|
||||
async submit() {
|
||||
@ -91,15 +176,40 @@ Page({
|
||||
});
|
||||
}
|
||||
if (moduleKey === "schedule") {
|
||||
if (!form.title || !form.start_time) {
|
||||
wx.showToast({ title: "请填写标题和开始时间", icon: "none" });
|
||||
return;
|
||||
}
|
||||
if (form.type !== "deadline" && !form.end_time) {
|
||||
wx.showToast({ title: "请填写结束时间", icon: "none" });
|
||||
return;
|
||||
}
|
||||
if (form.end_time && new Date(form.end_time).getTime() <= new Date(form.start_time).getTime()) {
|
||||
wx.showToast({ title: "结束时间应晚于开始时间", icon: "none" });
|
||||
return;
|
||||
}
|
||||
await post(`/api/schedule/?class_id=${classId}`, {
|
||||
title: form.title,
|
||||
type: form.type,
|
||||
start_time: form.start_time,
|
||||
end_time: form.end_time || null,
|
||||
location: form.location || null,
|
||||
description: form.description || null
|
||||
});
|
||||
}
|
||||
if (moduleKey === "fund") {
|
||||
if (!form.amount || Number(form.amount) <= 0) {
|
||||
wx.showToast({ title: "请输入有效金额", icon: "none" });
|
||||
return;
|
||||
}
|
||||
if (!form.category) {
|
||||
wx.showToast({ title: "请输入分类", icon: "none" });
|
||||
return;
|
||||
}
|
||||
if (!form.record_date) {
|
||||
wx.showToast({ title: "请选择日期", icon: "none" });
|
||||
return;
|
||||
}
|
||||
await post(`/api/fund/?class_id=${classId}`, {
|
||||
type: form.type,
|
||||
amount: Number(form.amount),
|
||||
|
||||
3
miniprogram/pages/manage/index.json
Normal file
3
miniprogram/pages/manage/index.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "新增"
|
||||
}
|
||||
@ -1,54 +1,114 @@
|
||||
<view class="page">
|
||||
<view class="section-title">{{title}}</view>
|
||||
|
||||
<view class="card">
|
||||
<view class="muted">标题</view>
|
||||
<input value="{{form.title}}" data-field="title" bindinput="onInput" placeholder="请输入标题" />
|
||||
<view class="page form-page">
|
||||
<view class="form-header">
|
||||
<view class="form-kicker">CLASS MANAGEMENT</view>
|
||||
<view class="form-title">{{title}}</view>
|
||||
<view class="form-subtitle">填写必要信息后保存,内容会同步到当前班级。</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{isAnnouncements}}" class="card">
|
||||
<view class="muted">内容</view>
|
||||
<textarea value="{{form.content}}" data-field="content" bindinput="onInput" placeholder="请输入公告内容" />
|
||||
<view class="muted">置顶</view>
|
||||
<view wx:if="{{!isFund}}" class="form-card">
|
||||
<view class="form-field">
|
||||
<view class="form-label">标题</view>
|
||||
<input class="form-input" value="{{form.title}}" data-field="title" bindinput="onInput" placeholder="请输入标题" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{isAnnouncements}}" class="form-card">
|
||||
<view class="form-field">
|
||||
<view class="form-label">内容</view>
|
||||
<textarea class="form-textarea" value="{{form.content}}" data-field="content" bindinput="onInput" placeholder="请输入公告内容" />
|
||||
</view>
|
||||
<view class="form-field form-switch-row">
|
||||
<view>
|
||||
<view class="form-label">置顶公告</view>
|
||||
<view class="form-hint">开启后会优先显示在首页</view>
|
||||
</view>
|
||||
<switch checked="{{form.is_pinned}}" data-field="is_pinned" bindchange="onSwitch" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{isVotes}}" class="card">
|
||||
<view class="muted">说明</view>
|
||||
<textarea value="{{form.description}}" data-field="description" bindinput="onInput" placeholder="请输入投票说明" />
|
||||
<view class="muted">选项,每行一个</view>
|
||||
<textarea value="{{form.options_text}}" data-field="options_text" bindinput="onInput" placeholder="选项 A 选项 B" />
|
||||
<view class="muted">匿名</view>
|
||||
<view wx:if="{{isVotes}}" class="form-card">
|
||||
<view class="form-field">
|
||||
<view class="form-label">说明</view>
|
||||
<textarea class="form-textarea" value="{{form.description}}" data-field="description" bindinput="onInput" placeholder="请输入投票说明" />
|
||||
</view>
|
||||
<view class="form-field">
|
||||
<view class="form-label">
|
||||
<text>选项</text>
|
||||
<text class="form-hint">每行一个</text>
|
||||
</view>
|
||||
<textarea class="form-textarea" value="{{form.options_text}}" data-field="options_text" bindinput="onInput" placeholder="选项 A 选项 B" />
|
||||
</view>
|
||||
<view class="form-field form-switch-row">
|
||||
<view>
|
||||
<view class="form-label">匿名投票</view>
|
||||
<view class="form-hint">同学参与后不展示个人选择</view>
|
||||
</view>
|
||||
<switch checked="{{form.is_anonymous}}" data-field="is_anonymous" bindchange="onSwitch" />
|
||||
</view>
|
||||
|
||||
<view wx:if="{{isSchedule}}" class="card">
|
||||
<view class="muted">类型</view>
|
||||
<picker mode="selector" range="{{scheduleTypes}}" data-field="type" data-values="course,deadline,activity" bindchange="onPicker">
|
||||
<view>{{form.type}}</view>
|
||||
</picker>
|
||||
<view class="muted">开始时间,格式:2026-05-07T09:00:00</view>
|
||||
<input value="{{form.start_time}}" data-field="start_time" bindinput="onInput" placeholder="2026-05-07T09:00:00" />
|
||||
<view class="muted">地点</view>
|
||||
<input value="{{form.location}}" data-field="location" bindinput="onInput" placeholder="地点" />
|
||||
<view class="muted">说明</view>
|
||||
<textarea value="{{form.description}}" data-field="description" bindinput="onInput" placeholder="说明" />
|
||||
</view>
|
||||
|
||||
<view wx:if="{{isFund}}" class="card">
|
||||
<view class="muted">类型</view>
|
||||
<picker mode="selector" range="{{fundTypes}}" data-field="type" data-values="income,expense" bindchange="onPicker">
|
||||
<view>{{form.type}}</view>
|
||||
<view wx:if="{{isSchedule}}" class="form-card">
|
||||
<view class="form-field">
|
||||
<view class="form-label">类型</view>
|
||||
<picker mode="selector" range="{{scheduleTypes}}" bindchange="onScheduleTypeChange">
|
||||
<view class="form-select">{{scheduleTypeLabel}}</view>
|
||||
</picker>
|
||||
<view class="muted">金额</view>
|
||||
<input type="digit" value="{{form.amount}}" data-field="amount" bindinput="onInput" placeholder="0.00" />
|
||||
<view class="muted">分类</view>
|
||||
<input value="{{form.category}}" data-field="category" bindinput="onInput" placeholder="例如:聚餐" />
|
||||
<view class="muted">日期,格式:2026-05-07</view>
|
||||
<input value="{{form.record_date}}" data-field="record_date" bindinput="onInput" placeholder="2026-05-07" />
|
||||
<view class="muted">说明</view>
|
||||
<textarea value="{{form.description}}" data-field="description" bindinput="onInput" placeholder="说明" />
|
||||
</view>
|
||||
<view class="form-field">
|
||||
<view class="form-label">
|
||||
<text>{{scheduleTimeLabel}}</text>
|
||||
<text class="form-hint">格式:2026-05-07T09:00:00</text>
|
||||
</view>
|
||||
<input class="form-input" value="{{form.start_time}}" data-field="start_time" bindinput="onInput" placeholder="2026-05-07T09:00:00" />
|
||||
</view>
|
||||
<view wx:if="{{!isScheduleDeadline}}" class="form-field">
|
||||
<view class="form-label">
|
||||
<text>结束时间</text>
|
||||
<text class="form-hint">课程/活动必填</text>
|
||||
</view>
|
||||
<input class="form-input" value="{{form.end_time}}" data-field="end_time" bindinput="onInput" placeholder="2026-05-07T12:00:00" />
|
||||
</view>
|
||||
<view class="form-field">
|
||||
<view class="form-label">地点</view>
|
||||
<input class="form-input" value="{{form.location}}" data-field="location" bindinput="onInput" placeholder="地点" />
|
||||
</view>
|
||||
<view class="form-field">
|
||||
<view class="form-label">说明</view>
|
||||
<textarea class="form-textarea" value="{{form.description}}" data-field="description" bindinput="onInput" placeholder="说明" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{isFund}}" class="form-card">
|
||||
<view class="form-field">
|
||||
<view class="form-label">收支类型</view>
|
||||
<view class="fund-type-switch">
|
||||
<view class="fund-type {{fundIncomeClass}}" data-type="income" bindtap="setFundType">入账</view>
|
||||
<view class="fund-type {{fundExpenseClass}}" data-type="expense" bindtap="setFundType">出账</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-field">
|
||||
<view class="form-label">金额</view>
|
||||
<input class="form-input" type="digit" value="{{form.amount}}" data-field="amount" bindinput="onInput" placeholder="0.00" />
|
||||
</view>
|
||||
<view class="form-field">
|
||||
<view class="form-label">小类型</view>
|
||||
<picker mode="selector" range="{{fundCategories}}" bindchange="onFundCategoryChange">
|
||||
<view class="form-select">{{form.category}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-field">
|
||||
<view class="form-label">日期</view>
|
||||
<picker mode="date" value="{{form.record_date}}" bindchange="onDateChange">
|
||||
<view class="form-select">{{form.record_date}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-field">
|
||||
<view class="form-label">备注</view>
|
||||
<textarea class="form-textarea" value="{{form.description}}" data-field="description" bindinput="onInput" placeholder="补充说明,可不填" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-submit-bar">
|
||||
<button class="button" loading="{{loading}}" bindtap="submit">保存</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@ -1,12 +1,32 @@
|
||||
input {
|
||||
.fund-type-switch {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
margin-top: 12rpx;
|
||||
height: 72rpx;
|
||||
font-size: 30rpx;
|
||||
padding: 8rpx;
|
||||
border-radius: 24rpx;
|
||||
background: #f1e4d4;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
min-height: 160rpx;
|
||||
margin-top: 12rpx;
|
||||
font-size: 28rpx;
|
||||
.fund-type {
|
||||
flex: 1;
|
||||
height: 72rpx;
|
||||
border-radius: 20rpx;
|
||||
color: #7f7065;
|
||||
font-size: 27rpx;
|
||||
font-weight: 700;
|
||||
line-height: 72rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.fund-type.active {
|
||||
background: #fffaf3;
|
||||
color: #6b1f2b;
|
||||
}
|
||||
|
||||
.fund-type.income {
|
||||
color: #1f7a4d;
|
||||
}
|
||||
|
||||
.fund-type.expense {
|
||||
color: #9a3a2f;
|
||||
}
|
||||
|
||||
@ -12,10 +12,11 @@ Page({
|
||||
const classRes = await get("/api/classes/");
|
||||
const app = getApp();
|
||||
const activeClassId = app.globalData.activeClassId;
|
||||
const activeMembership = user.memberships?.find((item) => item.class_id === activeClassId) || user.active_membership;
|
||||
const memberships = Array.isArray(user.memberships) ? user.memberships : [];
|
||||
const activeMembership = memberships.find((item) => item.class_id === activeClassId) || user.active_membership;
|
||||
this.setData({
|
||||
user,
|
||||
activeClassName: activeMembership?.class_name || "",
|
||||
activeClassName: (activeMembership && activeMembership.class_name) || "",
|
||||
classes: (classRes.items || []).map((item) => ({
|
||||
...item,
|
||||
is_active: item.id === activeClassId
|
||||
|
||||
@ -9,11 +9,33 @@ const ENDPOINTS = {
|
||||
timeline: "/api/timeline/",
|
||||
votes: "/api/votes/",
|
||||
schedule: "/api/schedule/",
|
||||
resources: "/api/resources/",
|
||||
reading_corner: "/api/reading/feed",
|
||||
fund: "/api/fund/"
|
||||
};
|
||||
|
||||
function formatDateTime(value) {
|
||||
if (!value) return "";
|
||||
return String(value).replace("T", " ").slice(0, 16);
|
||||
}
|
||||
|
||||
function formatScheduleTime(item) {
|
||||
const start = formatDateTime(item.start_time);
|
||||
const end = formatDateTime(item.end_time);
|
||||
if (item.type === "deadline") return `截止 ${start}`;
|
||||
if (!end) return start;
|
||||
if (start.slice(0, 10) === end.slice(0, 10)) {
|
||||
return `${start} - ${end.slice(11)}`;
|
||||
}
|
||||
return `${start} - ${end}`;
|
||||
}
|
||||
|
||||
function scheduleTypeText(type) {
|
||||
return {
|
||||
course: "课程",
|
||||
deadline: "截止日",
|
||||
activity: "活动"
|
||||
}[type] || type || "排期";
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
moduleKey: "",
|
||||
@ -25,6 +47,9 @@ Page({
|
||||
isTimeline: false,
|
||||
isDirectory: false,
|
||||
isSchedule: false,
|
||||
isFund: false,
|
||||
isVotes: false,
|
||||
fundStats: null,
|
||||
needsRefresh: false,
|
||||
loading: false
|
||||
},
|
||||
@ -34,24 +59,28 @@ Page({
|
||||
const module = getModule(moduleKey);
|
||||
const app = getApp();
|
||||
const classId = getActiveClassId();
|
||||
const title = module ? module.title : "功能";
|
||||
const icon = module ? module.icon : "项";
|
||||
this.setData({
|
||||
moduleKey,
|
||||
title: module?.title || "功能",
|
||||
moduleIcon: module?.icon || "项",
|
||||
title,
|
||||
moduleIcon: icon,
|
||||
isTimeline: moduleKey === "timeline",
|
||||
isDirectory: moduleKey === "directory",
|
||||
isSchedule: moduleKey === "schedule",
|
||||
isFund: moduleKey === "fund",
|
||||
isVotes: moduleKey === "votes",
|
||||
canPostTimeline: moduleKey === "timeline",
|
||||
canManage: ["announcements", "votes", "schedule", "fund"].includes(moduleKey) &&
|
||||
hasManagePermission(app.globalData.user, classId, moduleKey)
|
||||
});
|
||||
wx.setNavigationBarTitle({ title: module?.title || "功能" });
|
||||
wx.setNavigationBarTitle({ title });
|
||||
if (!module || !ensureModuleOpen(moduleKey)) return;
|
||||
this.load();
|
||||
},
|
||||
|
||||
onShow() {
|
||||
if (this.data.moduleKey) {
|
||||
if (this.data.moduleKey && getModule(this.data.moduleKey)) {
|
||||
this.setData({ needsRefresh: false });
|
||||
this.load();
|
||||
}
|
||||
@ -68,18 +97,40 @@ Page({
|
||||
if (!endpoint) return;
|
||||
this.setData({ loading: true });
|
||||
try {
|
||||
let stats = null;
|
||||
if (this.data.moduleKey === "fund") {
|
||||
stats = await get("/api/fund/statistics", { class_id: classId });
|
||||
}
|
||||
const res = await get(endpoint, { page_size: 20, class_id: classId });
|
||||
const rawItems = Array.isArray(res) ? res : res.items || [];
|
||||
const currentUserId = getApp().globalData.user?.id;
|
||||
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),
|
||||
schedule_day: item.start_time ? String(item.start_time).slice(8, 10) : "",
|
||||
schedule_month: item.start_time ? `${String(item.start_time).slice(5, 7)}月` : "",
|
||||
committee_text: item.committee_role ? ` · ${item.committee_role}` : ""
|
||||
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)
|
||||
}));
|
||||
this.setData({ items });
|
||||
const fundStats = stats ? {
|
||||
...stats,
|
||||
total_income_text: formatAmount(stats.total_income),
|
||||
total_expense_text: formatAmount(stats.total_expense),
|
||||
balance_text: formatAmount(stats.balance)
|
||||
} : null;
|
||||
this.setData({ items, fundStats });
|
||||
} catch (error) {
|
||||
if (error.message === "该功能当前未开放") {
|
||||
wx.redirectTo({
|
||||
@ -109,13 +160,16 @@ Page({
|
||||
wx.navigateTo({ url: `/pages/timeline-detail/index?id=${id}` });
|
||||
return;
|
||||
}
|
||||
if (key === "votes") {
|
||||
wx.navigateTo({ url: `/pages/vote-detail/index?id=${id}` });
|
||||
}
|
||||
},
|
||||
|
||||
previewImage(event) {
|
||||
const current = event.currentTarget.dataset.src;
|
||||
const postId = Number(event.currentTarget.dataset.postId);
|
||||
const post = this.data.items.find((item) => item.id === postId);
|
||||
const urls = post?.image_urls || [];
|
||||
const urls = post && post.image_urls ? post.image_urls : [];
|
||||
if (!current || !urls.length) return;
|
||||
wx.previewImage({ current, urls });
|
||||
},
|
||||
@ -152,5 +206,15 @@ Page({
|
||||
|
||||
openTimelineCreate() {
|
||||
wx.navigateTo({ url: "/pages/timeline-create/index" });
|
||||
},
|
||||
|
||||
openCreate() {
|
||||
if (this.data.canPostTimeline) {
|
||||
this.openTimelineCreate();
|
||||
return;
|
||||
}
|
||||
if (this.data.canManage) {
|
||||
this.openManage();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -4,9 +4,31 @@
|
||||
<view class="hero-title">{{title}}</view>
|
||||
<view class="hero-subtitle">当前内容来自已开放的班级模块,关闭后会自动隐藏入口。</view>
|
||||
</view>
|
||||
<button wx:if="{{canPostTimeline}}" class="button" bindtap="openTimelineCreate">发布动态</button>
|
||||
<button wx:if="{{canManage}}" class="button" bindtap="openManage">新增{{title}}</button>
|
||||
|
||||
<view wx:if="{{isFund && fundStats}}" class="section">
|
||||
<view class="section-head">
|
||||
<view class="section-title">班费汇总</view>
|
||||
</view>
|
||||
<view class="fund-summary">
|
||||
<view class="fund-summary-item">
|
||||
<view class="fund-label">收入</view>
|
||||
<view class="fund-number income">¥{{fundStats.total_income_text}}</view>
|
||||
</view>
|
||||
<view class="fund-summary-item">
|
||||
<view class="fund-label">支出</view>
|
||||
<view class="fund-number expense">¥{{fundStats.total_expense_text}}</view>
|
||||
</view>
|
||||
<view class="fund-summary-item strong">
|
||||
<view class="fund-label">结余</view>
|
||||
<view class="fund-number">¥{{fundStats.balance_text}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view wx:if="{{isFund}}" class="section-head">
|
||||
<view class="section-title">班费明细</view>
|
||||
</view>
|
||||
<view wx:for="{{items}}" wx:key="id" class="card" data-id="{{item.id || item.user_id}}" bindtap="openItem">
|
||||
<view wx:if="{{isTimeline}}" class="feed-card">
|
||||
<view class="feed-head">
|
||||
@ -44,9 +66,35 @@
|
||||
<view class="row-body">
|
||||
<view class="card-title">{{item.title}}</view>
|
||||
<view class="muted">{{item.location || "地点待定"}}</view>
|
||||
<view class="muted">{{item.start_time}}</view>
|
||||
<view class="muted">{{item.schedule_time_text}}</view>
|
||||
</view>
|
||||
<view class="pill">{{item.schedule_type_text}}</view>
|
||||
</view>
|
||||
|
||||
<view wx:elif="{{isVotes}}" class="vote-row">
|
||||
<view class="row-mark">选</view>
|
||||
<view class="row-body">
|
||||
<view class="card-title">{{item.title}}</view>
|
||||
<view wx:if="{{item.description}}" class="muted">{{item.description}}</view>
|
||||
<view class="vote-meta">
|
||||
<text>{{item.vote_type_text}}</text>
|
||||
<text>{{item.vote_options_text}}</text>
|
||||
<text>{{item.total_voters}} 人参与</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="pill {{item.vote_pill_class}}">{{item.vote_action_text}}</view>
|
||||
</view>
|
||||
|
||||
<view wx:elif="{{isFund}}" class="fund-row">
|
||||
<view class="fund-type-badge {{item.fund_type_class}}">{{item.fund_type_text}}</view>
|
||||
<view class="row-body">
|
||||
<view class="fund-row-top">
|
||||
<view class="card-title">{{item.category}}</view>
|
||||
<view class="fund-amount {{item.fund_type_class}}">¥{{item.amount_text}}</view>
|
||||
</view>
|
||||
<view class="muted">{{item.description || "无备注"}}</view>
|
||||
<view class="muted">{{item.recorder_name}} · {{item.record_date}}</view>
|
||||
</view>
|
||||
<view class="pill">{{item.type}}</view>
|
||||
</view>
|
||||
|
||||
<view wx:else class="list-row">
|
||||
@ -61,4 +109,6 @@
|
||||
<view wx:if="{{!loading && !items.length}}" class="empty">
|
||||
<view class="muted">暂无内容</view>
|
||||
</view>
|
||||
<view wx:if="{{canPostTimeline || canManage}}" class="fab-spacer"></view>
|
||||
<button wx:if="{{canPostTimeline || canManage}}" class="fab-button" bindtap="openCreate" aria-label="新增">+</button>
|
||||
</view>
|
||||
|
||||
@ -4,6 +4,20 @@ const { showError } = require("../../utils/page-helpers");
|
||||
|
||||
Page({
|
||||
data: {
|
||||
industryOptions: [
|
||||
"金融",
|
||||
"科技",
|
||||
"咨询",
|
||||
"医疗健康",
|
||||
"教育",
|
||||
"房地产",
|
||||
"制造业",
|
||||
"消费零售",
|
||||
"能源",
|
||||
"传媒",
|
||||
"政府/公共事业",
|
||||
"其他"
|
||||
],
|
||||
form: {
|
||||
name: "",
|
||||
industry: "",
|
||||
@ -35,6 +49,11 @@ Page({
|
||||
this.setData({ [`form.${field}`]: event.detail.value });
|
||||
},
|
||||
|
||||
onIndustryChange(event) {
|
||||
const index = Number(event.detail.value);
|
||||
this.setData({ "form.industry": this.data.industryOptions[index] });
|
||||
},
|
||||
|
||||
async save() {
|
||||
this.setData({ loading: true });
|
||||
try {
|
||||
|
||||
@ -1,18 +1,42 @@
|
||||
<view class="page">
|
||||
<view class="hero">
|
||||
<view class="eyebrow">PROFILE</view>
|
||||
<view class="hero-title">编辑个人资料</view>
|
||||
<view class="hero-subtitle">这些信息会在成员名录中展示给同班同学。</view>
|
||||
<view class="page form-page">
|
||||
<view class="form-header">
|
||||
<view class="form-kicker">PROFILE</view>
|
||||
<view class="form-title">编辑个人资料</view>
|
||||
<view class="form-subtitle">这些信息会在成员名录中展示给同班同学。</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="card"><view class="muted">姓名</view><input value="{{form.name}}" data-field="name" bindinput="onInput" /></view>
|
||||
<view class="card"><view class="muted">行业</view><input value="{{form.industry}}" data-field="industry" bindinput="onInput" placeholder="例如:金融" /></view>
|
||||
<view class="card"><view class="muted">公司</view><input value="{{form.company}}" data-field="company" bindinput="onInput" /></view>
|
||||
<view class="card"><view class="muted">职位</view><input value="{{form.position}}" data-field="position" bindinput="onInput" /></view>
|
||||
<view class="card"><view class="muted">微信号</view><input value="{{form.wechat_id}}" data-field="wechat_id" bindinput="onInput" /></view>
|
||||
<view class="card"><view class="muted">简介</view><textarea value="{{form.bio}}" data-field="bio" bindinput="onInput" placeholder="简单介绍一下自己" /></view>
|
||||
<view class="form-card">
|
||||
<view class="form-field">
|
||||
<view class="form-label">姓名</view>
|
||||
<input class="form-input" value="{{form.name}}" data-field="name" bindinput="onInput" />
|
||||
</view>
|
||||
<view class="form-field">
|
||||
<view class="form-label">行业</view>
|
||||
<picker mode="selector" range="{{industryOptions}}" bindchange="onIndustryChange">
|
||||
<view class="form-select">{{form.industry || "请选择行业"}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-field">
|
||||
<view class="form-label">公司</view>
|
||||
<input class="form-input" value="{{form.company}}" data-field="company" bindinput="onInput" />
|
||||
</view>
|
||||
<view class="form-field">
|
||||
<view class="form-label">职位</view>
|
||||
<input class="form-input" value="{{form.position}}" data-field="position" bindinput="onInput" />
|
||||
</view>
|
||||
<view class="form-field">
|
||||
<view class="form-label">微信号</view>
|
||||
<input class="form-input" value="{{form.wechat_id}}" data-field="wechat_id" bindinput="onInput" />
|
||||
</view>
|
||||
<view class="form-field">
|
||||
<view class="form-label">简介</view>
|
||||
<textarea class="form-textarea profile-bio" value="{{form.bio}}" data-field="bio" bindinput="onInput" placeholder="简单介绍一下自己" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-submit-bar">
|
||||
<button class="button" loading="{{loading}}" bindtap="save">保存资料</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@ -1,12 +1,7 @@
|
||||
input {
|
||||
margin-top: 12rpx;
|
||||
height: 72rpx;
|
||||
font-size: 30rpx;
|
||||
.section {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
min-height: 180rpx;
|
||||
margin-top: 12rpx;
|
||||
font-size: 28rpx;
|
||||
.profile-bio {
|
||||
min-height: 210rpx;
|
||||
}
|
||||
|
||||
@ -1,6 +1,19 @@
|
||||
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);
|
||||
}
|
||||
|
||||
function scheduleTypeText(type) {
|
||||
return {
|
||||
course: "课程",
|
||||
deadline: "截止日",
|
||||
activity: "活动"
|
||||
}[type] || type || "排期";
|
||||
}
|
||||
|
||||
Page({
|
||||
data: { item: null, loading: false },
|
||||
|
||||
@ -14,7 +27,15 @@ Page({
|
||||
this.setData({ loading: true });
|
||||
try {
|
||||
const item = await get(`/api/schedule/${id}`);
|
||||
this.setData({ item });
|
||||
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" ? "截止时间" : "开始时间"
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
showError(error, "加载排期失败");
|
||||
} finally {
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
<view class="row-mark">类</view>
|
||||
<view class="row-body">
|
||||
<view class="card-title">类型</view>
|
||||
<view class="muted">{{item.type}}</view>
|
||||
<view class="muted">{{item.type_text}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@ -19,8 +19,8 @@
|
||||
<view class="list-row">
|
||||
<view class="row-mark">始</view>
|
||||
<view class="row-body">
|
||||
<view class="card-title">开始时间</view>
|
||||
<view class="muted">{{item.start_time}}</view>
|
||||
<view class="card-title">{{item.start_label}}</view>
|
||||
<view class="muted">{{item.start_time_text}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@ -29,7 +29,7 @@
|
||||
<view class="row-mark">止</view>
|
||||
<view class="row-body">
|
||||
<view class="card-title">结束时间</view>
|
||||
<view class="muted">{{item.end_time}}</view>
|
||||
<view class="muted">{{item.end_time_text}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@ -61,7 +61,7 @@ Page({
|
||||
wx.showToast({ title: "已发布", icon: "success" });
|
||||
const pages = getCurrentPages();
|
||||
const previousPage = pages[pages.length - 2];
|
||||
if (previousPage?.setData) {
|
||||
if (previousPage && previousPage.setData) {
|
||||
previousPage.setData({ needsRefresh: true });
|
||||
}
|
||||
setTimeout(() => wx.navigateBack(), 500);
|
||||
|
||||
@ -1,20 +1,24 @@
|
||||
<view class="page">
|
||||
<view class="hero">
|
||||
<view class="eyebrow">CLASS FEED</view>
|
||||
<view class="hero-title">发布班级动态</view>
|
||||
<view class="hero-subtitle">分享课程现场、活动照片、学习收获或班级提醒。</view>
|
||||
<view class="page form-page">
|
||||
<view class="form-header">
|
||||
<view class="form-kicker">CLASS FEED</view>
|
||||
<view class="form-title">发布班级动态</view>
|
||||
<view class="form-subtitle">分享课程现场、活动照片、学习收获或班级提醒。</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="card">
|
||||
<view class="muted">标题</view>
|
||||
<input value="{{title}}" bindinput="onTitleInput" placeholder="一句话概括这条动态" />
|
||||
<view class="section form-section">
|
||||
<view class="form-card">
|
||||
<view class="form-field">
|
||||
<view class="form-label">标题</view>
|
||||
<input class="form-input" value="{{title}}" bindinput="onTitleInput" placeholder="一句话概括这条动态" />
|
||||
</view>
|
||||
<view class="card">
|
||||
<view class="muted">正文</view>
|
||||
<textarea value="{{content}}" bindinput="onContentInput" placeholder="写下想和同学分享的内容" />
|
||||
</view>
|
||||
<view class="card">
|
||||
<view class="form-card">
|
||||
<view class="form-field">
|
||||
<view class="form-label">正文</view>
|
||||
<textarea class="form-textarea timeline-content" value="{{content}}" bindinput="onContentInput" placeholder="写下想和同学分享的内容" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-card">
|
||||
<view class="section-head">
|
||||
<view class="card-title">图片</view>
|
||||
<view class="section-action" bindtap="chooseImages">添加</view>
|
||||
@ -29,5 +33,7 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-submit-bar">
|
||||
<button class="button" loading="{{loading}}" bindtap="submit">发布动态</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@ -1,14 +1,9 @@
|
||||
input {
|
||||
margin-top: 12rpx;
|
||||
height: 72rpx;
|
||||
font-size: 30rpx;
|
||||
.form-section {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
min-height: 220rpx;
|
||||
margin-top: 12rpx;
|
||||
font-size: 28rpx;
|
||||
.timeline-content {
|
||||
min-height: 250rpx;
|
||||
}
|
||||
|
||||
.image-grid {
|
||||
|
||||
@ -33,9 +33,10 @@ Page({
|
||||
get(`/api/timeline/${this.data.id}`),
|
||||
get(`/api/timeline/${this.data.id}/comments`, { page_size: 50 })
|
||||
]);
|
||||
const currentUser = getApp().globalData.user || {};
|
||||
this.setData({
|
||||
post: postDetail,
|
||||
canDelete: postDetail.author_id === getApp().globalData.user?.id,
|
||||
canDelete: postDetail.author_id === currentUser.id,
|
||||
comments: (commentsRes.items || []).map((item) => ({
|
||||
...item,
|
||||
initial: String(item.author_name || "评").slice(0, 1)
|
||||
@ -67,7 +68,7 @@ Page({
|
||||
|
||||
previewImage(event) {
|
||||
const current = event.currentTarget.dataset.src;
|
||||
const urls = this.data.post?.image_urls || [];
|
||||
const urls = this.data.post && this.data.post.image_urls ? this.data.post.image_urls : [];
|
||||
if (!current || !urls.length) return;
|
||||
wx.previewImage({ current, urls });
|
||||
},
|
||||
@ -89,7 +90,7 @@ Page({
|
||||
wx.showToast({ title: "已删除", icon: "success" });
|
||||
const pages = getCurrentPages();
|
||||
const previousPage = pages[pages.length - 2];
|
||||
if (previousPage?.setData) previousPage.setData({ needsRefresh: true });
|
||||
if (previousPage && previousPage.setData) previousPage.setData({ needsRefresh: true });
|
||||
setTimeout(() => wx.navigateBack(), 500);
|
||||
} catch (error) {
|
||||
showError(error, "删除失败");
|
||||
|
||||
107
miniprogram/pages/vote-detail/index.js
Normal file
107
miniprogram/pages/vote-detail/index.js
Normal file
@ -0,0 +1,107 @@
|
||||
const { get, post } = require("../../utils/api");
|
||||
const { showError } = require("../../utils/page-helpers");
|
||||
|
||||
function formatDateTime(value) {
|
||||
if (!value) return "";
|
||||
return String(value).replace("T", " ").slice(0, 16);
|
||||
}
|
||||
|
||||
function normalizeVote(item, selectedIds) {
|
||||
const optionIds = selectedIds || item.my_option_ids || [];
|
||||
const totalVotes = (item.options || []).reduce((sum, option) => sum + Number(option.vote_count || 0), 0);
|
||||
return {
|
||||
...item,
|
||||
status_text: item.status === "open" ? "进行中" : "已关闭",
|
||||
vote_type_text: item.vote_type === "multiple" ? `多选,最多 ${item.max_choices || 1} 项` : "单选",
|
||||
deadline_text: item.deadline ? formatDateTime(item.deadline) : "",
|
||||
can_submit: item.status === "open" && !item.has_voted,
|
||||
voted_action_text: item.has_voted ? "已参与" : "请选择",
|
||||
submit_text: item.has_voted ? "你已参与" : "投票已关闭",
|
||||
selectedIds: optionIds,
|
||||
options: (item.options || []).map((option) => {
|
||||
const voteCount = Number(option.vote_count || 0);
|
||||
const percent = totalVotes > 0 ? Math.round((voteCount / totalVotes) * 100) : 0;
|
||||
return {
|
||||
...option,
|
||||
checked: optionIds.includes(option.id),
|
||||
selected_class: optionIds.includes(option.id) ? "selected" : "",
|
||||
check_text: optionIds.includes(option.id) ? "✓" : "",
|
||||
percent,
|
||||
percent_style: `width: ${percent}%`,
|
||||
voter_names_text: Array.isArray(option.voter_names) ? option.voter_names.join("、") : ""
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
id: null,
|
||||
item: null,
|
||||
loading: false,
|
||||
submitting: 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/votes/${id}`);
|
||||
this.setData({ item: normalizeVote(item) });
|
||||
} catch (error) {
|
||||
showError(error, "加载投票失败");
|
||||
} finally {
|
||||
this.setData({ loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
toggleOption(event) {
|
||||
const optionId = Number(event.currentTarget.dataset.id);
|
||||
const item = this.data.item;
|
||||
if (!item || item.status !== "open" || item.has_voted) return;
|
||||
let selectedIds = item.selectedIds || [];
|
||||
if (item.vote_type === "single") {
|
||||
selectedIds = [optionId];
|
||||
} else if (selectedIds.includes(optionId)) {
|
||||
selectedIds = selectedIds.filter((id) => id !== optionId);
|
||||
} else {
|
||||
if (selectedIds.length >= Number(item.max_choices || 1)) {
|
||||
wx.showToast({ title: `最多选择 ${item.max_choices} 项`, icon: "none" });
|
||||
return;
|
||||
}
|
||||
selectedIds = [...selectedIds, optionId];
|
||||
}
|
||||
this.setData({ item: normalizeVote(item, selectedIds) });
|
||||
},
|
||||
|
||||
async submit() {
|
||||
const item = this.data.item;
|
||||
if (!item || item.has_voted || item.status !== "open") return;
|
||||
const selectedIds = item.selectedIds || [];
|
||||
if (!selectedIds.length) {
|
||||
wx.showToast({ title: "请选择投票选项", icon: "none" });
|
||||
return;
|
||||
}
|
||||
this.setData({ submitting: true });
|
||||
try {
|
||||
await post(`/api/votes/${item.id}/submit`, { option_ids: selectedIds });
|
||||
wx.showToast({ title: "已投票", icon: "success" });
|
||||
await this.load(item.id);
|
||||
} catch (error) {
|
||||
showError(error, "投票失败");
|
||||
} finally {
|
||||
this.setData({ submitting: false });
|
||||
}
|
||||
}
|
||||
});
|
||||
4
miniprogram/pages/vote-detail/index.json
Normal file
4
miniprogram/pages/vote-detail/index.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "投票详情",
|
||||
"enablePullDownRefresh": true
|
||||
}
|
||||
56
miniprogram/pages/vote-detail/index.wxml
Normal file
56
miniprogram/pages/vote-detail/index.wxml
Normal file
@ -0,0 +1,56 @@
|
||||
<view class="page" wx:if="{{item}}">
|
||||
<view class="hero">
|
||||
<view class="eyebrow">VOTE</view>
|
||||
<view class="hero-title">{{item.title}}</view>
|
||||
<view class="hero-subtitle">{{item.description || "请选择你的意见。"}}</view>
|
||||
<view class="hero-metrics">
|
||||
<view class="metric">
|
||||
<view class="metric-number">{{item.total_voters}}</view>
|
||||
<view class="metric-label">参与人数</view>
|
||||
</view>
|
||||
<view class="metric">
|
||||
<view class="metric-number">{{item.options.length}}</view>
|
||||
<view class="metric-label">选项</view>
|
||||
</view>
|
||||
<view class="metric">
|
||||
<view class="metric-number">{{item.status_text}}</view>
|
||||
<view class="metric-label">{{item.vote_type_text}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-head">
|
||||
<view class="section-title">选项</view>
|
||||
<view class="section-action">{{item.voted_action_text}}</view>
|
||||
</view>
|
||||
<view wx:for="{{item.options}}" wx:key="id" class="vote-option {{item.selected_class}}" data-id="{{item.id}}" bindtap="toggleOption">
|
||||
<view class="vote-option-main">
|
||||
<view class="vote-check">{{item.check_text}}</view>
|
||||
<view class="row-body">
|
||||
<view class="card-title">{{item.content}}</view>
|
||||
<view class="muted">{{item.vote_count}} 票 · {{item.percent}}%</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="vote-progress">
|
||||
<view class="vote-progress-fill" style="{{item.percent_style}}"></view>
|
||||
</view>
|
||||
<view wx:if="{{item.voter_names_text}}" class="vote-voters">{{item.voter_names_text}}</view>
|
||||
</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>
|
||||
|
||||
<view class="form-submit-bar">
|
||||
<button wx:if="{{item.can_submit}}" class="button" loading="{{submitting}}" bindtap="submit">提交投票</button>
|
||||
<button wx:else class="button secondary" disabled>{{item.submit_text}}</button>
|
||||
</view>
|
||||
</view>
|
||||
57
miniprogram/pages/vote-detail/index.wxss
Normal file
57
miniprogram/pages/vote-detail/index.wxss
Normal file
@ -0,0 +1,57 @@
|
||||
.page {
|
||||
padding-bottom: calc(140rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.vote-option {
|
||||
margin-bottom: 18rpx;
|
||||
border: 1rpx solid rgba(121, 84, 54, 0.12);
|
||||
border-radius: 28rpx;
|
||||
background: rgba(255, 252, 247, 0.96);
|
||||
padding: 24rpx;
|
||||
box-shadow: 0 14rpx 36rpx rgba(68, 39, 27, 0.06);
|
||||
}
|
||||
|
||||
.vote-option.selected {
|
||||
border-color: rgba(107, 31, 43, 0.42);
|
||||
background: #fff8ed;
|
||||
}
|
||||
|
||||
.vote-option-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18rpx;
|
||||
}
|
||||
|
||||
.vote-check {
|
||||
flex: none;
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border: 2rpx solid rgba(107, 31, 43, 0.28);
|
||||
border-radius: 999rpx;
|
||||
color: #6b1f2b;
|
||||
font-size: 28rpx;
|
||||
font-weight: 800;
|
||||
line-height: 46rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.vote-progress {
|
||||
overflow: hidden;
|
||||
height: 10rpx;
|
||||
margin-top: 18rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #f0dfcd;
|
||||
}
|
||||
|
||||
.vote-progress-fill {
|
||||
height: 100%;
|
||||
border-radius: 999rpx;
|
||||
background: #6b1f2b;
|
||||
}
|
||||
|
||||
.vote-voters {
|
||||
margin-top: 14rpx;
|
||||
color: #8a7b70;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.45;
|
||||
}
|
||||
@ -28,7 +28,8 @@ function request(path, options = {}) {
|
||||
return;
|
||||
}
|
||||
if (res.statusCode >= 400) {
|
||||
const message = res.data?.detail || res.data?.message || "操作失败";
|
||||
const data = res.data || {};
|
||||
const message = data.detail || data.message || "操作失败";
|
||||
reject(new Error(message));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ function saveSession(token, user) {
|
||||
wx.setStorageSync("auth_token", token);
|
||||
wx.setStorageSync("auth_user", user);
|
||||
const app = getApp();
|
||||
if (app?.setUser) app.setUser(user);
|
||||
if (app && app.setUser) app.setUser(user);
|
||||
}
|
||||
|
||||
function requireLogin() {
|
||||
@ -23,7 +23,7 @@ async function refreshMe() {
|
||||
const user = await get("/api/auth/me");
|
||||
wx.setStorageSync("auth_user", user);
|
||||
const app = getApp();
|
||||
if (app?.setUser) app.setUser(user);
|
||||
if (app && app.setUser) app.setUser(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
module.exports = {
|
||||
// 发布前改为后端 HTTPS 域名,例如 https://classhub.example.com
|
||||
apiBase: "http://127.0.0.1:8000"
|
||||
apiBase: "https://hkuicb.info"
|
||||
};
|
||||
|
||||
@ -2,11 +2,9 @@ const MODULES = {
|
||||
announcements: { key: "announcements", title: "公告", desc: "查看班级重要通知", group: "class", icon: "告" },
|
||||
schedule: { key: "schedule", title: "排期", desc: "课程、活动与截止日", group: "class", icon: "日" },
|
||||
directory: { key: "directory", title: "成员名录", desc: "查找同学与班委", group: "class", icon: "友" },
|
||||
resources: { key: "resources", 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: "选" },
|
||||
reading_corner: { key: "reading_corner", title: "读书角", desc: "记录阅读与笔记", group: "interact", icon: "书" }
|
||||
votes: { key: "votes", title: "投票", desc: "参与班级决策", group: "interact", icon: "选" }
|
||||
};
|
||||
|
||||
const MINI_PROGRAM_MODULE_KEYS = Object.keys(MODULES);
|
||||
|
||||
@ -3,13 +3,16 @@ const { getModule, isModuleEnabled } = require("./modules");
|
||||
function getActiveClassId() {
|
||||
const app = getApp();
|
||||
const user = app.globalData.user || wx.getStorageSync("auth_user");
|
||||
return app.globalData.activeClassId || user?.active_membership?.class_id || user?.memberships?.[0]?.class_id || null;
|
||||
const memberships = user && Array.isArray(user.memberships) ? user.memberships : [];
|
||||
const activeMembership = user && user.active_membership ? user.active_membership : null;
|
||||
const firstMembership = memberships.length ? memberships[0] : null;
|
||||
return app.globalData.activeClassId || (activeMembership && activeMembership.class_id) || (firstMembership && firstMembership.class_id) || null;
|
||||
}
|
||||
|
||||
function getEnabledModules() {
|
||||
const app = getApp();
|
||||
const user = app.globalData.user || wx.getStorageSync("auth_user");
|
||||
return app.globalData.enabledModules || user?.enabled_modules || null;
|
||||
return app.globalData.enabledModules || (user && user.enabled_modules) || null;
|
||||
}
|
||||
|
||||
function getActiveClassName() {
|
||||
@ -17,24 +20,27 @@ function getActiveClassName() {
|
||||
const user = app.globalData.user || wx.getStorageSync("auth_user");
|
||||
const classId = getActiveClassId();
|
||||
const savedClass = wx.getStorageSync("active_class") || null;
|
||||
if (savedClass?.id === classId && savedClass?.name) return savedClass.name;
|
||||
const membership = user?.memberships?.find((item) => item.class_id === classId);
|
||||
return membership?.class_name || user?.active_membership?.class_name || "HKU ICB";
|
||||
if (savedClass && savedClass.id === classId && savedClass.name) return savedClass.name;
|
||||
const memberships = user && Array.isArray(user.memberships) ? user.memberships : [];
|
||||
const membership = memberships.find((item) => item.class_id === classId);
|
||||
const activeMembership = user && user.active_membership ? user.active_membership : null;
|
||||
return (membership && membership.class_name) || (activeMembership && activeMembership.class_name) || "HKU ICB";
|
||||
}
|
||||
|
||||
function ensureModuleOpen(moduleKey) {
|
||||
const enabledModules = getEnabledModules();
|
||||
if (isModuleEnabled(moduleKey, enabledModules)) return true;
|
||||
const module = getModule(moduleKey);
|
||||
const title = module ? module.title : "功能";
|
||||
wx.redirectTo({
|
||||
url: `/pages/module-unavailable/index?title=${encodeURIComponent(module?.title || "功能")}`
|
||||
url: `/pages/module-unavailable/index?title=${encodeURIComponent(title)}`
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
function showError(error, fallback = "加载失败") {
|
||||
wx.showToast({
|
||||
title: error?.message || fallback,
|
||||
title: (error && error.message) || fallback,
|
||||
icon: "none"
|
||||
});
|
||||
}
|
||||
|
||||
@ -23,14 +23,19 @@ const TEACHER_DEFAULT_PERMISSIONS = new Set([
|
||||
]);
|
||||
|
||||
function activeMembership(user, classId) {
|
||||
return user?.memberships?.find((item) => item.class_id === classId) || user?.active_membership || null;
|
||||
if (!user) return null;
|
||||
const memberships = Array.isArray(user.memberships) ? user.memberships : [];
|
||||
return memberships.find((item) => item.class_id === classId) || user.active_membership || null;
|
||||
}
|
||||
|
||||
function hasManagePermission(user, classId, moduleKey) {
|
||||
const permission = MODULE_MANAGE_PERMISSIONS[moduleKey];
|
||||
if (!permission || !user) return false;
|
||||
if (user.role === "super_admin") return true;
|
||||
const membershipPermissions = activeMembership(user, classId)?.class_permissions || [];
|
||||
const membership = activeMembership(user, classId);
|
||||
const membershipPermissions = membership && Array.isArray(membership.class_permissions)
|
||||
? membership.class_permissions
|
||||
: [];
|
||||
if (user.role === "teacher") {
|
||||
return TEACHER_DEFAULT_PERMISSIONS.has(permission) || membershipPermissions.includes(permission);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user