feat(creator-studio): bottom status bar, editor-rendered math/footnote, code language picker, block delete
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
6928127bfa
commit
4a119358ca
11 files changed
+754
-63
No files matched your search
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* 底部固定状态栏。
|
||||
*
|
||||
* 与公众号/知乎一致:左侧是字数与输入法状态,中间是草稿时间,右侧是预览与发布。
|
||||
* 固定在视口底部,不随正文滚动。
|
||||
*
|
||||
* @package Jio
|
||||
*/
|
||||
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
|
||||
import { SaveState, 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;
|
||||
|
||||
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">{__('Markdown 语法输入中', 'jio')}</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">{savedAt || status}</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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user