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:
@@ -0,0 +1,49 @@
|
||||
// 从作品描述里解析抖音 hashtag(#xxx)。
|
||||
// 前端渲染可点击的 hashtag 与后端按 hashtag 精确筛选作品共用这里的边界规则,
|
||||
// 保证「点了 #丰川祥子 看到的列表」不会混进 #丰川祥子cos。
|
||||
|
||||
// hashtag 在遇到空白、下一个 #、或这些标点时结束
|
||||
const STOP_CHARS =
|
||||
"#,。!?、;:,.!?;:()()【】\\[\\]《》<>\"'“”‘’…~~|/\\\\@";
|
||||
const MAX_HASHTAG_LENGTH = 50;
|
||||
|
||||
const HASHTAG_RE = new RegExp(`#([^\\s${STOP_CHARS}]+)`, "gu");
|
||||
|
||||
export type DescPart =
|
||||
| { type: "text"; text: string }
|
||||
| { type: "hashtag"; text: string; name: string };
|
||||
|
||||
/** 把描述切成普通文字与 hashtag 片段。 */
|
||||
export function parseDescHashtags(desc: string): DescPart[] {
|
||||
const parts: DescPart[] = [];
|
||||
let cursor = 0;
|
||||
for (const m of desc.matchAll(HASHTAG_RE)) {
|
||||
const start = m.index ?? 0;
|
||||
if (start > cursor) {
|
||||
parts.push({ type: "text", text: desc.slice(cursor, start) });
|
||||
}
|
||||
parts.push({ type: "hashtag", text: m[0], name: m[1] });
|
||||
cursor = start + m[0].length;
|
||||
}
|
||||
if (cursor < desc.length) {
|
||||
parts.push({ type: "text", text: desc.slice(cursor) });
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
/** 规范化 hashtag 名(去掉开头的 #、首尾空白,限制长度);不合法时返回 null。 */
|
||||
export function normalizeHashtag(raw: string): string | null {
|
||||
const name = raw.trim().replace(/^#+/, "").slice(0, MAX_HASHTAG_LENGTH);
|
||||
if (!name) return null;
|
||||
if (new RegExp(`[\\s${STOP_CHARS}]`, "u").test(name)) return null;
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 匹配 lower() 后描述文本的 Postgres 正则(ARE):#名字 之后必须是结束符或文本末尾。
|
||||
* 名字里的正则特殊字符会被转义。
|
||||
*/
|
||||
export function hashtagToPgPattern(name: string): string {
|
||||
const escaped = name.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
return `#${escaped}([\\s${STOP_CHARS}]|$)`;
|
||||
}
|
||||
Reference in New Issue
Block a user