feat(plugin): add ratings and about dialog

This commit is contained in:
jxxghp
2026-07-31 13:41:29 +08:00
parent 3f414afff4
commit 246b8ff54b
15 changed files with 883 additions and 189 deletions
@@ -1,7 +1,8 @@
<script lang="ts" setup>
import api from '@/api'
import type { Plugin } from '@/api/types'
import type { ApiResponse, Plugin, PluginRating } from '@/api/types'
import { formatDownloadCount } from '@/@core/utils/formatters'
import PluginRatingDisplay from '@/components/common/PluginRatingDisplay.vue'
import { getLogoUrl } from '@/utils/imageUtils'
import { useToast } from 'vue-toastification'
import { useI18n } from 'vue-i18n'
@@ -46,6 +47,18 @@ const visible = computed({
},
})
const isInstalled = computed(() => Boolean(props.plugin?.installed))
const rating = ref<PluginRating>({
plugin_id: props.plugin?.id,
average_rating: props.plugin?.average_rating || 0,
rating_count: props.plugin?.rating_count || 0,
user_rating: props.plugin?.user_rating,
})
const selectedRating = ref(props.plugin?.user_rating || 0)
const ratingLoading = ref(false)
const ratingSubmitting = ref(false)
// 图片对象
const imageRef = ref<any>()
@@ -125,10 +138,12 @@ async function installPlugin(releaseVersion?: string, repoUrl?: string) {
try {
showInstallProgress(
t('plugin.installing', {
name: props.plugin?.plugin_name,
version: releaseVersion || props?.plugin?.plugin_version,
}),
isInstalled.value && !releaseVersion
? t('plugin.updating', { name: props.plugin?.plugin_name })
: t('plugin.installing', {
name: props.plugin?.plugin_name,
version: releaseVersion || props?.plugin?.plugin_version,
}),
)
const result: { [key: string]: any } = await api.get(`plugin/install/${props.plugin?.id}`, {
@@ -142,7 +157,11 @@ async function installPlugin(releaseVersion?: string, repoUrl?: string) {
closeInstallProgress()
if (result.success) {
$toast.success(t('plugin.installSuccess', { name: props.plugin?.plugin_name }))
$toast.success(
isInstalled.value
? t('plugin.updateSuccess', { name: props.plugin?.plugin_name })
: t('plugin.installSuccess', { name: props.plugin?.plugin_name }),
)
versionHistoryDialogController?.close()
versionHistoryDialogController = null
visible.value = false
@@ -161,7 +180,11 @@ function showUpdateHistory() {
versionHistoryDialogController?.close()
versionHistoryDialogController = openSharedDialog(
PluginVersionHistoryDialog,
{ plugin: props.plugin, actionMode: 'install' },
{
plugin: props.plugin,
actionMode: isInstalled.value ? 'update' : 'install',
showUpdateAction: isInstalled.value && Boolean(props.plugin?.has_update),
},
{
update: installPlugin,
},
@@ -169,6 +192,54 @@ function showUpdateHistory() {
)
}
/** 查询插件平均分和当前安装实例评分。 */
async function loadPluginRating() {
if (!props.plugin?.id) return
ratingLoading.value = true
try {
const result: PluginRating = await api.get(`plugin/rating/${props.plugin.id}`)
rating.value = result
selectedRating.value = result.user_rating || 0
} catch (error) {
console.error(error)
} finally {
ratingLoading.value = false
}
}
/** 提交已安装插件的当前安装实例评分。 */
async function submitPluginRating() {
if (!props.plugin?.id || !isInstalled.value || selectedRating.value <= 0) return
ratingSubmitting.value = true
try {
const result: ApiResponse<PluginRating> = await api.post(`plugin/rating/${props.plugin.id}`, {
rating: selectedRating.value,
})
if (result.success) {
rating.value = result.data
selectedRating.value = result.data.user_rating || selectedRating.value
$toast.success(t('plugin.ratingSuccess', { name: props.plugin?.plugin_name }))
} else {
$toast.error(t('plugin.ratingFailed', { message: result.message || t('common.unknown') }))
}
} catch (error) {
console.error(error)
$toast.error(t('plugin.ratingFailed', { message: t('common.serverConnectionFailed') }))
} finally {
ratingSubmitting.value = false
}
}
watch(
() => [visible.value, props.plugin?.id],
([isVisible]) => {
if (isVisible) loadPluginRating()
},
{ immediate: true },
)
onUnmounted(() => {
closeInstallProgress()
versionHistoryDialogController?.close()
@@ -218,6 +289,16 @@ onUnmounted(() => {
<span class="text-body-1">{{ props.plugin?.system_version }}</span>
</VListItemTitle>
</VListItem>
<VListItem v-if="rating.rating_count > 0" class="ps-0">
<VListItemTitle class="plugin-market-detail-rating-row text-center text-md-left">
<span class="font-weight-medium">{{ t('plugin.rating') }}</span>
<PluginRatingDisplay
:rating="rating.average_rating"
:count="rating.rating_count"
:icon-size="18"
/>
</VListItemTitle>
</VListItem>
</VList>
<VAlert
v-if="props.plugin?.system_version_compatible === false"
@@ -230,6 +311,7 @@ onUnmounted(() => {
<div class="plugin-market-detail-actions">
<div class="plugin-market-detail-actions__buttons">
<VBtn
v-if="!isInstalled"
color="primary"
@click="installPlugin()"
prepend-icon="mdi-download"
@@ -237,6 +319,15 @@ onUnmounted(() => {
>
{{ t('plugin.installToLocal') }}
</VBtn>
<VBtn
v-else-if="props.plugin?.has_update"
color="primary"
prepend-icon="mdi-arrow-up-circle-outline"
:disabled="props.plugin?.system_version_compatible === false"
@click="installPlugin()"
>
{{ t('plugin.update') }}
</VBtn>
<VBtn variant="tonal" @click="showUpdateHistory" prepend-icon="mdi-update">
{{ t('plugin.versionHistory') }}
</VBtn>
@@ -246,6 +337,31 @@ onUnmounted(() => {
{{ t('plugin.totalDownloads', { count: formatDownloadCount(props.count) }) }}
</div>
</div>
<div v-if="isInstalled" class="plugin-market-detail-user-rating mt-5">
<div class="text-body-2 font-weight-medium mb-2">
{{ t('plugin.yourRating') }}
</div>
<div class="plugin-market-detail-user-rating__controls">
<VRating
v-model="selectedRating"
:disabled="ratingLoading || ratingSubmitting"
half-increments
hover
density="compact"
active-color="warning"
/>
<VBtn
size="small"
variant="tonal"
prepend-icon="mdi-star-check-outline"
:loading="ratingSubmitting"
:disabled="ratingLoading || selectedRating <= 0"
@click="submitPluginRating"
>
{{ t('plugin.submitRating') }}
</VBtn>
</div>
</div>
</VCardItem>
</div>
</div>
@@ -279,6 +395,27 @@ onUnmounted(() => {
text-align: center;
}
.plugin-market-detail-rating-row {
display: flex;
align-items: center;
justify-content: center;
gap: 0.25rem;
flex-wrap: wrap;
white-space: normal;
}
.plugin-market-detail-user-rating {
padding-block-start: 1rem;
border-block-start: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
}
.plugin-market-detail-user-rating__controls {
display: flex;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
}
@media (width >= 960px) {
.plugin-market-detail-actions {
justify-content: flex-start;
@@ -291,5 +428,9 @@ onUnmounted(() => {
.plugin-market-detail-actions__downloads {
text-align: start;
}
.plugin-market-detail-rating-row {
justify-content: flex-start;
}
}
</style>