diff --git a/src/api/types.ts b/src/api/types.ts index 5e816122..0cab28bc 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -997,7 +997,7 @@ export interface Plugin { instance_mode?: 'virtual' } -/** 插件市场为已安装插件发现的当前最高在线更新候选。 */ +/** 插件市场为已安装插件选择的当前更新候选。 */ export interface PluginUpdateCandidate { // 候选仓库是官方来源还是第三方来源 source_type: 'official' | 'third_party' @@ -1092,6 +1092,23 @@ export interface PluginSourceChangeRequest { release_version?: string | null } +/** 已安装插件完成来源确认后产生的仓库变更意图。 */ +export type PluginSourceTransition = + | { + // 为存量未绑定插件建立初始可信仓库 + action: 'bind' + // 管理员明确选择的目标插件仓库地址 + repo_url: string + } + | { + // 把已绑定插件切换到另一个可信仓库 + action: 'change' + // 管理员明确选择的目标插件仓库地址 + repo_url: string + // 提交换仓时必须匹配的当前身份 revision + expected_revision: number + } + export interface PluginRuntimeSummary { // 本轮插件源码、依赖和加载是否已收敛 ready: boolean diff --git a/src/components/cards/PluginCard.vue b/src/components/cards/PluginCard.vue index 44059688..91f35ace 100644 --- a/src/components/cards/PluginCard.vue +++ b/src/components/cards/PluginCard.vue @@ -3,7 +3,7 @@ import { useToast } from 'vue-toastification' import { useConfirm } from '@/composables/useConfirm' import api from '@/api' import { getApiBusinessErrorMessage } from '@/api/client' -import type { Plugin, PluginRating } from '@/api/types' +import type { Plugin, PluginRating, PluginSourceTransition } from '@/api/types' import { getLogoUrl, getProxyImageUrl } from '@/utils/imageUtils' import { usePluginCardAccent } from '@/composables/usePluginCardAccent' import { formatDownloadCount } from '@/@core/utils/formatters' @@ -45,7 +45,13 @@ const props = defineProps({ const globalSettingsStore = useGlobalSettingsStore() // 定义触发的自定义事件 -const emit = defineEmits(['remove', 'save', 'actionDone', 'rating']) +const emit = defineEmits<{ + remove: [] + save: [] + actionDone: [] + rating: [pluginRating: PluginRating] + sourceTransition: [plugin: Plugin, transition: PluginSourceTransition] +}>() // 多语言 const { t } = useI18n() @@ -56,13 +62,15 @@ const hasCardStatus = computed( () => sourceBindingRequired.value || Boolean(props.plugin?.has_update) || hasCardRating.value, ) const updateCandidate = computed(() => props.plugin?.update_candidate) -const hasAlternativeUpdate = computed( - () => Boolean(props.plugin?.has_update && updateCandidate.value && !updateCandidate.value.is_bound), +const hasAlternativeUpdate = computed(() => + Boolean(props.plugin?.has_update && updateCandidate.value && !updateCandidate.value.is_bound), ) const updateSourceName = computed(() => { const candidate = updateCandidate.value if (!candidate) return '' - return candidate.source_key.startsWith('github:') ? candidate.source_key.slice('github:'.length) : candidate.source_key + return candidate.source_key.startsWith('github:') + ? candidate.source_key.slice('github:'.length) + : candidate.source_key }) const updateBadgeTitle = computed(() => { const candidate = updateCandidate.value @@ -420,13 +428,13 @@ async function fetchInstalledPluginDetail() { return pluginDetail } -/** 使用插件市场详情弹窗展示已安装插件信息和评分入口。 */ -async function showPluginAbout(initialSourceSelectionOpen = false) { - const pluginDetail = await fetchInstalledPluginDetail() +/** 先展示本地快照,再用市场详情补齐仍处于打开状态的同一个弹窗。 */ +function showPluginAbout(initialSourceSelectionOpen = false) { + const pluginDetail = props.plugin if (!pluginDetail) return marketDetailDialogController?.close() - marketDetailDialogController = openSharedDialog( + const controller = openSharedDialog( PluginMarketDetailDialog, { plugin: pluginDetail, @@ -440,9 +448,19 @@ async function showPluginAbout(initialSourceSelectionOpen = false) { void pluginSidebarNavStore.ensureSidebarNav(true) }, rating: (pluginRating: PluginRating) => emit('rating', pluginRating), + sourceTransition: (transition: PluginSourceTransition) => { + if (props.plugin) emit('sourceTransition', props.plugin, transition) + }, }, { closeOn: ['close', 'install', 'update:modelValue'] }, ) + marketDetailDialogController = controller + + void fetchInstalledPluginDetail().then(latestPluginDetail => { + if (latestPluginDetail && marketDetailDialogController === controller) { + controller.updateProps({ plugin: latestPluginDetail }) + } + }) } /** 更新来自其他仓库时先展开来源选择,否则进入绑定仓库的更新说明。 */ @@ -913,7 +931,7 @@ watch( justify-content: center; gap: 0.5rem; color: rgb(var(--v-theme-on-surface)); - background: rgba(var(--v-theme-surface), 88%); + background: rgba(var(--v-theme-surface), 72%); font-size: 0.875rem; font-weight: 600; inset: 0; @@ -922,7 +940,7 @@ watch( .plugin-card__runtime-state--error { color: rgb(var(--v-theme-error)); - background: rgba(var(--v-theme-surface), 94%); + background: rgba(var(--v-theme-surface), 80%); } .card-cover-blurred::before { diff --git a/src/components/cards/PluginMixedSortCard.vue b/src/components/cards/PluginMixedSortCard.vue index 02340152..d8af6c50 100644 --- a/src/components/cards/PluginMixedSortCard.vue +++ b/src/components/cards/PluginMixedSortCard.vue @@ -1,5 +1,5 @@