@akairos/typeset-studio 0.2.5 → 0.2.7
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/README.md +67 -17
- package/dist/assets/index-CVzTRIjV.js +317 -0
- package/dist/assets/index-JCv_QmUn.css +1 -0
- package/dist/index.html +2 -2
- package/dist/lib/adapter.js +1 -1
- package/dist/lib/{canvas-renderer-cxnar75n.js → canvas-renderer-wce6xgb2.js} +1 -1
- package/dist/lib/{index-zdhqg5f0.js → index-697fkv5y.js} +294 -57
- package/dist/lib/{index-h2580xma.js → index-dkwbnaj7.js} +3 -3
- package/dist/lib/{index-t9w3p2p9.js → index-qszn98j2.js} +73 -5
- package/dist/lib/index.js +3 -3
- package/dist/lib/{render-xxd348m1.js → render-064weavx.js} +2 -2
- package/package.json +3 -3
- package/src/adapter.test.ts +3 -1
- package/src/adapter.ts +1 -1
- package/src/core/canvas-draw.ts +53 -43
- package/src/core/canvas-presets.ts +37 -0
- package/src/core/canvas-renderer.test.ts +60 -1
- package/src/core/canvas-style.ts +149 -0
- package/src/core/engine.ts +2 -0
- package/src/core/geometry.ts +4 -3
- package/src/core/types.ts +30 -3
- package/src/index.ts +4 -0
- package/src/server/index.ts +1 -1
- package/src/server/routes/slots.test.ts +24 -0
- package/src/server/routes/templates.ts +17 -2
- package/src/web/App.tsx +27 -7
- package/src/web/components/CanvasStyleInspector.tsx +95 -0
- package/src/web/components/Header.tsx +25 -10
- package/src/web/components/ParamsInspector.tsx +6 -1
- package/src/web/components/SaveTemplateModal.tsx +106 -0
- package/src/web/components/TemplateDrawer.tsx +323 -0
- package/src/web/hooks/useTypeset.ts +3 -1
- package/dist/assets/index-3Ko1UCBU.css +0 -1
- package/dist/assets/index-De-aHsvp.js +0 -317
- package/src/web/components/TemplateModal.tsx +0 -393
|
@@ -1,393 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useState } from "react";
|
|
2
|
-
import { Check, FolderPlus, Trash2, X, Sparkles, Star } from "lucide-react";
|
|
3
|
-
import { validateTemplateAssets } from "../../core/template";
|
|
4
|
-
import { TemplateSlot, TypesetConfig } from "../../core/types";
|
|
5
|
-
|
|
6
|
-
interface TemplateModalProps {
|
|
7
|
-
isOpen: boolean;
|
|
8
|
-
onClose: () => void;
|
|
9
|
-
currentConfig: TypesetConfig;
|
|
10
|
-
onApplyTemplate: (tpl: TypesetConfig) => void;
|
|
11
|
-
adminKey: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface SlotsResponse {
|
|
15
|
-
ok: boolean;
|
|
16
|
-
account: string;
|
|
17
|
-
message?: string;
|
|
18
|
-
error?: string;
|
|
19
|
-
slots: {
|
|
20
|
-
article_cover?: { id: string; template?: TypesetConfig };
|
|
21
|
-
newspic_cover?: { id: string; template?: TypesetConfig };
|
|
22
|
-
newspic_content?: { id: string; template?: TypesetConfig };
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export const TemplateModal: React.FC<TemplateModalProps> = ({
|
|
27
|
-
isOpen,
|
|
28
|
-
onClose,
|
|
29
|
-
currentConfig,
|
|
30
|
-
onApplyTemplate,
|
|
31
|
-
adminKey,
|
|
32
|
-
}) => {
|
|
33
|
-
const [templates, setTemplates] = useState<TypesetConfig[]>([]);
|
|
34
|
-
const [slotsData, setSlotsData] = useState<SlotsResponse["slots"]>({});
|
|
35
|
-
const [account, setAccount] = useState<string>("default");
|
|
36
|
-
const [activeSlotFilter, setActiveSlotFilter] = useState<"all" | TemplateSlot>("all");
|
|
37
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
38
|
-
const [templateName, setTemplateName] = useState("");
|
|
39
|
-
const [saveSlot, setSaveSlot] = useState<TemplateSlot | "none">(() => {
|
|
40
|
-
return currentConfig.aspectRatio === "wechat-cover-2.35" ? "article_cover" : "newspic_content";
|
|
41
|
-
});
|
|
42
|
-
const [isSaving, setIsSaving] = useState(false);
|
|
43
|
-
const [error, setError] = useState<string | null>(null);
|
|
44
|
-
|
|
45
|
-
const fetchSlots = async (acc = account) => {
|
|
46
|
-
try {
|
|
47
|
-
const res = await fetch(`/api/templates/slots?account=${encodeURIComponent(acc)}`, { headers: { "x-admin-key": adminKey } });
|
|
48
|
-
const data: SlotsResponse = await res.json();
|
|
49
|
-
if (!res.ok || !data.ok) throw new Error(data.message || data.error || "获取槽位失败");
|
|
50
|
-
if (data.ok && data.slots) {
|
|
51
|
-
setSlotsData(data.slots);
|
|
52
|
-
}
|
|
53
|
-
} catch (err) {
|
|
54
|
-
setError(err instanceof Error ? err.message : "模板请求失败");
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const handleSetSlotDefault = async (slot: TemplateSlot, templateId: string) => {
|
|
59
|
-
try {
|
|
60
|
-
const res = await fetch("/api/templates/slots", {
|
|
61
|
-
method: "POST",
|
|
62
|
-
headers: {
|
|
63
|
-
"Content-Type": "application/json",
|
|
64
|
-
...(adminKey ? { "x-admin-key": adminKey } : {}),
|
|
65
|
-
},
|
|
66
|
-
body: JSON.stringify({ slot, templateId, account }),
|
|
67
|
-
});
|
|
68
|
-
const data = await res.json();
|
|
69
|
-
if (!res.ok || !data.ok) throw new Error(data.message || data.error || "设置默认模板失败");
|
|
70
|
-
if (data.ok) {
|
|
71
|
-
await fetchSlots(account);
|
|
72
|
-
}
|
|
73
|
-
} catch (err) {
|
|
74
|
-
setError(err instanceof Error ? err.message : "模板请求失败");
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const fetchTemplates = async () => {
|
|
79
|
-
setIsLoading(true);
|
|
80
|
-
setError(null);
|
|
81
|
-
try {
|
|
82
|
-
const res = await fetch("/api/templates", {
|
|
83
|
-
headers: adminKey ? { "x-admin-key": adminKey } : {},
|
|
84
|
-
});
|
|
85
|
-
const data = await res.json();
|
|
86
|
-
if (res.ok && data.ok && Array.isArray(data.templates)) {
|
|
87
|
-
setTemplates(data.templates);
|
|
88
|
-
} else {
|
|
89
|
-
setError(data.message || "获取模板失败,请检查密钥");
|
|
90
|
-
}
|
|
91
|
-
} catch {
|
|
92
|
-
setError("连接后端服务失败");
|
|
93
|
-
} finally {
|
|
94
|
-
setIsLoading(false);
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
useEffect(() => {
|
|
99
|
-
if (isOpen) {
|
|
100
|
-
fetchTemplates();
|
|
101
|
-
fetchSlots(account);
|
|
102
|
-
setTemplateName(currentConfig.name ? `${currentConfig.name} 副本` : "我的自定义模板");
|
|
103
|
-
setSaveSlot(currentConfig.aspectRatio === "wechat-cover-2.35" ? "article_cover" : "newspic_content");
|
|
104
|
-
}
|
|
105
|
-
}, [isOpen, account, adminKey]);
|
|
106
|
-
|
|
107
|
-
const handleSave = async () => {
|
|
108
|
-
if (!templateName.trim()) return;
|
|
109
|
-
setIsSaving(true);
|
|
110
|
-
try {
|
|
111
|
-
validateTemplateAssets(currentConfig.images);
|
|
112
|
-
const tplToSave: TypesetConfig = {
|
|
113
|
-
...currentConfig,
|
|
114
|
-
id: undefined, // auto generate
|
|
115
|
-
name: templateName.trim(),
|
|
116
|
-
slot: saveSlot !== "none" ? saveSlot : undefined,
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
const res = await fetch("/api/templates", {
|
|
120
|
-
method: "POST",
|
|
121
|
-
headers: {
|
|
122
|
-
"Content-Type": "application/json",
|
|
123
|
-
...(adminKey ? { "x-admin-key": adminKey } : {}),
|
|
124
|
-
},
|
|
125
|
-
body: JSON.stringify(tplToSave),
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
const data = await res.json();
|
|
129
|
-
if (res.ok && data.ok) {
|
|
130
|
-
await fetchTemplates();
|
|
131
|
-
await fetchSlots(account);
|
|
132
|
-
} else {
|
|
133
|
-
setError(data.message || data.error || "保存模板失败");
|
|
134
|
-
}
|
|
135
|
-
} catch (err) {
|
|
136
|
-
setError(err instanceof Error ? err.message : "保存模板请求异常");
|
|
137
|
-
} finally {
|
|
138
|
-
setIsSaving(false);
|
|
139
|
-
}
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
const handleDelete = async (id: string) => {
|
|
143
|
-
try {
|
|
144
|
-
const res = await fetch(`/api/templates/${encodeURIComponent(id)}`, {
|
|
145
|
-
method: "DELETE",
|
|
146
|
-
headers: adminKey ? { "x-admin-key": adminKey } : {},
|
|
147
|
-
});
|
|
148
|
-
const data = await res.json();
|
|
149
|
-
if (!res.ok || !data.ok) throw new Error(data.message || data.error || "删除模板失败");
|
|
150
|
-
setTemplates((prev) => prev.filter((t) => t.id !== id));
|
|
151
|
-
await fetchSlots(account);
|
|
152
|
-
} catch (err) {
|
|
153
|
-
setError(err instanceof Error ? err.message : "模板请求失败");
|
|
154
|
-
}
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
if (!isOpen) return null;
|
|
158
|
-
|
|
159
|
-
const filteredTemplates = templates.filter((tpl) => {
|
|
160
|
-
if (activeSlotFilter === "all") return true;
|
|
161
|
-
return tpl.slot === activeSlotFilter;
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
return (
|
|
165
|
-
<div className="fixed inset-0 z-50 bg-black/50 dark:bg-black/70 backdrop-blur-sm flex items-center justify-center p-4">
|
|
166
|
-
<div className="bg-white dark:bg-[#0f0f0f] border border-[#eaeaea] dark:border-[#262626] rounded-xl w-full max-w-xl flex flex-col shadow-2xl overflow-hidden animate-in fade-in zoom-in-95 duration-150 transition-colors">
|
|
167
|
-
{/* Header */}
|
|
168
|
-
<div className="flex items-center justify-between px-5 py-3.5 border-b border-[#eaeaea] dark:border-[#222222]">
|
|
169
|
-
<div className="flex items-center gap-2 font-semibold text-sm text-[#171717] dark:text-white">
|
|
170
|
-
<Sparkles className="w-4 h-4 text-emerald-500 dark:text-emerald-400" />
|
|
171
|
-
<span>排版模板中心</span>
|
|
172
|
-
</div>
|
|
173
|
-
<button
|
|
174
|
-
onClick={onClose}
|
|
175
|
-
className="text-[#888888] hover:text-black dark:hover:text-white p-1 rounded-md hover:bg-[#f5f5f5] dark:hover:bg-[#1a1a1a]"
|
|
176
|
-
>
|
|
177
|
-
<X className="w-4 h-4" />
|
|
178
|
-
</button>
|
|
179
|
-
</div>
|
|
180
|
-
|
|
181
|
-
{/* Content */}
|
|
182
|
-
<div className="p-5 flex flex-col gap-4 max-h-[75vh] overflow-y-auto">
|
|
183
|
-
{/* Save current config as template */}
|
|
184
|
-
<div className="bg-[#f9f9f9] dark:bg-[#141414] border border-[#eaeaea] dark:border-[#222222] rounded-lg p-3 flex flex-col gap-2.5">
|
|
185
|
-
<span className="text-xs text-[#666666] dark:text-[#888888] font-medium">将当前参数另存为模板</span>
|
|
186
|
-
<div className="flex flex-col sm:flex-row gap-2">
|
|
187
|
-
<input
|
|
188
|
-
type="text"
|
|
189
|
-
value={templateName}
|
|
190
|
-
onChange={(e) => setTemplateName(e.target.value)}
|
|
191
|
-
placeholder="模板名称..."
|
|
192
|
-
className="flex-1 bg-white dark:bg-[#0a0a0a] border border-[#d4d4d4] dark:border-[#2a2a2a] rounded px-3 py-1.5 text-xs text-black dark:text-white focus:outline-none focus:border-black dark:focus:border-[#444444]"
|
|
193
|
-
/>
|
|
194
|
-
<select
|
|
195
|
-
value={saveSlot}
|
|
196
|
-
onChange={(e) => setSaveSlot(e.target.value as TemplateSlot | "none")}
|
|
197
|
-
className="bg-white dark:bg-[#0a0a0a] border border-[#d4d4d4] dark:border-[#2a2a2a] rounded px-2 py-1.5 text-xs text-black dark:text-white focus:outline-none focus:border-black dark:focus:border-[#444444]"
|
|
198
|
-
>
|
|
199
|
-
<option value="article_cover">槽位: 文章封面 (2.35:1)</option>
|
|
200
|
-
<option value="newspic_cover">槽位: 贴图封面 (3:4)</option>
|
|
201
|
-
<option value="newspic_content">槽位: 贴图内容 (3:4)</option>
|
|
202
|
-
<option value="none">槽位: 通用模板</option>
|
|
203
|
-
</select>
|
|
204
|
-
<button
|
|
205
|
-
onClick={handleSave}
|
|
206
|
-
disabled={isSaving}
|
|
207
|
-
className="flex items-center justify-center gap-1.5 px-3 py-1.5 bg-black text-white hover:bg-[#262626] dark:bg-white dark:text-black dark:hover:bg-[#eaeaea] text-xs font-semibold rounded transition-all btn-vercel disabled:opacity-50 shrink-0"
|
|
208
|
-
>
|
|
209
|
-
<FolderPlus className="w-3.5 h-3.5" />
|
|
210
|
-
<span>{isSaving ? "保存中..." : "保存模板"}</span>
|
|
211
|
-
</button>
|
|
212
|
-
</div>
|
|
213
|
-
</div>
|
|
214
|
-
|
|
215
|
-
{error && (
|
|
216
|
-
<div className="text-xs text-amber-600 dark:text-amber-400 bg-amber-500/10 border border-amber-500/20 px-3 py-2 rounded-lg">
|
|
217
|
-
{error}
|
|
218
|
-
</div>
|
|
219
|
-
)}
|
|
220
|
-
|
|
221
|
-
{/* Account and Slot Filters */}
|
|
222
|
-
<div className="flex flex-col gap-2 pt-1 border-t border-[#eaeaea] dark:border-[#222222]">
|
|
223
|
-
<div className="flex items-center justify-between text-xs">
|
|
224
|
-
<div className="flex items-center gap-1.5 font-medium text-[#666666] dark:text-[#888888]">
|
|
225
|
-
<span>默认槽位配置账号:</span>
|
|
226
|
-
<select
|
|
227
|
-
value={account}
|
|
228
|
-
onChange={(e) => setAccount(e.target.value)}
|
|
229
|
-
className="bg-white dark:bg-[#141414] border border-[#d4d4d4] dark:border-[#2a2a2a] rounded px-2 py-0.5 text-xs text-black dark:text-white focus:outline-none font-mono"
|
|
230
|
-
>
|
|
231
|
-
<option value="default">default (主账号)</option>
|
|
232
|
-
<option value="ancientone">ancientone</option>
|
|
233
|
-
</select>
|
|
234
|
-
</div>
|
|
235
|
-
</div>
|
|
236
|
-
|
|
237
|
-
{/* Slot Category Tabs */}
|
|
238
|
-
<div className="flex items-center gap-1.5 text-xs overflow-x-auto pb-1">
|
|
239
|
-
<button
|
|
240
|
-
onClick={() => setActiveSlotFilter("all")}
|
|
241
|
-
className={`px-2.5 py-1 rounded text-xs transition-colors font-medium ${
|
|
242
|
-
activeSlotFilter === "all"
|
|
243
|
-
? "bg-black text-white dark:bg-white dark:text-black"
|
|
244
|
-
: "text-[#666666] dark:text-[#888888] hover:bg-[#f5f5f5] dark:hover:bg-[#1c1c1c]"
|
|
245
|
-
}`}
|
|
246
|
-
>
|
|
247
|
-
全部
|
|
248
|
-
</button>
|
|
249
|
-
<button
|
|
250
|
-
onClick={() => setActiveSlotFilter("article_cover")}
|
|
251
|
-
className={`px-2.5 py-1 rounded text-xs transition-colors font-medium ${
|
|
252
|
-
activeSlotFilter === "article_cover"
|
|
253
|
-
? "bg-black text-white dark:bg-white dark:text-black"
|
|
254
|
-
: "text-[#666666] dark:text-[#888888] hover:bg-[#f5f5f5] dark:hover:bg-[#1c1c1c]"
|
|
255
|
-
}`}
|
|
256
|
-
>
|
|
257
|
-
文章封面
|
|
258
|
-
</button>
|
|
259
|
-
<button
|
|
260
|
-
onClick={() => setActiveSlotFilter("newspic_cover")}
|
|
261
|
-
className={`px-2.5 py-1 rounded text-xs transition-colors font-medium ${
|
|
262
|
-
activeSlotFilter === "newspic_cover"
|
|
263
|
-
? "bg-black text-white dark:bg-white dark:text-black"
|
|
264
|
-
: "text-[#666666] dark:text-[#888888] hover:bg-[#f5f5f5] dark:hover:bg-[#1c1c1c]"
|
|
265
|
-
}`}
|
|
266
|
-
>
|
|
267
|
-
贴图封面
|
|
268
|
-
</button>
|
|
269
|
-
<button
|
|
270
|
-
onClick={() => setActiveSlotFilter("newspic_content")}
|
|
271
|
-
className={`px-2.5 py-1 rounded text-xs transition-colors font-medium ${
|
|
272
|
-
activeSlotFilter === "newspic_content"
|
|
273
|
-
? "bg-black text-white dark:bg-white dark:text-black"
|
|
274
|
-
: "text-[#666666] dark:text-[#888888] hover:bg-[#f5f5f5] dark:hover:bg-[#1c1c1c]"
|
|
275
|
-
}`}
|
|
276
|
-
>
|
|
277
|
-
贴图内容
|
|
278
|
-
</button>
|
|
279
|
-
</div>
|
|
280
|
-
</div>
|
|
281
|
-
|
|
282
|
-
{/* Template List */}
|
|
283
|
-
<div className="flex flex-col gap-2">
|
|
284
|
-
{isLoading ? (
|
|
285
|
-
<div className="text-center py-8 text-xs text-[#888888] dark:text-[#555555]">正在加载模板...</div>
|
|
286
|
-
) : filteredTemplates.length === 0 ? (
|
|
287
|
-
<div className="text-center py-8 text-xs text-[#888888] dark:text-[#555555]">暂无匹配模板</div>
|
|
288
|
-
) : (
|
|
289
|
-
filteredTemplates.map((tpl) => {
|
|
290
|
-
const isArticleCoverDefault = slotsData.article_cover?.id === tpl.id;
|
|
291
|
-
const isNewspicCoverDefault = slotsData.newspic_cover?.id === tpl.id;
|
|
292
|
-
const isNewspicContentDefault = slotsData.newspic_content?.id === tpl.id;
|
|
293
|
-
|
|
294
|
-
return (
|
|
295
|
-
<div
|
|
296
|
-
key={tpl.id}
|
|
297
|
-
className="flex flex-col sm:flex-row sm:items-center justify-between p-3 rounded-lg bg-[#fafafa] dark:bg-[#141414] border border-[#eaeaea] dark:border-[#222222] hover:border-[#cccccc] dark:hover:border-[#333333] transition-all gap-2 group"
|
|
298
|
-
>
|
|
299
|
-
<div className="flex items-center gap-3">
|
|
300
|
-
<div
|
|
301
|
-
className="w-5 h-5 rounded border border-[#d4d4d4] dark:border-[#333333] shrink-0"
|
|
302
|
-
style={{ backgroundColor: tpl.theme.bgColor }}
|
|
303
|
-
/>
|
|
304
|
-
<div>
|
|
305
|
-
<div className="flex items-center gap-1.5 flex-wrap">
|
|
306
|
-
<span className="text-xs font-semibold text-[#171717] dark:text-white">{tpl.name}</span>
|
|
307
|
-
{tpl.slot && (
|
|
308
|
-
<span className="text-[10px] px-1.5 py-0.2 rounded bg-blue-500/10 text-blue-600 dark:text-blue-400 border border-blue-500/20 font-medium">
|
|
309
|
-
{tpl.slot === "article_cover"
|
|
310
|
-
? "文章封面"
|
|
311
|
-
: tpl.slot === "newspic_cover"
|
|
312
|
-
? "贴图封面"
|
|
313
|
-
: "贴图内容"}
|
|
314
|
-
</span>
|
|
315
|
-
)}
|
|
316
|
-
{isArticleCoverDefault && (
|
|
317
|
-
<span className="text-[10px] px-1.5 py-0.2 rounded bg-amber-500/10 text-amber-600 dark:text-amber-400 border border-amber-500/20 font-medium flex items-center gap-0.5">
|
|
318
|
-
<Star className="w-2.5 h-2.5 fill-amber-500" /> 文章封面默认
|
|
319
|
-
</span>
|
|
320
|
-
)}
|
|
321
|
-
{isNewspicCoverDefault && (
|
|
322
|
-
<span className="text-[10px] px-1.5 py-0.2 rounded bg-amber-500/10 text-amber-600 dark:text-amber-400 border border-amber-500/20 font-medium flex items-center gap-0.5">
|
|
323
|
-
<Star className="w-2.5 h-2.5 fill-amber-500" /> 贴图封面默认
|
|
324
|
-
</span>
|
|
325
|
-
)}
|
|
326
|
-
{isNewspicContentDefault && (
|
|
327
|
-
<span className="text-[10px] px-1.5 py-0.2 rounded bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20 font-medium flex items-center gap-0.5">
|
|
328
|
-
<Star className="w-2.5 h-2.5 fill-emerald-500" /> 贴图内容默认
|
|
329
|
-
</span>
|
|
330
|
-
)}
|
|
331
|
-
</div>
|
|
332
|
-
<div className="text-[10px] text-[#888888] dark:text-[#666666] font-mono mt-0.5">
|
|
333
|
-
{tpl.aspectRatio} • 字号: {tpl.algorithm.baseFontSize}px
|
|
334
|
-
{tpl.images && tpl.images.length > 0 && ` • 配图: ${tpl.images.length}个`}
|
|
335
|
-
</div>
|
|
336
|
-
</div>
|
|
337
|
-
</div>
|
|
338
|
-
|
|
339
|
-
<div className="flex items-center gap-2 self-end sm:self-auto">
|
|
340
|
-
{/* Set as slot default selector */}
|
|
341
|
-
{tpl.id && (
|
|
342
|
-
<select
|
|
343
|
-
value=""
|
|
344
|
-
onChange={(e) => {
|
|
345
|
-
if (e.target.value) {
|
|
346
|
-
handleSetSlotDefault(e.target.value as TemplateSlot, tpl.id!);
|
|
347
|
-
}
|
|
348
|
-
}}
|
|
349
|
-
className="text-[11px] bg-white dark:bg-[#1a1a1a] border border-[#d4d4d4] dark:border-[#333333] text-[#666666] dark:text-[#aaaaaa] rounded px-2 py-1 focus:outline-none"
|
|
350
|
-
title="为此账号设为某个槽位的默认模板"
|
|
351
|
-
>
|
|
352
|
-
<option value="">设为槽位默认...</option>
|
|
353
|
-
<option value="article_cover">设为「文章封面默认」</option>
|
|
354
|
-
<option value="newspic_cover">设为「贴图封面默认」</option>
|
|
355
|
-
<option value="newspic_content">设为「贴图内容默认」</option>
|
|
356
|
-
</select>
|
|
357
|
-
)}
|
|
358
|
-
|
|
359
|
-
<button
|
|
360
|
-
onClick={() => {
|
|
361
|
-
onApplyTemplate(tpl);
|
|
362
|
-
onClose();
|
|
363
|
-
}}
|
|
364
|
-
className="flex items-center gap-1 px-2.5 py-1 bg-white dark:bg-[#1f1f1f] hover:bg-emerald-50 dark:hover:bg-emerald-500/20 text-[#333333] dark:text-[#cccccc] hover:text-emerald-700 dark:hover:text-emerald-400 border border-[#d4d4d4] dark:border-[#2a2a2a] hover:border-emerald-500/40 rounded text-xs transition-all font-medium"
|
|
365
|
-
>
|
|
366
|
-
<Check className="w-3 h-3" />
|
|
367
|
-
<span>套用</span>
|
|
368
|
-
</button>
|
|
369
|
-
|
|
370
|
-
{tpl.id &&
|
|
371
|
-
!tpl.id.startsWith("tpl_wechat_") &&
|
|
372
|
-
!tpl.id.startsWith("tpl_longform_") &&
|
|
373
|
-
!tpl.id.startsWith("tpl_square_") &&
|
|
374
|
-
!tpl.id.startsWith("tpl_wide_") && (
|
|
375
|
-
<button
|
|
376
|
-
onClick={() => handleDelete(tpl.id!)}
|
|
377
|
-
className="p-1 text-[#888888] dark:text-[#555555] hover:text-red-500 rounded hover:bg-[#f0f0f0] dark:hover:bg-[#1f1f1f]"
|
|
378
|
-
title="删除自定义模板"
|
|
379
|
-
>
|
|
380
|
-
<Trash2 className="w-3.5 h-3.5" />
|
|
381
|
-
</button>
|
|
382
|
-
)}
|
|
383
|
-
</div>
|
|
384
|
-
</div>
|
|
385
|
-
);
|
|
386
|
-
})
|
|
387
|
-
)}
|
|
388
|
-
</div>
|
|
389
|
-
</div>
|
|
390
|
-
</div>
|
|
391
|
-
</div>
|
|
392
|
-
);
|
|
393
|
-
};
|