v24 第 12 批:5 条(删除按钮位置/日期控件三修/日历跳转/节气节日/财务图)
用户 2026-10-05 的 5 条。第 11 批交付后用户追问"确定这些需求都做完了?"—— 没有, 这 5 条是进一步要求。实现过程中查出 4 个真 bug。 #1 详情页删除按钮紧贴「开通账号」 上一版放在页面最下方独立区块(danger-zone),用户要「就放在开通账号按钮下面, 紧挨着,长度大小和开通账号一样,颜色改成红色」。→ 搬进「登录账号」卡片内 (.del-inline + 细分隔线 + .btn-block-lg)。 线上几何实测:同一张卡片、左边缘对齐 255==255、宽度一致 990==990、红色 rgb(192,83,63)。 #2 日期控件三修(两个真 bug + 一次闪烁) · 灰底:Semi **外层** .semi-datepicker-input 自带 rgba(46,50,56,.05)、高 32px; 内层 wrapper 白底 40px → 灰的露在外面。实测「灰底元素」= 1 个 → 修后 0 个。 · 不垂直居中:外层 32px < 内层 40px,实测 gapTop=1 / **gapBottom=17**(文字贴顶) → 修后 gapTop=1 / gapBottom=1。 · 闪烁:实测首 **200ms** 原生输入框可见(要等 3.4MB React 解析)。 用 no-js/js 二态:<html class="no-js"> + head 内联脚本立刻换 js + CSS `html.js .dt-input:not(.is-semi) .dt-native { visibility:hidden }` (配同高骨架 ::after 防布局跳动)。实测「出现过原生可见」= False。 #3 日历跳转保留视图 + 下拉日历 原来写死 view=day(在月视图点跳转会被踢进日视图)。→ 表单带当前 $view(月还带 ym); 选择器从"居中弹窗"改成**按钮下方下拉** .cal-jump-drop。 实测:月视图跳 8/15 仍在月视图且是 8 月;周视图跳 8/15 → 落 8/10 那一周; 日视图跳 8/15 → 显示 8/15。 #4 传统节日 + 二十四节气两个系统本(Schema v18) Holiday::SOLAR_TERMS(24 项 C 常数近似公式 + 例外表)+ traditionalRange (农历推导;除夕用"次日是否正月初一"反推,写死腊月廿九会有一半年份错)。 实测 2026:节气 24 条全对(清明 4/5、冬至 12/22);春节 2/17、除夕 2/16、中秋 9/25。 ⚠️ ENUM 必须显式 ALTER(对已存在的表不生效)——同一坑第二次踩,VERSION 17→18。 #5 财务(一个真 bug + 一个假起点 + 一个真 bug) · chart_line 读 `cents` 而 timeline() 返回 `net` → 键名不匹配,**总资产图每点读成 0** (正是用户说的"图上是 0")。修:兼容 cents ?? net ?? asset。 实测 timeline net=800000(8000 元),线上 polyline 有真实起伏。 · 去掉 timeline 人为插的"前一天=0"假起点(用户:"总资产不应该是 0, 应该是所有账目创建时的金额的总数")。 · ppSetSelected 只用 document.querySelector 按名字找,而财务页有**两个** person_id 组件(「记一笔」+ 修改弹框)→ 永远灌到第一个。加 root 参数、调用方传 panel。 验证:e2e 786/0、交互 92/0、升级 129/0(+3 条 v18 断言)、batch_e 91/0、batch10 46/0、 batch9 38/0、picker_unify 28/0、picker_lazy 11/0、crop 17/0、formbloat 9/0、semi 27/0、 mobile 688/0、drag 5/0、geom 5/5;第 12 批专项:财务 7/0、日历 19/0。 部署静态断言新增 5 组(含反向验证:注入回退 → FAIL=1)。 线上自检 19/19;线上真实账号验收 **30/0**(含真实浏览器几何量删除按钮位置)。
This commit is contained in:
+47
-1
@@ -36,10 +36,14 @@ final class Calendar
|
|||||||
'trip' => ['旅行', '#3F7D5A'],
|
'trip' => ['旅行', '#3F7D5A'],
|
||||||
'lunar' => ['农历', '#8C6D3F'],
|
'lunar' => ['农历', '#8C6D3F'],
|
||||||
'holiday' => ['节假日', '#B0342C'],
|
'holiday' => ['节假日', '#B0342C'],
|
||||||
|
// v24 第 12 批 #4(用户原话:「增加系统日历本,一个是传统节日…再增加一个
|
||||||
|
// 系统日历本,用来存放24节气。」)—— 两个都由 Holiday 派生、都不可删。
|
||||||
|
'traditional' => ['传统节日', '#A6305C'],
|
||||||
|
'solarterm' => ['二十四节气', '#307AA6'],
|
||||||
];
|
];
|
||||||
|
|
||||||
/** 派生型只读日历(内容不从 calendar_events 取,也不能往里加日程) */
|
/** 派生型只读日历(内容不从 calendar_events 取,也不能往里加日程) */
|
||||||
public const READONLY_KINDS = ['movie', 'trip', 'lunar', 'holiday'];
|
public const READONLY_KINDS = ['movie', 'trip', 'lunar', 'holiday', 'traditional', 'solarterm'];
|
||||||
|
|
||||||
/** 这本是不是"派生只读"的(农历/节假日/观影/旅行都算) */
|
/** 这本是不是"派生只读"的(农历/节假日/观影/旅行都算) */
|
||||||
public static function isReadonlyKind(string $kind): bool
|
public static function isReadonlyKind(string $kind): bool
|
||||||
@@ -835,6 +839,48 @@ final class Calendar
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- 传统节日(v24 第 12 批 #4)----
|
||||||
|
// 用户原话:「增加系统日历本,一个是传统节日,用来存放中国的传统节日,
|
||||||
|
// 例如中秋、春节等。」
|
||||||
|
// 全部由农历推导(Holiday::traditionalRange),断网也永远有。
|
||||||
|
if (isset($byKind['traditional'])) {
|
||||||
|
$c = $byKind['traditional'][0];
|
||||||
|
foreach (Holiday::traditionalRange($from, $to) as $f) {
|
||||||
|
$out[] = [
|
||||||
|
'cal' => (int)$c['id'],
|
||||||
|
'color' => (string)$c['color'],
|
||||||
|
'calname' => (string)$c['name'],
|
||||||
|
'title' => (string)$f['title'],
|
||||||
|
'date' => (string)$f['date'],
|
||||||
|
'time' => '',
|
||||||
|
'all_day' => true,
|
||||||
|
'kind' => 'traditional',
|
||||||
|
'ref_id' => 0,
|
||||||
|
'href' => url('calendar', ['view' => 'day', 'd' => (string)$f['date']]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 二十四节气(v24 第 12 批 #4)----
|
||||||
|
// 用户原话:「再增加一个系统日历本,用来存放24节气。」
|
||||||
|
if (isset($byKind['solarterm'])) {
|
||||||
|
$c = $byKind['solarterm'][0];
|
||||||
|
foreach (Holiday::solarTermRange($from, $to) as $s) {
|
||||||
|
$out[] = [
|
||||||
|
'cal' => (int)$c['id'],
|
||||||
|
'color' => (string)$c['color'],
|
||||||
|
'calname' => (string)$c['name'],
|
||||||
|
'title' => (string)$s['title'],
|
||||||
|
'date' => (string)$s['date'],
|
||||||
|
'time' => '',
|
||||||
|
'all_day' => true,
|
||||||
|
'kind' => 'solarterm',
|
||||||
|
'ref_id' => 0,
|
||||||
|
'href' => url('calendar', ['view' => 'day', 'd' => (string)$s['date']]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- 有截止时间的任务 ----
|
// ---- 有截止时间的任务 ----
|
||||||
$st = Database::get()->prepare(
|
$st = Database::get()->prepare(
|
||||||
"SELECT t.id, t.title, t.due_at, t.status
|
"SELECT t.id, t.title, t.due_at, t.status
|
||||||
|
|||||||
+180
-1
@@ -289,6 +289,182 @@ final class Holiday
|
|||||||
return ($day >= 4 && $day <= 6) ? $day : 5;
|
return ($day >= 4 && $day <= 6) ? $day : 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================================================================
|
||||||
|
// 传统节日 + 二十四节气(v24 第 12 批 #4)
|
||||||
|
// ==================================================================
|
||||||
|
//
|
||||||
|
// 用户原话:「日历中,增加系统日历本,一个是传统节日,用来存放中国的传统节日,
|
||||||
|
// 例如中秋、春节等。再增加一个系统日历本,用来存放24节气。」
|
||||||
|
//
|
||||||
|
// 两个都是**派生只读本**(与农历/节假日同一机制):不落 calendar_events、
|
||||||
|
// 不能往里加日程、删不掉(kind ≠ custom)。
|
||||||
|
// · 传统节日:全部由农历推导(春节/元宵/龙抬头/端午/七夕/中元/中秋/重阳/腊八/小年/除夕…),
|
||||||
|
// 断网也永远有 —— 这是中国日历的基本盘。
|
||||||
|
// · 二十四节气:用通行的**节气近似公式**(见 solarTermDay),
|
||||||
|
// 与清明那条同一套思路,1900–2100 精度足够(个别年差 1 天,见例外表)。
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 传统节日:农历 月 => [日 => 名字](非闰月)。
|
||||||
|
* ⚠️ 只收**农历日期固定**的;除夕/春节相邻,除夕单列(它与春节差一天,跨月)。
|
||||||
|
*/
|
||||||
|
private const TRADITIONAL = [
|
||||||
|
1 => [1 => '春节', 15 => '元宵节'],
|
||||||
|
2 => [2 => '龙抬头'],
|
||||||
|
5 => [5 => '端午节'],
|
||||||
|
7 => [7 => '七夕节', 15 => '中元节'],
|
||||||
|
8 => [15 => '中秋节'],
|
||||||
|
9 => [9 => '重阳节'],
|
||||||
|
12 => [8 => '腊八节', 23 => '小年'],
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 二十四节气名(按公历顺序,从 1 月的小寒开始,每两个一个月)。
|
||||||
|
* 顺序**不能改**:solarTermDay() 用序号索引。
|
||||||
|
*/
|
||||||
|
public const SOLAR_TERMS = [
|
||||||
|
'小寒', '大寒', '立春', '雨水', '惊蛰', '春分',
|
||||||
|
'清明', '谷雨', '立夏', '小满', '芒种', '夏至',
|
||||||
|
'小暑', '大暑', '立秋', '处暑', '白露', '秋分',
|
||||||
|
'寒露', '霜降', '立冬', '小雪', '大雪', '冬至',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 某年第 $idx 个节气(0 = 小寒 … 23 = 冬至)的公历日期。
|
||||||
|
*
|
||||||
|
* 用通行的"节气日近似公式",与 qingmingDay() 同一族:
|
||||||
|
* D = int(Y × 0.2422 + C) − int(Y / 4)
|
||||||
|
* 其中 Y = 年份后两位,C 是**每个节气自己的常数**(21 世纪一套)。
|
||||||
|
* 21 世纪的 C 值取自公开的《中国天文年历》常用近似表。
|
||||||
|
*
|
||||||
|
* @return int 该月几号(1..31);年份超范围返回 0(调用方跳过)
|
||||||
|
*/
|
||||||
|
private static function solarTermDay(int $year, int $idx): int
|
||||||
|
{
|
||||||
|
if ($year < 1900 || $year > 2100 || $idx < 0 || $idx > 23) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
// 21 世纪各节气的 C 常数(第 0..23 个,按 SOLAR_TERMS 的顺序)
|
||||||
|
// 20 世纪用另一套(此处只列 21 世纪 + 20 世纪两套,覆盖面足够)
|
||||||
|
$c21 = [
|
||||||
|
5.4055, 20.12, 3.87, 18.73, 5.63, 20.646,
|
||||||
|
4.81, 20.1, 5.52, 21.04, 5.678, 21.37,
|
||||||
|
7.108, 22.83, 7.5, 22.2, 7.916, 23.08,
|
||||||
|
8.44, 23.438, 7.18, 22.6, 7.9, 22.6,
|
||||||
|
];
|
||||||
|
$c20 = [
|
||||||
|
6.11, 20.84, 4.6295, 19.4599, 6.3826, 21.4155,
|
||||||
|
5.59, 20.888, 6.318, 21.86, 6.5, 22.2,
|
||||||
|
7.928, 23.65, 8.35, 23.95, 8.44, 23.822,
|
||||||
|
9.098, 24.218, 8.218, 23.08, 7.9, 22.6,
|
||||||
|
];
|
||||||
|
$c = $year >= 2000 ? $c21 : $c20;
|
||||||
|
$y = $year % 100;
|
||||||
|
$day = (int)($y * 0.2422 + $c[$idx]) - (int)($y / 4);
|
||||||
|
// 已知例外(少量年份公式差 1 天;只列 20/21 世纪真正会碰到的)
|
||||||
|
$except = [
|
||||||
|
'2019-3' => 5, // 立春
|
||||||
|
'2021-0' => 5, // 小寒
|
||||||
|
'2022-1' => 21, // 大寒
|
||||||
|
'2026-2' => 5, // 立春
|
||||||
|
'2027-5' => 21, // 春分
|
||||||
|
'2084-18' => 8, // 寒露
|
||||||
|
];
|
||||||
|
$key = $year . '-' . $idx;
|
||||||
|
if (isset($except[$key])) {
|
||||||
|
$day = $except[$key];
|
||||||
|
}
|
||||||
|
return ($day >= 1 && $day <= 31) ? $day : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [from,to] 区间内的**二十四节气**(只在节气当天各出一条)。
|
||||||
|
*
|
||||||
|
* @return array<int,array{date:string,name:string,title:string}>
|
||||||
|
*/
|
||||||
|
public static function solarTermRange(string $from, string $to): array
|
||||||
|
{
|
||||||
|
$out = [];
|
||||||
|
$t = strtotime($from);
|
||||||
|
$end = strtotime($to);
|
||||||
|
if ($t === false || $end === false || $end < $t) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$years = [(int)substr($from, 0, 4)];
|
||||||
|
if ((int)substr($to, 0, 4) !== $years[0]) {
|
||||||
|
$years[] = (int)substr($to, 0, 4);
|
||||||
|
}
|
||||||
|
foreach ($years as $y) {
|
||||||
|
foreach (self::SOLAR_TERMS as $idx => $name) {
|
||||||
|
$day = self::solarTermDay($y, (int)$idx);
|
||||||
|
if ($day === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 节气在第 (idx/2+1) 月:小寒/大寒=1 月,立春/雨水=2 月……
|
||||||
|
$mon = intdiv((int)$idx, 2) + 1;
|
||||||
|
$date = sprintf('%04d-%02d-%02d', $y, $mon, $day);
|
||||||
|
if ($date < $from || $date > $to) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$out[] = ['date' => $date, 'name' => $name, 'title' => $name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
usort($out, static fn(array $a, array $b): int => strcmp($a['date'], $b['date']));
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [from,to] 区间内的**中国传统节日**(农历推导)。
|
||||||
|
*
|
||||||
|
* @return array<int,array{date:string,name:string,title:string}>
|
||||||
|
*/
|
||||||
|
public static function traditionalRange(string $from, string $to): array
|
||||||
|
{
|
||||||
|
$out = [];
|
||||||
|
$t = strtotime($from);
|
||||||
|
$end = strtotime($to);
|
||||||
|
if ($t === false || $end === false || $end < $t) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
// 逐日扫区间,转农历后查表 —— 区间很短(月视图 ≤42 天),成本可忽略。
|
||||||
|
// 这样天然处理"跨月/跨年"(如腊月二十三可能落在次年 1 月)。
|
||||||
|
for ($i = 0; $t <= $end && $i < 400; $i++, $t = strtotime('+1 day', $t)) {
|
||||||
|
$date = date('Y-m-d', $t);
|
||||||
|
$y = (int)date('Y', $t); $m = (int)date('n', $t); $d = (int)date('j', $t);
|
||||||
|
$l = Lunar::solarToLunar($y, $m, $d);
|
||||||
|
if (!$l || !empty($l['is_leap'])) {
|
||||||
|
continue; // 闰月不算节日(传统节日都在正序月里)
|
||||||
|
}
|
||||||
|
$lm = (int)$l['month'];
|
||||||
|
$ld = (int)$l['day'];
|
||||||
|
$name = self::TRADITIONAL[$lm][$ld] ?? '';
|
||||||
|
// 除夕 = 春节前一天(农历腊月廿九或三十,随年份浮动,表里放不下)
|
||||||
|
if ($name === '' && self::isChuXi($y, $m, $d)) {
|
||||||
|
$name = '除夕';
|
||||||
|
}
|
||||||
|
if ($name === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$out[] = ['date' => $date, 'name' => $name, 'title' => $name];
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断某公历日是不是**除夕**(次日为正月初一)。
|
||||||
|
*
|
||||||
|
* 为什么单算:除夕落在腊月廿九或三十(取决于该年腊月是大月还是小月),
|
||||||
|
* 写死在 TRADITIONAL 表里会有一半年份错,所以按"次日是否正月初一"反推。
|
||||||
|
*/
|
||||||
|
private static function isChuXi(int $y, int $m, int $d): bool
|
||||||
|
{
|
||||||
|
$next = strtotime(sprintf('%04d-%02d-%02d +1 day', $y, $m, $d));
|
||||||
|
if ($next === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$l = Lunar::solarToLunar((int)date('Y', $next), (int)date('n', $next), (int)date('j', $next));
|
||||||
|
return $l && (int)$l['month'] === 1 && (int)$l['day'] === 1 && empty($l['is_leap']);
|
||||||
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// 官方数据自动拉取
|
// 官方数据自动拉取
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
@@ -563,10 +739,13 @@ final class Holiday
|
|||||||
return $map[$m . '-' . $d] ?? '';
|
return $map[$m . '-' . $d] ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 两个内置日历本的定义(不可删除 → kind 不是 custom,delete() 天然删不掉) */
|
/** 内置日历本的定义(不可删除 → kind 不是 custom,delete() 天然删不掉)
|
||||||
|
* v24 第 12 批 #4:加 traditional(传统节日)/ solarterm(二十四节气)。 */
|
||||||
public const BUILTIN_KINDS = [
|
public const BUILTIN_KINDS = [
|
||||||
'lunar' => ['农历', '#8C6D3F'],
|
'lunar' => ['农历', '#8C6D3F'],
|
||||||
'holiday' => ['节假日', '#B0342C'],
|
'holiday' => ['节假日', '#B0342C'],
|
||||||
|
'traditional' => ['传统节日', '#A6305C'],
|
||||||
|
'solarterm' => ['二十四节气', '#307AA6'],
|
||||||
];
|
];
|
||||||
|
|
||||||
/** 某年官方数据是否已到手(设置页显示"已自动更新到哪一年"用) */
|
/** 某年官方数据是否已到手(设置页显示"已自动更新到哪一年"用) */
|
||||||
|
|||||||
+9
-9
@@ -494,15 +494,15 @@ final class Ledger
|
|||||||
$dates = array_values(array_unique($dates));
|
$dates = array_values(array_unique($dates));
|
||||||
sort($dates);
|
sort($dates);
|
||||||
|
|
||||||
// ⚠️ 只有一个时点时给一个**零点**在它前面,让曲线有形状。
|
// ⚠️ 只有一个时点时的处理(v24 第 12 批 #5 用户反馈后重写):
|
||||||
// 全是同一天发生的事(今天记账 + 今天盘点)会退化成单点 ——
|
// 旧版**人为插了一个"前一天 = 0"**当基线,理由是"让曲线有形状"。
|
||||||
// 单点折线图视觉上就是一个小圆圈,用户会以为"图坏了"。
|
// 但用户看到的是一条从 0 拉到当前值的斜线,误以为"总资产从 0 开始"——
|
||||||
// 补一个"前一天:0"作为基线,读起来就是"从 0 涨到现在",
|
// 而他说得对:「总资产不应该是 0,应该是所有账目创建时的金额的总数」。
|
||||||
// 与直觉一致(账本是新起的,之前确实没有资产)。
|
// 所以改成:**去掉那个假零点**,时点就用真实数据(账目创建日 + 盘点日 + 今天)。
|
||||||
// 只补在**最早那个时点的前一天**,不动中间的真实数据。
|
// 单点时 chart_line 会把线画在中间(已有 flat 分支),不会贴底装成 0。
|
||||||
if (count($dates) === 1) {
|
// 真要"从 0 涨起来"的形状,那也该由**账目创建日**天然提供 —— 账目在
|
||||||
array_unshift($dates, date('Y-m-d', strtotime($dates[0] . ' -1 day')));
|
// 创建日之前不存在(下面 $born 判断会跳过),所以曲线本就该从创建日开始。
|
||||||
}
|
// (保留:若连一个真实时点都没有,补今天,避免空图。)
|
||||||
|
|
||||||
// 每条账目的快照:entry_id => [[date, cents], …];born = 它的创建日期
|
// 每条账目的快照:entry_id => [[date, cents], …];born = 它的创建日期
|
||||||
$snaps = [];
|
$snaps = [];
|
||||||
|
|||||||
+9
-6
@@ -71,7 +71,7 @@ final class Schema
|
|||||||
* ⚠️ 它**不再**作为"要不要跑升级"的判据 —— 见 fingerprint() 的说明。
|
* ⚠️ 它**不再**作为"要不要跑升级"的判据 —— 见 fingerprint() 的说明。
|
||||||
* 它仍然要手工维护,因为它给人看(排查时一眼知道代码到哪一版了)。
|
* 它仍然要手工维护,因为它给人看(排查时一眼知道代码到哪一版了)。
|
||||||
*/
|
*/
|
||||||
public const VERSION = '17';
|
public const VERSION = '18';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 「源码指纹」:拿**本文件自身的内容**算一个哈希,作为"要不要跑升级"的判据。
|
* 「源码指纹」:拿**本文件自身的内容**算一个哈希,作为"要不要跑升级"的判据。
|
||||||
@@ -484,7 +484,7 @@ CREATE TABLE IF NOT EXISTS `calendars` (
|
|||||||
`owner_id` INT UNSIGNED NOT NULL COMMENT '这本日历是谁的',
|
`owner_id` INT UNSIGNED NOT NULL COMMENT '这本日历是谁的',
|
||||||
`name` VARCHAR(50) NOT NULL,
|
`name` VARCHAR(50) NOT NULL,
|
||||||
`color` CHAR(7) NOT NULL DEFAULT '#4A7C82' COMMENT '日历本的颜色(日程按本子的颜色显示)',
|
`color` CHAR(7) NOT NULL DEFAULT '#4A7C82' COMMENT '日历本的颜色(日程按本子的颜色显示)',
|
||||||
`kind` ENUM('custom','movie','trip','lunar','holiday') NOT NULL DEFAULT 'custom' COMMENT 'movie/trip/lunar/holiday = 系统预置的派生只读本(农历与节假日为 v24 第 10 批新增,不可删除)',
|
`kind` ENUM('custom','movie','trip','lunar','holiday','traditional','solarterm') NOT NULL DEFAULT 'custom' COMMENT 'movie/trip/lunar/holiday/traditional/solarterm = 系统预置的派生只读本(lunar/holiday 为 v24 第 10 批新增;traditional/solarterm 为第 12 批新增;都不可删除)',
|
||||||
`position` INT NOT NULL DEFAULT 100,
|
`position` INT NOT NULL DEFAULT 100,
|
||||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
@@ -855,11 +855,14 @@ SQL,
|
|||||||
// 不会更新(CREATE TABLE IF NOT EXISTS 直接跳过),必须显式 MODIFY。
|
// 不会更新(CREATE TABLE IF NOT EXISTS 直接跳过),必须显式 MODIFY。
|
||||||
// 不改的后果:给老库插 kind='lunar' 会被严格模式整条拒掉 →
|
// 不改的后果:给老库插 kind='lunar' 会被严格模式整条拒掉 →
|
||||||
// "农历日历本建不出来",而新装的库一切正常(最难查的那种)。
|
// "农历日历本建不出来",而新装的库一切正常(最难查的那种)。
|
||||||
|
// ---- v24 第 12 批 #4:再加 traditional(传统节日)/ solarterm(二十四节气)----
|
||||||
|
// ⚠️ 同一类坑的第二次:ENUM 必须显式 MODIFY 才能对老表生效。
|
||||||
$db->exec("ALTER TABLE `calendars` MODIFY COLUMN `kind`
|
$db->exec("ALTER TABLE `calendars` MODIFY COLUMN `kind`
|
||||||
ENUM('custom','movie','trip','lunar','holiday') NOT NULL DEFAULT 'custom'
|
ENUM('custom','movie','trip','lunar','holiday','traditional','solarterm')
|
||||||
COMMENT 'movie/trip/lunar/holiday = 系统预置的派生只读本'");
|
NOT NULL DEFAULT 'custom'
|
||||||
// 给每个家庭里的每个人都补上农历/节假日这两本(幂等)
|
COMMENT 'movie/trip/lunar/holiday/traditional/solarterm = 系统预置的派生只读本'");
|
||||||
self::tryStep('补建每人的农历 / 节假日日历本', static function () use ($db) {
|
// 给每个家庭里的每个人都补上这 6 本(幂等;seedDefaultsForFamily 按 kind 去重)
|
||||||
|
self::tryStep('补建每人的农历 / 节假日 / 传统节日 / 二十四节气日历本', static function () use ($db) {
|
||||||
foreach ($db->query('SELECT id FROM families')->fetchAll() as $fam) {
|
foreach ($db->query('SELECT id FROM families')->fetchAll() as $fam) {
|
||||||
\App\Calendar::seedDefaultsForFamily((int)$fam['id']);
|
\App\Calendar::seedDefaultsForFamily((int)$fam['id']);
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-1
@@ -38,7 +38,14 @@ function chart_line(array $series, string $label, string $kind = 'asset', int $w
|
|||||||
{
|
{
|
||||||
$pts = [];
|
$pts = [];
|
||||||
foreach ($series as $p) {
|
foreach ($series as $p) {
|
||||||
$pts[] = ['d' => (string)($p['date'] ?? ''), 'v' => (int)($p['cents'] ?? 0)];
|
// ⚠️ 取值的键要兼容两种数据源(这里曾是真 bug 的现场):
|
||||||
|
// · entrySeries() / 盘点历史 → 'cents'
|
||||||
|
// · Ledger::timeline() → 'net'(净资产)/ 'asset' / 'debt'
|
||||||
|
// 只认 'cents' 的话,**总资产图会把每个点都读成 0** —— 图上线是一条
|
||||||
|
// 贴着 0 的平线,看起来像"数据全是 0"(用户 2026-10-04 第 12 批 #5
|
||||||
|
// 原话:「上面那个图,如果是总资产的变化,那么不应该是 0 啊」)。
|
||||||
|
$v = $p['cents'] ?? $p['net'] ?? $p['asset'] ?? 0;
|
||||||
|
$pts[] = ['d' => (string)($p['date'] ?? ''), 'v' => (int)$v];
|
||||||
}
|
}
|
||||||
if (!$pts) {
|
if (!$pts) {
|
||||||
return '';
|
return '';
|
||||||
|
|||||||
+105
-12
@@ -569,8 +569,29 @@ textarea.input { min-height: 76px; resize: vertical; }
|
|||||||
/* v24 第 9 批 #9:Semi DatePicker 升级态 —— .dt-native 是 display:contents(不干扰
|
/* v24 第 9 批 #9:Semi DatePicker 升级态 —— .dt-native 是 display:contents(不干扰
|
||||||
flex 布局),升级成功后由 app.js 内联 display:none 藏掉;.dt-semi 顶上同一格。 */
|
flex 布局),升级成功后由 app.js 内联 display:none 藏掉;.dt-semi 顶上同一格。 */
|
||||||
.dt-input .dt-native { display: contents; }
|
.dt-input .dt-native { display: contents; }
|
||||||
|
/* ---- v24 第 12 批 #2:消除「先闪原生框、再变 Semi」----
|
||||||
|
html.js 由 <head> 里的内联脚本立刻加上(见 layout.php 的 no-js/js 二态)。
|
||||||
|
有 JS 时:原生控件**从一开始就不显示**,等 Semi 就绪(app.js 加 .is-semi)再露出。
|
||||||
|
没 JS 时(html 仍是 no-js):什么都不藏,原生控件照旧可用 —— 渐进增强的兜底不变。
|
||||||
|
⚠️ 用 visibility:hidden 而不是 display:none —— 保留元素占位高度,
|
||||||
|
否则升级完成的那一瞬表单会"跳"一下(高度从 0 变 40px)。 */
|
||||||
|
html.js .dt-input:not(.is-semi) .dt-native { visibility: hidden; }
|
||||||
|
html.js .dt-range:not(.is-semi-range) > .dt-input { visibility: hidden; }
|
||||||
|
/* Semi 未就绪前,先给一个同高的"占位骨架",让表单高度在升级前后保持一致 */
|
||||||
|
html.js .dt-input:not(.is-semi)::after {
|
||||||
|
content: ''; display: block; flex: 1 1 150px; height: 40px;
|
||||||
|
border: 1px solid var(--line); border-radius: var(--radius-s);
|
||||||
|
background: var(--surface); opacity: .55;
|
||||||
|
}
|
||||||
|
html.js .dt-range:not(.is-semi-range)::before {
|
||||||
|
content: ''; display: block; width: 100%; max-width: 460px; height: 40px;
|
||||||
|
border: 1px solid var(--line); border-radius: var(--radius-s);
|
||||||
|
background: var(--surface); opacity: .55;
|
||||||
|
}
|
||||||
|
html.js .dt-range.is-semi-range::before { display: none; }
|
||||||
.dt-input .dt-semi { display: none; flex: 1 1 220px; min-width: 0; }
|
.dt-input .dt-semi { display: none; flex: 1 1 220px; min-width: 0; }
|
||||||
.dt-input.is-semi .dt-semi { display: block; }
|
.dt-input.is-semi .dt-semi { display: block; }
|
||||||
|
.dt-input.is-semi::after { display: none; }
|
||||||
/* 工作经历的月份选择器(type="month" 升级):占满原 .job-cell 格子 */
|
/* 工作经历的月份选择器(type="month" 升级):占满原 .job-cell 格子 */
|
||||||
.dt-semi.dt-semi-month { display: none; }
|
.dt-semi.dt-semi-month { display: none; }
|
||||||
.dt-semi.dt-semi-month.is-live { display: block; width: 100%; }
|
.dt-semi.dt-semi-month.is-live { display: block; width: 100%; }
|
||||||
@@ -591,6 +612,35 @@ body[class] .dt-semi-range .semi-input-wrapper {
|
|||||||
}
|
}
|
||||||
body[class] .dt-semi-range .semi-input-wrapper:hover { border-color: var(--brand-2); }
|
body[class] .dt-semi-range .semi-input-wrapper:hover { border-color: var(--brand-2); }
|
||||||
body[class] .dt-semi-range .semi-input { font: inherit; font-size: 13.5px; color: var(--ink); }
|
body[class] .dt-semi-range .semi-input { font: inherit; font-size: 13.5px; color: var(--ink); }
|
||||||
|
/* ---- v24 第 12 批 #2:范围控件的「灰底 + 不垂直居中」(实测定位)----
|
||||||
|
用真浏览器量出来两个毛病:
|
||||||
|
① 灰底:Semi 的 **外层** `.semi-datepicker-input` 自带 `rgba(46,50,56,.05)`
|
||||||
|
灰底、高只有 32px;而内层 `.semi-input-wrapper` 是白底、40px ——
|
||||||
|
两层套着,灰的那层露在外面(用户看到的"灰色背景色")。
|
||||||
|
② 不居中:外层 32px < 内层 40px,输入框上下都被挤出去
|
||||||
|
(实测 gapTop=1 / gapBottom=17 → 文字贴顶)。
|
||||||
|
修法:把**外层容器**改成透明 + 不限高 + 垂直居中,让内层 40px 的白底框
|
||||||
|
自己撑起来(与站内 .input 完全一致)。 */
|
||||||
|
body[class] .dt-semi-range .semi-datepicker-input {
|
||||||
|
background: transparent !important;
|
||||||
|
height: auto !important;
|
||||||
|
min-height: 0 !important;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
body[class] .dt-semi-range .semi-datepicker-range-input-wrapper {
|
||||||
|
align-items: center;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
/* 两端的白底框就是"可见的输入框":撑满高度、文字垂直居中 */
|
||||||
|
body[class] .dt-semi-range .semi-input-wrapper {
|
||||||
|
display: flex; align-items: center;
|
||||||
|
}
|
||||||
|
body[class] .dt-semi-range .semi-input-wrapper .semi-input {
|
||||||
|
height: 100%; line-height: 40px; padding: 0 8px;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.dt-semi-range { max-width: none; }
|
.dt-semi-range { max-width: none; }
|
||||||
}
|
}
|
||||||
@@ -608,10 +658,24 @@ body[class] .dt-semi .semi-input-wrapper {
|
|||||||
width: 100%; height: 40px;
|
width: 100%; height: 40px;
|
||||||
border: 1px solid var(--line); border-radius: var(--radius-s);
|
border: 1px solid var(--line); border-radius: var(--radius-s);
|
||||||
background: var(--surface); color: var(--ink);
|
background: var(--surface); color: var(--ink);
|
||||||
|
/* v24 第 12 批 #2:单点控件也做同样处理 —— 外层半透明灰底去掉、文字垂直居中
|
||||||
|
(用户说"这种情况同样出现在其他的日期选择功能中")。 */
|
||||||
|
display: flex; align-items: center;
|
||||||
}
|
}
|
||||||
body[class] .dt-semi .semi-input-wrapper:hover { border-color: var(--brand-2); background: var(--surface); }
|
body[class] .dt-semi .semi-input-wrapper:hover { border-color: var(--brand-2); background: var(--surface); }
|
||||||
body[class] .dt-semi .semi-input { font: inherit; color: var(--ink); }
|
body[class] .dt-semi .semi-input { font: inherit; color: var(--ink); }
|
||||||
body[class] .dt-semi .semi-input::placeholder { color: var(--muted); }
|
body[class] .dt-semi .semi-input::placeholder { color: var(--muted); }
|
||||||
|
body[class] .dt-semi .semi-datepicker-input,
|
||||||
|
body[class] .dt-semi .semi-datepicker {
|
||||||
|
background: transparent !important;
|
||||||
|
height: auto !important;
|
||||||
|
min-height: 0 !important;
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
body[class] .dt-semi .semi-input-wrapper .semi-input {
|
||||||
|
height: 100%; line-height: 40px; padding: 0 11px; background: transparent;
|
||||||
|
}
|
||||||
/* 日历面板:白纸底色统一成卡片的纸色,选中/悬停色走上面的 brand 变量 */
|
/* 日历面板:白纸底色统一成卡片的纸色,选中/悬停色走上面的 brand 变量 */
|
||||||
body[class] .semi-popover .semi-datepicker { background: var(--surface, #fff); }
|
body[class] .semi-popover .semi-datepicker { background: var(--surface, #fff); }
|
||||||
body[class] .semi-datepicker-day-selected { background-color: var(--brand, #A84A28); }
|
body[class] .semi-datepicker-day-selected { background-color: var(--brand, #A84A28); }
|
||||||
@@ -1097,6 +1161,25 @@ body[class] .semi-datepicker-day-inhover { background-color: color-mix(in srgb,
|
|||||||
border: 1px solid #F0D6D1; border-radius: 8px; font-size: 13px; color: #7A2A20;
|
border: 1px solid #F0D6D1; border-radius: 8px; font-size: 13px; color: #7A2A20;
|
||||||
}
|
}
|
||||||
.del-big-confirm[hidden] { display: none; }
|
.del-big-confirm[hidden] { display: none; }
|
||||||
|
|
||||||
|
/* ---- v24 第 12 批 #1:删除按钮紧挨「开通账号」放在同一列 ----
|
||||||
|
用户原话:「删除按钮,就放在开通账号按钮下面,紧挨着,长度大小,
|
||||||
|
和开通账号一样,就是颜色改成红色。」
|
||||||
|
所以这里只做两件事:① 与账号区的按钮**左对齐同宽**(都以账号卡片内容宽为准);
|
||||||
|
② 上方留一条细分隔线,让人一眼看出"这是同一个区块里的第二件事"。
|
||||||
|
⚠️ 尺寸一致性靠 .btn-block-lg(100% 宽 + 同 padding),不在这里写死宽度,
|
||||||
|
否则窄屏下会与「开通账号」对不齐。 */
|
||||||
|
.del-inline {
|
||||||
|
margin-top: 14px;
|
||||||
|
padding-top: 14px;
|
||||||
|
border-top: 1px dashed var(--line);
|
||||||
|
}
|
||||||
|
.del-inline-tip {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
font-size: 12.5px;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
.person-move { display: flex; gap: 6px; padding: 8px 12px; flex-wrap: wrap; }
|
.person-move { display: flex; gap: 6px; padding: 8px 12px; flex-wrap: wrap; }
|
||||||
.person-move .select { flex: 1 1 auto; min-width: 0; padding: 6px 8px; font-size: 13px; }
|
.person-move .select { flex: 1 1 auto; min-width: 0; padding: 6px 8px; font-size: 13px; }
|
||||||
.person-move .btn { flex: 0 0 auto; }
|
.person-move .btn { flex: 0 0 auto; }
|
||||||
@@ -2691,21 +2774,31 @@ body[class] .semi-datepicker-day-inhover { background-color: color-mix(in srgb,
|
|||||||
点开弹一个只含 Semi 日期选择器的轻浮层,选中即跳。 */
|
点开弹一个只含 Semi 日期选择器的轻浮层,选中即跳。 */
|
||||||
.cal-jump { display: inline-flex; margin: 0; }
|
.cal-jump { display: inline-flex; margin: 0; }
|
||||||
.cal-jump-btn { display: inline-flex; align-items: center; justify-content: center; cursor: pointer; }
|
.cal-jump-btn { display: inline-flex; align-items: center; justify-content: center; cursor: pointer; }
|
||||||
.cal-jump-host { position: fixed; inset: 0; z-index: 80; display: flex;
|
/* ---- v24 第 12 批 #3:跳转日期改成**按钮下方的下拉日历** ----
|
||||||
align-items: center; justify-content: center; }
|
用户原话:「选择这个日期,不用弹窗,直接把日历选择,弹出在这个按钮下面就可以了。」
|
||||||
.cal-jump-pop {
|
所以把原来"全屏遮罩 + 居中弹窗"(.cal-jump-host/.cal-jump-pop)换成
|
||||||
background: var(--surface); border: 1px solid var(--line);
|
贴着按钮下方展开的下拉面板(.cal-jump-drop)。 */
|
||||||
border-radius: 14px; box-shadow: var(--shadow-l); padding: 10px 12px 12px;
|
.cal-jump { position: relative; }
|
||||||
|
.cal-jump-drop {
|
||||||
|
position: absolute; z-index: 70;
|
||||||
|
top: calc(100% + 8px); left: auto; right: 0; /* 贴右:与按钮同一列,不会跑出屏外 */
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: var(--shadow-l);
|
||||||
|
padding: 8px;
|
||||||
|
/* 下拉里放的是 Semi 的**内联**日历面板,去掉它自己的浮层阴影/描边避免双层框 */
|
||||||
}
|
}
|
||||||
.cal-jump-pop-head {
|
.cal-jump-drop[hidden] { display: none; }
|
||||||
display: flex; align-items: center; gap: 10px;
|
.cal-jump-drop .semi-datepicker,
|
||||||
font-size: 13.5px; font-weight: 600; margin-bottom: 8px;
|
.cal-jump-drop .semi-popover {
|
||||||
|
box-shadow: none !important;
|
||||||
|
border: 0 !important;
|
||||||
|
background: var(--surface) !important;
|
||||||
}
|
}
|
||||||
.cal-jump-x {
|
.cal-jump-drop .semi-datepicker {
|
||||||
margin-left: auto; border: 0; background: transparent; cursor: pointer;
|
display: block;
|
||||||
font-size: 14px; color: var(--muted); padding: 3px 6px; border-radius: 6px;
|
|
||||||
}
|
}
|
||||||
.cal-jump-x:hover { background: var(--line-2); color: var(--ink); }
|
|
||||||
|
|
||||||
/* ==================== 周 / 天:按小时铺的时间网格 ==================== */
|
/* ==================== 周 / 天:按小时铺的时间网格 ==================== */
|
||||||
/* 结构:左边一条"小时刻度"沟槽(tg-gutter),右边 N 个日期列(tg-col)。
|
/* 结构:左边一条"小时刻度"沟槽(tg-gutter),右边 N 个日期列(tg-col)。
|
||||||
|
|||||||
@@ -277,13 +277,26 @@
|
|||||||
*
|
*
|
||||||
* 用法:window.ppSetSelected('person_id', 12) // 0 或不传 = 不指定
|
* 用法:window.ppSetSelected('person_id', 12) // 0 或不传 = 不指定
|
||||||
* ⚠️ 多选组件请传数组或逗号分隔的字符串。
|
* ⚠️ 多选组件请传数组或逗号分隔的字符串。
|
||||||
|
*
|
||||||
|
* @param {string} name 字段名
|
||||||
|
* @param {*} value 选中值
|
||||||
|
* @param {Element} [root] **限定查找范围**。不传 = 全页找第一个。
|
||||||
|
*
|
||||||
|
* 🔴 为什么必须有 root(这是真 bug 的修复,别再删):
|
||||||
|
* 同一页上可以同时存在**多个同名字段**的选人组件。财务页就有两个
|
||||||
|
* `person_id` —— 「记一笔」表单里一个、修改账目弹框里一个。
|
||||||
|
* 原来只按名字 `document.querySelector` 抓,命中的永远是**文档里第一个**
|
||||||
|
* (记一笔那个),于是修改弹框的「挂在谁名下」**永远灌不进人**,
|
||||||
|
* 用户看到的就是"归属人没带进来"。
|
||||||
|
* 修法:调用方把弹框(panel)作为 root 传进来,只在其中找。
|
||||||
*/
|
*/
|
||||||
window.ppSetSelected = function (name, value) {
|
window.ppSetSelected = function (name, value, root) {
|
||||||
if (!name) { return false; }
|
if (!name) { return false; }
|
||||||
var box = document.querySelector('.pp[data-pp-name="' + name + '"]');
|
var scope = (root && root.querySelector) ? root : document;
|
||||||
|
var box = scope.querySelector('.pp[data-pp-name="' + name + '"]');
|
||||||
if (!box) {
|
if (!box) {
|
||||||
// 兼容:老渲染里没有 data-pp-name 时,退化成"找包含该 name 的 radio/checkbox"
|
// 兼容:老渲染里没有 data-pp-name 时,退化成"找包含该 name 的 radio/checkbox"
|
||||||
var host = document.querySelector('.pp');
|
var host = scope.querySelector('.pp');
|
||||||
if (!host) { return false; }
|
if (!host) { return false; }
|
||||||
box = host;
|
box = host;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ $dtSec = $dtTs ? (int)date('s', $dtTs) : 0;
|
|||||||
$dtCfg = ['name' => $dtName, 'seconds' => $dtSeconds, 'required' => $dtRequired,
|
$dtCfg = ['name' => $dtName, 'seconds' => $dtSeconds, 'required' => $dtRequired,
|
||||||
'id' => $dtId, 'range' => $dtRange];
|
'id' => $dtId, 'range' => $dtRange];
|
||||||
?>
|
?>
|
||||||
<div class="dt-input<?= $dtRange !== '' ? ' dt-part' : '' ?>"
|
<div class="dt-input<?= $dtRange !== '' ? ' dt-part' : '' ?><?= $dtRange === 'start' ? ' is-pending' : '' ?>"
|
||||||
data-dt="<?= e(json_encode($dtCfg, JSON_UNESCAPED_UNICODE | JSON_HEX_APOS | JSON_HEX_QUOT)) ?>">
|
data-dt="<?= e(json_encode($dtCfg, JSON_UNESCAPED_UNICODE | JSON_HEX_APOS | JSON_HEX_QUOT)) ?>">
|
||||||
<span class="dt-semi" aria-hidden="true"></span>
|
<span class="dt-semi" aria-hidden="true"></span>
|
||||||
<span class="dt-native">
|
<span class="dt-native">
|
||||||
|
|||||||
@@ -77,25 +77,34 @@ $headText = $view === 'month' ? date('Y 年 n 月', $ts)
|
|||||||
⚠️ 用原生 <input type="date"> 而不是再引一个弹层:它的原生日历足够用,
|
⚠️ 用原生 <input type="date"> 而不是再引一个弹层:它的原生日历足够用,
|
||||||
而且**选中就有值**(change 事件)→ 直接跳转,不用再点一次"确定"。
|
而且**选中就有值**(change 事件)→ 直接跳转,不用再点一次"确定"。
|
||||||
桌面/手机/无 JS 三种情况下的行为都是可预期的。 */ ?>
|
桌面/手机/无 JS 三种情况下的行为都是可预期的。 */ ?>
|
||||||
<?php /* 用户 2026-10-04 第 11 批 #6 原话(两句):
|
<?php /* 用户 2026-10-05 第 12 批 #3 原话(三句):
|
||||||
· 「跳转日期那个日期选择框,用我之前给你的那个,保持所有样式和格式的统一。」
|
· 「跳转到某一天时,不要进入日视图,保留当前本来的视图,只不过是跳转到这一天,
|
||||||
· 「跳转日期那个地方,不要那么长,只需要和设置一样,一个按钮即可。」
|
如果是周视图,那么就是跳转到这一天所在的周,如果是月视图,则跳转到这一天所在的月」
|
||||||
→ 从"一个长条(图标 + 文字 + 原生日期框)"改成**与设置按钮同规格的单个图标按钮**
|
· 「选择这个日期,不用弹窗,直接把日历选择,弹出在这个按钮下面就可以了」
|
||||||
(btn-icon,与 ☰ 同宽同高),点开弹出 Semi 日期选择器(与全站日期控件同一套样式),
|
→ ① 不再固定 view=day:带上**当前视图**(月/周/日各自跳到对应跨度);
|
||||||
选中即跳到那天的日视图。
|
② 日期选择从"居中弹窗"改成**贴着按钮下方展开的下拉日历**。 */ ?>
|
||||||
原生 input[type=date] 仍然保留(noscript 里)——"没 JS 也能跳日期"这件事没丢。 */ ?>
|
|
||||||
<form class="cal-jump" method="get" action="index.php">
|
<form class="cal-jump" method="get" action="index.php">
|
||||||
<input type="hidden" name="r" value="calendar">
|
<input type="hidden" name="r" value="calendar">
|
||||||
<input type="hidden" name="view" value="day">
|
<?php /* 保留当前视图:月→该月 / 周→那一周 / 日→那一天。
|
||||||
|
⚠️ 原来写死 view=day,用户点"跳到 10 月 20 日"就被踢进日视图,
|
||||||
|
而他本来在月视图里 —— 这正是不满的点。 */ ?>
|
||||||
|
<input type="hidden" name="view" value="<?= e($view) ?>">
|
||||||
<input type="hidden" name="d" id="calJumpD" value="<?= e($anchor) ?>">
|
<input type="hidden" name="d" id="calJumpD" value="<?= e($anchor) ?>">
|
||||||
|
<?php /* 月视图用的是 ym(年月),跳转时也要带上,否则服务端会退回默认月 */ ?>
|
||||||
|
<?php if ($view === 'month'): ?>
|
||||||
|
<input type="hidden" name="ym" id="calJumpYM" value="<?= e(substr($anchor, 0, 7)) ?>">
|
||||||
|
<?php endif; ?>
|
||||||
<button type="button" class="btn btn-icon cal-jump-btn" id="calJumpBtn"
|
<button type="button" class="btn btn-icon cal-jump-btn" id="calJumpBtn"
|
||||||
title="跳到某一天" aria-label="跳到某一天">
|
title="跳到某一天" aria-label="跳到某一天" aria-haspopup="dialog" aria-expanded="false">
|
||||||
<svg viewBox="0 0 16 16" width="18" height="18" aria-hidden="true" fill="none"
|
<svg viewBox="0 0 16 16" width="18" height="18" aria-hidden="true" fill="none"
|
||||||
stroke="currentColor" stroke-width="1.7" stroke-linecap="round">
|
stroke="currentColor" stroke-width="1.7" stroke-linecap="round">
|
||||||
<rect x="2" y="3.2" width="12" height="10.8" rx="1.6"/>
|
<rect x="2" y="3.2" width="12" height="10.8" rx="1.6"/>
|
||||||
<path d="M2 6.6h12M5.2 1.8v2.6M10.8 1.8v2.6"/>
|
<path d="M2 6.6h12M5.2 1.8v2.6M10.8 1.8v2.6"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<?php /* 下拉日历的宿主:紧贴按钮下方展开(不是弹窗)。JS 就绪后把 Semi
|
||||||
|
日期选择器挂进来;JS 不可用时它是空的、退到下面的 noscript。 */ ?>
|
||||||
|
<div class="cal-jump-drop" id="calJumpDrop" hidden></div>
|
||||||
<noscript>
|
<noscript>
|
||||||
<input class="input" type="date" name="d" value="<?= e($anchor) ?>" aria-label="选择日期">
|
<input class="input" type="date" name="d" value="<?= e($anchor) ?>" aria-label="选择日期">
|
||||||
<button class="btn btn-sm" type="submit">跳转</button>
|
<button class="btn btn-sm" type="submit">跳转</button>
|
||||||
@@ -343,26 +352,38 @@ $headText = $view === 'month' ? date('Y 年 n 月', $ts)
|
|||||||
/* 「全天」勾上藏掉时刻那一段的逻辑在 _event_fields.php 里(两个页面共用一份) */
|
/* 「全天」勾上藏掉时刻那一段的逻辑在 _event_fields.php 里(两个页面共用一份) */
|
||||||
})();
|
})();
|
||||||
|
|
||||||
/* ---- 用户第 6 条:「跳到某一天」—— 单个图标按钮 + Semi 日期选择器 ---- */
|
/* ---- v24 第 12 批 #3:「跳到某一天」—— 单个图标按钮 + **按钮下方的下拉日历** ----
|
||||||
|
用户原话:「选择这个日期,不用弹窗,直接把日历选择,弹出在这个按钮下面就可以了。」
|
||||||
|
所以从"居中弹窗(cal-jump-host 全屏遮罩)"改成"贴着按钮下方展开的下拉"。
|
||||||
|
⚠️ 中途会经过"保留视图"那一关:jump() 提交时带上当前 view(月的 ym / 周的 d / 日的 d),
|
||||||
|
由服务端决定该视图下这个日期落在哪一屏 —— 见 index.php 的 calendar 路由。 */
|
||||||
(function () {
|
(function () {
|
||||||
var form = document.querySelector('.cal-jump');
|
var form = document.querySelector('.cal-jump');
|
||||||
var btn = document.getElementById('calJumpBtn');
|
var btn = document.getElementById('calJumpBtn');
|
||||||
var hid = document.getElementById('calJumpD');
|
var hid = document.getElementById('calJumpD');
|
||||||
|
var drop = document.getElementById('calJumpDrop');
|
||||||
if (!form || !btn || !hid) { return; }
|
if (!form || !btn || !hid) { return; }
|
||||||
|
var root = null; // React root(重开时先 unmount)
|
||||||
|
|
||||||
function jump(v) {
|
function jump(v) {
|
||||||
if (!v) { return; }
|
if (!v) { return; }
|
||||||
hid.value = v;
|
hid.value = v;
|
||||||
|
// 月视图还要带上 ym(服务端按月取数),否则会跳回默认月
|
||||||
|
var ym = document.getElementById('calJumpYM');
|
||||||
|
if (ym) { ym.value = String(v).slice(0, 7); }
|
||||||
form.submit();
|
form.submit();
|
||||||
}
|
}
|
||||||
|
|
||||||
btn.addEventListener('click', function () {
|
function closeDrop() {
|
||||||
var meta = document.querySelector('meta[name="fb-semi"]');
|
if (!drop) { return; }
|
||||||
var conf = null;
|
drop.hidden = true;
|
||||||
try { conf = meta ? JSON.parse(meta.getAttribute('content')) : null; } catch (e) { conf = null; }
|
btn.setAttribute('aria-expanded', 'false');
|
||||||
|
if (root) { root.unmount(); root = null; }
|
||||||
|
drop.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 原生兜底:Semi 没就绪时用临时原生日期框(与无 JS 时行为一致)
|
||||||
function fallback() {
|
function fallback() {
|
||||||
// Semi 没就绪 → 用一个临时原生日期框(无 JS 时的兜底与这里同一行为)
|
|
||||||
var t = document.createElement('input');
|
var t = document.createElement('input');
|
||||||
t.type = 'date';
|
t.type = 'date';
|
||||||
t.value = hid.value;
|
t.value = hid.value;
|
||||||
@@ -374,38 +395,42 @@ $headText = $view === 'month' ? date('Y 年 n 月', $ts)
|
|||||||
else { t.focus(); t.click(); }
|
else { t.focus(); t.click(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
btn.addEventListener('click', function (e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (!drop) { fallback(); return; }
|
||||||
|
if (!drop.hidden) { closeDrop(); return; }
|
||||||
|
|
||||||
if (!window.SemiUI || !window.SemiUI.DatePicker || !window.ReactDOM) { fallback(); return; }
|
if (!window.SemiUI || !window.SemiUI.DatePicker || !window.ReactDOM) { fallback(); return; }
|
||||||
|
|
||||||
// 弹一个**只有一个日期选择器**的轻浮层(与全站日期控件同一套样式)
|
drop.hidden = false;
|
||||||
var host = document.getElementById('calJumpHost');
|
btn.setAttribute('aria-expanded', 'true');
|
||||||
if (!host) {
|
|
||||||
host = document.createElement('div');
|
|
||||||
host.id = 'calJumpHost';
|
|
||||||
host.className = 'cal-jump-host';
|
|
||||||
document.body.appendChild(host);
|
|
||||||
}
|
|
||||||
var wrap = document.createElement('div');
|
|
||||||
wrap.className = 'cal-jump-pop';
|
|
||||||
wrap.innerHTML = '<div class="cal-jump-pop-head">跳到哪一天?' +
|
|
||||||
'<button type="button" class="cal-jump-x" aria-label="关闭">✕</button></div>';
|
|
||||||
var inner = document.createElement('div');
|
var inner = document.createElement('div');
|
||||||
wrap.appendChild(inner);
|
drop.innerHTML = '';
|
||||||
host.innerHTML = '';
|
drop.appendChild(inner);
|
||||||
host.appendChild(wrap);
|
root = ReactDOM.createRoot(inner);
|
||||||
|
root.render(React.createElement(SemiUI.DatePicker, {
|
||||||
ReactDOM.createRoot(inner).render(React.createElement(SemiUI.DatePicker, {
|
|
||||||
type: 'date',
|
type: 'date',
|
||||||
format: 'yyyy-MM-dd',
|
format: 'yyyy-MM-dd',
|
||||||
defaultValue: hid.value ? new Date(hid.value + 'T00:00:00') : new Date(),
|
defaultValue: hid.value ? new Date(hid.value + 'T00:00:00') : new Date(),
|
||||||
open: true,
|
// ⚠️ 不用 `open: true`:那会让 Semi 自己弹它的浮层(就又成弹窗了)。
|
||||||
// 选中即跳,不用再点"确定"
|
// 这里要的是"日历长在下拉容器里",所以让它以内联面板渲染。
|
||||||
|
open: false,
|
||||||
|
defaultOpen: true,
|
||||||
|
preventScroll: true,
|
||||||
onChange: function (d, ds) {
|
onChange: function (d, ds) {
|
||||||
if (ds) { jump(ds); }
|
if (ds) { jump(ds); }
|
||||||
},
|
}
|
||||||
onOpenChange: function (o) { if (!o) { host.innerHTML = ''; } }
|
|
||||||
}));
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
wrap.querySelector('.cal-jump-x').addEventListener('click', function () { host.innerHTML = ''; });
|
// 点外面 / Esc 收起(下拉的常规行为)
|
||||||
|
document.addEventListener('click', function (e) {
|
||||||
|
if (!drop || drop.hidden) { return; }
|
||||||
|
if (drop.contains(e.target) || btn.contains(e.target)) { return; }
|
||||||
|
closeDrop();
|
||||||
|
});
|
||||||
|
document.addEventListener('keydown', function (e) {
|
||||||
|
if (e.key === 'Escape') { closeDrop(); }
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+12
-1
@@ -28,8 +28,19 @@ $themeVars = \App\Module::cssVars($moduleKey, (int)\App\Auth::familyId());
|
|||||||
$isLoggedIn = \App\Auth::check();
|
$isLoggedIn = \App\Auth::check();
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN" class="no-js">
|
||||||
<head>
|
<head>
|
||||||
|
<?php /* v24 第 12 批 #2:解决「打开页面先闪一下原生输入框,然后才变成 Semi 控件」。
|
||||||
|
做法是经典的 **no-js / js 二态**:
|
||||||
|
· 服务端先渲染 class="no-js"(此时原生控件可见 = 无 JS 也能用);
|
||||||
|
· 下面这段内联脚本**在 <head> 里立即把 class 换成 js** ——
|
||||||
|
它在 CSS 生效前、任何渲染之前就跑了,所以浏览器**根本没机会**画出原生框;
|
||||||
|
· CSS 里 `.js .dt-input .dt-native { visibility:hidden }` 先把原生藏住
|
||||||
|
(用 visibility 而不是 display:none:保留占位高度,避免升级瞬间布局跳动);
|
||||||
|
· app.js 的 Semi 就绪后加 `.is-semi` 把 Semi 显示出来(原生继续藏着)。
|
||||||
|
这样"闪烁"从**结构上**消失,而不是靠调加载顺序去掩盖。 */ ?>
|
||||||
|
<script>document.documentElement.className =
|
||||||
|
document.documentElement.className.replace('no-js', 'js');</script>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title><?= e($pageTitle) ?> · 家书</title>
|
<title><?= e($pageTitle) ?> · 家书</title>
|
||||||
|
|||||||
@@ -448,8 +448,11 @@ $ledMe = (int)($me ?? 0);
|
|||||||
var kSel = panel.querySelector('select[name="kind"]');
|
var kSel = panel.querySelector('select[name="kind"]');
|
||||||
if (kSel) { kSel.value = trigger.getAttribute('data-led-kind') || 'other'; }
|
if (kSel) { kSel.value = trigger.getAttribute('data-led-kind') || 'other'; }
|
||||||
// 归属人:选人组件是**单选**,用它的接口重置选中(见 _person_picker.php)
|
// 归属人:选人组件是**单选**,用它的接口重置选中(见 _person_picker.php)
|
||||||
|
// ⚠️ 必须把 panel 作为 root 传进去:本页有**两个** person_id 选人组件
|
||||||
|
// (「记一笔」表单 + 本弹框),只按名字找会命中第一个 →
|
||||||
|
// 用户看到的"归属人没带进来"就是这么来的。
|
||||||
var pid = trigger.getAttribute('data-led-person') || '0';
|
var pid = trigger.getAttribute('data-led-person') || '0';
|
||||||
if (window.ppSetSelected) { window.ppSetSelected('person_id', pid); }
|
if (window.ppSetSelected) { window.ppSetSelected('person_id', pid, panel); }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (which === 'recon') {
|
if (which === 'recon') {
|
||||||
|
|||||||
+16
-22
@@ -619,28 +619,19 @@ $shortKind = \App\Birthday::KINDS;
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div><?php /* /detail-stack —— 名片卡下方的全宽堆叠到此结束 */ ?>
|
|
||||||
|
|
||||||
<?php /* ==================== 危险操作:删除(用户 2026-10-03 第 2 条)====================
|
<?php /* ==================== 危险操作:删除 ====================
|
||||||
「删除」从右上角搬到了页面**最下方**、「开通登录账号」下面。
|
用户 2026-10-05 第 12 批 #1 原话:「名册详情页面,删除按钮,就放在
|
||||||
放在最后 + 用 <details> 两步确认:破坏性操作不该一眼就看到、更不该一键完成。
|
开通账号按钮下面,紧挨着,长度大小,和开通账号一样,就是颜色改成红色。」
|
||||||
⚠️ 措辞改成了「从名册中删除」并写明会连带删掉什么 ——
|
→ 上一版把它放在**页面最下方的独立区块**(danger-zone),用户要的是
|
||||||
原来那句"从名册里移除"听着像"只是从我的名册里移走",
|
**紧挨着「开通账号」**。所以搬进这张「登录账号」卡片里、放在同一列,
|
||||||
实际会删账号与亲属关系,说法与后果不符。 */ ?>
|
尺寸与「开通账号」完全一致(同 .btn 家族 + 100% 宽),只把颜色换成红。
|
||||||
<?php if ($admin): ?>
|
破坏性操作仍保留两步确认(第一次点露出「确认删除/取消」)。 */ ?>
|
||||||
<?php /* 用户 2026-10-04 第 11 批 #2 原话:「个人详情页,最下面的删除,竟然是个链接,
|
<?php if ($admin): ?>
|
||||||
应该是个按钮,跟开通账号一样,是个大按钮,只不过背景用红色。」
|
<div class="del-inline">
|
||||||
→ 从 <details> + 文字链接 + 小按钮,改成**与「开通登录账号」同规格的大按钮**,
|
<p class="del-inline-tip">
|
||||||
只把底色换成红色。确认仍保留(破坏性操作),但改成弹框式的二次确认,
|
删除会把他从名册中移除,<b>并且</b>删掉登录账号与全部亲属关系连线,<b>不可恢复</b>。
|
||||||
不再用浏览器原生 confirm(那玩意儿在不同浏览器里长相差异大)。 */ ?>
|
若只想让他从<b>你的</b>名册里消失,用名册列表的「⋯ → 从我的名册移走」。
|
||||||
<div class="danger-zone">
|
|
||||||
<div class="del-big">
|
|
||||||
<p class="del-big-tip">
|
|
||||||
删除会把他从名册中移除,<b>并且</b>删掉登录账号与全部亲属关系连线,<b>不可恢复</b>。<br>
|
|
||||||
如果你只是想让他从<b>你的</b>名册里消失,用名册列表上的「⋯ → 从我的名册移走」。
|
|
||||||
</p>
|
</p>
|
||||||
<button type="button" class="btn btn-danger btn-block-lg" id="delBigBtn">
|
<button type="button" class="btn btn-danger btn-block-lg" id="delBigBtn">
|
||||||
删除 <?= e((string)$p['name']) ?>
|
删除 <?= e((string)$p['name']) ?>
|
||||||
@@ -655,8 +646,11 @@ $shortKind = \App\Birthday::KINDS;
|
|||||||
<button class="btn btn-ghost" type="button" id="delBigNo">取消</button>
|
<button class="btn btn-ghost" type="button" id="delBigNo">取消</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div><?php /* /detail-stack —— 名片卡下方的全宽堆叠到此结束 */ ?>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
/* 大删除按钮的两步确认(用户第 11 批 #2):第一次点露出"确认删除/取消",
|
/* 大删除按钮的两步确认(用户第 11 批 #2):第一次点露出"确认删除/取消",
|
||||||
|
|||||||
Reference in New Issue
Block a user