feat: customize workflow category labels
This commit is contained in:
parent
ba539ce236
commit
5ce1e48392
@ -913,6 +913,8 @@ export function AgentWorkflowSettingsClient({
|
||||
() => getAgentWorkflowCategoryLabels(config),
|
||||
[config]
|
||||
);
|
||||
const firstCategoryLabel = categoryLabels.firstCategory;
|
||||
const secondCategoryLabel = categoryLabels.secondCategory;
|
||||
const thirdCategoryLabel = categoryLabels.thirdCategory;
|
||||
const fourthCategoryLabel = categoryLabels.fourthCategory;
|
||||
const fifthCategoryLabel = categoryLabels.fifthCategory;
|
||||
@ -1013,12 +1015,12 @@ export function AgentWorkflowSettingsClient({
|
||||
const handleAddBrandEntry = () => {
|
||||
const nextValue = normalizeInputOptionValue(brandDraft);
|
||||
if (!nextValue) {
|
||||
toast.error("请输入品牌名称");
|
||||
toast.error(`请输入${firstCategoryLabel}名称`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (brandStoreEntries.some((item) => item.brandName.toLowerCase() === nextValue.toLowerCase())) {
|
||||
toast.error("该品牌已存在");
|
||||
toast.error(`该${firstCategoryLabel}已存在`);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1074,7 +1076,7 @@ export function AgentWorkflowSettingsClient({
|
||||
const draft = storeDrafts[brandName] || "";
|
||||
const nextValue = normalizeInputOptionValue(draft);
|
||||
if (!nextValue) {
|
||||
toast.error("请输入店铺名称");
|
||||
toast.error(`请输入${secondCategoryLabel}名称`);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1085,7 +1087,7 @@ export function AgentWorkflowSettingsClient({
|
||||
) ||
|
||||
targetEntry?.stores.some((store) => store.toLowerCase() === nextValue.toLowerCase())
|
||||
) {
|
||||
toast.error("该店铺已存在");
|
||||
toast.error(`该${secondCategoryLabel}已存在`);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -2014,7 +2016,7 @@ export function AgentWorkflowSettingsClient({
|
||||
const handleDownloadBrandTemplate = async () => {
|
||||
try {
|
||||
await downloadAgentWorkflowBrandImportTemplate(categoryLabels);
|
||||
toast.success("品牌导入模板已开始下载");
|
||||
toast.success("层级导入模板已开始下载");
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "下载模板失败");
|
||||
}
|
||||
@ -2031,7 +2033,7 @@ export function AgentWorkflowSettingsClient({
|
||||
setIsImportingBrandSheet(true);
|
||||
const { entries, rowCount } = await parseAgentWorkflowBrandImportFile(file, categoryLabels);
|
||||
updateInputOptions([...brandStoreEntries, ...entries]);
|
||||
toast.success(`已从表格导入 ${rowCount} 行,品牌树已自动合并去重`);
|
||||
toast.success(`已从表格导入 ${rowCount} 行,层级树已自动合并去重`);
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "导入表格失败");
|
||||
} finally {
|
||||
@ -3477,7 +3479,9 @@ export function AgentWorkflowSettingsClient({
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-base">基础下拉选项</CardTitle>
|
||||
<CardDescription>配置品牌、店铺与分类层级关系,以及第一页可选的价格带区间</CardDescription>
|
||||
<CardDescription>
|
||||
配置{firstCategoryLabel}、{secondCategoryLabel}与分类层级关系,以及第一页可选的价格带区间
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@ -3485,14 +3489,14 @@ export function AgentWorkflowSettingsClient({
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">品牌树</Label>
|
||||
<Label className="text-xs">层级树</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
支持一键下载模板并导入品牌、店铺、{thirdCategoryLabel}、{fourthCategoryLabel}、{fifthCategoryLabel}表格
|
||||
支持一键下载模板并导入{firstCategoryLabel}、{secondCategoryLabel}、{thirdCategoryLabel}、{fourthCategoryLabel}、{fifthCategoryLabel}表格
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center justify-end gap-2">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{brandStoreEntries.length} 个品牌
|
||||
{brandStoreEntries.length} 个{firstCategoryLabel}
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
@ -3526,7 +3530,23 @@ export function AgentWorkflowSettingsClient({
|
||||
className="hidden"
|
||||
onChange={handleBrandSheetImport}
|
||||
/>
|
||||
<div className="grid grid-cols-1 gap-3 rounded-xl border bg-background/70 p-3 md:grid-cols-3">
|
||||
<div className="grid grid-cols-1 gap-3 rounded-xl border bg-background/70 p-3 md:grid-cols-2 xl:grid-cols-5">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">第一级名称</Label>
|
||||
<Input
|
||||
value={firstCategoryLabel}
|
||||
onChange={(e) => handleCategoryLabelChange("firstCategory", e.target.value)}
|
||||
placeholder="例如:品牌分类"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">第二级名称</Label>
|
||||
<Input
|
||||
value={secondCategoryLabel}
|
||||
onChange={(e) => handleCategoryLabelChange("secondCategory", e.target.value)}
|
||||
placeholder="例如:门店分类"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">第三级名称</Label>
|
||||
<Input
|
||||
@ -3563,7 +3583,7 @@ export function AgentWorkflowSettingsClient({
|
||||
handleAddBrandEntry();
|
||||
}
|
||||
}}
|
||||
placeholder="输入一个品牌名称后点击添加"
|
||||
placeholder={`输入一个${firstCategoryLabel}名称后点击添加`}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
@ -3571,7 +3591,7 @@ export function AgentWorkflowSettingsClient({
|
||||
className="shrink-0"
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
添加品牌
|
||||
添加{firstCategoryLabel}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -3587,13 +3607,13 @@ export function AgentWorkflowSettingsClient({
|
||||
type="button"
|
||||
onClick={() => handleRemoveBrandEntry(entry.brandName)}
|
||||
className="rounded-full p-0.5 text-muted-foreground transition-colors hover:bg-muted hover:text-destructive"
|
||||
aria-label={`删除品牌 ${entry.brandName}`}
|
||||
aria-label={`删除${firstCategoryLabel} ${entry.brandName}`}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{(entry.storeEntries || []).length} 个店铺
|
||||
{(entry.storeEntries || []).length} 个{secondCategoryLabel}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -3614,7 +3634,7 @@ export function AgentWorkflowSettingsClient({
|
||||
handleAddStoreToBrand(entry.brandName);
|
||||
}
|
||||
}}
|
||||
placeholder={`给品牌“${entry.brandName}”添加店铺`}
|
||||
placeholder={`给${firstCategoryLabel}“${entry.brandName}”添加${secondCategoryLabel}`}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
@ -3623,7 +3643,7 @@ export function AgentWorkflowSettingsClient({
|
||||
variant="outline"
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
添加店铺
|
||||
添加{secondCategoryLabel}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -3649,7 +3669,7 @@ export function AgentWorkflowSettingsClient({
|
||||
)
|
||||
}
|
||||
className="rounded-full p-0.5 text-muted-foreground transition-colors hover:bg-muted hover:text-destructive"
|
||||
aria-label={`删除店铺 ${storeEntry.storeName}`}
|
||||
aria-label={`删除${secondCategoryLabel} ${storeEntry.storeName}`}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
@ -3679,7 +3699,7 @@ export function AgentWorkflowSettingsClient({
|
||||
);
|
||||
}
|
||||
}}
|
||||
placeholder={`给店铺“${storeEntry.storeName}”添加${thirdCategoryLabel}`}
|
||||
placeholder={`给${secondCategoryLabel}“${storeEntry.storeName}”添加${thirdCategoryLabel}`}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
@ -3908,7 +3928,7 @@ export function AgentWorkflowSettingsClient({
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-3 rounded-lg border border-dashed bg-background/70 px-3 py-4 text-sm text-muted-foreground">
|
||||
这个店铺下还没有添加任何{thirdCategoryLabel}。
|
||||
这个{secondCategoryLabel}下还没有添加任何{thirdCategoryLabel}。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -3918,7 +3938,7 @@ export function AgentWorkflowSettingsClient({
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-3 rounded-lg border border-dashed bg-background/70 px-3 py-4 text-sm text-muted-foreground">
|
||||
这个品牌下还没有添加任何店铺。
|
||||
这个{firstCategoryLabel}下还没有添加任何{secondCategoryLabel}。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -3927,7 +3947,7 @@ export function AgentWorkflowSettingsClient({
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-3 rounded-lg border border-dashed bg-background/70 px-3 py-4 text-sm text-muted-foreground">
|
||||
还没有添加品牌。先添加品牌,再在每个品牌下继续添加店铺以及{thirdCategoryLabel}到{fifthCategoryLabel}。
|
||||
还没有添加{firstCategoryLabel}。先添加{firstCategoryLabel},再在每个{firstCategoryLabel}下继续添加{secondCategoryLabel}以及{thirdCategoryLabel}到{fifthCategoryLabel}。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -3985,7 +4005,7 @@ export function AgentWorkflowSettingsClient({
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-3 rounded-lg border border-dashed bg-background/70 px-3 py-4 text-sm text-muted-foreground">
|
||||
还没有添加价格带。添加后,第一页品牌和店铺右侧会显示价格带下拉框。
|
||||
还没有添加价格带。添加后,第一页{firstCategoryLabel}和{secondCategoryLabel}右侧会显示价格带下拉框。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -4036,7 +4056,7 @@ export function AgentWorkflowSettingsClient({
|
||||
<div>
|
||||
<CardTitle className="text-base">PSD 文字颜色选项</CardTitle>
|
||||
<CardDescription>
|
||||
按品牌配置套用 PSD 模板时可选的两组文字颜色,前台会跟随当前品牌读取。
|
||||
按{firstCategoryLabel}配置套用 PSD 模板时可选的两组文字颜色,前台会跟随当前{firstCategoryLabel}读取。
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
@ -4044,22 +4064,22 @@ export function AgentWorkflowSettingsClient({
|
||||
<CardContent className="space-y-4">
|
||||
{psdTextColorBrandOptions.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">选择品牌</Label>
|
||||
<Label className="text-xs">选择{firstCategoryLabel}</Label>
|
||||
<SearchableSelect
|
||||
value={selectedPsdTextColorBrand}
|
||||
options={psdTextColorBrandOptions}
|
||||
placeholder="选择一个品牌后配置颜色"
|
||||
searchPlaceholder="输入品牌名筛选"
|
||||
emptyText="暂无品牌可选"
|
||||
placeholder={`选择一个${firstCategoryLabel}后配置颜色`}
|
||||
searchPlaceholder={`输入${firstCategoryLabel}名筛选`}
|
||||
emptyText={`暂无${firstCategoryLabel}可选`}
|
||||
onValueChange={setSelectedPsdTextColorBrand}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
未单独配置的品牌会回退系统默认颜色;当前编辑品牌:{selectedPsdTextColorBrand}
|
||||
未单独配置的{firstCategoryLabel}会回退系统默认颜色;当前编辑{firstCategoryLabel}:{selectedPsdTextColorBrand}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border border-dashed bg-muted/10 px-4 py-3 text-sm text-muted-foreground">
|
||||
请先在上方“品牌树”里配置品牌,再按品牌设置 PSD 文字颜色。
|
||||
请先在上方“层级树”里配置{firstCategoryLabel},再按{firstCategoryLabel}设置 PSD 文字颜色。
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -4146,14 +4166,14 @@ export function AgentWorkflowSettingsClient({
|
||||
<UserCircle className="h-4 w-4 text-rose-600" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-base">品牌模特图库</CardTitle>
|
||||
<CardTitle className="text-base">{firstCategoryLabel}模特图库</CardTitle>
|
||||
<CardDescription>
|
||||
为每个品牌配置模特图,第二步会按品牌展示供用户选择
|
||||
为每个{firstCategoryLabel}配置模特图,第二步会按{firstCategoryLabel}展示供用户选择
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="secondary">
|
||||
{brandModelConfig.entries.length} 个品牌
|
||||
{brandModelConfig.entries.length} 个{firstCategoryLabel}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@ -4254,7 +4274,7 @@ export function AgentWorkflowSettingsClient({
|
||||
<Package className="h-6 w-6 text-muted-foreground" />
|
||||
</div>
|
||||
<p className="mt-4 text-sm text-muted-foreground">
|
||||
先在"基础配置"中添加品牌,然后为每个品牌上传模特图
|
||||
先在“基础配置”中添加{firstCategoryLabel},然后为每个{firstCategoryLabel}上传模特图
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -740,6 +740,8 @@ const updateSchema = z.object({
|
||||
.optional(),
|
||||
categoryLabels: z
|
||||
.object({
|
||||
firstCategory: z.string().trim().min(1).max(24).optional(),
|
||||
secondCategory: z.string().trim().min(1).max(24).optional(),
|
||||
thirdCategory: z.string().trim().min(1).max(24).optional(),
|
||||
fourthCategory: z.string().trim().min(1).max(24).optional(),
|
||||
fifthCategory: z.string().trim().min(1).max(24).optional(),
|
||||
|
||||
@ -10,6 +10,7 @@ import { Textarea } from "@/components/ui/textarea";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
defaultAgentWorkflowUiConfig,
|
||||
getAgentWorkflowCategoryLabels,
|
||||
getAgentWorkflowBrandStoreEntries,
|
||||
getAgentWorkflowFifthCategoryOptionsByBrandStoreThirdFourth,
|
||||
getAgentWorkflowFourthCategoryOptionsByBrandStoreThirdCategory,
|
||||
@ -92,6 +93,15 @@ export function AgentWorkflowPanel({
|
||||
onNextStep,
|
||||
onSkipStep,
|
||||
}: AgentWorkflowPanelProps) {
|
||||
const categoryLabels = useMemo(
|
||||
() => getAgentWorkflowCategoryLabels(config),
|
||||
[config]
|
||||
);
|
||||
const firstCategoryLabel = categoryLabels.firstCategory;
|
||||
const secondCategoryLabel = categoryLabels.secondCategory;
|
||||
const thirdCategoryLabel = categoryLabels.thirdCategory;
|
||||
const fourthCategoryLabel = categoryLabels.fourthCategory;
|
||||
const fifthCategoryLabel = categoryLabels.fifthCategory;
|
||||
const brandOptions = useMemo(
|
||||
() => config?.inputOptions?.brands || defaultAgentWorkflowUiConfig.inputOptions?.brands || [],
|
||||
[config?.inputOptions?.brands]
|
||||
@ -248,9 +258,9 @@ export function AgentWorkflowPanel({
|
||||
<SearchableSelect
|
||||
value={inputs.brand}
|
||||
options={brandOptions}
|
||||
placeholder="品牌"
|
||||
searchPlaceholder="输入品牌名筛选"
|
||||
emptyText="暂无品牌可选,请先到系统设置配置"
|
||||
placeholder={firstCategoryLabel}
|
||||
searchPlaceholder={`输入${firstCategoryLabel}名筛选`}
|
||||
emptyText={`暂无${firstCategoryLabel}可选,请先到系统设置配置`}
|
||||
onValueChange={(value) =>
|
||||
onInputsChange({
|
||||
...inputs,
|
||||
@ -271,9 +281,13 @@ export function AgentWorkflowPanel({
|
||||
<SearchableSelect
|
||||
value={inputs.store}
|
||||
options={storeOptions}
|
||||
placeholder="店铺"
|
||||
searchPlaceholder="输入店铺名筛选"
|
||||
emptyText={inputs.brand ? "当前品牌下暂无店铺可选" : "请先选择品牌"}
|
||||
placeholder={secondCategoryLabel}
|
||||
searchPlaceholder={`输入${secondCategoryLabel}名筛选`}
|
||||
emptyText={
|
||||
inputs.brand
|
||||
? `当前${firstCategoryLabel}下暂无${secondCategoryLabel}可选`
|
||||
: `请先选择${firstCategoryLabel}`
|
||||
}
|
||||
disabled={!inputs.brand || (brandStoreEntries.length > 0 && storeOptions.length === 0)}
|
||||
onValueChange={(value) =>
|
||||
onInputsChange({
|
||||
@ -291,9 +305,13 @@ export function AgentWorkflowPanel({
|
||||
<SearchableSelect
|
||||
value={inputs.thirdCategory}
|
||||
options={thirdCategoryOptions}
|
||||
placeholder="选择三级分类"
|
||||
searchPlaceholder="输入三级分类筛选"
|
||||
emptyText={inputs.store ? "当前店铺下暂无三级分类" : "请先选择店铺"}
|
||||
placeholder={`选择${thirdCategoryLabel}`}
|
||||
searchPlaceholder={`输入${thirdCategoryLabel}筛选`}
|
||||
emptyText={
|
||||
inputs.store
|
||||
? `当前${secondCategoryLabel}下暂无${thirdCategoryLabel}`
|
||||
: `请先选择${secondCategoryLabel}`
|
||||
}
|
||||
disabled={!inputs.store || thirdCategoryOptions.length === 0}
|
||||
onValueChange={(value) =>
|
||||
onInputsChange({
|
||||
@ -310,9 +328,13 @@ export function AgentWorkflowPanel({
|
||||
<SearchableSelect
|
||||
value={inputs.fourthCategory}
|
||||
options={fourthCategoryOptions}
|
||||
placeholder="选择四级分类"
|
||||
searchPlaceholder="输入四级分类筛选"
|
||||
emptyText={inputs.thirdCategory ? "当前三级分类下暂无四级分类" : "请先选择三级分类"}
|
||||
placeholder={`选择${fourthCategoryLabel}`}
|
||||
searchPlaceholder={`输入${fourthCategoryLabel}筛选`}
|
||||
emptyText={
|
||||
inputs.thirdCategory
|
||||
? `当前${thirdCategoryLabel}下暂无${fourthCategoryLabel}`
|
||||
: `请先选择${thirdCategoryLabel}`
|
||||
}
|
||||
disabled={!inputs.thirdCategory || fourthCategoryOptions.length === 0}
|
||||
onValueChange={(value) =>
|
||||
onInputsChange({
|
||||
@ -328,9 +350,13 @@ export function AgentWorkflowPanel({
|
||||
<SearchableSelect
|
||||
value={inputs.fifthCategory}
|
||||
options={fifthCategoryOptions}
|
||||
placeholder="选择五级分类"
|
||||
searchPlaceholder="输入五级分类筛选"
|
||||
emptyText={inputs.fourthCategory ? "当前四级分类下暂无五级分类" : "请先选择四级分类"}
|
||||
placeholder={`选择${fifthCategoryLabel}`}
|
||||
searchPlaceholder={`输入${fifthCategoryLabel}筛选`}
|
||||
emptyText={
|
||||
inputs.fourthCategory
|
||||
? `当前${fourthCategoryLabel}下暂无${fifthCategoryLabel}`
|
||||
: `请先选择${fourthCategoryLabel}`
|
||||
}
|
||||
disabled={!inputs.fourthCategory || fifthCategoryOptions.length === 0}
|
||||
onValueChange={(value) => onInputsChange({ ...inputs, fifthCategory: value })}
|
||||
/>
|
||||
@ -346,7 +372,7 @@ export function AgentWorkflowPanel({
|
||||
</div>
|
||||
{(brandOptions.length > 0 || storeOptions.length > 0) && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
品牌和店铺支持输入关键字匹配下拉选择,候选列表由系统设置配置。
|
||||
{firstCategoryLabel}和{secondCategoryLabel}支持输入关键字匹配下拉选择,候选列表由系统设置配置。
|
||||
</div>
|
||||
)}
|
||||
<div className="text-xs text-muted-foreground">
|
||||
@ -438,19 +464,19 @@ export function AgentWorkflowPanel({
|
||||
<span className="rounded-full bg-background px-2 py-1">搭配SKU:{inputs.secondarySku}</span>
|
||||
) : null}
|
||||
{inputs.brand ? (
|
||||
<span className="rounded-full bg-background px-2 py-1">品牌:{inputs.brand}</span>
|
||||
<span className="rounded-full bg-background px-2 py-1">{firstCategoryLabel}:{inputs.brand}</span>
|
||||
) : null}
|
||||
{inputs.store ? (
|
||||
<span className="rounded-full bg-background px-2 py-1">店铺:{inputs.store}</span>
|
||||
<span className="rounded-full bg-background px-2 py-1">{secondCategoryLabel}:{inputs.store}</span>
|
||||
) : null}
|
||||
{inputs.thirdCategory ? (
|
||||
<span className="rounded-full bg-background px-2 py-1">三级分类:{inputs.thirdCategory}</span>
|
||||
<span className="rounded-full bg-background px-2 py-1">{thirdCategoryLabel}:{inputs.thirdCategory}</span>
|
||||
) : null}
|
||||
{inputs.fourthCategory ? (
|
||||
<span className="rounded-full bg-background px-2 py-1">四级分类:{inputs.fourthCategory}</span>
|
||||
<span className="rounded-full bg-background px-2 py-1">{fourthCategoryLabel}:{inputs.fourthCategory}</span>
|
||||
) : null}
|
||||
{inputs.fifthCategory ? (
|
||||
<span className="rounded-full bg-background px-2 py-1">五级分类:{inputs.fifthCategory}</span>
|
||||
<span className="rounded-full bg-background px-2 py-1">{fifthCategoryLabel}:{inputs.fifthCategory}</span>
|
||||
) : null}
|
||||
{inputs.priceBand ? (
|
||||
<span className="rounded-full bg-background px-2 py-1">价格带:{inputs.priceBand}</span>
|
||||
|
||||
@ -11,6 +11,7 @@ import { Textarea } from "@/components/ui/textarea";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
defaultAgentWorkflowUiConfig,
|
||||
getAgentWorkflowCategoryLabels,
|
||||
getAgentWorkflowBrandStoreEntries,
|
||||
getAgentWorkflowFifthCategoryOptionsByBrandStoreThirdFourth,
|
||||
getAgentWorkflowFourthCategoryOptionsByBrandStoreThirdCategory,
|
||||
@ -174,6 +175,15 @@ export function AgentWorkflowPanelDark({
|
||||
onNextStep,
|
||||
onSkipStep,
|
||||
}: AgentWorkflowPanelDarkProps) {
|
||||
const categoryLabels = useMemo(
|
||||
() => getAgentWorkflowCategoryLabels(config),
|
||||
[config]
|
||||
);
|
||||
const firstCategoryLabel = categoryLabels.firstCategory;
|
||||
const secondCategoryLabel = categoryLabels.secondCategory;
|
||||
const thirdCategoryLabel = categoryLabels.thirdCategory;
|
||||
const fourthCategoryLabel = categoryLabels.fourthCategory;
|
||||
const fifthCategoryLabel = categoryLabels.fifthCategory;
|
||||
const brandOptions = useMemo(
|
||||
() => config?.inputOptions?.brands || defaultAgentWorkflowUiConfig.inputOptions?.brands || [],
|
||||
[config?.inputOptions?.brands]
|
||||
@ -351,9 +361,9 @@ export function AgentWorkflowPanelDark({
|
||||
<SearchableSelect
|
||||
value={inputs.brand}
|
||||
options={brandOptions}
|
||||
placeholder="选择品牌"
|
||||
searchPlaceholder="输入品牌名筛选"
|
||||
emptyText="暂无品牌可选"
|
||||
placeholder={`选择${firstCategoryLabel}`}
|
||||
searchPlaceholder={`输入${firstCategoryLabel}名筛选`}
|
||||
emptyText={`暂无${firstCategoryLabel}可选`}
|
||||
onValueChange={(value) =>
|
||||
onInputsChange({
|
||||
...inputs,
|
||||
@ -374,9 +384,13 @@ export function AgentWorkflowPanelDark({
|
||||
<SearchableSelect
|
||||
value={inputs.store}
|
||||
options={storeOptions}
|
||||
placeholder="选择店铺"
|
||||
searchPlaceholder="输入店铺名筛选"
|
||||
emptyText={inputs.brand ? "当前品牌下暂无店铺可选" : "请先选择品牌"}
|
||||
placeholder={`选择${secondCategoryLabel}`}
|
||||
searchPlaceholder={`输入${secondCategoryLabel}名筛选`}
|
||||
emptyText={
|
||||
inputs.brand
|
||||
? `当前${firstCategoryLabel}下暂无${secondCategoryLabel}可选`
|
||||
: `请先选择${firstCategoryLabel}`
|
||||
}
|
||||
disabled={!inputs.brand || (brandStoreEntries.length > 0 && storeOptions.length === 0)}
|
||||
onValueChange={(value) =>
|
||||
onInputsChange({
|
||||
@ -394,9 +408,13 @@ export function AgentWorkflowPanelDark({
|
||||
<SearchableSelect
|
||||
value={inputs.thirdCategory}
|
||||
options={thirdCategoryOptions}
|
||||
placeholder="选择三级分类"
|
||||
searchPlaceholder="输入三级分类筛选"
|
||||
emptyText={inputs.store ? "当前店铺下暂无三级分类" : "请先选择店铺"}
|
||||
placeholder={`选择${thirdCategoryLabel}`}
|
||||
searchPlaceholder={`输入${thirdCategoryLabel}筛选`}
|
||||
emptyText={
|
||||
inputs.store
|
||||
? `当前${secondCategoryLabel}下暂无${thirdCategoryLabel}`
|
||||
: `请先选择${secondCategoryLabel}`
|
||||
}
|
||||
disabled={!inputs.store || thirdCategoryOptions.length === 0}
|
||||
onValueChange={(value) =>
|
||||
onInputsChange({
|
||||
@ -413,9 +431,13 @@ export function AgentWorkflowPanelDark({
|
||||
<SearchableSelect
|
||||
value={inputs.fourthCategory}
|
||||
options={fourthCategoryOptions}
|
||||
placeholder="选择四级分类"
|
||||
searchPlaceholder="输入四级分类筛选"
|
||||
emptyText={inputs.thirdCategory ? "当前三级分类下暂无四级分类" : "请先选择三级分类"}
|
||||
placeholder={`选择${fourthCategoryLabel}`}
|
||||
searchPlaceholder={`输入${fourthCategoryLabel}筛选`}
|
||||
emptyText={
|
||||
inputs.thirdCategory
|
||||
? `当前${thirdCategoryLabel}下暂无${fourthCategoryLabel}`
|
||||
: `请先选择${thirdCategoryLabel}`
|
||||
}
|
||||
disabled={!inputs.thirdCategory || fourthCategoryOptions.length === 0}
|
||||
onValueChange={(value) =>
|
||||
onInputsChange({
|
||||
@ -431,9 +453,13 @@ export function AgentWorkflowPanelDark({
|
||||
<SearchableSelect
|
||||
value={inputs.fifthCategory}
|
||||
options={fifthCategoryOptions}
|
||||
placeholder="选择五级分类"
|
||||
searchPlaceholder="输入五级分类筛选"
|
||||
emptyText={inputs.fourthCategory ? "当前四级分类下暂无五级分类" : "请先选择四级分类"}
|
||||
placeholder={`选择${fifthCategoryLabel}`}
|
||||
searchPlaceholder={`输入${fifthCategoryLabel}筛选`}
|
||||
emptyText={
|
||||
inputs.fourthCategory
|
||||
? `当前${fourthCategoryLabel}下暂无${fifthCategoryLabel}`
|
||||
: `请先选择${fourthCategoryLabel}`
|
||||
}
|
||||
disabled={!inputs.fourthCategory || fifthCategoryOptions.length === 0}
|
||||
onValueChange={(value) => onInputsChange({ ...inputs, fifthCategory: value })}
|
||||
/>
|
||||
|
||||
@ -285,9 +285,17 @@ export function AgentWorkflowBrandStorePanel({
|
||||
() => getAgentWorkflowCategoryLabels(config),
|
||||
[config]
|
||||
);
|
||||
const firstCategoryLabel = categoryLabels.firstCategory;
|
||||
const secondCategoryLabel = categoryLabels.secondCategory;
|
||||
const thirdCategoryLabel = categoryLabels.thirdCategory;
|
||||
const fourthCategoryLabel = categoryLabels.fourthCategory;
|
||||
const fifthCategoryLabel = categoryLabels.fifthCategory;
|
||||
const resolvedTitle =
|
||||
title === "品牌层级选择" ? `${firstCategoryLabel}层级选择` : title;
|
||||
const resolvedDescription =
|
||||
description === "先选择品牌、店铺、分类层级和价格带,后续品牌模特与风格步骤会按这里的选择联动。"
|
||||
? `先选择${firstCategoryLabel}、${secondCategoryLabel}、分类层级和价格带,后续品牌模特与风格步骤会按这里的选择联动。`
|
||||
: description;
|
||||
const brandOptions = useMemo(
|
||||
() => config?.inputOptions?.brands || defaultAgentWorkflowUiConfig.inputOptions?.brands || [],
|
||||
[config?.inputOptions?.brands]
|
||||
@ -422,19 +430,19 @@ export function AgentWorkflowBrandStorePanel({
|
||||
>
|
||||
<div className="mb-4 space-y-1">
|
||||
<div className={cn("text-sm font-semibold", isDark ? "text-white" : "text-foreground")}>
|
||||
{title}
|
||||
{resolvedTitle}
|
||||
</div>
|
||||
<div className={cn("text-xs leading-5", isDark ? "text-white/48" : "text-muted-foreground")}>
|
||||
{description}
|
||||
{resolvedDescription}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid max-w-7xl grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-6">
|
||||
<SearchableSelect
|
||||
value={inputs.brand}
|
||||
options={brandOptions}
|
||||
placeholder="选择品牌"
|
||||
searchPlaceholder="输入品牌名筛选"
|
||||
emptyText="暂无品牌可选"
|
||||
placeholder={`选择${firstCategoryLabel}`}
|
||||
searchPlaceholder={`输入${firstCategoryLabel}名筛选`}
|
||||
emptyText={`暂无${firstCategoryLabel}可选`}
|
||||
theme={theme}
|
||||
onValueChange={(value) =>
|
||||
onInputsChange({
|
||||
@ -456,9 +464,13 @@ export function AgentWorkflowBrandStorePanel({
|
||||
<SearchableSelect
|
||||
value={inputs.store}
|
||||
options={storeOptions}
|
||||
placeholder="选择店铺"
|
||||
searchPlaceholder="输入店铺名筛选"
|
||||
emptyText={inputs.brand ? "当前品牌下暂无店铺可选" : "请先选择品牌"}
|
||||
placeholder={`选择${secondCategoryLabel}`}
|
||||
searchPlaceholder={`输入${secondCategoryLabel}名筛选`}
|
||||
emptyText={
|
||||
inputs.brand
|
||||
? `当前${firstCategoryLabel}下暂无${secondCategoryLabel}可选`
|
||||
: `请先选择${firstCategoryLabel}`
|
||||
}
|
||||
theme={theme}
|
||||
disabled={!inputs.brand || (brandStoreEntries.length > 0 && storeOptions.length === 0)}
|
||||
onValueChange={(value) =>
|
||||
@ -480,7 +492,11 @@ export function AgentWorkflowBrandStorePanel({
|
||||
options={thirdCategoryOptions}
|
||||
placeholder={`选择${thirdCategoryLabel}`}
|
||||
searchPlaceholder={`输入${thirdCategoryLabel}筛选`}
|
||||
emptyText={inputs.store ? `当前店铺下暂无${thirdCategoryLabel}` : "请先选择店铺"}
|
||||
emptyText={
|
||||
inputs.store
|
||||
? `当前${secondCategoryLabel}下暂无${thirdCategoryLabel}`
|
||||
: `请先选择${secondCategoryLabel}`
|
||||
}
|
||||
theme={theme}
|
||||
disabled={!inputs.store || thirdCategoryOptions.length === 0}
|
||||
onValueChange={(value) =>
|
||||
@ -570,6 +586,12 @@ export function AgentWorkflowPanelDarkV2({
|
||||
embedded = false,
|
||||
showBrandStoreSection = true,
|
||||
}: AgentWorkflowPanelDarkV2Props) {
|
||||
const categoryLabels = useMemo(
|
||||
() => getAgentWorkflowCategoryLabels(config),
|
||||
[config]
|
||||
);
|
||||
const firstCategoryLabel = categoryLabels.firstCategory;
|
||||
const secondCategoryLabel = categoryLabels.secondCategory;
|
||||
const resolvedConfig = useMemo(() => {
|
||||
const source = config || defaultAgentWorkflowUiConfig;
|
||||
return {
|
||||
@ -866,7 +888,7 @@ export function AgentWorkflowPanelDarkV2({
|
||||
<span className="flex h-6 w-6 items-center justify-center rounded-md bg-emerald-500/20 text-xs font-bold text-emerald-400">
|
||||
1
|
||||
</span>
|
||||
品牌和店铺选择
|
||||
{firstCategoryLabel}和{secondCategoryLabel}选择
|
||||
</h2>
|
||||
<AgentWorkflowBrandStorePanel
|
||||
config={config}
|
||||
|
||||
@ -11,6 +11,7 @@ import { Textarea } from "@/components/ui/textarea";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
defaultAgentWorkflowUiConfig,
|
||||
getAgentWorkflowCategoryLabels,
|
||||
getAgentWorkflowBrandStoreEntries,
|
||||
getAgentWorkflowFifthCategoryOptionsByBrandStoreThirdFourth,
|
||||
getAgentWorkflowFourthCategoryOptionsByBrandStoreThirdCategory,
|
||||
@ -186,6 +187,15 @@ export function AgentWorkflowPanelNew({
|
||||
onNextStep,
|
||||
onSkipStep,
|
||||
}: AgentWorkflowPanelNewProps) {
|
||||
const categoryLabels = useMemo(
|
||||
() => getAgentWorkflowCategoryLabels(config),
|
||||
[config]
|
||||
);
|
||||
const firstCategoryLabel = categoryLabels.firstCategory;
|
||||
const secondCategoryLabel = categoryLabels.secondCategory;
|
||||
const thirdCategoryLabel = categoryLabels.thirdCategory;
|
||||
const fourthCategoryLabel = categoryLabels.fourthCategory;
|
||||
const fifthCategoryLabel = categoryLabels.fifthCategory;
|
||||
const brandOptions = useMemo(
|
||||
() => config?.inputOptions?.brands || defaultAgentWorkflowUiConfig.inputOptions?.brands || [],
|
||||
[config?.inputOptions?.brands]
|
||||
@ -386,9 +396,9 @@ export function AgentWorkflowPanelNew({
|
||||
<SearchableSelect
|
||||
value={inputs.brand}
|
||||
options={brandOptions}
|
||||
placeholder="选择品牌"
|
||||
searchPlaceholder="输入品牌名筛选"
|
||||
emptyText="暂无品牌可选"
|
||||
placeholder={`选择${firstCategoryLabel}`}
|
||||
searchPlaceholder={`输入${firstCategoryLabel}名筛选`}
|
||||
emptyText={`暂无${firstCategoryLabel}可选`}
|
||||
onValueChange={(value) =>
|
||||
onInputsChange({
|
||||
...inputs,
|
||||
@ -409,9 +419,13 @@ export function AgentWorkflowPanelNew({
|
||||
<SearchableSelect
|
||||
value={inputs.store}
|
||||
options={storeOptions}
|
||||
placeholder="选择店铺"
|
||||
searchPlaceholder="输入店铺名筛选"
|
||||
emptyText={inputs.brand ? "当前品牌下暂无店铺可选" : "请先选择品牌"}
|
||||
placeholder={`选择${secondCategoryLabel}`}
|
||||
searchPlaceholder={`输入${secondCategoryLabel}名筛选`}
|
||||
emptyText={
|
||||
inputs.brand
|
||||
? `当前${firstCategoryLabel}下暂无${secondCategoryLabel}可选`
|
||||
: `请先选择${firstCategoryLabel}`
|
||||
}
|
||||
disabled={!inputs.brand || (brandStoreEntries.length > 0 && storeOptions.length === 0)}
|
||||
onValueChange={(value) =>
|
||||
onInputsChange({
|
||||
@ -429,9 +443,13 @@ export function AgentWorkflowPanelNew({
|
||||
<SearchableSelect
|
||||
value={inputs.thirdCategory}
|
||||
options={thirdCategoryOptions}
|
||||
placeholder="选择三级分类"
|
||||
searchPlaceholder="输入三级分类筛选"
|
||||
emptyText={inputs.store ? "当前店铺下暂无三级分类" : "请先选择店铺"}
|
||||
placeholder={`选择${thirdCategoryLabel}`}
|
||||
searchPlaceholder={`输入${thirdCategoryLabel}筛选`}
|
||||
emptyText={
|
||||
inputs.store
|
||||
? `当前${secondCategoryLabel}下暂无${thirdCategoryLabel}`
|
||||
: `请先选择${secondCategoryLabel}`
|
||||
}
|
||||
disabled={!inputs.store || thirdCategoryOptions.length === 0}
|
||||
onValueChange={(value) =>
|
||||
onInputsChange({
|
||||
@ -448,9 +466,13 @@ export function AgentWorkflowPanelNew({
|
||||
<SearchableSelect
|
||||
value={inputs.fourthCategory}
|
||||
options={fourthCategoryOptions}
|
||||
placeholder="选择四级分类"
|
||||
searchPlaceholder="输入四级分类筛选"
|
||||
emptyText={inputs.thirdCategory ? "当前三级分类下暂无四级分类" : "请先选择三级分类"}
|
||||
placeholder={`选择${fourthCategoryLabel}`}
|
||||
searchPlaceholder={`输入${fourthCategoryLabel}筛选`}
|
||||
emptyText={
|
||||
inputs.thirdCategory
|
||||
? `当前${thirdCategoryLabel}下暂无${fourthCategoryLabel}`
|
||||
: `请先选择${thirdCategoryLabel}`
|
||||
}
|
||||
disabled={!inputs.thirdCategory || fourthCategoryOptions.length === 0}
|
||||
onValueChange={(value) =>
|
||||
onInputsChange({
|
||||
@ -466,9 +488,13 @@ export function AgentWorkflowPanelNew({
|
||||
<SearchableSelect
|
||||
value={inputs.fifthCategory}
|
||||
options={fifthCategoryOptions}
|
||||
placeholder="选择五级分类"
|
||||
searchPlaceholder="输入五级分类筛选"
|
||||
emptyText={inputs.fourthCategory ? "当前四级分类下暂无五级分类" : "请先选择四级分类"}
|
||||
placeholder={`选择${fifthCategoryLabel}`}
|
||||
searchPlaceholder={`输入${fifthCategoryLabel}筛选`}
|
||||
emptyText={
|
||||
inputs.fourthCategory
|
||||
? `当前${fourthCategoryLabel}下暂无${fifthCategoryLabel}`
|
||||
: `请先选择${fourthCategoryLabel}`
|
||||
}
|
||||
disabled={!inputs.fourthCategory || fifthCategoryOptions.length === 0}
|
||||
onValueChange={(value) => onInputsChange({ ...inputs, fifthCategory: value })}
|
||||
/>
|
||||
|
||||
@ -60,8 +60,8 @@ const TEMPLATE_SAMPLE_ROWS = [
|
||||
function getTemplateHeaders(labels?: Partial<AgentWorkflowCategoryLabels>) {
|
||||
const resolvedLabels = normalizeAgentWorkflowCategoryLabels(labels);
|
||||
return {
|
||||
brandName: "品牌",
|
||||
storeName: "店铺",
|
||||
brandName: resolvedLabels.firstCategory,
|
||||
storeName: resolvedLabels.secondCategory,
|
||||
thirdCategory: resolvedLabels.thirdCategory,
|
||||
fourthCategory: resolvedLabels.fourthCategory,
|
||||
fifthCategory: resolvedLabels.fifthCategory,
|
||||
@ -71,8 +71,12 @@ function getTemplateHeaders(labels?: Partial<AgentWorkflowCategoryLabels>) {
|
||||
function getHeaderAliases(labels?: Partial<AgentWorkflowCategoryLabels>) {
|
||||
const headers = getTemplateHeaders(labels);
|
||||
return {
|
||||
brandName: STATIC_HEADER_ALIASES.brandName,
|
||||
storeName: STATIC_HEADER_ALIASES.storeName,
|
||||
brandName: Array.from(
|
||||
new Set([headers.brandName, ...STATIC_HEADER_ALIASES.brandName].filter(Boolean))
|
||||
),
|
||||
storeName: Array.from(
|
||||
new Set([headers.storeName, ...STATIC_HEADER_ALIASES.storeName].filter(Boolean))
|
||||
),
|
||||
thirdCategory: Array.from(
|
||||
new Set([headers.thirdCategory, ...STATIC_HEADER_ALIASES.thirdCategory].filter(Boolean))
|
||||
),
|
||||
@ -230,21 +234,21 @@ export async function downloadAgentWorkflowBrandImportTemplate(
|
||||
instructionSheet.views = [{ state: "frozen", ySplit: 1 }];
|
||||
instructionSheet.addRows([
|
||||
{
|
||||
field: "品牌",
|
||||
field: resolvedLabels.firstCategory,
|
||||
required: "是",
|
||||
description: "每一行都必须填写品牌名称;相同品牌会自动合并。",
|
||||
description: `每一行都必须填写${resolvedLabels.firstCategory}名称;相同${resolvedLabels.firstCategory}会自动合并。`,
|
||||
example: "朴美",
|
||||
},
|
||||
{
|
||||
field: "店铺",
|
||||
field: resolvedLabels.secondCategory,
|
||||
required: "否",
|
||||
description: "不填时只导入品牌;填写后会挂到对应品牌下。",
|
||||
description: `不填时只导入${resolvedLabels.firstCategory};填写后会挂到对应${resolvedLabels.firstCategory}下。`,
|
||||
example: "天猫朴美家居旗舰店",
|
||||
},
|
||||
{
|
||||
field: resolvedLabels.thirdCategory,
|
||||
required: "否",
|
||||
description: `填写后会挂到对应店铺下;如果不填,${resolvedLabels.fourthCategory}/${resolvedLabels.fifthCategory}也必须留空。`,
|
||||
description: `填写后会挂到对应${resolvedLabels.secondCategory}下;如果不填,${resolvedLabels.fourthCategory}/${resolvedLabels.fifthCategory}也必须留空。`,
|
||||
example: "桌类",
|
||||
},
|
||||
{
|
||||
@ -290,7 +294,7 @@ export async function parseAgentWorkflowBrandImportFile(
|
||||
matchColumn(firstRecord, headerAliases.brandName) ||
|
||||
Object.keys(firstRecord).find((key) => normalizeHeader(key) === normalizeHeader(templateHeaders.brandName));
|
||||
if (!brandColumn) {
|
||||
throw new Error("未找到品牌列,请使用模板文件填写");
|
||||
throw new Error(`未找到${templateHeaders.brandName}列,请使用模板文件填写`);
|
||||
}
|
||||
|
||||
const storeColumn = matchColumn(firstRecord, headerAliases.storeName);
|
||||
@ -373,10 +377,12 @@ export async function parseAgentWorkflowBrandImportFile(
|
||||
}
|
||||
|
||||
if (!brandName) {
|
||||
throw new Error(`第 ${lineNumber} 行缺少品牌名称`);
|
||||
throw new Error(`第 ${lineNumber} 行缺少${resolvedLabels.firstCategory}名称`);
|
||||
}
|
||||
if (!storeName && (thirdCategory || fourthCategory || fifthCategory)) {
|
||||
throw new Error(`第 ${lineNumber} 行缺少店铺名称,不能直接填写分类`);
|
||||
throw new Error(
|
||||
`第 ${lineNumber} 行缺少${resolvedLabels.secondCategory}名称,不能直接填写分类`
|
||||
);
|
||||
}
|
||||
if (!thirdCategory && (fourthCategory || fifthCategory)) {
|
||||
throw new Error(
|
||||
|
||||
@ -14,6 +14,8 @@ export const AGENT_WORKFLOW_UI_CONFIG_KEY = "agent_workflow_ui_config";
|
||||
export const AGENT_WORKFLOW_MAX_TREE_DEPTH = 8;
|
||||
export const AGENT_WORKFLOW_MAX_OPTION_DEPTH = 8;
|
||||
export const defaultAgentWorkflowCategoryLabels: AgentWorkflowCategoryLabels = {
|
||||
firstCategory: "品牌",
|
||||
secondCategory: "店铺",
|
||||
thirdCategory: "三级分类",
|
||||
fourthCategory: "四级分类",
|
||||
fifthCategory: "五级分类",
|
||||
@ -183,6 +185,14 @@ export function normalizeAgentWorkflowCategoryLabels(
|
||||
};
|
||||
|
||||
return {
|
||||
firstCategory: normalizeLabel(
|
||||
record.firstCategory ?? record.firstCategoryLabel ?? record.brand ?? record.brandLabel,
|
||||
defaultAgentWorkflowCategoryLabels.firstCategory
|
||||
),
|
||||
secondCategory: normalizeLabel(
|
||||
record.secondCategory ?? record.secondCategoryLabel ?? record.store ?? record.storeLabel,
|
||||
defaultAgentWorkflowCategoryLabels.secondCategory
|
||||
),
|
||||
thirdCategory: normalizeLabel(
|
||||
record.thirdCategory ?? record.thirdCategoryLabel,
|
||||
defaultAgentWorkflowCategoryLabels.thirdCategory
|
||||
|
||||
@ -116,6 +116,8 @@ export interface AgentWorkflowUiConfig {
|
||||
}
|
||||
|
||||
export interface AgentWorkflowCategoryLabels {
|
||||
firstCategory: string;
|
||||
secondCategory: string;
|
||||
thirdCategory: string;
|
||||
fourthCategory: string;
|
||||
fifthCategory: string;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user