v26 第 26 批(5 条):财务多选按集合取图 / 首页全天置顶与红线定位 / 任务复选框去提示与看板颜色 / 导航搜索引擎切换与站点复用 / 详情页去复选框
- 财务:勾选多个标签时上面那张「总金额」图按**选中集合**统计(新增 ledger/total_chart 路由 + 单一容器 data-ck-total-host,按集合现取并缓存; Ledger::timeline 第二参数放宽为 string|array) - 首页日程:全天日程移出滚动容器固定置顶;进场把当前时刻红线居中 (scrollTop 计算,不用 scrollIntoView),触发条件不绑 $showNow - 任务:复选框去除 title 提示;看板可设颜色(Task::boardColor/updateBoard), 首页任务行用该颜色显示所属看板 - 导航:展示页补回 id=engine-tabs(切换搜索引擎无效的根因);补内置引擎; 新增站点输 URL 可复用他人已加过的站点(sharedByUrl/copyIconFile/adoptSite) - 任务详情页页头去掉复选框(已有「标记完成」按钮)
This commit is contained in:
+24
-4
@@ -465,20 +465,40 @@ final class Ledger
|
||||
* 取值规则(每个时点 t):账目在 t 之后才创建 → **不计入**(那时它还不存在);
|
||||
* 在 t 之前盘点过 → 取最后一次盘点的值;从未盘点过 → 取它现在的值(一直没变过)。
|
||||
*
|
||||
* @param string $kind v24 第 23 批 用户第 2 条:**只看某一类别**时传它的 key
|
||||
* @param string|array $kind v24 第 23 批 用户第 2 条:**只看某一类别**时传它的 key
|
||||
* ('' = 整本账)。只影响"哪些账目参与累加",口径一字不改 ——
|
||||
* 所以"存款"那条线与"只记了存款这一笔的账本"完全等价。
|
||||
* 🔴 v24 第 26 批 用户第 1 条:多选之后传**类别集合**
|
||||
* (`['deposit','insurance']`)—— 语义同样是"只让这些类别的账目参与累加"。
|
||||
* ⚠️ 保持接受**字符串**(老调用点:只选了一类时仍然传字符串,见 index.php
|
||||
* 的 kindLines 预渲染),在下面统一归一成数组 ——
|
||||
* 两套参数并存是最容易漏改的那种接口。
|
||||
* @return array<int,array{date:string,net:int,asset:int,debt:int}> 空账本返回 []
|
||||
*/
|
||||
public static function timeline(int $ledgerId, string $kind = ''): array
|
||||
public static function timeline(int $ledgerId, $kind = ''): array
|
||||
{
|
||||
// 归一:'' / null → 空数组(= 全部);'deposit' → ['deposit'];数组原样
|
||||
$kinds = [];
|
||||
if (is_array($kind)) {
|
||||
foreach ($kind as $k) {
|
||||
$k = trim((string)$k);
|
||||
if ($k !== '') {
|
||||
$kinds[$k] = true;
|
||||
}
|
||||
}
|
||||
$kinds = array_keys($kinds);
|
||||
} elseif (is_string($kind) && trim($kind) !== '') {
|
||||
$kinds = [trim($kind)];
|
||||
}
|
||||
|
||||
$entries = self::entries($ledgerId);
|
||||
if ($kind !== '') {
|
||||
if ($kinds) {
|
||||
// ⚠️ 必须在**取时点之前**过滤(而不是算完再筛):
|
||||
// 时点集合里有"每条账目的创建日",包含被筛掉那些账目的创建日 →
|
||||
// 图右端就会凭空多出几个"其实什么都没变"的时点。
|
||||
// ⚠️ 多选时是**白名单**(在集合里才留);集合为空 = 不过滤 = 整本账。
|
||||
$entries = array_values(array_filter(
|
||||
$entries, static fn($e): bool => (string)$e['kind'] === $kind));
|
||||
$entries, static fn($e): bool => in_array((string)$e['kind'], $kinds, true)));
|
||||
}
|
||||
if (!$entries) {
|
||||
return [];
|
||||
|
||||
+194
-3
@@ -280,6 +280,144 @@ final class Nav
|
||||
return $n !== '' ? $n : self::domainOf((string)($s['url'] ?? ''));
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 跨用户复用同一个站点(v24 第 26 批 用户第 4 条后半)
|
||||
//
|
||||
// 用户原话:「导航,每个人添加新的导航,输入 url 之后,如果其他用户添加了,
|
||||
// 他可以直接复用,复用的时候,自定义图标和简称,他可以自己再设置,
|
||||
// 设置好以后就固定了。」
|
||||
//
|
||||
// 三条语义,逐条落成代码:
|
||||
// ① **输入 URL 之后**才知道能不能复用 → 需要一个"按 URL 查"的接口
|
||||
// (`sharedByUrl()`),供前端在输入框失焦时问一次。
|
||||
// ② "他可以直接复用" → 复用的是一个**模板**:把别人那条的
|
||||
// 名字 / 图标 / 颜色搬过来当预填,**不是**共享同一条记录
|
||||
// (共享一条的话,A 改简称会改到 B 的导航上 —— 那谁也说不清是谁的)。
|
||||
// ③ "设置好以后就固定了" → 复用来的图标必须标 `icon_custom = 1`
|
||||
// (见下面 `adoptSite()` 的说明:否则下次「全部更新」会把它冲掉)。
|
||||
//
|
||||
// ⚠️ 隐私边界:这里**只暴露"这个 URL 有人加过"以及可复用的展示信息**
|
||||
// (名字 / 简称 / 图标文件名),**不暴露是谁加的**、也不暴露他还有哪些站。
|
||||
// 导航本身就是"要给别人看的"(展示页公开),所以这几个字段不算隐私;
|
||||
// 但"谁加过什么"是另一回事,不查也不给。
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* 按 URL 找**别人**已经加过的同款站点(用来做"可复用"提示与预填)。
|
||||
*
|
||||
* @return array<int,array> 最多返回几条(同一 URL 可能被多个人加过,去重后取前几个)
|
||||
*/
|
||||
public static function sharedByUrl(int $personId, string $url, int $limit = 6): array
|
||||
{
|
||||
$norm = self::normUrl($url);
|
||||
if ($norm === '') {
|
||||
return [];
|
||||
}
|
||||
// ⚠️ 用**归一化后的 URL** 比:用户手输时 https / 末尾斜杠 / 大小写都可能不同,
|
||||
// 直接比原串的话"同一个站点"会认不出来(这正是复用的价值所在)。
|
||||
// 库里存的也是归一化后的(saveSite 里就走 normUrl),所以等价比较成立。
|
||||
$st = Database::get()->prepare(
|
||||
'SELECT url, name, short_name, icon, icon_custom
|
||||
FROM nav_sites
|
||||
WHERE person_id <> ? AND url = ?
|
||||
ORDER BY (short_name <> "") DESC, id ASC
|
||||
LIMIT ' . max(1, min(20, $limit))
|
||||
);
|
||||
$st->execute([$personId, $norm]);
|
||||
$out = [];
|
||||
$seen = [];
|
||||
foreach ($st->fetchAll() as $r) {
|
||||
// 同一 URL 多人加过 → 按"显示名 + 图标"去重(对用户来说它们就是同一条)
|
||||
$key = self::siteLabel($r) . '|' . (string)$r['icon'];
|
||||
if (isset($seen[$key])) {
|
||||
continue;
|
||||
}
|
||||
$seen[$key] = true;
|
||||
$out[] = $r;
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 把别人那条站点的展示信息**搬到自己这条**上(复用)。
|
||||
*
|
||||
* 与 `saveSite()` 分开写而不是给它加参数:`saveSite` 是"用户手填"那条路,
|
||||
* 这里是"照抄别人"那条路 —— 两者的**校验重点不同**(前者校验输入合法性,
|
||||
* 后者校验"那个图标文件还在不在")。混在一个函数里会让两条路的判断互相干扰。
|
||||
*
|
||||
* @param array $from sharedByUrl() 返回的一条
|
||||
* @param int $siteId 自己站点表里那条新建记录的 id
|
||||
*/
|
||||
public static function adoptSite(int $personId, int $siteId, array $from, string $name, string $short): bool
|
||||
{
|
||||
$db = Database::get();
|
||||
$row = self::site($personId, $siteId);
|
||||
if (!$row) {
|
||||
return false;
|
||||
}
|
||||
$name = trim($name);
|
||||
$short = trim($short);
|
||||
if ($name === '' && $short === '') {
|
||||
// 用户什么都没填 → 用别人的显示名当兜底(否则这条站没有名字可显示)
|
||||
$name = self::siteLabel($from);
|
||||
}
|
||||
|
||||
$icon = '';
|
||||
$isCus = 0;
|
||||
$srcIcon = trim((string)($from['icon'] ?? ''));
|
||||
if ($srcIcon !== '') {
|
||||
/* 🔴 "设置好以后就固定了" 这句话的落点就在这里。
|
||||
*
|
||||
* 复用的图标是**别人抓的**(或别人自定义的)。搬过来时必须标
|
||||
* `icon_custom = 1`,否则「全部更新」会按本站的抓取规则重新抓一遍
|
||||
* —— 那时抓到的可能是另一个图标,用户会看到"我明明选好了,怎么变了"。
|
||||
* 标成自定义 = 从此不再自动改它,与用户自己上传图标同等待遇。
|
||||
* ⚠️ 文件是**复制**一份到本站点名下(不是引用别人那个):
|
||||
* 否则别人删站 / 更新图标会连带影响你的展示页。
|
||||
*/
|
||||
$copied = self::copyIconFile($srcIcon);
|
||||
if ($copied !== '') {
|
||||
$icon = $copied;
|
||||
$isCus = 1;
|
||||
}
|
||||
}
|
||||
$db->prepare(
|
||||
'UPDATE nav_sites SET name = ?, short_name = ?, icon = ?, icon_custom = ?,
|
||||
updated_at = ? WHERE id = ? AND person_id = ?'
|
||||
)->execute([$name, $short, $icon, $isCus, date('Y-m-d H:i:s'), $siteId, $personId]);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 把图标文件复制一份、返回新文件名(失败返回 '')。
|
||||
*
|
||||
* ⚠️ 只认 `ICON_DIR` 下的**纯文件名**(与 iconUrl/deleteIconFile 同一套判据):
|
||||
* 带斜杠或 `..` 一律拒绝 —— 否则"复用"会变成一个任意文件读取的口子。
|
||||
*/
|
||||
public static function copyIconFile(string $file): string
|
||||
{
|
||||
$file = trim($file);
|
||||
if ($file === '' || strpos($file, '/') !== false || strpos($file, '\\') !== false
|
||||
|| strpos($file, '..') !== false) {
|
||||
return '';
|
||||
}
|
||||
$dir = FB_ROOT . '/' . self::ICON_DIR;
|
||||
$abs = $dir . '/' . $file;
|
||||
if (!is_file($abs)) {
|
||||
return ''; // 源文件不在了(别人删过)→ 不复制,退化成"没有图标"
|
||||
}
|
||||
// 新文件名:保持同样的扩展名(图标出口按扩展名给 Content-Type)
|
||||
$ext = strtolower((string)pathinfo($file, PATHINFO_EXTENSION));
|
||||
if (!preg_match('/^[a-z0-9]{1,5}$/', $ext)) {
|
||||
$ext = 'png';
|
||||
}
|
||||
$new = bin2hex(random_bytes(8)) . '.' . $ext;
|
||||
if (!is_dir($dir) && !@mkdir($dir, 0755, true)) {
|
||||
return '';
|
||||
}
|
||||
return @copy($abs, $dir . '/' . $new) ? $new : '';
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 搜索引擎
|
||||
// ================================================================
|
||||
@@ -823,15 +961,68 @@ final class Nav
|
||||
/** 默认搜索引擎(照搬 SimpleNav 的 seed_engines) */
|
||||
public const ENGINE_SEED = [
|
||||
// 名称, slug, 搜索地址, 参数, 联想地址, 联想类型, 颜色, 是否启用
|
||||
//
|
||||
// 🔴 v24 第 26 批 用户第 4 条(后半):「**可以内置几个,让用户选择**」。
|
||||
// 原来只启用必应/百度两档,用户根本看不出"可选"这件事(列表里另外 4 个
|
||||
// 是以"未启用"状态躺着的,展示页的标签条只渲染启用的)。
|
||||
// ⇒ 把**实测可用的三档**都默认启用:必应 / 百度 / 360。
|
||||
// 谷歌 / DuckGo 保持未启用 —— 它们在**国内网络下连不上**
|
||||
// (见 nav_suggest.php 头部那段线上实测:连接超时),
|
||||
// 默认启用只会让用户点了发现"搜不动",比不提供更差。
|
||||
// 搜狗保持未启用:它的联想域名已不解析(实测 0 字节)。
|
||||
// ⚠️ 这三条实测结论来自 nav_suggest.php 的注释,不要凭印象改动。
|
||||
['必应', 'bing', 'https://cn.bing.com/search', 'q',
|
||||
'https://api.bing.com/osjson.aspx?query={q}', 'bing_osjson', '#60A504', 1],
|
||||
['百度', 'baidu', 'https://www.baidu.com/s', 'wd',
|
||||
'https://www.baidu.com/sugrec?prod=pc&wd={q}', 'baidu_sugrec', '#2100E0', 1],
|
||||
['谷歌', 'google', 'https://www.google.com/search', 'q', '', '', '#3B83FA', 0],
|
||||
['搜狗', 'sogou', 'https://www.sogou.com/web', 'query', '', '', '#FE6B14', 0],
|
||||
['360', 'so360', 'https://www.so.com/s', 'q',
|
||||
'https://sug.so.360.cn/suggest?encodein=utf-8&encodeout=utf-8&format=json&word={q}',
|
||||
'so360', '#1BB04A', 0],
|
||||
'so360', '#1BB04A', 1],
|
||||
['谷歌', 'google', 'https://www.google.com/search', 'q', '', '', '#3B83FA', 0],
|
||||
['搜狗', 'sogou', 'https://www.sogou.com/web', 'query', '', '', '#FE6B14', 0],
|
||||
['DuckGo', 'ddg', 'https://duckduckgo.com/', 'q', '', '', '#DE5833', 0],
|
||||
];
|
||||
|
||||
/**
|
||||
* 把**缺失的内置引擎**补给这个人(v24 第 26 批 用户第 4 条)。
|
||||
*
|
||||
* 为什么需要它:`ensureSeed()` 只在"一条引擎都没有"时才灌种子
|
||||
* (它的判据是 `nav_groups` 是否为空)—— 于是**老用户**(在更新前就建过导航的人)
|
||||
* 永远拿不到后加的内置引擎:他们的库里只有当年那几条。
|
||||
* 用户说"内置几个让用户选择",对老用户就等于"一个都没有"。
|
||||
*
|
||||
* ⚠️ 按 **slug** 判重(不是 name):用户可能把"百度"改名成"度娘",
|
||||
* 若按名字判重会再灌一条重复的 baidu。
|
||||
* ⚠️ 只补**缺的**、绝不改动已有的(用户可能改过 URL / 颜色 / 启用状态,
|
||||
* 覆盖回去等于"我帮你改好了" —— 那种"好意"最难查)。
|
||||
*
|
||||
* @return int 新补了几条
|
||||
*/
|
||||
public static function addMissingEngines(int $personId): int
|
||||
{
|
||||
$db = Database::get();
|
||||
$st = $db->prepare('SELECT slug FROM nav_engines WHERE person_id = ?');
|
||||
$st->execute([$personId]);
|
||||
$have = [];
|
||||
foreach ($st->fetchAll() as $r) {
|
||||
$have[(string)$r['slug']] = true;
|
||||
}
|
||||
$sort = (int)$db->query('SELECT COALESCE(MAX(sort_order),0) FROM nav_engines WHERE person_id = '
|
||||
. (int)$personId)->fetchColumn();
|
||||
$ins = $db->prepare(
|
||||
'INSERT INTO nav_engines (person_id, name, slug, search_url, param, suggest_url,
|
||||
suggest_type, color, is_active, sort_order) VALUES (?,?,?,?,?,?,?,?,?,?)');
|
||||
$n = 0;
|
||||
foreach (self::ENGINE_SEED as $e) {
|
||||
if (isset($have[(string)$e[1]])) {
|
||||
continue;
|
||||
}
|
||||
$sort += self::SORT_STEP;
|
||||
// ⚠️ 补进来的按种子的 `is_active` 走(必应/百度/360 = 1)。
|
||||
// "老用户库里已经有的那几条"保持原状 —— 不在这里动它们。
|
||||
$ins->execute([$personId, $e[0], $e[1], $e[2], $e[3], $e[4], $e[5], $e[6], $e[7], $sort]);
|
||||
$n++;
|
||||
}
|
||||
return $n;
|
||||
}
|
||||
}
|
||||
+14
-1
@@ -104,7 +104,7 @@ final class Schema
|
||||
* ⚠️ 它**不再**作为"要不要跑升级"的判据 —— 见 fingerprint() 的说明。
|
||||
* 它仍然要手工维护,因为它给人看(排查时一眼知道代码到哪一版了)。
|
||||
*/
|
||||
public const VERSION = '25';
|
||||
public const VERSION = '26';
|
||||
|
||||
/**
|
||||
* 「源码指纹」:拿**本文件自身的内容**算一个哈希,作为"要不要跑升级"的判据。
|
||||
@@ -1459,6 +1459,19 @@ SQL);
|
||||
"ADD COLUMN `board_id` INT UNSIGNED DEFAULT NULL COMMENT '属于哪个看板(用户自建;NULL=默认看板)' AFTER `family_id`");
|
||||
self::addColumn($db, 'tasks', 'category_id',
|
||||
"ADD COLUMN `category_id` INT UNSIGNED DEFAULT NULL COMMENT '属于哪个分类(用户自建;NULL=未分类)' AFTER `board_id`");
|
||||
/* 🔴 v24 第 26 批 用户第 3 条:看板可以选颜色。
|
||||
*
|
||||
* 用户原话:「任务在设置看板的时候,可以选择颜色,这样在首页列表中显示的时候,
|
||||
* 就可以更加直观的看到他属于哪个看板。」
|
||||
*
|
||||
* ⚠️ 默认值**留空**而不是给个具体色:空 = "没选过颜色",
|
||||
* 由渲染层按看板名**稳定地**算一个色(同名永远同色,见 Task::boardColor())。
|
||||
* 若这里给死 `#2F6BF7`,所有没设过的看板会全变成同一个蓝 ——
|
||||
* 那反而看不出"属于哪个看板"(正是这条需求要解决的问题)。
|
||||
* ⚠️ `addColumn` 自带"列不存在才加",所以老库/新装都走这一条,不必写两遍。
|
||||
* 列长 7 = `#RRGGBB`;不用枚举(用户可选任意色),合法性在入库时校验。 */
|
||||
self::addColumn($db, 'task_boards', 'color',
|
||||
"ADD COLUMN `color` CHAR(7) DEFAULT NULL COMMENT '看板色 #RRGGBB;NULL=按名字派生' AFTER `position`");
|
||||
// 每个家庭至少有一个看板,否则"看板"这个概念在界面上就是空的
|
||||
self::tryStep('给每个家庭补一个默认看板', static function () use ($db) {
|
||||
foreach ($db->query('SELECT id FROM families')->fetchAll() as $fam) {
|
||||
|
||||
+77
-6
@@ -137,6 +137,79 @@ final class Task
|
||||
return $v === false ? '' : (string)$v;
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// 看板颜色(v24 第 26 批 用户第 3 条)
|
||||
//
|
||||
// 用户原话:「任务在设置看板的时候,可以选择颜色,这样在首页列表中显示的时候,
|
||||
// 就可以更加直观的看到他属于哪个看板。」
|
||||
//
|
||||
// 设计要点(两条,都是为了"一眼能认出来"):
|
||||
// ① 用户没选过颜色时**按看板名派生**一个稳定色 —— 同名永远同色。
|
||||
// 若改成"按 id 派生",看板一改名颜色就跳,用户反而记不住;
|
||||
// 若统一给一个默认色,所有看板长得一样,就完全失去这条需求的意义。
|
||||
// ② 派生只从**固定调色板**里挑(跟名册头像色块同一套观感),
|
||||
// 不做 HSL 随机 —— 随机色会造出刺眼/低对比的颜色,白字压不住。
|
||||
// ====================================================================
|
||||
|
||||
/** 看板可选色的调色板(10 个,覆盖暖冷中性;都够深,能压白字) */
|
||||
public const BOARD_COLORS = [
|
||||
'#C0533F', '#B87333', '#8A7B3F', '#5E9E4E', '#3F8F7A',
|
||||
'#3E7A9E', '#5B6DC0', '#8B5CF6', '#B0508A', '#7A6A5E',
|
||||
];
|
||||
|
||||
/**
|
||||
* 取一个看板该用什么颜色。
|
||||
*
|
||||
* @param string|null $color 库里存的(可能为 NULL / 空 / 非法值)
|
||||
* @param string $name 看板名(没存颜色时按它派生)
|
||||
* @return string '#RRGGBB'
|
||||
*/
|
||||
public static function boardColor(?string $color, string $name = ''): string
|
||||
{
|
||||
$c = strtolower(trim((string)$color));
|
||||
if (preg_match('/^#[0-9a-f]{6}$/', $c)) {
|
||||
return $c;
|
||||
}
|
||||
// 按名字派生:同一个名字永远同一个色(改 id 不影响,用户能记住)
|
||||
$h = 0;
|
||||
foreach (preg_split('//u', $name, -1, PREG_SPLIT_NO_EMPTY) ?: [] as $ch) {
|
||||
// 用码点而不是字节:中文名在不同编码下字节不同,码点是稳的
|
||||
$h = ($h * 31 + (int)(mb_ord($ch, 'UTF-8') ?: 0)) % 1000000007;
|
||||
}
|
||||
return self::BOARD_COLORS[abs($h) % count(self::BOARD_COLORS)];
|
||||
}
|
||||
|
||||
/** 看板 id → 颜色(board_name 一起取,省一次查询;查不到给灰) */
|
||||
public static function boardColorById(int $familyId, ?int $boardId): string
|
||||
{
|
||||
if (!$boardId) {
|
||||
return self::boardColor(null, '默认看板');
|
||||
}
|
||||
$st = Database::get()->prepare('SELECT name, color FROM task_boards WHERE id = ? AND family_id = ?');
|
||||
$st->execute([$boardId, $familyId]);
|
||||
$r = $st->fetch();
|
||||
if (!$r) {
|
||||
return self::boardColor(null, '');
|
||||
}
|
||||
return self::boardColor($r['color'] ?? null, (string)$r['name']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 改看板名 + 颜色(设置页那一行表单)。
|
||||
*
|
||||
* ⚠️ 颜色与名字**同一个动作**:用户改色时不该被迫把名字也重打一遍
|
||||
* (表单里两个都在、一起提交,这里是它们共同的落点)。
|
||||
* ⚠️ 颜色合法性在这里收口(只认 `#RRGGBB`),空串 = 清除自定义、回到按名派生。
|
||||
*/
|
||||
public static function updateBoard(int $familyId, int $id, string $name, string $color): void
|
||||
{
|
||||
$db = Database::get();
|
||||
$c = strtolower(trim($color));
|
||||
$c = preg_match('/^#[0-9a-f]{6}$/', $c) ? $c : null;
|
||||
$db->prepare('UPDATE task_boards SET name = ?, color = ? WHERE id = ? AND family_id = ?')
|
||||
->execute([self::shortName($name), $c, $id, $familyId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 这个家庭至少有一个看板(升级时给老家庭补,新家庭在建号时补)。
|
||||
* @return int 默认看板的 id
|
||||
@@ -164,11 +237,9 @@ final class Task
|
||||
return (int)$db->lastInsertId();
|
||||
}
|
||||
|
||||
public static function renameBoard(int $familyId, int $id, string $name): void
|
||||
{
|
||||
Database::get()->prepare('UPDATE task_boards SET name = ? WHERE id = ? AND family_id = ?')
|
||||
->execute([self::shortName($name), $id, $familyId]);
|
||||
}
|
||||
/* ⚠️ 原来的 `renameBoard()` 已在 v24 第 26 批**并入 `updateBoard()`**(用户第 3 条
|
||||
要给看板加颜色,两个字段同一个动作),这里不再保留 —— 留一个只改名字的方法,
|
||||
下一个改颜色的人会顺手调错它("改了色但名字被重置"这类事故就是这么来的)。 */
|
||||
|
||||
/**
|
||||
* 删看板:里头的任务**不删**,挪到默认看板上(删看板 ≠ 删任务,这个代价太突然)。
|
||||
@@ -428,7 +499,7 @@ final class Task
|
||||
'SELECT t.*, a.name AS assignee_name, a.avatar, a.avatar_color,
|
||||
na.nickname AS assignee_nick,
|
||||
c.name AS category_name,
|
||||
b.name AS board_name,
|
||||
b.name AS board_name, b.color AS board_color,
|
||||
(SELECT COUNT(*) FROM tasks k WHERE k.parent_id = t.id) AS child_total,
|
||||
(SELECT COUNT(*) FROM tasks k WHERE k.parent_id = t.id AND k.status = "done") AS child_done
|
||||
FROM tasks t
|
||||
|
||||
Reference in New Issue
Block a user