feat(keywords): rename brand-question terminology to search-keyword + add entity exclusion
- Rename UI/prompt labels: "品牌主问题" → "优化关键词", "搜索问题" → "搜索词" - Add brand/competitor entity exclusion filter in question expansion (AI distill path) - Refactor combination question text normalization, remove manual question-mark appending - Update prompts to emphasize commercial-intent search queries over editorial phrasing - Sync prompt changes across server/configs, deploy, and k3s configs - Update i18n, frontend views (BrandQuestionCreate, TemplateWizard), and test fixtures Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,7 @@ import {
|
||||
insertHrCommand,
|
||||
toggleEmphasisCommand,
|
||||
toggleStrongCommand,
|
||||
turnIntoTextCommand,
|
||||
wrapInBlockquoteCommand,
|
||||
wrapInBulletListCommand,
|
||||
wrapInHeadingCommand,
|
||||
@@ -2266,6 +2267,7 @@ function runTableContextAction(action: TableContextMenuAction): void {
|
||||
<template #icon><IconFont type="icon-redo" /></template>
|
||||
</a-button>
|
||||
<span class="article-editor-canvas__divider"></span>
|
||||
<a-button size="small" type="text" @click="runCommand(turnIntoTextCommand)">正文</a-button>
|
||||
<a-button size="small" type="text" @click="runCommand(wrapInHeadingCommand, 1)">H1</a-button>
|
||||
<a-button size="small" type="text" @click="runCommand(wrapInHeadingCommand, 2)">H2</a-button>
|
||||
<a-button size="small" type="text" @click="runCommand(wrapInHeadingCommand, 3)">H3</a-button>
|
||||
|
||||
@@ -154,7 +154,7 @@ const enUS = {
|
||||
template_outline_generate: 'AI Outline Generation',
|
||||
kol_prompt_generate: 'KOL Prompt Generation',
|
||||
kol_prompt_optimize: 'KOL Prompt Optimize',
|
||||
question_distill: 'AI Candidate Question Generation',
|
||||
question_distill: 'AI Candidate Search Term Generation',
|
||||
question_combination_fill: 'Expansion Tool AI Fill',
|
||||
},
|
||||
aiUsageStatus: {
|
||||
@@ -214,12 +214,13 @@ const enUS = {
|
||||
'Review desktop publish tasks, external links, account workbench entry points, and retry state.',
|
||||
},
|
||||
brands: {
|
||||
title: 'Brand Library',
|
||||
description: 'Manage keywords, question sets, and competitors around each brand.',
|
||||
title: 'Company & Lexicon',
|
||||
description: 'Manage the keyword library (search terms) and competitors around each brand.',
|
||||
},
|
||||
brandQuestionCreate: {
|
||||
title: 'New question',
|
||||
description: 'Create brand questions with the expansion tool, AI expansion, or manual input.',
|
||||
title: 'New search term',
|
||||
description:
|
||||
'Create brand search terms with the expansion tool, AI expansion, or manual input.',
|
||||
},
|
||||
tracking: {
|
||||
title: 'Data Details',
|
||||
@@ -459,11 +460,11 @@ const enUS = {
|
||||
},
|
||||
tracking: {
|
||||
brandPlaceholder: 'Select brand',
|
||||
keywordPlaceholder: 'Select question set',
|
||||
allQuestionSets: 'All question sets',
|
||||
keywordPlaceholder: 'Select keyword library (search terms)',
|
||||
allQuestionSets: 'All keyword library (search terms)',
|
||||
collectNow: 'Collect Now',
|
||||
collectBrandRequired: 'Select a brand before collecting.',
|
||||
collectKeywordRequired: 'Select a question set before collecting.',
|
||||
collectKeywordRequired: 'Select a keyword library (search terms) before collecting.',
|
||||
collectTodayOnly: "Collect Now only supports today's data. Switch back to today to continue.",
|
||||
collectClientChecking: "Checking whether the current account's desktop client is online.",
|
||||
collectClientOnlineRequired:
|
||||
@@ -488,7 +489,7 @@ const enUS = {
|
||||
hotQuestions: 'Hot Questions',
|
||||
hotQuestionsTitle: 'Hot Questions',
|
||||
hotQuestionsHint:
|
||||
'Choose all question sets or one specific question. Selecting one question limits display and collection to that question.',
|
||||
'Choose the full keyword library (search terms) or one specific search term. Selecting one search term limits display and collection to that term.',
|
||||
questionSearchPlaceholder: 'Search questions in the selected scope',
|
||||
questionDetailEyebrow: 'Question Detail',
|
||||
questionDetailCollectedAt: 'Collected at {time}',
|
||||
@@ -841,13 +842,13 @@ const enUS = {
|
||||
language: 'Language',
|
||||
brandInfo: 'Brand',
|
||||
brandLibrary: 'Brand library',
|
||||
brandName: 'Brand name',
|
||||
brandName: 'Company name',
|
||||
website: 'Official website',
|
||||
brandSummary: 'AI brand summary',
|
||||
keywords: 'Keywords',
|
||||
brandQuestions: 'Brand questions',
|
||||
primaryQuestion: 'Primary brand question',
|
||||
supplementalQuestions: 'Supplemental questions',
|
||||
brandQuestions: 'Keyword library (search terms)',
|
||||
primaryQuestion: 'Optimization keyword',
|
||||
supplementalQuestions: 'Supplemental keywords',
|
||||
competitors: 'Competitors',
|
||||
chooseTitle: 'Choose a title',
|
||||
customTitle: 'Custom title',
|
||||
@@ -864,17 +865,17 @@ const enUS = {
|
||||
hints: {
|
||||
brandContext: 'Optional. Selecting a brand pulls in keywords and competitor context.',
|
||||
brandFlow:
|
||||
'You can pick a brand from the library or type one manually. After you choose a brand question, AI will prepare the summary, competitors, and title options.',
|
||||
'You can type a company name or choose one from the brand library. After you choose an optimization keyword, AI will prepare the summary, competitors, and title options.',
|
||||
brandSummaryPlaceholder:
|
||||
'After analysis, AI will summarize the brand angle, positioning, and content opportunities here.',
|
||||
keywords:
|
||||
'Type keywords directly or reuse brand-library keywords. AI results will be merged into this list.',
|
||||
brandQuestions:
|
||||
'The primary question controls the article spine; supplemental questions broaden nearby search intent.',
|
||||
'The optimization keyword controls the article spine; supplemental keywords broaden nearby search intent.',
|
||||
supplementalQuestions:
|
||||
'Choose up to 3 so the article covers related intent without drifting.',
|
||||
questionEmpty: 'No questions are available for this brand yet.',
|
||||
selectBrandFirst: 'Choose a brand from the library first.',
|
||||
selectBrandFirst: 'Choose a library brand from the company name dropdown first.',
|
||||
competitors:
|
||||
'Competitors stay editable. You can add rows manually; saving to the competitor library is only available after choosing a library brand.',
|
||||
competitorEmptyEditable:
|
||||
@@ -931,11 +932,11 @@ const enUS = {
|
||||
},
|
||||
placeholders: {
|
||||
brandLibrary: 'Choose an existing brand from the library',
|
||||
brandName: 'Enter a brand name, or refine the selected library brand',
|
||||
brandName: 'Enter a company name, or choose from the brand library',
|
||||
website: 'Official website, optional',
|
||||
keywords: 'Type keywords and press Enter',
|
||||
primaryQuestion: 'Choose one primary brand question',
|
||||
supplementalQuestions: 'Optional, choose up to 3 supplemental questions',
|
||||
primaryQuestion: 'Choose one optimization keyword',
|
||||
supplementalQuestions: 'Optional, choose up to 3 supplemental keywords',
|
||||
customTitle: 'Override the AI title here if needed',
|
||||
customOutline: 'Add another section, for example “Buying advice”',
|
||||
keyPoints: 'Add any extra instructions, key angles, or constraints for the article',
|
||||
@@ -949,9 +950,9 @@ const enUS = {
|
||||
aiBanner: {
|
||||
title: 'One-click AI analysis',
|
||||
defaultDescription:
|
||||
'AI will use the brand question to complete the brand summary and extract competitor context.',
|
||||
'AI will use the optimization keyword to complete the brand summary and extract competitor context.',
|
||||
productReviewDescription:
|
||||
'AI will use the brand question to organize the product context and suggest comparable alternatives.',
|
||||
'AI will use the optimization keyword to organize the product context and suggest comparable alternatives.',
|
||||
researchReportDescription:
|
||||
'AI will use the brand question to organize the topic context and suggest reference targets.',
|
||||
},
|
||||
@@ -983,15 +984,18 @@ const enUS = {
|
||||
},
|
||||
messages: {
|
||||
requiredField: 'Please fill in {field} first.',
|
||||
missingBrand: 'Choose a brand from the library or enter a brand name first.',
|
||||
selectBrandBeforeQuestion: 'Choose a brand from the library before selecting a question.',
|
||||
missingBrand: 'Enter or choose a company name first.',
|
||||
selectBrandBeforeQuestion:
|
||||
'Choose a library brand from the company name dropdown before selecting a question.',
|
||||
missingPrimaryQuestion: 'Choose one primary brand question.',
|
||||
missingTitle: 'Please choose or enter an article title.',
|
||||
missingOutline: 'Please select at least one outline section.',
|
||||
missingGeneratedOutline: 'Please generate and confirm the article outline first.',
|
||||
customOutlineTooLong: 'Custom sections cannot exceed {count} characters.',
|
||||
selectBrandBeforeCompetitors: 'Choose a brand from the library before managing competitors.',
|
||||
selectBrandBeforeFavorite: 'Choose a brand from the library before saving competitors.',
|
||||
selectBrandBeforeCompetitors:
|
||||
'Choose a library brand from the company name dropdown before managing competitors.',
|
||||
selectBrandBeforeFavorite:
|
||||
'Choose a library brand from the company name dropdown before saving competitors.',
|
||||
missingCompetitorName: 'Enter a competitor name before saving it to the library.',
|
||||
savedCompetitor: 'Competitor saved to the brand library.',
|
||||
unsavedCompetitor: 'Competitor removed from the brand library.',
|
||||
@@ -1285,20 +1289,21 @@ const enUS = {
|
||||
},
|
||||
},
|
||||
brands: {
|
||||
eyebrow: 'Brand Library',
|
||||
title: 'Brand Management',
|
||||
description: 'Manage question sets and competitor assets around each brand.',
|
||||
eyebrow: 'Company & Lexicon',
|
||||
title: 'Company & Lexicon',
|
||||
description: 'Manage the keyword library (search terms) and competitor assets around each brand.',
|
||||
railTitle: 'Brands',
|
||||
selectBrand: 'Pick a brand to continue managing question sets.',
|
||||
selectBrand: 'Pick a brand to continue managing the keyword library (search terms).',
|
||||
newBrand: 'New brand',
|
||||
editBrand: 'Edit brand',
|
||||
deleteBrand: 'Delete brand',
|
||||
guide: {
|
||||
title: 'Brand management guide',
|
||||
body: 'Manage brands, question sets, and competitor libraries for AI platform monitoring and GEO analysis.',
|
||||
body:
|
||||
'Manage brands, the keyword library (search terms), and competitor libraries for AI platform monitoring and GEO analysis.',
|
||||
},
|
||||
tabs: {
|
||||
questions: 'Question set',
|
||||
questions: 'Keyword library (search terms)',
|
||||
competitors: 'Competitors',
|
||||
},
|
||||
quota: {
|
||||
@@ -1321,20 +1326,20 @@ const enUS = {
|
||||
},
|
||||
sections: {
|
||||
keywords: 'Keywords',
|
||||
questions: 'Question set',
|
||||
questions: 'Keyword library (search terms)',
|
||||
competitors: 'Competitor library',
|
||||
versions: 'Version history',
|
||||
},
|
||||
actions: {
|
||||
addBrand: 'Add brand',
|
||||
newKeyword: 'New keyword',
|
||||
newQuestion: 'New question',
|
||||
newQuestion: 'New search term',
|
||||
newCompetitor: 'New competitor',
|
||||
viewVersions: 'View versions',
|
||||
},
|
||||
empty: {
|
||||
brands: 'No brand data yet. Create your first brand to start.',
|
||||
questions: 'No questions under this brand yet.',
|
||||
questions: 'No search terms under this brand yet.',
|
||||
competitors: 'No competitors under this brand yet.',
|
||||
brandDescription: 'No brand description yet',
|
||||
competitorDescription: 'No competitor description yet',
|
||||
@@ -1344,7 +1349,7 @@ const enUS = {
|
||||
brandWebsite: 'Company website (optional)',
|
||||
brandDescription: 'Brand description',
|
||||
keywordName: 'Keyword',
|
||||
questionText: 'Question text',
|
||||
questionText: 'Search term',
|
||||
competitorName: 'Competitor name',
|
||||
competitorWebsite: 'Competitor website',
|
||||
competitorDescription: 'Competitor description',
|
||||
@@ -1358,9 +1363,9 @@ const enUS = {
|
||||
createKeyword: 'Keyword created.',
|
||||
updateKeyword: 'Keyword updated.',
|
||||
deleteKeyword: 'Keyword deleted.',
|
||||
createQuestion: 'Question created.',
|
||||
updateQuestion: 'Question updated.',
|
||||
deleteQuestion: 'Question deleted.',
|
||||
createQuestion: 'Search term created.',
|
||||
updateQuestion: 'Search term updated.',
|
||||
deleteQuestion: 'Search term deleted.',
|
||||
createCompetitor: 'Competitor created.',
|
||||
updateCompetitor: 'Competitor updated.',
|
||||
deleteCompetitor: 'Competitor deleted.',
|
||||
@@ -1368,21 +1373,21 @@ const enUS = {
|
||||
chooseKeyword: 'Please choose a keyword first.',
|
||||
brandLimitReached: 'Your current plan allows up to {limit} brand companies.',
|
||||
keywordLimitReached: 'This account allows up to {limit} keywords.',
|
||||
questionLimitReached: 'This account allows up to {limit} questions.',
|
||||
questionLimitReached: 'This account allows up to {limit} search terms.',
|
||||
},
|
||||
questions: {
|
||||
create: 'New question',
|
||||
createSingle: 'Add single question',
|
||||
expand: 'Bulk expand questions',
|
||||
usage: 'Questions {used} / {total}',
|
||||
create: 'New search term',
|
||||
createSingle: 'Add single search term',
|
||||
expand: 'Bulk expand search terms',
|
||||
usage: 'Search terms {used} / {total}',
|
||||
remaining: '{count} remaining',
|
||||
generate: 'Generate candidates',
|
||||
emptyCandidates: 'No candidates yet. Generate or paste questions above.',
|
||||
emptyCandidates: 'No candidates yet. Generate or paste search terms above.',
|
||||
table: {
|
||||
question: 'Question set',
|
||||
question: 'Keyword library (search terms)',
|
||||
},
|
||||
modal: {
|
||||
title: 'New question',
|
||||
title: 'New search term',
|
||||
currentBrand: 'Current brand',
|
||||
methods: {
|
||||
single: 'Single',
|
||||
@@ -1393,17 +1398,17 @@ const enUS = {
|
||||
preview: 'Candidate preview',
|
||||
summary:
|
||||
'{total} candidates · {selected} selected · {tooShort} too short · {duplicate} duplicates',
|
||||
save: 'Save questions ({count})',
|
||||
save: 'Save search terms ({count})',
|
||||
},
|
||||
combination: {
|
||||
region: '1. Region',
|
||||
prefix: '2. Prefix',
|
||||
core: '3. Core (required)',
|
||||
core: '3. Precise term (required)',
|
||||
industry: '4. Industry (required)',
|
||||
suffix: '5. Suffix',
|
||||
defaultWord: 'Default',
|
||||
estimate: '{count} candidates estimated',
|
||||
coreRequired: 'Core is required',
|
||||
coreRequired: 'Precise term is required',
|
||||
industryRequired: 'Industry is required',
|
||||
},
|
||||
aiFill: {
|
||||
@@ -1414,25 +1419,25 @@ const enUS = {
|
||||
waitingStages: {
|
||||
region: 'Detecting city, province capital, and macro region...',
|
||||
generate: 'Analyzing brand context and high-frequency intent...',
|
||||
refine: 'Refining short combinable keyword groups...',
|
||||
refine: 'Refining short combinable precise term groups...',
|
||||
},
|
||||
},
|
||||
ai: {
|
||||
note: 'AI expansion consumes 1 AI point. Failed generations are refunded automatically.',
|
||||
seedTopic: 'Seed topic',
|
||||
seedTopicPlaceholder: 'Enter a topic and press Enter; multiple topics supported',
|
||||
seedTopic: 'User search term',
|
||||
seedTopicPlaceholder: 'Enter a search term and press Enter; multiple terms supported',
|
||||
seedTopicHint:
|
||||
'Use Enter, commas, or semicolons to add multiple topics. Candidates are merged and deduplicated automatically.',
|
||||
'Candidates expand from user search terms with strong commercial intent. The company name is a target term, not a candidate search term.',
|
||||
generate: 'Generate with AI',
|
||||
waitingTitle: 'AI is generating candidate questions. Please wait...',
|
||||
waitingTitle: 'AI is generating candidate search terms. Please wait...',
|
||||
waitingStages: {
|
||||
context: 'Organizing brand and topic context...',
|
||||
generate: 'Generating high-intent candidate questions...',
|
||||
refine: 'Deduplicating and filtering savable questions...',
|
||||
context: 'Organizing user search terms and the target term...',
|
||||
generate: 'Generating commercial candidate search terms suited for recommending companies...',
|
||||
refine: 'Deduplicating and filtering savable search terms...',
|
||||
},
|
||||
},
|
||||
batch: {
|
||||
label: 'One question per line',
|
||||
label: 'One search term per line',
|
||||
},
|
||||
badges: {
|
||||
duplicate: 'Duplicate',
|
||||
@@ -1440,20 +1445,22 @@ const enUS = {
|
||||
},
|
||||
messages: {
|
||||
partialSaved: 'Saved {created}; skipped {skipped}.',
|
||||
limitReached: 'This account allows up to {limit} questions shared across all brands.',
|
||||
selectionLimitReached: 'This plan allows up to {limit} questions. Selection limit reached.',
|
||||
limitReached: 'This account allows up to {limit} search terms shared across all brands.',
|
||||
selectionLimitReached:
|
||||
'This plan allows up to {limit} search terms. Selection limit reached.',
|
||||
truncated: 'Candidates exceeded the limit and were truncated to 500.',
|
||||
cacheHit: 'Used the most recent AI expansion result.',
|
||||
aiCharged: 'This AI expansion consumed {count} AI point.',
|
||||
},
|
||||
errors: {
|
||||
emptyInput: 'Enter question content first.',
|
||||
emptyInput: 'Enter search term content first.',
|
||||
noSelection: 'Select at least one candidate.',
|
||||
noValidQuestions: 'No valid questions can be saved. Adjust candidates and try again.',
|
||||
noValidQuestions: 'No valid search terms can be saved. Adjust candidates and try again.',
|
||||
},
|
||||
page: {
|
||||
title: 'New question',
|
||||
subtitle: 'Generate candidates, review them, then save them to this brand question set.',
|
||||
title: 'New search term',
|
||||
subtitle:
|
||||
'Generate candidates, review them, then save them to this brand keyword library (search terms).',
|
||||
steps: {
|
||||
configure: 'Configure',
|
||||
preview: 'Review candidates',
|
||||
@@ -1462,13 +1469,13 @@ const enUS = {
|
||||
methodHint:
|
||||
'Candidates remain temporary until saved, so you can generate, edit, and select first.',
|
||||
methods: {
|
||||
combination: 'Expand candidates from the column-based word tool.',
|
||||
ai: 'Let AI generate candidates around a seed topic.',
|
||||
batch: 'Paste questions from a sheet or document.',
|
||||
combination: 'Expand search term candidates from the column-based word tool.',
|
||||
ai: 'Let AI generate search term candidates around a seed topic.',
|
||||
batch: 'Paste search terms from a sheet or document.',
|
||||
},
|
||||
expansionHint:
|
||||
'One word per line. Empty optional columns are skipped; core and industry are required.',
|
||||
batchHint: 'One question per line. You can edit and select each one before saving.',
|
||||
'One word per line. Empty optional columns are skipped; precise term and industry are required.',
|
||||
batchHint: 'One search term per line. You can edit and select each one before saving.',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -79,7 +79,7 @@ const zhCN = {
|
||||
media: "媒体管理",
|
||||
publishManagement: "发文管理",
|
||||
brandManagement: "品牌管理",
|
||||
brands: "品牌词库",
|
||||
brands: "公司和词库",
|
||||
tracking: "数据追踪",
|
||||
trackingDetail: "数据详情",
|
||||
contentManagement: "内容管理",
|
||||
@@ -150,7 +150,7 @@ const zhCN = {
|
||||
template_outline_generate: "AI 生成大纲",
|
||||
kol_prompt_generate: "KOL 模版生成",
|
||||
kol_prompt_optimize: "KOL 模版优化",
|
||||
question_distill: "AI 生成候选问题",
|
||||
question_distill: "AI 生成候选搜索词",
|
||||
question_combination_fill: "拓词工具 AI 填词",
|
||||
},
|
||||
aiUsageStatus: {
|
||||
@@ -206,12 +206,12 @@ const zhCN = {
|
||||
description: "查看桌面端发布任务、外链、账号工作台入口和重发状态。",
|
||||
},
|
||||
brands: {
|
||||
title: "品牌词库",
|
||||
description: "围绕品牌维护关键词、问题集和竞品信息。",
|
||||
title: "公司和词库",
|
||||
description: "围绕品牌维护关键词库(搜索词)和竞品信息。",
|
||||
},
|
||||
brandQuestionCreate: {
|
||||
title: "新建问题",
|
||||
description: "用拓词工具、AI 扩展或手动录入创建品牌问题集。",
|
||||
title: "新建搜索词",
|
||||
description: "用拓词工具、AI 扩展或手动录入创建品牌关键词库(搜索词)。",
|
||||
},
|
||||
tracking: {
|
||||
title: "数据详情",
|
||||
@@ -440,11 +440,11 @@ const zhCN = {
|
||||
},
|
||||
tracking: {
|
||||
brandPlaceholder: "选择品牌",
|
||||
keywordPlaceholder: "选择问题集",
|
||||
allQuestionSets: "全部问题集",
|
||||
keywordPlaceholder: "选择关键词库(搜索词)",
|
||||
allQuestionSets: "全部关键词库(搜索词)",
|
||||
collectNow: "立即采集",
|
||||
collectBrandRequired: "请先选择品牌,再发起采集",
|
||||
collectKeywordRequired: "请选择问题集后再发起采集",
|
||||
collectKeywordRequired: "请选择关键词库(搜索词)后再发起采集",
|
||||
collectTodayOnly: "立即采集只支持今天的数据,请切回今天后再操作",
|
||||
collectClientChecking: "正在检查当前账号的桌面客户端状态",
|
||||
collectClientOnlineRequired: "当前登录账号的桌面客户端未在线,暂时不能立即采集",
|
||||
@@ -464,7 +464,7 @@ const zhCN = {
|
||||
platformMatrixTitle: "平台对比矩阵",
|
||||
hotQuestions: "Hot Questions",
|
||||
hotQuestionsTitle: "高频问题",
|
||||
hotQuestionsHint: "可选择全部问题集或具体单个问题;选择单个问题时只展示并采集该问题。",
|
||||
hotQuestionsHint: "可选择全部关键词库(搜索词)或具体单个搜索词;选择单个搜索词时只展示并采集该搜索词。",
|
||||
questionSearchPlaceholder: "搜索当前范围下的问题",
|
||||
questionDetailEyebrow: "Question Detail",
|
||||
questionDetailCollectedAt: "采集时间 {time}",
|
||||
@@ -806,13 +806,13 @@ const zhCN = {
|
||||
language: "文章语言",
|
||||
brandInfo: "品牌信息",
|
||||
brandLibrary: "品牌库",
|
||||
brandName: "品牌名称",
|
||||
brandName: "公司名称",
|
||||
website: "官网地址",
|
||||
brandSummary: "AI 品牌摘要",
|
||||
keywords: "关键词",
|
||||
brandQuestions: "品牌问题",
|
||||
primaryQuestion: "品牌主问题",
|
||||
supplementalQuestions: "补充覆盖问题",
|
||||
brandQuestions: "关键词库(搜索词)",
|
||||
primaryQuestion: "优化关键词",
|
||||
supplementalQuestions: "补充关键词",
|
||||
competitors: "竞品列表",
|
||||
chooseTitle: "选择文章标题",
|
||||
customTitle: "自定义标题",
|
||||
@@ -829,13 +829,13 @@ const zhCN = {
|
||||
},
|
||||
hints: {
|
||||
brandContext: "可选。选择后会自动带出关键词与竞品信息。",
|
||||
brandFlow: "品牌可直接输入,也可以从品牌库带入。选择品牌问题后,AI 会补充品牌摘要、竞品和标题建议。",
|
||||
brandFlow: "公司名称可直接输入,也可以从品牌库选择。选择优化关键词后,AI 会补充品牌摘要、竞品和标题建议。",
|
||||
brandSummaryPlaceholder: "AI 分析后会在这里补充品牌简介、差异点和适合切入的内容角度。",
|
||||
keywords: "支持直接输入,也支持复用品牌词库里的关键词。AI 分析结果会自动补充到这里。",
|
||||
brandQuestions: "主问题决定文章主线,补充覆盖问题只用于扩展相关搜索意图。",
|
||||
brandQuestions: "优化关键词决定文章主线,补充关键词只用于扩展相关搜索意图。",
|
||||
supplementalQuestions: "最多选择 3 个,用于覆盖相邻搜索问题,避免文章主题发散。",
|
||||
questionEmpty: "当前品牌还没有可选问题",
|
||||
selectBrandFirst: "请先选择品牌库对象",
|
||||
selectBrandFirst: "请先从公司名称下拉选择品牌库对象",
|
||||
competitors: "竞品支持手动编辑、追加;只有从品牌库选择品牌后,才可以收藏到品牌词库。AI 分析会尽量补齐候选竞品。",
|
||||
competitorEmptyEditable: "还没有竞品,先手动添加一条或点击分析让 AI 自动补齐。",
|
||||
templateFields: "这里保留当前模板自己的补充字段,用来约束 AI 生成角度。",
|
||||
@@ -887,11 +887,11 @@ const zhCN = {
|
||||
},
|
||||
placeholders: {
|
||||
brandLibrary: "从品牌库选择已有品牌",
|
||||
brandName: "输入品牌名,或在选择品牌后继续微调",
|
||||
brandName: "输入公司名称,或从品牌库选择",
|
||||
website: "输入官网地址,选填",
|
||||
keywords: "输入关键词后回车,可多选",
|
||||
primaryQuestion: "选择一个品牌主问题",
|
||||
supplementalQuestions: "可选,最多选择 3 个补充问题",
|
||||
primaryQuestion: "选择一个优化关键词",
|
||||
supplementalQuestions: "可选,最多选择 3 个补充关键词",
|
||||
customTitle: "如需覆盖 AI 标题,可在这里直接输入",
|
||||
customOutline: "补充一个额外结构,例如“选购建议”",
|
||||
keyPoints: "告诉 AI 还需要强调哪些要点、风格或限制条件",
|
||||
@@ -904,9 +904,9 @@ const zhCN = {
|
||||
promptSealed: "Prompt 已封装",
|
||||
aiBanner: {
|
||||
title: "一键智能分析",
|
||||
defaultDescription: "AI 将基于品牌问题补充品牌简介,并智能提取竞品信息",
|
||||
productReviewDescription: "AI 将基于品牌问题梳理产品背景,并生成同类替代方案参考",
|
||||
researchReportDescription: "AI 将基于品牌问题梳理主题背景,并生成参照对象参考",
|
||||
defaultDescription: "AI 将基于优化关键词补充品牌简介,并智能提取竞品信息",
|
||||
productReviewDescription: "AI 将基于优化关键词梳理产品背景,并生成同类替代方案参考",
|
||||
researchReportDescription: "AI 将基于优化关键词梳理主题背景,并生成参照对象参考",
|
||||
},
|
||||
assist: {
|
||||
analyzeTitle: "AI 正在补齐竞品信息,请稍后…",
|
||||
@@ -916,36 +916,36 @@ const zhCN = {
|
||||
analyzeStages: {
|
||||
brand: "品牌信息分析中…",
|
||||
keywords: "关键词提炼中…",
|
||||
questions: "品牌问题整理中…",
|
||||
questions: "关键词库(搜索词)整理中…",
|
||||
competitors: "竞品与官网信息补充中…",
|
||||
context: "正在整理品牌与官网上下文…",
|
||||
refine: "正在合并并筛选竞品列表…",
|
||||
},
|
||||
titleStages: {
|
||||
context: "正在整理当前内容上下文…",
|
||||
strategy: "正在结合品牌问题和竞品生成标题策略…",
|
||||
strategyNoCompetitors: "正在结合品牌问题生成标题策略…",
|
||||
strategy: "正在结合优化关键词和竞品生成标题策略…",
|
||||
strategyNoCompetitors: "正在结合优化关键词生成标题策略…",
|
||||
finalize: "正在输出 5 个标题候选…",
|
||||
},
|
||||
outlineStages: {
|
||||
context: "正在整理标题、品牌问题和竞品上下文…",
|
||||
contextNoCompetitors: "正在整理标题、品牌问题和评测上下文…",
|
||||
context: "正在整理标题、优化关键词和竞品上下文…",
|
||||
contextNoCompetitors: "正在整理标题、优化关键词和评测上下文…",
|
||||
structure: "正在根据已选结构生成文章大纲…",
|
||||
finalize: "正在输出可编辑的大纲结果…",
|
||||
},
|
||||
},
|
||||
messages: {
|
||||
requiredField: "请先填写 {field}",
|
||||
missingBrand: "请先选择品牌库对象或输入品牌名称",
|
||||
selectBrandBeforeQuestion: "请先从品牌库选择品牌后再选择品牌问题",
|
||||
missingPrimaryQuestion: "请选择一个品牌主问题",
|
||||
missingBrand: "请先输入或选择公司名称",
|
||||
selectBrandBeforeQuestion: "请先从公司名称下拉选择品牌库对象后再选择优化关键词",
|
||||
missingPrimaryQuestion: "请选择一个优化关键词",
|
||||
missingTitle: "请先选择或输入文章标题",
|
||||
missingReviewIntroHook: "请先选择一个评测引言钩子",
|
||||
missingOutline: "请至少选择一个文章结构段落",
|
||||
missingGeneratedOutline: "请先生成并确认文章大纲",
|
||||
customOutlineTooLong: "自定义结构不能超过 {count} 个字",
|
||||
selectBrandBeforeCompetitors: "请先从品牌库选择品牌后再管理竞品",
|
||||
selectBrandBeforeFavorite: "请先从品牌库选择品牌后再收藏竞品",
|
||||
selectBrandBeforeCompetitors: "请先从公司名称下拉选择品牌库对象后再管理竞品",
|
||||
selectBrandBeforeFavorite: "请先从公司名称下拉选择品牌库对象后再收藏竞品",
|
||||
missingCompetitorName: "至少需要填写竞品名称后才能收藏",
|
||||
savedCompetitor: "竞品已收藏到品牌词库",
|
||||
unsavedCompetitor: "已取消收藏该竞品",
|
||||
@@ -1213,20 +1213,20 @@ const zhCN = {
|
||||
},
|
||||
},
|
||||
brands: {
|
||||
eyebrow: "Brand Management",
|
||||
title: "品牌管理",
|
||||
description: "通过品牌名称和品牌相关信息维护问题集和竞品资产。",
|
||||
eyebrow: "Company & Lexicon",
|
||||
title: "公司和词库",
|
||||
description: "通过品牌名称和品牌相关信息维护关键词库(搜索词)和竞品资产。",
|
||||
railTitle: "品牌库",
|
||||
selectBrand: "选择品牌后即可继续维护问题集。",
|
||||
selectBrand: "选择品牌后即可继续维护关键词库(搜索词)。",
|
||||
newBrand: "新建品牌",
|
||||
editBrand: "编辑品牌",
|
||||
deleteBrand: "删除品牌",
|
||||
guide: {
|
||||
title: "品牌管理说明",
|
||||
body: "管理品牌、问题集和竞品库,用于后续 AI 平台监测与 GEO 分析。",
|
||||
body: "管理品牌、关键词库(搜索词)和竞品库,用于后续 AI 平台监测与 GEO 分析。",
|
||||
},
|
||||
tabs: {
|
||||
questions: "问题集",
|
||||
questions: "关键词库(搜索词)",
|
||||
competitors: "竞品库",
|
||||
},
|
||||
quota: {
|
||||
@@ -1249,20 +1249,20 @@ const zhCN = {
|
||||
},
|
||||
sections: {
|
||||
keywords: "关键词",
|
||||
questions: "问题集",
|
||||
questions: "关键词库(搜索词)",
|
||||
competitors: "竞品库",
|
||||
versions: "版本记录",
|
||||
},
|
||||
actions: {
|
||||
addBrand: "添加品牌",
|
||||
newKeyword: "新建关键词",
|
||||
newQuestion: "新建问题",
|
||||
newQuestion: "新建搜索词",
|
||||
newCompetitor: "新建竞品",
|
||||
viewVersions: "查看版本",
|
||||
},
|
||||
empty: {
|
||||
brands: "还没有品牌数据,先创建一个品牌。",
|
||||
questions: "当前品牌下暂无问题。",
|
||||
questions: "当前品牌下暂无搜索词。",
|
||||
competitors: "当前品牌下暂无竞品数据。",
|
||||
brandDescription: "暂无品牌描述",
|
||||
competitorDescription: "暂无竞品描述",
|
||||
@@ -1272,7 +1272,7 @@ const zhCN = {
|
||||
brandWebsite: "公司官网(选填)",
|
||||
brandDescription: "品牌描述",
|
||||
keywordName: "关键词名称",
|
||||
questionText: "问题内容",
|
||||
questionText: "搜索词内容",
|
||||
competitorName: "竞品名称",
|
||||
competitorWebsite: "竞品官网",
|
||||
competitorDescription: "竞品描述",
|
||||
@@ -1286,9 +1286,9 @@ const zhCN = {
|
||||
createKeyword: "关键词已创建",
|
||||
updateKeyword: "关键词已更新",
|
||||
deleteKeyword: "关键词已删除",
|
||||
createQuestion: "问题已创建",
|
||||
updateQuestion: "问题已更新",
|
||||
deleteQuestion: "问题已删除",
|
||||
createQuestion: "搜索词已创建",
|
||||
updateQuestion: "搜索词已更新",
|
||||
deleteQuestion: "搜索词已删除",
|
||||
createCompetitor: "竞品已创建",
|
||||
updateCompetitor: "竞品已更新",
|
||||
deleteCompetitor: "竞品已删除",
|
||||
@@ -1296,21 +1296,21 @@ const zhCN = {
|
||||
chooseKeyword: "请先选择关键词",
|
||||
brandLimitReached: "当前套餐最多可绑定 {limit} 个品牌公司",
|
||||
keywordLimitReached: "当前账号最多可绑定 {limit} 个关键词",
|
||||
questionLimitReached: "当前账号最多可维护 {limit} 个问题",
|
||||
questionLimitReached: "当前账号最多可维护 {limit} 个搜索词",
|
||||
},
|
||||
questions: {
|
||||
create: "新建问题",
|
||||
createSingle: "新增单条问题",
|
||||
expand: "批量扩展问题",
|
||||
usage: "问题 {used} / {total}",
|
||||
create: "新建搜索词",
|
||||
createSingle: "新增单条搜索词",
|
||||
expand: "批量扩展搜索词",
|
||||
usage: "搜索词 {used} / {total}",
|
||||
remaining: "还可保存 {count} 条",
|
||||
generate: "生成候选",
|
||||
emptyCandidates: "还没有候选,先从上方生成或粘贴问题。",
|
||||
emptyCandidates: "还没有候选,先从上方生成或粘贴搜索词。",
|
||||
table: {
|
||||
question: "问题集",
|
||||
question: "关键词库(搜索词)",
|
||||
},
|
||||
modal: {
|
||||
title: "新建问题",
|
||||
title: "新建搜索词",
|
||||
currentBrand: "当前品牌",
|
||||
methods: {
|
||||
single: "单条新增",
|
||||
@@ -1320,17 +1320,17 @@ const zhCN = {
|
||||
},
|
||||
preview: "候选预览",
|
||||
summary: "共 {total} 条候选 · 已选择 {selected} 条 · 过短 {tooShort} 条 · 重复 {duplicate} 条",
|
||||
save: "保存问题 ({count})",
|
||||
save: "保存搜索词 ({count})",
|
||||
},
|
||||
combination: {
|
||||
region: "1.地域词",
|
||||
prefix: "2.前缀词",
|
||||
core: "3.核心词(必填)",
|
||||
core: "3.精准词(必填)",
|
||||
industry: "4.行业词(必填)",
|
||||
suffix: "5.后缀词",
|
||||
defaultWord: "默认词",
|
||||
estimate: "预计生成 {count} 条候选",
|
||||
coreRequired: "核心不能为空",
|
||||
coreRequired: "精准词不能为空",
|
||||
industryRequired: "行业不能为空",
|
||||
},
|
||||
aiFill: {
|
||||
@@ -1341,24 +1341,24 @@ const zhCN = {
|
||||
waitingStages: {
|
||||
region: "正在识别所在地域并匹配城市、省会与大区…",
|
||||
generate: "正在分析品牌和高频搜索意图…",
|
||||
refine: "正在整理可组合的核心词组…",
|
||||
refine: "正在整理前缀词、精准词、行业词和后缀词…",
|
||||
},
|
||||
},
|
||||
ai: {
|
||||
note: "AI 扩展会消耗 1 个 AI 点;生成失败时系统会自动退还。",
|
||||
seedTopic: "扩展主题",
|
||||
seedTopicPlaceholder: "输入主题后按回车,可添加多个",
|
||||
seedTopicHint: "支持回车、逗号或分号分隔多个主题;系统会逐个扩展并自动合并去重。",
|
||||
seedTopic: "用户搜索词",
|
||||
seedTopicPlaceholder: "输入搜索词后按回车,可添加多个",
|
||||
seedTopicHint: "按用户搜索词扩展强商业属性候选;公司名称是目标达成词,不是候选搜索词。",
|
||||
generate: "AI 生成候选",
|
||||
waitingTitle: "AI 正在生成候选问题,请稍后…",
|
||||
waitingTitle: "AI 正在生成候选搜索词,请稍后…",
|
||||
waitingStages: {
|
||||
context: "正在整理品牌与主题上下文…",
|
||||
generate: "正在生成高意图候选问题…",
|
||||
refine: "正在去重并筛选可保存问题…",
|
||||
context: "正在整理用户搜索词和目标达成词…",
|
||||
generate: "正在生成适合推荐企业的强商业候选搜索词…",
|
||||
refine: "正在去重并筛选可保存搜索词…",
|
||||
},
|
||||
},
|
||||
batch: {
|
||||
label: "每行一个问题",
|
||||
label: "每行一个搜索词",
|
||||
},
|
||||
badges: {
|
||||
duplicate: "重复",
|
||||
@@ -1366,20 +1366,20 @@ const zhCN = {
|
||||
},
|
||||
messages: {
|
||||
partialSaved: "已保存 {created} 条,跳过 {skipped} 条",
|
||||
limitReached: "当前账号最多可维护 {limit} 个问题,所有品牌共用该额度",
|
||||
selectionLimitReached: "当前套餐最多可维护 {limit} 个问题,已达到可选上限",
|
||||
limitReached: "当前账号最多可维护 {limit} 个搜索词,所有品牌共用该额度",
|
||||
selectionLimitReached: "当前套餐最多可维护 {limit} 个搜索词,已达到可选上限",
|
||||
truncated: "候选超过上限,已截断为 500 条。",
|
||||
cacheHit: "已使用最近一次 AI 扩展结果。",
|
||||
aiCharged: "本次 AI 扩展已消耗 {count} 个 AI 点。",
|
||||
},
|
||||
errors: {
|
||||
emptyInput: "请先输入问题内容",
|
||||
emptyInput: "请先输入搜索词内容",
|
||||
noSelection: "请至少选择 1 条候选",
|
||||
noValidQuestions: "没有可保存的问题,请调整候选后重试",
|
||||
noValidQuestions: "没有可保存的搜索词,请调整候选后重试",
|
||||
},
|
||||
page: {
|
||||
title: "新建问题",
|
||||
subtitle: "按向导生成候选,确认后一次性保存到当前品牌的问题集。",
|
||||
title: "新建搜索词",
|
||||
subtitle: "按向导生成候选,确认后一次性保存到当前品牌的关键词库(搜索词)。",
|
||||
steps: {
|
||||
configure: "配置生成方式",
|
||||
preview: "确认候选",
|
||||
@@ -1387,12 +1387,12 @@ const zhCN = {
|
||||
methodTitle: "选择创建方式",
|
||||
methodHint: "候选保存前都是临时内容,你可以先生成、再筛选和编辑。",
|
||||
methods: {
|
||||
combination: "按图示词列批量拓展问题候选。",
|
||||
ai: "让 AI 围绕主题生成问题候选。",
|
||||
batch: "从表格或文档中批量粘贴问题。",
|
||||
combination: "按图示词列批量拓展搜索词候选。",
|
||||
ai: "让 AI 围绕用户搜索词生成候选,不把公司名称写进候选。",
|
||||
batch: "从表格或文档中批量粘贴搜索词。",
|
||||
},
|
||||
expansionHint: "每行一个词;空列会按默认词跳过,核心词和行业词必填。",
|
||||
batchHint: "每行一个问题,保存前可以逐条编辑和勾选。",
|
||||
expansionHint: "前缀词填商业修饰,如性价比高的、口碑好的;精准词填具体需求,如精装、100平;行业词填品类,如全屋定制、家具定制;后缀词填哪家好、怎么选。",
|
||||
batchHint: "每行一个搜索词,保存前可以逐条编辑和勾选。",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -3,6 +3,7 @@ import { QueryClient, VueQueryPlugin } from '@tanstack/vue-query'
|
||||
import {
|
||||
Alert,
|
||||
App as AntApp,
|
||||
AutoComplete,
|
||||
Avatar,
|
||||
Badge,
|
||||
Button,
|
||||
@@ -111,6 +112,7 @@ app.component('ATextarea', Input.TextArea)
|
||||
;[
|
||||
Alert,
|
||||
AntApp,
|
||||
AutoComplete,
|
||||
Avatar,
|
||||
Badge,
|
||||
Button,
|
||||
|
||||
@@ -51,7 +51,9 @@ let aiCandidateCloseTimer: number | null = null
|
||||
const vFocus = {
|
||||
mounted(el: HTMLElement) {
|
||||
const input =
|
||||
el instanceof HTMLInputElement ? el : el.querySelector<HTMLInputElement>('input')
|
||||
el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement
|
||||
? el
|
||||
: el.querySelector<HTMLInputElement | HTMLTextAreaElement>('input, textarea')
|
||||
if (input) {
|
||||
input.focus()
|
||||
const len = input.value.length
|
||||
@@ -333,11 +335,11 @@ const materializeMutation = useMutation({
|
||||
|
||||
function splitLines(value: string | string[]): string[] {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => item.trim()).filter(Boolean)
|
||||
return value.map(normalizeSearchTermText).filter(Boolean)
|
||||
}
|
||||
return value
|
||||
.split(/\r?\n|,/)
|
||||
.map((item) => item.trim())
|
||||
.map(normalizeSearchTermText)
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
@@ -494,30 +496,47 @@ function combinationPartCount(value: string | string[]): number {
|
||||
}
|
||||
|
||||
function normalizeQuestionKey(value: string): string {
|
||||
return value.trim().toLowerCase()
|
||||
return normalizeSearchTermText(value).toLowerCase()
|
||||
}
|
||||
|
||||
function normalizeSearchTermText(value: string): string {
|
||||
return value
|
||||
.trim()
|
||||
.replace(/[\p{P}]/gu, '')
|
||||
.replace(/\s+/g, '')
|
||||
}
|
||||
|
||||
function questionLooksLikeQuestion(value: string): boolean {
|
||||
const text = value.trim()
|
||||
const text = normalizeSearchTermText(value)
|
||||
if (text.length < 4) {
|
||||
return false
|
||||
}
|
||||
return /[??]|什么|如何|怎么|哪|为什么|是否|能不能|适合|区别|对比|推荐/.test(text)
|
||||
return /什么|如何|怎么|哪|为什么|是否|能不能|适合|区别|对比|推荐/.test(text)
|
||||
}
|
||||
|
||||
function toCandidateRows(candidates: QuestionCandidate[]): CandidateRow[] {
|
||||
return candidates.map((candidate, index) => ({
|
||||
...candidate,
|
||||
id: `${Date.now()}-${index}-${candidate.text}`,
|
||||
selected: false,
|
||||
}))
|
||||
return candidates
|
||||
.map((candidate, index) => {
|
||||
const text = normalizeSearchTermText(candidate.text)
|
||||
const tooShort = text.length < 4
|
||||
const invalid = !questionLooksLikeQuestion(text)
|
||||
return {
|
||||
...candidate,
|
||||
text,
|
||||
too_short: candidate.too_short || tooShort,
|
||||
suggest_skip: candidate.suggest_skip || tooShort || invalid,
|
||||
id: `${Date.now()}-${index}-${text}`,
|
||||
selected: false,
|
||||
}
|
||||
})
|
||||
.filter((candidate) => candidate.text)
|
||||
}
|
||||
|
||||
function buildLocalCandidates(texts: string[], source: QuestionSource): CandidateRow[] {
|
||||
const seen = new Set<string>()
|
||||
const rows: CandidateRow[] = []
|
||||
for (const raw of texts) {
|
||||
const text = raw.trim()
|
||||
const text = normalizeSearchTermText(raw)
|
||||
if (!text) {
|
||||
continue
|
||||
}
|
||||
@@ -656,7 +675,7 @@ function removeCandidate(id: string): void {
|
||||
}
|
||||
|
||||
function refreshCandidate(row: CandidateRow): void {
|
||||
row.text = row.text.trim()
|
||||
row.text = normalizeSearchTermText(row.text)
|
||||
const key = normalizeQuestionKey(row.text)
|
||||
const duplicateInBatch = candidateRows.value.some(
|
||||
(item) => item.id !== row.id && normalizeQuestionKey(item.text) === key,
|
||||
@@ -985,10 +1004,12 @@ function backToBrands(): void {
|
||||
:disabled="isCandidateSelectionDisabled(row)"
|
||||
@change="(event: Event) => updateCandidateSelection(row, (event.target as HTMLInputElement).checked)"
|
||||
/>
|
||||
<a-input
|
||||
<a-textarea
|
||||
v-if="editingCandidateId === row.id"
|
||||
v-focus
|
||||
v-model:value="row.text"
|
||||
class="candidate-row__editor"
|
||||
auto-size
|
||||
@blur="stopEditingCandidate(row)"
|
||||
@keydown.enter="stopEditingCandidate(row)"
|
||||
/>
|
||||
@@ -999,13 +1020,13 @@ function backToBrands(): void {
|
||||
>
|
||||
{{ row.text }}
|
||||
</span>
|
||||
<a-tag v-if="row.duplicate" color="orange">
|
||||
<a-tag v-if="editingCandidateId !== row.id && row.duplicate" color="orange">
|
||||
{{ t('brands.questions.badges.duplicate') }}
|
||||
</a-tag>
|
||||
<a-tag v-else-if="row.too_short" color="red">
|
||||
<a-tag v-else-if="editingCandidateId !== row.id && row.too_short" color="red">
|
||||
{{ t('brands.questions.badges.tooShort') }}
|
||||
</a-tag>
|
||||
<div class="candidate-row__actions">
|
||||
<div v-if="editingCandidateId !== row.id" class="candidate-row__actions">
|
||||
<a-button type="text" shape="circle" class="action-btn-edit" @click="startEditingCandidate(row.id)">
|
||||
<EditOutlined />
|
||||
</a-button>
|
||||
@@ -1533,6 +1554,24 @@ function backToBrands(): void {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.candidate-row__editor {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 44px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.candidate-row__editor :deep(.ant-input) {
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.candidate-row__actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
|
||||
@@ -286,6 +286,12 @@ const templateMeta = computed(() => {
|
||||
const selectedBrand = computed(
|
||||
() => brandsQuery.data.value?.find((brand) => brand.id === selectedBrandId.value) ?? null,
|
||||
)
|
||||
const brandNameOptions = computed(() =>
|
||||
(brandsQuery.data.value ?? []).map((item) => ({
|
||||
label: item.name,
|
||||
value: item.name,
|
||||
})),
|
||||
)
|
||||
|
||||
function findQuestionById(id: number | null): Question | null {
|
||||
if (!id) {
|
||||
@@ -654,6 +660,23 @@ watch(supplementalQuestionIds, (ids) => {
|
||||
}
|
||||
})
|
||||
|
||||
function handleBrandNameInput(value: string): void {
|
||||
brandName.value = value
|
||||
const matchedBrand = brandsQuery.data.value?.find((item) => item.name === value)
|
||||
selectedBrandId.value = matchedBrand?.id ?? null
|
||||
}
|
||||
|
||||
function handleBrandNameSelect(value: string): void {
|
||||
const brand = brandsQuery.data.value?.find((item) => item.name === value)
|
||||
if (!brand) {
|
||||
selectedBrandId.value = null
|
||||
brandName.value = value
|
||||
return
|
||||
}
|
||||
selectedBrandId.value = brand.id
|
||||
brandName.value = brand.name
|
||||
}
|
||||
|
||||
watch(selectedBrandId, async (brandId) => {
|
||||
if (restoringDraft.value) {
|
||||
return
|
||||
@@ -1192,8 +1215,9 @@ function sanitizeLegacyKeywordCopy(copy: TemplateCardCopy): TemplateCardCopy {
|
||||
|
||||
function sanitizeLegacyKeywordText(value: string | undefined): string {
|
||||
return (value || '')
|
||||
.replace(/核心关键词/g, '品牌主问题')
|
||||
.replace(/关键词/g, '品牌问题')
|
||||
.replace(/品牌主问题/g, '优化关键词')
|
||||
.replace(/补充覆盖问题/g, '补充关键词')
|
||||
.replace(/核心关键词/g, '优化关键词')
|
||||
.replace(/标题生成/g, '后续生成')
|
||||
}
|
||||
|
||||
@@ -2392,29 +2416,17 @@ function onStructureDragEnd(): void {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field-item">
|
||||
<label>{{ t('templates.wizard.sections.brandLibrary') }}</label>
|
||||
<a-select
|
||||
v-model:value="selectedBrandId"
|
||||
allow-clear
|
||||
show-search
|
||||
:placeholder="t('templates.wizard.placeholders.brandLibrary')"
|
||||
:options="
|
||||
(brandsQuery.data.value || []).map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
}))
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field-item">
|
||||
<label class="required-asterisk">
|
||||
{{ t('templates.wizard.sections.brandName') }}
|
||||
</label>
|
||||
<a-input
|
||||
<a-auto-complete
|
||||
v-model:value="brandName"
|
||||
allow-clear
|
||||
:options="brandNameOptions"
|
||||
:placeholder="t('templates.wizard.placeholders.brandName')"
|
||||
@change="handleBrandNameInput"
|
||||
@select="handleBrandNameSelect"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -862,7 +862,7 @@ function disableTrackingDate(current: dayjs.Dayjs): boolean {
|
||||
/>
|
||||
</div>
|
||||
<div class="tracking-action-item">
|
||||
<span class="tracking-action-label">问题集</span>
|
||||
<span class="tracking-action-label">关键词库(搜索词)</span>
|
||||
<a-select
|
||||
v-model:value="selectedQuestionSetOptionValue"
|
||||
show-search
|
||||
|
||||
Reference in New Issue
Block a user