@starwish-ai/xingyuan-dsh 0.5.2 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +187 -0
- package/lib/client/api.js +21 -2
- package/lib/client/cards.js +28 -19
- package/lib/client/hooks.js +29 -0
- package/lib/client/i18n.js +26 -14
- package/lib/client/index.js +5 -3
- package/lib/client/pages/calendar.js +10 -4
- package/lib/client/pages/detail.js +36 -10
- package/lib/client/pages/format.js +3 -2
- package/lib/client/pages/memory.js +5 -0
- package/lib/client/pages/settings.js +73 -21
- package/lib/client/pages/today.js +7 -4
- package/lib/client.js +219 -58
- package/lib/consistency-sweep.js +47 -0
- package/lib/domain.js +2 -1
- package/lib/growth.js +5 -16
- package/lib/index.js +18 -1
- package/lib/micro.js +10 -9
- package/lib/pref-policy.js +38 -0
- package/lib/pref-settings.js +27 -0
- package/lib/preset/charts.js +38 -26
- package/lib/preset/prompts.js +6 -0
- package/lib/preset/side.js +12 -23
- package/lib/preset/tools.js +37 -19
- package/lib/routes/api.js +86 -60
- package/lib/routes/index.js +6 -3
- package/lib/routes/pages-html.js +61 -12
- package/lib/store.js +103 -34
- package/lib/types/client/hooks.d.ts +5 -0
- package/lib/types/client/i18n.d.ts +15 -9
- package/lib/types/client/pages/detail.d.ts +11 -2
- package/lib/types/client/pages/format.d.ts +2 -1
- package/lib/types/client/pages/settings.d.ts +11 -6
- package/lib/types/consistency-sweep.d.ts +26 -0
- package/lib/types/domain.d.ts +4 -1
- package/lib/types/events.d.ts +12 -0
- package/lib/types/pref-policy.d.ts +34 -0
- package/lib/types/pref-settings.d.ts +34 -0
- package/lib/types/preset/side.d.ts +6 -7
- package/lib/types/routes/pages-html.d.ts +5 -5
- package/lib/types/store.d.ts +18 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,193 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.5.4] - 2026-08-28
|
|
9
|
+
|
|
10
|
+
> **Note:** trailing chart statistics no longer count future pre-checked days (see
|
|
11
|
+
> *Changed*). Chart cards generated by this version carry a "generated on" stamp so a
|
|
12
|
+
> stale card is distinguishable from current data; cards written by earlier versions
|
|
13
|
+
> replay unchanged, without the stamp.
|
|
14
|
+
|
|
15
|
+
**Charts and records now tell the truth about time.** Future pre-checked days were
|
|
16
|
+
counted by every trailing statistic as if they had already happened — a "how is this
|
|
17
|
+
week going" chart could be inflated by check-ins for days that have not occurred, and a
|
|
18
|
+
task's check-in grid could end up showing nothing but future days while today's check-in
|
|
19
|
+
was invisible. Undoing a check-in said "revert today's?" while actually reverting a
|
|
20
|
+
future pre-check, because the pages let the server fall back to "most recent record".
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- **Chart snapshot stamp**: chart events carry `generatedAt` and the card renders a
|
|
24
|
+
localized "generated on" date; cards generated by older versions replay without it
|
|
25
|
+
(the field is optional and degrades honestly).
|
|
26
|
+
- **"No schedule" ≠ 0%**: in the daily completion-rate trend, days with no scheduled
|
|
27
|
+
opportunity render as an empty slot labelled "no schedule" (tooltip and screen reader
|
|
28
|
+
alike) instead of a 0% data point.
|
|
29
|
+
- The completion-rate chart annotates its subtitle with "including unclaimed tasks"
|
|
30
|
+
when pending tasks count toward the total, so the denominator is never a mystery.
|
|
31
|
+
- **Startup consistency sweep** (`src/consistency-sweep.ts`): check-ins whose task no
|
|
32
|
+
longer exists, tasks whose wish was deleted out from under them, and dangling
|
|
33
|
+
micro-action states are cleaned once per launch through the same cascade the delete
|
|
34
|
+
tools use. Idempotent, runs after activation without blocking it, and pinned by
|
|
35
|
+
`test/integrity.test.ts`.
|
|
36
|
+
- **Task-detail grid window**: the check-in grid shows the last 28 opportunity days up
|
|
37
|
+
to today, plus every checked day — future pre-checks stay visible (they are standing
|
|
38
|
+
commitments), and check-ins outside the opportunity series (deadline-free tasks) make
|
|
39
|
+
the grid their check-in history. Today is never squeezed out of the window, however
|
|
40
|
+
many future days are pre-checked; unchecked future days remain in the "upcoming"
|
|
41
|
+
preview line.
|
|
42
|
+
- **gzip content negotiation** for the standalone pages (`/xingyuan/today`, `/calendar`,
|
|
43
|
+
`/growth`) per RFC 9110 §12.5.3: absent header accepts any coding, an empty value or
|
|
44
|
+
an explicit opt-out gets plaintext, `Vary: Accept-Encoding` is always sent, and the
|
|
45
|
+
compressed template is cached per body.
|
|
46
|
+
- **Request timeout**: client fetches abort after 15 s with a localized message instead
|
|
47
|
+
of hanging forever.
|
|
48
|
+
- **Focus refetch**: page data refetches when the window regains focus if older than
|
|
49
|
+
60 s, and always when the local day has changed — overnight sessions no longer show
|
|
50
|
+
yesterday's "today".
|
|
51
|
+
- **Undo readability**: the detail panel's undo button announces the exact day it will
|
|
52
|
+
revert (screen-reader label), and its visibility no longer requires today to be the
|
|
53
|
+
checked day — a pre-check can be undone right where it was made.
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
- **The cancel dialog said "today" while a different day was reverted.** The today page
|
|
57
|
+
and the task detail panel posted the cancel without a date and the server falls back
|
|
58
|
+
to the most recent check-in — which a future pre-check satisfies. Every page now sends
|
|
59
|
+
the explicit date; the dialog, the toast and the reverted record all name the same day.
|
|
60
|
+
- `weekComparison` folded future pre-checks into weekday buckets as if they were in the
|
|
61
|
+
current week; the week window is now a closed interval from last Monday to today.
|
|
62
|
+
- Wish progress **rounded** instead of flooring: 249/250 displayed as 100% and could
|
|
63
|
+
archive the wish one day early.
|
|
64
|
+
- Checking in an expired (closed) task failed with a generic error; it now explains that
|
|
65
|
+
the deadline must be extended to revive the task before further check-ins.
|
|
66
|
+
- Concurrent writes to the global document (coach style, profile, category colors,
|
|
67
|
+
micro-action state) could lose updates under interleaving; all writers now serialize
|
|
68
|
+
through one promise chain, a failed writer no longer poisons the chain, and no-op
|
|
69
|
+
mutations skip the write entirely.
|
|
70
|
+
- The memory page could deadlock with a perpetual "loading more" spinner when an
|
|
71
|
+
in-flight request was superseded by a newer one (the reset ran on only one of two
|
|
72
|
+
completion paths).
|
|
73
|
+
- The calendar's post-action detail refetch swallowed failures; the panel now shows an
|
|
74
|
+
error state with a retry button instead of silently keeping stale content.
|
|
75
|
+
- A deadline-free one-shot task disappeared from the today page the moment it was
|
|
76
|
+
checked; the completed row now stays revocable for the rest of the day (it never
|
|
77
|
+
reappears the next day).
|
|
78
|
+
- `/xingyuan/api/day` and `/xingyuan/api/range` answered a malformed `date` with an
|
|
79
|
+
empty day list or an unlabelled `RangeError`; both now reject with a structured
|
|
80
|
+
`bad_date` error, consistent with the POST action surface.
|
|
81
|
+
- Creating a task with a name over 100 characters through a page action failed with a
|
|
82
|
+
bare validation message; the error now states the limit and the actual length.
|
|
83
|
+
- The claim button in the calendar is gated to the picked day being today, matching the
|
|
84
|
+
server-side anchor semantics.
|
|
85
|
+
- Display dates across cards, toasts and pages are localized (ISO strings remain only in
|
|
86
|
+
confirm-dialog copy and screen-reader strings, per the documented convention).
|
|
87
|
+
- The standalone growth page used `color-mix()`, which some shells render as an invalid
|
|
88
|
+
declaration; the darker tone is now precomputed in JS.
|
|
89
|
+
|
|
90
|
+
### Changed
|
|
91
|
+
- Trailing chart statistics (trend, daily rate, week comparison, by-category, ranking,
|
|
92
|
+
time-of-day, weekday activity) only count check-ins up to today; the calendar heatmap
|
|
93
|
+
deliberately keeps future pre-checks visible. Chart cards are snapshots of their
|
|
94
|
+
generation time — the coach guide tells the model to regenerate for current data.
|
|
95
|
+
- `cancel_check_in_task` parameter guidance now instructs the model to pass the date
|
|
96
|
+
whenever the user names a day (including "today"), so the "most recent" fallback is a
|
|
97
|
+
deliberate choice rather than an accident.
|
|
98
|
+
- zh copy no longer pads localized dates with spaces ("已撤销 8月29日 的打卡" →
|
|
99
|
+
"已撤销8月29日的打卡"), and the grid legend says "待打卡" where it previously said
|
|
100
|
+
"未来机会日" for what is now only ever today's pending slot.
|
|
101
|
+
|
|
102
|
+
## [0.5.3] - 2026-08-28
|
|
103
|
+
|
|
104
|
+
> **Note:** this is a patch release but it carries one breaking change — the `xingyuan`
|
|
105
|
+
> settings namespace is replaced by `xingyuan-pref` (see *Removed*). Consumers of the
|
|
106
|
+
> `./domain` subpath must also adapt to `makeXingyuanStore()` taking a second, required
|
|
107
|
+
> argument. Read *Upgrading from 0.5.2 or earlier* before updating.
|
|
108
|
+
|
|
109
|
+
**Chat preferences are now always editable.** Settings → XingYuan rendered the
|
|
110
|
+
write-confirmation toggle and the memory-injection limit as permanently disabled until a
|
|
111
|
+
XingYuan session had been opened once since the last dsh start; clicking them did nothing
|
|
112
|
+
and produced no error. Both preferences lived in the preset-layer namespace, which only
|
|
113
|
+
comes into existence when the preset is first mounted, so they now live in a
|
|
114
|
+
bundle-resident namespace registered whenever the bundle row is active.
|
|
115
|
+
|
|
116
|
+
### Upgrading from 0.5.2 or earlier
|
|
117
|
+
|
|
118
|
+
The two chat preferences moved from the `xingyuan` block of `$DSH_HOME/settings.yaml` to a
|
|
119
|
+
new `xingyuan-pref` block. **A value you had customised is not carried over** and falls back
|
|
120
|
+
to the default: `confirmWrites` returns to `true` (the safe direction — write actions ask for
|
|
121
|
+
confirmation again) and `memoryInjectLimit` to `40`. Re-apply your choices under
|
|
122
|
+
**Settings → XingYuan → Chat preferences**. The leftover `xingyuan:` block is inert and is
|
|
123
|
+
safe to delete.
|
|
124
|
+
|
|
125
|
+
### Added
|
|
126
|
+
- **`xingyuan-pref` settings namespace** (`src/pref-settings.ts`), installed by the bundle
|
|
127
|
+
layer next to the existing `xingyuan-ui` namespace, carrying `confirmWrites` (default
|
|
128
|
+
`true`) and `memoryInjectLimit` (default `40`, integer `5`–`200`). The schema declares
|
|
129
|
+
`.step(1)`, so the Host also rejects fractional values that a hand-edited settings
|
|
130
|
+
document or a direct RPC write could otherwise smuggle in.
|
|
131
|
+
- **Shared preference policy module** (`src/pref-policy.ts`) holding the bounds, defaults
|
|
132
|
+
and input parser for both halves — the host module depends on
|
|
133
|
+
`@deepseek-ai/dsh-settings` and must never reach the browser bundle. This also removes
|
|
134
|
+
the `5`/`200`/`40` literals that were previously repeated in the settings page. Covered
|
|
135
|
+
by `test/pref-policy.test.ts`.
|
|
136
|
+
- **Write verification**: `scope.set()` resolves rather than rejects on failure (the client
|
|
137
|
+
controller catches, recovers and returns silently), so `toastError` never fired. The page
|
|
138
|
+
now compares the snapshot after each write and raises a "did not save" toast when the
|
|
139
|
+
value did not change. Both controls share one scope — and therefore one write queue — so
|
|
140
|
+
the comparison is gated on a per-component write sequence number to stay honest under
|
|
141
|
+
interleaved writes (see below).
|
|
142
|
+
- **Regression lock** (`test/pref-settings.test.ts`): five tests pinning that the bundle
|
|
143
|
+
layer registers `xingyuan-pref` as soon as the settings service is available, that the
|
|
144
|
+
read thunk resolves fresh on every call, that the headless path falls back to the schema
|
|
145
|
+
defaults, and — structurally — that **no namespace bound by the settings page is ever
|
|
146
|
+
registered from the preset layer**. That last one is the abstract shape of this bug and is
|
|
147
|
+
what stops it from coming back.
|
|
148
|
+
- **Four-way readiness notice** for the preferences card: loading, remote/temporary
|
|
149
|
+
connection (`mode === 'memory'`), namespace not registered, and read-only document. The
|
|
150
|
+
previous code only covered the first two, leaving the rest as silently disabled controls.
|
|
151
|
+
|
|
152
|
+
### Fixed
|
|
153
|
+
- The two preferences could not be changed before the first XingYuan session of the process
|
|
154
|
+
— the root cause described above.
|
|
155
|
+
- The write-confirmation toggle rendered as **on** whenever its value was unavailable
|
|
156
|
+
(`value?.confirmWrites !== false` reads "unknown" as "true"), which is the worst possible
|
|
157
|
+
default for a safety switch. It now falls back to the schema default explicitly.
|
|
158
|
+
- Namespace-unavailable and read-only states disabled the controls with no explanation.
|
|
159
|
+
- The "did not save" toast could fire on a write that **had** been saved. Both controls share
|
|
160
|
+
one settings scope, and the client controller fences writes with a generation counter: when
|
|
161
|
+
a write is superseded by a newer one it only records a pending revision and does **not**
|
|
162
|
+
fold the result into the snapshot, while the superseded write's `.then` still runs first and
|
|
163
|
+
sees the stale value. Verification is now skipped for superseded writes, and the number
|
|
164
|
+
input got the pending guard the toggle already had.
|
|
165
|
+
- Clearing the memory-injection-limit field and blurring reported "enter an integer between 5
|
|
166
|
+
and 200" while simultaneously refilling the field with the saved value — two contradictory
|
|
167
|
+
messages at once. An empty field is again treated as abandoning the edit.
|
|
168
|
+
- Tool descriptions for the creation / check-in / cancel-check-in tools asserted that a system
|
|
169
|
+
confirmation card would appear. Toggle **Confirm write actions** off and the model kept
|
|
170
|
+
telling users a card had been shown when none was. Tool descriptions are baked in at
|
|
171
|
+
registration and dsh-tools requires a plain string (no getter), so they are now worded to
|
|
172
|
+
hold in both states instead of asserting the card. Delete tools are unaffected — deletes
|
|
173
|
+
always confirm.
|
|
174
|
+
|
|
175
|
+
### Changed
|
|
176
|
+
- The preset layer no longer registers a settings namespace. It reads preferences through
|
|
177
|
+
`ctx.xingyuan.prefs()` — a thunk that resolves the current value on every call, so edits
|
|
178
|
+
still take effect immediately without re-registering tools or prompts. The remaining
|
|
179
|
+
`Config` fields (`batchWishLimit`, `chartTrendDays`, …) have no settings UI and stay as
|
|
180
|
+
composition-only entry config, which matches the upstream rule that a namespace should
|
|
181
|
+
carry only the user-editable subset.
|
|
182
|
+
- **Breaking for `./domain` consumers**: `makeXingyuanStore(domain)` now takes a second,
|
|
183
|
+
required argument — `makeXingyuanStore(domain, readPrefs)`, where `readPrefs` is a thunk
|
|
184
|
+
returning the resolved chat preferences. The required-ness is deliberate: every call site
|
|
185
|
+
(one in production, five in tests) is surfaced by the compiler rather than silently
|
|
186
|
+
missing the wiring. `XingyuanStore` gained a matching `prefs()` method.
|
|
187
|
+
|
|
188
|
+
### Removed
|
|
189
|
+
- The preset-layer `xingyuan` settings namespace and the `confirmWrites` /
|
|
190
|
+
`memoryInjectLimit` fields of the preset `Config`. **A value customised under the old
|
|
191
|
+
`xingyuan:` section of `$DSH_HOME/settings.yaml` is no longer read and falls back to the
|
|
192
|
+
default** — `confirmWrites` therefore returns to `true`, the conservative direction. The
|
|
193
|
+
stale section is left in the document and is harmless.
|
|
194
|
+
|
|
8
195
|
## [0.5.2] - 2026-08-27
|
|
9
196
|
|
|
10
197
|
### Added
|
package/lib/client/api.js
CHANGED
|
@@ -40,8 +40,27 @@ export function describeError(e) {
|
|
|
40
40
|
export async function postAction(path, body) {
|
|
41
41
|
return postJson(`/xingyuan/api/action/${path}`, body);
|
|
42
42
|
}
|
|
43
|
+
/** 请求超时:手动 AbortController——AbortSignal.timeout 为 Baseline 2024 "Newly available",
|
|
44
|
+
* 不满足 dsh 壳的保守浏览器矩阵(同 color-mix 禁令的判断口径),不用。 */
|
|
45
|
+
const REQUEST_TIMEOUT_MS = 15_000;
|
|
46
|
+
async function fetchWithTimeout(path, init) {
|
|
47
|
+
const controller = new AbortController();
|
|
48
|
+
const timer = window.setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
49
|
+
try {
|
|
50
|
+
return await fetch(path, { ...init, signal: controller.signal });
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
// 超时中止统一转本地化文案;真实网络错误原样抛出(保留连接拒绝等语义)
|
|
54
|
+
if (e instanceof DOMException && e.name === 'AbortError')
|
|
55
|
+
throw new Error(t('common.requestTimeout'));
|
|
56
|
+
throw e;
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
window.clearTimeout(timer);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
43
62
|
export async function postJson(path, body) {
|
|
44
|
-
const response = await
|
|
63
|
+
const response = await fetchWithTimeout(path, {
|
|
45
64
|
method: 'POST',
|
|
46
65
|
headers: { 'content-type': 'application/json' },
|
|
47
66
|
body: JSON.stringify(body),
|
|
@@ -49,7 +68,7 @@ export async function postJson(path, body) {
|
|
|
49
68
|
return parsePayload(response, () => t('common.actionFailed'));
|
|
50
69
|
}
|
|
51
70
|
export async function getJson(path) {
|
|
52
|
-
const response = await
|
|
71
|
+
const response = await fetchWithTimeout(path, { headers: { accept: 'application/json' } });
|
|
53
72
|
return parsePayload(response, () => t('common.requestFailed'));
|
|
54
73
|
}
|
|
55
74
|
async function parsePayload(response, fallback) {
|
package/lib/client/cards.js
CHANGED
|
@@ -11,7 +11,7 @@ import { toast, toastError } from './ui.js';
|
|
|
11
11
|
import { localYmd, softConfirm } from './hooks.js';
|
|
12
12
|
import { useXyT, t, activeLocale } from './i18n.js';
|
|
13
13
|
import { categoryVars } from '../category-color.js';
|
|
14
|
-
import { cycleLabel, durationText, dateSuffix } from './pages/format.js';
|
|
14
|
+
import { cycleLabel, durationText, dateSuffix, formatMediumDate, formatShortDate } from './pages/format.js';
|
|
15
15
|
function WishView(props) {
|
|
16
16
|
const t = useXyT();
|
|
17
17
|
const wish = props.node.data.data;
|
|
@@ -95,7 +95,7 @@ function TaskView(props) {
|
|
|
95
95
|
const showClaim = !deleted && phase === 'idle' && task.status === 'pending';
|
|
96
96
|
const showCheckIn = !deleted && phase !== 'done' && (task.status === 'in_progress' || phase === 'claimed');
|
|
97
97
|
const preview = event.opportunityPreview.length > 0
|
|
98
|
-
? t('task.upcoming', { dates: event.opportunityPreview.join(activeLocale() === 'en' ? ', ' : '、') })
|
|
98
|
+
? t('task.upcoming', { dates: event.opportunityPreview.map((date) => formatShortDate(date)).join(activeLocale() === 'en' ? ', ' : '、') })
|
|
99
99
|
: undefined;
|
|
100
100
|
return createElement('div', { className: `xy-card ${deleted ? 'xy-deleted' : ''}` }, createElement('div', { className: 'xy-card-head' }, createElement('span', { className: 'xy-badge xy-badge-task' }, t('badge.task')), createElement('span', { className: 'xy-title' }, deleted ? t('state.deleted', { title: task.name }) : task.name), !deleted && phase === 'done'
|
|
101
101
|
? createElement('span', { className: 'xy-meta xy-actioned' },
|
|
@@ -105,7 +105,7 @@ function TaskView(props) {
|
|
|
105
105
|
? createElement('button', { className: 'xy-btn xy-btn-inline', disabled: busy, onClick: () => run('claim') }, t('action.claim'))
|
|
106
106
|
: null, showCheckIn
|
|
107
107
|
? createElement('button', { className: 'xy-btn xy-btn-primary xy-btn-inline', disabled: busy, onClick: () => run('checkin') }, t('action.checkin'))
|
|
108
|
-
: null), createElement('div', { className: 'xy-meta' }, `${cycleLabel(task.checkInCycle)}${task.dueDate !== undefined ? ` · ${t('task.due', { date: task.dueDate })}` : ''} · ${durationText(task.completedDays, task.requiredDays)}`), deleted === false && preview !== undefined
|
|
108
|
+
: null), createElement('div', { className: 'xy-meta' }, `${cycleLabel(task.checkInCycle)}${task.dueDate !== undefined ? ` · ${t('task.due', { date: formatShortDate(task.dueDate) })}` : ''} · ${durationText(task.completedDays, task.requiredDays)}`), deleted === false && preview !== undefined
|
|
109
109
|
? createElement('div', { className: 'xy-preview' }, preview)
|
|
110
110
|
: null);
|
|
111
111
|
}
|
|
@@ -139,23 +139,29 @@ function CheckinView(props) {
|
|
|
139
139
|
const event = props.node.data.data;
|
|
140
140
|
const cancelled = event.op === 'cancelled';
|
|
141
141
|
return createElement('div', { className: 'xy-card xy-checkin' }, createElement('div', { className: 'xy-card-head' }, createElement('span', { className: `xy-glyph ${cancelled ? 'xy-glyph-back' : 'xy-glyph-ok'}`, 'aria-hidden': 'true' }, cancelled ? '↩' : '✓'), createElement('span', { className: 'xy-title' }, cancelled
|
|
142
|
-
? t('checkin.cancelled', { task: event.taskName, date: event.date })
|
|
143
|
-
: t('checkin.success', { task: event.taskName }))), createElement('div', { className: 'xy-meta' }, `${event.date}${event.wishName !== undefined ? ` · ${event.wishName}` : ''} · ${durationText(event.completedDays, event.requiredDays)}`));
|
|
142
|
+
? t('checkin.cancelled', { task: event.taskName, date: formatShortDate(event.date) })
|
|
143
|
+
: t('checkin.success', { task: event.taskName }))), createElement('div', { className: 'xy-meta' }, `${formatShortDate(event.date)}${event.wishName !== undefined ? ` · ${event.wishName}` : ''} · ${durationText(event.completedDays, event.requiredDays)}`));
|
|
144
144
|
}
|
|
145
145
|
function ChartView(props) {
|
|
146
146
|
const t = useXyT();
|
|
147
147
|
const event = props.node.data.data;
|
|
148
148
|
// 悬停柱下标(与成长页悬浮明细同一交互语法;null = 未悬停)
|
|
149
149
|
const [hoverIdx, setHoverIdx] = useState(null);
|
|
150
|
-
return createElement('div', { className: 'xy-card xy-chart' }, createElement('div', { className: 'xy-card-head' }, createElement('span', { className: 'xy-badge xy-badge-chart' }, t('badge.chart')), createElement('span', { className: 'xy-title' }, event.title), event.subtitle !== undefined ? createElement('span', { className: 'xy-meta' }, event.subtitle) : null),
|
|
150
|
+
return createElement('div', { className: 'xy-card xy-chart' }, createElement('div', { className: 'xy-card-head' }, createElement('span', { className: 'xy-badge xy-badge-chart' }, t('badge.chart')), createElement('span', { className: 'xy-title' }, event.title), event.subtitle !== undefined ? createElement('span', { className: 'xy-meta' }, event.subtitle) : null),
|
|
151
|
+
// 快照时点标注(事件溯源:卡片=当时的事实):无 generatedAt 的旧事件诚实降级不显示。
|
|
152
|
+
// 中格式带年份——回放的旧卡可能跨年,「生成于 3月5日」须能定位到哪一年。
|
|
153
|
+
// ISO → 本地日再格式化:直接 slice 会把 UTC 日期当本地日(UTC+8 零点后 8 小时内差一天)
|
|
154
|
+
event.generatedAt !== undefined
|
|
155
|
+
? createElement('div', { className: 'xy-meta' }, t('chart.generatedAt', { date: formatMediumDate(localYmd(new Date(event.generatedAt))) }))
|
|
156
|
+
: null, chartBody(event, t('chart.noData'), t('chart.noSchedule'), hoverIdx, setHoverIdx));
|
|
151
157
|
}
|
|
152
158
|
/** 图表卡主体:四类渲染路径(占比条/占比列表/热力格/柱图)颜色全部走主题变量,深浅色自适应。 */
|
|
153
|
-
function chartBody(event, noDataText, hoverIdx, onHover) {
|
|
159
|
+
function chartBody(event, noDataText, noScheduleText, hoverIdx, onHover) {
|
|
154
160
|
const { chartType, data } = event;
|
|
155
161
|
if (data.length === 0)
|
|
156
162
|
return createElement('div', { className: 'xy-meta' }, noDataText);
|
|
157
|
-
const max = Math.max(...data.map((d) => d.value), 1);
|
|
158
|
-
const labelValue = (d) => `${d.label}${activeLocale() === 'en' ? ': ' : ':'}${d.value}`;
|
|
163
|
+
const max = Math.max(...data.filter((d) => !d.inactive).map((d) => d.value), 1);
|
|
164
|
+
const labelValue = (d) => `${d.label}${activeLocale() === 'en' ? ': ' : ':'}${d.inactive === true ? noScheduleText : d.value}`;
|
|
159
165
|
if (chartType === 'arcbars') {
|
|
160
166
|
const ratio = Math.min(Math.max(data[0].ratio ?? 0, 0), 1);
|
|
161
167
|
return createElement('div', { className: 'xy-arcwrap' }, createElement('div', { className: 'xy-arcnum' }, `${Math.round(ratio * 100)}%`),
|
|
@@ -187,19 +193,22 @@ function chartBody(event, noDataText, hoverIdx, onHover) {
|
|
|
187
193
|
const r = Math.min(2, h, w / 2);
|
|
188
194
|
return `M${x},${y + h}L${x},${y + r}Q${x},${y} ${x + r},${y}L${x + w - r},${y}Q${x + w},${y} ${x + w},${y + r}L${x + w},${y + h}Z`;
|
|
189
195
|
};
|
|
190
|
-
const bars = data.
|
|
196
|
+
const bars = data.flatMap((d, i) => {
|
|
197
|
+
// 无安排日不画柱(缺失≠0):空槽由下标占位保留,柱序与日期轴不塌缩
|
|
198
|
+
if (d.inactive === true)
|
|
199
|
+
return [];
|
|
191
200
|
// 微值可见性下限:有值但按比例不足 2px 的柱给 2px,避免「有数据却看不见」
|
|
192
201
|
const rawH = Math.round((d.value / max) * (height - 24));
|
|
193
202
|
const h = d.value > 0 ? Math.max(rawH, 2) : rawH;
|
|
194
203
|
const seriesIdx = d.series !== undefined ? seriesList.indexOf(d.series) : -1;
|
|
195
|
-
return createElement('path', {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
204
|
+
return [createElement('path', {
|
|
205
|
+
key: i,
|
|
206
|
+
d: topRoundedBar(i * step + 2, height - 18 - h, Math.max(step - 4, 2), h),
|
|
207
|
+
style: { fill: seriesIdx > 0 ? seriesColor(seriesIdx) : 'var(--xyd-accent)', opacity: seriesIdx > 0 ? 0.55 : 0.85 },
|
|
208
|
+
onMouseEnter: () => onHover(i),
|
|
209
|
+
},
|
|
210
|
+
// 每根柱子带 <title>:原生悬停提示;精确数值另由上方悬浮明细条实时呈现
|
|
211
|
+
createElement('title', null, labelValue(d)))];
|
|
203
212
|
});
|
|
204
213
|
// 标签用原始下标定位:indexOf 会把重复标签(如分组系列的「周一」×2)全部钉到首个位置
|
|
205
214
|
const labelStep = data.length <= 10 ? 1 : Math.ceil(data.length / 8);
|
|
@@ -221,7 +230,7 @@ function chartBody(event, noDataText, hoverIdx, onHover) {
|
|
|
221
230
|
createElement('svg', {
|
|
222
231
|
viewBox: `0 0 ${width} ${height}`, className: 'xy-svg',
|
|
223
232
|
role: 'img', 'aria-label': event.title,
|
|
224
|
-
}, createElement('line', { x1: 0, y1: height - 18, x2: width, y2: height - 18, style: { stroke: 'var(--dsw-alias-border-l2)' } }), ...bars, ...labels)), createElement('span', { className: 'xy-visually-hidden' }, data.map((d) => `${d.label} ${d.value}`).join(activeLocale() === 'en' ? '; ' : ';')));
|
|
233
|
+
}, createElement('line', { x1: 0, y1: height - 18, x2: width, y2: height - 18, style: { stroke: 'var(--dsw-alias-border-l2)' } }), ...bars, ...labels)), createElement('span', { className: 'xy-visually-hidden' }, data.map((d) => `${d.label} ${d.inactive === true ? noScheduleText : d.value}`).join(activeLocale() === 'en' ? '; ' : ';')));
|
|
225
234
|
}
|
|
226
235
|
export const CARD_VIEWS = {
|
|
227
236
|
WishView,
|
package/lib/client/hooks.js
CHANGED
|
@@ -18,10 +18,18 @@ export function recentRangeDays(days) {
|
|
|
18
18
|
end: localYmd(now),
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
|
+
/** 窗口重新可见后的重取门槛(ms):等价 TanStack Query 的 staleTime——
|
|
22
|
+
* 数据在此年龄内视为新鲜,聚焦不重取;跨天翻篇则无视门槛一律重取。 */
|
|
23
|
+
const FOCUS_REFETCH_MIN_AGE_MS = 60_000;
|
|
21
24
|
/**
|
|
22
25
|
* 取数三态统一:挂载(或 deps 变化)即拉取,error 存本地化文案,
|
|
23
26
|
* reload 供错误态按钮与动作后刷新复用。path 支持函数形态(如日历按月取参);
|
|
24
27
|
* 内置序号守卫:快速翻页/连点时慢的旧响应不得覆盖新状态。
|
|
28
|
+
*
|
|
29
|
+
* 跨天/回焦自动新鲜化(习惯类应用的基本预期——凌晨挂着的今日页不能还是昨天):
|
|
30
|
+
* 监听 visibilitychange/focus,翻篇(本地日期变化)必重取,未翻篇仅当数据年龄
|
|
31
|
+
* 超过门槛才重取(有节流上限,不空转不定时器)。NN/g「系统应始终让用户知情」:
|
|
32
|
+
* 时间流逝导致的状态变化要在回到页面时即刻呈现。
|
|
25
33
|
*/
|
|
26
34
|
export function usePageData(path, deps = []) {
|
|
27
35
|
const [data, setData] = useState(undefined);
|
|
@@ -29,9 +37,13 @@ export function usePageData(path, deps = []) {
|
|
|
29
37
|
const seqRef = useRef(0);
|
|
30
38
|
const pathRef = useRef(path);
|
|
31
39
|
pathRef.current = path;
|
|
40
|
+
const lastLoadAtRef = useRef(0);
|
|
41
|
+
const lastLoadDayRef = useRef(undefined);
|
|
32
42
|
const load = useCallback(() => {
|
|
33
43
|
const seq = ++seqRef.current;
|
|
34
44
|
setError(undefined);
|
|
45
|
+
lastLoadAtRef.current = Date.now();
|
|
46
|
+
lastLoadDayRef.current = localYmd(new Date());
|
|
35
47
|
return getJson(typeof pathRef.current === 'function' ? pathRef.current() : pathRef.current)
|
|
36
48
|
.then((payload) => { if (seq === seqRef.current)
|
|
37
49
|
setData(payload); })
|
|
@@ -39,6 +51,23 @@ export function usePageData(path, deps = []) {
|
|
|
39
51
|
setError(describeError(e)); });
|
|
40
52
|
}, deps);
|
|
41
53
|
useEffect(() => { void load(); }, [load]);
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
const maybeReload = () => {
|
|
56
|
+
if (document.visibilityState !== 'visible')
|
|
57
|
+
return;
|
|
58
|
+
const today = localYmd(new Date());
|
|
59
|
+
// 翻篇必取;未翻篇按数据年龄节流(TanStack Query focus 重取的 staleTime 语义)
|
|
60
|
+
if (lastLoadDayRef.current !== today || Date.now() - lastLoadAtRef.current > FOCUS_REFETCH_MIN_AGE_MS) {
|
|
61
|
+
void load();
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
document.addEventListener('visibilitychange', maybeReload);
|
|
65
|
+
window.addEventListener('focus', maybeReload);
|
|
66
|
+
return () => {
|
|
67
|
+
document.removeEventListener('visibilitychange', maybeReload);
|
|
68
|
+
window.removeEventListener('focus', maybeReload);
|
|
69
|
+
};
|
|
70
|
+
}, [load]);
|
|
42
71
|
return { data, error, reload: load };
|
|
43
72
|
}
|
|
44
73
|
/**
|
package/lib/client/i18n.js
CHANGED
|
@@ -41,6 +41,7 @@ const ZH_DICT = {
|
|
|
41
41
|
'common.edit': '编辑',
|
|
42
42
|
'common.requestFailed': '请求失败',
|
|
43
43
|
'common.actionFailed': '操作失败',
|
|
44
|
+
'common.requestTimeout': '请求超时,请重试',
|
|
44
45
|
'common.dayUnit': '{n} 天',
|
|
45
46
|
'common.countUnit': '{n} 次',
|
|
46
47
|
'common.countUnitOne': '{n} 次',
|
|
@@ -86,18 +87,18 @@ const ZH_DICT = {
|
|
|
86
87
|
'action.checkinFuture': '提前打卡 {date}',
|
|
87
88
|
'action.checkinThisDay': '打卡此日',
|
|
88
89
|
'action.undoCheckin': '撤销',
|
|
89
|
-
'action.cancelCheckin': '取消打卡',
|
|
90
|
+
'action.cancelCheckin': '取消打卡',
|
|
91
|
+
'action.cancelCheckinAria': '撤销{date}的打卡', 'action.expand': '展开详情',
|
|
90
92
|
'action.collapse': '收起详情',
|
|
91
93
|
'action.createWish': '+ 新建愿望',
|
|
92
94
|
'action.createTask': '+ 新建任务',
|
|
93
95
|
'action.manageCategories': '分类管理',
|
|
94
96
|
'action.askAi': '让 AI 总结',
|
|
95
|
-
'today.notCheckinDay': '今天不是打卡日',
|
|
96
97
|
// --- 动作反馈 toast ---
|
|
97
98
|
'toast.claimed': '已领取「{name}」,去打卡吧',
|
|
98
99
|
'toast.checkinOk': '打卡成功',
|
|
99
100
|
'toast.undone': '打卡已撤销',
|
|
100
|
-
'toast.undoneAt': '已撤销
|
|
101
|
+
'toast.undoneAt': '已撤销{date}的打卡',
|
|
101
102
|
'toast.wishCreated': '已创建愿望「{title}」',
|
|
102
103
|
'toast.taskCreated': '已创建任务「{name}」',
|
|
103
104
|
'toast.deleted': '已删除「{name}」',
|
|
@@ -106,10 +107,10 @@ const ZH_DICT = {
|
|
|
106
107
|
'toast.categoryColorReset': '「{name}」配色已重置,跟随愿望默认色',
|
|
107
108
|
'toast.categoryDeleted': '已清除「{name}」的自定义颜色',
|
|
108
109
|
// --- 确认弹窗 ---
|
|
109
|
-
'confirm.futureCheckin': '「{name}」今天不是打卡日,提前打卡
|
|
110
|
+
'confirm.futureCheckin': '「{name}」今天不是打卡日,提前打卡{date}表示承诺当天完成。确认?',
|
|
110
111
|
'confirm.undoToday': '确定撤销「{name}」今天的打卡吗?进度将回退。',
|
|
111
|
-
'confirm.undoAt': '确定撤销「{name}」在
|
|
112
|
-
'confirm.futureAt': '「{name}
|
|
112
|
+
'confirm.undoAt': '确定撤销「{name}」在{date}的打卡吗?进度将回退。',
|
|
113
|
+
'confirm.futureAt': '「{name}」{date}不是打卡日,提前打卡表示承诺当天完成。确认?',
|
|
113
114
|
'confirm.deleteTask': '确定删除任务「{name}」吗?其打卡记录将一并删除,不可恢复。',
|
|
114
115
|
'confirm.deleteWish': '确定删除愿望「{name}」吗?下属任务与打卡记录将一并删除,不可恢复。',
|
|
115
116
|
'confirm.renameCategory': '把分类「{old}」下的全部愿望改名为「{new}」吗?(共 {count} 个)', 'confirm.deleteEmptyCategory': '删除空分类「{name}」的自定义颜色吗?',
|
|
@@ -230,11 +231,14 @@ const ZH_DICT = {
|
|
|
230
231
|
'settings.pref.title': '对话偏好',
|
|
231
232
|
'settings.pref.unavailable': '当前连接不支持持久化偏好(远程或临时模式),以下两项不可用。',
|
|
232
233
|
'settings.pref.loading': '偏好加载中…',
|
|
234
|
+
'settings.pref.notRegistered': '偏好暂不可用(组件未就绪),请稍后重试。',
|
|
235
|
+
'settings.pref.readOnly': '当前设置文档为只读,无法保存修改。',
|
|
233
236
|
'settings.pref.confirmWrites': '写操作二次确认',
|
|
234
237
|
'settings.pref.confirmWritesHint': '创建愿望/任务、打卡、取消打卡时先弹应用内确认卡(删除始终确认);关闭后对话中的这类操作将直接执行。',
|
|
235
238
|
'settings.pref.memoryLimit': '记忆注入上限',
|
|
236
239
|
'settings.pref.memoryLimitHint': '每次对话自动注入上下文的记忆条数上限(5-200,默认 40);失焦后保存。',
|
|
237
240
|
'settings.pref.limitInvalid': '请输入 5-200 的整数',
|
|
241
|
+
'settings.pref.writeFailed': '保存未生效,请重试。',
|
|
238
242
|
'settings.tabs.title': '标签页显示',
|
|
239
243
|
'settings.tabs.mode.follow': '跟随会话',
|
|
240
244
|
'settings.tabs.mode.show': '始终显示',
|
|
@@ -254,18 +258,20 @@ const ZH_DICT = {
|
|
|
254
258
|
'micro.progress': '进度 {done}/{total} 完成',
|
|
255
259
|
// --- 打卡卡 ---
|
|
256
260
|
'checkin.success': '「{task}」打卡成功',
|
|
257
|
-
'checkin.cancelled': '已取消「{task}」{date}
|
|
261
|
+
'checkin.cancelled': '已取消「{task}」{date}的打卡',
|
|
258
262
|
// --- 图表卡 ---
|
|
259
263
|
'chart.noData': '暂无数据',
|
|
264
|
+
'chart.noSchedule': '无安排',
|
|
265
|
+
'chart.generatedAt': '生成于 {date}',
|
|
260
266
|
// --- 详情聚合视图 ---
|
|
261
267
|
'detail.grid.title': '打卡记录',
|
|
262
|
-
'detail.grid.summary': '近 {total}
|
|
268
|
+
'detail.grid.summary': '近 {total} 格:已打 {checked},未打 {missed},待打卡 {future}',
|
|
263
269
|
'detail.legend.checked': '已打卡',
|
|
264
270
|
'detail.legend.missed': '未打卡',
|
|
265
|
-
'detail.legend.future': '
|
|
271
|
+
'detail.legend.future': '待打卡',
|
|
266
272
|
'detail.grid.checked': '{date} 已打卡',
|
|
267
273
|
'detail.grid.missed': '{date} 未打卡',
|
|
268
|
-
'detail.grid.future': '{date}
|
|
274
|
+
'detail.grid.future': '{date} 待打卡',
|
|
269
275
|
'detail.next.title': '接下来的机会日:{dates}',
|
|
270
276
|
'detail.micro.title': '微行动进度',
|
|
271
277
|
'detail.micro.idle': '尚未开始微行动拆解(可在对话里说「帮我拆解」)。',
|
|
@@ -356,6 +362,7 @@ const EN_DICT = {
|
|
|
356
362
|
'common.edit': 'Edit',
|
|
357
363
|
'common.requestFailed': 'Request failed',
|
|
358
364
|
'common.actionFailed': 'Action failed',
|
|
365
|
+
'common.requestTimeout': 'Request timed out, please retry',
|
|
359
366
|
'common.dayUnit': '{n} days',
|
|
360
367
|
'common.countUnit': '{n} times',
|
|
361
368
|
'common.countUnitOne': '{n} time',
|
|
@@ -398,13 +405,13 @@ const EN_DICT = {
|
|
|
398
405
|
'action.checkinThisDay': 'Check in',
|
|
399
406
|
'action.undoCheckin': 'Undo',
|
|
400
407
|
'action.cancelCheckin': 'Undo check-in',
|
|
408
|
+
'action.cancelCheckinAria': 'Undo the check-in on {date}',
|
|
401
409
|
'action.expand': 'Details',
|
|
402
410
|
'action.collapse': 'Hide',
|
|
403
411
|
'action.createWish': '+ New wish',
|
|
404
412
|
'action.createTask': '+ New task',
|
|
405
413
|
'action.manageCategories': 'Categories',
|
|
406
414
|
'action.askAi': 'AI summary',
|
|
407
|
-
'today.notCheckinDay': 'Not a check-in day today',
|
|
408
415
|
'toast.claimed': 'Claimed “{name}”. Time to check in!',
|
|
409
416
|
'toast.checkinOk': 'Checked in',
|
|
410
417
|
'toast.undone': 'Check-in reverted',
|
|
@@ -536,11 +543,14 @@ const EN_DICT = {
|
|
|
536
543
|
'settings.pref.title': 'Chat preferences',
|
|
537
544
|
'settings.pref.unavailable': 'This connection cannot persist preferences (remote or temporary mode); the two options below are disabled.',
|
|
538
545
|
'settings.pref.loading': 'Loading preferences…',
|
|
546
|
+
'settings.pref.notRegistered': 'Preferences are unavailable right now (component not ready); please retry later.',
|
|
547
|
+
'settings.pref.readOnly': 'The settings document is read-only; changes cannot be saved.',
|
|
539
548
|
'settings.pref.confirmWrites': 'Confirm write actions',
|
|
540
549
|
'settings.pref.confirmWritesHint': 'Show an in-app confirmation before creating wishes/tasks, checking in or canceling (deletes always confirm). Turn off to execute directly.',
|
|
541
550
|
'settings.pref.memoryLimit': 'Memory injection limit',
|
|
542
551
|
'settings.pref.memoryLimitHint': 'Max memories auto-injected per chat (5-200, default 40); saved on blur.',
|
|
543
552
|
'settings.pref.limitInvalid': 'Enter an integer between 5 and 200',
|
|
553
|
+
'settings.pref.writeFailed': 'The change did not save; please try again.',
|
|
544
554
|
'settings.tabs.title': 'Tab visibility',
|
|
545
555
|
'settings.tabs.mode.follow': 'Follow session',
|
|
546
556
|
'settings.tabs.mode.show': 'Always show',
|
|
@@ -560,14 +570,16 @@ const EN_DICT = {
|
|
|
560
570
|
'checkin.success': '“{task}” checked in',
|
|
561
571
|
'checkin.cancelled': 'Cancelled the check-in of “{task}” on {date}',
|
|
562
572
|
'chart.noData': 'No data yet',
|
|
573
|
+
'chart.noSchedule': 'Not scheduled',
|
|
574
|
+
'chart.generatedAt': 'Generated on {date}',
|
|
563
575
|
'detail.grid.title': 'Check-in record',
|
|
564
|
-
'detail.grid.summary': 'Last {total}
|
|
576
|
+
'detail.grid.summary': 'Last {total} cells: {checked} checked, {missed} missed, {future} pending',
|
|
565
577
|
'detail.legend.checked': 'Checked in',
|
|
566
578
|
'detail.legend.missed': 'Missed',
|
|
567
|
-
'detail.legend.future': '
|
|
579
|
+
'detail.legend.future': 'Pending today',
|
|
568
580
|
'detail.grid.checked': '{date} checked in',
|
|
569
581
|
'detail.grid.missed': '{date} missed',
|
|
570
|
-
'detail.grid.future': '{date}
|
|
582
|
+
'detail.grid.future': '{date} pending check-in',
|
|
571
583
|
'detail.next.title': 'Upcoming opportunity dates: {dates}',
|
|
572
584
|
'detail.micro.title': 'Micro-step progress',
|
|
573
585
|
'detail.micro.idle': 'No micro-step plan yet (ask “help me break it down” in chat).',
|
package/lib/client/index.js
CHANGED
|
@@ -79,10 +79,12 @@ export function apply(ctx) {
|
|
|
79
79
|
// label 用 thunk 跟随当前语言;locale 声明让壳在语言切换时刷新标签行。
|
|
80
80
|
ctx.slots.inject('conversation.view', () => installTabVisibility(ctx));
|
|
81
81
|
// 星愿设置整页(设置 → 星愿):教练风格/画像(星愿库)+ 二次确认开关与注入上限
|
|
82
|
-
// (
|
|
82
|
+
// (bundle 常驻命名空间 xingyuan-pref)+ 标签页显隐(bundle 常驻命名空间 xingyuan-ui)。
|
|
83
|
+
// 两个偏好命名空间都挂在常驻层:整页由本文件无条件注册,命名空间若随 preset 懒加载
|
|
84
|
+
// 缺席,就会出现「整页可见但两项写不进去且静默失败」。
|
|
83
85
|
ctx.slots.inject('settings.section', () => {
|
|
84
|
-
const
|
|
86
|
+
const prefscope = ctx.settingsScope.bind({ namespace: 'xingyuan-pref' });
|
|
85
87
|
const uiscope = ctx.settingsScope.bind({ namespace: 'xingyuan-ui' });
|
|
86
|
-
return ctx.slots.register({ name: 'settings.section', id: 'xingyuan', order: 60, label: () => t('settings.tabLabel'), locale: XY_NS }, () => SettingsSection({ scope, uiscope }));
|
|
88
|
+
return ctx.slots.register({ name: 'settings.section', id: 'xingyuan', order: 60, label: () => t('settings.tabLabel'), locale: XY_NS }, () => SettingsSection({ scope: prefscope, uiscope }));
|
|
87
89
|
});
|
|
88
90
|
}
|
|
@@ -4,7 +4,7 @@ import { getJson, postAction } from '../api.js';
|
|
|
4
4
|
import { t } from '../i18n.js';
|
|
5
5
|
import { softConfirm, useActionGuard, usePageData, useStableScrollbar, localYmd } from '../hooks.js';
|
|
6
6
|
import { PageError, PageSkeleton, toast } from '../ui.js';
|
|
7
|
-
import { cycleLabel, dateSuffix, formatFriendlyDate, formatMonth } from './format.js';
|
|
7
|
+
import { cycleLabel, dateSuffix, formatFriendlyDate, formatMonth, formatShortDate } from './format.js';
|
|
8
8
|
function monthOf(offset) {
|
|
9
9
|
const base = new Date();
|
|
10
10
|
base.setDate(1);
|
|
@@ -66,7 +66,7 @@ export function CalendarPage() {
|
|
|
66
66
|
? t('toast.claimed', { name: taskName })
|
|
67
67
|
: action === 'checkin'
|
|
68
68
|
? t('toast.checkinOk') + dateSuffix(date)
|
|
69
|
-
: t('toast.undoneAt', { date }), 'ok');
|
|
69
|
+
: t('toast.undoneAt', { date: formatShortDate(date) }), 'ok');
|
|
70
70
|
// busy 窗口覆盖月历重取;之后仅当操作日仍是当前选中日才回填详情——
|
|
71
71
|
// 动作与快速连点另一日期竞争时,慢的旧响应不得覆盖新选中日的面板
|
|
72
72
|
return page.reload().then(() => {
|
|
@@ -77,7 +77,10 @@ export function CalendarPage() {
|
|
|
77
77
|
.then((dayAfter) => { if (seq === pickSeqRef.current) {
|
|
78
78
|
setDetail(dayAfter);
|
|
79
79
|
setPickState('idle');
|
|
80
|
-
} })
|
|
80
|
+
} })
|
|
81
|
+
// 动作已成功、仅详情回取失败:转面板错误态(重试即重新拾取),不静默吞掉也不误报动作失败
|
|
82
|
+
.catch(() => { if (seq === pickSeqRef.current)
|
|
83
|
+
setPickState('error'); });
|
|
81
84
|
});
|
|
82
85
|
}));
|
|
83
86
|
};
|
|
@@ -202,7 +205,10 @@ export function CalendarPage() {
|
|
|
202
205
|
...(task.wishName !== undefined ? [task.wishName] : []),
|
|
203
206
|
...(stateText !== '' ? [stateText] : []),
|
|
204
207
|
];
|
|
205
|
-
return createElement('li', { key: task.taskId, className: 'xy-grouprow' }, createElement('div', { className: 'xy-rowmain' }, createElement('span', { className: 'xy-rowtitle' }, task.name), createElement('span', { className: 'xy-meta' }, segments.join(' · '))),
|
|
208
|
+
return createElement('li', { key: task.taskId, className: 'xy-grouprow' }, createElement('div', { className: 'xy-rowmain' }, createElement('span', { className: 'xy-rowtitle' }, task.name), createElement('span', { className: 'xy-meta' }, segments.join(' · '))),
|
|
209
|
+
// 领取仅限今日:claim 无日期语义,claimDate 恒为领取当天——
|
|
210
|
+
// 过去/未来选中日的「领取」按钮会造出锚点与所见不符的任务
|
|
211
|
+
task.status === 'pending' && !task.checked && detail.date === data.today
|
|
206
212
|
? createElement('button', { className: 'xy-btn', disabled: busy, onClick: () => act('claim', task.taskId, task.name, detail.date) }, t('action.claim'))
|
|
207
213
|
: !task.checked && task.status === 'in_progress'
|
|
208
214
|
? createElement('button', { className: 'xy-btn xy-btn-primary', disabled: busy || !task.canCheckIn, onClick: () => act('checkin', task.taskId, task.name, detail.date) }, t('action.checkinThisDay'))
|