Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 075e282c76 | |||
| 0d690371d2 |
@@ -1,4 +1,7 @@
|
||||
import DOMPurify from 'dompurify'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import type { ArticleDetail } from '@geo/shared-types'
|
||||
import type { ClipboardPayload } from '@/lib/clipboard'
|
||||
|
||||
export interface ArticleActionState {
|
||||
showPublish: boolean
|
||||
@@ -69,12 +72,32 @@ export function resolveArticleActionState(
|
||||
}
|
||||
}
|
||||
|
||||
const markdownRenderer = new MarkdownIt({
|
||||
html: true,
|
||||
linkify: true,
|
||||
breaks: true,
|
||||
})
|
||||
|
||||
export function buildArticleClipboardContent(
|
||||
detail: Pick<ArticleDetail, 'title' | 'markdown_content'>,
|
||||
): string {
|
||||
): ClipboardPayload | null {
|
||||
const title = detail.title?.trim() ?? ''
|
||||
const markdown = detail.markdown_content?.trim() ?? ''
|
||||
const text = buildArticleMarkdownText(title, markdown)
|
||||
|
||||
if (!text) {
|
||||
return null
|
||||
}
|
||||
|
||||
const html = DOMPurify.sanitize(markdownRenderer.render(text))
|
||||
|
||||
return {
|
||||
text,
|
||||
html,
|
||||
}
|
||||
}
|
||||
|
||||
function buildArticleMarkdownText(title: string, markdown: string): string {
|
||||
if (!title) {
|
||||
return markdown
|
||||
}
|
||||
|
||||
@@ -1,7 +1,37 @@
|
||||
export async function copyTextToClipboard(text: string): Promise<void> {
|
||||
export interface ClipboardPayload {
|
||||
text: string
|
||||
html?: string
|
||||
}
|
||||
|
||||
export async function copyTextToClipboard(content: string | ClipboardPayload): Promise<void> {
|
||||
const payload = normalizeClipboardPayload(content)
|
||||
|
||||
if (payload.html?.trim()) {
|
||||
try {
|
||||
if (navigator.clipboard?.write && typeof ClipboardItem !== 'undefined') {
|
||||
await navigator.clipboard.write([
|
||||
new ClipboardItem({
|
||||
'text/html': new Blob([payload.html], { type: 'text/html' }),
|
||||
'text/plain': new Blob([payload.text], { type: 'text/plain' }),
|
||||
}),
|
||||
])
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
// Fall back to the selection-based copy path below.
|
||||
}
|
||||
|
||||
try {
|
||||
copyHtmlWithSelection(payload)
|
||||
return
|
||||
} catch {
|
||||
// Fall back to plain text copy below.
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(text)
|
||||
await navigator.clipboard.writeText(payload.text)
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
@@ -9,7 +39,7 @@ export async function copyTextToClipboard(text: string): Promise<void> {
|
||||
}
|
||||
|
||||
const textarea = document.createElement('textarea')
|
||||
textarea.value = text
|
||||
textarea.value = payload.text
|
||||
textarea.setAttribute('readonly', 'true')
|
||||
textarea.style.position = 'fixed'
|
||||
textarea.style.top = '0'
|
||||
@@ -30,3 +60,43 @@ export async function copyTextToClipboard(text: string): Promise<void> {
|
||||
document.body.removeChild(textarea)
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeClipboardPayload(content: string | ClipboardPayload): ClipboardPayload {
|
||||
if (typeof content === 'string') {
|
||||
return { text: content }
|
||||
}
|
||||
|
||||
return {
|
||||
text: content.text,
|
||||
html: content.html,
|
||||
}
|
||||
}
|
||||
|
||||
function copyHtmlWithSelection(payload: ClipboardPayload): void {
|
||||
const container = document.createElement('div')
|
||||
container.innerHTML = payload.html ?? ''
|
||||
container.setAttribute('contenteditable', 'true')
|
||||
container.style.position = 'fixed'
|
||||
container.style.top = '0'
|
||||
container.style.left = '-9999px'
|
||||
container.style.opacity = '0'
|
||||
container.style.pointerEvents = 'none'
|
||||
|
||||
document.body.appendChild(container)
|
||||
|
||||
const selection = window.getSelection()
|
||||
const range = document.createRange()
|
||||
range.selectNodeContents(container)
|
||||
selection?.removeAllRanges()
|
||||
selection?.addRange(range)
|
||||
|
||||
try {
|
||||
const copied = document.execCommand('copy')
|
||||
if (!copied) {
|
||||
throw new Error('copy command failed')
|
||||
}
|
||||
} finally {
|
||||
selection?.removeAllRanges()
|
||||
document.body.removeChild(container)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,8 +30,16 @@ const trackingToday = dayjs().format('YYYY-MM-DD')
|
||||
const routeBrandId = computed(() => parsePositiveNumber(route.params.brandId))
|
||||
const brandId = computed(() => companyStore.currentBrandId ?? routeBrandId.value)
|
||||
const questionId = computed(() => parsePositiveNumber(route.params.questionId))
|
||||
const questionHash = computed(() => normalizeQueryValue(route.query.question_hash))
|
||||
const questionTitleFallback = computed(() => normalizeQueryValue(route.query.question_text))
|
||||
const questionHash = computed(
|
||||
() =>
|
||||
normalizeHistoryStateValue(history.state?.tracking_question_hash) ||
|
||||
normalizeQueryValue(route.query.question_hash),
|
||||
)
|
||||
const questionTitleFallback = computed(
|
||||
() =>
|
||||
normalizeHistoryStateValue(history.state?.tracking_question_text) ||
|
||||
normalizeQueryValue(route.query.question_text),
|
||||
)
|
||||
const keywordId = computed(() => normalizeQueryValue(route.query.keyword_id))
|
||||
const requestedPlatformId = computed(() => {
|
||||
const platformId = normalizeMonitoringPlatformId(normalizeQueryValue(route.query.ai_platform_id))
|
||||
@@ -1086,6 +1094,10 @@ function normalizeQueryValue(value: unknown): string {
|
||||
return String(raw ?? '').trim()
|
||||
}
|
||||
|
||||
function normalizeHistoryStateValue(value: unknown): string {
|
||||
return typeof value === 'string' ? value.trim() : ''
|
||||
}
|
||||
|
||||
function normalizeTrackingBusinessDate(value: unknown): string | null {
|
||||
const normalized = normalizeQueryValue(value)
|
||||
if (!normalized) {
|
||||
|
||||
@@ -101,12 +101,12 @@ const allQuestionOptionValue = 'all'
|
||||
|
||||
const selectedQuestionId = useStorage<number | null>('tracking_selected_question', null)
|
||||
const selectedPlatformId = useStorage<string>('tracking_selected_ai_platform_id', 'all')
|
||||
const selectedBusinessDate = useStorage<string>('tracking_selected_business_date', trackingToday)
|
||||
const selectedBusinessDate = ref<string>(
|
||||
normalizeTrackingBusinessDate(route.query.business_date) ?? trackingToday,
|
||||
)
|
||||
const selectedCitationWindowDays = useStorage<number>('tracking_selected_citation_window_days', 7)
|
||||
|
||||
selectedPlatformId.value = normalizeMonitoringPlatformFilter(selectedPlatformId.value)
|
||||
selectedBusinessDate.value =
|
||||
normalizeTrackingBusinessDate(selectedBusinessDate.value) ?? trackingToday
|
||||
selectedCitationWindowDays.value = normalizeCitationWindowDays(selectedCitationWindowDays.value)
|
||||
|
||||
const selectedQuestionSetOptionValue = computed<string | number>({
|
||||
@@ -168,10 +168,7 @@ watch(selectedBrandId, (newId, oldId) => {
|
||||
watch(
|
||||
() => route.query.business_date,
|
||||
(value) => {
|
||||
const requestedBusinessDate = normalizeTrackingBusinessDate(value)
|
||||
if (requestedBusinessDate) {
|
||||
selectedBusinessDate.value = requestedBusinessDate
|
||||
}
|
||||
selectedBusinessDate.value = normalizeTrackingBusinessDate(value) ?? trackingToday
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
@@ -636,13 +633,13 @@ function openQuestion(question: MonitoringHotQuestion): void {
|
||||
questionId: String(question.question_id),
|
||||
},
|
||||
query: {
|
||||
question_hash: question.question_hash,
|
||||
question_text: question.question_text,
|
||||
business_date: selectedBusinessDate.value,
|
||||
date_from: selectedBusinessDate.value,
|
||||
date_to: selectedBusinessDate.value,
|
||||
...(selectedPlatformId.value !== 'all' ? { ai_platform_id: selectedPlatformId.value } : {}),
|
||||
},
|
||||
state: {
|
||||
tracking_question_hash: question.question_hash,
|
||||
tracking_question_text: question.question_text,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user