Files
omniai-ds-code-package/src/features/ecommerce/panels/EcommerceCopywritingPanel.tsx
T
Codex ad38a4a0e3 feat(ecommerce): add one-click copywriting tool with quick-board entry
- Add EcommerceCopywritingPanel component

- Wire copywriting tool into EcommercePage routing and state

- Add quick action entry; place before '更多功能'

- Add copywriting styles aligned with quick-set/hot-clone pages

- Merge latest main
2026-06-16 21:47:07 +08:00

290 lines
11 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.
import { useState } from "react";
import {
AppstoreOutlined,
CopyOutlined,
EditOutlined,
FileTextOutlined,
FireOutlined,
GlobalOutlined,
MessageOutlined,
SmileOutlined,
ThunderboltOutlined,
} from "@ant-design/icons";
export type CopywritingType =
| "self-media"
| "universal"
| "original"
| "imitate"
| "wechat"
| "crossborder"
| "emoji"
| "more";
interface CopywritingTypeOption {
key: CopywritingType;
label: string;
icon: React.ReactNode;
description: string;
}
const copywritingTypes: CopywritingTypeOption[] = [
{ key: "self-media", label: "自媒体文案", icon: <MessageOutlined />, description: "小红书/抖音/公众号风格" },
{ key: "universal", label: "万能写作", icon: <EditOutlined />, description: "通用场景长文短句" },
{ key: "original", label: "一键原创", icon: <ThunderboltOutlined />, description: "快速改写去重" },
{ key: "imitate", label: "文案仿写", icon: <CopyOutlined />, description: "参照爆款风格重写" },
{ key: "wechat", label: "微信营销文案", icon: <FileTextOutlined />, description: "朋友圈/社群转化文案" },
{ key: "crossborder", label: "跨境商品文案", icon: <GlobalOutlined />, description: "Amazon/Temu 卖点描述" },
{ key: "emoji", label: "文案加Emoji", icon: <SmileOutlined />, description: "自动插入表情符号" },
{ key: "more", label: "更多场景", icon: <AppstoreOutlined />, description: "持续更新中" },
];
const wordCountOptions = ["不限", "100字", "300字", "500字", "800字"];
const exampleResults: Record<CopywritingType, Array<{ title: string; body: string; points: string[] }>> = {
"self-media": [
{
title: "超值干发神器,吸水力 MAX",
body: "家人们,我发现了一款干发帽,双层加厚吸水力超强!而且只要个位数就能到手啊!",
points: [
"超强吸水力:这款干发帽采用微纤维材质,轻轻一裹,水分立马被吸走,头发快速告别湿漉漉。",
"柔软亲肤:触感超级柔软,对皮肤和头发都是温柔的抚摸,不会有摩擦伤害哦。",
"加厚设计:比普通干发帽更厚实,吸水效果自然更胜一筹,长发妹子的福音!",
"方便携带:轻巧不占空间,不论是去健身房还是旅行,携带都毫无负担。",
],
},
],
universal: [
{
title: "直接抄作业!科学的减重方法必试!",
body: "姐妹们冲鸭!有很多科学有效的方式可以帮助我们实现理想体重,今天就来分享一下必试的方法!",
points: [
"快乐有氧运动:科学的减重方式,通过有氧运动如慢跑、游泳等,能够促进脂肪燃烧,让身体更健康!",
"均衡饮食规划:摄入足够的蛋白质、蔬果以及谷物,避免过多的高糖和高脂食物,帮助达到减重目标!",
"科学计算热量:了解自己每日所需的卡路里摄入量,合理安排每餐的热量搭配,控制总摄入量。",
"坚持低强度运动:逐渐增加日常活动量,如步行、瑜伽等,通过持续的轻度运动,加速代谢!",
"合理休息调节:不要忽视睡眠的重要性,保证每晚充足的睡眠时间,帮助恢复体力和新陈代谢。",
],
},
],
original: [
{
title: "原创种草|这款干发帽真的值得入!",
body: "洗完头最烦的就是湿哒哒滴水?试试这条双层加厚干发帽,吸水速度真的惊艳到我。",
points: [
"加厚材质,吸水更快更彻底",
"柔软不勒头,长发短发都能用",
"轻便好收纳,差旅党必备",
"性价比超高,入手不亏",
],
},
],
imitate: [
{
title: "仿写爆款|让头发速干的小心机",
body: "姐妹们有没有发现,最近超火的干发帽真的太好用了!轻轻一裹,几分钟头发就半干了。",
points: [
"双层加厚,吸水力翻倍",
"柔软亲肤,不伤发质",
"小巧便携,出门也能带",
"颜值在线,多色可选",
],
},
],
wechat: [
{
title: "朋友圈文案|个位数到手的干发神器",
body: "今天必须给大家安利这个干发帽!双层加厚,吸水超强,个位数就能到手,真的不冲吗?",
points: [
"微纤维材质,轻柔速干",
"加厚设计,吸水更彻底",
"小巧便携,旅行出差都能带",
"限时好价,手慢无",
],
},
],
crossborder: [
{
title: "Amazon ListingSuper Absorbent Hair Turban",
body: "Made with ultra-soft microfiber, this double-layer hair turban dries hair quickly while protecting delicate strands.",
points: [
"Double-layer microfiber for maximum absorbency",
"Gentle on hair and skin, no frizz or breakage",
"Lightweight and travel-friendly design",
"Secure button closure stays in place",
],
},
],
emoji: [
{
title: "✨个位数到手的干发神器,吸水力 MAX!",
body: "家人们👋,我发现了一款超棒的干发帽💧,双层加厚吸水力超强!而且只要个位数就能到手啊🛒!",
points: [
"💦 超强吸水力:微纤维材质,轻轻一裹水分吸走",
"☁️ 柔软亲肤:触感温柔,不伤头发和皮肤",
"🎒 方便携带:轻巧不占空间,旅行健身都能带",
"💰 超值价格:个位数到手,性价比拉满",
],
},
],
more: [
{
title: "更多场景示例",
body: "选择左侧具体文案类型即可生成对应场景内容,更多场景持续更新中。",
points: ["选择合适的文案类型", "填写内容需求", "选择生成字数", "点击开始生成"],
},
],
};
export interface EcommerceCopywritingPanelProps {
onClose: () => void;
}
export default function EcommerceCopywritingPanel({ onClose }: EcommerceCopywritingPanelProps) {
const [selectedType, setSelectedType] = useState<CopywritingType>("self-media");
const [requirement, setRequirement] = useState("");
const [wordCount, setWordCount] = useState("不限");
const [loading, setLoading] = useState(false);
const [results, setResults] = useState<typeof exampleResults["self-media"]>([]);
const handleGenerate = () => {
setLoading(true);
setResults([]);
// 模拟生成延迟
window.setTimeout(() => {
setResults(exampleResults[selectedType]);
setLoading(false);
}, 1200);
};
const selectedTypeLabel = copywritingTypes.find((item) => item.key === selectedType)?.label ?? "文案";
return (
<main className="ecom-copywriting-page ecom-tool-page-enter" aria-label="一键文案">
<div className="ecom-copywriting-body">
<aside className="ecom-copywriting-panel" aria-label="文案设置">
<header className="ecom-copywriting-panel-head">
<strong className="ecom-copywriting-page-title"></strong>
<button type="button" className="ecom-copywriting-back" onClick={onClose}>
</button>
<button type="button" className="ecom-copywriting-back" onClick={onClose}>
</button>
</header>
<section className="ecom-copywriting-section">
<strong className="ecom-copywriting-section-title"></strong>
<div className="ecom-copywriting-type-grid">
{copywritingTypes.map((item) => (
<button
key={item.key}
type="button"
className={`ecom-copywriting-type-card${selectedType === item.key ? " is-active" : ""}`}
onClick={() => setSelectedType(item.key)}
>
<span className="ecom-copywriting-type-icon" aria-hidden="true">
{item.icon}
</span>
<span className="ecom-copywriting-type-label">{item.label}</span>
<span className="ecom-copywriting-type-desc">{item.description}</span>
</button>
))}
</div>
</section>
<section className="ecom-copywriting-section">
<strong className="ecom-copywriting-section-title"></strong>
<textarea
className="ecom-copywriting-textarea"
value={requirement}
onChange={(event) => setRequirement(event.target.value)}
placeholder="例如:主题、核心卖点、适用人群、期望场景等"
rows={5}
/>
</section>
<section className="ecom-copywriting-section">
<strong className="ecom-copywriting-section-title"></strong>
<div className="ecom-copywriting-wordcount">
{wordCountOptions.map((item) => (
<button
key={item}
type="button"
className={wordCount === item ? "is-active" : ""}
onClick={() => setWordCount(item)}
>
{item}
</button>
))}
</div>
</section>
<button
type="button"
className="ecom-copywriting-generate"
onClick={handleGenerate}
disabled={loading}
>
{loading ? (
<>
<span className="ecom-copywriting-spinner" />
</>
) : (
<>
<ThunderboltOutlined />
</>
)}
</button>
</aside>
<section className="ecom-copywriting-stage" aria-label="生成文案预览">
<header className="ecom-copywriting-preview-head">
<h1></h1>
<p>
<span>{selectedTypeLabel}</span> AI
</p>
</header>
<div className="ecom-copywriting-results">
{results.length === 0 && !loading ? (
<div className="ecom-copywriting-empty">
<FileTextOutlined />
<strong></strong>
<em></em>
</div>
) : null}
{loading ? (
<div className="ecom-copywriting-loading">
<span className="ecom-copywriting-spinner" />
<span>AI </span>
</div>
) : null}
{results.map((item, index) => (
<article key={index} className="ecom-copywriting-result-card">
<header>
<span> {index + 1}</span>
<strong>{item.title}</strong>
</header>
<p className="ecom-copywriting-result-body">{item.body}</p>
<ul className="ecom-copywriting-result-points">
{item.points.map((point, pointIndex) => (
<li key={pointIndex}>
<span>{pointIndex + 1}</span>
{point}
</li>
))}
</ul>
</article>
))}
</div>
</section>
</div>
</main>
);
}