From 5659a156365e0fb8c27423c3b4b3465e8fbc18f3 Mon Sep 17 00:00:00 2001 From: aaron <> Date: Fri, 15 May 2026 11:16:52 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E7=8F=AD=E8=B4=B9=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E4=B8=8A=E4=BC=A0=20=E5=92=8C=20=E8=AF=A6=E6=83=85?= =?UTF-8?q?=E9=A1=B5=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../20260515_add_fund_record_images.py | 35 ++++ backend/app/api/directory.py | 21 ++- backend/app/api/fund.py | 15 ++ backend/app/db/models.py | 12 ++ backend/app/schemas/fund.py | 5 +- backend/app/schemas/user.py | 1 + backend/app/services/directory_service.py | 16 ++ backend/app/services/fund_service.py | 11 +- .../src/app/(app)/directory/[id]/page.tsx | 7 +- frontend/src/app/(app)/directory/page.tsx | 15 +- frontend/src/app/(app)/fund/page.tsx | 170 ++++++++++++++---- frontend/src/lib/types.ts | 2 + miniprogram/app.json | 1 + miniprogram/app.wxss | 38 ++++ miniprogram/pages/fund-detail/index.js | 66 +++++++ miniprogram/pages/fund-detail/index.json | 3 + miniprogram/pages/fund-detail/index.wxml | 73 ++++++++ miniprogram/pages/fund-detail/index.wxss | 60 +++++++ miniprogram/pages/home/index.js | 58 ++++++ miniprogram/pages/home/index.wxml | 83 ++------- miniprogram/pages/home/index.wxss | 32 ++++ miniprogram/pages/manage/index.js | 45 ++++- miniprogram/pages/manage/index.wxml | 13 ++ miniprogram/pages/manage/index.wxss | 43 +++++ miniprogram/pages/member-detail/index.js | 11 +- miniprogram/pages/member-detail/index.wxml | 4 +- miniprogram/pages/module/index.js | 26 ++- miniprogram/pages/module/index.wxml | 13 +- 28 files changed, 762 insertions(+), 117 deletions(-) create mode 100644 backend/alembic/versions/20260515_add_fund_record_images.py create mode 100644 miniprogram/pages/fund-detail/index.js create mode 100644 miniprogram/pages/fund-detail/index.json create mode 100644 miniprogram/pages/fund-detail/index.wxml create mode 100644 miniprogram/pages/fund-detail/index.wxss diff --git a/backend/alembic/versions/20260515_add_fund_record_images.py b/backend/alembic/versions/20260515_add_fund_record_images.py new file mode 100644 index 0000000..e226ccb --- /dev/null +++ b/backend/alembic/versions/20260515_add_fund_record_images.py @@ -0,0 +1,35 @@ +"""add fund record images + +Revision ID: 20260515_add_fund_record_images +Revises: 20260507_add_wechat_identity +Create Date: 2026-05-15 09:00:00 +""" + +from __future__ import annotations + +from alembic import op +import sqlalchemy as sa + + +revision = "20260515_add_fund_record_images" +down_revision = "20260507_add_wechat_identity" +branch_labels = None +depends_on = None + + +def _has_column(inspector: sa.engine.reflection.Inspector, table_name: str, column_name: str) -> bool: + return any(column["name"] == column_name for column in inspector.get_columns(table_name)) + + +def upgrade() -> None: + bind = op.get_bind() + inspector = sa.inspect(bind) + if "fund_records" in inspector.get_table_names() and not _has_column(inspector, "fund_records", "image_urls"): + op.add_column("fund_records", sa.Column("image_urls", sa.Text(), nullable=True)) + + +def downgrade() -> None: + bind = op.get_bind() + inspector = sa.inspect(bind) + if "fund_records" in inspector.get_table_names() and _has_column(inspector, "fund_records", "image_urls"): + op.drop_column("fund_records", "image_urls") diff --git a/backend/app/api/directory.py b/backend/app/api/directory.py index e4b8002..21ed5b6 100644 --- a/backend/app/api/directory.py +++ b/backend/app/api/directory.py @@ -6,12 +6,31 @@ from app.db.database import get_db from app.db.models import User from app.schemas.user import UserPublic from app.schemas.common import PageResponse -from app.services.directory_service import search_directory, user_to_public +from app.services.directory_service import get_directory_role_counts, search_directory, user_to_public from app.services.user_service import get_user_by_id router = APIRouter(prefix="/api/directory", tags=["directory"]) +@router.get("/stats") +async def get_directory_stats( + class_id: int | None = None, + user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + effective_class_id = resolve_class_id_for_user(user, class_id) + if effective_class_id is None: + return {"student_count": 0, "teacher_count": 0, "total": 0} + ensure_class_access(user, effective_class_id) + await ensure_class_module_enabled(db, effective_class_id, "directory") + counts = await get_directory_role_counts(db, effective_class_id) + return { + "student_count": counts["student"], + "teacher_count": counts["teacher"], + "total": counts["total"], + } + + @router.get("/", response_model=PageResponse[UserPublic]) async def search_members( search: str | None = None, diff --git a/backend/app/api/fund.py b/backend/app/api/fund.py index b9827da..0cff749 100644 --- a/backend/app/api/fund.py +++ b/backend/app/api/fund.py @@ -22,6 +22,7 @@ def record_to_out(record: FundRecord) -> FundRecordOut: amount=record.amount, category=record.category, description=record.description, + image_urls=record.get_image_urls_list(), record_date=record.record_date, recorder_id=record.recorder_id, recorder_name=record.recorder.name if record.recorder else "Unknown", @@ -69,6 +70,20 @@ async def get_fund_records( return PageResponse(items=items, total=total, page=page, page_size=page_size, total_pages=total_pages) +@router.get("/{record_id}", response_model=FundRecordOut) +async def get_fund_record_detail( + record_id: int, + user: User = Depends(require_role("super_admin", "teacher", "student")), + db: AsyncSession = Depends(get_db), +): + record = await get_fund_record_by_id(db, record_id) + if record is None: + raise HTTPException(status_code=404, detail="Record not found") + ensure_class_access(user, record.class_id) + await ensure_class_module_enabled(db, record.class_id, "fund") + return record_to_out(record) + + @router.post("/", response_model=FundRecordOut) async def create_new_record( data: FundRecordCreate, diff --git a/backend/app/db/models.py b/backend/app/db/models.py index 2872d58..adf3b56 100644 --- a/backend/app/db/models.py +++ b/backend/app/db/models.py @@ -562,6 +562,7 @@ class FundRecord(Base): amount: Mapped[float] = mapped_column(Float, nullable=False) category: Mapped[str] = mapped_column(String(100), nullable=False) description: Mapped[str | None] = mapped_column(Text, nullable=True) + image_urls: Mapped[str | None] = mapped_column(Text, nullable=True) # JSON array record_date: Mapped[datetime] = mapped_column(Date, nullable=False) recorder_id: Mapped[int] = mapped_column( Integer, ForeignKey("users.id"), nullable=False @@ -573,3 +574,14 @@ class FundRecord(Base): class_: Mapped["Class_"] = relationship("Class_", back_populates="fund_records") recorder: Mapped["User"] = relationship("User") + + def get_image_urls_list(self) -> list[str]: + if not self.image_urls: + return [] + try: + return json.loads(self.image_urls) + except (json.JSONDecodeError, TypeError): + return [] + + def set_image_urls_list(self, urls: list[str]): + self.image_urls = json.dumps(urls) if urls else None diff --git a/backend/app/schemas/fund.py b/backend/app/schemas/fund.py index 6263f0b..24d5269 100644 --- a/backend/app/schemas/fund.py +++ b/backend/app/schemas/fund.py @@ -8,6 +8,7 @@ class FundRecordCreate(BaseModel): amount: float category: str description: str | None = None + image_urls: list[str] | None = None record_date: date @@ -16,6 +17,7 @@ class FundRecordUpdate(BaseModel): amount: float | None = None category: str | None = None description: str | None = None + image_urls: list[str] | None = None record_date: date | None = None @@ -26,6 +28,7 @@ class FundRecordOut(BaseModel): amount: float category: str description: str | None + image_urls: list[str] | None record_date: date recorder_id: int recorder_name: str @@ -43,4 +46,4 @@ class FundStatistics(BaseModel): total_expense: float balance: float income_by_category: list[CategoryAmount] - expense_by_category: list[CategoryAmount] \ No newline at end of file + expense_by_category: list[CategoryAmount] diff --git a/backend/app/schemas/user.py b/backend/app/schemas/user.py index 2639eb2..ab7a343 100644 --- a/backend/app/schemas/user.py +++ b/backend/app/schemas/user.py @@ -49,6 +49,7 @@ class UserPublic(BaseModel): id: int name: str student_id: str | None + membership_role: str | None = None industry: str | None company: str | None position: str | None diff --git a/backend/app/services/directory_service.py b/backend/app/services/directory_service.py index b805b62..4f2b9a1 100644 --- a/backend/app/services/directory_service.py +++ b/backend/app/services/directory_service.py @@ -78,6 +78,21 @@ async def search_directory( return users, total +async def get_directory_role_counts(db: AsyncSession, class_id: int) -> dict[str, int]: + result = await db.execute( + select(ClassMembership.membership_role, func.count(ClassMembership.id)) + .join(User) + .where(ClassMembership.class_id == class_id, User.status == "approved") + .group_by(ClassMembership.membership_role) + ) + counts = {"student": 0, "teacher": 0} + for role, count in result.all(): + if role in counts: + counts[role] = count + counts["total"] = counts["student"] + counts["teacher"] + return counts + + def user_to_public( user: User, class_id: int | None = None, include_contact: bool = True ) -> UserPublic: @@ -87,6 +102,7 @@ def user_to_public( id=user.id, name=user.name, student_id=user.student_id, + membership_role=membership.membership_role if membership else None, industry=user.industry, company=user.company, position=user.position, diff --git a/backend/app/services/fund_service.py b/backend/app/services/fund_service.py index d8bfb61..808ee26 100644 --- a/backend/app/services/fund_service.py +++ b/backend/app/services/fund_service.py @@ -16,8 +16,10 @@ async def create_fund_record( amount=data.amount, category=data.category, description=data.description, + image_urls=None, record_date=data.record_date, ) + record.set_image_urls_list(data.image_urls or []) db.add(record) await db.commit() await db.refresh(record) @@ -27,8 +29,13 @@ async def create_fund_record( async def update_fund_record( db: AsyncSession, record: FundRecord, data: FundRecordUpdate ) -> FundRecord: - for field, value in data.model_dump(exclude_unset=True).items(): + values = data.model_dump(exclude_unset=True) + has_image_urls = "image_urls" in values + image_urls = values.pop("image_urls", None) + for field, value in values.items(): setattr(record, field, value) + if has_image_urls: + record.set_image_urls_list(image_urls or []) await db.commit() await db.refresh(record) return record @@ -124,4 +131,4 @@ async def get_fund_statistics(db: AsyncSession, class_id: int) -> FundStatistics balance=balance, income_by_category=income_by_category, expense_by_category=expense_by_category, - ) \ No newline at end of file + ) diff --git a/frontend/src/app/(app)/directory/[id]/page.tsx b/frontend/src/app/(app)/directory/[id]/page.tsx index 7fe5049..b4d705c 100644 --- a/frontend/src/app/(app)/directory/[id]/page.tsx +++ b/frontend/src/app/(app)/directory/[id]/page.tsx @@ -68,12 +68,17 @@ export default function MemberDetailPage() {

{member.name}

+ {member.membership_role === "teacher" && ( + + 老师 + + )} {member.committee_role && ( {member.committee_role} )} - {member.student_id && ( + {member.membership_role !== "teacher" && member.student_id && (

学号: {member.student_id}

)} {member.company && ( diff --git a/frontend/src/app/(app)/directory/page.tsx b/frontend/src/app/(app)/directory/page.tsx index f6ed95a..2f56c4a 100644 --- a/frontend/src/app/(app)/directory/page.tsx +++ b/frontend/src/app/(app)/directory/page.tsx @@ -24,6 +24,7 @@ export default function DirectoryPage() { const { activeClassId } = useActiveClass(); const [members, setMembers] = useState([]); const [total, setTotal] = useState(0); + const [roleCounts, setRoleCounts] = useState({ student_count: 0, teacher_count: 0, total: 0 }); const [search, setSearch] = useState(""); const [industry, setIndustry] = useState(""); const [company, setCompany] = useState(""); @@ -36,6 +37,7 @@ export default function DirectoryPage() { if (!activeClassId) { setMembers([]); setTotal(0); + setRoleCounts({ student_count: 0, teacher_count: 0, total: 0 }); setLoading(false); return; } @@ -47,8 +49,12 @@ export default function DirectoryPage() { if (industry) params.industry = industry; if (company) params.company = company; const res = await fetchAPI>("/api/directory/", params); + const stats = await fetchAPI<{ student_count: number; teacher_count: number; total: number }>("/api/directory/stats", { + class_id: String(activeClassId), + }); setMembers(res.items ?? []); setTotal(res.total ?? 0); + setRoleCounts(stats); setTotalPages(res.total_pages ?? 1); } catch (err: unknown) { setError(getErrorMessage(err, "加载失败")); @@ -72,7 +78,9 @@ export default function DirectoryPage() {
Directory

成员名录

-

共 {total} 位成员,按行业、公司与研究兴趣建立连接

+

+ 同学 {roleCounts.student_count} 人,老师 {roleCounts.teacher_count} 人;当前筛选 {total} 条结果 +

{/* Search & Filters */} @@ -136,6 +144,11 @@ export default function DirectoryPage() {

{member.name}

+ {member.membership_role === "teacher" && ( + + 老师 + + )} {member.committee_role && ( {member.committee_role} diff --git a/frontend/src/app/(app)/fund/page.tsx b/frontend/src/app/(app)/fund/page.tsx index 1caae51..f8b1d9d 100644 --- a/frontend/src/app/(app)/fund/page.tsx +++ b/frontend/src/app/(app)/fund/page.tsx @@ -3,7 +3,7 @@ import { useCallback, useEffect, useState } from "react"; import { useActiveClass } from "@/hooks/use-active-class"; import { useAuth } from "@/hooks/use-auth"; -import { fetchAPI, postAPI, putAPI, deleteAPI, getErrorMessage } from "@/lib/api"; +import { fetchAPI, postAPI, putAPI, deleteAPI, uploadAPI, getErrorMessage } from "@/lib/api"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; @@ -28,6 +28,8 @@ import { ConfirmDialog } from "@/components/confirm-dialog"; import { ErrorState } from "@/components/error-state"; import { Pagination } from "@/components/pagination"; import { toast } from "sonner"; +import { ImagePlus, X } from "lucide-react"; +import Image from "next/image"; import type { FundRecord, FundStatistics, PageResponse } from "@/lib/types"; import { FUND_TYPES, FUND_INCOME_CATEGORIES, FUND_EXPENSE_CATEGORIES } from "@/lib/constants"; import { hasClassPermission } from "@/lib/permissions"; @@ -57,7 +59,9 @@ export default function FundPage() { const [formCategory, setFormCategory] = useState(""); const [formDescription, setFormDescription] = useState(""); const [formDate, setFormDate] = useState(""); + const [formImageUrls, setFormImageUrls] = useState([]); const [submitting, setSubmitting] = useState(false); + const [uploadingImages, setUploadingImages] = useState(false); // Delete state const [deleteTarget, setDeleteTarget] = useState(null); @@ -124,6 +128,7 @@ export default function FundPage() { setFormCategory(""); setFormDescription(""); setFormDate(new Date().toISOString().slice(0, 10)); + setFormImageUrls([]); setEditingId(null); }; @@ -139,9 +144,32 @@ export default function FundPage() { setFormCategory(record.category); setFormDescription(record.description || ""); setFormDate(record.record_date); + setFormImageUrls(record.image_urls || []); setDialogOpen(true); }; + const uploadFundImages = async (files: FileList | File[]) => { + const next = [...formImageUrls]; + const remainingSlots = Math.max(0, 6 - next.length); + const selectedFiles = Array.from(files).slice(0, remainingSlots); + if (selectedFiles.length === 0) { + toast.error("最多上传 6 张小票图片"); + return; + } + setUploadingImages(true); + try { + for (const file of selectedFiles) { + const formData = new FormData(); + formData.append("file", file); + const res = await uploadAPI<{ url: string }>("/api/upload/image", formData); + next.push(res.url); + } + setFormImageUrls(next.slice(0, 6)); + } finally { + setUploadingImages(false); + } + }; + const handleSubmit = async () => { if (!activeClassId) return; const amount = parseFloat(formAmount); @@ -165,6 +193,7 @@ export default function FundPage() { amount, category: formCategory.trim(), description: formDescription.trim() || null, + image_urls: formImageUrls, record_date: formDate, }; @@ -186,6 +215,31 @@ export default function FundPage() { } }; + const handlePickImages = async () => { + if (formImageUrls.length >= 6) { + toast.error("最多上传 6 张小票图片"); + return; + } + const input = document.createElement("input"); + input.type = "file"; + input.accept = "image/*"; + input.multiple = true; + input.onchange = async () => { + const files = input.files; + if (!files?.length) return; + try { + await uploadFundImages(files); + } catch (err: unknown) { + toast.error(getErrorMessage(err, "上传图片失败")); + } + }; + input.click(); + }; + + const removeFundImage = (url: string) => { + setFormImageUrls((current) => current.filter((item) => item !== url)); + }; + const handleDelete = async () => { if (!deleteTarget) return; try { @@ -325,45 +379,61 @@ export default function FundPage() { {typeFilter === "all" ? "暂无班费记录" : `暂无${FUND_TYPES[typeFilter as keyof typeof FUND_TYPES]}记录`}
) : ( -
+
{records.map((r) => ( - -
- - {FUND_TYPES[r.type as keyof typeof FUND_TYPES]} - -
-

- - {r.type === "income" ? "+" : "-"}¥{r.amount.toFixed(2)} - - {r.category} -

-

- {r.record_date} · {r.recorder_name} - {r.description ? ` · ${r.description}` : ""} -

-
-
- {isAdmin && ( -
- - + {FUND_TYPES[r.type as keyof typeof FUND_TYPES]} + +
+

+ + {r.type === "income" ? "+" : "-"}¥{r.amount.toFixed(2)} + + {r.category} +

+

+ {r.record_date} · {r.recorder_name} +

+
+
+ {isAdmin && ( +
+ + +
+ )} +
+ {r.description &&

{r.description}

} + {r.image_urls && r.image_urls.length > 0 && ( +
+ {r.image_urls.map((url) => ( + + ))}
)} @@ -428,6 +498,32 @@ export default function FundPage() { rows={2} />
+
+
+ + +
+

可上传收据、小票或转账凭证,最多 6 张。

+ {formImageUrls.length > 0 && ( +
+ {formImageUrls.map((url) => ( +
+ 小票预览 + +
+ ))} +
+ )} +
+ + FUND LEDGER + {{record.signed_amount_text}} + {{record.category}} · {{record.record_date}} + {{record.image_count_text}} + + + + + 账目摘要 + {{record.type_text}} + + + + + + 分类 + {{record.category}} + + + + + + 发生日期 + {{record.record_date}} + + + + + + 录入人 + {{record.recorder_name}} + + + + + + 录入时间 + {{record.created_at_text}} + + + + + + + + 备注 + + + {{record.description || "暂无备注"}} + + + + + + 小票凭证 + {{record.image_count_text}} + + + + + + 这条记录没有上传小票图片 + + + + + + + 未找到班费记录 + + diff --git a/miniprogram/pages/fund-detail/index.wxss b/miniprogram/pages/fund-detail/index.wxss new file mode 100644 index 0000000..2cd623e --- /dev/null +++ b/miniprogram/pages/fund-detail/index.wxss @@ -0,0 +1,60 @@ +.fund-detail-hero.income { + background: linear-gradient(145deg, #1f7a4d 0%, #8b5a36 72%, #d6a653 135%); +} + +.fund-detail-hero.expense { + background: linear-gradient(145deg, #6b1f2b 0%, #9a3a2f 72%, #d6a653 135%); +} + +.fund-proof-pill { + 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; +} + +.fund-detail-row { + display: flex; + align-items: center; + gap: 22rpx; + padding: 22rpx 0; + border-bottom: 1rpx solid rgba(121, 84, 54, 0.1); +} + +.fund-detail-row:first-child { + padding-top: 0; +} + +.fund-detail-row.last { + padding-bottom: 0; + border-bottom: 0; +} + +.fund-note { + color: #4f3930; + font-size: 27rpx; + line-height: 1.65; + white-space: pre-wrap; +} + +.fund-proof-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 14rpx; +} + +.fund-proof-grid image { + width: 100%; + height: 300rpx; + border-radius: 24rpx; + background: #efe0ca; + box-shadow: 0 14rpx 32rpx rgba(68, 39, 27, 0.08); +} diff --git a/miniprogram/pages/home/index.js b/miniprogram/pages/home/index.js index 8654c58..ce2bdb4 100644 --- a/miniprogram/pages/home/index.js +++ b/miniprogram/pages/home/index.js @@ -30,11 +30,13 @@ function scheduleTypeText(type) { Page({ data: { className: "HKU ICB", + homeStatus: "班级信息已同步", announcements: [], schedules: [], votes: [], timelines: [], quickModules: [], + focusItems: [], unreadCount: 0, loading: false }, @@ -102,6 +104,48 @@ Page({ } if (name === "timelines") next.timelines = value.items || []; }); + const pendingVotes = next.votes.filter((item) => !item.has_voted); + const focusItems = []; + if (next.schedules.length) { + const schedule = next.schedules[0]; + focusItems.push({ + id: schedule.id, + type: "schedule", + mark: "日", + label: "下一项排期", + title: schedule.title, + detail: schedule.schedule_time_text, + badge: schedule.schedule_type_text + }); + } + if (pendingVotes.length) { + const vote = pendingVotes[0]; + focusItems.push({ + id: vote.id, + type: "vote", + mark: "选", + label: "待参与投票", + title: vote.title, + detail: `${vote.vote_type_text} · ${vote.total_voters} 人参与`, + badge: "去参与" + }); + } + if (next.announcements.length) { + const announcement = next.announcements[0]; + focusItems.push({ + id: announcement.id, + type: "announcements", + mark: "告", + label: "最新公告", + title: announcement.title, + detail: announcement.author_name || "班级公告", + badge: "查看" + }); + } + next.focusItems = focusItems.slice(0, 3); + next.homeStatus = next.unreadCount > 0 + ? `${next.unreadCount} 条未读通知` + : "暂无未读通知"; this.setData(next); } catch (error) { showError(error); @@ -115,6 +159,20 @@ Page({ wx.navigateTo({ url: `/pages/module/index?module=${key}` }); }, + openFocus(event) { + const type = event.currentTarget.dataset.type; + const id = event.currentTarget.dataset.id; + if (type === "schedule") { + wx.navigateTo({ url: `/pages/schedule-detail/index?id=${id}` }); + return; + } + if (type === "vote") { + wx.navigateTo({ url: `/pages/vote-detail/index?id=${id}` }); + return; + } + wx.navigateTo({ url: `/pages/module/index?module=${type}` }); + }, + openSchedule(event) { wx.navigateTo({ url: `/pages/schedule-detail/index?id=${event.currentTarget.dataset.id}` }); }, diff --git a/miniprogram/pages/home/index.wxml b/miniprogram/pages/home/index.wxml index fa38be6..f9a3b62 100644 --- a/miniprogram/pages/home/index.wxml +++ b/miniprogram/pages/home/index.wxml @@ -2,27 +2,28 @@ HKU ICB CLASSHUB {{className}} - 把公告、排期、投票和班级互动放在一个安静清晰的移动入口。 - - - {{unreadCount}} - 未读通知 - - - {{schedules.length}} - 近期安排 - - - {{votes.length}} - 班级投票 + 今天需要关注的班级信息,都在下面按优先级整理好了。 + {{homeStatus}} + + + + + 今日关注 + + + {{item.mark}} + + {{item.label}} + {{item.title}} + {{item.detail}} + {{item.badge}} - 常用入口 - 按班级开放 + 功能入口 @@ -33,56 +34,6 @@ - - - 最新公告 - 全部 - - - - - - {{item.title}} - {{item.author_name}} - - - - - - - - 近期排期 - 日程 - - - - - - {{item.title}} - {{item.location || "地点待定"}} - {{item.schedule_time_text}} - - {{item.schedule_type_text}} - - - - - - - 班级投票 - 参与 - - - - - - {{item.title}} - {{item.vote_type_text}} · {{item.vote_action_text}} · {{item.total_voters}} 人参与 - - - - - 班级动态 @@ -99,7 +50,7 @@ - + 暂无可展示内容 diff --git a/miniprogram/pages/home/index.wxss b/miniprogram/pages/home/index.wxss index 8b13789..edd9098 100644 --- a/miniprogram/pages/home/index.wxss +++ b/miniprogram/pages/home/index.wxss @@ -1 +1,33 @@ +.home-status { + 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; +} +.focus-card { + display: flex; + align-items: center; + gap: 22rpx; + margin-bottom: 18rpx; + border: 1rpx solid rgba(121, 84, 54, 0.12); + border-radius: 30rpx; + background: linear-gradient(180deg, #fffdf8 0%, #fff7ed 100%); + padding: 26rpx; + box-shadow: 0 18rpx 42rpx rgba(68, 39, 27, 0.07); +} + +.focus-label { + margin-bottom: 6rpx; + color: #8b5a36; + font-size: 22rpx; + font-weight: 760; +} diff --git a/miniprogram/pages/manage/index.js b/miniprogram/pages/manage/index.js index beed6a7..8ca468b 100644 --- a/miniprogram/pages/manage/index.js +++ b/miniprogram/pages/manage/index.js @@ -1,4 +1,4 @@ -const { post } = require("../../utils/api"); +const { post, uploadFile } = require("../../utils/api"); const { getModule } = require("../../utils/modules"); const { hasManagePermission } = require("../../utils/permissions"); const { ensureModuleOpen, getActiveClassId, showError } = require("../../utils/page-helpers"); @@ -33,6 +33,8 @@ Page({ fundIncomeCategories: ["班费收取", "活动赞助", "其他收入"], fundExpenseCategories: ["聚餐", "活动物资", "场地费", "交通费", "礼品", "其他支出"], fundCategories: ["聚餐", "活动物资", "场地费", "交通费", "礼品", "其他支出"], + fundImageUrls: [], + uploadingImages: false, loading: false }, @@ -72,7 +74,8 @@ Page({ fundExpenseClass: form.type === "expense" ? "active expense" : "", fundCategories: form.type === "income" ? this.data.fundIncomeCategories - : this.data.fundExpenseCategories + : this.data.fundExpenseCategories, + fundImageUrls: [] }); wx.setNavigationBarTitle({ title: `新增${module.title}` }); }, @@ -148,6 +151,43 @@ Page({ this.setData({ "form.category": this.data.fundCategories[index] }); }, + chooseFundImages() { + if (this.data.uploadingImages) return; + if (this.data.fundImageUrls.length >= 6) { + wx.showToast({ title: "最多 6 张图片", icon: "none" }); + return; + } + wx.chooseMedia({ + count: 6 - this.data.fundImageUrls.length, + mediaType: ["image"], + sourceType: ["album", "camera"], + success: async (res) => { + const paths = (res.tempFiles || []).map((item) => item.tempFilePath); + if (!paths.length) return; + this.setData({ uploadingImages: true }); + try { + const uploaded = [...this.data.fundImageUrls]; + for (const path of paths) { + const result = await uploadFile("/api/upload/image", path, {}, "file"); + if (result && result.url) uploaded.push(result.url); + } + this.setData({ fundImageUrls: uploaded.slice(0, 6) }); + } catch (error) { + showError(error, "上传图片失败"); + } finally { + this.setData({ uploadingImages: false }); + } + } + }); + }, + + removeFundImage(event) { + const index = Number(event.currentTarget.dataset.index); + this.setData({ + fundImageUrls: this.data.fundImageUrls.filter((_, itemIndex) => itemIndex !== index) + }); + }, + async submit() { const classId = getActiveClassId(); const moduleKey = this.data.moduleKey; @@ -215,6 +255,7 @@ Page({ amount: Number(form.amount), category: form.category, description: form.description || null, + image_urls: this.data.fundImageUrls, record_date: form.record_date }); } diff --git a/miniprogram/pages/manage/index.wxml b/miniprogram/pages/manage/index.wxml index 7b0e141..8c904d3 100644 --- a/miniprogram/pages/manage/index.wxml +++ b/miniprogram/pages/manage/index.wxml @@ -106,6 +106,19 @@ 备注