feat: Add "My Published Tools" page in Development menu
- Add MyPublishedTools component aligned with MyPublishedApps functionality - Add backend API endpoints: - GET /jingrow/my-published-tools (list published tools with search, pagination, sorting) - POST /jingrow/delete-published-tool (delete published tool) - Add route configuration for my-published-tools page - Add menu item "My Published Tools" under Development group - Add Chinese translations for all tool-related text - Remove uppercase transformation for tool_name display in list The new page provides the same functionality as "My Published Apps": - Search and filter tools - Sort by various criteria - Pagination support - View tool details - Delete published tools - Publish new tool button
This commit is contained in:
parent
964f65dc1d
commit
f702b8596a
@ -185,6 +185,12 @@ const router = createRouter({
|
|||||||
name: 'MyPublishedAgents',
|
name: 'MyPublishedAgents',
|
||||||
component: () => import('../../views/dev/MyPublishedAgents.vue'),
|
component: () => import('../../views/dev/MyPublishedAgents.vue'),
|
||||||
meta: { requiresAuth: true }
|
meta: { requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'my-published-tools',
|
||||||
|
name: 'MyPublishedTools',
|
||||||
|
component: () => import('../../views/dev/MyPublishedTools.vue'),
|
||||||
|
meta: { requiresAuth: true }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1048,14 +1048,20 @@
|
|||||||
|
|
||||||
"My Published Nodes": "已发布节点",
|
"My Published Nodes": "已发布节点",
|
||||||
"My Published Agents": "已发布智能体",
|
"My Published Agents": "已发布智能体",
|
||||||
|
"My Published Tools": "已发布工具",
|
||||||
"Manage your published nodes in the marketplace": "管理您在市场中发布的节点",
|
"Manage your published nodes in the marketplace": "管理您在市场中发布的节点",
|
||||||
"Manage your published agents in the marketplace": "管理您在市场中发布的智能体",
|
"Manage your published agents in the marketplace": "管理您在市场中发布的智能体",
|
||||||
|
"Manage your published tools in the marketplace": "管理您在工具市场中发布的工具",
|
||||||
"Node name does not exist": "节点名称不存在",
|
"Node name does not exist": "节点名称不存在",
|
||||||
"Agent name does not exist": "智能体名称不存在",
|
"Agent name does not exist": "智能体名称不存在",
|
||||||
"Are you sure you want to delete node \"{0}\"? This action cannot be undone.": "确定要删除节点 \"{0}\" 吗?此操作不可恢复。",
|
"Are you sure you want to delete node \"{0}\"? This action cannot be undone.": "确定要删除节点 \"{0}\" 吗?此操作不可恢复。",
|
||||||
"Are you sure you want to delete agent \"{0}\"? This action cannot be undone.": "确定要删除智能体 \"{0}\" 吗?此操作不可恢复。",
|
"Are you sure you want to delete agent \"{0}\"? This action cannot be undone.": "确定要删除智能体 \"{0}\" 吗?此操作不可恢复。",
|
||||||
"Node deleted successfully": "节点删除成功",
|
"Node deleted successfully": "节点删除成功",
|
||||||
"Agent deleted successfully": "智能体删除成功",
|
"Agent deleted successfully": "智能体删除成功",
|
||||||
|
"Tool name does not exist": "工具名称不存在",
|
||||||
|
"Tool deleted successfully": "工具删除成功",
|
||||||
|
"Are you sure you want to delete tool \"{0}\"? This action cannot be undone.": "确定要删除工具 \"{0}\" 吗?此操作不可恢复。",
|
||||||
|
"Publish Your First Tool": "发布您的第一个工具",
|
||||||
|
|
||||||
"Tools": "工具",
|
"Tools": "工具",
|
||||||
"Add Tool": "添加工具",
|
"Add Tool": "添加工具",
|
||||||
|
|||||||
@ -65,6 +65,7 @@ function getDefaultMenus(): AppMenuItem[] {
|
|||||||
{ id: 'my-published-apps', key: 'MyPublishedApps', label: 'My Published Apps', icon: 'tabler:cloud-upload', type: 'route', routeName: 'MyPublishedApps', parentId: 'dev-group', order: 7 },
|
{ id: 'my-published-apps', key: 'MyPublishedApps', label: 'My Published Apps', icon: 'tabler:cloud-upload', type: 'route', routeName: 'MyPublishedApps', parentId: 'dev-group', order: 7 },
|
||||||
{ id: 'my-published-nodes', key: 'MyPublishedNodes', label: 'My Published Nodes', icon: 'carbon:add-child-node', type: 'route', routeName: 'MyPublishedNodes', parentId: 'dev-group', order: 7.1 },
|
{ id: 'my-published-nodes', key: 'MyPublishedNodes', label: 'My Published Nodes', icon: 'carbon:add-child-node', type: 'route', routeName: 'MyPublishedNodes', parentId: 'dev-group', order: 7.1 },
|
||||||
{ id: 'my-published-agents', key: 'MyPublishedAgents', label: 'My Published Agents', icon: 'hugeicons:robotic', type: 'route', routeName: 'MyPublishedAgents', parentId: 'dev-group', order: 7.2 },
|
{ id: 'my-published-agents', key: 'MyPublishedAgents', label: 'My Published Agents', icon: 'hugeicons:robotic', type: 'route', routeName: 'MyPublishedAgents', parentId: 'dev-group', order: 7.2 },
|
||||||
|
{ id: 'my-published-tools', key: 'MyPublishedTools', label: 'My Published Tools', icon: 'tabler:tool', type: 'route', routeName: 'MyPublishedTools', parentId: 'dev-group', order: 7.3 },
|
||||||
{ id: 'app-marketplace', key: 'AppMarketplace', label: 'App Marketplace', icon: 'tabler:shopping-cart', type: 'route', routeName: 'AppMarketplace', parentId: 'dev-group', order: 7.5 },
|
{ id: 'app-marketplace', key: 'AppMarketplace', label: 'App Marketplace', icon: 'tabler:shopping-cart', type: 'route', routeName: 'AppMarketplace', parentId: 'dev-group', order: 7.5 },
|
||||||
{ id: 'node-marketplace', key: 'NodeMarketplace', label: 'Node Marketplace', icon: 'carbon:add-child-node', type: 'route', routeName: 'NodeMarketplace', parentId: 'dev-group', order: 8 },
|
{ id: 'node-marketplace', key: 'NodeMarketplace', label: 'Node Marketplace', icon: 'carbon:add-child-node', type: 'route', routeName: 'NodeMarketplace', parentId: 'dev-group', order: 8 },
|
||||||
{ id: 'agent-marketplace', key: 'AgentMarketplace', label: 'Agent Marketplace', icon: 'hugeicons:robotic', type: 'route', routeName: 'AgentMarketplace', parentId: 'dev-group', order: 8.5 },
|
{ id: 'agent-marketplace', key: 'AgentMarketplace', label: 'Agent Marketplace', icon: 'hugeicons:robotic', type: 'route', routeName: 'AgentMarketplace', parentId: 'dev-group', order: 8.5 },
|
||||||
|
|||||||
687
apps/jingrow/frontend/src/views/dev/MyPublishedTools.vue
Normal file
687
apps/jingrow/frontend/src/views/dev/MyPublishedTools.vue
Normal file
@ -0,0 +1,687 @@
|
|||||||
|
<template>
|
||||||
|
<div class="my-published-tools">
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="header-content">
|
||||||
|
<div class="header-text">
|
||||||
|
<h1>{{ t('My Published Tools') }}</h1>
|
||||||
|
<p>{{ t('Manage your published tools in the marketplace') }}</p>
|
||||||
|
</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 class="content">
|
||||||
|
<div class="search-container">
|
||||||
|
<div class="search-bar">
|
||||||
|
<n-input
|
||||||
|
v-model:value="searchQuery"
|
||||||
|
:placeholder="t('Search tools...')"
|
||||||
|
clearable
|
||||||
|
size="large"
|
||||||
|
@keyup.enter="loadTools"
|
||||||
|
class="search-input"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<n-icon><Icon icon="tabler:search" /></n-icon>
|
||||||
|
</template>
|
||||||
|
</n-input>
|
||||||
|
<n-button type="primary" size="large" @click="loadTools" class="search-button">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon><Icon icon="tabler:search" /></n-icon>
|
||||||
|
</template>
|
||||||
|
{{ t('Search') }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tools-section" v-if="!loading && tools.length > 0">
|
||||||
|
<!-- 排序控件 -->
|
||||||
|
<div class="tools-header">
|
||||||
|
<div class="tools-title">
|
||||||
|
</div>
|
||||||
|
<div class="sort-controls">
|
||||||
|
<n-select
|
||||||
|
v-model:value="sortBy"
|
||||||
|
:options="sortOptions"
|
||||||
|
:placeholder="t('Sort by')"
|
||||||
|
style="width: 150px"
|
||||||
|
@update:value="loadTools"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tools-grid">
|
||||||
|
<div v-for="tool in tools" :key="tool.name" class="tool-card">
|
||||||
|
<!-- 工具图片 -->
|
||||||
|
<div class="tool-image" @click="viewToolDetail(tool)">
|
||||||
|
<img
|
||||||
|
v-if="tool.tool_image"
|
||||||
|
:src="getImageUrl(tool.tool_image)"
|
||||||
|
:alt="tool.title || tool.name"
|
||||||
|
@error="handleImageError"
|
||||||
|
/>
|
||||||
|
<div v-else class="tool-image-placeholder">
|
||||||
|
<n-icon size="48"><Icon icon="tabler:tool" /></n-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 工具信息 -->
|
||||||
|
<div class="tool-content">
|
||||||
|
<div class="tool-header">
|
||||||
|
<div class="tool-title-section">
|
||||||
|
<h3 @click="viewToolDetail(tool)" class="clickable-title">{{ tool.title || tool.name }}</h3>
|
||||||
|
<div class="tool-meta">
|
||||||
|
<div class="tool-team" v-if="tool.team">
|
||||||
|
<n-icon><Icon icon="tabler:users" /></n-icon>
|
||||||
|
<span>{{ tool.team }}</span>
|
||||||
|
</div>
|
||||||
|
<span v-if="tool.status" class="status-badge" :class="getStatusClass(tool.status)">
|
||||||
|
{{ t(tool.status) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tool-name" v-if="tool.tool_name">
|
||||||
|
{{ tool.tool_name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tool-subtitle" v-if="tool.subtitle">
|
||||||
|
{{ truncateText(tool.subtitle, 60) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tool-actions">
|
||||||
|
<n-button type="default" @click="viewToolDetail(tool)">
|
||||||
|
{{ t('View Details') }}
|
||||||
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
type="error"
|
||||||
|
@click="deleteTool(tool)"
|
||||||
|
>
|
||||||
|
{{ t('Delete') }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-container">
|
||||||
|
<n-pagination
|
||||||
|
v-model:page="page"
|
||||||
|
:page-count="pageCount"
|
||||||
|
size="large"
|
||||||
|
show-size-picker
|
||||||
|
:page-sizes="[20, 50, 100]"
|
||||||
|
:page-size="pageSize"
|
||||||
|
@update:page="loadTools"
|
||||||
|
@update:page-size="handlePageSizeChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="loading" class="loading">
|
||||||
|
<n-spin size="large">
|
||||||
|
<template #description>{{ t('Loading tools...') }}</template>
|
||||||
|
</n-spin>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!loading && tools.length === 0" class="empty">
|
||||||
|
<n-empty :description="t('No tools found')">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon><Icon icon="tabler:tool" /></n-icon>
|
||||||
|
</template>
|
||||||
|
<template #extra>
|
||||||
|
<n-button type="primary" @click="publishTool">
|
||||||
|
{{ t('Publish Your First Tool') }}
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
</n-empty>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, computed, watch } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { NInput, NButton, NIcon, NSpin, NEmpty, NSelect, NPagination, useMessage, useDialog } from 'naive-ui'
|
||||||
|
import { Icon } from '@iconify/vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { t } from '@/shared/i18n'
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
const dialog = useDialog()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const searchQuery = ref('')
|
||||||
|
const loading = ref(false)
|
||||||
|
const tools = ref<any[]>([])
|
||||||
|
const total = ref(0)
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = ref(parseInt(localStorage.getItem('itemsPerPage') || '20'))
|
||||||
|
const sortBy = ref('creation desc')
|
||||||
|
|
||||||
|
// 排序选项
|
||||||
|
const sortOptions = computed(() => [
|
||||||
|
{ label: t('Latest'), value: 'creation desc' },
|
||||||
|
{ label: t('Oldest'), value: 'creation asc' },
|
||||||
|
{ label: t('Name A-Z'), value: 'tool_name asc' },
|
||||||
|
{ label: t('Name Z-A'), value: 'tool_name desc' },
|
||||||
|
{ label: t('Most Popular'), value: 'modified desc' }
|
||||||
|
])
|
||||||
|
|
||||||
|
// 计算总页数
|
||||||
|
const pageCount = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)))
|
||||||
|
|
||||||
|
async function loadTools() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
page: page.value.toString(),
|
||||||
|
page_size: pageSize.value.toString(),
|
||||||
|
search: searchQuery.value,
|
||||||
|
sort_by: sortBy.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await axios.get(`/jingrow/my-published-tools?${params}`)
|
||||||
|
const data = response.data
|
||||||
|
|
||||||
|
// 如果API返回分页数据
|
||||||
|
if (data.items) {
|
||||||
|
tools.value = data.items
|
||||||
|
total.value = data.total || 0
|
||||||
|
} else {
|
||||||
|
// 兼容旧API格式
|
||||||
|
tools.value = data || []
|
||||||
|
total.value = tools.value.length
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load tools:', error)
|
||||||
|
message.error(t('Failed to load tools'))
|
||||||
|
tools.value = []
|
||||||
|
total.value = 0
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function publishTool() {
|
||||||
|
router.push('/publish-tool')
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePageSizeChange(newPageSize: number) {
|
||||||
|
pageSize.value = newPageSize
|
||||||
|
page.value = 1
|
||||||
|
localStorage.setItem('itemsPerPage', newPageSize.toString())
|
||||||
|
loadTools()
|
||||||
|
}
|
||||||
|
|
||||||
|
function viewToolDetail(tool: any) {
|
||||||
|
// 跳转到工具详情页面,传递返回路径
|
||||||
|
router.push({
|
||||||
|
path: `/tool-marketplace/${tool.name}`,
|
||||||
|
query: { returnTo: '/my-published-tools' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getImageUrl(imageUrl: string): string {
|
||||||
|
if (!imageUrl) return ''
|
||||||
|
if (imageUrl.startsWith('http')) {
|
||||||
|
return imageUrl
|
||||||
|
}
|
||||||
|
// 使用云端URL拼接
|
||||||
|
const cloudUrl = 'https://cloud.jingrow.com'
|
||||||
|
return `${cloudUrl}${imageUrl.startsWith('/') ? '' : '/'}${imageUrl}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleImageError(event: Event) {
|
||||||
|
const img = event.target as HTMLImageElement
|
||||||
|
img.style.display = 'none'
|
||||||
|
img.parentElement?.querySelector('.tool-image-placeholder')?.classList.add('show')
|
||||||
|
}
|
||||||
|
|
||||||
|
function truncateText(text: string, maxLength: number): string {
|
||||||
|
if (!text) return ''
|
||||||
|
if (text.length <= maxLength) return text
|
||||||
|
return text.substring(0, maxLength) + '...'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusClass(status: string): string {
|
||||||
|
if (!status) return ''
|
||||||
|
// 将状态转换为小写,并将空格替换为连字符
|
||||||
|
return status.toLowerCase().replace(/\s+/g, '-')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteTool(tool: any) {
|
||||||
|
// 使用记录的name字段删除
|
||||||
|
const recordName = tool.name
|
||||||
|
if (!recordName) {
|
||||||
|
message.error(t('Tool name does not exist'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示确认对话框,显示工具标题
|
||||||
|
const toolTitle = tool.title || tool.tool_name || recordName
|
||||||
|
dialog.warning({
|
||||||
|
title: t('确认删除'),
|
||||||
|
content: t('Are you sure you want to delete tool "{0}"? This action cannot be undone.').replace('{0}', toolTitle),
|
||||||
|
positiveText: t('确认删除'),
|
||||||
|
negativeText: t('取消'),
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
await performDelete(recordName)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function performDelete(toolName: string) {
|
||||||
|
try {
|
||||||
|
// 调用本地API,由后端转发到云端
|
||||||
|
const response = await axios.post('/jingrow/delete-published-tool', {
|
||||||
|
name: toolName
|
||||||
|
}, {
|
||||||
|
withCredentials: true
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.data && response.data.success) {
|
||||||
|
message.success(response.data.message || t('Tool deleted successfully'))
|
||||||
|
// 刷新工具列表
|
||||||
|
loadTools()
|
||||||
|
} else {
|
||||||
|
const errorMsg = response.data?.message || response.data?.error || t('删除失败')
|
||||||
|
message.error(errorMsg)
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Delete tool error:', error)
|
||||||
|
const errorMsg = error.response?.data?.detail ||
|
||||||
|
error.response?.data?.message ||
|
||||||
|
error.message ||
|
||||||
|
t('删除失败')
|
||||||
|
message.error(errorMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadTools()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听搜索和排序变化
|
||||||
|
watch([searchQuery, sortBy], () => {
|
||||||
|
page.value = 1 // 重置到第一页
|
||||||
|
loadTools()
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
|
// 监听分页变化
|
||||||
|
watch([page], () => {
|
||||||
|
loadTools()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听每页数量变化(从系统设置)
|
||||||
|
watch(() => localStorage.getItem('itemsPerPage'), (newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
pageSize.value = parseInt(newValue)
|
||||||
|
page.value = 1 // 重置到第一页
|
||||||
|
loadTools()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.my-published-tools {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools-section {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools-title h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 32px;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
max-width: 600px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 20px;
|
||||||
|
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input .n-input {
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input .n-input:focus-within {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-button {
|
||||||
|
border-radius: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0 24px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-button:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-card {
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: white;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||||
|
border-color: #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-image {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 200px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-image:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-image img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-card:hover .tool-image img {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-image-placeholder {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
color: #9ca3af;
|
||||||
|
background: linear-gradient(135deg, #f9fafb 0%, #f3f4f6 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-image-placeholder.show {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-content {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-title-section {
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-title-section h3 {
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clickable-title {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clickable-title:hover {
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-team {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6b7280;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-team .n-icon {
|
||||||
|
color: #9ca3af;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-name {
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: 'SF Mono', 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
text-align: center;
|
||||||
|
min-width: 70px;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
font-size: 10px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-name:hover {
|
||||||
|
border-color: #9ca3af;
|
||||||
|
color: #374151;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-subtitle {
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.tool-actions {
|
||||||
|
padding: 0 20px 20px;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-actions .n-button {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading, .empty {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.header-content {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-card {
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-image {
|
||||||
|
height: 180px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@ -2,37 +2,37 @@
|
|||||||
<!-- 根据视图模式决定容器类名,组件自包含所有样式 -->
|
<!-- 根据视图模式决定容器类名,组件自包含所有样式 -->
|
||||||
<div :class="containerClass">
|
<div :class="containerClass">
|
||||||
<!-- 默认操作按钮 -->
|
<!-- 默认操作按钮 -->
|
||||||
<button class="action-btn" @click.stop="context.openDetail(context.row.name)" :title="context.t('View')">
|
<button class="action-btn" @click.stop="handleView" :title="t('View')">
|
||||||
<i class="fa fa-eye"></i>
|
<i class="fa fa-eye"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="action-btn" @click.stop="context.editRecord(context.row)" :title="context.t('Edit')">
|
<button class="action-btn" @click.stop="handleEdit" :title="t('Edit')">
|
||||||
<i class="fa fa-edit"></i>
|
<i class="fa fa-edit"></i>
|
||||||
</button>
|
</button>
|
||||||
<!-- Schema 编辑按钮 -->
|
<!-- Schema 编辑按钮 -->
|
||||||
<button
|
<button
|
||||||
class="action-btn schema-btn"
|
class="action-btn schema-btn"
|
||||||
@click.stop="handleOpenSchemaEditor"
|
@click.stop="handleOpenSchemaEditor"
|
||||||
:title="context.t('Edit Schema')"
|
:title="t('Edit Schema')"
|
||||||
:disabled="!canEditSchema"
|
:disabled="!canEditSchema"
|
||||||
>
|
>
|
||||||
<i class="fa fa-table"></i>
|
<i class="fa fa-table"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="action-btn delete-btn" @click.stop="context.deleteRecord(context.row.name)" :title="context.t('Delete')">
|
<button class="action-btn delete-btn" @click.stop="handleDelete" :title="t('Delete')">
|
||||||
<i class="fa fa-trash"></i>
|
<i class="fa fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- Schema 编辑器模态框 - 使用 Teleport 渲染到 body,避免事件冒泡问题 -->
|
||||||
|
<Teleport to="body">
|
||||||
|
<SchemaEditorModal
|
||||||
|
v-model:visible="showSchemaEditor"
|
||||||
|
:node-type="nodeType"
|
||||||
|
:node-name="nodeName"
|
||||||
|
:initial-schema="initialSchema"
|
||||||
|
:on-save="handleSchemaSave"
|
||||||
|
@close="showSchemaEditor = false"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Schema 编辑器模态框 - 使用 Teleport 渲染到 body,避免事件冒泡问题 -->
|
|
||||||
<Teleport to="body">
|
|
||||||
<SchemaEditorModal
|
|
||||||
v-model:visible="showSchemaEditor"
|
|
||||||
:node-type="nodeType"
|
|
||||||
:node-name="nodeName"
|
|
||||||
:initial-schema="initialSchema"
|
|
||||||
:on-save="handleSchemaSave"
|
|
||||||
@close="showSchemaEditor = false"
|
|
||||||
/>
|
|
||||||
</Teleport>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@ -41,10 +41,12 @@ import { Teleport } from 'vue'
|
|||||||
import { useMessage } from 'naive-ui'
|
import { useMessage } from 'naive-ui'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { get_session_api_headers } from '@/shared/api/auth'
|
import { get_session_api_headers } from '@/shared/api/auth'
|
||||||
|
import { t } from '@/shared/i18n'
|
||||||
import SchemaEditorModal from '@/core/components/SchemaEditorModal.vue'
|
import SchemaEditorModal from '@/core/components/SchemaEditorModal.vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
interface Props {
|
||||||
context: {
|
// 支持两种接口:context 或直接 props
|
||||||
|
context?: {
|
||||||
row: any
|
row: any
|
||||||
entity: string
|
entity: string
|
||||||
openDetail: (name: string) => void
|
openDetail: (name: string) => void
|
||||||
@ -54,21 +56,37 @@ const props = defineProps<{
|
|||||||
t: (key: string) => string
|
t: (key: string) => string
|
||||||
viewMode?: 'card' | 'list'
|
viewMode?: 'card' | 'list'
|
||||||
}
|
}
|
||||||
}>()
|
row?: any
|
||||||
|
entity?: string
|
||||||
|
viewMode?: 'card' | 'list'
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'view', row: any): void
|
||||||
|
(e: 'edit', row: any): void
|
||||||
|
(e: 'delete', row: any): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const emit = defineEmits<Emits>()
|
||||||
|
|
||||||
|
// 兼容两种接口:优先使用直接 props,如果没有则使用 context
|
||||||
|
const row = computed(() => props.row || props.context?.row)
|
||||||
|
const entity = computed(() => props.entity || props.context?.entity)
|
||||||
|
const viewMode = computed(() => props.viewMode || props.context?.viewMode || 'list')
|
||||||
|
|
||||||
// 根据视图模式决定容器类名,组件自包含所有样式
|
// 根据视图模式决定容器类名,组件自包含所有样式
|
||||||
const containerClass = computed(() => {
|
const containerClass = computed(() => {
|
||||||
const viewMode = props.context.viewMode || 'list'
|
return viewMode.value === 'card' ? 'actions-container card-actions' : 'actions-container col-actions'
|
||||||
return viewMode === 'card' ? 'actions-container card-actions' : 'actions-container col-actions'
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const showSchemaEditor = ref(false)
|
const showSchemaEditor = ref(false)
|
||||||
|
|
||||||
const nodeName = computed(() => props.context.row.name || '')
|
const nodeName = computed(() => row.value?.name || '')
|
||||||
const nodeType = computed(() => props.context.row.node_type || '')
|
const nodeType = computed(() => row.value?.node_type || '')
|
||||||
const initialSchema = computed(() => {
|
const initialSchema = computed(() => {
|
||||||
const schema = props.context.row.node_schema
|
const schema = row.value?.node_schema
|
||||||
if (!schema) return {}
|
if (!schema) return {}
|
||||||
if (typeof schema === 'string') {
|
if (typeof schema === 'string') {
|
||||||
try {
|
try {
|
||||||
@ -84,17 +102,42 @@ const canEditSchema = computed(() => {
|
|||||||
return !!nodeType.value
|
return !!nodeType.value
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 事件处理函数
|
||||||
|
function handleView() {
|
||||||
|
if (props.context?.openDetail) {
|
||||||
|
props.context.openDetail(row.value.name)
|
||||||
|
} else {
|
||||||
|
emit('view', row.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEdit() {
|
||||||
|
if (props.context?.editRecord) {
|
||||||
|
props.context.editRecord(row.value)
|
||||||
|
} else {
|
||||||
|
emit('edit', row.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDelete() {
|
||||||
|
if (props.context?.deleteRecord) {
|
||||||
|
props.context.deleteRecord(row.value.name)
|
||||||
|
} else {
|
||||||
|
emit('delete', row.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleOpenSchemaEditor() {
|
async function handleOpenSchemaEditor() {
|
||||||
if (!canEditSchema.value) {
|
if (!canEditSchema.value) {
|
||||||
message.warning(props.context.t('Please select node type first'))
|
message.warning(t('Please select node type first'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果列表数据中没有完整的 node_schema,需要从API获取完整记录
|
// 如果列表数据中没有完整的 node_schema,需要从API获取完整记录
|
||||||
if (!props.context.row.node_schema) {
|
if (!row.value?.node_schema) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(
|
const response = await axios.get(
|
||||||
`/api/data/${encodeURIComponent(props.context.entity)}/${encodeURIComponent(nodeName.value)}`,
|
`/api/data/${encodeURIComponent(entity.value)}/${encodeURIComponent(nodeName.value)}`,
|
||||||
{
|
{
|
||||||
headers: get_session_api_headers(),
|
headers: get_session_api_headers(),
|
||||||
withCredentials: true
|
withCredentials: true
|
||||||
@ -103,12 +146,12 @@ async function handleOpenSchemaEditor() {
|
|||||||
|
|
||||||
const record = response.data?.data || {}
|
const record = response.data?.data || {}
|
||||||
// 更新 row 数据以便后续使用
|
// 更新 row 数据以便后续使用
|
||||||
if (record.node_schema) {
|
if (record.node_schema && row.value) {
|
||||||
props.context.row.node_schema = record.node_schema
|
row.value.node_schema = record.node_schema
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取节点数据失败:', error)
|
console.error('获取节点数据失败:', error)
|
||||||
message.error(props.context.t('Failed to load node data'))
|
message.error(t('Failed to load node data'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -120,7 +163,7 @@ async function handleSchemaSave(schemaData: any) {
|
|||||||
try {
|
try {
|
||||||
// 保存 Schema 数据到后端
|
// 保存 Schema 数据到后端
|
||||||
const response = await axios.put(
|
const response = await axios.put(
|
||||||
`/api/data/${encodeURIComponent(props.context.entity)}/${encodeURIComponent(nodeName.value)}`,
|
`/api/data/${encodeURIComponent(entity.value)}/${encodeURIComponent(nodeName.value)}`,
|
||||||
{
|
{
|
||||||
node_schema: schemaData
|
node_schema: schemaData
|
||||||
},
|
},
|
||||||
@ -132,15 +175,17 @@ async function handleSchemaSave(schemaData: any) {
|
|||||||
|
|
||||||
if (response.data?.success !== false) {
|
if (response.data?.success !== false) {
|
||||||
// 更新本地 row 数据
|
// 更新本地 row 数据
|
||||||
props.context.row.node_schema = schemaData
|
if (row.value) {
|
||||||
message.success(props.context.t('Schema saved successfully'))
|
row.value.node_schema = schemaData
|
||||||
|
}
|
||||||
|
message.success(t('Schema saved successfully'))
|
||||||
showSchemaEditor.value = false
|
showSchemaEditor.value = false
|
||||||
} else {
|
} else {
|
||||||
throw new Error(response.data?.message || props.context.t('Save failed'))
|
throw new Error(response.data?.message || t('Save failed'))
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('保存 Schema 失败:', error)
|
console.error('保存 Schema 失败:', error)
|
||||||
message.error(error?.response?.data?.message || error?.message || props.context.t('Save failed, please check permission and server logs'))
|
message.error(error?.response?.data?.message || error?.message || t('Save failed, please check permission and server logs'))
|
||||||
throw error // 让组件处理错误显示
|
throw error // 让组件处理错误显示
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,7 @@ Jingrow Tools API
|
|||||||
工具相关的 FastAPI 路由
|
工具相关的 FastAPI 路由
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, Form, UploadFile, File
|
from fastapi import APIRouter, HTTPException, Form, UploadFile, File, Request
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional
|
||||||
import json
|
import json
|
||||||
import requests
|
import requests
|
||||||
@ -444,3 +444,91 @@ async def publish_tool_to_marketplace(
|
|||||||
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.get("/jingrow/my-published-tools")
|
||||||
|
async def get_my_published_tools(
|
||||||
|
request: Request,
|
||||||
|
search: Optional[str] = None,
|
||||||
|
page: int = 1,
|
||||||
|
page_size: int = 20,
|
||||||
|
sort_by: Optional[str] = None
|
||||||
|
):
|
||||||
|
"""获取当前用户已发布的工具列表,支持搜索、分页和排序"""
|
||||||
|
session_cookie = request.cookies.get('sid')
|
||||||
|
if not session_cookie:
|
||||||
|
raise HTTPException(status_code=401, detail="未提供认证信息")
|
||||||
|
|
||||||
|
url = f"{get_jingrow_cloud_url()}/api/action/jcloud.api.jlocal.get_my_local_tool_list"
|
||||||
|
|
||||||
|
# 构建参数
|
||||||
|
params = {
|
||||||
|
'order_by': sort_by or "tool_name asc",
|
||||||
|
'limit_start': (page - 1) * page_size,
|
||||||
|
'limit_page_length': page_size
|
||||||
|
}
|
||||||
|
|
||||||
|
if search:
|
||||||
|
params['filters'] = json.dumps({"title": ["like", f"%{search}%"]}, ensure_ascii=False)
|
||||||
|
|
||||||
|
# 获取总数
|
||||||
|
total_params = params.copy()
|
||||||
|
total_params['limit_start'] = 0
|
||||||
|
total_params['limit_page_length'] = 0
|
||||||
|
|
||||||
|
headers = get_jingrow_cloud_api_headers()
|
||||||
|
headers['Cookie'] = f'sid={session_cookie}'
|
||||||
|
|
||||||
|
try:
|
||||||
|
total_response = requests.get(url, params=total_params, headers=headers, timeout=20)
|
||||||
|
total_response.raise_for_status()
|
||||||
|
total_count = len(total_response.json().get('message', []))
|
||||||
|
|
||||||
|
# 获取分页数据
|
||||||
|
response = requests.get(url, params=params, headers=headers, timeout=20)
|
||||||
|
response.raise_for_status()
|
||||||
|
tools = response.json().get('message', [])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"items": tools,
|
||||||
|
"total": total_count,
|
||||||
|
"page": page,
|
||||||
|
"page_size": page_size
|
||||||
|
}
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logger.error(f"获取已发布工具列表失败: {str(e)}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"获取已发布工具列表失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/jingrow/delete-published-tool")
|
||||||
|
async def delete_published_tool(request: Request, payload: Dict[str, Any]):
|
||||||
|
"""删除已发布的工具,根据记录的name字段删除"""
|
||||||
|
session_cookie = request.cookies.get('sid')
|
||||||
|
if not session_cookie:
|
||||||
|
raise HTTPException(status_code=401, detail="未提供认证信息")
|
||||||
|
|
||||||
|
# 使用记录的name字段,不是tool_name字段
|
||||||
|
record_name = payload.get('name')
|
||||||
|
if not record_name:
|
||||||
|
raise HTTPException(status_code=400, detail="记录名称不能为空")
|
||||||
|
|
||||||
|
url = f"{get_jingrow_cloud_url()}/api/action/jcloud.api.jlocal.delete_local_tool"
|
||||||
|
|
||||||
|
headers = get_jingrow_cloud_api_headers()
|
||||||
|
headers['Cookie'] = f'sid={session_cookie}'
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 传递记录的name字段到云端API
|
||||||
|
response = requests.post(url, json={'name': record_name}, headers=headers, timeout=20)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
result = data.get('message', data)
|
||||||
|
|
||||||
|
if result.get('success'):
|
||||||
|
return {"success": True, "message": result.get('message', '工具删除成功')}
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=400, detail=result.get('message', '删除失败'))
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logger.error(f"删除已发布工具失败: {str(e)}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"删除已发布工具失败: {str(e)}")
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user