用户 2026-10-02 提的 19 条需求,本轮完成剩下的 8 条 + CalDAV 服务端:
- ③ 附件:图片/视频给缩略图,PDF/Word/Excel 给**同尺寸**图标;可预览的在本页
弹层里看(不再新开标签页),渲染不了的点了直接下载;预览层右上角恒有下载按钮。
- ⑦b CalDAV:把「我的日历本」按 RFC 4791 只读子集分享出去(OPTIONS/PROPFIND/
REPORT/GET,写方法一律 403)。用**专用口令**(库里只存 sha256),不是登录口令。
- ⑨⑩ 任务:分类与看板都由用户自建;已完成的任务从看板消失,改在「已完成」页
按分类查看/搜索;设置入口在「新建任务」旁的 ☰。
- ⑪ 全局人员选择器:一个片段铺到观影/旅程/资源/日历共享/任务共享/财务共享;
修掉了老组件「名字和拼音都搜不动」的 bug —— .att-item 自己声明了 display:flex,
压过了浏览器 [hidden]{display:none},过滤根本没生效。
- ⑫ 家谱:头像改方形不再裁切;姻亲不在图上的卡片给「⇄ 姻亲」入口,
以 TA 为中心重画 + 「返回我的家谱」。
- ⑬ 纪念日:阴历/阳历都能设且按对应历法重复;名册里每个人的生日默认可见
(实时派生,不复制一份);全部作为全天日程进日历。
- ⑮ 财务:账本可共享,双方都能记账、都能看全部账目,资产−负债=净资产;
金额一律以「分」存,不用浮点。
验证:端到端 724 项通过 / 0 失败;几何 368、交互 97、移动端 516、家谱 24、
升级 123 全绿;SQL 列名静态校验 0 处可疑;部署 11 条线上探针全过。
288 lines
13 KiB
PHP
288 lines
13 KiB
PHP
<?php
|
||
/**
|
||
* 财务 / 账本(v24 用户第 15 条)
|
||
*
|
||
* 用户原话:「先创建一个家庭,或者说,创建一个账本,这个账本,可以共享给某个人,
|
||
* 共享后,这个人就可以往这个账本加入账目,这样,双方都可以看到所有的账目,
|
||
* 最后计算出总数,这就是一个家庭的总账目。」
|
||
*
|
||
* 页面结构就是照着这句话来的:
|
||
* 顶上一排账本(切换 / 新建)→ 汇总卡(资产 / 负债 / 净资产)→ 账目表 → 共享设置。
|
||
*
|
||
* ⚠️ 金额全程以**分**参与计算(Ledger 里做的),这里只负责显示成元。
|
||
* 任何地方都不要拿元去加总。
|
||
*
|
||
* @param array $books 我能看到的账本(带 my_perm / entry_count)
|
||
* @param array $cur 当前账本
|
||
* @param array $entries 当前账本的账目
|
||
* @param array $summary ['asset','debt','net','by_kind','count'](单位:分)
|
||
* @param array $shares 当前账本的共享列表
|
||
* @param array $people 名册
|
||
*/
|
||
$canWrite = \App\Auth::canWrite() && \App\Ledger::canWrite($cur);
|
||
$curId = (int)$cur['id'];
|
||
$money = static fn($c): string => \App\Ledger::money($c);
|
||
?>
|
||
<div class="page-head">
|
||
<h1>财务</h1>
|
||
<span class="sub">家庭的账 · 共享后双方都能记、都能看</span>
|
||
<div class="actions">
|
||
<a class="btn btn-ghost" href="<?= e(url('ledger', ['book' => $curId])) ?>">刷新</a>
|
||
</div>
|
||
</div>
|
||
|
||
<?php if (count($books) > 1): ?>
|
||
<div class="seg">
|
||
<?php foreach ($books as $b): ?>
|
||
<a class="seg-btn<?= (int)$b['id'] === $curId ? ' is-on' : '' ?>"
|
||
href="<?= e(url('ledger', ['book' => (int)$b['id']])) ?>">
|
||
<?= e((string)$b['name']) ?><?= ($b['my_perm'] ?? '') === 'read' ? '(只读)' : '' ?>
|
||
</a>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<div class="card card-pad led-sum">
|
||
<div class="led-sum-head">
|
||
<h2 class="cinema-h2" style="margin:0"><?= e((string)$cur['name']) ?></h2>
|
||
<span class="small muted">
|
||
<?= (int)($cur['entry_count'] ?? count($entries)) ?> 笔账
|
||
<?php if (($cur['my_perm'] ?? '') === 'read'): ?>
|
||
· <b>只读</b>(不能记账)
|
||
<?php endif; ?>
|
||
</span>
|
||
</div>
|
||
<div class="led-total">
|
||
<div class="led-total-item">
|
||
<span class="led-total-lb">总资产</span>
|
||
<span class="led-total-num led-asset"><?= e($money($summary['asset'])) ?></span>
|
||
</div>
|
||
<div class="led-total-item">
|
||
<span class="led-total-lb">总负债</span>
|
||
<span class="led-total-num led-debt"><?= e($money($summary['debt'])) ?></span>
|
||
</div>
|
||
<div class="led-total-item led-net">
|
||
<span class="led-total-lb">净资产</span>
|
||
<span class="led-total-num"><?= e($money($summary['net'])) ?></span>
|
||
</div>
|
||
</div>
|
||
<?php if ($summary['by_kind']): ?>
|
||
<div class="led-kinds">
|
||
<?php foreach ($summary['by_kind'] as $k => $c): ?>
|
||
<span class="led-kind-chip<?= \App\Ledger::isDebt((string)$k) ? ' is-debt' : '' ?>">
|
||
<?= \App\Ledger::kindIcon((string)$k) ?> <?= e(\App\Ledger::kindWord((string)$k)) ?>
|
||
<b><?= e($money($c)) ?></b>
|
||
</span>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<?php if ($canWrite): ?>
|
||
<div class="card card-pad">
|
||
<h2 class="cinema-h2">记一笔</h2>
|
||
<form method="post" action="<?= e(url('ledger/entry_add')) ?>">
|
||
<?= csrf_field() ?>
|
||
<input type="hidden" name="ledger_id" value="<?= $curId ?>">
|
||
<div class="form-grid">
|
||
<div class="field">
|
||
<label for="le-kind">类别 *</label>
|
||
<select class="select" id="le-kind" name="kind">
|
||
<?php foreach (\App\Ledger::KINDS as $k => $v): ?>
|
||
<option value="<?= e($k) ?>"><?= e($v[2] . ' ' . $v[0]) ?><?= $v[1] === 'debt' ? '(负债)' : '' ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
<div class="field">
|
||
<label for="le-title">名称 *</label>
|
||
<input class="input" id="le-title" name="title" required placeholder="如:工商银行定期">
|
||
</div>
|
||
<div class="field">
|
||
<label for="le-amount">金额(元)*</label>
|
||
<input class="input" id="le-amount" name="amount" required inputmode="decimal"
|
||
placeholder="如:120000.00">
|
||
<span class="tip">负债(贷款/信用卡)也填正数,系统按类别算成负债。</span>
|
||
</div>
|
||
<div class="field">
|
||
<label for="le-person">挂在谁名下</label>
|
||
<select class="select" id="le-person" name="person_id">
|
||
<option value="0">不指定</option>
|
||
<?php foreach ($people as $p): ?>
|
||
<option value="<?= (int)$p['id'] ?>"><?= e(person_name($p)) ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
<div class="field full">
|
||
<label for="le-note">备注</label>
|
||
<input class="input" id="le-note" name="note" placeholder="可选">
|
||
</div>
|
||
</div>
|
||
<div class="btn-row"><button class="btn" type="submit">添加账目</button></div>
|
||
</form>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<div class="card card-pad">
|
||
<h2 class="cinema-h2">账目(<?= count($entries) ?>)</h2>
|
||
<?php if (!$entries): ?>
|
||
<p class="empty-hint">这本账还没有账目。上面「记一笔」开始记吧。</p>
|
||
<?php else: ?>
|
||
<div class="table-wrap">
|
||
<table class="table">
|
||
<thead>
|
||
<tr><th>类别</th><th>名称</th><th class="num">金额</th><th>挂在谁名下</th><th>备注</th><th></th></tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($entries as $en): ?>
|
||
<?php $ek = (string)$en['kind']; $debt = \App\Ledger::isDebt($ek); ?>
|
||
<tr>
|
||
<td class="nowrap">
|
||
<span class="led-kind-chip<?= $debt ? ' is-debt' : '' ?>">
|
||
<?= \App\Ledger::kindIcon($ek) ?> <?= e(\App\Ledger::kindWord($ek)) ?>
|
||
</span>
|
||
</td>
|
||
<td><?= e((string)$en['title']) ?></td>
|
||
<td class="num<?= $debt ? ' led-debt' : ' led-asset' ?>">
|
||
<?= $debt ? '-' : '' ?><?= e($money((int)$en['amount_cents'])) ?>
|
||
</td>
|
||
<td class="nowrap">
|
||
<?php if (!empty($en['person_name'])): ?>
|
||
<?= person_chip([
|
||
'id' => (int)$en['person_id'], 'name' => (string)$en['person_name'],
|
||
'my_nickname' => (string)($en['person_nick'] ?? ''),
|
||
'avatar' => $en['avatar'] ?? null, 'avatar_color' => $en['avatar_color'] ?? null,
|
||
]) ?>
|
||
<?php else: ?><span class="small muted">—</span><?php endif; ?>
|
||
</td>
|
||
<td class="small muted"><?= e((string)($en['note'] ?? '')) ?></td>
|
||
<td class="nowrap">
|
||
<?php if ($canWrite): ?>
|
||
<details class="led-edit">
|
||
<summary class="link-quiet">改</summary>
|
||
<form method="post" action="<?= e(url('ledger/entry_update')) ?>" class="node-form">
|
||
<?= csrf_field() ?>
|
||
<input type="hidden" name="ledger_id" value="<?= $curId ?>">
|
||
<input type="hidden" name="id" value="<?= (int)$en['id'] ?>">
|
||
<div class="nf-grid">
|
||
<div class="field">
|
||
<label>类别</label>
|
||
<select class="select" name="kind">
|
||
<?php foreach (\App\Ledger::KINDS as $k => $v): ?>
|
||
<option value="<?= e($k) ?>" <?= $ek === $k ? 'selected' : '' ?>>
|
||
<?= e($v[0]) ?>
|
||
</option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
<div class="field">
|
||
<label>名称</label>
|
||
<input class="input" name="title" value="<?= e((string)$en['title']) ?>" required>
|
||
</div>
|
||
<div class="field">
|
||
<label>金额(元)</label>
|
||
<input class="input" name="amount"
|
||
value="<?= e(\App\Ledger::amountYuan((int)$en['amount_cents'])) ?>" required>
|
||
</div>
|
||
<div class="field">
|
||
<label>备注</label>
|
||
<input class="input" name="note" value="<?= e((string)($en['note'] ?? '')) ?>">
|
||
</div>
|
||
</div>
|
||
<div class="btn-row"><button class="btn btn-sm" type="submit">保存</button></div>
|
||
</form>
|
||
<form method="post" action="<?= e(url('ledger/entry_del')) ?>" class="res-inline">
|
||
<?= csrf_field() ?>
|
||
<input type="hidden" name="ledger_id" value="<?= $curId ?>">
|
||
<input type="hidden" name="id" value="<?= (int)$en['id'] ?>">
|
||
<button class="btn btn-ghost btn-sm" type="submit"
|
||
onclick="return confirm('删掉这笔账目?')">删除</button>
|
||
</form>
|
||
</details>
|
||
<?php endif; ?>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
<tfoot>
|
||
<tr>
|
||
<th colspan="2">合计(资产 − 负债)</th>
|
||
<th class="num led-net"><?= e($money($summary['net'])) ?></th>
|
||
<th colspan="3"></th>
|
||
</tr>
|
||
</tfoot>
|
||
</table>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<div class="card card-pad">
|
||
<h2 class="cinema-h2">共享与账本</h2>
|
||
<div class="led-shares">
|
||
<span class="res-label">已共享给</span>
|
||
<?php if (!$shares): ?>
|
||
<span class="small muted">还没有共享给任何人。</span>
|
||
<?php endif; ?>
|
||
<?php foreach ($shares as $s): ?>
|
||
<span class="led-share-chip">
|
||
<?= person_chip([
|
||
'id' => (int)$s['person_id'], 'name' => (string)$s['name'],
|
||
'avatar' => $s['avatar'] ?? null, 'avatar_color' => $s['avatar_color'] ?? null,
|
||
]) ?>
|
||
<span class="node-badge"><?= ($s['perm'] ?? 'read') === 'write' ? '可写' : '只读' ?></span>
|
||
<?php if ($canWrite): ?>
|
||
<form method="post" action="<?= e(url('ledger/unshare')) ?>" class="res-inline">
|
||
<?= csrf_field() ?>
|
||
<input type="hidden" name="ledger_id" value="<?= $curId ?>">
|
||
<input type="hidden" name="person_id" value="<?= (int)$s['person_id'] ?>">
|
||
<button class="btn btn-ghost btn-sm" type="submit">取消</button>
|
||
</form>
|
||
<?php endif; ?>
|
||
</span>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
|
||
<?php if ($canWrite): ?>
|
||
<div class="led-ops">
|
||
<?php
|
||
$ppName = 'person_id';
|
||
$ppPeople = $people;
|
||
$ppSelected = [];
|
||
$ppMode = 'single';
|
||
$ppOpenBtn = '选择共享给谁';
|
||
$ppTitle = '账本共享给谁';
|
||
$ppEmptyText = '未选择';
|
||
?>
|
||
<form class="inline-form" method="post" action="<?= e(url('ledger/share')) ?>">
|
||
<?= csrf_field() ?>
|
||
<input type="hidden" name="ledger_id" value="<?= $curId ?>">
|
||
<?php require __DIR__ . '/../_person_picker.php'; ?>
|
||
<select class="select" name="perm">
|
||
<?php foreach (\App\Ledger::PERM as $pk => $pv): ?>
|
||
<option value="<?= e($pk) ?>"><?= e($pv) ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<button class="btn btn-sm" type="submit">共享</button>
|
||
</form>
|
||
|
||
<form class="inline-form" method="post" action="<?= e(url('ledger/book_rename')) ?>">
|
||
<?= csrf_field() ?>
|
||
<input type="hidden" name="ledger_id" value="<?= $curId ?>">
|
||
<input class="input" name="name" value="<?= e((string)$cur['name']) ?>" required>
|
||
<button class="btn btn-ghost btn-sm" type="submit">账本改名</button>
|
||
</form>
|
||
|
||
<form class="inline-form" method="post" action="<?= e(url('ledger/book_add')) ?>">
|
||
<?= csrf_field() ?>
|
||
<input class="input" name="name" placeholder="新账本名,如:孩子的教育金" required>
|
||
<button class="btn btn-ghost btn-sm" type="submit">新建账本</button>
|
||
</form>
|
||
|
||
<form class="inline-form" method="post" action="<?= e(url('ledger/book_del')) ?>">
|
||
<?= csrf_field() ?>
|
||
<input type="hidden" name="ledger_id" value="<?= $curId ?>">
|
||
<button class="btn btn-ghost btn-sm" type="submit"
|
||
onclick="return confirm('删掉这本账?里面的账目会一起删掉。')">删除这本账</button>
|
||
</form>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|