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 { ArticleDetail } from '@geo/shared-types'
|
||||||
|
import type { ClipboardPayload } from '@/lib/clipboard'
|
||||||
|
|
||||||
export interface ArticleActionState {
|
export interface ArticleActionState {
|
||||||
showPublish: boolean
|
showPublish: boolean
|
||||||
@@ -69,12 +72,32 @@ export function resolveArticleActionState(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const markdownRenderer = new MarkdownIt({
|
||||||
|
html: true,
|
||||||
|
linkify: true,
|
||||||
|
breaks: true,
|
||||||
|
})
|
||||||
|
|
||||||
export function buildArticleClipboardContent(
|
export function buildArticleClipboardContent(
|
||||||
detail: Pick<ArticleDetail, 'title' | 'markdown_content'>,
|
detail: Pick<ArticleDetail, 'title' | 'markdown_content'>,
|
||||||
): string {
|
): ClipboardPayload | null {
|
||||||
const title = detail.title?.trim() ?? ''
|
const title = detail.title?.trim() ?? ''
|
||||||
const markdown = detail.markdown_content?.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) {
|
if (!title) {
|
||||||
return markdown
|
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 {
|
try {
|
||||||
if (navigator.clipboard?.writeText) {
|
if (navigator.clipboard?.writeText) {
|
||||||
await navigator.clipboard.writeText(text)
|
await navigator.clipboard.writeText(payload.text)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -9,7 +39,7 @@ export async function copyTextToClipboard(text: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const textarea = document.createElement('textarea')
|
const textarea = document.createElement('textarea')
|
||||||
textarea.value = text
|
textarea.value = payload.text
|
||||||
textarea.setAttribute('readonly', 'true')
|
textarea.setAttribute('readonly', 'true')
|
||||||
textarea.style.position = 'fixed'
|
textarea.style.position = 'fixed'
|
||||||
textarea.style.top = '0'
|
textarea.style.top = '0'
|
||||||
@@ -30,3 +60,43 @@ export async function copyTextToClipboard(text: string): Promise<void> {
|
|||||||
document.body.removeChild(textarea)
|
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 routeBrandId = computed(() => parsePositiveNumber(route.params.brandId))
|
||||||
const brandId = computed(() => companyStore.currentBrandId ?? routeBrandId.value)
|
const brandId = computed(() => companyStore.currentBrandId ?? routeBrandId.value)
|
||||||
const questionId = computed(() => parsePositiveNumber(route.params.questionId))
|
const questionId = computed(() => parsePositiveNumber(route.params.questionId))
|
||||||
const questionHash = computed(() => normalizeQueryValue(route.query.question_hash))
|
const questionHash = computed(
|
||||||
const questionTitleFallback = computed(() => normalizeQueryValue(route.query.question_text))
|
() =>
|
||||||
|
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 keywordId = computed(() => normalizeQueryValue(route.query.keyword_id))
|
||||||
const requestedPlatformId = computed(() => {
|
const requestedPlatformId = computed(() => {
|
||||||
const platformId = normalizeMonitoringPlatformId(normalizeQueryValue(route.query.ai_platform_id))
|
const platformId = normalizeMonitoringPlatformId(normalizeQueryValue(route.query.ai_platform_id))
|
||||||
@@ -1086,6 +1094,10 @@ function normalizeQueryValue(value: unknown): string {
|
|||||||
return String(raw ?? '').trim()
|
return String(raw ?? '').trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeHistoryStateValue(value: unknown): string {
|
||||||
|
return typeof value === 'string' ? value.trim() : ''
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeTrackingBusinessDate(value: unknown): string | null {
|
function normalizeTrackingBusinessDate(value: unknown): string | null {
|
||||||
const normalized = normalizeQueryValue(value)
|
const normalized = normalizeQueryValue(value)
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
|
|||||||
@@ -101,12 +101,12 @@ const allQuestionOptionValue = 'all'
|
|||||||
|
|
||||||
const selectedQuestionId = useStorage<number | null>('tracking_selected_question', null)
|
const selectedQuestionId = useStorage<number | null>('tracking_selected_question', null)
|
||||||
const selectedPlatformId = useStorage<string>('tracking_selected_ai_platform_id', 'all')
|
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)
|
const selectedCitationWindowDays = useStorage<number>('tracking_selected_citation_window_days', 7)
|
||||||
|
|
||||||
selectedPlatformId.value = normalizeMonitoringPlatformFilter(selectedPlatformId.value)
|
selectedPlatformId.value = normalizeMonitoringPlatformFilter(selectedPlatformId.value)
|
||||||
selectedBusinessDate.value =
|
|
||||||
normalizeTrackingBusinessDate(selectedBusinessDate.value) ?? trackingToday
|
|
||||||
selectedCitationWindowDays.value = normalizeCitationWindowDays(selectedCitationWindowDays.value)
|
selectedCitationWindowDays.value = normalizeCitationWindowDays(selectedCitationWindowDays.value)
|
||||||
|
|
||||||
const selectedQuestionSetOptionValue = computed<string | number>({
|
const selectedQuestionSetOptionValue = computed<string | number>({
|
||||||
@@ -168,10 +168,7 @@ watch(selectedBrandId, (newId, oldId) => {
|
|||||||
watch(
|
watch(
|
||||||
() => route.query.business_date,
|
() => route.query.business_date,
|
||||||
(value) => {
|
(value) => {
|
||||||
const requestedBusinessDate = normalizeTrackingBusinessDate(value)
|
selectedBusinessDate.value = normalizeTrackingBusinessDate(value) ?? trackingToday
|
||||||
if (requestedBusinessDate) {
|
|
||||||
selectedBusinessDate.value = requestedBusinessDate
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
@@ -636,13 +633,13 @@ function openQuestion(question: MonitoringHotQuestion): void {
|
|||||||
questionId: String(question.question_id),
|
questionId: String(question.question_id),
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
question_hash: question.question_hash,
|
|
||||||
question_text: question.question_text,
|
|
||||||
business_date: selectedBusinessDate.value,
|
business_date: selectedBusinessDate.value,
|
||||||
date_from: selectedBusinessDate.value,
|
|
||||||
date_to: selectedBusinessDate.value,
|
|
||||||
...(selectedPlatformId.value !== 'all' ? { ai_platform_id: selectedPlatformId.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