v78 服务端:首页任务改成「就地处理」面板 + 新建任务搬到首页

用户原话:
「网页端,首页点击一个任务后,除了展示信息,应该有相应的功能,可以修改状态:
  待办、进行中,可以挪动分组,可以拖动上传附件,可以添加评论,添加子任务等。
  其目地,是为了让用户不离开首页,就能对任务进行简单的处理。」
「另外,在全部任务旁边,放一个新建任务的功能,其实就是把任务中的新建和编辑功能,
  搬过来,用弹出框来处理。」

一、首页任务浮层由「只读信息」改成「可操作面板」
- 新增 templates/tasks/_panel.php:状态三段 / 看板 chips(带看板色)/
  子任务(复用 _task_check.php)/ 附件拖放框(复用 _attachments.php)/ 讨论
- 新增路由 tasks/panel:只吐一块 HTML(找不到 404)
- 既有七个动作路由各加一条 ajax_panel 回包分支(状态 / 挪分组 / 新建子任务 /
  评论增删 / 附件增删),没有新开写接口
- helpers.php 新增 task_panel_wanted() / task_panel_json();
  fb_entry_json() 的 payload 增加 panel 字段(只有任务会给)
- 每次动作回**整块新面板**:一次动作会牵动子任务计数、附件张数、看板选中态,
  逐处同步 DOM 必然漏一处
- 拖进来的附件**立刻上传**(任务已存在,不该再让用户点一次保存),
  不复用 _stage_drop.php 那套「暂存 + 认领」

二、新建任务搬到首页
- 从 templates/tasks/index.php 抽出公共片段 templates/tasks/_add_modal.php
  (字段与原来一字未改),首页与任务页共用同一份
- 新增 assets/js/task_add.js 管开关(两页共用,幂等绑定);
  打开那一刻才 fbUpgradeRange() + fbBootAntd()(hidden 时宽高为 0,antd 会定位错乱)
- App\HomeCards 登记表里加 add => 新建任务,首页卡头与「全部任务」并排
- tasks/create 支持 back=home(白名单),新建完回首页

三、其它
- assets/js/task_panel.js:事件委托;动作后整块换面板;
  顺手同步首页那张卡的状态徽标与复选框(关掉浮层不能还是旧状态);
  传附件时保住用户写了一半的留言与子任务标题;取不到面板时退回只读信息
- app.css 追加面板样式(含 color-mix 的 @supports 兜底)与卡片窄到宽 2 时允许折行

