@fecommunity/reactpress 4.0.0-beta.14 → 4.0.0-beta.17
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/admin-dist/assets/{admin-bamoE2Tn.js → admin-CF4LIXsB.js} +1 -1
- package/admin-dist/assets/{admin-CrL6Xc9b.js → admin-DYBZ60Ac.js} +1 -1
- package/admin-dist/assets/{cssMode-J2cfgFkq.js → cssMode-BJqbvgLG.js} +1 -1
- package/admin-dist/assets/{freemarker2-L_gr_Wq4.js → freemarker2-CYGOmtYQ.js} +1 -1
- package/admin-dist/assets/{handlebars-D81kzUDC.js → handlebars-qhE4jgvZ.js} +1 -1
- package/admin-dist/assets/{html-Cung-Yr0.js → html-U5gI8Ly8.js} +1 -1
- package/admin-dist/assets/{htmlMode-C6QemWl2.js → htmlMode-DQd5Eb2D.js} +1 -1
- package/admin-dist/assets/{index-DXTkTQ21.js → index-BKNoDtfb.js} +178 -178
- package/admin-dist/assets/{javascript-WUcGtCKt.js → javascript-DU3Ebk7L.js} +1 -1
- package/admin-dist/assets/{jsonMode-Dp0R9P1F.js → jsonMode-D1w8Vhjb.js} +1 -1
- package/admin-dist/assets/{liquid-OFb9c-6B.js → liquid-BPVbnl_K.js} +1 -1
- package/admin-dist/assets/{mdx-CUfjLxA3.js → mdx-CMSYE9vh.js} +1 -1
- package/admin-dist/assets/{python-zxEYY77U.js → python-Dbpms-Xp.js} +1 -1
- package/admin-dist/assets/{razor-DJrSr4ly.js → razor-CWQdYrqP.js} +1 -1
- package/admin-dist/assets/{tsMode-Dtxx1cRn.js → tsMode-DAsZhwU2.js} +1 -1
- package/admin-dist/assets/{typescript-DT11gl4K.js → typescript-7mrVr_47.js} +1 -1
- package/admin-dist/assets/{xml-BIbLvdwx.js → xml-DbPOrhNN.js} +1 -1
- package/admin-dist/assets/{yaml-BEeUZskE.js → yaml-6hDyKyrt.js} +1 -1
- package/admin-dist/index.html +1 -1
- package/out/bin/reactpress.js +4 -0
- package/out/core/services/local-site.js +68 -0
- package/out/lib/i18n/strings.js +2 -0
- package/out/lib/theme-api-proxy-patch.js +62 -17
- package/package.json +2 -1
- package/plugins/hello-world/README.md +104 -0
- package/plugins/hello-world/dist/config.d.ts +4 -0
- package/plugins/hello-world/dist/config.js +17 -0
- package/plugins/hello-world/dist/index.d.ts +10 -0
- package/plugins/hello-world/dist/index.js +15 -0
- package/plugins/hello-world/dist/summary.d.ts +3 -0
- package/plugins/hello-world/dist/summary.js +33 -0
- package/plugins/hello-world/dist/types.d.ts +16 -0
- package/plugins/hello-world/dist/types.js +2 -0
- package/plugins/hello-world/locales/en.json +24 -0
- package/plugins/hello-world/locales/zh.json +24 -0
- package/plugins/hello-world/package.json +20 -0
- package/plugins/hello-world/plugin.json +61 -0
- package/plugins/hello-world/tsconfig.json +16 -0
- package/plugins/image-optimizer/README.md +43 -0
- package/plugins/image-optimizer/admin/OptimizeDashboard.tsx +249 -0
- package/plugins/image-optimizer/admin/api.ts +133 -0
- package/plugins/image-optimizer/admin/index.ts +16 -0
- package/plugins/image-optimizer/admin/locale.ts +68 -0
- package/plugins/image-optimizer/admin/optimize-dashboard.module.css +80 -0
- package/plugins/image-optimizer/dist/config.d.ts +8 -0
- package/plugins/image-optimizer/dist/config.js +13 -0
- package/plugins/image-optimizer/dist/index.d.ts +10 -0
- package/plugins/image-optimizer/dist/index.js +13 -0
- package/plugins/image-optimizer/locales/en.json +51 -0
- package/plugins/image-optimizer/locales/zh.json +53 -0
- package/plugins/image-optimizer/package.json +20 -0
- package/plugins/image-optimizer/plugin.json +58 -0
- package/plugins/image-optimizer/tsconfig.json +16 -0
- package/plugins/package.json +14 -0
- package/plugins/seo/README.md +60 -0
- package/plugins/seo/admin/ArticleEditorSeoPanel.tsx +67 -0
- package/plugins/seo/admin/article-editor-seo-panel.module.css +74 -0
- package/plugins/seo/admin/index.ts +18 -0
- package/plugins/seo/dist/config.d.ts +4 -0
- package/plugins/seo/dist/config.js +19 -0
- package/plugins/seo/dist/index.d.ts +10 -0
- package/plugins/seo/dist/index.js +23 -0
- package/plugins/seo/dist/seo.d.ts +3 -0
- package/plugins/seo/dist/seo.js +79 -0
- package/plugins/seo/dist/slug.d.ts +2 -0
- package/plugins/seo/dist/slug.js +13 -0
- package/plugins/seo/dist/types.d.ts +23 -0
- package/plugins/seo/dist/types.js +2 -0
- package/plugins/seo/locales/en.json +32 -0
- package/plugins/seo/locales/zh.json +32 -0
- package/plugins/seo/package.json +20 -0
- package/plugins/seo/plugin.json +76 -0
- package/plugins/seo/tsconfig.json +16 -0
- package/server/package-lock.json +3 -3
- package/server/package.json +1 -1
- package/toolkit/dist/theme/build/next-config.js +4 -1
- package/toolkit/package.json +1 -1
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import type { PluginSettingsPanelProps } from "@fecommunity/reactpress-toolkit/plugin/admin";
|
|
2
|
+
import { App, Button, Progress, Spin } from "antd";
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
fetchOptimizeJob,
|
|
7
|
+
fetchOptimizeReport,
|
|
8
|
+
readApiErrorMessage,
|
|
9
|
+
startOptimizeJob,
|
|
10
|
+
type ImageOptimizeReport,
|
|
11
|
+
type OptimizeJob,
|
|
12
|
+
} from "./api";
|
|
13
|
+
import { usePluginDashboardText } from "./locale";
|
|
14
|
+
|
|
15
|
+
import styles from "./optimize-dashboard.module.css";
|
|
16
|
+
|
|
17
|
+
function formatBytes(bytes: number): string {
|
|
18
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
19
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
20
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resolveConfig(config: Record<string, unknown>) {
|
|
24
|
+
const batchSize = Number(config.batchSize);
|
|
25
|
+
return {
|
|
26
|
+
batchSize: Number.isFinite(batchSize) ? Math.min(Math.max(batchSize, 1), 200) : 50,
|
|
27
|
+
skipGif: config.skipGif !== false,
|
|
28
|
+
rewriteContent: config.rewriteContent === true,
|
|
29
|
+
cleanupOriginals: config.cleanupOriginals === true,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function OptimizeDashboard({ pluginId, pluginActive, config }: PluginSettingsPanelProps) {
|
|
34
|
+
const { message } = App.useApp();
|
|
35
|
+
const t = usePluginDashboardText(pluginId);
|
|
36
|
+
|
|
37
|
+
const [report, setReport] = useState<ImageOptimizeReport | null>(null);
|
|
38
|
+
const [job, setJob] = useState<OptimizeJob | null>(null);
|
|
39
|
+
const [loadingReport, setLoadingReport] = useState(false);
|
|
40
|
+
const [startingJob, setStartingJob] = useState(false);
|
|
41
|
+
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
42
|
+
|
|
43
|
+
const stopPolling = useCallback(() => {
|
|
44
|
+
if (pollRef.current) {
|
|
45
|
+
clearInterval(pollRef.current);
|
|
46
|
+
pollRef.current = null;
|
|
47
|
+
}
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
const loadReport = useCallback(async () => {
|
|
51
|
+
setLoadingReport(true);
|
|
52
|
+
try {
|
|
53
|
+
const data = await fetchOptimizeReport();
|
|
54
|
+
setReport(data);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
message.error(readApiErrorMessage(err, t("analyzeFailed", "分析失败")));
|
|
57
|
+
} finally {
|
|
58
|
+
setLoadingReport(false);
|
|
59
|
+
}
|
|
60
|
+
}, [message, t]);
|
|
61
|
+
|
|
62
|
+
const pollJob = useCallback(
|
|
63
|
+
(jobId: string) => {
|
|
64
|
+
stopPolling();
|
|
65
|
+
|
|
66
|
+
const refresh = () => {
|
|
67
|
+
void fetchOptimizeJob(jobId)
|
|
68
|
+
.then((next) => {
|
|
69
|
+
setJob(next);
|
|
70
|
+
if (next.status === "completed" || next.status === "failed") {
|
|
71
|
+
stopPolling();
|
|
72
|
+
void loadReport();
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
.catch((err) => {
|
|
76
|
+
stopPolling();
|
|
77
|
+
message.error(readApiErrorMessage(err, t("jobPollFailed", "获取任务进度失败")));
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
refresh();
|
|
82
|
+
pollRef.current = setInterval(refresh, 800);
|
|
83
|
+
},
|
|
84
|
+
[loadReport, message, stopPolling, t],
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (pluginActive) {
|
|
89
|
+
void loadReport();
|
|
90
|
+
}
|
|
91
|
+
}, [loadReport, pluginActive]);
|
|
92
|
+
|
|
93
|
+
useEffect(() => () => stopPolling(), [stopPolling]);
|
|
94
|
+
|
|
95
|
+
const runJob = async (dryRun: boolean) => {
|
|
96
|
+
if (!pluginActive) return;
|
|
97
|
+
const opts = resolveConfig(config);
|
|
98
|
+
setStartingJob(true);
|
|
99
|
+
try {
|
|
100
|
+
const started = await startOptimizeJob({
|
|
101
|
+
dryRun,
|
|
102
|
+
batchSize: opts.batchSize,
|
|
103
|
+
limit: opts.batchSize,
|
|
104
|
+
skipGif: opts.skipGif,
|
|
105
|
+
rewriteContent: opts.rewriteContent,
|
|
106
|
+
cleanupOriginals: opts.cleanupOriginals,
|
|
107
|
+
});
|
|
108
|
+
setJob(started);
|
|
109
|
+
if (started.status === "completed" || started.status === "failed") {
|
|
110
|
+
void loadReport();
|
|
111
|
+
if (started.total === 0) {
|
|
112
|
+
message.info(t("jobEmpty", "没有待优化的图片(可能已全部优化,或原文件已不存在)。"));
|
|
113
|
+
} else if (dryRun) {
|
|
114
|
+
message.success(t("dryRunDone", "试运行完成(未写入数据库)。"));
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
pollJob(started.id);
|
|
118
|
+
if (dryRun) {
|
|
119
|
+
message.info(t("dryRun", "试运行(不写入)"));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} catch (err) {
|
|
123
|
+
message.error(readApiErrorMessage(err, t("runFailed", "启动任务失败")));
|
|
124
|
+
} finally {
|
|
125
|
+
setStartingJob(false);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
if (!pluginActive) {
|
|
130
|
+
return (
|
|
131
|
+
<div className={styles.warn} data-testid="image-optimizer-inactive">
|
|
132
|
+
{t("inactiveHint", "请先在插件列表中启用本插件,才能执行分析与优化。")}
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const jobRunning = job?.status === "pending" || job?.status === "running";
|
|
138
|
+
const jobDone = job?.status === "completed" || job?.status === "failed";
|
|
139
|
+
const jobPercent =
|
|
140
|
+
job && job.total > 0 ? Math.round((job.processed / job.total) * 100) : undefined;
|
|
141
|
+
const failedItems = job?.items.filter((item) => item.status === "failed").slice(0, 5) ?? [];
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<div className={styles.optimizeDashboard} data-testid="image-optimizer-dashboard">
|
|
145
|
+
<h2 className={styles.sectionTitle}>{t("title", "资源分析")}</h2>
|
|
146
|
+
|
|
147
|
+
<div className={styles.actions}>
|
|
148
|
+
<Button loading={loadingReport} onClick={() => void loadReport()}>
|
|
149
|
+
{t("analyze", "分析现状")}
|
|
150
|
+
</Button>
|
|
151
|
+
<Button loading={startingJob} onClick={() => void runJob(true)}>
|
|
152
|
+
{t("dryRun", "试运行(不写入)")}
|
|
153
|
+
</Button>
|
|
154
|
+
<Button type="primary" loading={startingJob || jobRunning} onClick={() => void runJob(false)}>
|
|
155
|
+
{t("run", "开始批量优化")}
|
|
156
|
+
</Button>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
{loadingReport && !report ? (
|
|
160
|
+
<Spin />
|
|
161
|
+
) : report ? (
|
|
162
|
+
<div className={styles.statsGrid}>
|
|
163
|
+
<div className={styles.statCard}>
|
|
164
|
+
<p className={styles.statLabel}>{t("total", "素材总数")}</p>
|
|
165
|
+
<p className={styles.statValue}>{report.total}</p>
|
|
166
|
+
</div>
|
|
167
|
+
<div className={styles.statCard}>
|
|
168
|
+
<p className={styles.statLabel}>{t("optimized", "已优化")}</p>
|
|
169
|
+
<p className={styles.statValue}>{report.alreadyOptimized}</p>
|
|
170
|
+
</div>
|
|
171
|
+
<div className={styles.statCard}>
|
|
172
|
+
<p className={styles.statLabel}>{t("pending", "待优化")}</p>
|
|
173
|
+
<p className={styles.statValue}>{report.needsOptimization}</p>
|
|
174
|
+
</div>
|
|
175
|
+
<div className={styles.statCard}>
|
|
176
|
+
<p className={styles.statLabel}>{t("estimatedSaved", "预估可节省")}</p>
|
|
177
|
+
<p className={styles.statValue}>{formatBytes(report.storageBytes.estimatedSaved)}</p>
|
|
178
|
+
</div>
|
|
179
|
+
<div className={styles.statCard}>
|
|
180
|
+
<p className={styles.statLabel}>{t("contentRefs", "内容引用")}</p>
|
|
181
|
+
<p className={styles.statValue}>
|
|
182
|
+
{report.contentRefs.articles} {t("articles", "文章")} / {report.contentRefs.pages}{" "}
|
|
183
|
+
{t("pages", "页面")}
|
|
184
|
+
</p>
|
|
185
|
+
</div>
|
|
186
|
+
<div className={styles.statCard}>
|
|
187
|
+
<p className={styles.statLabel}>{t("skipped", "已跳过")}</p>
|
|
188
|
+
<p className={styles.statValue}>
|
|
189
|
+
{report.skipped.nonImage + report.skipped.svg + report.skipped.gif + report.skipped.missing}
|
|
190
|
+
</p>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
) : null}
|
|
194
|
+
|
|
195
|
+
{job ? (
|
|
196
|
+
<div className={styles.progressBlock}>
|
|
197
|
+
<Progress
|
|
198
|
+
percent={jobPercent}
|
|
199
|
+
status={
|
|
200
|
+
job.status === "failed"
|
|
201
|
+
? "exception"
|
|
202
|
+
: jobRunning
|
|
203
|
+
? "active"
|
|
204
|
+
: job.total === 0
|
|
205
|
+
? "normal"
|
|
206
|
+
: "success"
|
|
207
|
+
}
|
|
208
|
+
showInfo={job.total > 0}
|
|
209
|
+
/>
|
|
210
|
+
<p className={styles.hint}>
|
|
211
|
+
{job.status === "failed"
|
|
212
|
+
? `${t("jobFailed", "任务失败")}: ${job.error ?? ""}`
|
|
213
|
+
: jobRunning
|
|
214
|
+
? job.total === 0
|
|
215
|
+
? t("jobScanning", "正在扫描待优化素材…")
|
|
216
|
+
: t("jobRunning", "优化任务进行中…")
|
|
217
|
+
: t("jobDone", "任务已完成")}
|
|
218
|
+
{" · "}
|
|
219
|
+
{t("processed", "已处理")} {job.processed}/{job.total} · {t("succeeded", "成功")}{" "}
|
|
220
|
+
{job.succeeded} · {t("failed", "失败")} {job.failed}
|
|
221
|
+
{job.savedBytes > 0 ? ` · ${t("savedBytes", "已节省空间")} ${formatBytes(job.savedBytes)}` : ""}
|
|
222
|
+
</p>
|
|
223
|
+
{jobDone && job.total === 0 ? (
|
|
224
|
+
<p className={styles.emptyJob}>
|
|
225
|
+
{t("jobEmpty", "没有待优化的图片(可能已全部优化,或原文件已不存在)。")}
|
|
226
|
+
</p>
|
|
227
|
+
) : null}
|
|
228
|
+
{job.contentRewrite ? (
|
|
229
|
+
<p className={styles.hint}>
|
|
230
|
+
URL 回写:{job.contentRewrite.articles} {t("articles", "文章")}、{job.contentRewrite.pages}{" "}
|
|
231
|
+
{t("pages", "页面")}、{job.contentRewrite.revisions} 修订
|
|
232
|
+
</p>
|
|
233
|
+
) : null}
|
|
234
|
+
{failedItems.length > 0 ? (
|
|
235
|
+
<ul className={styles.errorList}>
|
|
236
|
+
{failedItems.map((item) => (
|
|
237
|
+
<li key={item.fileId}>
|
|
238
|
+
{item.originalname}: {item.error}
|
|
239
|
+
</li>
|
|
240
|
+
))}
|
|
241
|
+
</ul>
|
|
242
|
+
) : null}
|
|
243
|
+
</div>
|
|
244
|
+
) : null}
|
|
245
|
+
</div>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export { OptimizeDashboard as SettingsPanel };
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { resolveApiBaseUrl } from "@fecommunity/reactpress-toolkit/plugin/react";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_API_BASE = "/api";
|
|
4
|
+
|
|
5
|
+
export interface ImageOptimizeReport {
|
|
6
|
+
total: number;
|
|
7
|
+
alreadyOptimized: number;
|
|
8
|
+
needsOptimization: number;
|
|
9
|
+
skipped: {
|
|
10
|
+
nonImage: number;
|
|
11
|
+
svg: number;
|
|
12
|
+
gif: number;
|
|
13
|
+
missing: number;
|
|
14
|
+
};
|
|
15
|
+
storageBytes: {
|
|
16
|
+
current: number;
|
|
17
|
+
estimatedAfter: number;
|
|
18
|
+
estimatedSaved: number;
|
|
19
|
+
};
|
|
20
|
+
contentRefs: {
|
|
21
|
+
articles: number;
|
|
22
|
+
pages: number;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface OptimizeRunOptions {
|
|
27
|
+
dryRun?: boolean;
|
|
28
|
+
batchSize?: number;
|
|
29
|
+
skipGif?: boolean;
|
|
30
|
+
rewriteContent?: boolean;
|
|
31
|
+
cleanupOriginals?: boolean;
|
|
32
|
+
limit?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface OptimizeJobItemResult {
|
|
36
|
+
fileId: string;
|
|
37
|
+
originalname: string;
|
|
38
|
+
status: "success" | "skipped" | "failed";
|
|
39
|
+
oldUrl?: string;
|
|
40
|
+
newUrl?: string;
|
|
41
|
+
savedBytes?: number;
|
|
42
|
+
error?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface OptimizeJob {
|
|
46
|
+
id: string;
|
|
47
|
+
status: "pending" | "running" | "completed" | "failed";
|
|
48
|
+
dryRun: boolean;
|
|
49
|
+
total: number;
|
|
50
|
+
processed: number;
|
|
51
|
+
succeeded: number;
|
|
52
|
+
failed: number;
|
|
53
|
+
skipped: number;
|
|
54
|
+
savedBytes: number;
|
|
55
|
+
urlMap: Record<string, string>;
|
|
56
|
+
items: OptimizeJobItemResult[];
|
|
57
|
+
rewriteContent: boolean;
|
|
58
|
+
contentRewrite?: {
|
|
59
|
+
articles: number;
|
|
60
|
+
pages: number;
|
|
61
|
+
revisions: number;
|
|
62
|
+
};
|
|
63
|
+
error?: string;
|
|
64
|
+
startedAt: string;
|
|
65
|
+
finishedAt?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function readAccessToken(): string | null {
|
|
69
|
+
if (typeof localStorage === "undefined") return null;
|
|
70
|
+
try {
|
|
71
|
+
const raw = localStorage.getItem("auth-storage");
|
|
72
|
+
if (!raw) return null;
|
|
73
|
+
const parsed = JSON.parse(raw) as { state?: { tokens?: { accessToken?: string } } };
|
|
74
|
+
return parsed.state?.tokens?.accessToken ?? null;
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function readApiErrorMessage(err: unknown, fallback: string): string {
|
|
81
|
+
if (err instanceof Error && err.message.trim()) return err.message;
|
|
82
|
+
return fallback;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function optimizeFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
|
86
|
+
const base = (await resolveApiBaseUrl(DEFAULT_API_BASE)).replace(/\/$/, "");
|
|
87
|
+
const token = readAccessToken();
|
|
88
|
+
const res = await fetch(`${base}${path}`, {
|
|
89
|
+
...init,
|
|
90
|
+
headers: {
|
|
91
|
+
"Content-Type": "application/json",
|
|
92
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
93
|
+
...init?.headers,
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
const body = (await res.json()) as {
|
|
97
|
+
success?: boolean;
|
|
98
|
+
code?: number;
|
|
99
|
+
data?: T;
|
|
100
|
+
msg?: string;
|
|
101
|
+
message?: string;
|
|
102
|
+
};
|
|
103
|
+
if (res.status === 401) {
|
|
104
|
+
throw new Error("SESSION_EXPIRED");
|
|
105
|
+
}
|
|
106
|
+
if (!res.ok) {
|
|
107
|
+
throw new Error(body.msg ?? body.message ?? `Request failed: ${res.status}`);
|
|
108
|
+
}
|
|
109
|
+
if (typeof body.code === "number" && body.code !== 0) {
|
|
110
|
+
throw new Error(body.message ?? "Request failed");
|
|
111
|
+
}
|
|
112
|
+
if (body.success === false) {
|
|
113
|
+
throw new Error(body.msg ?? "Request failed");
|
|
114
|
+
}
|
|
115
|
+
return body.data as T;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function fetchOptimizeReport() {
|
|
119
|
+
return optimizeFetch<ImageOptimizeReport>("/file/optimize/report");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function startOptimizeJob(options: OptimizeRunOptions) {
|
|
123
|
+
return optimizeFetch<OptimizeJob>("/file/optimize/run", {
|
|
124
|
+
method: "POST",
|
|
125
|
+
body: JSON.stringify(options),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function fetchOptimizeJob(jobId: string) {
|
|
130
|
+
return optimizeFetch<OptimizeJob>(`/file/optimize/job/${encodeURIComponent(jobId)}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export { readApiErrorMessage };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { PluginAdminModule } from "@fecommunity/reactpress-toolkit/plugin/admin";
|
|
2
|
+
|
|
3
|
+
export { SettingsPanel } from "./OptimizeDashboard";
|
|
4
|
+
|
|
5
|
+
import { SettingsPanel } from "./OptimizeDashboard";
|
|
6
|
+
|
|
7
|
+
export function registerAdmin(): void {
|
|
8
|
+
// Dashboard is mounted via SettingsPanel on the plugin settings page.
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const pluginAdmin: PluginAdminModule = {
|
|
12
|
+
registerAdmin,
|
|
13
|
+
SettingsPanel,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default pluginAdmin;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import { resolveApiBaseUrl } from "@fecommunity/reactpress-toolkit/plugin/react";
|
|
4
|
+
|
|
5
|
+
type LocaleMessages = Record<string, unknown>;
|
|
6
|
+
|
|
7
|
+
function readNestedMessage(messages: LocaleMessages, path: string): string | undefined {
|
|
8
|
+
const parts = path.split(".");
|
|
9
|
+
let current: unknown = messages;
|
|
10
|
+
for (const part of parts) {
|
|
11
|
+
if (!current || typeof current !== "object") return undefined;
|
|
12
|
+
current = (current as Record<string, unknown>)[part];
|
|
13
|
+
}
|
|
14
|
+
return typeof current === "string" ? current : undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function detectLocale(): string {
|
|
18
|
+
if (typeof document === "undefined") return "en";
|
|
19
|
+
const lang = document.documentElement.lang || navigator.language || "en";
|
|
20
|
+
return lang.toLowerCase().startsWith("zh") ? "zh" : "en";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function fetchPluginLocale(pluginId: string, locale: string): Promise<LocaleMessages> {
|
|
24
|
+
const base = (await resolveApiBaseUrl("/api")).replace(/\/$/, "");
|
|
25
|
+
const token = (() => {
|
|
26
|
+
try {
|
|
27
|
+
const raw = localStorage.getItem("auth-storage");
|
|
28
|
+
if (!raw) return null;
|
|
29
|
+
const parsed = JSON.parse(raw) as { state?: { tokens?: { accessToken?: string } } };
|
|
30
|
+
return parsed.state?.tokens?.accessToken ?? null;
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
})();
|
|
35
|
+
const res = await fetch(
|
|
36
|
+
`${base}/extension/plugins/${encodeURIComponent(pluginId)}/locales/${encodeURIComponent(locale)}`,
|
|
37
|
+
{
|
|
38
|
+
headers: {
|
|
39
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
if (!res.ok) return {};
|
|
44
|
+
const body = (await res.json()) as { data?: { messages?: LocaleMessages } };
|
|
45
|
+
return body.data?.messages ?? {};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function usePluginDashboardText(pluginId: string) {
|
|
49
|
+
const locale = detectLocale();
|
|
50
|
+
const [messages, setMessages] = useState<LocaleMessages>({});
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
let cancelled = false;
|
|
54
|
+
void fetchPluginLocale(pluginId, locale).then((next) => {
|
|
55
|
+
if (!cancelled) setMessages(next);
|
|
56
|
+
});
|
|
57
|
+
return () => {
|
|
58
|
+
cancelled = true;
|
|
59
|
+
};
|
|
60
|
+
}, [locale, pluginId]);
|
|
61
|
+
|
|
62
|
+
return useMemo(
|
|
63
|
+
() =>
|
|
64
|
+
(key: string, fallback: string) =>
|
|
65
|
+
readNestedMessage(messages, `dashboard.${key}`) ?? fallback,
|
|
66
|
+
[messages],
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
.optimizeDashboard {
|
|
2
|
+
margin-bottom: 24px;
|
|
3
|
+
padding-bottom: 20px;
|
|
4
|
+
border-bottom: 1px solid var(--ant-color-border-secondary);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.sectionTitle {
|
|
8
|
+
margin: 0 0 16px;
|
|
9
|
+
font-size: 14px;
|
|
10
|
+
font-weight: 600;
|
|
11
|
+
color: var(--ant-color-text);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.statsGrid {
|
|
15
|
+
display: grid;
|
|
16
|
+
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
|
17
|
+
gap: 12px;
|
|
18
|
+
margin-bottom: 16px;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.statCard {
|
|
22
|
+
padding: 12px 14px;
|
|
23
|
+
border: 1px solid var(--ant-color-border-secondary);
|
|
24
|
+
border-radius: 8px;
|
|
25
|
+
background: var(--ant-color-fill-quaternary);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.statLabel {
|
|
29
|
+
margin: 0 0 4px;
|
|
30
|
+
font-size: 12px;
|
|
31
|
+
color: var(--ant-color-text-secondary);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.statValue {
|
|
35
|
+
margin: 0;
|
|
36
|
+
font-size: 20px;
|
|
37
|
+
font-weight: 600;
|
|
38
|
+
line-height: 1.2;
|
|
39
|
+
color: var(--ant-color-text);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.actions {
|
|
43
|
+
display: flex;
|
|
44
|
+
flex-wrap: wrap;
|
|
45
|
+
gap: 8px;
|
|
46
|
+
margin-bottom: 16px;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.progressBlock {
|
|
50
|
+
margin-top: 12px;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.hint {
|
|
54
|
+
margin: 8px 0 0;
|
|
55
|
+
font-size: 13px;
|
|
56
|
+
color: var(--ant-color-text-secondary);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.emptyJob {
|
|
60
|
+
margin: 8px 0 0;
|
|
61
|
+
font-size: 13px;
|
|
62
|
+
color: var(--ant-color-text-tertiary);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.warn {
|
|
66
|
+
margin: 0 0 16px;
|
|
67
|
+
padding: 10px 12px;
|
|
68
|
+
border-radius: 8px;
|
|
69
|
+
background: var(--ant-color-warning-bg);
|
|
70
|
+
border: 1px solid var(--ant-color-warning-border);
|
|
71
|
+
color: var(--ant-color-text);
|
|
72
|
+
font-size: 13px;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.errorList {
|
|
76
|
+
margin: 8px 0 0;
|
|
77
|
+
padding-left: 18px;
|
|
78
|
+
font-size: 13px;
|
|
79
|
+
color: var(--ant-color-error);
|
|
80
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { PluginContext } from '@fecommunity/reactpress-toolkit/plugin/server';
|
|
2
|
+
export interface ImageOptimizerConfig {
|
|
3
|
+
batchSize: number;
|
|
4
|
+
skipGif: boolean;
|
|
5
|
+
rewriteContent: boolean;
|
|
6
|
+
cleanupOriginals: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function resolveConfig(ctx: PluginContext): ImageOptimizerConfig;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveConfig = resolveConfig;
|
|
4
|
+
function resolveConfig(ctx) {
|
|
5
|
+
const raw = ctx.config ?? {};
|
|
6
|
+
const batchSize = Number(raw.batchSize);
|
|
7
|
+
return {
|
|
8
|
+
batchSize: Number.isFinite(batchSize) ? Math.min(Math.max(batchSize, 1), 200) : 50,
|
|
9
|
+
skipGif: raw.skipGif !== false,
|
|
10
|
+
rewriteContent: raw.rewriteContent === true,
|
|
11
|
+
cleanupOriginals: raw.cleanupOriginals === true,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { HookService, PluginContext } from '@fecommunity/reactpress-toolkit/plugin/server';
|
|
2
|
+
/**
|
|
3
|
+
* 图片资源优化:批量压缩历史素材由 Core FileOptimizationService 执行;
|
|
4
|
+
* 本插件负责注册与配置,管理端仪表盘见 admin/OptimizeDashboard。
|
|
5
|
+
*/
|
|
6
|
+
export declare function register(_hooks: HookService, ctx: PluginContext): void;
|
|
7
|
+
declare const _default: {
|
|
8
|
+
register: typeof register;
|
|
9
|
+
};
|
|
10
|
+
export default _default;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.register = register;
|
|
4
|
+
const config_1 = require("./config");
|
|
5
|
+
/**
|
|
6
|
+
* 图片资源优化:批量压缩历史素材由 Core FileOptimizationService 执行;
|
|
7
|
+
* 本插件负责注册与配置,管理端仪表盘见 admin/OptimizeDashboard。
|
|
8
|
+
*/
|
|
9
|
+
function register(_hooks, ctx) {
|
|
10
|
+
const config = (0, config_1.resolveConfig)(ctx);
|
|
11
|
+
ctx.logger.log(`registered (batchSize=${config.batchSize}, skipGif=${config.skipGif}, rewriteContent=${config.rewriteContent})`);
|
|
12
|
+
}
|
|
13
|
+
exports.default = { register };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"meta": {
|
|
3
|
+
"name": "Image Optimizer",
|
|
4
|
+
"description": "Analyze legacy media and batch-compress to WebP variants; optionally rewrite URLs in articles and pages."
|
|
5
|
+
},
|
|
6
|
+
"settings": {
|
|
7
|
+
"batchSize": {
|
|
8
|
+
"title": "Batch size",
|
|
9
|
+
"description": "Maximum images processed per job (1–200)."
|
|
10
|
+
},
|
|
11
|
+
"skipGif": {
|
|
12
|
+
"title": "Skip GIF",
|
|
13
|
+
"description": "GIF to WebP loses animation; skipped by default."
|
|
14
|
+
},
|
|
15
|
+
"rewriteContent": {
|
|
16
|
+
"title": "Rewrite content URLs after optimize",
|
|
17
|
+
"description": "Replace old image URLs in articles and pages when the job completes."
|
|
18
|
+
},
|
|
19
|
+
"cleanupOriginals": {
|
|
20
|
+
"title": "Delete originals after optimize",
|
|
21
|
+
"description": "Enable only after verification; old URLs will stop working."
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"dashboard": {
|
|
25
|
+
"title": "Asset analysis",
|
|
26
|
+
"analyze": "Analyze",
|
|
27
|
+
"dryRun": "Dry run",
|
|
28
|
+
"run": "Start batch optimize",
|
|
29
|
+
"refresh": "Refresh",
|
|
30
|
+
"total": "Total assets",
|
|
31
|
+
"optimized": "Optimized",
|
|
32
|
+
"pending": "Pending",
|
|
33
|
+
"estimatedSaved": "Est. savings",
|
|
34
|
+
"contentRefs": "Content references",
|
|
35
|
+
"articles": "Articles",
|
|
36
|
+
"pages": "Pages",
|
|
37
|
+
"skipped": "Skipped",
|
|
38
|
+
"jobRunning": "Optimization in progress…",
|
|
39
|
+
"jobScanning": "Scanning for optimizable assets…",
|
|
40
|
+
"jobEmpty": "No images to optimize (already optimized or source files missing).",
|
|
41
|
+
"jobDone": "Job completed",
|
|
42
|
+
"jobFailed": "Job failed",
|
|
43
|
+
"processed": "Processed",
|
|
44
|
+
"succeeded": "Succeeded",
|
|
45
|
+
"failed": "Failed",
|
|
46
|
+
"savedBytes": "Space saved",
|
|
47
|
+
"inactiveHint": "Activate this plugin from the plugin list before running analysis or optimization.",
|
|
48
|
+
"analyzeFailed": "Analysis failed",
|
|
49
|
+
"runFailed": "Failed to start job"
|
|
50
|
+
}
|
|
51
|
+
}
|