From eda90ea1e432500383d4bc4c16a31edd7383bdbd Mon Sep 17 00:00:00 2001 From: liangxu Date: Wed, 20 May 2026 17:00:57 +0800 Subject: [PATCH] fix(admin-web): make article copy robust --- .../src/components/ArticleDetailDrawer.vue | 3 ++- .../src/components/CustomArticleTab.vue | 3 ++- .../admin-web/src/lib/article-list-actions.ts | 27 ++++++++++++++++--- apps/admin-web/src/views/ImitationView.vue | 3 ++- .../src/views/PublishManagementView.vue | 18 +------------ apps/admin-web/src/views/TemplatesView.vue | 3 ++- apps/admin-web/src/views/WorkspaceView.vue | 3 ++- 7 files changed, 34 insertions(+), 26 deletions(-) diff --git a/apps/admin-web/src/components/ArticleDetailDrawer.vue b/apps/admin-web/src/components/ArticleDetailDrawer.vue index ea530e2..32f38e5 100644 --- a/apps/admin-web/src/components/ArticleDetailDrawer.vue +++ b/apps/admin-web/src/components/ArticleDetailDrawer.vue @@ -15,6 +15,7 @@ import { subscribeArticleGeneration, type ArticleGenerationStreamEvent, } from '@/lib/api' +import { copyTextToClipboard } from '@/lib/clipboard' import { formatDateTime, getGenerateStatusMeta, @@ -257,7 +258,7 @@ async function copyPublishLink(record: PublishRecord): Promise { return } - await navigator.clipboard.writeText(target) + await copyTextToClipboard(target) message.success(t('media.records.copySuccess')) } diff --git a/apps/admin-web/src/components/CustomArticleTab.vue b/apps/admin-web/src/components/CustomArticleTab.vue index c17e9a7..253234c 100644 --- a/apps/admin-web/src/components/CustomArticleTab.vue +++ b/apps/admin-web/src/components/CustomArticleTab.vue @@ -16,6 +16,7 @@ import PublishArticleModal from '@/components/PublishArticleModal.vue' import { articlesApi, promptRulesApi } 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 } from '@/lib/display' import { formatError } from '@/lib/errors' import { formatPublishPlatformList } from '@/lib/publish-platforms' @@ -223,7 +224,7 @@ async function copyArticle(articleId: number): Promise { return } - await navigator.clipboard.writeText(content) + await copyTextToClipboard(content) message.success(t('common.copySuccess')) } catch (error) { message.error(formatError(error) || t('common.noData')) diff --git a/apps/admin-web/src/lib/article-list-actions.ts b/apps/admin-web/src/lib/article-list-actions.ts index a778a20..46be15f 100644 --- a/apps/admin-web/src/lib/article-list-actions.ts +++ b/apps/admin-web/src/lib/article-list-actions.ts @@ -72,8 +72,27 @@ export function resolveArticleActionState( export function buildArticleClipboardContent( detail: Pick, ): string { - return [detail.title ? `# ${detail.title}` : '', detail.markdown_content ?? ''] - .filter(Boolean) - .join('\n\n') - .trim() + const title = detail.title?.trim() ?? '' + const markdown = detail.markdown_content?.trim() ?? '' + + if (!title) { + return markdown + } + + if (!markdown) { + return `# ${title}` + } + + if (getLeadingH1Text(markdown) === title) { + return markdown + } + + return [`# ${title}`, markdown].join('\n\n').trim() +} + +function getLeadingH1Text(markdown: string): string | null { + const normalizedMarkdown = markdown.replace(/^\uFEFF/, '').trimStart() + const firstLine = normalizedMarkdown.split(/\r?\n/, 1)[0]?.trim() ?? '' + const match = firstLine.match(/^#\s+(.+?)\s*#*$/) + return match?.[1]?.trim() ?? null } diff --git a/apps/admin-web/src/views/ImitationView.vue b/apps/admin-web/src/views/ImitationView.vue index 6a605bf..faeb07b 100644 --- a/apps/admin-web/src/views/ImitationView.vue +++ b/apps/admin-web/src/views/ImitationView.vue @@ -18,6 +18,7 @@ import PublishArticleModal from '@/components/PublishArticleModal.vue' 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 { formatError } from '@/lib/errors' import { useCompanyStore } from '@/stores/company' @@ -268,7 +269,7 @@ async function copyArticle(articleId: number): Promise { return } - await navigator.clipboard.writeText(content) + await copyTextToClipboard(content) message.success(t('common.copySuccess')) } catch (error) { message.error(formatError(error) || t('common.noData')) diff --git a/apps/admin-web/src/views/PublishManagementView.vue b/apps/admin-web/src/views/PublishManagementView.vue index e32b9a2..488946c 100644 --- a/apps/admin-web/src/views/PublishManagementView.vue +++ b/apps/admin-web/src/views/PublishManagementView.vue @@ -16,6 +16,7 @@ import { computed, ref, watch } from 'vue' import { useI18n } from 'vue-i18n' import { publishTasksApi, tenantAccountsApi } from '@/lib/api' +import { copyTextToClipboard } from '@/lib/clipboard' import { formatDateTime } from '@/lib/display' import { formatError } from '@/lib/errors' import { resolvePlatformLogoUrl } from '@/lib/publish-account-cards' @@ -473,23 +474,6 @@ function rowClassName(record: PublishTaskItem): string { return isPendingStatus(record.status) ? 'publish-management-row--pending' : '' } -async function copyTextToClipboard(text: string): Promise { - if (navigator.clipboard?.writeText) { - await navigator.clipboard.writeText(text) - return - } - - const textarea = document.createElement('textarea') - textarea.value = text - textarea.setAttribute('readonly', 'true') - textarea.style.position = 'fixed' - textarea.style.left = '-9999px' - document.body.appendChild(textarea) - textarea.select() - document.execCommand('copy') - document.body.removeChild(textarea) -} - async function copyErrorMessage(record: PublishTaskItem): Promise { if (!record.errorMessage) { return diff --git a/apps/admin-web/src/views/TemplatesView.vue b/apps/admin-web/src/views/TemplatesView.vue index 5d89ae5..eed5252 100644 --- a/apps/admin-web/src/views/TemplatesView.vue +++ b/apps/admin-web/src/views/TemplatesView.vue @@ -34,6 +34,7 @@ import PublishArticleModal from '@/components/PublishArticleModal.vue' import { articlesApi, templatesApi, workspaceApi } from '@/lib/api' import { buildArticleClipboardContent, resolveArticleActionState } from '@/lib/article-list-actions' import { useArticleRegenerateAction } from '@/lib/article-regenerate-action' +import { copyTextToClipboard } from '@/lib/clipboard' import { formatDateTime, getGenerateStatusMeta, @@ -466,7 +467,7 @@ async function copyArticle(articleId: number): Promise { return } - await navigator.clipboard.writeText(content) + await copyTextToClipboard(content) message.success(t('common.copySuccess')) } catch (error) { message.error(formatError(error) || t('common.noData')) diff --git a/apps/admin-web/src/views/WorkspaceView.vue b/apps/admin-web/src/views/WorkspaceView.vue index 7003aca..4578435 100644 --- a/apps/admin-web/src/views/WorkspaceView.vue +++ b/apps/admin-web/src/views/WorkspaceView.vue @@ -29,6 +29,7 @@ import PublishArticleModal from '@/components/PublishArticleModal.vue' import { articlesApi, tenantAccountsApi, workspaceApi } from '@/lib/api' import { buildArticleClipboardContent, resolveArticleActionState } from '@/lib/article-list-actions' import { useArticleRegenerateAction } from '@/lib/article-regenerate-action' +import { copyTextToClipboard } from '@/lib/clipboard' import { formatDateTime, getTemplateMeta, hasActiveGenerationStatus } from '@/lib/display' import { formatError } from '@/lib/errors' import { isMediaPublishAccount } from '@/lib/publish-platforms' @@ -265,7 +266,7 @@ async function copyArticle(articleId: number): Promise { return } - await navigator.clipboard.writeText(content) + await copyTextToClipboard(content) message.success(t('common.copySuccess')) } catch (error) { message.error(formatError(error) || t('common.noData'))