This commit is contained in:
aaron 2026-05-15 20:22:12 +08:00
parent 5659a15636
commit f105cb369c
3 changed files with 400 additions and 3 deletions

View File

@ -1,6 +1,10 @@
const { get } = require("../../utils/api");
const { del, get, put, uploadFile } = require("../../utils/api");
const { hasManagePermission } = require("../../utils/permissions");
const { showError } = require("../../utils/page-helpers");
const FUND_INCOME_CATEGORIES = ["班费收取", "活动赞助", "其他收入"];
const FUND_EXPENSE_CATEGORIES = ["聚餐", "活动物资", "场地费", "交通费", "礼品", "其他支出"];
function formatAmount(value) {
return Number(value || 0).toFixed(2);
}
@ -30,7 +34,17 @@ Page({
data: {
id: null,
record: null,
loading: false
canManage: false,
editOpen: false,
editForm: {},
editImageUrls: [],
editCategories: FUND_EXPENSE_CATEGORIES,
editIncomeClass: "",
editExpenseClass: "active expense",
loading: false,
saving: false,
deleting: false,
uploadingImages: false
},
onLoad(options) {
@ -49,7 +63,9 @@ Page({
this.setData({ loading: true });
try {
const record = await get(`/api/fund/${id}`);
this.setData({ record: normalizeRecord(record) });
const user = getApp().globalData.user || wx.getStorageSync("auth_user");
const canManage = hasManagePermission(user, record.class_id, "fund");
this.setData({ record: normalizeRecord(record), canManage });
} catch (error) {
showError(error, "加载班费详情失败");
} finally {
@ -62,5 +78,166 @@ Page({
const urls = this.data.record && this.data.record.image_urls ? this.data.record.image_urls : [];
if (!current || !urls.length) return;
wx.previewImage({ current, urls });
},
noop() {},
openEdit() {
const record = this.data.record;
if (!record || !this.data.canManage) return;
const categories = record.type === "income" ? FUND_INCOME_CATEGORIES : FUND_EXPENSE_CATEGORIES;
this.setData({
editOpen: true,
editForm: {
type: record.type,
amount: String(record.amount || ""),
category: record.category || categories[0],
description: record.description || "",
record_date: record.record_date || ""
},
editImageUrls: [...record.image_urls],
editCategories: categories,
editIncomeClass: record.type === "income" ? "active income" : "",
editExpenseClass: record.type === "expense" ? "active expense" : ""
});
},
closeEdit() {
if (this.data.saving || this.data.uploadingImages) return;
this.setData({ editOpen: false });
},
onEditInput(event) {
const field = event.currentTarget.dataset.field;
this.setData({ [`editForm.${field}`]: event.detail.value });
},
setEditFundType(event) {
const type = event.currentTarget.dataset.type;
const categories = type === "income" ? FUND_INCOME_CATEGORIES : FUND_EXPENSE_CATEGORIES;
this.setData({
"editForm.type": type,
"editForm.category": categories[0],
editCategories: categories,
editIncomeClass: type === "income" ? "active income" : "",
editExpenseClass: type === "expense" ? "active expense" : ""
});
},
onEditCategoryChange(event) {
const index = Number(event.detail.value);
this.setData({ "editForm.category": this.data.editCategories[index] });
},
onEditDateChange(event) {
this.setData({ "editForm.record_date": event.detail.value });
},
chooseEditImages() {
if (this.data.uploadingImages) return;
if (this.data.editImageUrls.length >= 6) {
wx.showToast({ title: "最多 6 张图片", icon: "none" });
return;
}
wx.chooseMedia({
count: 6 - this.data.editImageUrls.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.editImageUrls];
for (const path of paths) {
const result = await uploadFile("/api/upload/image", path, {}, "file");
if (result && result.url) uploaded.push(result.url);
}
this.setData({ editImageUrls: uploaded.slice(0, 6) });
} catch (error) {
showError(error, "上传图片失败");
} finally {
this.setData({ uploadingImages: false });
}
}
});
},
removeEditImage(event) {
const index = Number(event.currentTarget.dataset.index);
this.setData({
editImageUrls: this.data.editImageUrls.filter((_, itemIndex) => itemIndex !== index)
});
},
async saveEdit() {
if (this.data.saving || this.data.uploadingImages) return;
const form = this.data.editForm;
const amount = Number(form.amount);
if (!amount || 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;
}
this.setData({ saving: true });
try {
const updated = await put(`/api/fund/${this.data.id}`, {
type: form.type,
amount,
category: form.category,
description: form.description || null,
image_urls: this.data.editImageUrls,
record_date: form.record_date
});
this.setData({
record: normalizeRecord(updated),
editOpen: false
});
this.refreshPreviousPage();
wx.showToast({ title: "已更新", icon: "success" });
} catch (error) {
showError(error, "保存失败");
} finally {
this.setData({ saving: false });
}
},
deleteRecord() {
if (this.data.deleting || !this.data.canManage || !this.data.record) return;
wx.showModal({
title: "删除班费记录",
content: "删除后无法恢复,确认删除这条班费记录?",
confirmText: "删除",
confirmColor: "#b42318",
success: async (res) => {
if (!res.confirm) return;
this.setData({ deleting: true });
try {
await del(`/api/fund/${this.data.id}`);
this.refreshPreviousPage();
wx.showToast({ title: "已删除", icon: "success" });
setTimeout(() => wx.navigateBack(), 500);
} catch (error) {
showError(error, "删除失败");
} finally {
this.setData({ deleting: false });
}
}
});
},
refreshPreviousPage() {
const pages = getCurrentPages();
const previousPage = pages[pages.length - 2];
if (previousPage && previousPage.load) {
previousPage.load();
}
}
});

View File

@ -4,6 +4,10 @@
<view class="hero-title">{{record.signed_amount_text}}</view>
<view class="hero-subtitle">{{record.category}} · {{record.record_date}}</view>
<view class="fund-proof-pill">{{record.image_count_text}}</view>
<view wx:if="{{canManage}}" class="fund-action-bar">
<view class="fund-action-button" bindtap="openEdit">编辑</view>
<view class="fund-action-button danger" bindtap="deleteRecord">{{deleting ? "删除中..." : "删除"}}</view>
</view>
</view>
<view class="section">
@ -71,3 +75,61 @@
<view class="muted">未找到班费记录</view>
</view>
</view>
<view wx:if="{{editOpen}}" class="edit-mask" catchtap="closeEdit">
<view class="edit-panel" catchtap="noop">
<view class="edit-head">
<view>
<view class="edit-kicker">FUND RECORD</view>
<view class="edit-title">修改班费记录</view>
</view>
<view class="edit-close" bindtap="closeEdit">×</view>
</view>
<view class="form-field">
<view class="form-label">收支类型</view>
<view class="fund-type-switch">
<view class="fund-type {{editIncomeClass}}" data-type="income" bindtap="setEditFundType">入账</view>
<view class="fund-type {{editExpenseClass}}" data-type="expense" bindtap="setEditFundType">出账</view>
</view>
</view>
<view class="form-field">
<view class="form-label">金额</view>
<input class="form-input" type="digit" value="{{editForm.amount}}" data-field="amount" bindinput="onEditInput" placeholder="0.00" />
</view>
<view class="form-field">
<view class="form-label">小类型</view>
<picker mode="selector" range="{{editCategories}}" bindchange="onEditCategoryChange">
<view class="form-select">{{editForm.category}}</view>
</picker>
</view>
<view class="form-field">
<view class="form-label">日期</view>
<picker mode="date" value="{{editForm.record_date}}" bindchange="onEditDateChange">
<view class="form-select">{{editForm.record_date}}</view>
</picker>
</view>
<view class="form-field">
<view class="form-label">备注</view>
<textarea class="form-textarea" value="{{editForm.description}}" data-field="description" bindinput="onEditInput" placeholder="补充说明,可不填" />
</view>
<view class="form-field">
<view class="edit-label-row">
<view class="form-label">小票图片</view>
<view class="section-action" bindtap="chooseEditImages">{{uploadingImages ? "上传中..." : "添加图片"}}</view>
</view>
<view class="form-hint">最多 6 张,可拍照或从相册选择。</view>
<view wx:if="{{editImageUrls.length}}" class="edit-image-grid">
<view wx:for="{{editImageUrls}}" wx:key="*this" class="edit-image-cell">
<image src="{{item}}" mode="aspectFill" />
<view class="edit-image-remove" data-index="{{index}}" bindtap="removeEditImage">×</view>
</view>
</view>
</view>
<view class="edit-submit-row">
<button class="button secondary" bindtap="closeEdit" disabled="{{saving || uploadingImages}}">取消</button>
<button class="button" loading="{{saving}}" bindtap="saveEdit">保存修改</button>
</view>
</view>
</view>

View File

@ -21,6 +21,31 @@
font-weight: 650;
}
.fund-action-bar {
position: relative;
display: flex;
gap: 16rpx;
margin-top: 26rpx;
}
.fund-action-button {
min-width: 132rpx;
height: 58rpx;
border: 1rpx solid rgba(255, 248, 237, 0.22);
border-radius: 999rpx;
background: rgba(255, 255, 255, 0.12);
color: #fff8ed;
font-size: 25rpx;
font-weight: 720;
line-height: 58rpx;
text-align: center;
}
.fund-action-button.danger {
background: rgba(255, 248, 237, 0.9);
color: #9a3a2f;
}
.fund-detail-row {
display: flex;
align-items: center;
@ -58,3 +83,136 @@
background: #efe0ca;
box-shadow: 0 14rpx 32rpx rgba(68, 39, 27, 0.08);
}
.edit-mask {
position: fixed;
inset: 0;
z-index: 50;
display: flex;
align-items: flex-end;
background: rgba(47, 33, 28, 0.42);
}
.edit-panel {
width: 100%;
max-height: 88vh;
overflow-y: auto;
box-sizing: border-box;
padding: 30rpx 28rpx calc(34rpx + env(safe-area-inset-bottom));
border-radius: 34rpx 34rpx 0 0;
background: #fffaf3;
box-shadow: 0 -18rpx 60rpx rgba(47, 33, 28, 0.18);
}
.edit-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 24rpx;
margin-bottom: 26rpx;
}
.edit-kicker {
color: #9d8068;
font-size: 20rpx;
font-weight: 760;
letter-spacing: 1rpx;
}
.edit-title {
margin-top: 6rpx;
color: #2f211c;
font-size: 34rpx;
font-weight: 780;
}
.edit-close {
width: 54rpx;
height: 54rpx;
border-radius: 999rpx;
background: #f1e4d4;
color: #6b1f2b;
font-size: 42rpx;
line-height: 48rpx;
text-align: center;
}
.fund-type-switch {
display: flex;
gap: 12rpx;
margin-top: 12rpx;
padding: 8rpx;
border-radius: 24rpx;
background: #f1e4d4;
}
.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;
}
.edit-label-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.edit-image-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12rpx;
margin-top: 16rpx;
}
.edit-image-cell {
position: relative;
overflow: hidden;
height: 180rpx;
border-radius: 18rpx;
background: #efe0ca;
}
.edit-image-cell image {
width: 100%;
height: 100%;
}
.edit-image-remove {
position: absolute;
top: 8rpx;
right: 8rpx;
width: 40rpx;
height: 40rpx;
border-radius: 999rpx;
background: rgba(47, 33, 28, 0.72);
color: #fff;
font-size: 32rpx;
line-height: 36rpx;
text-align: center;
}
.edit-submit-row {
display: grid;
grid-template-columns: 1fr 1.35fr;
gap: 16rpx;
margin-top: 28rpx;
}