feat: Add platform adapters for various content publishing platforms

- Implemented Dongchedi adapter for user detection and publishing.
- Implemented Jianshu adapter for user detection and publishing.
- Implemented Juejin adapter for user detection and publishing.
- Implemented Qiehao adapter for user detection and publishing.
- Implemented Smzdm adapter for user detection and publishing.
- Implemented Sohuhao adapter for user detection and publishing.
- Implemented Toutiaohao adapter for user detection and publishing.
- Implemented Wangyihao adapter for user detection and publishing.
- Implemented Weixin Gzh adapter for user detection and publishing.
- Implemented Zol adapter for user detection and publishing.
- Added documentation for manual testing of media publishing.
This commit is contained in:
2026-04-03 17:48:30 +08:00
parent 32d6a462cd
commit 134dd063c3
33 changed files with 2722 additions and 457 deletions
@@ -1,9 +1,15 @@
<script setup lang="ts">
import { ReloadOutlined, SendOutlined } from "@ant-design/icons-vue";
import { ReloadOutlined, SendOutlined, PlusOutlined } from "@ant-design/icons-vue";
import { useMutation, useQuery, useQueryClient } from "@tanstack/vue-query";
import { message } from "ant-design-vue";
import type { MediaPlatform, PlatformAccount, PublisherLocalPlatformState } from "@geo/shared-types";
import { computed, ref, watch } from "vue";
import { message, notification } from "ant-design-vue";
import type {
MediaPlatform,
PlatformAccount,
PublisherLocalPlatformState,
PublisherPublishResponse,
PublisherPublishTaskResult,
} from "@geo/shared-types";
import { computed, h, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { articlesApi, getApiBaseURL, mediaApi } from "@/lib/api";
@@ -32,6 +38,24 @@ const pluginInstalled = ref(false);
const pluginVersion = ref<string | undefined>();
const pluginInstallationId = ref<number | null>(null);
const localPlatforms = ref<PublisherLocalPlatformState[]>([]);
const fileList = ref<any[]>([]);
function handleBeforeUpload(file: File) {
const url = URL.createObjectURL(file);
coverAssetUrl.value = url;
fileList.value = [{
uid: file.name,
name: file.name,
status: 'done',
url: url,
}];
return false;
}
function handleRemove() {
coverAssetUrl.value = "";
fileList.value = [];
}
const detailQuery = useQuery({
queryKey: computed(() => ["articles", "detail", props.articleId, "publish-modal"]),
@@ -57,6 +81,7 @@ watch(
if (!open) {
selectedAccountIds.value = [];
coverAssetUrl.value = "";
fileList.value = [];
pluginInstallationId.value = null;
localPlatforms.value = [];
return;
@@ -70,6 +95,10 @@ const localPlatformMap = computed(() => {
return new Map(localPlatforms.value.map((item) => [item.platform_id, item]));
});
const platformNameMap = computed(() => {
return new Map((platformsQuery.data.value ?? []).map((platform) => [platform.platform_id, platform.name]));
});
const accountCards = computed(() => {
const accountMap = new Map((accountsQuery.data.value ?? []).map((account) => [account.platform_id, account]));
@@ -87,6 +116,8 @@ const accountCards = computed(() => {
accountId: account?.id ?? null,
platformId: platform.platform_id,
platformName: platform.name,
platformShortName: platform.short_name,
platformAccentColor: platform.accent_color,
platformCategory: platform.category,
platformUid: account?.platform_uid ?? local?.platform_uid ?? "--",
nickname: account?.nickname ?? local?.nickname ?? t("media.card.unbound"),
@@ -131,9 +162,14 @@ const publishMutation = useMutation({
const successCount = result.results.filter((item) => item.success).length;
const failedCount = result.results.length - successCount;
if (failedCount === 0) {
message.success(t("media.publish.messages.success", { count: successCount }));
notification.success({
message: t("media.publish.messages.successTitle"),
description: t("media.publish.messages.success", { count: successCount }),
placement: "topRight",
duration: 4.5,
});
} else {
message.warning(t("media.publish.messages.partial", { success: successCount, failed: failedCount }));
showPublishFailures(result);
}
await Promise.all([
queryClient.invalidateQueries({ queryKey: ["articles"] }),
@@ -206,33 +242,72 @@ function resolveAccountStatusText(
}
return t("media.account.ready");
}
function platformNameForResult(result: PublisherPublishTaskResult): string {
return platformNameMap.value.get(result.platform_id) ?? result.platform_id;
}
function publishFailureText(result: PublisherPublishTaskResult): string {
return t("media.publish.messages.failureItem", {
platform: platformNameForResult(result),
reason: result.message?.trim() || t("media.publish.messages.unknownFailure"),
});
}
function showPublishFailures(result: PublisherPublishResponse): void {
const failedResults = result.results.filter((item) => !item.success);
if (!failedResults.length) {
return;
}
const successCount = result.results.filter((item) => item.success).length;
const failedCount = failedResults.length;
const summary = t("media.publish.messages.partial", {
success: successCount,
failed: failedCount,
});
notification.error({
message: t("media.publish.messages.failureTitle"),
description: h(
"div",
[
h("div", { style: "line-height:1.75;font-weight:500;margin-bottom:8px;" }, summary),
...failedResults.map((item) =>
h("div", { style: "line-height:1.75;" }, publishFailureText(item)),
),
],
),
placement: "topRight",
duration: 8,
});
}
</script>
<template>
<a-modal
:open="open"
:title="t('media.publish.title')"
width="920"
:width="650"
:confirm-loading="publishMutation.isPending.value"
@ok="publishMutation.mutate()"
@cancel="emit('update:open', false)"
>
<div class="publish-modal">
<section class="publish-modal__hero">
<div>
<p class="eyebrow">{{ t("media.publish.subtitle") }}</p>
<h3>{{ modalTitle }}</h3>
<p>{{ t("media.publish.description") }}</p>
</div>
<div class="publish-modal__hero-actions">
<a-tag :color="pluginInstalled ? 'success' : 'error'">
{{ pluginInstalled ? t("media.plugin.ready") : t("media.plugin.notInstalled") }}
</a-tag>
<a-button :loading="runtimeLoading" @click="refreshRuntime">
<template #icon><ReloadOutlined /></template>
{{ t("media.actions.redetect") }}
</a-button>
<div class="publish-modal__hero-top">
<span class="eyebrow">标题</span>
<div class="publish-modal__hero-actions">
<a-tag :color="pluginInstalled ? 'success' : 'error'" class="hero-tag">
{{ pluginInstalled ? t("media.plugin.ready") : t("media.plugin.notInstalled") }}
</a-tag>
<a-button :loading="runtimeLoading" @click="refreshRuntime" size="small" class="hero-btn">
<template #icon><ReloadOutlined /></template>
{{ t("media.actions.redetect") }}
</a-button>
</div>
</div>
<h3>{{ modalTitle }}</h3>
</section>
<a-alert
@@ -244,11 +319,9 @@ function resolveAccountStatusText(
/>
<section class="publish-modal__section">
<div class="section-heading">
<div>
<h3>{{ t("media.publish.platformsTitle") }}</h3>
<p class="muted">{{ t("media.publish.platformsHint") }}</p>
</div>
<div class="publish-modal__section-header">
<h3><span class="required-star">*</span> {{ t("media.publish.platformsTitle") }}</h3>
<p class="muted">{{ t("media.publish.platformsHint") }}</p>
</div>
<div v-if="accountsQuery.isPending.value || platformsQuery.isPending.value" class="publish-modal__loading">
@@ -267,20 +340,31 @@ function resolveAccountStatusText(
}"
@click="account.accountId ? toggleAccount(account.accountId, account.selectable) : undefined"
>
<span class="publish-modal__check">
<span
v-if="account.accountId && isSelected(account.accountId)"
class="publish-modal__check-dot"
></span>
</span>
<span class="publish-modal__card-copy">
<strong>{{ account.platformName }}</strong>
<small>{{ account.nickname }} · ID: {{ account.platformUid }}</small>
<small>{{ account.statusText }}</small>
</span>
<a-tag :color="account.selectable ? 'success' : account.accountId ? 'warning' : 'default'">
{{ account.selectable ? t("media.account.selectable") : t("media.account.unavailable") }}
</a-tag>
<div class="publish-modal__card-left">
<span class="publish-modal__check">
<span
v-if="account.accountId && isSelected(account.accountId)"
class="publish-modal__check-inner"
></span>
</span>
<span class="publish-modal__card-badge" :style="{ color: account.platformAccentColor }">
{{ account.platformShortName }}
</span>
<div class="publish-modal__card-text">
<span class="publish-modal__card-name">{{ account.platformName }}</span>
<span class="publish-modal__card-sub" v-if="account.accountId">
{{ account.nickname }}
</span>
<span class="publish-modal__card-sub" v-else>
{{ t("media.card.unbound") }}
</span>
</div>
</div>
<div class="publish-modal__card-right">
<span class="publish-modal__card-status" :class="account.selectable ? 'status-selectable' : 'status-disabled'">
{{ account.selectable ? t("media.account.selectable") : t("media.account.unavailable") }}
</span>
</div>
</button>
</div>
@@ -288,20 +372,29 @@ function resolveAccountStatusText(
</section>
<section class="publish-modal__section">
<div class="publish-modal__cover">
<div class="publish-modal__cover-head">
<div>
<h3>{{ t("media.publish.coverTitle") }}</h3>
<p class="muted">{{ t("media.publish.coverHint") }}</p>
</div>
<a-switch v-model:checked="coverEnabled" />
</div>
<div class="publish-modal__section-header inline-header">
<h3>{{ t("media.publish.coverTitle") }}</h3>
<a-switch v-model:checked="coverEnabled" />
</div>
<p class="muted">请保证封面清晰美观和完整</p>
<a-input
v-if="coverEnabled"
v-model:value="coverAssetUrl"
:placeholder="t('media.publish.coverPlaceholder')"
/>
<div class="publish-modal__cover-body" v-if="coverEnabled">
<a-upload
v-model:file-list="fileList"
list-type="picture-card"
class="cover-uploader"
:show-upload-list="true"
:before-upload="handleBeforeUpload"
@remove="handleRemove"
accept="image/*"
>
<div v-if="fileList.length < 1">
<div class="upload-icon-circle">
<PlusOutlined />
</div>
<div class="upload-text">上传封面图</div>
</div>
</a-upload>
</div>
</section>
</div>
@@ -312,133 +405,245 @@ function resolveAccountStatusText(
.publish-modal {
display: flex;
flex-direction: column;
gap: 20px;
gap: 24px;
}
.publish-modal__hero {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px 20px;
border: 1px solid #e5e5e5;
border-radius: 12px;
background: #f7f7f7;
}
.publish-modal__hero-top {
display: flex;
align-items: center;
gap: 16px;
justify-content: space-between;
gap: 20px;
padding: 20px 22px;
border: 1px solid #e6edf5;
border-radius: 18px;
background: linear-gradient(135deg, #fbfdff 0%, #f4f9ff 100%);
margin-bottom: 12px;
}
.publish-modal__hero h3,
.publish-modal__section h3 {
.publish-modal__hero h3 {
margin: 0;
font-size: 18px;
color: #111827;
font-size: 16px;
font-weight: 600;
color: #262626;
line-height: 1.5;
}
.publish-modal__hero p:last-child {
margin: 8px 0 0;
color: #6b7280;
.eyebrow {
font-size: 16px;
text-transform: uppercase;
color: #000000;
margin: 0;
}
.publish-modal__hero-actions {
display: flex;
align-items: flex-start;
align-items: center;
gap: 10px;
}
.hero-tag {
margin: 0;
}
.publish-modal__section {
display: flex;
flex-direction: column;
gap: 14px;
}
.publish-modal__section-header {
margin-bottom: 12px;
}
.publish-modal__section-header.inline-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
}
.publish-modal__section-header h3 {
margin: 0 0 6px 0;
font-size: 15px;
font-weight: 600;
color: #262626;
}
.publish-modal__section-header.inline-header h3 {
margin: 0;
}
.required-star {
color: #ff4d4f;
margin-right: 4px;
font-family: SimSun, sans-serif;
}
.muted {
font-size: 13px;
color: #8c8c8c;
margin: 0;
}
.publish-modal__grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
gap: 12px;
margin-top: 4px;
}
.publish-modal__card {
display: flex;
align-items: center;
gap: 16px;
justify-content: space-between;
width: 100%;
padding: 18px;
border: 1px solid #dbe5f0;
border-radius: 18px;
padding: 12px 16px;
border: 1px solid #d9d9d9;
border-radius: 8px;
background: #fff;
text-align: left;
cursor: pointer;
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
transition: all 0.2s ease;
}
.publish-modal__card:hover {
border-color: #adc6ff;
box-shadow: 0 16px 28px rgba(22, 119, 255, 0.08);
transform: translateY(-1px);
.publish-modal__card:hover:not(.publish-modal__card--disabled) {
border-color: #1677ff;
}
.publish-modal__card--active {
border-color: #1677ff;
background: linear-gradient(135deg, #f7fbff 0%, #eef6ff 100%);
}
.publish-modal__card--disabled {
opacity: 0.72;
opacity: 0.6;
cursor: not-allowed;
background-color: #f7f7f7;
}
.publish-modal__card-left {
display: flex;
align-items: center;
gap: 12px;
}
.publish-modal__check {
width: 22px;
height: 22px;
border: 1px solid #cbd5e1;
border-radius: 999px;
width: 16px;
height: 16px;
border: 1px solid #d9d9d9;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
transition: all 0.2s;
}
.publish-modal__card--active .publish-modal__check {
background-color: #1677ff;
border-color: #1677ff;
}
.publish-modal__check-inner {
width: 8px;
height: 8px;
background-color: transparent;
border: 2px solid #fff;
border-top: 0;
border-left: 0;
transform: rotate(45deg) scale(1) translate(-1px, -1px);
display: block;
}
.publish-modal__card-badge {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 24px;
height: 24px;
border-radius: 6px;
background: #f7f7f7;
border: 1px solid #e5e5e5;
font-size: 12px;
font-weight: 700;
}
.publish-modal__check-dot {
width: 10px;
height: 10px;
border-radius: 999px;
background: #1677ff;
}
.publish-modal__card-copy {
flex: 1;
.publish-modal__card-text {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.publish-modal__card-copy strong {
font-size: 16px;
color: #0f172a;
.publish-modal__card-name {
font-size: 14px;
font-weight: 500;
color: #262626;
line-height: 1.3;
}
.publish-modal__card-copy small {
color: #64748b;
.publish-modal__card-sub {
font-size: 12px;
font-weight: 400;
color: #8c8c8c;
margin-top: 2px;
}
.publish-modal__cover {
padding: 18px;
border: 1px solid #e6edf5;
border-radius: 18px;
background: #fff;
.publish-modal__card-status {
font-size: 13px;
}
.publish-modal__cover-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 14px;
.status-selectable {
color: #52c41a;
}
.status-disabled {
color: #1677ff;
}
.publish-modal__loading {
padding: 12px 0;
}
.publish-modal__cover-body {
margin-top: 12px;
}
.cover-uploader :deep(.ant-upload.ant-upload-select-picture-card) {
width: 160px;
height: 120px;
background-color: #fff;
border: 1px dashed #d9d9d9;
border-radius: 8px;
}
.cover-uploader :deep(.ant-upload-list-picture-card-container) {
width: 160px;
height: 120px;
}
.cover-uploader :deep(.ant-upload-list-item) {
border-radius: 8px;
}
.upload-icon-circle {
width: 28px;
height: 28px;
background-color: #2b303b;
color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 8px;
font-size: 14px;
}
.upload-text {
font-size: 13px;
color: #595959;
}
@media (max-width: 860px) {
.publish-modal__hero {
flex-direction: column;
+11
View File
@@ -476,8 +476,14 @@ const enUS = {
},
messages: {
bindSuccess: "{platform} bound successfully.",
bindSuccessTitle: "Binding successful",
bindSuccessDetail: "Bound account [{account}] on [{platform}]",
bindPending: "The extension did not detect a local session for this platform yet. Sign in via the extension and retry.",
bindPendingTitle: "Binding incomplete",
bindFailureTitle: "Binding failed",
unbindSuccess: "Account unbound.",
unbindSuccessTitle: "Unbind successful",
unbindFailureTitle: "Unbind failed",
unbindConfirm: "Unbind this platform account? Publishing will no longer be available until it is bound again.",
},
publish: {
@@ -491,8 +497,13 @@ const enUS = {
coverPlaceholder: "Enter an optional cover asset URL",
messages: {
success: "{count} publish tasks submitted.",
successTitle: "Publish successful",
partial: "Publish finished with {success} succeeded and {failed} failed.",
partialTitle: "Partial publish failure",
selectPlatform: "Choose at least one available platform first.",
failureTitle: "Publish failed",
failureItem: "Publishing to [{platform}] failed: [{reason}]",
unknownFailure: "Unknown error",
},
},
records: {
+11
View File
@@ -483,8 +483,14 @@ const zhCN = {
},
messages: {
bindSuccess: "{platform} 绑定成功",
bindSuccessTitle: "绑定成功",
bindSuccessDetail: "已绑定【{platform}】的账号【{account}】",
bindPending: "插件尚未检测到该平台登录态,请先在插件中登录后重试。",
bindPendingTitle: "绑定未完成",
bindFailureTitle: "绑定失败",
unbindSuccess: "账号解绑成功",
unbindSuccessTitle: "解绑成功",
unbindFailureTitle: "解绑失败",
unbindConfirm: "确认解绑这个平台账号吗?解绑后将无法直接发布。",
},
publish: {
@@ -498,8 +504,13 @@ const zhCN = {
coverPlaceholder: "请输入封面素材 URL(可选)",
messages: {
success: "已提交 {count} 个平台发布任务",
successTitle: "发布成功",
partial: "发布执行完成,成功 {success} 个,失败 {failed} 个",
partialTitle: "部分发布失败",
selectPlatform: "请先选择至少一个可发布的平台",
failureTitle: "发布失败",
failureItem: "文章发布到平台【{platform}】失败【{reason}】",
unknownFailure: "未知错误",
},
},
records: {
+5 -1
View File
@@ -28,6 +28,10 @@ const errorMessageMap: Record<string, string> = {
article_title_required: "请输入文章标题",
invalid_payload: "请求参数不合法",
update_failed: "保存文章失败",
publisher_plugin_timeout: "浏览器插件响应超时,请刷新当前页面后重试",
publisher_plugin_empty_response: "浏览器插件未返回数据,请刷新当前页面后重试",
publisher_plugin_invalid_response: "浏览器插件返回了无效数据,请刷新当前页面后重试",
publisher_plugin_unknown_error: "浏览器插件调用失败,请稍后重试",
network_error: "网络连接失败,请稍后重试",
unknown_error: "发生未知错误",
};
@@ -39,7 +43,7 @@ export function formatError(error: unknown): string {
}
if (error instanceof Error) {
return error.message;
return errorMessageMap[error.message] ?? error.message;
}
return "发生未知错误";
+60 -4
View File
@@ -35,6 +35,47 @@ type PluginPayloadMap = {
publishArticle: PublisherPublishResponse;
};
function isPlainObject(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
function isPingPayload(value: unknown): value is PublisherPluginPingResponse {
return isPlainObject(value) && typeof value.installed === "boolean";
}
function isDetectPlatformsPayload(value: unknown): value is PublisherLocalPlatformState[] {
return Array.isArray(value);
}
function isRegisterInstallationPayload(value: unknown): value is PublisherRegisterInstallationResult {
return isPlainObject(value) && typeof value.success === "boolean" && typeof value.plugin_installation_id === "number";
}
function isBindPayload(value: unknown): value is PublisherBindResponse {
return isPlainObject(value) && typeof value.success === "boolean" && typeof value.platform_id === "string";
}
function isPublishPayload(value: unknown): value is PublisherPublishResponse {
return isPlainObject(value) && Array.isArray(value.results);
}
function isPayloadValid<T extends PluginAction>(action: T, value: unknown): value is PluginPayloadMap[T] {
switch (action) {
case "ping":
return isPingPayload(value);
case "detectPlatforms":
return isDetectPlatformsPayload(value);
case "registerInstallation":
return isRegisterInstallationPayload(value);
case "bindAccount":
return isBindPayload(value);
case "publishArticle":
return isPublishPayload(value);
default:
return false;
}
}
function nextRequestId(): string {
if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
return crypto.randomUUID();
@@ -42,6 +83,12 @@ function nextRequestId(): string {
return `geo-${Date.now()}-${Math.random().toString(36).slice(2)}`;
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => {
window.setTimeout(resolve, ms);
});
}
async function requestPlugin<T extends PluginAction>(
action: T,
payload?: T extends "bindAccount"
@@ -75,6 +122,10 @@ async function requestPlugin<T extends PluginAction>(
cleanup();
if (event.data.success) {
if (!isPayloadValid(action, event.data.data)) {
reject(new Error("publisher_plugin_invalid_response"));
return;
}
resolve(event.data.data);
return;
}
@@ -104,10 +155,15 @@ export async function pingPublisherPlugin(): Promise<PublisherPluginPingResponse
try {
return await requestPlugin("ping", undefined, 1500);
} catch {
return {
installed: false,
capabilities: [],
};
try {
await sleep(300);
return await requestPlugin("ping", undefined, 2000);
} catch {
return {
installed: false,
capabilities: [],
};
}
}
}
+18 -1
View File
@@ -13,6 +13,23 @@ export interface PublisherRuntimeState {
pluginInstallationId: number | null;
}
function normalizePingResponse(ping: PublisherPluginPingResponse | null | undefined): PublisherPluginPingResponse {
if (!ping || typeof ping.installed !== "boolean") {
return {
installed: false,
capabilities: [],
};
}
return {
installed: ping.installed,
version: ping.version,
capabilities: Array.isArray(ping.capabilities) ? ping.capabilities : [],
installation_key: ping.installation_key,
plugin_installation_id: ping.plugin_installation_id ?? null,
};
}
function detectBrowserName(): string {
const ua = navigator.userAgent;
if (/Edg\//.test(ua)) {
@@ -31,7 +48,7 @@ function detectBrowserName(): string {
}
export async function loadPublisherRuntimeState(): Promise<PublisherRuntimeState> {
const ping = await pingPublisherPlugin();
const ping = normalizePingResponse(await pingPublisherPlugin());
if (!ping.installed || !ping.installation_key) {
return {
ping,
+2
View File
@@ -39,6 +39,7 @@ import {
Tag,
TimePicker,
Tooltip,
Upload,
} from "ant-design-vue";
import App from "./App.vue";
@@ -100,6 +101,7 @@ app.component("ATextarea", Input.TextArea);
Tag,
TimePicker,
Tooltip,
Upload,
].forEach((component) => {
app.use(component);
});
+15 -13
View File
@@ -275,6 +275,14 @@ function toneClass(tone: CardTone): string {
</div>
</section>
<a-alert
v-if="!pluginInstalled"
type="warning"
show-icon
:message="t('media.plugin.installTitle')"
:description="t('media.plugin.installDesc')"
/>
<section class="media-runtime-card panel">
<div class="media-runtime-card__copy">
<h3 class="panel-title">{{ t("media.plugin.title") }}</h3>
@@ -298,13 +306,7 @@ function toneClass(tone: CardTone): string {
</div>
</section>
<a-alert
v-if="!pluginInstalled"
type="warning"
show-icon
:message="t('media.plugin.installTitle')"
:description="t('media.plugin.installDesc')"
/>
<section class="media-list-wrapper panel">
<div class="custom-tabs">
@@ -331,7 +333,7 @@ function toneClass(tone: CardTone): string {
class="media-search"
>
<template #prefix>
<SearchOutlined style="color: #bfbfbf;" />
<SearchOutlined style="color: #737373;" />
</template>
</a-input>
@@ -375,7 +377,7 @@ function toneClass(tone: CardTone): string {
</a>
</a-menu-item>
<a-menu-item v-if="card.accountCount > 1" disabled>
<span style="color: #bfbfbf;">{{ t("media.card.multiAccount", { count: card.accountCount }) }}</span>
<span style="color: #737373;">{{ t("media.card.multiAccount", { count: card.accountCount }) }}</span>
</a-menu-item>
</a-menu>
</template>
@@ -596,7 +598,7 @@ function toneClass(tone: CardTone): string {
top: 4px;
right: -8px;
background: #f5f5f5;
color: #bfbfbf;
color: f3f#737373;
font-size: 10px;
padding: 2px 6px;
border-radius: 4px;
@@ -716,12 +718,12 @@ function toneClass(tone: CardTone): string {
.media-card__identity-text p {
margin: 2px 0 0;
color: #bfbfbf;
color: #737373;
font-size: 11px;
}
.more-icon {
color: #bfbfbf;
color: #737373;
font-size: 18px;
cursor: pointer;
padding: 0 4px;
@@ -750,7 +752,7 @@ function toneClass(tone: CardTone): string {
.media-card__account-unbound {
font-size: 13px;
color: #bfbfbf;
color: #737373;
}
.media-card__footer {
+1 -1
View File
@@ -501,7 +501,7 @@ function refreshDashboard(): void {
align-items: center;
gap: 6px;
}
.muted { color: #bfbfbf; font-size: 11px; }
.muted { color: #737373; font-size: 11px; }
.muted-dot { color: #d9d9d9; font-size: 10px; }
/* Status Badges */