Merge branch 'xuhd' into 'master'

Xuhd

See merge request knowledge-base-bj/deerflow-server!125
This commit is contained in:
xu hd committed 2026-06-16 09:27:02 +00:00
commit 12f50123dc
3 files changed
+438 -110

No files matched your search

@@ -27,6 +27,7 @@ import { Button } from "@/components/ui/button";
import { toast } from "sonner";
import { getStoredAuth } from "@/core/auth";
import { queryClient } from "@/components/query-client-provider";
import { Checkbox } from "@/components/ui/checkbox";
import Empty from "./empty";
import { NetworkActor } from "./network-actor";
@@ -693,169 +694,402 @@ const DocumentUploadPopupContent: FC<{
{/* A系统基因库 */}
{activeTab === "A" && (
<div className="bg-white rounded-lg">
<div className="custom-radio-group pt-[10px] ml-[10px]">
{searchTypes.map((option) => (
<div
key={option.value}
className={`custom-radio-btn ${searchType === option.value ? "active" : ""}`}
onClick={() => searchTypeChange(option.value)}
>
{option.label}
</div>
))}
</div>
{false && (
<div className="relative pt-[10px]">
<div className="relative rounded-lg border border-neutral-200 bg-white dark-theme-input ml-[10px] mr-[10px] mb-[10px] transition-colors hover:border-neutral-300">
<Textarea
placeholder="搜索资料..."
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
onKeyDown={async (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
await searchAChange(searchType);
}
}}
className=" max-h-[160px] resize-none border-0 dark-theme-input bg-transparent px-10 py-3 pr-20 text-sm text-neutral-700 placeholder:text-neutral-400 focus-visible:ring-0 focus-visible:outline-none"
rows={1}
/>
{/* 左侧搜索图标 */}
<div className="absolute left-3 top-3 text-neutral-400 pointer-events-none dark-theme-search">
<Search className="h-4 w-4" strokeWidth={1.5} />
</div>
{/* 右侧搜索按钮 */}
<Button
size="sm"
onClick={async () =>
await searchAChange(searchType)
}
disabled={isSearching || !searchText.trim()}
className="absolute right-2 top-2 h-7 rounded-md bg-neutral-900 px-3 text-xs font-medium text-white dark-theme-search transition-colors hover:bg-neutral-800 disabled:pointer-events-none disabled:opacity-40"
>
{isSearching ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<span>搜索</span>
)}
</Button>
{/* 左下角模式选择 */}
<div className="absolute left-2 bottom-1.5 flex items-center">
{/* <Select value={searchType} onValueChange={(value) => searchTypeChange(value)}>
<SelectTrigger className="h-6 w-auto min-w-[80px] gap-1.5 rounded border border-neutral-200 bg-white px-2 py-0 pr-1.5 text-[11px] font-medium text-neutral-600 hover:bg-neutral-50 data-[state=open]:bg-white">
<SelectValue />
</SelectTrigger>
<SelectContent align="start" className="min-w-[120px] rounded-md border border-neutral-200 bg-white p-0.5 shadow-sm">
{searchTypes.map((mode) => {
return (
<SelectItem
key={mode.value}
value={mode.value}
className="rounded px-2 py-1.5 text-[11px] font-medium data-[highlighted]:bg-neutral-100"
>
<div className="flex items-center gap-1.5">
<span className="text-neutral-700">{mode.label}</span>
</div>
</SelectItem>
);
})}
</SelectContent>
</Select> */}
</div>
</div>
</div>
)}
<div className="custom-radio-group pt-[10px] ml-[10px]">
{" "}
{/* 外层容器,模拟 nz-radio-group */}
{searchTypes.map((option) => {
const isActive = searchType === option.value;
return (
<div
key={option.value}
className={`custom-radio-btn ${isActive ? "active" : ""
}`}
onClick={() => searchTypeChange(option.value)}
>
{option.label}
</div>
);
})}
</div>
{/* 批量操作栏 - 当有选中项时显示 */}
{selectedSearchSources.length > 0 && (
<div className="flex items-center justify-between px-4 py-2 bg-gray-50 border-b border-gray-100">
<span className="text-sm text-gray-600">已选中 {selectedSearchSources.length} 项</span>
<span className="text-sm text-gray-600">
已选中 {selectedSearchSources.length} 项
</span>
<div className="flex gap-2">
<Button size="sm" variant="outline" onClick={handleBatchDelete} className="h-8 px-3 text-sm">
<Button
size="sm"
variant="outline"
onClick={handleBatchDelete}
className="h-8 px-3 text-sm"
>
<Trash2 className="h-3.5 w-3.5 mr-1" />
删除选中
</Button>
<Button
size="sm"
onClick={() => void handleImport()}
onClick={() => handleImport()}
disabled={isImporting}
className="h-8 px-3 text-sm bg-blue-50 hover:bg-blue-100 text-blue-600 border border-blue-200"
>
{isImporting ? <Loader2 className="h-3.5 w-3.5 mr-1 animate-spin" /> : <Plus className="h-3.5 w-3.5 mr-1" />}
{isImporting ? (
<Loader2 className="h-3.5 w-3.5 mr-1 animate-spin" />
) : (
<Plus className="h-3.5 w-3.5 mr-1" />
)}
导入选中
</Button>
</div>
</div>
)}
{/* 如果没有选中项,显示普通导入按钮 */}
{isImporting && (
<div className="px-4 py-2 border-b border-gray-100">
<Button
onClick={() => handleImport()}
disabled={isImporting}
className="w-full h-8 text-sm bg-blue-50 hover:bg-blue-100 text-blue-600 border border-blue-200 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isImporting ? (
<Loader2 className="h-4 w-4 mr-1 animate-spin" />
) : (
<Plus className="h-4 w-4 mr-1" />
)}
{isImporting ? "导入中..." : "导入全部"}
</Button>
</div>
)}
{listData.length === 0 && <Empty />}
{searchType === "human" && (
<div className="bg-white p-[10px] grid grid-cols-5 gap-[10px]">
{listData.map((item, index) => (
<div key={index} className="relative flex items-center shadow-[0px_0px_2px_1px_#ededed] mb-[10px] p-[10px] rounded-[5px]">
{/* 上传文件显示 loading 状态 */}
{item.is_uploaded_file && item.processing_status === 'processing' && (
<div className="absolute inset-0 bg-white/80 flex items-center justify-center z-10">
<Loader2 className="h-6 w-6 animate-spin text-primary" />
{listData.map((item: listDataItem, index: number) => {
return (
<div
key={index}
className="relative flex items-center shadow-[0px_0px_2px_1px_#ededed] mb-[10px] p-[10px] rounded-[5px]"
>
<div className="w-[50px] mr-[10px]">
<img
src={item.imagePath || ""}
className="w-[50px] h-[50px]"
alt=""
/>
</div>
)}
{/* 上传文件完成状态标记 */}
{item.is_uploaded_file && item.processing_status === 'error' && (
<div className="absolute top-1 left-1">
<span className="text-xs text-red-500">处理失败</span>
<div className="flex-1 min-w-0">
<p
className="truncate hover:underline cursor-pointer"
onClick={() => linkToA(item.url)}
>
{item.name}
</p>
<p className="truncate text-[#909090]">
职业:{item.occupationData}
</p>
<p className="truncate text-[#909090]">
属地:{item.country}
</p>
</div>
)}
{item.is_uploaded_file && item.processing_status === 'success' && (
<div className="absolute top-1 left-1">
<span className="text-xs text-green-500">✓ 已完成</span>
<div className="absolute top-[5px] right-[10px]">
<Checkbox
checked={item.checked}
onCheckedChange={() =>
listDataCheckedChange(index, !item.checked)
}
/>
</div>
)}
<div className="w-[50px] mr-[10px]">
<img src={item.imagePath || ""} className="w-[50px] h-[50px]" alt="" />
</div>
<div className="flex-1 min-w-0">
<p className="truncate hover:underline cursor-pointer" onClick={() => linkToA(item.url)}>{item.name}</p>
<p className="truncate text-[#909090]">职业:{item.occupationData}</p>
<p className="truncate text-[#909090]">属地:{item.country}</p>
</div>
<div className="absolute top-[5px] right-[10px]">
<input type="checkbox" checked={item.checked} onChange={() => listDataCheckedChange(index, !item.checked)} className="w-4 h-4 cursor-pointer accent-primary" />
</div>
</div>
))}
);
})}
</div>
)}
{searchType === "organization" && (
<div className="bg-white p-[10px] grid grid-cols-5 gap-[10px]">
{listData.map((item, index) => (
<div key={index} className="relative flex items-center shadow-[0px_0px_2px_1px_#ededed] mb-[10px] p-[10px] rounded-[5px]">
<div className="w-[50px] mr-[10px]">
<img src={item.imagePath || ""} className="w-[50px] h-[50px]" alt="" />
{listData.map((item: listDataItem, index: number) => {
return (
<div
key={index}
className="relative flex items-center shadow-[0px_0px_2px_1px_#ededed] mb-[10px] p-[10px] rounded-[5px]"
>
<div className="w-[50px] mr-[10px]">
<img
src={item.imagePath || ""}
className="w-[50px] h-[50px]"
alt=""
/>
</div>
<div className="flex-1 min-w-0">
<p
className="truncate hover:underline cursor-pointer"
onClick={() => linkToA(item.url)}
>
{item.name}
</p>
<p className="truncate text-[#909090]">
组织类别:{item.subtype}
</p>
<p className="truncate text-[#909090]">
简介:{item.text}
</p>
</div>
<div className="absolute top-[5px] right-[10px]">
<Checkbox
checked={item.checked}
onCheckedChange={() =>
listDataCheckedChange(index, !item.checked)
}
/>
</div>
</div>
<div className="flex-1 min-w-0">
<p className="truncate hover:underline cursor-pointer" onClick={() => linkToA(item.url)}>{item.name}</p>
<p className="truncate text-[#909090]">组织类别:{item.subtype}</p>
<p className="truncate text-[#909090]">简介:{item.text}</p>
</div>
<div className="absolute top-[5px] right-[10px]">
<input type="checkbox" checked={item.checked} onChange={() => listDataCheckedChange(index, !item.checked)} className="w-4 h-4 cursor-pointer accent-primary" />
</div>
</div>
))}
);
})}
</div>
)}
{searchType === "community" && (
<div className="bg-white p-[10px] grid grid-cols-4 gap-[10px]">
{listData.map((item, index) => (
<div key={index} className="relative shadow-[0px_0px_2px_1px_#ededed] mb-[10px] p-[10px] rounded-[5px]">
<div>
<div className="truncate mb-[5px] hover:underline cursor-pointer" onClick={() => linkToA(item.url)}>{item.title}</div>
<div className="flex items-center text-[#909090] mb-[5px]">
{item.nations && <span className="truncate max-w-[200px] inline-block border border-[#cf1322] rounded px-1 text-[#cf1322]">{item.nations}</span>}
{item.labels && <span className="inline-block border border-[#bbb] rounded px-1 ml-[10px]">{item.labels}</span>}
{listData.map((item: listDataItem, index: number) => {
return (
<div
key={index}
className="relative shadow-[0px_0px_2px_1px_#ededed] mb-[10px] p-[10px] rounded-[5px]"
>
<div>
<div
className="truncate mb-[5px] hover:underline cursor-pointer"
onClick={() => linkToA(item.url)}
>
{item.title}
</div>
<div className="flex items-center text-[#909090] mb-[5px]">
{item.nations && (
<span className="truncate max-width-[200px] inline-block border border-[#cf1322] rounded px-1 text-[#cf1322]">
{item.nations}
</span>
)}
{item.labels && (
<span className="inline-block border border-[#bbb] rounded px-1 ml-[10px]">
{item.labels}
</span>
)}
</div>
<div className="flex justify-between mb-[5px]">
<span className="text-[#909090]">
群体规模:
<span className="text-[#cf1322]">
{item.total}
</span>
</span>
<span className="text-[#909090]">
对应实体数:
<span className="text-[#cf1322]">
{item.real_total}
</span>
</span>
</div>
<div>
<span className="text-[#909090]">
涉及平台:
<span className="text-[#222222]">
{item.platforms}
</span>
</span>
</div>
</div>
<div className="flex justify-between mb-[5px]">
<span className="text-[#909090]">群体规模:<span className="text-[#cf1322]">{item.total}</span></span>
<span className="text-[#909090]">对应实体数:<span className="text-[#cf1322]">{item.real_total}</span></span>
<div className="absolute top-[5px] right-[10px]">
<Checkbox
checked={item.checked}
onCheckedChange={() =>
listDataCheckedChange(index, !item.checked)
}
/>
</div>
<div><span className="text-[#909090]">涉及平台:<span className="text-[#222222]">{item.platforms}</span></span></div>
</div>
<div className="absolute top-[5px] right-[10px]">
<input type="checkbox" checked={item.checked} onChange={() => listDataCheckedChange(index, !item.checked)} className="w-4 h-4 cursor-pointer accent-primary" />
</div>
</div>
))}
);
})}
</div>
)}
{searchType === "issue" && (
<div className="bg-white p-[10px] grid grid-cols-4 gap-[10px]">
{listData.map((item, index) => (
<div key={index} className="relative shadow-[0px_0px_2px_1px_#ededed] mb-[10px] p-[10px] rounded-[5px]">
<div>
<div className="truncate mb-[5px]">{item.title}</div>
<div className="flex justify-between mb-[5px]">
<span className="text-[#909090]">方向:<span className="text-[#222222]">{item.direction}</span></span>
<span className="text-[#909090]">类别:<span className="text-[#222222]">{item.issue_type}</span></span>
{listData.map((item: listDataItem, index: number) => {
return (
<div
key={index}
className="relative shadow-[0px_0px_2px_1px_#ededed] mb-[10px] p-[10px] rounded-[5px]"
>
<div>
<div className="truncate mb-[5px]">
{item.title}
</div>
<div className="flex justify-between mb-[5px]">
<span className="text-[#909090]">
方向:
<span className="text-[#222222]">
{item.direction}
</span>
</span>
<span className="text-[#909090]">
类别:
<span className="text-[#222222]">
{item.issue_type}
</span>
</span>
</div>
</div>
<div className="absolute top-[5px] right-[10px]">
<Checkbox
checked={item.checked}
onCheckedChange={() =>
listDataCheckedChange(index, !item.checked)
}
/>
</div>
</div>
<div className="absolute top-[5px] right-[10px]">
<input type="checkbox" checked={item.checked} onChange={() => listDataCheckedChange(index, !item.checked)} className="w-4 h-4 cursor-pointer accent-primary" />
</div>
</div>
))}
);
})}
</div>
)}
{searchType === "event" && (
<div className="bg-white p-[10px] grid grid-cols-5 gap-[10px]">
{listData.map((item, index) => (
<div key={index} className="relative shadow-[0px_0px_2px_1px_#ededed] mb-[10px] p-[10px] rounded-[5px]">
<div className="flex-1 min-w-0">
<p className="truncate hover:underline cursor-pointer" onClick={() => linkToA(item.url)}>{item.title}</p>
<p className="truncate text-[#909090]">首发新闻:{item.site_name}</p>
<p className="truncate text-[#909090]">事件时间:{item.first_publish_time}</p>
<p className="truncate text-[#909090]">关键词:{item.keywords?.join("、")}</p>
{listData.map((item: listDataItem, index: number) => {
return (
<div
key={index}
className="relative shadow-[0px_0px_2px_1px_#ededed] mb-[10px] p-[10px] rounded-[5px]"
>
<div className="flex-1 min-w-0">
<p
className="truncate hover:underline cursor-pointer"
onClick={() => linkToA(item.url)}
>
{item.title}
</p>
<p className="truncate text-[#909090]">
首发新闻:{item.site_name}
</p>
<p className="truncate text-[#909090]">
事件时间:{item.first_publish_time}
</p>
<p className="truncate text-[#909090]">
事件时间:{item.keywords?.join("、")}
</p>
</div>
<div className="absolute top-[5px] right-[10px]">
<Checkbox
checked={item.checked}
onCheckedChange={() =>
listDataCheckedChange(index, !item.checked)
}
/>
</div>
</div>
<div className="absolute top-[5px] right-[10px]">
<input type="checkbox" checked={item.checked} onChange={() => listDataCheckedChange(index, !item.checked)} className="w-4 h-4 cursor-pointer accent-primary" />
</div>
</div>
))}
);
})}
</div>
)}
{searchType === "threat" && (
<div className="bg-white p-[10px] grid grid-cols-5 gap-[10px]">
{listData.map((item, index) => (
<div key={index} className="relative shadow-[0px_0px_2px_1px_#ededed] mb-[10px] p-[10px] rounded-[5px]">
<div className="flex-1 min-w-0 pr-[25px]">
<p className="truncate hover:underline cursor-pointer" onClick={() => linkToA(item.url)}>{item.title}</p>
<p className="truncate text-[#909090]">发布平台:{item.site_name || "暂无数据"}</p>
<p className="truncate text-[#909090]">事件时间:{item.first_publish_time || "暂无数据"}</p>
<p className="truncate text-[#909090]">关键词:{item.keywords?.join("、") || "暂无数据"}</p>
{listData.map((item: listDataItem, index: number) => {
return (
<div
key={index}
className="relative shadow-[0px_0px_2px_1px_#ededed] mb-[10px] p-[10px] rounded-[5px]"
>
<div className="flex-1 min-w-0 pr-[25px]">
<p
className="truncate hover:underline cursor-pointer"
onClick={() => linkToA(item.url)}
>
{item.title}
</p>
<p className="truncate text-[#909090]">
发布平台:{item.site_name || "暂无数据"}
</p>
<p className="truncate text-[#909090]">
事件时间:
{item.first_publish_time || "暂无数据"}
</p>
<p className="truncate text-[#909090]">
关键词:
{item.keywords?.join("、") || "暂无数据"}
</p>
</div>
<div className="absolute top-[5px] right-[10px]">
<Checkbox
checked={item.checked}
onCheckedChange={() =>
listDataCheckedChange(index, !item.checked)
}
/>
</div>
</div>
<div className="absolute top-[5px] right-[10px]">
<input type="checkbox" checked={item.checked} onChange={() => listDataCheckedChange(index, !item.checked)} className="w-4 h-4 cursor-pointer accent-primary" />
</div>
</div>
))}
);
})}
</div>
)}
</div>
@@ -0,0 +1,46 @@
"use client";
import * as React from "react";
import { Check } from "lucide-react";
import { cn } from "@/lib/utils";
export interface CheckboxProps
extends Omit<
React.ButtonHTMLAttributes<HTMLButtonElement>,
"onChange" | "checked" | "type"
> {
checked?: boolean;
onCheckedChange?: (checked: boolean) => void;
}
/**
* 轻量级 Checkbox(不依赖 @radix-ui/react-checkbox,适配离线部署)。
* API 与 Radix/shadcn 对齐:受控 `checked` + `onCheckedChange(next)`。
*/
export const Checkbox = React.forwardRef<HTMLButtonElement, CheckboxProps>(
({ className, checked = false, onCheckedChange, disabled, ...props }, ref) => {
return (
<button
ref={ref}
type="button"
role="checkbox"
aria-checked={checked}
disabled={disabled}
data-state={checked ? "checked" : "unchecked"}
onClick={() => onCheckedChange?.(!checked)}
className={cn(
"peer flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input shadow-sm outline-none transition-colors",
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
"disabled:cursor-not-allowed disabled:opacity-50",
checked && "border-primary bg-primary text-primary-foreground",
className,
)}
{...props}
>
{checked && <Check className="size-3.5" strokeWidth={3} />}
</button>
);
},
);
Checkbox.displayName = "Checkbox";
+48
View File
@@ -1016,3 +1016,51 @@ html.lightblue .workspace-assistant-card::before {
html.lightblue .tiptap-writing-editor .ProseMirror ::selection {
background: color-mix(in srgb, #55a6fe 35%, transparent);
}
/* ── 来源上传弹窗:A系统基因库的分段单选条(custom-radio) ─────────────── */
/* 外层容器,模拟 nz-radio-group */
.custom-radio-group {
display: inline-flex;
align-items: center;
font-size: 12px;
}
/* 单个按钮样式 */
.custom-radio-btn {
position: relative;
display: inline-block;
padding: 5px 16px;
line-height: 12px;
background: #fff;
border: 1px solid #d9d9d9;
cursor: pointer;
/* 除第一个外向左移 1px,实现边框重叠,消除双层边框 */
margin-left: -1px;
transition: all 0.1s ease;
user-select: none;
}
/* 第一个按钮:左侧圆角 */
.custom-radio-btn:first-child {
margin-left: 0;
border-radius: 4px 0 0 4px;
}
/* 最后一个按钮:右侧圆角 */
.custom-radio-btn:last-child {
border-radius: 0 4px 4px 0;
}
/* 悬浮状态 */
.custom-radio-btn:hover {
color: #1890ff;
z-index: 1;
}
/* 选中状态 */
.custom-radio-btn.active {
background-color: #e6f7ff;
border-color: #1890ff;
color: #1890ff;
z-index: 2;
}