更新已发布应用列表页和翻译
This commit is contained in:
parent
f92aa40510
commit
ed4e11872b
@ -14,6 +14,7 @@
|
|||||||
"Published": "已发布",
|
"Published": "已发布",
|
||||||
"Unpublished": "未发布",
|
"Unpublished": "未发布",
|
||||||
"Draft": "草稿",
|
"Draft": "草稿",
|
||||||
|
"Pending Review": "待审核",
|
||||||
"Dashboard": "仪表板",
|
"Dashboard": "仪表板",
|
||||||
"Agents": "AI智能体",
|
"Agents": "AI智能体",
|
||||||
"Settings": "设置",
|
"Settings": "设置",
|
||||||
|
|||||||
@ -75,9 +75,14 @@
|
|||||||
<div class="app-header">
|
<div class="app-header">
|
||||||
<div class="app-title-section">
|
<div class="app-title-section">
|
||||||
<h3 @click="viewAppDetail(app)" class="clickable-title">{{ app.title || app.name }}</h3>
|
<h3 @click="viewAppDetail(app)" class="clickable-title">{{ app.title || app.name }}</h3>
|
||||||
<div class="app-team" v-if="app.team">
|
<div class="app-meta">
|
||||||
<n-icon><Icon icon="tabler:users" /></n-icon>
|
<div class="app-team" v-if="app.team">
|
||||||
<span>{{ app.team }}</span>
|
<n-icon><Icon icon="tabler:users" /></n-icon>
|
||||||
|
<span>{{ app.team }}</span>
|
||||||
|
</div>
|
||||||
|
<span v-if="app.status" class="status-badge" :class="getStatusClass(app.status)">
|
||||||
|
{{ t(app.status) }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="app-name" v-if="app.app_name">
|
<div class="app-name" v-if="app.app_name">
|
||||||
@ -95,10 +100,18 @@
|
|||||||
{{ t('View Details') }}
|
{{ t('View Details') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button
|
<n-button
|
||||||
type="info"
|
v-if="isAppInstalled(app.app_name || app.name)"
|
||||||
@click="viewInMarketplace(app)"
|
type="warning"
|
||||||
|
@click="installApp(app)"
|
||||||
>
|
>
|
||||||
{{ t('View in Marketplace') }}
|
{{ t('Update') }}
|
||||||
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
v-else
|
||||||
|
type="primary"
|
||||||
|
@click="installApp(app)"
|
||||||
|
>
|
||||||
|
{{ t('Install') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -138,18 +151,30 @@
|
|||||||
</n-empty>
|
</n-empty>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 安装进度弹窗 -->
|
||||||
|
<InstallProgressModal
|
||||||
|
v-model="showProgressModal"
|
||||||
|
:progress="installProgress"
|
||||||
|
:message="installMessage"
|
||||||
|
:status="installStatus"
|
||||||
|
:installing="installing"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, computed, watch } from 'vue'
|
import { ref, onMounted, computed, watch } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { NInput, NButton, NIcon, NSpin, NEmpty, NSelect, NPagination, useMessage } from 'naive-ui'
|
import { NInput, NButton, NIcon, NSpin, NEmpty, NSelect, NPagination, useMessage, useDialog } from 'naive-ui'
|
||||||
import { Icon } from '@iconify/vue'
|
import { Icon } from '@iconify/vue'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { t } from '@/shared/i18n'
|
import { t } from '@/shared/i18n'
|
||||||
|
import { get_session_api_headers } from '@/shared/api/auth'
|
||||||
|
import InstallProgressModal from './InstallProgressModal.vue'
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
const dialog = useDialog()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
@ -160,6 +185,16 @@ const page = ref(1)
|
|||||||
const pageSize = ref(parseInt(localStorage.getItem('itemsPerPage') || '20'))
|
const pageSize = ref(parseInt(localStorage.getItem('itemsPerPage') || '20'))
|
||||||
const sortBy = ref('creation desc')
|
const sortBy = ref('creation desc')
|
||||||
|
|
||||||
|
// 安装相关状态
|
||||||
|
const installing = ref(false)
|
||||||
|
const installProgress = ref(0)
|
||||||
|
const installMessage = ref('')
|
||||||
|
const installStatus = ref<'success' | 'error' | 'info'>('info')
|
||||||
|
const showProgressModal = ref(false)
|
||||||
|
|
||||||
|
// 已安装应用集合
|
||||||
|
const installedAppNames = ref<Set<string>>(new Set())
|
||||||
|
|
||||||
// 排序选项
|
// 排序选项
|
||||||
const sortOptions = computed(() => [
|
const sortOptions = computed(() => [
|
||||||
{ label: t('Latest'), value: 'creation desc' },
|
{ label: t('Latest'), value: 'creation desc' },
|
||||||
@ -223,10 +258,6 @@ function viewAppDetail(app: any) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function viewInMarketplace(app: any) {
|
|
||||||
// 跳转到应用市场查看该应用
|
|
||||||
router.push(`/app-marketplace?app=${app.name}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getImageUrl(imageUrl: string): string {
|
function getImageUrl(imageUrl: string): string {
|
||||||
if (!imageUrl) return ''
|
if (!imageUrl) return ''
|
||||||
@ -250,8 +281,157 @@ function truncateText(text: string, maxLength: number): string {
|
|||||||
return text.substring(0, maxLength) + '...'
|
return text.substring(0, maxLength) + '...'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getStatusClass(status: string): string {
|
||||||
|
if (!status) return ''
|
||||||
|
// 将状态转换为小写,并将空格替换为连字符
|
||||||
|
return status.toLowerCase().replace(/\s+/g, '-')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function installApp(app: any) {
|
||||||
|
if (!app.file_url && !app.repository_url) {
|
||||||
|
message.error(t('应用文件URL或仓库地址不存在'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先检查应用是否已存在
|
||||||
|
try {
|
||||||
|
const appName = app.app_name || app.name
|
||||||
|
if (appName) {
|
||||||
|
const checkResponse = await axios.get(`/jingrow/check-app/${appName}`)
|
||||||
|
|
||||||
|
if (checkResponse.data.exists) {
|
||||||
|
// 显示确认对话框
|
||||||
|
dialog.warning({
|
||||||
|
title: t('应用已存在'),
|
||||||
|
content: t('应用 "{0}" 已安装,是否覆盖安装?').replace('{0}', appName),
|
||||||
|
positiveText: t('确认覆盖'),
|
||||||
|
negativeText: t('取消'),
|
||||||
|
onPositiveClick: () => {
|
||||||
|
performInstall(app)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Check app exists error:', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
performInstall(app)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function performInstall(app: any) {
|
||||||
|
try {
|
||||||
|
installing.value = true
|
||||||
|
installProgress.value = 0
|
||||||
|
installMessage.value = t('正在准备安装...')
|
||||||
|
installStatus.value = 'info'
|
||||||
|
showProgressModal.value = true
|
||||||
|
|
||||||
|
let response
|
||||||
|
|
||||||
|
// 优先使用文件URL,否则使用git仓库
|
||||||
|
if (app.file_url) {
|
||||||
|
installMessage.value = t('正在下载应用包...')
|
||||||
|
setTimeout(() => {
|
||||||
|
installProgress.value = 20
|
||||||
|
}, 300)
|
||||||
|
|
||||||
|
installProgress.value = 30
|
||||||
|
installMessage.value = t('正在安装应用...')
|
||||||
|
|
||||||
|
response = await axios.post('/jingrow/install-from-url', new URLSearchParams({
|
||||||
|
url: app.file_url
|
||||||
|
}), {
|
||||||
|
headers: {
|
||||||
|
...get_session_api_headers(),
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if (app.repository_url) {
|
||||||
|
installMessage.value = t('正在克隆仓库...')
|
||||||
|
setTimeout(() => {
|
||||||
|
installProgress.value = 20
|
||||||
|
}, 300)
|
||||||
|
|
||||||
|
installProgress.value = 30
|
||||||
|
installMessage.value = t('正在安装应用...')
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
repo_url: app.repository_url
|
||||||
|
})
|
||||||
|
|
||||||
|
response = await axios.post('/jingrow/install-from-git', params, {
|
||||||
|
headers: {
|
||||||
|
...get_session_api_headers(),
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
throw new Error(t('无法确定安装方式'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新进度到安装完成
|
||||||
|
installProgress.value = 100
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
// 所有步骤完成后才显示成功
|
||||||
|
installing.value = false
|
||||||
|
installStatus.value = 'success'
|
||||||
|
installMessage.value = t('应用安装成功!')
|
||||||
|
message.success(t('应用安装成功'))
|
||||||
|
|
||||||
|
// 刷新已安装应用列表
|
||||||
|
loadInstalledApps()
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
showProgressModal.value = false
|
||||||
|
}, 2000)
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.error || t('安装失败'))
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Install app error:', error)
|
||||||
|
installing.value = false
|
||||||
|
installStatus.value = 'error'
|
||||||
|
installMessage.value = error.response?.data?.detail || error.message || t('安装失败')
|
||||||
|
message.error(error.response?.data?.detail || t('安装失败'))
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
showProgressModal.value = false
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载已安装应用列表
|
||||||
|
async function loadInstalledApps() {
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/jingrow/installed-app-names')
|
||||||
|
if (response.data.success) {
|
||||||
|
const apps = response.data.apps || []
|
||||||
|
installedAppNames.value = new Set(apps)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Load installed apps error:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查应用是否已安装
|
||||||
|
function isAppInstalled(appName: string): boolean {
|
||||||
|
if (!appName) return false
|
||||||
|
return installedAppNames.value.has(appName.toLowerCase())
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadApps()
|
loadApps()
|
||||||
|
loadInstalledApps()
|
||||||
|
|
||||||
|
// 监听全局事件
|
||||||
|
window.addEventListener('installedAppsUpdated', () => {
|
||||||
|
loadInstalledApps()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// 监听搜索和排序变化
|
// 监听搜索和排序变化
|
||||||
@ -474,6 +654,14 @@ watch(() => localStorage.getItem('itemsPerPage'), (newValue) => {
|
|||||||
color: #10b981;
|
color: #10b981;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.app-team {
|
.app-team {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -488,6 +676,49 @@ watch(() => localStorage.getItem('itemsPerPage'), (newValue) => {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.published {
|
||||||
|
background: #d1fae5;
|
||||||
|
color: #065f46;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.unpublished {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.draft {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.active {
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #1e40af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.inactive {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.pending {
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #1e40af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.pending-review {
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #1e40af;
|
||||||
|
}
|
||||||
|
|
||||||
.app-name {
|
.app-name {
|
||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user