From 39ec8185e9f41e4c31e3c9f86a93794419cb6f5b Mon Sep 17 00:00:00 2001 From: root Date: Tue, 6 Oct 2026 12:04:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E8=B5=84=E6=BA=90=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=E7=BB=93=E6=9E=9C=E6=94=B9=E7=89=88(=E8=A7=84?= =?UTF-8?q?=E6=A0=BC=E6=A0=87=E7=AD=BE=E3=80=81=E4=BF=83=E9=94=80=E5=89=8D?= =?UTF-8?q?=E7=BD=AE=E3=80=81=E6=95=B0=E5=AD=97=E5=AF=B9=E9=BD=90=E3=80=81?= =?UTF-8?q?=E6=8C=89=E4=BD=9C=E5=93=81=E5=88=86=E7=BB=84=E3=80=81=E8=A7=84?= =?UTF-8?q?=E5=88=99=E5=88=A4=E5=AE=9A)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 参考 MP 资源卡片:促销放在标题前(免费用 success,其余折扣中性加粗,带剩余时间); 规格拆成中性小标签(季集 → 分辨率 → 来源 → 编码 → HDR → 音频 → 语言 → 制作组); 站点 / 大小 / 做种·下载 / 发布时间宽屏定宽对齐、窄屏折行,做种加粗、为 0 时弱化; 结果涉及多部作品时按作品分组(组头 = 作品名 + 年份 + 库存状态),只有一部时不重复作品名; 每行显示服务端规则判定(命中第 N 档 / 不满足 / 与库里版本比较)。保留工作台、委托智能体、下载。 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei --- .../app/components/search/release-results.tsx | 247 ++++++++++++++---- .../components/search/release-rows.test.ts | 203 ++++++++++++++ .../app/components/search/search-labels.ts | 164 +++++++++++- 3 files changed, 555 insertions(+), 59 deletions(-) create mode 100644 apps/web/app/components/search/release-rows.test.ts diff --git a/apps/web/app/components/search/release-results.tsx b/apps/web/app/components/search/release-results.tsx index dbb26476..a8c4dcf7 100644 --- a/apps/web/app/components/search/release-results.tsx +++ b/apps/web/app/components/search/release-results.tsx @@ -11,6 +11,7 @@ import { PackageSearch, RefreshCw, TriangleAlert, + UsersRound, } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import { Link } from "react-router"; @@ -25,7 +26,7 @@ import { Input } from "~/components/ui/input.tsx"; import { Pagination } from "~/components/ui/pagination.tsx"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select.tsx"; import { ReleaseDetailSheet } from "~/components/workbench/release-detail-sheet.tsx"; -import { relTime } from "~/components/workbench/release-fields.ts"; +import { absTime, relTime, remainingLabel } from "~/components/workbench/release-fields.ts"; import { errorMessage } from "~/lib/api.ts"; import { cn, formatBytes } from "~/lib/utils.ts"; import { QuickFilterBar } from "./quick-filter-bar.tsx"; @@ -37,11 +38,25 @@ import { useQuickFilters, } from "./quick-filter-state.ts"; import { delegateToAgent, SearchDownloadDialog } from "./search-download-dialog.tsx"; -import { gbToBytes, INVENTORY_STATE, promoLabel, SITE_STATUS, SORT_OPTIONS } from "./search-labels.ts"; +import { + gbToBytes, + groupByWork, + hrBadge, + INVENTORY_STATE, + promoBadge, + releaseSpecTags, + SITE_STATUS, + SORT_OPTIONS, + verdictView, + vsLibraryLabel, + type WorkGroup, +} from "./search-labels.ts"; // 资源段(SEARCH-2 第二阶段):按作品或关键词、在选中的站点上搜;快速筛选(SEARCH-3:分辨率/编码/版本/站点/ // 促销/HDR/H&R/制作组/季集,多选带计数),同一行右侧是大小区间与排序(POLISH-1,手机收进抽屉);游标翻页; // 行操作同站点工作台(库存叠加、下载、委托智能体)。耗时、查询词放进「技术信息」。 +// SEARCH-5(参考 MP 资源卡片):促销放标题前、规格拆成中性标签、关键数字定宽对齐、按作品分组, +// 每行附规则判定(命中第几档 / 不满足什么 / 与库里版本比较,服务端 ruleVerdict)。 /** URL 里的资源筛选(字符串形态,空串 = 不筛) */ export type ReleaseFilterState = { @@ -237,11 +252,7 @@ export function ReleaseResults({ className="py-12" /> ) : ( - + )} {rows.length > 0 || res.hasNextPage ? : null} @@ -420,60 +431,180 @@ function SizeSortControls({ ); } -function ReleaseRow({ r, onOpen, onDownload }: { r: SearchRelease; onOpen: () => void; onDownload: () => void }) { - const promo = promoLabel(r.promotion.type); - const inv = r.inventory ? INVENTORY_STATE[r.inventory.state] : null; - const p = r.parsed; - const tech = [p.resolution, p.source, p.videoCodec, ...p.hdr, p.releaseGroup].filter(Boolean).join(" · "); +/** 作品组头:作品名 + 年份 + 未确认 + 库存状态 + 条数 */ +function WorkHeader({ g, count }: { g: WorkGroup; count: number }) { + const inv = g.inventory ? INVENTORY_STATE[g.inventory.state] : null; return ( -
  • -
    - - - {promo ? {promo} : null} - {r.hr === true ? H&R : null} - {r.hr === "unknown" ? H&R 未知 : null} - -
    - {r.subtitle ?

    {r.subtitle}

    : null} -
    - {r.siteName} - {r.category.name ? {r.category.name} : null} - {r.sizeBytes === null ? "大小未知" : formatBytes(r.sizeBytes, 1)} - - - {r.seeders ?? "—"} - - {r.leechers ?? "—"} - - 发布 {relTime(r.uploadedAt)} - {tech ? {tech} : null} -
    -
    - - {r.entity ? ( - <> - - {r.entity.title} - {r.entity.year ? ` (${r.entity.year})` : ""} - - {r.entityMatch === "alias" ? 未确认 : null} - {inv ? {inv.label} : null} - - ) : ( - 未关联本地作品 +
    + {g.entity ? ( + <> + + {g.entity.title} + {g.entity.year ? ` (${g.entity.year})` : ""} + + {g.entityMatch === "alias" ? 未确认 : null} + {inv ? {inv.label} : null} + + ) : ( + 未关联本地作品 + )} + {count} 条 +
    + ); +} + +/** 按作品分组的结果列表;只涉及一部作品时作品名只在上方出现一次 */ +function ReleaseList({ + rows, + onOpen, + onDownload, +}: { + rows: SearchRelease[]; + onOpen: (r: SearchRelease) => void; + onDownload: (r: SearchRelease) => void; +}) { + const { grouped, groups } = groupByWork(rows); + const single = !grouped && groups[0]?.entity ? groups[0] : null; + const row = (r: SearchRelease) => ( + onOpen(r)} onDownload={() => onDownload(r)} /> + ); + if (!grouped) + return ( + <> + {single ? : null} +
      {rows.map(row)}
    + + ); + return ( +
    + {groups.map((g) => ( +
    + +
      {g.items.map(row)}
    +
    + ))} +
    + ); +} + +/** 促销放在标题前:免费用 success(属状态),其余折扣中性加粗;带剩余时间 */ +function PromoTag({ r }: { r: SearchRelease }) { + const promo = promoBadge(r); + if (!promo) return null; + const left = remainingLabel(r.promotion.expiresAt); + const text = left ? `${promo.label} · ${left}` : promo.label; + const title = r.promotion.expiresAt ? `促销截止 ${absTime(r.promotion.expiresAt)}` : undefined; + return promo.tone === "success" ? ( + + + {text} + + + ) : ( + + {text} + + ); +} + +/** 规格标签:中性小标签,固定顺序;制作组带图标区分 */ +function SpecTagList({ r }: { r: SearchRelease }) { + const tags = releaseSpecTags(r.parsed); + if (!tags.length) return null; + return ( + + {tags.map((t) => ( + + {t.dim === "group" ? : null} + {t.label} + ))} + + ); +} + +/** 规则判定 + 与库里版本比较 */ +function VerdictLine({ r }: { r: SearchRelease }) { + const v = r.ruleVerdict; + if (!v) return null; + const view = verdictView(v); + const vs = vsLibraryLabel(v); + return ( + + + {view.label} + + {vs ? {vs} : null} + + ); +} + +/** + * 资源行:左边 = 促销 + 标题 / 规格标签 / 规则判定;右边 = 站点、大小、做种·下载、发布时间定宽对齐, + * 下面是操作。宽屏像表格一样上下对齐,窄屏右栏折到下面。 + */ +function ReleaseRow({ r, onOpen, onDownload }: { r: SearchRelease; onOpen: () => void; onDownload: () => void }) { + const hr = hrBadge(r); + const seedless = r.seeders === 0 || r.seeders === null; + return ( +
  • +
    +
    + + + {hr ? ( + + {hr.label} + + ) : null} +
    + {r.subtitle ?

    {r.subtitle}

    : null} + + +
    +
    +
    + + {r.siteName} + + {r.sizeBytes === null ? "—" : formatBytes(r.sizeBytes, 1)} + + + + {r.seeders ?? "—"} + + + {r.leechers ?? "—"} + + + {relTime(r.uploadedAt)} + +
    {r.sourceReleaseId ? (