Files
wordpress-theme-aksrt/assets/creator-studio/src/components/status-bar.tsx
T
kate522 a5386f72c6 fix(submit): make the draft chain survive continuous writing, reloads and flaky networks
Autosave only ran after 4s of idle, and every keystroke reset that timer, so a
writer who never stopped had zero saves (measured: 15s of typing = 0 requests).
Saving also never rebound the URL, so a new draft could not be found again after
a reload, the local backup was cleared on every autosave, and a failed save had
no retry and stayed failed once the network came back.

Client
- autosave: keep the idle debounce but add a 30s max-wait window that is not
  reset by further keystrokes, and retry failures at 5s / 15s / 60s
- bind `?edit=<id>` into the URL once the server returns a post id, so reloads,
  new tabs and history all reopen the same draft
- only clear the local backup when the save settled and nothing changed since
- key the local backup by draft id instead of the constant `jio-studio-0`
- restore snapshots through hydrateBlocks so code blocks come back editable
- never disable publish while an autosave is in flight, and mark autosave
  states separately (自动保存中… / 已自动保存 / 自动保存失败,将重试)
- on 409, adopt the server rev and tell the author instead of failing silently
- status bar reports the real save state instead of a bogus Markdown hint

Server
- draft saves no longer rewrite the lifecycle: pending / future / publish /
  private posts keep their status, so an autosave cannot pull a submitted post
  out of the review queue
- drafts and autosaves stop consuming the publish flood quota, which is meant to
  cap actual publishing (measured: 4 drafts used to burn 4 slots)
- autosave skips wp_save_post_revision for both plain drafts and submission
  versions; manual saves and submits still create a revision to roll back to
- media-only drafts (image / table / code / hr) count as content
- the response now carries the resulting status so the client can tell
  "just submitted" from "still under review"

Tests cover the max-wait window, retry schedule, URL binding, backup clearing,
status labelling and every server-side draft rule.
2026-11-10 17:58:22 +08:00

128 lines
3.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 底部固定状态栏。
*
* 与公众号/知乎一致:左侧是字数与输入法状态,中间是草稿时间,右侧是预览与发布。
* 固定在视口底部,不随正文滚动。
*
* @package Jio
*/
import { __, sprintf } from '@wordpress/i18n';
import { SaveState, saveStateLabel, type SaveStateValue } from './save-state';
type Props = {
charCount: number;
saveState: SaveStateValue;
isPublishedEdit: boolean;
saving: boolean;
onSaveDraft: () => void;
onPreview: () => void;
onPublish: () => void;
};
const MINUTE = 60 * 1000;
const HOUR = 60 * MINUTE;
const DAY = 24 * HOUR;
const MONTH = 30 * DAY;
const YEAR = 365 * DAY;
/** 相对时间:刚刚 / X分钟前 / X小时前 … 与参考编辑器一致。 */
export function relativeTime(iso: string, now = Date.now()): string {
const at = Date.parse(iso);
if (!Number.isFinite(at)) {
return '';
}
const diff = Math.max(0, now - at);
if (diff < MINUTE) {
return __('刚刚', 'jio');
}
if (diff < HOUR) {
return sprintf(
/* translators: %d: minutes */
__('%d分钟前', 'jio'),
Math.floor(diff / MINUTE)
);
}
if (diff < DAY) {
return sprintf(
/* translators: %d: hours */
__('%d小时前', 'jio'),
Math.floor(diff / HOUR)
);
}
if (diff < MONTH) {
return sprintf(
/* translators: %d: days */
__('%d天前', 'jio'),
Math.floor(diff / DAY)
);
}
if (diff < YEAR) {
return sprintf(
/* translators: %d: months */
__('%d个月前', 'jio'),
Math.floor(diff / MONTH)
);
}
return sprintf(
/* translators: %d: years */
__('%d年前', 'jio'),
Math.floor(diff / YEAR)
);
}
const STATUS_LABELS: Record<SaveStateValue['state'], string> = {
ready: __('草稿', 'jio'),
saving: __('保存中', 'jio'),
saved: __('草稿', 'jio'),
error: __('保存失败', 'jio'),
conflict: __('冲突', 'jio'),
submitting: __('提交中', 'jio'),
submitted: __('已提交', 'jio'),
};
export function StatusBar({
charCount,
saveState,
isPublishedEdit,
saving,
onSaveDraft,
onPreview,
onPublish,
}: Props) {
const savedAt = saveState.at ? relativeTime(saveState.at) : '';
const status = STATUS_LABELS[saveState.state] || STATUS_LABELS.ready;
// 时间和状态各显示一次会重复(「17:04:15 · 草稿」),有具体时间时以时间为准。
const meta = savedAt || status;
return (
<footer className="jio-creator__status" role="contentinfo">
<div className="jio-creator__status-left">
<span className="jio-creator__status-count">
{__('字数', 'jio')}:{charCount}
</span>
<span className="jio-creator__status-mode">{saveStateLabel(saveState)}</span>
</div>
<div className="jio-creator__status-meta">
<SaveState value={saveState} />
<span className="jio-creator__status-dot" aria-hidden="true">·</span>
<span className="jio-creator__status-time">{meta}</span>
</div>
<div className="jio-creator__status-actions">
<button type="button" className="jio-creator__status-ghost" onClick={onSaveDraft} disabled={saving}>
{__('保存草稿', 'jio')}
</button>
<button type="button" className="jio-creator__status-ghost" onClick={onPreview} disabled={saving}>
{__('预览', 'jio')}
</button>
<button type="button" className="jio-creator__status-primary" onClick={onPublish} disabled={saving}>
{isPublishedEdit ? __('提交改稿', 'jio') : __('发布', 'jio')}
</button>
</div>
</footer>
);
}