Add tool publishing functionality to marketplace
This commit is contained in:
parent
026b2a5f50
commit
f7d9fa01d5
@ -156,6 +156,12 @@ const router = createRouter({
|
|||||||
name: 'ToolDetail',
|
name: 'ToolDetail',
|
||||||
component: () => import('../../views/dev/ToolDetail.vue')
|
component: () => import('../../views/dev/ToolDetail.vue')
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'publish-tool',
|
||||||
|
name: 'PublishTool',
|
||||||
|
component: () => import('../../views/dev/PublishTool.vue'),
|
||||||
|
meta: { requiresAuth: true }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'app-marketplace/:name',
|
path: 'app-marketplace/:name',
|
||||||
name: 'AppDetail',
|
name: 'AppDetail',
|
||||||
|
|||||||
@ -952,6 +952,7 @@
|
|||||||
"Environment restart request submitted. The system will restart shortly.": "环境重启请求已提交,系统将在稍后重启。",
|
"Environment restart request submitted. The system will restart shortly.": "环境重启请求已提交,系统将在稍后重启。",
|
||||||
"Failed to restart environment": "重启环境失败",
|
"Failed to restart environment": "重启环境失败",
|
||||||
"Publish App": "发布应用",
|
"Publish App": "发布应用",
|
||||||
|
"Publish Tool": "发布工具",
|
||||||
"My Published Apps": "已发布应用",
|
"My Published Apps": "已发布应用",
|
||||||
"Manage your published applications in the marketplace": "管理您在应用市场中发布的应用",
|
"Manage your published applications in the marketplace": "管理您在应用市场中发布的应用",
|
||||||
"View in Marketplace": "在市场查看",
|
"View in Marketplace": "在市场查看",
|
||||||
@ -1064,6 +1065,7 @@
|
|||||||
"Loading tools...": "加载工具中...",
|
"Loading tools...": "加载工具中...",
|
||||||
"Edit Tool": "编辑工具",
|
"Edit Tool": "编辑工具",
|
||||||
"Tool Name": "工具名称",
|
"Tool Name": "工具名称",
|
||||||
|
"Tool name": "工具名称",
|
||||||
"Enter tool name": "请输入工具名称",
|
"Enter tool name": "请输入工具名称",
|
||||||
"Enter tool description": "请输入工具描述",
|
"Enter tool description": "请输入工具描述",
|
||||||
"Enter category": "请输入分类",
|
"Enter category": "请输入分类",
|
||||||
|
|||||||
552
apps/jingrow/frontend/src/views/dev/PublishTool.vue
Normal file
552
apps/jingrow/frontend/src/views/dev/PublishTool.vue
Normal file
@ -0,0 +1,552 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tool-detail">
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="header-content">
|
||||||
|
<div class="header-text">
|
||||||
|
<h1>{{ t('Publish Tool') }}</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="handlePublishTool" :loading="publishing" size="medium">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon><Icon icon="tabler:upload" /></n-icon>
|
||||||
|
</template>
|
||||||
|
{{ t('Publish') }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tool-content">
|
||||||
|
<!-- 整体卡片布局 -->
|
||||||
|
<div class="tool-card">
|
||||||
|
<!-- 上部分:工具信息 -->
|
||||||
|
<div class="tool-info-section">
|
||||||
|
<div class="tool-content-layout">
|
||||||
|
<!-- 左侧:工具图标 -->
|
||||||
|
<div class="tool-image-section">
|
||||||
|
<div
|
||||||
|
class="tool-image"
|
||||||
|
@dragover.prevent="handleDragOver"
|
||||||
|
@dragenter.prevent="handleDragEnter"
|
||||||
|
@dragleave.prevent="handleDragLeave"
|
||||||
|
@drop.prevent="handleImageDrop"
|
||||||
|
@click="handleImageClick"
|
||||||
|
:class="{ 'drag-over': isDragOver }"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-if="imagePreview"
|
||||||
|
:src="imagePreview"
|
||||||
|
alt="工具图片预览"
|
||||||
|
/>
|
||||||
|
<div v-if="imagePreview" class="image-actions">
|
||||||
|
<n-button
|
||||||
|
type="error"
|
||||||
|
size="small"
|
||||||
|
circle
|
||||||
|
@click.stop="removeImage"
|
||||||
|
class="remove-btn"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<n-icon><Icon icon="tabler:x" /></n-icon>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
<div v-else class="placeholder-image">
|
||||||
|
<n-icon size="80"><Icon icon="tabler:tool" /></n-icon>
|
||||||
|
<p class="upload-hint">{{ t('Click or drag to upload') }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="isDragOver" class="drag-overlay">
|
||||||
|
<n-icon size="60"><Icon icon="tabler:upload" /></n-icon>
|
||||||
|
<p>{{ t('Drop image here') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 隐藏的文件输入 -->
|
||||||
|
<input
|
||||||
|
ref="fileInput"
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
style="display: none"
|
||||||
|
@change="handleFileSelect"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧:工具信息 -->
|
||||||
|
<div class="tool-info-content">
|
||||||
|
<div class="tool-header">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-list">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">{{ t('Title') }}:<span class="required">*</span></span>
|
||||||
|
<n-input v-model:value="form.title" :placeholder="t('Title')" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">{{ t('Tool Name') }}:<span class="required">*</span></span>
|
||||||
|
<n-input v-model:value="form.tool_name" :placeholder="t('Tool name')" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">{{ t('Subtitle') }}:</span>
|
||||||
|
<n-input
|
||||||
|
v-model:value="form.subtitle"
|
||||||
|
type="textarea"
|
||||||
|
:placeholder="t('Brief description')"
|
||||||
|
:rows="3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">{{ t('File URL') }}:</span>
|
||||||
|
<n-input v-model:value="form.file_url" :placeholder="t('File URL')" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">{{ t('Icon') }}:</span>
|
||||||
|
<IconPicker v-model="form.icon" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">{{ t('Color') }}:</span>
|
||||||
|
<n-color-picker v-model:value="form.color" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 下部分:描述 -->
|
||||||
|
<div class="tool-description-section">
|
||||||
|
<div class="description-content">
|
||||||
|
<Jeditor
|
||||||
|
:df="{ fieldname: 'description', label: t('Description'), fieldtype: 'Jeditor' }"
|
||||||
|
v-model="form.description"
|
||||||
|
:ctx="{ t }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { NButton, NInput, NIcon, NColorPicker, useMessage } from 'naive-ui'
|
||||||
|
import { Icon } from '@iconify/vue'
|
||||||
|
import { t } from '@/shared/i18n'
|
||||||
|
import axios from 'axios'
|
||||||
|
import Jeditor from '@/core/pagetype/form/controls/Jeditor.vue'
|
||||||
|
import IconPicker from '@/core/components/IconPicker.vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
const publishing = ref(false)
|
||||||
|
const imagePreview = ref('')
|
||||||
|
const uploadedImageUrl = ref('')
|
||||||
|
const isDragOver = ref(false)
|
||||||
|
const fileInput = ref<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
title: '',
|
||||||
|
tool_name: '',
|
||||||
|
subtitle: '',
|
||||||
|
file_url: '',
|
||||||
|
description: '',
|
||||||
|
icon: '',
|
||||||
|
color: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const goBack = () => {
|
||||||
|
router.push('/tool-marketplace')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleImageDrop = async (event: DragEvent) => {
|
||||||
|
isDragOver.value = false
|
||||||
|
const files = event.dataTransfer?.files
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
const file = files[0]
|
||||||
|
if (file.type.startsWith('image/')) {
|
||||||
|
await handleImageUpload({ file: { file } })
|
||||||
|
} else {
|
||||||
|
message.error('请选择图片文件')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDragOver = (event: DragEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDragEnter = (event: DragEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
isDragOver.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDragLeave = (event: DragEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
// 只有当离开整个拖拽区域时才设置为 false
|
||||||
|
const currentTarget = event.currentTarget as HTMLElement
|
||||||
|
const relatedTarget = event.relatedTarget as Node | null
|
||||||
|
if (!currentTarget.contains(relatedTarget)) {
|
||||||
|
isDragOver.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleImageClick = () => {
|
||||||
|
fileInput.value?.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFileSelect = async (event: Event) => {
|
||||||
|
const target = event.target as HTMLInputElement
|
||||||
|
const file = target.files?.[0]
|
||||||
|
if (file) {
|
||||||
|
await handleImageUpload({ file: { file } })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeImage = () => {
|
||||||
|
imagePreview.value = ''
|
||||||
|
uploadedImageUrl.value = ''
|
||||||
|
// 清空文件输入
|
||||||
|
if (fileInput.value) {
|
||||||
|
fileInput.value.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleImageUpload = async (options: any) => {
|
||||||
|
const { file, onFinish, onError } = options.file?.file ?
|
||||||
|
{ file: options.file.file, onFinish: options.onFinish, onError: options.onError } :
|
||||||
|
options.fileList?.length ?
|
||||||
|
{ file: options.fileList[0].file, onFinish: options.onFinish, onError: options.onError } :
|
||||||
|
options instanceof File ?
|
||||||
|
{ file: options, onFinish: null, onError: null } :
|
||||||
|
{ file: null, onFinish: null, onError: null }
|
||||||
|
|
||||||
|
if (!file || !(file instanceof File)) {
|
||||||
|
if (onError) onError(new Error('无效的文件'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建预览
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (e) => {
|
||||||
|
imagePreview.value = e.target?.result as string
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
|
||||||
|
const response = await axios.post('/jingrow/upload-image', formData, { timeout: 30000 })
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
uploadedImageUrl.value = response.data.url
|
||||||
|
message.success('图片上传成功')
|
||||||
|
if (onFinish) onFinish()
|
||||||
|
} else {
|
||||||
|
const errorMsg = response.data.error || '图片上传失败'
|
||||||
|
message.error(errorMsg)
|
||||||
|
if (onError) onError(new Error(errorMsg))
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
const errorMsg = error.response?.data?.detail || error.message || '图片上传失败'
|
||||||
|
message.error('图片上传失败: ' + errorMsg)
|
||||||
|
if (onError) onError(new Error(errorMsg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePublishTool = async () => {
|
||||||
|
if (!form.value.title || !form.value.tool_name) {
|
||||||
|
message.error('请填写工具标题和工具名称')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
publishing.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData()
|
||||||
|
Object.entries(form.value).forEach(([key, value]) => {
|
||||||
|
if (value) formData.append(key, value)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (uploadedImageUrl.value) {
|
||||||
|
formData.append('tool_image', uploadedImageUrl.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await axios.post('/jingrow/tool/publish', formData)
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
message.success('工具发布成功')
|
||||||
|
router.push('/tool-marketplace')
|
||||||
|
} else {
|
||||||
|
message.error('工具发布失败')
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.response?.data?.detail || '工具发布失败')
|
||||||
|
} finally {
|
||||||
|
publishing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.tool-detail {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 整体卡片布局 */
|
||||||
|
.tool-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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 上部分:工具信息 */
|
||||||
|
.tool-content-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 2fr;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-image-section {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-image {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 300px;
|
||||||
|
min-height: 300px;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
border: 2px dashed #d1d5db;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-image:hover {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
background-color: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-image.drag-over {
|
||||||
|
border-color: #10b981;
|
||||||
|
background-color: #f0fdf4;
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(16, 185, 129, 0.1);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #10b981;
|
||||||
|
font-weight: 500;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-overlay p {
|
||||||
|
margin: 8px 0 0 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-image img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-hint {
|
||||||
|
margin: 12px 0 0 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6b7280;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-actions {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-btn {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
background: rgba(239, 68, 68, 0.9);
|
||||||
|
border: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-btn:hover {
|
||||||
|
background: #ef4444;
|
||||||
|
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-btn .n-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-info-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-header {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
min-width: 100px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item .value {
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item .n-input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.required {
|
||||||
|
color: #ef4444;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 下部分:描述 */
|
||||||
|
.tool-description-section {
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
padding-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-content {
|
||||||
|
color: #6b7280;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Jeditor 样式调整 */
|
||||||
|
.description-content .field-wrapper {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-content .field-label {
|
||||||
|
display: none; /* 隐藏标签,因为已经有 h3 标题了 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-content .jeditor-control {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.tool-detail {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-content-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@ -6,6 +6,12 @@
|
|||||||
<h1>{{ t('Tool Marketplace') }}</h1>
|
<h1>{{ t('Tool Marketplace') }}</h1>
|
||||||
<p>{{ t('Browse and install tools from Jingrow Tool Marketplace') }}</p>
|
<p>{{ t('Browse and install tools from Jingrow Tool Marketplace') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<n-button type="primary" @click="publishTool">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon><Icon icon="tabler:plus" /></n-icon>
|
||||||
|
</template>
|
||||||
|
{{ t('Publish Tool') }}
|
||||||
|
</n-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -236,6 +242,10 @@ function handlePageSizeChange(newPageSize: number) {
|
|||||||
loadTools()
|
loadTools()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function publishTool() {
|
||||||
|
router.push('/publish-tool')
|
||||||
|
}
|
||||||
|
|
||||||
function viewToolDetail(tool: any) {
|
function viewToolDetail(tool: any) {
|
||||||
// 跳转到工具详情页面,传递返回路径
|
// 跳转到工具详情页面,传递返回路径
|
||||||
router.push({
|
router.push({
|
||||||
|
|||||||
@ -383,3 +383,79 @@ async def uninstall_tool(tool_name: str):
|
|||||||
logger.error(f"Traceback: {traceback.format_exc()}")
|
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||||
raise HTTPException(status_code=500, detail=f"卸载工具失败: {str(e)}")
|
raise HTTPException(status_code=500, detail=f"卸载工具失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/jingrow/tool/publish")
|
||||||
|
async def publish_tool_to_marketplace(
|
||||||
|
tool_name: str = Form(...),
|
||||||
|
title: str = Form(...),
|
||||||
|
subtitle: Optional[str] = Form(None),
|
||||||
|
description: Optional[str] = Form(None),
|
||||||
|
category: Optional[str] = Form(None),
|
||||||
|
file_url: Optional[str] = Form(None),
|
||||||
|
repository_url: Optional[str] = Form(None),
|
||||||
|
tool_image: Optional[str] = Form(None),
|
||||||
|
icon: Optional[str] = Form(None),
|
||||||
|
color: Optional[str] = Form(None)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
发布工具到Jingrow Cloud工具市场
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 如果file_url不是绝对地址,拼接完整的服务器地址
|
||||||
|
if file_url and not file_url.startswith('http://') and not file_url.startswith('https://'):
|
||||||
|
from jingrow.config import Config
|
||||||
|
# 确保URL以 / 开头
|
||||||
|
if not file_url.startswith('/'):
|
||||||
|
file_url = '/' + file_url
|
||||||
|
|
||||||
|
# 使用Config中的服务器URL
|
||||||
|
server_url = Config.jingrow_server_url
|
||||||
|
# 确保服务器URL不以 / 结尾
|
||||||
|
server_url = server_url.rstrip('/')
|
||||||
|
file_url = f"{server_url}{file_url}"
|
||||||
|
|
||||||
|
url = f"{get_jingrow_cloud_url()}/api/action/jcloud.api.jlocal.create_local_tool"
|
||||||
|
|
||||||
|
headers = get_jingrow_cloud_api_headers()
|
||||||
|
headers['Content-Type'] = 'application/json'
|
||||||
|
|
||||||
|
response = requests.post(url, json={
|
||||||
|
"tool_data": {
|
||||||
|
"tool_name": tool_name,
|
||||||
|
"title": title,
|
||||||
|
"subtitle": subtitle or "",
|
||||||
|
"description": description or "",
|
||||||
|
"category": category or "",
|
||||||
|
"file_url": file_url,
|
||||||
|
"repository_url": repository_url,
|
||||||
|
"tool_image": tool_image,
|
||||||
|
"icon": icon,
|
||||||
|
"color": color
|
||||||
|
}
|
||||||
|
}, headers=headers, timeout=20)
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
error_detail = response.json().get('detail', f"HTTP {response.status_code}") if response.headers.get('content-type', '').startswith('application/json') else f"HTTP {response.status_code}"
|
||||||
|
raise HTTPException(status_code=response.status_code, detail=error_detail)
|
||||||
|
|
||||||
|
result = response.json()
|
||||||
|
|
||||||
|
# 检查错误
|
||||||
|
if isinstance(result, dict) and result.get('error'):
|
||||||
|
raise HTTPException(status_code=400, detail=result['error'])
|
||||||
|
|
||||||
|
message = result.get('message', {})
|
||||||
|
if isinstance(message, dict) and message.get('error'):
|
||||||
|
raise HTTPException(status_code=400, detail=message['error'])
|
||||||
|
|
||||||
|
# 成功响应
|
||||||
|
tool_name_result = message.get('name', 'unknown') if isinstance(message, dict) else result.get('message', 'unknown')
|
||||||
|
return {"success": True, "message": f"工具发布成功,工具名称: {tool_name_result}"}
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"发布工具失败: {str(e)}")
|
||||||
|
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"发布工具失败: {str(e)}")
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user