mirror of
https://github.com/langbot-app/LangBot.git
synced 2025-11-25 03:15:06 +08:00
refactor: market plugin detail dialog
This commit is contained in:
@@ -58,6 +58,7 @@
|
||||
"react-i18next": "^15.5.1",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-photo-view": "^1.2.7",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"sonner": "^2.0.3",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tailwindcss": "^4.1.5",
|
||||
|
||||
@@ -6,11 +6,12 @@ import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Loader2, Download, Users } from 'lucide-react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { PluginV4 } from '@/app/infra/entities/plugin';
|
||||
import { extractI18nObject } from '@/i18n/I18nProvider';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
import { PluginV4 } from '@/app/infra/entities/plugin';
|
||||
import { getCloudServiceClientSync } from '@/app/infra/http';
|
||||
import { extractI18nObject } from '@/i18n/I18nProvider';
|
||||
|
||||
interface PluginDetailDialogProps {
|
||||
open: boolean;
|
||||
@@ -50,7 +51,6 @@ export default function PluginDetailDialog({
|
||||
author,
|
||||
pluginName,
|
||||
);
|
||||
console.log('detailResponse', detailResponse);
|
||||
setPlugin(detailResponse.plugin);
|
||||
|
||||
// 获取README
|
||||
@@ -58,7 +58,6 @@ export default function PluginDetailDialog({
|
||||
try {
|
||||
const readmeResponse =
|
||||
await getCloudServiceClientSync().getPluginREADME(author, pluginName);
|
||||
console.log('readmeResponse', readmeResponse);
|
||||
setReadme(readmeResponse.readme);
|
||||
} catch (error) {
|
||||
console.warn('Failed to load README:', error);
|
||||
@@ -77,210 +76,212 @@ export default function PluginDetailDialog({
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
const PluginHeader = () => (
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<img
|
||||
src={getCloudServiceClientSync().getPluginIconURL(author!, pluginName!)}
|
||||
alt={plugin!.name}
|
||||
className="w-16 h-16 rounded-xl border bg-gray-50 object-cover flex-shrink-0"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-2">
|
||||
{extractI18nObject(plugin!.label) || plugin!.name}
|
||||
</h1>
|
||||
<div className="flex items-center gap-2 text-sm text-gray-600 mb-3">
|
||||
<Users className="w-4 h-4" />
|
||||
<span>
|
||||
{plugin!.author} / {plugin!.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge variant="outline">v{plugin!.latest_version}</Badge>
|
||||
<Badge variant="outline" className="flex items-center gap-1">
|
||||
<Download className="w-4 h-4" />
|
||||
{plugin!.install_count.toLocaleString()} {t('market.downloads')}
|
||||
</Badge>
|
||||
{plugin!.repository && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
window.open(plugin!.repository, '_blank');
|
||||
}}
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs bg-gray-100 hover:bg-gray-200 rounded-md transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12.001 2C6.47598 2 2.00098 6.475 2.00098 12C2.00098 16.425 4.86348 20.1625 8.83848 21.4875C9.33848 21.575 9.52598 21.275 9.52598 21.0125C9.52598 20.775 9.51348 19.9875 9.51348 19.15C7.00098 19.6125 6.35098 18.5375 6.15098 17.975C6.03848 17.6875 5.55098 16.8 5.12598 16.5625C4.77598 16.375 4.27598 15.9125 5.11348 15.9C5.90098 15.8875 6.46348 16.625 6.65098 16.925C7.55098 18.4375 8.98848 18.0125 9.56348 17.75C9.65098 17.1 9.91348 16.6625 10.201 16.4125C7.97598 16.1625 5.65098 15.3 5.65098 11.475C5.65098 10.3875 6.03848 9.4875 6.67598 8.7875C6.57598 8.5375 6.22598 7.5125 6.77598 6.1375C6.77598 6.1375 7.61348 5.875 9.52598 7.1625C10.326 6.9375 11.176 6.825 12.026 6.825C12.876 6.825 13.726 6.9375 14.526 7.1625C16.4385 5.8625 17.276 6.1375 17.276 6.1375C17.826 7.5125 17.476 8.5375 17.376 8.7875C18.0135 9.4875 18.401 10.375 18.401 11.475C18.401 15.3125 16.0635 16.1625 13.8385 16.4125C14.201 16.725 14.5135 17.325 14.5135 18.2625C14.5135 19.6 14.501 20.675 14.501 21.0125C14.501 21.275 14.6885 21.5875 15.1885 21.4875C19.259 20.1133 21.9999 16.2963 22.001 12C22.001 6.475 17.526 2 12.001 2Z" />
|
||||
</svg>
|
||||
GitHub
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const PluginDescription = () => (
|
||||
<div className="mb-6">
|
||||
<p className="text-gray-700 leading-relaxed text-base">
|
||||
{extractI18nObject(plugin!.description) || t('market.noDescription')}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
const PluginOptions = () => (
|
||||
<div className="space-y-4">
|
||||
<Button
|
||||
onClick={() => installPlugin(plugin!)}
|
||||
className="w-full h-12 text-base font-medium"
|
||||
>
|
||||
<Download className="w-5 h-5 mr-2" />
|
||||
{t('market.install')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
const ReadmeContent = () => (
|
||||
<div className="prose prose-sm max-w-none text-gray-800">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
// 表格组件
|
||||
table: ({ ...props }) => (
|
||||
<div className="my-6 w-full overflow-x-auto border border-gray-200 rounded-lg">
|
||||
<table className="w-full border-collapse bg-white" {...props} />
|
||||
</div>
|
||||
),
|
||||
thead: ({ ...props }) => <thead className="bg-gray-50" {...props} />,
|
||||
tbody: ({ ...props }) => (
|
||||
<tbody className="divide-y divide-gray-200" {...props} />
|
||||
),
|
||||
th: ({ ...props }) => (
|
||||
<th
|
||||
className="px-4 py-3 text-left text-sm font-semibold text-gray-900 border-r border-gray-200 last:border-r-0"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
td: ({ ...props }) => (
|
||||
<td
|
||||
className="px-4 py-3 text-sm text-gray-700 border-r border-gray-200 last:border-r-0"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
tr: ({ ...props }) => (
|
||||
<tr className="hover:bg-gray-50 transition-colors" {...props} />
|
||||
),
|
||||
// 删除线支持
|
||||
del: ({ ...props }) => (
|
||||
<del className="text-gray-500 line-through" {...props} />
|
||||
),
|
||||
// Todo 列表支持
|
||||
input: ({ type, checked, ...props }) => {
|
||||
if (type === 'checkbox') {
|
||||
return (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
disabled
|
||||
className="mr-2 rounded border-gray-300 text-blue-600 focus:ring-blue-500 cursor-default"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <input type={type} {...props} />;
|
||||
},
|
||||
ul: ({ ...props }) => <ul className="list-disc ml-5" {...props} />,
|
||||
ol: ({ ...props }) => <ol className="list-decimal ml-5" {...props} />,
|
||||
li: ({ ...props }) => <li className="mb-1" {...props} />,
|
||||
h1: ({ ...props }) => (
|
||||
<h1 className="text-3xl font-bold my-2" {...props} />
|
||||
),
|
||||
h2: ({ ...props }) => (
|
||||
<h2 className="text-2xl font-semibold mb-2 mt-4" {...props} />
|
||||
),
|
||||
p: ({ ...props }) => <p className="leading-relaxed" {...props} />,
|
||||
code: ({ className, children, ...props }) => {
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
const isCodeBlock = match ? true : false;
|
||||
|
||||
// 如果是代码块(有语言标识),由 pre 标签处理样式,淡灰色底,黑色字
|
||||
if (isCodeBlock) {
|
||||
return (
|
||||
<code
|
||||
className="bg-gray-100 text-gray-800 px-1.5 py-0.5 rounded text-sm font-mono"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
|
||||
// 内联代码样式 - 淡灰色底
|
||||
return (
|
||||
<code
|
||||
className="bg-gray-100 text-gray-800 px-1.5 py-0.5 rounded text-sm font-mono inline-block"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
pre: ({ ...props }) => (
|
||||
<pre
|
||||
className="bg-gray-100 text-gray-800 rounded-lg my-4 border border-gray-200 shadow-sm max-h-[500px] relative"
|
||||
style={{
|
||||
// 内边距确保内容不被滚动条覆盖
|
||||
padding: '16px',
|
||||
// 保持代码不换行以启用横向滚动
|
||||
whiteSpace: 'pre',
|
||||
// 滚动设置
|
||||
overflowX: 'auto',
|
||||
overflowY: 'auto',
|
||||
// 确保滚动条在内部
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{readme}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="!min-w-[65vw] !min-h-[65vh] max-h-[85vh] overflow-hidden p-0 dark:bg-[#1f1f22]">
|
||||
<DialogContent className="!min-w-[50vw] max-w-none max-h-[90vh] h-[90vh] p-0">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="flex items-center justify-center py-12 h-full">
|
||||
<Loader2 className="h-8 w-8 animate-spin" />
|
||||
<span className="ml-2">{t('market.loading')}</span>
|
||||
<span className="ml-2">{t('cloud.loading')}</span>
|
||||
</div>
|
||||
) : plugin ? (
|
||||
<div className="flex h-full">
|
||||
{/* 左侧:插件基本信息 */}
|
||||
<div className="w-2/5 p-6 border-r border-gray-200 overflow-y-auto">
|
||||
{/* 插件图标和标题 */}
|
||||
<div className="flex items-start gap-4 mb-6">
|
||||
<img
|
||||
src={getCloudServiceClientSync().getPluginIconURL(
|
||||
author!,
|
||||
pluginName!,
|
||||
)}
|
||||
alt={plugin.name}
|
||||
className="w-16 h-16 rounded-xl border bg-gray-50 object-cover flex-shrink-0 dark:bg-[#1f1f22]"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-2 dark:text-[#f0f0f0]">
|
||||
{extractI18nObject(plugin.label) || plugin.name}
|
||||
</h2>
|
||||
<div className="flex items-center gap-2 text-sm text-gray-600 mb-2 dark:text-[#999]">
|
||||
<Users className="w-4 h-4" />
|
||||
<span>
|
||||
{plugin.author} / {plugin.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2 text-sm text-gray-600 mb-2 dark:text-[#999]">
|
||||
<Badge variant="outline" className="text-sm">
|
||||
v{plugin.latest_version}
|
||||
</Badge>
|
||||
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-sm flex items-center gap-1"
|
||||
>
|
||||
<Download className="w-5 h-5" />
|
||||
<span className="font-medium">
|
||||
{plugin.install_count.toLocaleString()}{' '}
|
||||
{t('market.downloads')}
|
||||
</span>
|
||||
</Badge>
|
||||
|
||||
{plugin.repository && (
|
||||
<svg
|
||||
className="w-[1.2rem] h-[1.2rem] text-black cursor-pointer hover:text-gray-600 dark:text-[#f0f0f0]"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
window.open(plugin.repository, '_blank');
|
||||
}}
|
||||
>
|
||||
<path d="M12.001 2C6.47598 2 2.00098 6.475 2.00098 12C2.00098 16.425 4.86348 20.1625 8.83848 21.4875C9.33848 21.575 9.52598 21.275 9.52598 21.0125C9.52598 20.775 9.51348 19.9875 9.51348 19.15C7.00098 19.6125 6.35098 18.5375 6.15098 17.975C6.03848 17.6875 5.55098 16.8 5.12598 16.5625C4.77598 16.375 4.27598 15.9125 5.11348 15.9C5.90098 15.8875 6.46348 16.625 6.65098 16.925C7.55098 18.4375 8.98848 18.0125 9.56348 17.75C9.65098 17.1 9.91348 16.6625 10.201 16.4125C7.97598 16.1625 5.65098 15.3 5.65098 11.475C5.65098 10.3875 6.03848 9.4875 6.67598 8.7875C6.57598 8.5375 6.22598 7.5125 6.77598 6.1375C6.77598 6.1375 7.61348 5.875 9.52598 7.1625C10.326 6.9375 11.176 6.825 12.026 6.825C12.876 6.825 13.726 6.9375 14.526 7.1625C16.4385 5.8625 17.276 6.1375 17.276 6.1375C17.826 7.5125 17.476 8.5375 17.376 8.7875C18.0135 9.4875 18.401 10.375 18.401 11.475C18.401 15.3125 16.0635 16.1625 13.8385 16.4125C14.201 16.725 14.5135 17.325 14.5135 18.2625C14.5135 19.6 14.501 20.675 14.501 21.0125C14.501 21.275 14.6885 21.5875 15.1885 21.4875C19.259 20.1133 21.9999 16.2963 22.001 12C22.001 6.475 17.526 2 12.001 2Z"></path>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col h-full overflow-hidden">
|
||||
{/* 插件信息区域 */}
|
||||
<div className="flex-shrink-0 bg-white border-b m-4 pt-2">
|
||||
<div className="flex gap-6 p-2 px-4">
|
||||
<div className="flex-1">
|
||||
<PluginHeader />
|
||||
<PluginDescription />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 插件描述 */}
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-3 dark:text-[#f0f0f0]">
|
||||
{t('market.description')}
|
||||
</h3>
|
||||
<p className="text-gray-700 leading-relaxed dark:text-[#999]">
|
||||
{extractI18nObject(plugin.description) ||
|
||||
t('market.noDescription')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 标签 */}
|
||||
{plugin.tags && plugin.tags.length > 0 && (
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-3 dark:text-[#f0f0f0]">
|
||||
{t('market.tags')}
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{plugin.tags.map((tag) => (
|
||||
<Badge
|
||||
key={tag}
|
||||
variant="secondary"
|
||||
className="text-sm dark:bg-[#1f1f22]"
|
||||
>
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<div className="w-40 pr-4 flex-shrink-0">
|
||||
<PluginOptions />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="space-y-3">
|
||||
<Button
|
||||
onClick={() => installPlugin(plugin)}
|
||||
className="w-full h-12 text-base font-medium"
|
||||
>
|
||||
<Download className="w-5 h-5 mr-2" />
|
||||
{t('market.install')}
|
||||
</Button>
|
||||
{/* {plugin.repository && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleOpenRepository}
|
||||
className="w-full h-12 text-base"
|
||||
>
|
||||
<ExternalLink className="w-5 h-5 mr-2" />
|
||||
{t('market.repository')}
|
||||
</Button>
|
||||
)} */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧:README内容 */}
|
||||
<div className="w-3/5 p-2 overflow-y-auto">
|
||||
<div className=" rounded-lg p-6 min-h-[500px]">
|
||||
{/* README 区域 */}
|
||||
<div className="flex-1 overflow-hidden px-8">
|
||||
<div className="h-full bg-white overflow-y-auto pb-2">
|
||||
{isLoadingReadme ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="h-8 w-8 animate-spin" />
|
||||
<span className="ml-3 text-gray-600">
|
||||
{t('market.loading')}
|
||||
{t('cloud.loading')}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="prose prose-sm max-w-none text-gray-800">
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
// 自定义样式
|
||||
h1: ({ children }) => (
|
||||
<h1 className="text-xl font-bold mb-4 text-gray-900 border-b border-gray-200 pb-2 dark:text-[#f0f0f0]">
|
||||
{children}
|
||||
</h1>
|
||||
),
|
||||
h2: ({ children }) => (
|
||||
<h2 className="text-lg font-semibold mb-3 text-gray-900 mt-6 dark:text-[#f0f0f0]">
|
||||
{children}
|
||||
</h2>
|
||||
),
|
||||
h3: ({ children }) => (
|
||||
<h3 className="text-base font-medium mb-2 text-gray-900 mt-4 dark:text-[#f0f0f0]">
|
||||
{children}
|
||||
</h3>
|
||||
),
|
||||
p: ({ children }) => (
|
||||
<p className="mb-4 leading-relaxed text-gray-700 dark:text-[#999]">
|
||||
{children}
|
||||
</p>
|
||||
),
|
||||
ul: ({ children }) => (
|
||||
<ul className="mb-4 pl-6 list-disc space-y-1 dark:text-[#999]">
|
||||
{children}
|
||||
</ul>
|
||||
),
|
||||
ol: ({ children }) => (
|
||||
<ol className="mb-4 pl-6 list-decimal space-y-1 dark:text-[#999]">
|
||||
{children}
|
||||
</ol>
|
||||
),
|
||||
li: ({ children }) => (
|
||||
<li className="text-gray-700 dark:text-[#999]">
|
||||
{children}
|
||||
</li>
|
||||
),
|
||||
code: ({ children, node }) => {
|
||||
const isInline =
|
||||
node?.children?.length === 1 &&
|
||||
node?.children[0]?.type === 'text';
|
||||
return isInline ? (
|
||||
<code className="bg-gray-200 p-1 rounded-md text-sm font-mono whitespace-pre-wrap border dark:bg-[#1f1f22]">
|
||||
{children}
|
||||
</code>
|
||||
) : (
|
||||
<code className="block bg-gray-200 p-3 rounded-md text-sm font-mono whitespace-pre-wrap border dark:bg-[#1f1f22]">
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
blockquote: ({ children }) => (
|
||||
<blockquote className="border-l-4 border-blue-300 pl-4 py-2 mb-4 italic bg-blue-50 text-gray-700 rounded-r-md dark:bg-[#1f1f22]">
|
||||
{children}
|
||||
</blockquote>
|
||||
),
|
||||
a: ({ href, children }) => (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:text-blue-800 underline dark:text-[#29f]"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{readme}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
<ReadmeContent />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user