feat: implement browser extension for media publishing and add backend support for media management
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { useQuery, useQueryClient } from "@tanstack/vue-query";
|
||||
import { message } from "ant-design-vue";
|
||||
import type { TableColumnsType } from "ant-design-vue";
|
||||
import type { ArticleVersion } from "@geo/shared-types";
|
||||
import type { ArticleVersion, PublishRecord } from "@geo/shared-types";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
@@ -48,6 +49,12 @@ const versionsQuery = useQuery({
|
||||
queryFn: () => articlesApi.versions(props.articleId as number),
|
||||
});
|
||||
|
||||
const publishRecordsQuery = useQuery({
|
||||
queryKey: computed(() => ["articles", "publish-records", props.articleId]),
|
||||
enabled,
|
||||
queryFn: () => articlesApi.publishRecords(props.articleId as number),
|
||||
});
|
||||
|
||||
const versionColumns = computed<TableColumnsType<ArticleVersion>>(() => [
|
||||
{
|
||||
title: t("common.versions"),
|
||||
@@ -80,6 +87,32 @@ const versionColumns = computed<TableColumnsType<ArticleVersion>>(() => [
|
||||
},
|
||||
]);
|
||||
|
||||
const publishRecordColumns = computed<TableColumnsType<PublishRecord>>(() => [
|
||||
{
|
||||
title: t("media.records.platform"),
|
||||
dataIndex: "platform_name",
|
||||
key: "platform_name",
|
||||
width: 140,
|
||||
},
|
||||
{
|
||||
title: t("media.records.status"),
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
width: 130,
|
||||
},
|
||||
{
|
||||
title: t("media.records.publishedAt"),
|
||||
dataIndex: "published_at",
|
||||
key: "published_at",
|
||||
width: 168,
|
||||
},
|
||||
{
|
||||
title: t("media.records.link"),
|
||||
dataIndex: "external_article_url",
|
||||
key: "external_article_url",
|
||||
},
|
||||
]);
|
||||
|
||||
const detail = computed(() => detailQuery.data.value);
|
||||
const displayTitle = computed(() => streamTitle.value || detail.value?.title || t("article.untitled"));
|
||||
const displayMarkdown = computed(() => streamMarkdown.value || detail.value?.markdown_content || "");
|
||||
@@ -185,10 +218,30 @@ async function refreshArticleData(): Promise<void> {
|
||||
await Promise.all([
|
||||
detailQuery.refetch(),
|
||||
versionsQuery.refetch(),
|
||||
publishRecordsQuery.refetch(),
|
||||
queryClient.invalidateQueries({ queryKey: ["articles"] }),
|
||||
queryClient.invalidateQueries({ queryKey: ["workspace"] }),
|
||||
]);
|
||||
}
|
||||
|
||||
async function copyPublishLink(record: PublishRecord): Promise<void> {
|
||||
const target = record.external_article_url || record.external_manage_url;
|
||||
if (!target) {
|
||||
message.warning(t("media.records.linkEmpty"));
|
||||
return;
|
||||
}
|
||||
await navigator.clipboard.writeText(target);
|
||||
message.success(t("media.records.copySuccess"));
|
||||
}
|
||||
|
||||
function openPublishLink(record: PublishRecord): void {
|
||||
const target = record.external_article_url || record.external_manage_url;
|
||||
if (!target) {
|
||||
message.warning(t("media.records.linkEmpty"));
|
||||
return;
|
||||
}
|
||||
window.open(target, "_blank", "noopener,noreferrer");
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -275,6 +328,57 @@ async function refreshArticleData(): Promise<void> {
|
||||
</template>
|
||||
</a-table>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="published" :tab="t('media.records.tabTitle')">
|
||||
<a-table
|
||||
:columns="publishRecordColumns"
|
||||
:data-source="publishRecordsQuery.data.value || []"
|
||||
:loading="publishRecordsQuery.isPending.value"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
size="small"
|
||||
>
|
||||
<template #emptyText>{{ t("media.records.empty") }}</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'platform_name'">
|
||||
<div class="article-drawer__record-platform">
|
||||
<strong>{{ record.platform_name }}</strong>
|
||||
<span>{{ record.platform_nickname }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-tag :color="getPublishStatusMeta(record.status).color">
|
||||
{{ getPublishStatusMeta(record.status).label }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'published_at'">
|
||||
{{ formatDateTime(record.published_at) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'external_article_url'">
|
||||
<div class="article-drawer__record-actions">
|
||||
<a
|
||||
v-if="record.external_article_url || record.external_manage_url"
|
||||
class="article-drawer__record-link"
|
||||
:href="record.external_article_url || record.external_manage_url || '#'"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{{ record.external_article_url || record.external_manage_url }}
|
||||
</a>
|
||||
<span v-else class="article-drawer__record-empty">--</span>
|
||||
<div class="article-drawer__record-buttons">
|
||||
<a-button size="small" type="link" @click="openPublishLink(record)">
|
||||
{{ t("media.records.open") }}
|
||||
</a-button>
|
||||
<a-button size="small" type="link" @click="copyPublishLink(record)">
|
||||
{{ t("media.records.copy") }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</template>
|
||||
|
||||
@@ -361,6 +465,37 @@ async function refreshArticleData(): Promise<void> {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.article-drawer__record-platform {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.article-drawer__record-platform span,
|
||||
.article-drawer__record-empty {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.article-drawer__record-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.article-drawer__record-link {
|
||||
overflow: hidden;
|
||||
color: #1677ff;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.article-drawer__record-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.article-drawer__hero {
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user