Files
谢宇宁andClaude Opus 5.5 8cdbfee08f Initial commit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 16:23:48 +07:00

98 lines
6.9 KiB
TypeScript

import type { Drama, HomeBlock, HomeData, PageData, Topic } from '../api/types';
import type { ApiClient } from './api';
import { recordList } from '../api/protocol';
import { contentPath, contentQuery, contentSegment, getRankPage, matchesContentKey, paginationPath, RANK_PAGES, type SiteRoute } from '../routing';
import { card, empty, escape, pagination, topicCard } from './render';
const labels:Record<string,string>={shorts:'短剧',topics:'专题推荐',theme:'题材短剧',tag:'标签短剧',config:'更多好剧',rank:'热播榜'};
const shortsSortNames=['最新更新','当前热播','独家原创','随机推荐'];
function channelSort(route:SiteRoute,search:string):number {
const value=Number(new URLSearchParams(search).get('sort')||1);
return (route.key==='shorts'?[1,2,3,4]:[1,2,3]).includes(value)?value:1;
}
export function rankNavigation(slug='hot'):string {
return `<nav class="channel-tabs rank-tabs" aria-label="排行榜">${RANK_PAGES.map(item=>`<a href="${item.path}"${item.slug===slug?' aria-current="page"':''}>${item.title}</a>`).join('')}</nav>`;
}
export function channelRequest(route:SiteRoute,search=''):{endpoint:string;query:Record<string,string|number>} {
const sort=channelSort(route,search);
const query:Record<string,string|number>={pageNum:route.page||1,pageSize:24};
let endpoint='';
if(route.key==='shorts'){endpoint='playlet/all/list';query.sortType=sort;}
if(route.key==='rank'){endpoint='playlet/hot/rank/list';query.rankType=getRankPage(route.value)!.rankType;}
if(route.key==='topics'){endpoint=route.value?'playlet/topic/detail':'playlet/topic/list';}
// The service exposes the same theme IDs through its paginated topic detail endpoint.
if(route.key==='theme')endpoint='playlet/topic/detail';
if(endpoint==='playlet/topic/detail'){
const identity=contentQuery(route.value!);
if(!identity.id)throw new Error('专题 slug 需要先解析为 ID');
query.id=identity.id;
}
if(route.key==='tag'){endpoint='playlet/tag/list';Object.assign(query,contentQuery(route.value!,'tagId','tagSlug'));query.sortType=sort;}
if(route.key==='config'){endpoint='playlet/home';return {endpoint,query:{}};}
if(!endpoint)throw new Error('该页面不存在');
return {endpoint,query};
}
/** Keep server rendering and browser navigation on the same documented contracts. */
export async function loadChannel(api:Pick<ApiClient,'get'>,route:SiteRoute,search='',signal?:AbortSignal):Promise<PageData<unknown>> {
let topic:Topic|undefined;
if((route.key==='theme'||route.key==='topics'&&route.value)&&contentQuery(route.value!).slug){
// topic/detail accepts only an ID. Resolve its slug through the complete topic directory.
const seen=new Set<number>();
for(let page=1;!topic;page++){
const data=await api.get<PageData<Topic>>('playlet/topic/list',{pageNum:page,pageSize:50},signal);
if(!Array.isArray(data.list))throw new Error('专题目录暂不可用');
const list=recordList<Topic>(data.list);
topic=list.find(item=>Number.isSafeInteger(item.id)&&item.id>0&&matchesContentKey(item,route.value!));
if(topic)break;
const before=seen.size;for(const item of list)if(Number.isSafeInteger(item.id)&&item.id>0)seen.add(item.id);
if(!list.length||Number.isSafeInteger(data.totalCount)&&data.totalCount!>=0&&seen.size>=data.totalCount!)return {missing:true};
if(seen.size===before)throw new Error('专题目录分页异常');
}
}
const {endpoint,query}=channelRequest(topic?{...route,value:String(topic.id)}:route,search);
if(route.key!=='config'){
const data=await api.get<PageData<unknown>>(endpoint,query,signal);
if(route.key==='theme'||route.key==='topics'&&route.value){
const expected=topic?.id??Number(route.value);
if(data.id!==undefined&&data.id!==expected||data.topicInfo?.id!==undefined&&data.topicInfo.id!==expected)return {missing:true};
return {...data,id:expected,slug:data.topicInfo?.slug||data.slug||topic?.slug};
}
return data;
}
// No paginated home-config endpoint exists in this API. Render only this block's real items.
const home=await api.get<HomeData>(endpoint,query,signal);
const block=recordList<HomeBlock>(home?.homeConfigData).find(item=>matchesContentKey(item,route.value!)&&item.status&&[1,3].includes(item.type));
if(!block)return {list:[],totalCount:0,missing:true};
const count=recordList(block.type===3?block.themeList:block.list).length;
return {...block,list:(route.page||1)>1?[]:recordList(block.list),themeList:(route.page||1)>1?[]:recordList(block.themeList),totalCount:count,pageNum:route.page||1,pageSize:24};
}
/** Shared by the initial server response and client-side page navigation. */
export function channelView(route:SiteRoute,data:PageData<unknown>,path:string) {
const ranking=route.key==='rank'?getRankPage(route.value):undefined;
const sort=channelSort(route,new URL(path,'https://site.invalid').search);
const title=ranking?.title||(typeof data.name==='string'?data.name:'')||(typeof data.topicInfo?.name==='string'?data.topicInfo.name:'')||labels[route.key]||'内容列表';
const subtitle=typeof data.desc==='string'?data.desc:typeof data.topicInfo?.desc==='string'?data.topicInfo.desc:'';
const total=typeof data.totalCount==='number'&&Number.isFinite(data.totalCount)?Math.max(0,data.totalCount):0;
const identity={id:Number(data.topicInfo?.id??data.id??route.value),slug:data.topicInfo?.slug||data.slug};
const pagePath=route.value&&['topics','theme','tag','config'].includes(route.key)&&contentSegment(identity)?contentPath(route.key==='theme'?'topics':route.key as 'topics'|'tag'|'config',identity)+new URL(path,'https://site.invalid').search:path;
const page=route.page||1,href=(page:number)=>paginationPath(pagePath,page);
let tabs=ranking?rankNavigation(ranking.slug):'';
if(!ranking&&route.key==='shorts'){
tabs=`<div class="channel-tabs" aria-label="页面分类">${shortsSortNames.map((name,index)=>`<button type="button" data-sort="${index+1}" aria-pressed="${sort===index+1}">${name}</button>`).join('')}</div>`;
}
let content:string;
if(route.key==='topics'&&!route.value){
content=`<div class="topic-recommendation-grid channel-topic-grid">${recordList<Topic>(data.list).map(item=>topicCard(item,false,2)).join('')||empty('暂无专题')}</div>`;
}else if(route.key==='config'&&data.type===3){
content=`<div class="topic-recommendation-grid">${recordList<Topic>(data.themeList).map(item=>topicCard(item,true,2)).join('')||empty()}</div>`;
}else{
const summary=ranking?`<div class="channel-summary"><span>${ranking.title}</span><span>共 ${total} 部作品</span></div>`:'';
content=tabs+summary+`<div class="cards${ranking?' channel-ranking-cards':''}">${recordList<Drama>(data.list).map((item,index)=>card(item,false,{headingLevel:2,...(ranking?{rank:(page-1)*24+index+1}:{})})).join('')||empty(typeof data.message==='string'?data.message:undefined)}</div>`;
}
return {title,subtitle,html:content+pagination(total,page,href,route.key==='config'?Math.max(24,total):24)};
}