节点详情:照片灯箱浏览、参与人点击编辑、花销只记总计

1) 照片与附件不再新开窗口看
   之前点缩略图直接 target=_blank 打开原始文件,看完还得切回来。
   现在统一走当前页灯箱(templates/_lightbox.php,详情页照片墙与
   照片管理页共用):左右箭头 / 键盘 ←→ / 手机滑动切上一张下一张,
   Esc 或点背景关闭,带「第几张 / 共几张」计数并预加载相邻两张。
   非图片附件(PDF 等)仍保留直接打开与下载。

2) 参与人可以直接改了
   之前加完只能移出重加。现在每个参与人旁有「编辑」,点开预填弹窗
   (角色标签 / 心情 / 签到时间),保存仍走 members/store ——
   Members::save() 本来就是 upsert(同节点同人一条),服务端零改动。
   失败回填按 user_id 精确匹配,只回填刚才没保存成的那个人。

3) 花销去掉预算,只记真实花费
   明细上移,底部一行「总计(N 笔)¥X」。表单不再有「预算」选项,
   查询一律 is_budget=0(历史预算行不再出现在列表与合计里);
   活动页统计同步改成「共 N 笔 · 总计」。列保留在库里不删数据。

已验证:本地全链 11 步全绿(php -l / 渲染 286 项 / SQL / PDO / CSS /
Mime / amap / 内联 JS×11 / 布局几何 / 表单交互 73 项);远端 e2e
296 项 0 失败(含灯箱、编辑弹窗、历史预算行排除的专项断言);
部署后双 PHP 渲染逐字节一致、60 文件哈希一致,线上 style.css 含新规则。
This commit is contained in:
2026-09-23 14:59:02 +08:00
parent 86e0481a19
commit 48237c8947
8 changed files with 386 additions and 105 deletions
+23 -39
View File
@@ -6,8 +6,11 @@ namespace App;
/**
* 花销与分摊
*
* 表结构:expenses(挂在 trip_nodes 上,is_budget 区分"预算"和"实际")
* + expense_splits(这笔钱按人分摊,谁承担多少)
* 表结构:expenses(挂在 trip_nodes 上)+ expense_splits(这笔钱按人分摊,谁承担多少)
*
* ⚠️ 历史上 expenses 有 is_budget(预算 / 实际)之分,用户明确反馈「预算没有意义」,
* 现在只记真实花销:查询一律 is_budget = 0,表单也不再提供「预算」选项。
* 列本身保留(老数据还在库里,不至于丢),但 UI 与汇总都不再出现预算概念。
*/
class Expenses
{
@@ -23,7 +26,7 @@ class Expenses
return ['CNY' => '人民币 ¥', 'JPY' => '日元 ¥', 'USD' => '美元 $', 'EUR' => '欧元 €', 'HKD' => '港币 HK$'];
}
/** 某个节点下的花销,带付款人和分摊明细 */
/** 某个节点下的花销(只看真实花销),带付款人和分摊明细 */
public static function forNode(int $nodeId): array
{
$db = Database::get();
@@ -31,8 +34,8 @@ class Expenses
SELECT e.*, u.name AS payer_name, u.avatar AS payer_avatar
FROM expenses e
LEFT JOIN users u ON u.id = e.payer_user_id
WHERE e.trip_node_id = ?
ORDER BY e.is_budget ASC, e.created_at ASC, e.id ASC');
WHERE e.trip_node_id = ? AND e.is_budget = 0
ORDER BY e.created_at ASC, e.id ASC');
$stmt->execute([$nodeId]);
$rows = $stmt->fetchAll();
if (empty($rows)) return [];
@@ -56,41 +59,32 @@ class Expenses
return $rows;
}
/** 活动下的花销合计(预算 / 实际),以及各节点的小计 */
/** 活动下的真实花销总计,以及各节点的小计 */
public static function summaryForActivity(int $activityId): array
{
$db = Database::get();
$stmt = $db->prepare('
SELECT tn.id AS node_id, e.is_budget, SUM(e.total_amount) AS amount, COUNT(*) AS cnt
SELECT tn.id AS node_id, SUM(e.total_amount) AS amount, COUNT(*) AS cnt
FROM expenses e
JOIN trip_nodes tn ON tn.id = e.trip_node_id
WHERE tn.activity_id = ?
GROUP BY tn.id, e.is_budget');
WHERE tn.activity_id = ? AND e.is_budget = 0
GROUP BY tn.id');
$stmt->execute([$activityId]);
$byNode = [];
$budgetTotal = 0.0;
$actualTotal = 0.0;
$total = 0.0;
$count = 0;
foreach ($stmt->fetchAll() as $row) {
$nodeId = (int)$row['node_id'];
$isBudget = (int)$row['is_budget'] === 1;
$amount = (float)$row['amount'];
$byNode[$nodeId][$isBudget ? 'budget' : 'actual'] = $amount;
if ($isBudget) {
$budgetTotal += $amount;
} else {
$actualTotal += $amount;
}
$byNode[(int)$row['node_id']] = $amount;
$total += $amount;
$count += (int)$row['cnt'];
}
return [
'budget' => $budgetTotal,
'actual' => $actualTotal,
'count' => $count,
'by_node' => $byNode,
'diff' => $actualTotal - $budgetTotal,
'total' => $total,
'count' => $count,
'by_node' => $byNode,
];
}
@@ -191,23 +185,13 @@ class Expenses
return true;
}
/** 某个节点的花销合计 */
/** 某个节点的真实花销合计 */
public static function summaryForNode(int $nodeId): array
{
$stmt = Database::get()->prepare('SELECT is_budget, SUM(total_amount) AS amount, COUNT(*) AS cnt
FROM expenses WHERE trip_node_id = ? GROUP BY is_budget');
$stmt = Database::get()->prepare('SELECT COALESCE(SUM(total_amount), 0) AS amount, COUNT(*) AS cnt
FROM expenses WHERE trip_node_id = ? AND is_budget = 0');
$stmt->execute([$nodeId]);
$budget = 0.0;
$actual = 0.0;
$count = 0;
foreach ($stmt->fetchAll() as $row) {
if ((int)$row['is_budget'] === 1) {
$budget = (float)$row['amount'];
} else {
$actual = (float)$row['amount'];
}
$count += (int)$row['cnt'];
}
return ['budget' => $budget, 'actual' => $actual, 'count' => $count, 'diff' => $actual - $budget];
$row = $stmt->fetch();
return ['total' => (float)$row['amount'], 'count' => (int)$row['cnt']];
}
}