feat: 播放页按来源列表切换,新增作者/搜索/hashtag 侧栏
- 播放上下文:作品链接带上来源(搜索、标签、作者、hashtag),上一条/下一条 在该列表内移动;当前作品不在列表中时退回全部作品的发布时间顺序 - 顶部提示标签显示当前列表与位置,点击打开侧栏列表,✕ 退出 - 侧栏改为「评论 | TA的作品 | 搜索结果/#hashtag」标签页,作品封面网格可就地切换 - 作者模式:打开「TA的作品」进入,关闭侧栏不退出,点提示 ✕ 后回到原来的列表 - 从搜索结果进入或点 hashtag 时自动打开对应列表 - 描述中的 hashtag 可点击,按完整 hashtag 精确匹配(#丰川祥子 不含 #丰川祥子cos) - 新增 /api/playlist;搜索 SQL 抽到 lib/search-query.ts 与播放页共用,保证顺序一致 - 移动端:底部抽屉、点作品后收起,媒体区支持上下滑切换 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+7
-90
@@ -2,26 +2,10 @@ import { NextResponse } from "next/server";
|
||||
import { json } from "@/lib/json";
|
||||
import { getFileUrl } from "@/lib/minio";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import {
|
||||
buildSnippet,
|
||||
highlightAll,
|
||||
parseSearchQuery,
|
||||
termToPgPattern,
|
||||
} from "@/lib/search";
|
||||
import { buildSnippet, highlightAll, parseSearchQuery } from "@/lib/search";
|
||||
import { searchRows } from "@/lib/search-query";
|
||||
import { isVideoContentTag } from "@/lib/video-tags";
|
||||
|
||||
type SearchRow = {
|
||||
awemeId: string;
|
||||
type: "video" | "image";
|
||||
score: number;
|
||||
inDesc: boolean;
|
||||
inTranscript: boolean;
|
||||
rawDesc: string;
|
||||
rawTranscript: string;
|
||||
contentTags: string[];
|
||||
total: number;
|
||||
};
|
||||
|
||||
export async function GET(req: Request) {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const q = (searchParams.get("q") || "").trim();
|
||||
@@ -43,79 +27,12 @@ export async function GET(req: Request) {
|
||||
return json({ results: [], total: 0, page, limit, terms });
|
||||
}
|
||||
|
||||
// 关键词已在 parseSearchQuery 里转为小写,这里转成正则后与 lower() 后的文本匹配
|
||||
const patterns = terms.map(termToPgPattern);
|
||||
const excludePatterns = excludes.map(termToPgPattern);
|
||||
|
||||
try {
|
||||
// 子串匹配:每个关键词都必须出现在「描述 + 语音转写」里(不区分大小写),
|
||||
// 不依赖分词,所以输入任意片段都能搜到。数据量小,全表扫描足够快。
|
||||
// 打分:描述命中权重更高,按出现次数累加(设上限防止长转写刷分);同分按发布时间倒序。
|
||||
// 没有关键词、只按标签筛选时,score 都是 0,等价于按时间排序。
|
||||
const rows = await prisma.$queryRaw<SearchRow[]>`
|
||||
WITH params AS (
|
||||
SELECT ${patterns}::text[] AS terms, ${excludePatterns}::text[] AS excludes
|
||||
),
|
||||
docs AS (
|
||||
SELECT
|
||||
v.aweme_id AS "awemeId",
|
||||
'video'::text AS type,
|
||||
v."desc" AS "rawDesc",
|
||||
COALESCE(array_to_string(vt.transcript, E'\n'), '') AS "rawTranscript",
|
||||
v.created_at AS created,
|
||||
v.manual_content_tags AS "contentTags"
|
||||
FROM "Video" v
|
||||
LEFT JOIN "VideoTranscript" vt ON vt."videoId" = v.aweme_id
|
||||
WHERE ${contentTag}::text IS NULL
|
||||
OR v.manual_content_tags @> ARRAY[${contentTag}]::text[]
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
ip.aweme_id,
|
||||
'image'::text,
|
||||
ip."desc",
|
||||
'',
|
||||
ip.created_at,
|
||||
ARRAY[]::text[]
|
||||
FROM "ImagePost" ip
|
||||
WHERE ${contentTag}::text IS NULL
|
||||
),
|
||||
lowered AS (
|
||||
SELECT docs.*, lower(docs."rawDesc") AS d, lower(docs."rawTranscript") AS t
|
||||
FROM docs
|
||||
),
|
||||
matched AS (
|
||||
SELECT
|
||||
l.*,
|
||||
COALESCE((
|
||||
SELECT SUM(
|
||||
4 * LEAST(regexp_count(l.d, term), 3)
|
||||
+ LEAST(regexp_count(l.t, term), 10)
|
||||
)
|
||||
FROM unnest(p.terms) term
|
||||
), 0)::float8 AS score,
|
||||
EXISTS (SELECT 1 FROM unnest(p.terms) term WHERE l.d ~ term) AS "inDesc",
|
||||
EXISTS (SELECT 1 FROM unnest(p.terms) term WHERE l.t ~ term) AS "inTranscript"
|
||||
FROM lowered l, params p
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM unnest(p.terms) term
|
||||
WHERE l.d !~ term AND l.t !~ term
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM unnest(p.excludes) x
|
||||
WHERE l.d ~ x OR l.t ~ x
|
||||
)
|
||||
)
|
||||
SELECT
|
||||
"awemeId", type, score, "inDesc", "inTranscript",
|
||||
"rawDesc", "rawTranscript", "contentTags",
|
||||
(COUNT(*) OVER ())::int AS total
|
||||
FROM matched
|
||||
ORDER BY score DESC, created DESC, "awemeId"
|
||||
OFFSET ${offset}
|
||||
LIMIT ${limit};
|
||||
`;
|
||||
const rows = await searchRows(
|
||||
{ terms, excludes, contentTag },
|
||||
offset,
|
||||
limit,
|
||||
);
|
||||
|
||||
const videoIds = rows
|
||||
.filter((r) => r.type === "video")
|
||||
|
||||
Reference in New Issue
Block a user