验证:本地 e2e 1558/0、几何 428/0 + 660/0、v78 反向验证 11/0、
片段作用域体检 35/0、php -l 与 node --check 全过
This commit is contained in:
2026-10-22 12:40:14 +08:00
parent 036e208809
commit 40fd6c501c
13 changed files with 1216 additions and 178 deletions
+116
View File
@@ -915,6 +915,26 @@ switch ($r) {
$weather = $weatherLoc === null ? null
: Home::weather($familyId, $weatherLoc['lat'], $weatherLoc['lon'], $weatherLoc['place']);
/* 🔴 v78(用户 2026-10-22):「在全部任务旁边,放一个新建任务的功能,
其实就是把任务中的新建和编辑功能,搬过来,用弹出框来处理。」
⇒ 那张弹框(`templates/tasks/_add_modal.php`)需要看板 / 分类 / 名册三样。
⚠️ 只有「近期任务」卡真的摆在首页上、且当前账号可写时才准备:
卡片关掉就没有入口,不该让每个首页请求白跑三条查询。
⚠️ 弹框本身也在模板里按 `$tkAdd` 是否为空决定渲不渲染 —— 两处一个判据。 */
$tkAdd = null;
if (Auth::canWrite()) {
foreach ($homeCards as $hcRow) {
if ((string)($hcRow['k'] ?? '') === 'tasks') {
$tkAdd = [
'boards' => Task::boards($familyId),
'categories' => Task::categories($familyId),
'people' => Person::roster($familyId, $me, 'member', ''),
];
break;
}
}
}
View::render('home', [
'title' => '首页',
'family' => Family::find($familyId),
@@ -948,6 +968,8 @@ switch ($r) {
// v62 卡片体系
'homeCards' => $homeCards,
'cardData' => $cardData,
// v78:首页「新建任务」弹框的数据(卡片没摆/只读账号时为 null)
'tkAdd' => $tkAdd,
/* 🔴 v71c(用户 2026-10-21):「通知消息这个,也是首页的卡片,可以调整
顺序和大小。」
⇒ 通知**不再是**这里单独传的两个变量(`notifs` / `notifUnread`),
@@ -6327,6 +6349,51 @@ $want = array_keys($want);
redirect('tasks/settings');
break;
/**
* 首页「近期任务」的就地面板(v78 用户 2026-10-22)。
*
* 用户原话:「网页端,首页点击一个任务后,除了展示信息,应该有相应的功能,
* 可以修改状态:待办、进行中,可以挪动分组,可以拖动上传附件,可以添加评论,
* 添加子任务等。其目地,是为了让用户不离开首页,就能对任务进行简单的处理。」
*
* ⇒ 这个路由只吐**一块 HTML**(`templates/tasks/_panel.php`),由
* `entry_popup.js` 塞进条目浮层的正文里。动作全部走既有路由
* (status / move / create / comment_add / attach_add…),这里不新开写接口。
*
* ⚠️ 找不到就回 404 纯文本:调用方(浮层)拿到非 200 会退回"只有详情"的形态,
* 不会把一段错误页塞进面板。
*/
case 'tasks/panel':
$familyId = (int)Auth::familyId();
$me = (int)Auth::personId();
$t = Task::find($familyId, (int)($_GET['id'] ?? 0), $me);
if (!$t) {
http_response_code(404);
header('Content-Type: text/plain; charset=utf-8');
echo '找不到这项任务。';
exit;
}
$people = Person::roster($familyId, $me, 'member', '');
$names = [];
foreach ($people as $p) {
$names[] = person_name($p);
$nm = trim((string)($p['name'] ?? ''));
if ($nm !== '') {
$names[] = $nm;
}
}
header('Content-Type: text/html; charset=utf-8');
echo View::renderPartial('tasks/_panel', [
't' => $t,
'people' => $people,
'names' => array_values(array_unique(array_filter($names))),
'boards' => Task::boards($familyId),
'categories' => Task::categories($familyId),
'canWrite' => Auth::canWrite() && Task::canWrite($t, $me),
'meId' => $me,
]);
exit;
case 'tasks/show':
$familyId = (int)Auth::familyId();
$me = (int)Auth::personId();
@@ -6399,6 +6466,12 @@ $want = array_keys($want);
}
$title = trim((string)($_POST['title'] ?? ''));
if ($title === '') {
/* 🔴 v78:面板里"加子任务"是一条 AJAX 动作 —— 失败也得回面板,
否则用户被跳到看板(人在首页,却被扔去了另一个模块)。 */
if (task_panel_wanted()) {
$panelId = (int)($_POST['parent_id'] ?? 0) ?: (int)($_POST['id'] ?? 0);
task_panel_json($panelId, '任务得写清楚要做什么。', false);
}
flash('err', '任务得写清楚要做什么。');
redirect('tasks');
}
@@ -6447,6 +6520,18 @@ $want = array_keys($want);
if (!$isEdit && (string)($_POST['back'] ?? '') === 'tasks') {
redirect('tasks');
}
/* 🔴 v78:首页那张「新建任务」弹框提交时带 `back=home` —— 回首页,
卡片上立刻能看到刚建的这条(用户是"不离开首页就把事办了")。 */
if (!$isEdit && (string)($_POST['back'] ?? '') === 'home') {
redirect('home');
}
/* 🔴 v78:面板里"加子任务"—— 回**父任务**的面板。
⚠️ 不能回子任务自己(`$tid`):那等于把用户从"这件事"弹到"其中一小步",
而且面板里子任务不挂子任务,回过去看到的是一块没有被加进去的界面。 */
if (!$isEdit && task_panel_wanted()) {
$panelId = (int)($_POST['parent_id'] ?? 0) > 0 ? (int)$_POST['parent_id'] : $tid;
task_panel_json($panelId, '子任务已加上。');
}
redirect('tasks/show', ['id' => $tid]);
break;
@@ -6469,6 +6554,12 @@ $want = array_keys($want);
// (状态那条保留给任务详情页的"标记完成"和子任务用)。
if (array_key_exists('board', $_POST)) {
Task::moveToBoard($familyId, $id, (int)$_POST['board'], $before, $me);
/* 🔴 v78:面板里"挪动分组"(换看板)走这里 —— 回新面板,别跳走。
⚠️ 提示语要说出**挪到哪儿**:看板名在服务端取(前端那排 chip 的文字
可能被用户改过名,用 POST 回来的 id 现查一次才是真的)。 */
if (task_panel_wanted()) {
task_panel_json($id, '已挪到「' . Task::boardName($familyId, (int)$_POST['board']) . '」。');
}
redirect('tasks');
break;
}
@@ -6524,6 +6615,13 @@ $want = array_keys($want);
// v24 第 25 批:勾复选框 / 点状态徽标都走这条 ——
// 带 X-Requested-With 时回 JSON(前端就地改那张卡,不刷页);
// 普通表单提交照旧 302(没有 JS 时的兜底)。
// 🔴 v78:首页任务面板(`ajax_panel=1`)要的是**整块新面板**,
// 它比"只回一个状态"多带了子任务进度、看板选中态等一整页的一致性。
// ⚠️ 面板里勾的是**子任务**,重画时要用 `panel_of`(父任务)而不是子任务自己。
if (task_panel_wanted()) {
$panelId = (int)($_POST['panel_of'] ?? 0);
task_panel_json($panelId > 0 ? $panelId : $id, $msg);
}
if (($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '') === 'XMLHttpRequest') {
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
@@ -6564,8 +6662,14 @@ $want = array_keys($want);
$cid = Task::addComment($id, $me, (string)($_POST['content'] ?? ''));
if ($cid > 0) {
attach_save_files($familyId, 'task_comment', $cid, 'attach', $me);
if (task_panel_wanted()) {
task_panel_json($id, '留言已发表。');
}
flash('ok', '留言已发表。');
} else {
if (task_panel_wanted()) {
task_panel_json($id, '留言是空的。', false);
}
flash('err', '留言是空的。');
}
redirect('tasks/show', ['id' => $id]);
@@ -6578,6 +6682,9 @@ $want = array_keys($want);
csrf_check();
$taskId = (int)($_POST['task_id'] ?? 0);
Task::deleteComment((int)($_POST['id'] ?? 0), $me); // 内部只删自己的
if (task_panel_wanted()) {
task_panel_json($taskId, '留言已删除。');
}
flash('ok', '留言已删除。');
redirect('tasks/show', ['id' => $taskId]);
break;
@@ -6594,6 +6701,12 @@ $want = array_keys($want);
redirect('tasks');
}
$warns = attach_save_files($familyId, Task::ATTACH, $id, 'attach', $me);
/* 🔴 v78:面板里"拖动上传"是**一个文件一条请求**(拖几张就是几次),
所以这里每次都要把最新面板回过去 —— 缩略图墙与"附件 n"随之长出来。 */
if (task_panel_wanted()) {
task_panel_json($id, $warns ? '有附件没有保存:' . implode(';', $warns) : '附件已上传。',
!$warns);
}
flash($warns ? 'err' : 'ok', $warns ? '有附件没有保存:' . implode(';', $warns) : '附件已上传。');
redirect('tasks/show', ['id' => $id]);
break;
@@ -6613,6 +6726,9 @@ $want = array_keys($want);
redirect('tasks');
}
\App\Attachment::remove($aid);
if (task_panel_wanted()) {
task_panel_json($tid, '附件已删除。');
}
flash('ok', '附件已删除。');
redirect('tasks/show', ['id' => $tid]);
break;