style: format web apps with prettier and sort imports

Apply repo-wide Prettier/lint normalization across admin-web,
desktop-client and ops-web: single quotes, no semicolons, trailing
commas, consistent line wrapping, and import ordering. Also drop an
unused brand-logo import in DesktopShell.vue.

No behavior changes — formatting only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 11:56:18 +08:00
parent a44ed21967
commit aa96143754
81 changed files with 2929 additions and 2252 deletions
@@ -247,10 +247,7 @@ function getAIPointAmountMeta(item: AIPointUsageLog): {
</template>
<template v-else-if="column.key === 'points'">
<div
class="points-cell"
:class="`points-cell--${getAIPointAmountMeta(record).tone}`"
>
<div class="points-cell" :class="`points-cell--${getAIPointAmountMeta(record).tone}`">
<strong>{{ getAIPointAmountMeta(record).primary }}</strong>
<span>{{ getAIPointAmountMeta(record).detail }}</span>
</div>
+17 -6
View File
@@ -55,7 +55,10 @@ const exportLoading = ref<ArticleExportFormat | null>(null)
const detailQuery = useQuery({
queryKey: computed(() => ['articles', 'detail', companyStore.currentBrandId, articleId.value]),
enabled: computed(
() => Boolean(companyStore.currentBrandId) && Number.isInteger(articleId.value) && articleId.value > 0,
() =>
Boolean(companyStore.currentBrandId) &&
Number.isInteger(articleId.value) &&
articleId.value > 0,
),
queryFn: () => articlesApi.detail(articleId.value),
})
@@ -125,7 +128,9 @@ const complianceCheckLoading = ref(false)
const editorComplianceViolations = computed(() =>
compliancePanelOpen.value ? (editorComplianceResult.value?.violations ?? []) : [],
)
const complianceGroups = computed(() => groupViolations(editorComplianceResult.value?.violations ?? []))
const complianceGroups = computed(() =>
groupViolations(editorComplianceResult.value?.violations ?? []),
)
const complianceElapsedLabel = computed(() => {
const duration = editorComplianceResult.value?.duration_ms ?? 0
return duration > 0 ? `${duration} ms` : '< 1 ms'
@@ -527,15 +532,21 @@ onBeforeUnmount(() => {
<template #overlay>
<a-menu class="article-editor-view__download-menu">
<a-menu-item key="word" @click="handleDownload('word')">
<FileWordOutlined class="article-editor-view__download-icon article-editor-view__download-icon--word" />
<FileWordOutlined
class="article-editor-view__download-icon article-editor-view__download-icon--word"
/>
<span>{{ t('article.editor.downloadFormats.word') }}</span>
</a-menu-item>
<a-menu-item key="pdf" @click="handleDownload('pdf')">
<FilePdfOutlined class="article-editor-view__download-icon article-editor-view__download-icon--pdf" />
<FilePdfOutlined
class="article-editor-view__download-icon article-editor-view__download-icon--pdf"
/>
<span>{{ t('article.editor.downloadFormats.pdf') }}</span>
</a-menu-item>
<a-menu-item key="markdown" @click="handleDownload('markdown')">
<FileMarkdownOutlined class="article-editor-view__download-icon article-editor-view__download-icon--markdown" />
<FileMarkdownOutlined
class="article-editor-view__download-icon article-editor-view__download-icon--markdown"
/>
<span>{{ t('article.editor.downloadFormats.markdown') }}</span>
</a-menu-item>
</a-menu>
@@ -656,7 +667,7 @@ onBeforeUnmount(() => {
</div>
<p v-if="violation.dictionary_name || violation.term_pattern">
{{ violation.dictionary_name || '词库规则' }}
<span v-if="violation.term_pattern"> · {{ violation.term_pattern }}</span>
<span v-if="violation.term_pattern">· {{ violation.term_pattern }}</span>
</p>
<p v-if="violation.hint">{{ violation.hint }}</p>
<div class="article-editor-view__violation-actions">
@@ -8,12 +8,7 @@ import {
TagsOutlined,
} from '@ant-design/icons-vue'
import { ApiClientError } from '@geo/http-client'
import type {
Brand,
BrandLibrarySummary,
BrandRequest,
QuestionCandidate,
} from '@geo/shared-types'
import type { Brand, BrandLibrarySummary, BrandRequest, QuestionCandidate } from '@geo/shared-types'
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
import { message } from 'ant-design-vue'
import { computed, onBeforeUnmount, reactive, ref } from 'vue'
@@ -226,9 +221,7 @@ function applyDefaultSelection(rows: CandidateRow[]): void {
}
let selected = 0
const preferredRows = rows.filter(
(row) => !row.suggest_skip && !row.too_short && !row.duplicate,
)
const preferredRows = rows.filter((row) => !row.suggest_skip && !row.too_short && !row.duplicate)
const orderedRows = [...preferredRows, ...rows.filter((row) => !preferredRows.includes(row))]
for (const row of orderedRows) {
@@ -585,12 +578,16 @@ async function saveSelectedQuestions(): Promise<void> {
padding: 40px 24px;
color: #0f172a;
background-color: #f8fafc;
background-image:
background-image:
radial-gradient(circle at 10% 20%, rgba(22, 119, 255, 0.05) 0%, transparent 40%),
radial-gradient(circle at 90% 80%, rgba(99, 102, 241, 0.06) 0%, transparent 50%),
radial-gradient(circle at 50% 0%, rgba(6, 182, 212, 0.04) 0%, transparent 35%),
radial-gradient(rgba(148, 163, 184, 0.1) 1px, transparent 1px);
background-size: 100% 100%, 100% 100%, 100% 100%, 24px 24px;
background-size:
100% 100%,
100% 100%,
100% 100%,
24px 24px;
}
.glow-orb {
@@ -641,7 +638,7 @@ async function saveSelectedQuestions(): Promise<void> {
-webkit-backdrop-filter: blur(24px);
border: 1px solid rgba(255, 255, 255, 0.7);
border-radius: 24px;
box-shadow:
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.02),
0 4px 12px rgba(0, 0, 0, 0.02),
0 16px 36px rgba(22, 119, 255, 0.04),
@@ -738,7 +735,7 @@ async function saveSelectedQuestions(): Promise<void> {
.brand-choice:hover {
background: #ffffff;
border-color: rgba(22, 119, 255, 0.4);
box-shadow:
box-shadow:
0 10px 25px -5px rgba(22, 119, 255, 0.08),
0 4px 12px -5px rgba(22, 119, 255, 0.05);
transform: translateY(-2px);
@@ -862,7 +859,7 @@ async function saveSelectedQuestions(): Promise<void> {
.create-brand-card:hover {
background: rgba(255, 255, 255, 0.9);
border-color: #1677ff;
box-shadow:
box-shadow:
0 12px 30px -5px rgba(22, 119, 255, 0.08),
0 4px 12px -5px rgba(22, 119, 255, 0.04);
transform: translateY(-2px);
@@ -8,11 +8,7 @@ import {
ThunderboltOutlined,
} from '@ant-design/icons-vue'
import { ApiClientError } from '@geo/http-client'
import type {
BrandLibrarySummary,
QuestionCandidate,
QuestionSource,
} from '@geo/shared-types'
import type { BrandLibrarySummary, QuestionCandidate, QuestionSource } from '@geo/shared-types'
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
import { message } from 'ant-design-vue'
import { computed, onBeforeUnmount, reactive, ref } from 'vue'
@@ -896,7 +892,9 @@ function backToBrands(): void {
{{ t('brands.questions.aiFill.button') }}
</a-button>
<div class="tool-estimate">
{{ t('brands.questions.combination.estimate', { count: expansionToolPreviewCount }) }}
{{
t('brands.questions.combination.estimate', { count: expansionToolPreviewCount })
}}
</div>
</div>
</div>
@@ -905,10 +903,19 @@ function backToBrands(): void {
<div class="word-column" data-col="region">
<div class="word-column__head">
<strong>{{ t('brands.questions.combination.region') }}</strong>
<span>{{ splitLines(expansionForm.region).length || t('brands.questions.combination.defaultWord') }}</span>
<span>
{{
splitLines(expansionForm.region).length ||
t('brands.questions.combination.defaultWord')
}}
</span>
</div>
<div class="word-column__list">
<div v-for="(item, index) in expansionForm.region" :key="index" class="word-column__item">
<div
v-for="(item, index) in expansionForm.region"
:key="index"
class="word-column__item"
>
<span class="word-column__dot"></span>
<input
v-model="expansionForm.region[index]"
@@ -919,17 +926,33 @@ function backToBrands(): void {
@keydown.backspace="handleBackspace('region', index, $event)"
@blur="handleBlur('region', index)"
/>
<button v-if="index < expansionForm.region.length - 1" class="word-column__remove" tabindex="-1" @click="removeItem('region', index)">×</button>
<button
v-if="index < expansionForm.region.length - 1"
class="word-column__remove"
tabindex="-1"
@click="removeItem('region', index)"
>
×
</button>
</div>
</div>
</div>
<div class="word-column" data-col="prefix">
<div class="word-column__head">
<strong>{{ t('brands.questions.combination.prefix') }}</strong>
<span>{{ splitLines(expansionForm.prefix).length || t('brands.questions.combination.defaultWord') }}</span>
<span>
{{
splitLines(expansionForm.prefix).length ||
t('brands.questions.combination.defaultWord')
}}
</span>
</div>
<div class="word-column__list">
<div v-for="(item, index) in expansionForm.prefix" :key="index" class="word-column__item">
<div
v-for="(item, index) in expansionForm.prefix"
:key="index"
class="word-column__item"
>
<span class="word-column__dot"></span>
<input
v-model="expansionForm.prefix[index]"
@@ -940,7 +963,14 @@ function backToBrands(): void {
@keydown.backspace="handleBackspace('prefix', index, $event)"
@blur="handleBlur('prefix', index)"
/>
<button v-if="index < expansionForm.prefix.length - 1" class="word-column__remove" tabindex="-1" @click="removeItem('prefix', index)">×</button>
<button
v-if="index < expansionForm.prefix.length - 1"
class="word-column__remove"
tabindex="-1"
@click="removeItem('prefix', index)"
>
×
</button>
</div>
</div>
</div>
@@ -950,7 +980,11 @@ function backToBrands(): void {
<span>{{ splitLines(expansionForm.core).length }}</span>
</div>
<div class="word-column__list">
<div v-for="(item, index) in expansionForm.core" :key="index" class="word-column__item">
<div
v-for="(item, index) in expansionForm.core"
:key="index"
class="word-column__item"
>
<span class="word-column__dot"></span>
<input
v-model="expansionForm.core[index]"
@@ -961,7 +995,14 @@ function backToBrands(): void {
@keydown.backspace="handleBackspace('core', index, $event)"
@blur="handleBlur('core', index)"
/>
<button v-if="index < expansionForm.core.length - 1" class="word-column__remove" tabindex="-1" @click="removeItem('core', index)">×</button>
<button
v-if="index < expansionForm.core.length - 1"
class="word-column__remove"
tabindex="-1"
@click="removeItem('core', index)"
>
×
</button>
</div>
</div>
</div>
@@ -971,7 +1012,11 @@ function backToBrands(): void {
<span>{{ splitLines(expansionForm.industry).length }}</span>
</div>
<div class="word-column__list">
<div v-for="(item, index) in expansionForm.industry" :key="index" class="word-column__item">
<div
v-for="(item, index) in expansionForm.industry"
:key="index"
class="word-column__item"
>
<span class="word-column__dot"></span>
<input
v-model="expansionForm.industry[index]"
@@ -982,17 +1027,33 @@ function backToBrands(): void {
@keydown.backspace="handleBackspace('industry', index, $event)"
@blur="handleBlur('industry', index)"
/>
<button v-if="index < expansionForm.industry.length - 1" class="word-column__remove" tabindex="-1" @click="removeItem('industry', index)">×</button>
<button
v-if="index < expansionForm.industry.length - 1"
class="word-column__remove"
tabindex="-1"
@click="removeItem('industry', index)"
>
×
</button>
</div>
</div>
</div>
<div class="word-column" data-col="suffix">
<div class="word-column__head">
<strong>{{ t('brands.questions.combination.suffix') }}</strong>
<span>{{ splitLines(expansionForm.suffix).length || t('brands.questions.combination.defaultWord') }}</span>
<span>
{{
splitLines(expansionForm.suffix).length ||
t('brands.questions.combination.defaultWord')
}}
</span>
</div>
<div class="word-column__list">
<div v-for="(item, index) in expansionForm.suffix" :key="index" class="word-column__item">
<div
v-for="(item, index) in expansionForm.suffix"
:key="index"
class="word-column__item"
>
<span class="word-column__dot"></span>
<input
v-model="expansionForm.suffix[index]"
@@ -1003,7 +1064,14 @@ function backToBrands(): void {
@keydown.backspace="handleBackspace('suffix', index, $event)"
@blur="handleBlur('suffix', index)"
/>
<button v-if="index < expansionForm.suffix.length - 1" class="word-column__remove" tabindex="-1" @click="removeItem('suffix', index)">×</button>
<button
v-if="index < expansionForm.suffix.length - 1"
class="word-column__remove"
tabindex="-1"
@click="removeItem('suffix', index)"
>
×
</button>
</div>
</div>
</div>
@@ -1068,15 +1136,14 @@ function backToBrands(): void {
<div v-if="!candidateRows.length" class="candidate-empty">
{{ t('brands.questions.emptyCandidates') }}
</div>
<div
v-for="row in candidateRows"
:key="row.id"
class="candidate-row"
>
<div v-for="row in candidateRows" :key="row.id" class="candidate-row">
<a-checkbox
:checked="row.selected"
:disabled="isCandidateSelectionDisabled(row)"
@change="(event: Event) => updateCandidateSelection(row, (event.target as HTMLInputElement).checked)"
@change="
(event: Event) =>
updateCandidateSelection(row, (event.target as HTMLInputElement).checked)
"
/>
<a-textarea
v-if="editingCandidateId === row.id"
@@ -1087,11 +1154,7 @@ function backToBrands(): void {
@blur="stopEditingCandidate(row)"
@keydown.enter="stopEditingCandidate(row)"
/>
<span
v-else
class="candidate-row__text"
@dblclick="startEditingCandidate(row.id)"
>
<span v-else class="candidate-row__text" @dblclick="startEditingCandidate(row.id)">
{{ row.text }}
</span>
<a-tag v-if="editingCandidateId !== row.id && row.duplicate" color="orange">
@@ -1101,7 +1164,12 @@ function backToBrands(): void {
{{ t('brands.questions.badges.tooShort') }}
</a-tag>
<div v-if="editingCandidateId !== row.id" class="candidate-row__actions">
<a-button type="text" shape="circle" class="action-btn-edit" @click="startEditingCandidate(row.id)">
<a-button
type="text"
shape="circle"
class="action-btn-edit"
@click="startEditingCandidate(row.id)"
>
<EditOutlined />
</a-button>
<a-button type="text" shape="circle" danger @click="removeCandidate(row.id)">
@@ -1429,8 +1497,6 @@ function backToBrands(): void {
font-size: 16px;
}
.tool-estimate {
padding: 8px 12px;
color: #475569;
@@ -1460,7 +1526,9 @@ function backToBrands(): void {
.word-column:focus-within {
background: #ffffff;
border-color: #1677ff;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05), 0 0 0 2px rgba(22, 119, 255, 0.1);
box-shadow:
0 4px 12px rgba(0, 0, 0, 0.05),
0 0 0 2px rgba(22, 119, 255, 0.1);
}
.word-column__head {
@@ -1573,7 +1641,9 @@ function backToBrands(): void {
font-size: 16px;
cursor: pointer;
padding: 0 4px;
transition: opacity 0.2s, color 0.2s;
transition:
opacity 0.2s,
color 0.2s;
display: flex;
align-items: center;
justify-content: center;
+44 -14
View File
@@ -6,12 +6,7 @@ import {
ThunderboltOutlined,
} from '@ant-design/icons-vue'
import { ApiClientError } from '@geo/http-client'
import type {
Brand,
BrandLibrarySummary,
Competitor,
Question,
} from '@geo/shared-types'
import type { Brand, BrandLibrarySummary, Competitor, Question } from '@geo/shared-types'
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
import type { TableColumnsType } from 'ant-design-vue'
import { message } from 'ant-design-vue'
@@ -288,7 +283,11 @@ watch(
selectedBrandId.value = brands[0].id
return
}
if (selectedBrandId.value && brands && !brands.some((item) => item.id === selectedBrandId.value)) {
if (
selectedBrandId.value &&
brands &&
!brands.some((item) => item.id === selectedBrandId.value)
) {
selectedBrandId.value = brands[0]?.id ?? null
}
},
@@ -464,7 +463,13 @@ async function invalidateBrandQueries(): Promise<void> {
</div>
<div class="brand-card__actions">
<a-tooltip :title="t('common.edit')">
<a-button type="text" shape="circle" size="small" class="action-btn-edit" @click.stop="openBrandModal(brand)">
<a-button
type="text"
shape="circle"
size="small"
class="action-btn-edit"
@click.stop="openBrandModal(brand)"
>
<EditOutlined />
</a-button>
</a-tooltip>
@@ -478,7 +483,9 @@ async function invalidateBrandQueries(): Promise<void> {
</div>
<div class="brand-card__meta">
<span>{{ t('brands.meta.questionCount', { count: brand.question_count }) }}</span>
<span>{{ t('brands.meta.competitorCount', { count: brand.competitor_count ?? 0 }) }}</span>
<span>
{{ t('brands.meta.competitorCount', { count: brand.competitor_count ?? 0 }) }}
</span>
</div>
</article>
<button type="button" class="brand-card brand-card--add" @click="openBrandModal()">
@@ -498,7 +505,11 @@ async function invalidateBrandQueries(): Promise<void> {
<div class="brand-detail__head">
<div>
<p class="eyebrow">{{ selectedBrand?.name }}</p>
<h3>{{ activeTab === 'questions' ? t('brands.tabs.questions') : t('brands.tabs.competitors') }}</h3>
<h3>
{{
activeTab === 'questions' ? t('brands.tabs.questions') : t('brands.tabs.competitors')
}}
</h3>
</div>
<div v-if="activeTab === 'questions'" class="brand-detail__actions">
<a-button :disabled="!selectedBrand" @click="openQuestionExpansionPage">
@@ -538,7 +549,13 @@ async function invalidateBrandQueries(): Promise<void> {
<template v-else-if="column.key === 'actions'">
<div class="table-actions-row">
<a-tooltip :title="t('common.edit')">
<a-button type="text" shape="circle" size="small" class="action-btn-edit" @click="openQuestionEditModal(record)">
<a-button
type="text"
shape="circle"
size="small"
class="action-btn-edit"
@click="openQuestionEditModal(record)"
>
<EditOutlined />
</a-button>
</a-tooltip>
@@ -556,7 +573,11 @@ async function invalidateBrandQueries(): Promise<void> {
<a-tab-pane key="competitors" :tab="t('brands.tabs.competitors')">
<div v-if="competitorsQuery.data.value?.length" class="competitor-grid">
<article v-for="competitor in competitorsQuery.data.value" :key="competitor.id" class="competitor-card">
<article
v-for="competitor in competitorsQuery.data.value"
:key="competitor.id"
class="competitor-card"
>
<div class="competitor-card__head">
<div>
<h4>{{ competitor.name }}</h4>
@@ -565,7 +586,13 @@ async function invalidateBrandQueries(): Promise<void> {
</a>
</div>
<div class="inline-actions">
<a-button type="text" shape="circle" size="small" class="action-btn-edit" @click="openCompetitorModal(competitor)">
<a-button
type="text"
shape="circle"
size="small"
class="action-btn-edit"
@click="openCompetitorModal(competitor)"
>
<EditOutlined />
</a-button>
<a-popconfirm @confirm="competitorMutations.remove.mutate(competitor.id)">
@@ -911,5 +938,8 @@ async function invalidateBrandQueries(): Promise<void> {
border-left: 0;
}
}
.action-btn-edit:hover { color: #52c41a !important; background: #f6ffed !important; }
.action-btn-edit:hover {
color: #52c41a !important;
background: #f6ffed !important;
}
</style>
+11 -3
View File
@@ -19,7 +19,11 @@ import { articlesApi } from '@/lib/api'
import { buildArticleClipboardContent, resolveArticleActionState } from '@/lib/article-list-actions'
import { useArticleRegenerateAction } from '@/lib/article-regenerate-action'
import { copyTextToClipboard } from '@/lib/clipboard'
import { getGenerateStatusMeta, getPublishStatusMeta, hasActiveGenerationStatus } from '@/lib/display'
import {
getGenerateStatusMeta,
getPublishStatusMeta,
hasActiveGenerationStatus,
} from '@/lib/display'
import { formatError } from '@/lib/errors'
import { useCompanyStore } from '@/stores/company'
@@ -432,9 +436,13 @@ async function copyArticle(articleId: number): Promise<void> {
<template v-else-if="column.key === 'generate_status'">
<div class="imitation-view__status-cell">
<ArticleGenerateStatus :status="record.generate_status" />
<a-tooltip v-if="record.generate_status === 'failed' && record.generation_error_message">
<a-tooltip
v-if="record.generate_status === 'failed' && record.generation_error_message"
>
<template #title>{{ record.generation_error_message }}</template>
<span class="imitation-view__error-text">{{ record.generation_error_message }}</span>
<span class="imitation-view__error-text">
{{ record.generation_error_message }}
</span>
</a-tooltip>
</div>
</template>
+10 -2
View File
@@ -7,7 +7,11 @@
}"
>
<div class="brand">
<img class="brand-logo brand-logo--white" src="/logos/icon-with-title.svg" :alt="t('app.name')" />
<img
class="brand-logo brand-logo--white"
src="/logos/icon-with-title.svg"
:alt="t('app.name')"
/>
</div>
<div class="characters-area">
<AnimatedCharacters
@@ -28,7 +32,11 @@
<div class="right">
<div class="form-wrapper">
<div class="mobile-brand">
<img class="brand-logo brand-logo--mobile" src="/logos/icon-with-title.svg" :alt="t('app.name')" />
<img
class="brand-logo brand-logo--mobile"
src="/logos/icon-with-title.svg"
:alt="t('app.name')"
/>
</div>
<div class="header">
<h1>{{ t('auth.welcomeBack') }}</h1>
@@ -879,11 +879,7 @@ function toResourceRow(item: CustomerSupplierMediaResource): MediaSupplyResource
</a-table>
</a-drawer>
<a-modal
v-model:open="favoriteModalOpen"
title="加入常发分组"
width="440px"
>
<a-modal v-model:open="favoriteModalOpen" title="加入常发分组" width="440px">
<div class="favorite-modal">
<div class="favorite-modal__resource">
<div class="favorite-modal__resource-header">
@@ -901,7 +897,13 @@ function toResourceRow(item: CustomerSupplierMediaResource): MediaSupplyResource
<span class="resource-tag" v-if="pendingFavoriteResource.region">
{{ pendingFavoriteResource.region }}
</span>
<span class="resource-tag resource-tag--weight" v-if="pendingFavoriteResource.baidu_weight !== null && pendingFavoriteResource.baidu_weight !== undefined">
<span
class="resource-tag resource-tag--weight"
v-if="
pendingFavoriteResource.baidu_weight !== null &&
pendingFavoriteResource.baidu_weight !== undefined
"
>
百度 {{ pendingFavoriteResource.baidu_weight }}
</span>
</div>
@@ -912,7 +914,11 @@ function toResourceRow(item: CustomerSupplierMediaResource): MediaSupplyResource
<div class="favorite-modal__form-item">
<span class="favorite-modal__form-label">选择已有分组</span>
<a-radio-group v-model:value="selectedFavoriteGroupId" class="favorite-modal__groups">
<a-radio-button v-for="group in favoriteGroupOptions" :key="group.id" :value="group.id">
<a-radio-button
v-for="group in favoriteGroupOptions"
:key="group.id"
:value="group.id"
>
{{ group.name }}
</a-radio-button>
</a-radio-group>
@@ -929,14 +935,16 @@ function toResourceRow(item: CustomerSupplierMediaResource): MediaSupplyResource
placeholder="输入新分组名称"
@press-enter="confirmAddFavorite"
>
<template #prefix><PlusOutlined style="color: #98a2b3;" /></template>
<template #prefix><PlusOutlined style="color: #98a2b3" /></template>
</a-input>
</div>
</div>
</div>
<template #footer>
<div class="favorite-modal-footer">
<button type="button" class="modal-btn-cancel" @click="favoriteModalOpen = false">取消</button>
<button type="button" class="modal-btn-cancel" @click="favoriteModalOpen = false">
取消
</button>
<button type="button" class="modal-btn-ok" @click="confirmAddFavorite">确认加入</button>
</div>
</template>
@@ -1271,7 +1279,6 @@ div.favorite-modal :deep(.ant-input::placeholder) {
transform: translateY(0);
}
.confirm-btn,
.danger-action {
gap: 4px;
@@ -22,17 +22,13 @@ import { useRouter } from 'vue-router'
import ArticleEditorCanvas from '@/components/ArticleEditorCanvas.vue'
import { articlesApi, imagesApi, mediaSupplyApi } from '@/lib/api'
import { formatDateTime, getGenerateStatusMeta, getSourceTypeLabel } from '@/lib/display'
import { formatError } from '@/lib/errors'
import {
clearMediaSupplySubmitSelection,
loadMediaSupplySubmitSelection,
type MediaSupplySubmitResourceSnapshot,
} from '@/lib/media-supply-submit-selection'
import {
formatDateTime,
getGenerateStatusMeta,
getSourceTypeLabel,
} from '@/lib/display'
import { formatError } from '@/lib/errors'
import { useCompanyStore } from '@/stores/company'
const MODEL_ID_AUTHORITY_NEWS = 1
@@ -225,7 +221,9 @@ function displayValue(value?: string | null): string {
return value?.trim() || '--'
}
function sourceLabel(article: Pick<ArticleDetail | ArticleListItem, 'source_type' | 'generation_mode'>): string {
function sourceLabel(
article: Pick<ArticleDetail | ArticleListItem, 'source_type' | 'generation_mode'>,
): string {
return getSourceTypeLabel(article.source_type, article.generation_mode)
}
@@ -292,7 +290,9 @@ function stripLeadingTitleHeading(titleValue: string, markdownValue: string): st
<article v-for="resource in resources" :key="resource.id" class="media-row">
<div>
<strong>{{ resource.name }}</strong>
<span>{{ displayValue(resource.channel_type) }} · {{ displayValue(resource.region) }}</span>
<span>
{{ displayValue(resource.channel_type) }} · {{ displayValue(resource.region) }}
</span>
</div>
<em>{{ formatSellPrice(resource.sell_price_cents) }}</em>
<p v-if="resource.resource_remark">{{ resource.resource_remark }}</p>
@@ -335,7 +335,11 @@ function stripLeadingTitleHeading(titleValue: string, markdownValue: string): st
>
<template #prefix><SearchOutlined /></template>
</a-input>
<a-select v-model:value="articleSourceType" :options="sourceOptions" @change="applyArticleSearch" />
<a-select
v-model:value="articleSourceType"
:options="sourceOptions"
@change="applyArticleSearch"
/>
</div>
<a-spin :spinning="articlesQuery.isPending.value || selectedArticleLoading">
<div v-if="articleRows.length" class="article-list">
@@ -386,14 +390,15 @@ function stripLeadingTitleHeading(titleValue: string, markdownValue: string): st
/>
</div>
</section>
</main>
</div>
<footer class="submit-footer">
<div class="footer-balance" :class="{ 'footer-balance--danger': !balanceEnough }">
<WalletOutlined />
<span>合计 {{ formatSellPrice(selectedTotal) }}余额 {{ formatMoney(walletBalance) }}</span>
<span>
合计 {{ formatSellPrice(selectedTotal) }}余额 {{ formatMoney(walletBalance) }}
</span>
</div>
<div class="footer-actions">
<a-button @click="cancelSubmit">取消</a-button>
+17 -10
View File
@@ -24,7 +24,7 @@ function goHome(): void {
<section class="not-found-page" role="alert" aria-labelledby="not-found-title">
<div class="not-found-card">
<div class="ambient-glow" aria-hidden="true"></div>
<div class="error-code" aria-hidden="true">404</div>
<h1 id="not-found-title" class="not-found-title">{{ t('notFound.title') }}</h1>
<p class="not-found-description">{{ t('notFound.description') }}</p>
@@ -67,7 +67,7 @@ function goHome(): void {
background-size: 24px 24px;
background-position: center top;
border: 1px solid rgba(235, 240, 245, 0.8);
box-shadow:
box-shadow:
0 24px 48px -12px rgba(19, 94, 204, 0.05),
0 0 0 1px rgba(255, 255, 255, 0.8) inset;
text-align: center;
@@ -83,7 +83,7 @@ function goHome(): void {
transform: translate(-50%, -50%);
width: 400px;
height: 400px;
background: radial-gradient(circle, rgba(47, 124, 246, 0.08) 0%, rgba(255,255,255,0) 70%);
background: radial-gradient(circle, rgba(47, 124, 246, 0.08) 0%, rgba(255, 255, 255, 0) 70%);
pointer-events: none;
z-index: 0;
border-radius: 50%;
@@ -108,9 +108,15 @@ function goHome(): void {
}
@keyframes float {
0% { transform: translateY(0px); }
50% { transform: translateY(-10px); }
100% { transform: translateY(0px); }
0% {
transform: translateY(0px);
}
50% {
transform: translateY(-10px);
}
100% {
transform: translateY(0px);
}
}
.not-found-title {
@@ -153,8 +159,7 @@ function goHome(): void {
}
.not-found-path code {
font-family:
'SFMono-Regular', Menlo, Consolas, 'Liberation Mono', 'Courier New', monospace;
font-family: 'SFMono-Regular', Menlo, Consolas, 'Liberation Mono', 'Courier New', monospace;
font-size: 13px;
color: #334155;
word-break: break-all;
@@ -172,7 +177,8 @@ function goHome(): void {
gap: 16px;
}
.btn-primary, .btn-secondary {
.btn-primary,
.btn-secondary {
display: inline-flex;
align-items: center;
gap: 8px;
@@ -248,7 +254,8 @@ function goHome(): void {
.not-found-actions {
flex-direction: column;
}
.btn-primary, .btn-secondary {
.btn-primary,
.btn-secondary {
width: 100%;
justify-content: center;
}
@@ -80,10 +80,15 @@ function isPendingStatus(status: string): boolean {
}
function hasActivePublishingRecord(page: PublishRecordListResponse | undefined): boolean {
return Boolean(page?.items.some((record) => isPendingStatus(normalizeRecordStatus(record.status))))
return Boolean(
page?.items.some((record) => isPendingStatus(normalizeRecordStatus(record.status))),
)
}
function normalizeRecordErrorMessage(messageText: string | null, platform?: string | null): string | null {
function normalizeRecordErrorMessage(
messageText: string | null,
platform?: string | null,
): string | null {
const normalized = extractString(messageText)
if (!normalized) {
return null
@@ -222,8 +227,7 @@ const publishRecords = computed<PublishRecordItem[]>(() =>
const platformMeta = getPublishPlatformMeta(platformId)
const isEnterpriseSite = record.target_type === 'enterprise_site'
const accountName =
extractString(record.platform_nickname) ??
(isEnterpriseSite ? '企业自建站点' : '待同步账号')
extractString(record.platform_nickname) ?? (isEnterpriseSite ? '企业自建站点' : '待同步账号')
const statusMeta = statusMetaForRecord(status)
const externalUrl =
extractString(record.external_article_url) ?? extractString(record.external_manage_url)
@@ -538,7 +542,11 @@ async function copyErrorMessage(record: PublishRecordItem): Promise<void> {
</a-button>
</span>
</a-tooltip>
<a-tooltip v-if="record.canOpenWorkbench" :title="`打开 ${record.accountName} 的${record.platformName}工作台`" placement="top">
<a-tooltip
v-if="record.canOpenWorkbench"
:title="`打开 ${record.accountName} 的${record.platformName}工作台`"
placement="top"
>
<a-button
type="text"
class="publish-action-btn"
+64 -57
View File
@@ -409,17 +409,17 @@ const structureStepCopy = computed<TemplateStepConfig>(
const generateStepCopy = computed<TemplateStepConfig>(
() => wizardConfig.value?.steps?.generate ?? {},
)
const brandCardCopy = computed<TemplateCardCopy>(
() => sanitizeLegacyKeywordCopy(wizardConfig.value?.basic?.cards?.brand ?? {}),
const brandCardCopy = computed<TemplateCardCopy>(() =>
sanitizeLegacyKeywordCopy(wizardConfig.value?.basic?.cards?.brand ?? {}),
)
const questionCardCopy = computed<TemplateCardCopy>(
() => sanitizeQuestionCardCopy(wizardConfig.value?.basic?.cards?.keywords ?? {}),
const questionCardCopy = computed<TemplateCardCopy>(() =>
sanitizeQuestionCardCopy(wizardConfig.value?.basic?.cards?.keywords ?? {}),
)
const competitorsCardCopy = computed<TemplateCardCopy>(
() => wizardConfig.value?.basic?.cards?.competitors ?? {},
)
const titleCardCopy = computed<TemplateCardCopy>(
() => sanitizeLegacyKeywordCopy(wizardConfig.value?.structure?.cards?.choose_title ?? {}),
const titleCardCopy = computed<TemplateCardCopy>(() =>
sanitizeLegacyKeywordCopy(wizardConfig.value?.structure?.cards?.choose_title ?? {}),
)
const outlineCardCopy = computed<TemplateCardCopy>(
() => wizardConfig.value?.structure?.cards?.outline ?? {},
@@ -427,9 +427,7 @@ const outlineCardCopy = computed<TemplateCardCopy>(
const previewCardCopy = computed<TemplateCardCopy>(
() => wizardConfig.value?.structure?.cards?.preview ?? {},
)
const analyzeButtonLabel = computed(
() => t('templates.wizard.actions.analyzeCompetitors'),
)
const analyzeButtonLabel = computed(() => t('templates.wizard.actions.analyzeCompetitors'))
const showBrandSummaryBox = computed(() => Boolean(selectedBrandId.value))
const reviewAlertMessage = computed(
() => wizardConfig.value?.review?.alert_message || t('templates.wizard.hints.async'),
@@ -509,9 +507,9 @@ function resetWizardState(detail: NonNullable<typeof templateDetail.value>): voi
detail.template_key,
buildTemplateRenderContext({
locale: 'zh-CN',
brandName: normalizedBrandName.value,
officialWebsite: officialWebsite.value,
brandSummary: brandSummary.value,
brandName: normalizedBrandName.value,
officialWebsite: officialWebsite.value,
brandSummary: brandSummary.value,
primaryQuestion: '',
supplementalQuestions: [],
inputParams: {},
@@ -525,9 +523,9 @@ function resetWizardState(detail: NonNullable<typeof templateDetail.value>): voi
detail.template_key,
buildTemplateRenderContext({
locale: 'zh-CN',
brandName: normalizedBrandName.value,
officialWebsite: officialWebsite.value,
brandSummary: brandSummary.value,
brandName: normalizedBrandName.value,
officialWebsite: officialWebsite.value,
brandSummary: brandSummary.value,
primaryQuestion: '',
supplementalQuestions: [],
inputParams: {},
@@ -565,7 +563,8 @@ function applyDraftArticleState(detail: ArticleDetail): void {
selectedKnowledgeGroupIds.value = numberListFromUnknown(draftState.knowledge_group_ids) ?? []
customOutlineSections.value = parseCustomOutlineSections(draftState.custom_outline_sections)
outlineOptionOrder.value =
sanitizeOutlineKeys(stringListFromUnknown(draftState.outline_option_order)) ?? outlineOptionOrder.value
sanitizeOutlineKeys(stringListFromUnknown(draftState.outline_option_order)) ??
outlineOptionOrder.value
outlineSections.value =
sanitizeOutlineKeys(stringListFromUnknown(draftState.outline_sections)) ?? outlineSections.value
generatedOutline.value = decorateOutlineNodes(parseOutlineNodes(draftState.generated_outline))
@@ -652,46 +651,50 @@ watch(supplementalQuestionIds, (ids) => {
}
})
watch(selectedBrandId, async (brandId) => {
if (restoringDraft.value) {
return
}
if (!brandId) {
competitorDrafts.value = competitorDrafts.value.map((item) => ({
...item,
libraryId: undefined,
saved: false,
watch(
selectedBrandId,
async (brandId) => {
if (restoringDraft.value) {
return
}
if (!brandId) {
competitorDrafts.value = competitorDrafts.value.map((item) => ({
...item,
libraryId: undefined,
saved: false,
}))
return
}
const brand = selectedBrand.value
if (brand?.website && !officialWebsite.value.trim()) {
officialWebsite.value = brand.website
}
if (brand?.description && !brandSummary.value.trim()) {
brandSummary.value = brand.description
}
primaryQuestionId.value = null
supplementalQuestionIds.value = []
await questionsQuery.refetch()
if (!showCompetitorsCard.value) {
competitorDrafts.value = []
return
}
const competitorsResult = await competitorsQuery.refetch()
competitorDrafts.value = (competitorsResult.data ?? []).map((item) => ({
key: nextCompetitorKey(),
libraryId: item.id,
name: item.name,
website: item.website ?? '',
description: item.description ?? '',
saved: true,
}))
return
}
const brand = selectedBrand.value
if (brand?.website && !officialWebsite.value.trim()) {
officialWebsite.value = brand.website
}
if (brand?.description && !brandSummary.value.trim()) {
brandSummary.value = brand.description
}
primaryQuestionId.value = null
supplementalQuestionIds.value = []
await questionsQuery.refetch()
if (!showCompetitorsCard.value) {
competitorDrafts.value = []
return
}
const competitorsResult = await competitorsQuery.refetch()
competitorDrafts.value = (competitorsResult.data ?? []).map((item) => ({
key: nextCompetitorKey(),
libraryId: item.id,
name: item.name,
website: item.website ?? '',
description: item.description ?? '',
saved: true,
}))
}, { immediate: true })
},
{ immediate: true },
)
function hydrateBrandContextFromCurrentBrand(): void {
const brand = selectedBrand.value
@@ -1778,9 +1781,13 @@ function sanitizeOutlineKeys(keys: string[] | undefined): string[] | undefined {
return undefined
}
const blockedKeys = new Set(
baseOutlineOptions.value.filter((section) => isBlockedOutlineOption(section)).map((section) => section.key),
baseOutlineOptions.value
.filter((section) => isBlockedOutlineOption(section))
.map((section) => section.key),
)
const filtered = keys.filter(
(key) => !blockedKeys.has(key) && key.trim().toLowerCase() !== 'site_list',
)
const filtered = keys.filter((key) => !blockedKeys.has(key) && key.trim().toLowerCase() !== 'site_list')
return filtered.length > 0 ? filtered : []
}
+7 -2
View File
@@ -40,8 +40,8 @@ import {
formatDateTime,
getGenerateStatusMeta,
getPublishStatusMeta,
hasActiveGenerationStatus,
getTemplateMeta,
hasActiveGenerationStatus,
} from '@/lib/display'
import { formatError } from '@/lib/errors'
import { useCompanyStore } from '@/stores/company'
@@ -94,7 +94,12 @@ const kolCardsQuery = useQuery({
})
const recentArticlesQuery = useQuery({
queryKey: computed(() => ['workspace', 'recent-articles', 'templates', companyStore.currentBrandId]),
queryKey: computed(() => [
'workspace',
'recent-articles',
'templates',
companyStore.currentBrandId,
]),
enabled: computed(() => Boolean(companyStore.currentBrandId)),
queryFn: () => workspaceApi.recentArticles(),
})
@@ -810,7 +810,10 @@ function normalizeMarkdownTableLine(line: string, targetCellCount?: number): str
normalized = `| ${cells.join(' | ')} |`
}
return normalized.replace(/\s*\|\s*/g, ' | ').replace(/\s{2,}/g, ' ').trim()
return normalized
.replace(/\s*\|\s*/g, ' | ')
.replace(/\s{2,}/g, ' ')
.trim()
}
function normalizeLooseMarkdownTables(content: string): string {
@@ -822,9 +825,7 @@ function normalizeLooseMarkdownTables(content: string): string {
)
const lines = detachedTableBlocks.split('\n').flatMap((line: string) => {
const separatorWithFirstRow = line
.trim()
.match(/^((?:\|\s*:?-{3,}:?\s*){2,}\|?)(\S.*)$/)
const separatorWithFirstRow = line.trim().match(/^((?:\|\s*:?-{3,}:?\s*){2,}\|?)(\S.*)$/)
if (
!separatorWithFirstRow?.[1] ||
!separatorWithFirstRow[2] ||
@@ -878,7 +879,10 @@ function normalizeLooseMarkdownTables(content: string): string {
function normalizeAnswerMarkdown(content: string): string {
const headingMarkerNormalized = content
.replace(/(^|\n)[\u200b-\u200f\ufeff]*([ \t]{0,12})(?:\\#|&#35;|&#x23;|)/gi, '$1$2#')
.replace(/([^\n])(?=[\u200b-\u200f\ufeff]*[ \t]{0,12}(?:\\#|&#35;|&#x23;||#){1,6}\s*(?:\d{1,3}[.)、]|[一二三四五六七八九十]+[、..]|第[一二三四五六七八九十0-9]+))/gi, '$1\n\n')
.replace(
/([^\n])(?=[\u200b-\u200f\ufeff]*[ \t]{0,12}(?:\\#|&#35;|&#x23;||#){1,6}\s*(?:\d{1,3}[.)、]|[一二三四五六七八九十]+[、..]|第[一二三四五六七八九十0-9]+))/gi,
'$1\n\n',
)
const detachedHeadings = normalizeLooseMarkdownTables(headingMarkerNormalized)
.replace(
+4 -1
View File
@@ -150,7 +150,10 @@ watch(
return
}
if (selectedQuestionId.value && questions.some((item) => item.id === selectedQuestionId.value)) {
if (
selectedQuestionId.value &&
questions.some((item) => item.id === selectedQuestionId.value)
) {
return
}