2025-10-24 23:10:22 +08:00

446 lines
9.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="app-detail">
<div class="page-header">
<div class="header-content">
<div class="header-text">
<h1>{{ app?.title || t('App Details') }}</h1>
</div>
<div class="header-actions">
<n-button @click="goBack" size="medium">
<template #icon>
<n-icon><Icon icon="tabler:arrow-left" /></n-icon>
</template>
{{ t('Back') }}
</n-button>
<n-button type="primary" @click="installApp" size="medium">
<template #icon>
<n-icon><Icon icon="tabler:download" /></n-icon>
</template>
{{ t('Install') }}
</n-button>
</div>
</div>
</div>
<div v-if="loading" class="loading-container">
<n-spin size="large">
<template #description>
{{ t('Loading application details...') }}
</template>
</n-spin>
</div>
<div v-else-if="error" class="error-container">
<n-empty :description="error">
<template #icon>
<n-icon><Icon icon="tabler:alert-circle" /></n-icon>
</template>
</n-empty>
</div>
<div v-else-if="app" class="app-content">
<!-- 整体卡片布局 -->
<div class="app-card">
<!-- 上部分应用信息 -->
<div class="app-info-section">
<div class="app-content-layout">
<!-- 左侧应用图片 -->
<div class="app-image-section">
<div class="app-image">
<img
v-if="app.app_image"
:src="getImageUrl(app.app_image)"
:alt="app.title"
@error="handleImageError"
/>
<div v-else class="placeholder-image">
<n-icon size="80"><Icon icon="tabler:apps" /></n-icon>
</div>
</div>
</div>
<!-- 右侧应用信息 -->
<div class="app-info-content">
<div class="app-header">
<h2 class="app-title">{{ app.title || t('Untitled App') }}</h2>
<div v-if="app.subtitle" class="app-subtitle">{{ app.subtitle }}</div>
</div>
<div class="info-list">
<div v-if="app.app_name" class="info-item">
<span class="label">{{ t('App Name') }}:</span>
<span class="value">{{ app.app_name }}</span>
</div>
<div v-if="app.team" class="info-item">
<span class="label">{{ t('Team') }}:</span>
<span class="value">{{ app.team }}</span>
</div>
<div v-if="app.repository_url" class="info-item">
<span class="label">{{ t('Repository URL') }}:</span>
<a :href="app.repository_url" target="_blank" class="link">
{{ app.repository_url }}
</a>
</div>
<div v-if="app.file_url" class="info-item">
<span class="label">{{ t('File URL') }}:</span>
<a :href="app.file_url" target="_blank" class="link">
{{ app.file_url }}
</a>
</div>
<div v-if="app.creation" class="info-item">
<span class="label">{{ t('Created') }}:</span>
<span class="value">{{ formatDate(app.creation) }}</span>
</div>
<div v-if="app.modified" class="info-item">
<span class="label">{{ t('Last Updated') }}:</span>
<span class="value">{{ formatDate(app.modified) }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 下部分描述内容 -->
<div v-if="app.description" class="description-section">
<h3>{{ t('Description') }}</h3>
<div class="description-content" v-html="app.description"></div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { NButton, NIcon, NSpin, NEmpty, useMessage } from 'naive-ui'
import { Icon } from '@iconify/vue'
import axios from 'axios'
import { t } from '@/shared/i18n'
const route = useRoute()
const router = useRouter()
const message = useMessage()
const loading = ref(true)
const error = ref('')
const app = ref<any>(null)
const appName = computed(() => route.params.name as string)
async function loadAppDetail() {
loading.value = true
error.value = ''
try {
const response = await axios.get(`/jingrow/app-marketplace/${appName.value}`)
app.value = response.data
} catch (err: any) {
console.error('Failed to load app detail:', err)
error.value = err.response?.data?.detail || t('Failed to load application details')
} finally {
loading.value = false
}
}
function formatDate(dateString: string): string {
if (!dateString) return ''
const date = new Date(dateString)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
function getImageUrl(url: string) {
if (!url) return ''
if (url.startsWith('http')) return url
return `https://cloud.jingrow.com${url}`
}
function handleImageError(event: Event) {
const img = event.target as HTMLImageElement
img.style.display = 'none'
}
function goBack() {
router.push('/app-marketplace')
}
function installApp() {
// TODO: 实现安装应用
message.info(t('Install feature coming soon'))
}
onMounted(() => {
loadAppDetail()
})
</script>
<style scoped>
.app-detail {
padding: 24px;
}
.page-header {
margin-bottom: 32px;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-actions {
display: flex;
align-items: center;
gap: 30px;
}
.header-text h1 {
margin: 0 0 8px 0;
font-size: 28px;
font-weight: 700;
color: #1a1a1a;
}
.header-text p {
margin: 0;
color: #666;
font-size: 16px;
}
.loading-container,
.error-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
}
.app-content {
display: flex;
flex-direction: column;
gap: 32px;
}
/* 整体卡片布局 */
.app-card {
background: white;
border-radius: 8px;
padding: 24px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
gap: 24px;
}
/* 上部分:应用信息 */
.app-content-layout {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 50px;
align-items: start;
}
.app-image-section {
display: flex;
justify-content: center;
align-items: center;
}
.app-image {
width: 100%;
min-height: 300px;
border-radius: 8px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.app-image img {
width: 100%;
height: auto;
object-fit: contain;
}
.placeholder-image {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #9ca3af;
}
.app-header {
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid #e5e7eb;
}
.app-title {
margin: 0;
font-size: 22px;
font-weight: 600;
color: #1a1a1a;
}
.app-subtitle {
margin: 8px 0 0 0;
font-size: 14px;
color: #6b7280;
}
.info-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.info-item {
display: flex;
align-items: center;
gap: 12px;
}
.info-item .label {
font-weight: 500;
color: #374151;
font-size: 14px;
min-width: 80px;
}
.info-item .value {
color: #6b7280;
font-size: 14px;
}
.link {
color: #2563eb;
text-decoration: none;
}
.link:hover {
text-decoration: underline;
}
.text-muted {
color: #9ca3af;
font-style: italic;
}
/* 下部分:描述内容 */
.description-section {
padding-top: 24px;
border-top: 1px solid #e5e7eb;
}
.description-section h3 {
margin: 0 0 20px 0;
font-size: 20px;
font-weight: 600;
color: #1f2937;
}
.description-content {
color: #374151;
line-height: 1.6;
}
.description-content :deep(h1),
.description-content :deep(h2),
.description-content :deep(h3),
.description-content :deep(h4),
.description-content :deep(h5),
.description-content :deep(h6) {
margin: 16px 0 8px 0;
color: #1f2937;
}
.description-content :deep(p) {
margin: 8px 0;
}
.description-content :deep(ul),
.description-content :deep(ol) {
margin: 8px 0;
padding-left: 20px;
}
.description-content :deep(li) {
margin: 4px 0;
}
.description-content :deep(code) {
background: #f3f4f6;
padding: 2px 6px;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 14px;
}
.description-content :deep(pre) {
background: #f3f4f6;
padding: 16px;
border-radius: 8px;
overflow-x: auto;
margin: 16px 0;
}
.description-content :deep(blockquote) {
border-left: 4px solid #e5e7eb;
padding-left: 16px;
margin: 16px 0;
color: #6b7280;
font-style: italic;
}
@media (max-width: 768px) {
.app-card {
padding: 20px;
}
.app-content-layout {
grid-template-columns: 1fr;
gap: 30px;
}
.app-image-section {
order: 2;
justify-content: center;
}
.app-image {
max-width: 150px;
}
.app-title {
font-size: 20px;
}
.info-item {
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.info-item .label {
min-width: auto;
}
.header-content {
flex-direction: column;
gap: 16px;
}
}
</style>