This commit is contained in:
谢宇宁
2026-10-23 15:07:10 +07:00
parent 0d924839f3
commit f96f722186
10 changed files with 878 additions and 201 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts"; import "./.next-dev/dev/types/routes.d.ts";
import "./.next/types/root-params.d.ts"; import "./.next-dev/dev/types/root-params.d.ts";
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+3 -4
View File
@@ -415,10 +415,9 @@ try {
name + " action must be removed", name + " action must be removed",
); );
assert.equal(await page.locator(".hk-playback-comments, .hk-playback-composer").count(), 0); assert.equal(await page.locator(".hk-playback-comments, .hk-playback-composer").count(), 0);
assert.deepEqual( // Recommendations stay in the main column; the trending list sits in the right sidebar.
await page.locator(".hk-playback-dual > *").evaluateAll((nodes) => nodes.map((node) => node.className)), assert.equal(await page.locator(".hk-playback-body__main > .hk-playback-like").count(), 1);
["hk-playback-like", "hk-playback-hot"], assert.equal(await page.locator(".hk-playback-body__aside > .hk-playback-hot").count(), 1);
);
assert.match( assert.match(
(await page.locator(".hk-playback-like").textContent()) || "", (await page.locator(".hk-playback-like").textContent()) || "",
/猜你喜欢测试/, /猜你喜欢测试/,
+1
View File
@@ -35,6 +35,7 @@ import "@/styles/pages/mobile-search.css";
import "@/styles/pages/site-navigation.css"; import "@/styles/pages/site-navigation.css";
import "@/styles/pages/home-road.css"; import "@/styles/pages/home-road.css";
import "@/styles/pages/playback.css"; import "@/styles/pages/playback.css";
import "@/styles/overrides/drama-tiles.css";
import "@/styles/overrides/pwa-safe-area.css"; import "@/styles/overrides/pwa-safe-area.css";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { PwaProvider } from "@/components/pwa/PwaProvider"; import { PwaProvider } from "@/components/pwa/PwaProvider";
+39 -13
View File
@@ -9,7 +9,6 @@ import {
cardTags, cardTags,
categoryURL, categoryURL,
displayName, displayName,
episodeChip,
episodeLabel, episodeLabel,
episodeStatus, episodeStatus,
externalProps, externalProps,
@@ -49,25 +48,50 @@ export function CardTags({
</> </>
); );
} }
/** Episode chip precedes the theme tags, matching the reference card meta row. */ /** Genre line under the title: slash-separated theme tags (episode info lives on the cover). */
export function DramaCardMeta({ item }: { item: CatalogRecord }) { export function DramaCardMeta({ item }: { item: CatalogRecord }) {
const chip = episodeChip(item); const tags = cardTags(item);
if (!tags.length) return null;
return ( return (
<p className="hk-drama-card__meta hk-drama-card__meta--ep"> <p className="hk-drama-card__meta hk-drama-card__genres">
{chip && ( {tags.map((tag, index) => (
<span <Fragment key={tag.Name}>
className="hk-drama-card__meta-ep" {index > 0 && (
data-status={num(item, "UpdateStatus") === 1 ? "serial" : "done"} <span className="hk-drama-card__genre-sep" aria-hidden="true">
> /
{chip}
</span> </span>
)} )}
<span className="hk-drama-card__meta-tags"> {tag.URL ? (
<CardTags item={item} /> <a href={tag.URL} {...externalProps(tag.URL)}>
</span> {tag.Name}
</a>
) : (
<span>{tag.Name}</span>
)}
</Fragment>
))}
</p> </p>
); );
} }
/** Cover corner badge: episode count first, then serial/finished state. */
export function DramaCardEpisodes({ item }: { item: CatalogRecord }) {
const total = num(item, "TotalEpisode"),
serial = num(item, "UpdateStatus") === 1;
if (!total && !serial) return null;
return (
<span
className="hk-drama-card__eps"
data-status={serial ? "serial" : "done"}
>
{total > 0 && (
<>
<b>{total}</b> 集 ·{" "}
</>
)}
{serial ? "连更" : "完结"}
</span>
);
}
export function DramaCard({ export function DramaCard({
item, item,
recent = false, recent = false,
@@ -94,6 +118,7 @@ export function DramaCard({
) : str(item, "Anchor") === "原创" ? ( ) : str(item, "Anchor") === "原创" ? (
<span className="hk-badge hk-badge--original">原创</span> <span className="hk-badge hk-badge--original">原创</span>
) : null} ) : null}
<DramaCardEpisodes item={item} />
</EncryptedBackground> </EncryptedBackground>
<h3 className="hk-drama-card__title"> <h3 className="hk-drama-card__title">
<a href={href}>{name}</a> <a href={href}>{name}</a>
@@ -278,6 +303,7 @@ export function PlayRecommendationCard({ item }: { item: CatalogRecord }) {
href={href} href={href}
src={str(item, "Cover")} src={str(item, "Cover")}
> >
<DramaCardEpisodes item={item} />
</EncryptedBackground> </EncryptedBackground>
<h3 className="hk-drama-card__title"> <h3 className="hk-drama-card__title">
<a href={href}>{displayName(item)}</a> <a href={href}>{displayName(item)}</a>
+74 -29
View File
@@ -25,7 +25,14 @@ import {
import { coinGate } from "./model"; import { coinGate } from "./model";
import { PlaybackStage } from "./PlaybackStage"; import { PlaybackStage } from "./PlaybackStage";
import { AppOnlyDialog } from "./AppOnlyDialog"; import { AppOnlyDialog } from "./AppOnlyDialog";
import { GuessLike, HotList, PlaybackShare, Related, Synopsis } from "./Shared"; import {
GuessLike,
HotList,
PlaybackShare,
Related,
SectionEyebrow,
Synopsis,
} from "./Shared";
export function DramaWatchPage({ data }: { data: Data }) { export function DramaWatchPage({ data }: { data: Data }) {
const p = data.playlet || {}, const p = data.playlet || {},
id = num(p, "ID"), id = num(p, "ID"),
@@ -109,7 +116,7 @@ export function DramaWatchPage({ data }: { data: Data }) {
</a> </a>
<Icon name="chevron-right" className="hk-breadcrumb__sep" /> <Icon name="chevron-right" className="hk-breadcrumb__sep" />
<a className="hk-breadcrumb__item" href="/dramas/"> <a className="hk-breadcrumb__item" href="/dramas/">
短剧 短剧库
</a> </a>
<Icon name="chevron-right" className="hk-breadcrumb__sep" /> <Icon name="chevron-right" className="hk-breadcrumb__sep" />
<a className="hk-breadcrumb__item" href={playURL(get(p, "Slug"), id)}> <a className="hk-breadcrumb__item" href={playURL(get(p, "Slug"), id)}>
@@ -128,8 +135,14 @@ export function DramaWatchPage({ data }: { data: Data }) {
onPlaying={onPlaying} onPlaying={onPlaying}
autoPlay={autoPlay} autoPlay={autoPlay}
/> />
<div className="hk-playback-body">
<div className="hk-playback-body__main">
<section className="hk-playback-info" aria-label="剧集信息"> <section className="hk-playback-info" aria-label="剧集信息">
<div className="hk-playback-meta"> <div className="hk-playback-meta">
<p className="hk-playback-meta__eyebrow">
<span>正在播放</span>
第 {episode} 集{total > 0 ? ` / 共 ${total} 集` : ""}
</p>
<div className="hk-playback-meta__title-row"> <div className="hk-playback-meta__title-row">
<h1 className="hk-playback-meta__title">{name}</h1> <h1 className="hk-playback-meta__title">{name}</h1>
{get(p, "IsFeatured") ? ( {get(p, "IsFeatured") ? (
@@ -138,23 +151,40 @@ export function DramaWatchPage({ data }: { data: Data }) {
<span className="hk-badge hk-badge--original">原创</span> <span className="hk-badge hk-badge--original">原创</span>
) : null} ) : null}
</div> </div>
<div className="hk-playback-meta__chips"> <dl className="hk-playback-facts">
{total > 0 && <span className="hk-playback-chip">全 {total} 集</span>} {total > 0 && (
<span className="hk-playback-chip">{status}</span> <div className="hk-playback-facts__item">
{year && <span className="hk-playback-chip">{year}</span>} <dt>集数</dt>
<span className="hk-playback-chip">1080P</span> <dd>{total} 集</dd>
</div>
)}
<div className="hk-playback-facts__item">
<dt>状态</dt>
<dd data-status={status === "连载中" ? "serial" : "done"}>{status}</dd>
</div>
{year && (
<div className="hk-playback-facts__item">
<dt>上线</dt>
<dd>{year} 年</dd>
</div>
)}
<div className="hk-playback-facts__item">
<dt>画质</dt>
<dd>1080P</dd>
</div> </div>
{["hk-desktop-only", "hk-mobile-only"].map((device) => (
<p className={`hk-playback-meta__stats ${device}`} key={device}>
{anchor && !["原创", "已注销"].includes(anchor) && ( {anchor && !["原创", "已注销"].includes(anchor) && (
<span className="hk-playback-meta__stat">主演 {anchor}</span> <div className="hk-playback-facts__item">
<dt>主演</dt>
<dd>{anchor}</dd>
</div>
)} )}
{!!themes.length && ( {!!themes.length && (
<span className="hk-playback-meta__stat"> <div className="hk-playback-facts__item hk-playback-facts__item--wide">
{device === "hk-desktop-only" ? "题材 " : ""} <dt>题材</dt>
<dd>
{themes.map((tag, index) => ( {themes.map((tag, index) => (
<span key={tag.Name}> <span key={tag.Name}>
{index ? " " : ""} {index ? " / " : ""}
{tag.URL ? ( {tag.URL ? (
<a <a
className="hk-playback-meta__tag" className="hk-playback-meta__tag"
@@ -168,16 +198,18 @@ export function DramaWatchPage({ data }: { data: Data }) {
)} )}
</span> </span>
))} ))}
</span> </dd>
</div>
)} )}
<span className="hk-playback-meta__stat"> <div className="hk-playback-facts__item">
热度 {formatLikeCount(get(p, "HeatValue"))} <dt>热度</dt>
</span> <dd>{formatLikeCount(get(p, "HeatValue"))}</dd>
<span className="hk-playback-meta__stat"> </div>
收藏 <span>{formatLikeCount(get(p, "CountCollect"))}</span> <div className="hk-playback-facts__item">
</span> <dt>收藏</dt>
</p> <dd>{formatLikeCount(get(p, "CountCollect"))}</dd>
))} </div>
</dl>
</div> </div>
<div className="hk-playback-actions"> <div className="hk-playback-actions">
<button <button
@@ -208,14 +240,18 @@ export function DramaWatchPage({ data }: { data: Data }) {
</a> </a>
</div> </div>
<Synopsis <Synopsis
name={name}
text={first(get(p, "Desc"), get(p, "Description"))} text={first(get(p, "Desc"), get(p, "Description"))}
tags={namedTags(get(p, "TagIDs"), get(p, "TagNames"), get(p, "TagSlugs"))} tags={namedTags(get(p, "TagIDs"), get(p, "TagNames"), get(p, "TagSlugs"))}
/> />
</section> </section>
<section className="hk-playback-episodes" aria-label="分集"> <section className="hk-playback-episodes" aria-label="全集列表">
<div className="hk-playback-episodes__head"> <div className="hk-playback-episodes__head">
<div className="hk-playback-episodes__head-left"> <div className="hk-playback-episodes__head-left">
<h2 className="hk-playback-episodes__title">分集</h2> <div className="hk-section-heading">
<SectionEyebrow>EPISODES</SectionEyebrow>
<h2 className="hk-playback-episodes__title">全集列表</h2>
</div>
<span className="hk-playback-episodes__count"> <span className="hk-playback-episodes__count">
共 {total || chapters.length} 集 {status} 共 {total || chapters.length} 集 {status}
</span> </span>
@@ -258,13 +294,20 @@ export function DramaWatchPage({ data }: { data: Data }) {
key={chid} key={chid}
className={`hk-playback-ep${chid === chapterID ? " hk-is-active" : ""}${coinGate(chapter) ? " hk-playback-ep--coin" : ""}`} className={`hk-playback-ep${chid === chapterID ? " hk-is-active" : ""}${coinGate(chapter) ? " hk-playback-ep--coin" : ""}`}
href={chapterPlayURL(get(p, "Slug"), id, chid)} href={chapterPlayURL(get(p, "Slug"), id, chid)}
aria-label={`第 ${ep} 集`}
> >
{ep} <span className="hk-playback-ep__label" aria-hidden="true">EP</span>
<span className="hk-playback-ep__num">{String(ep).padStart(2, "0")}</span>
</a> </a>
); );
}, },
)} )}
{!chapters.length && <span className="hk-playback-ep hk-is-active">1</span>} {!chapters.length && (
<span className="hk-playback-ep hk-is-active">
<span className="hk-playback-ep__label" aria-hidden="true">EP</span>
<span className="hk-playback-ep__num">01</span>
</span>
)}
</div> </div>
<div className="hk-playback-episodes__legend"> <div className="hk-playback-episodes__legend">
<span className="hk-playback-legend"> <span className="hk-playback-legend">
@@ -292,15 +335,17 @@ export function DramaWatchPage({ data }: { data: Data }) {
/> />
</div> </div>
<Related data={data} /> <Related data={data} />
<section className="hk-playback-dual" aria-label="推荐与热榜">
<GuessLike data={data} playletId={id} /> <GuessLike data={data} playletId={id} />
<HotList data={data} />
</section>
<div className="hk-playback-promo hk-playback-promo--bottom"> <div className="hk-playback-promo hk-playback-promo--bottom">
<AdBanners <AdBanners
items={array(get(data.advitise, "DetailBannerList")).slice(2, 4)} items={array(get(data.advitise, "DetailBannerList")).slice(2, 4)}
/> />
</div> </div>
</div>
<div className="hk-playback-body__aside">
<HotList data={data} />
</div>
</div>
</main> </main>
</div> </div>
{appOnly && ( {appOnly && (
+28 -7
View File
@@ -66,12 +66,22 @@ export function PlaybackShare({
</button> </button>
); );
} }
/** Small uppercase label above a section heading; decorative, so hidden from assistive tech. */
export function SectionEyebrow({ children }: { children: string }) {
return (
<span className="hk-section-eyebrow" aria-hidden="true">
{children}
</span>
);
}
export function Synopsis({ export function Synopsis({
text, text,
tags = [], tags = [],
name = "",
}: { }: {
text: string; text: string;
tags?: NamedTag[]; tags?: NamedTag[];
name?: string;
}) { }) {
const [expanded, setExpanded] = useState(false), const [expanded, setExpanded] = useState(false),
[overflow, setOverflow] = useState(false), [overflow, setOverflow] = useState(false),
@@ -94,7 +104,12 @@ export function Synopsis({
return ( return (
<div className={`hk-playback-synopsis${expanded ? " hk-is-expanded" : ""}`}> <div className={`hk-playback-synopsis${expanded ? " hk-is-expanded" : ""}`}>
<div className="hk-playback-synopsis__head"> <div className="hk-playback-synopsis__head">
<h2 className="hk-playback-synopsis__title">剧情简介</h2> <div className="hk-section-heading">
<SectionEyebrow>STORY</SectionEyebrow>
<h2 className="hk-playback-synopsis__title">
{name ? `《${name}》剧情介绍` : "剧情介绍"}
</h2>
</div>
{(overflow || expanded) && ( {(overflow || expanded) && (
<button <button
className="hk-playback-synopsis__more" className="hk-playback-synopsis__more"
@@ -131,9 +146,12 @@ export function Synopsis({
export function HotList({ data }: { data: Data }) { export function HotList({ data }: { data: Data }) {
const items = array(data.hotList); const items = array(data.hotList);
return ( return (
<aside className="hk-playback-hot" aria-label="热播榜单"> <aside className="hk-playback-hot" aria-label="人气排行">
<div className="hk-playback-hot__head"> <div className="hk-playback-hot__head">
<h2 className="hk-playback-hot__title">热播榜单</h2> <div className="hk-section-heading">
<SectionEyebrow>TRENDING</SectionEyebrow>
<h2 className="hk-playback-hot__title">人气排行</h2>
</div>
<a <a
className="hk-playback-hot__more" className="hk-playback-hot__more"
href={String(data.hotMoreURL || "/dramas/all/0/2/")} href={String(data.hotMoreURL || "/dramas/all/0/2/")}
@@ -238,11 +256,11 @@ export function GuessLike({
} }
}; };
return ( return (
<section className="hk-playback-like" aria-label="同类推荐"> <section className="hk-playback-like" aria-label="相似短剧">
<div className="hk-playback-like__head"> <div className="hk-playback-like__head">
<div className="hk-playback-like__titles"> <div className="hk-playback-like__titles hk-section-heading">
<h2 className="hk-playback-like__title">同类推荐</h2> <SectionEyebrow>MORE LIKE THIS</SectionEyebrow>
<span className="hk-playback-like__desc hk-desktop-only">与本剧题材相近的好剧</span> <h2 className="hk-playback-like__title">相似短剧推荐</h2>
</div> </div>
<button <button
type="button" type="button"
@@ -268,7 +286,10 @@ export function Related({ data }: { data: Data }) {
return ( return (
<section className="hk-playback-related hk-mobile-only" aria-label="大家都在看"> <section className="hk-playback-related hk-mobile-only" aria-label="大家都在看">
<div className="hk-playback-related__head"> <div className="hk-playback-related__head">
<div className="hk-section-heading">
<SectionEyebrow>ALSO WATCHING</SectionEyebrow>
<h2 className="hk-playback-related__title">大家都在看</h2> <h2 className="hk-playback-related__title">大家都在看</h2>
</div>
<a className="hk-playback-related__more" href="/featured/"> <a className="hk-playback-related__more" href="/featured/">
更多 更多
<Icon name="chevron-right" /> <Icon name="chevron-right" />
+209
View File
@@ -0,0 +1,209 @@
/* 短剧卡片统一外观(全站共用,加载在页面样式之后):
直角偏小的封面 + 细描边,集数/状态角标放封面右下,悬停时封面上浮并出现底部强调条,
标题下方为斜杠分隔的题材行。选择器带父级类名,覆盖各页面零特异性的局部卡片样式。 */
.hk-drama-card {
gap: 8px;
}
.hk-drama-card .hk-drama-card__cover {
border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.28);
background-image:
linear-gradient(180deg, rgba(0, 0, 0, 0) 58%, rgba(0, 0, 0, 0.72) 100%),
var(--cover);
transition:
transform 220ms ease,
border-color 220ms ease,
box-shadow 220ms ease;
}
/* 悬停遮罩:去掉居中圆形“立即播放”,改为左下角三角播放键 + 底部强调条 */
.hk-drama-card .hk-drama-card__cover::before {
inset: auto 0 0 0;
height: 3px;
border-radius: 0;
background: var(--accent);
opacity: 0;
transform: scaleX(0);
transform-origin: left center;
transition:
opacity 220ms ease,
transform 260ms ease;
}
.hk-drama-card .hk-drama-card__cover::after {
top: auto;
left: 10px;
bottom: 12px;
width: 30px;
height: 30px;
padding: 0;
border-radius: 50%;
background: var(--accent) url("/static/images/play.svg") 55% 50% / 12px 12px no-repeat;
box-shadow: 0 4px 12px rgba(255, 15, 83, 0.45);
content: "";
transform: translateY(6px);
}
.hk-drama-card .hk-drama-card__cover:focus-visible {
border-color: rgba(255, 15, 83, 0.6);
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.4);
}
.hk-drama-card .hk-drama-card__cover:focus-visible::before {
transform: scaleX(1);
}
.hk-drama-card .hk-drama-card__cover:focus-visible::after {
transform: translateY(0);
}
@media (hover: hover) and (pointer: fine) {
.hk-drama-card .hk-drama-card__cover:hover {
border-color: rgba(255, 15, 83, 0.6);
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.4);
transform: translateY(-4px);
}
.hk-drama-card .hk-drama-card__cover:hover::before {
transform: scaleX(1);
}
.hk-drama-card .hk-drama-card__cover:hover::after {
transform: translateY(0);
}
/* 悬停时播放键接管封面底部,角标淡出避免窄卡片上重叠 */
.hk-drama-card .hk-drama-card__cover:hover .hk-drama-card__eps {
opacity: 0;
}
}
/* 封面右下角:集数 · 连更/完结 */
.hk-drama-card .hk-drama-card__eps {
position: absolute;
right: 8px;
bottom: 9px;
z-index: 2;
display: inline-flex;
align-items: center;
gap: 2px;
max-width: calc(100% - 16px);
padding: 2px 7px 2px 6px;
border-radius: 4px;
background: rgba(10, 10, 12, 0.62);
-webkit-backdrop-filter: blur(6px);
backdrop-filter: blur(6px);
color: rgba(255, 255, 255, 0.9);
font-size: 11px;
font-weight: 500;
line-height: 16px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
pointer-events: none;
transition: opacity 180ms ease;
}
.hk-drama-card .hk-drama-card__eps b {
color: #ffffff;
font-size: 12px;
font-weight: 700;
}
.hk-drama-card .hk-drama-card__eps::before {
width: 6px;
height: 6px;
margin-right: 4px;
flex: none;
border-radius: 50%;
background: var(--text-dim);
content: "";
}
.hk-drama-card .hk-drama-card__eps[data-status="serial"]::before {
background: var(--success);
box-shadow: 0 0 0 2px rgba(61, 201, 138, 0.25);
}
/* 标题与题材行 */
.hk-drama-card .hk-drama-card__title {
font-size: 15px;
font-weight: 600;
line-height: 21px;
letter-spacing: 0.2px;
}
.hk-drama-card .hk-drama-card__title a {
color: inherit;
transition: color 160ms ease;
}
.hk-drama-card .hk-drama-card__title a:hover {
color: var(--accent);
}
.hk-drama-card .hk-drama-card__genres {
display: block;
margin: -2px 0 0;
color: var(--text-dim);
font-size: 12px;
line-height: 18px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.hk-drama-card .hk-drama-card__genres a {
color: var(--text-muted);
transition: color 160ms ease;
}
.hk-drama-card .hk-drama-card__genres a:hover {
color: var(--accent);
}
.hk-drama-card .hk-drama-card__genre-sep {
margin: 0 5px;
color: rgba(255, 255, 255, 0.22);
}
@media (max-width: 768px) {
.hk-drama-card {
gap: 6px;
}
.hk-drama-card .hk-drama-card__eps {
right: 5px;
bottom: 5px;
gap: 1px;
max-width: calc(100% - 10px);
padding: 1px 5px 1px 4px;
font-size: 10px;
}
.hk-drama-card .hk-drama-card__eps::before {
width: 5px;
height: 5px;
margin-right: 3px;
}
.hk-drama-card .hk-drama-card__eps b {
font-size: 11px;
}
.hk-drama-card .hk-drama-card__title {
font-size: 14px;
line-height: 20px;
}
.hk-drama-card .hk-drama-card__genres {
font-size: 11px;
}
.hk-drama-card .hk-drama-card__genre-sep {
margin: 0 3px;
}
}
+366
View File
@@ -1941,3 +1941,369 @@
border-radius: 12px; border-radius: 12px;
} }
} }
/* ===== 2026-10 播放页改版:主栏 + 右侧人气排行、信息表、EP 选集格、英文眉标题 ===== */
:where(html[data-hk-page-styles~="playback"]) .hk-playback-body {
display: grid;
grid-template-columns: minmax(0, 1fr) 340px;
align-items: start;
gap: 28px;
}
:where(html[data-hk-page-styles~="playback"]) .hk-playback-body__main {
min-width: 0;
display: flex;
flex-direction: column;
gap: 20px;
}
:where(html[data-hk-page-styles~="playback"]) .hk-playback-body__aside {
position: sticky;
top: 92px;
min-width: 0;
}
:where(html[data-hk-page-styles~="playback"]) .hk-playback-body__aside .hk-playback-hot {
width: auto;
}
/* 面包屑:轻量文字链,当前项强调色 */
:where(html[data-hk-page-styles~="playback"]) .hk-playback-breadcrumb .hk-breadcrumb__item {
color: var(--text-dim);
}
:where(html[data-hk-page-styles~="playback"]) .hk-playback-breadcrumb a.hk-breadcrumb__item:hover {
color: var(--text);
}
:where(html[data-hk-page-styles~="playback"]) .hk-playback-breadcrumb .hk-breadcrumb__item.hk-is-current {
color: var(--accent);
}
/* 区块标题:英文眉 + 中文标题,取代原竖条装饰 */
:where(html[data-hk-page-styles~="playback"]) .hk-section-heading {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
:where(html[data-hk-page-styles~="playback"]) .hk-section-eyebrow {
display: inline-flex;
align-items: center;
gap: 6px;
color: var(--accent);
font-family: Inter, var(--font);
font-size: 10px;
font-weight: 700;
line-height: 14px;
letter-spacing: 2px;
}
:where(html[data-hk-page-styles~="playback"]) .hk-section-eyebrow::before {
width: 14px;
height: 1px;
background: currentColor;
content: "";
}
:where(html[data-hk-page-styles~="playback"]) :is(.hk-playback-synopsis__title, .hk-playback-episodes__title, .hk-playback-like__title, .hk-playback-hot__title, .hk-playback-related__title)::before {
display: none;
}
:where(html[data-hk-page-styles~="playback"]) :is(.hk-playback-synopsis__title, .hk-playback-episodes__title, .hk-playback-like__title, .hk-playback-hot__title, .hk-playback-related__title) {
margin: 0;
font-size: 18px;
font-weight: 600;
line-height: 26px;
}
:where(html[data-hk-page-styles~="playback"]) .hk-playback-synopsis__head,
:where(html[data-hk-page-styles~="playback"]) .hk-playback-hot__head,
:where(html[data-hk-page-styles~="playback"]) .hk-playback-like__head,
:where(html[data-hk-page-styles~="playback"]) .hk-playback-related__head {
align-items: flex-end;
}
/* 信息区与选集面板:实色面板 + 顶部强调线,替代渐变卡片 */
:where(html[data-hk-page-styles~="playback"]) .hk-playback-info,
:where(html[data-hk-page-styles~="playback"]) .hk-playback-episodes {
background: var(--bg-elevated);
border: 1px solid var(--line-soft);
border-top: 2px solid var(--accent);
border-radius: 10px;
}
:where(html[data-hk-page-styles~="playback"]) .hk-playback-meta__eyebrow {
margin: 0;
display: inline-flex;
align-items: center;
gap: 8px;
color: var(--text-muted);
font-size: 13px;
line-height: 20px;
}
:where(html[data-hk-page-styles~="playback"]) .hk-playback-meta__eyebrow span {
padding: 1px 8px;
border-radius: 4px;
color: #ffffff;
font-size: 12px;
font-weight: 600;
background: var(--accent);
}
:where(html[data-hk-page-styles~="playback"]) .hk-playback-meta__title {
font-size: 26px;
line-height: 34px;
letter-spacing: 0.3px;
}
/* 信息表 */
:where(html[data-hk-page-styles~="playback"]) .hk-playback-facts {
margin: 4px 0 0;
display: flex;
flex-wrap: wrap;
overflow: hidden;
border: 1px solid var(--line-soft);
border-radius: 8px;
}
:where(html[data-hk-page-styles~="playback"]) .hk-playback-facts__item {
flex: 1 1 112px;
min-width: 0;
margin: 0 -1px -1px 0;
padding: 9px 12px;
border-right: 1px solid var(--line-soft);
border-bottom: 1px solid var(--line-soft);
}
:where(html[data-hk-page-styles~="playback"]) .hk-playback-facts__item--wide {
flex-basis: 224px;
}
:where(html[data-hk-page-styles~="playback"]) .hk-playback-facts dt {
color: var(--text-dim);
font-size: 11px;
line-height: 16px;
letter-spacing: 1px;
}
:where(html[data-hk-page-styles~="playback"]) .hk-playback-facts dd {
margin: 2px 0 0;
color: var(--text);
font-size: 14px;
font-weight: 500;
line-height: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
:where(html[data-hk-page-styles~="playback"]) .hk-playback-facts dd[data-status="serial"] {
color: var(--success);
}
:where(html[data-hk-page-styles~="playback"]) .hk-playback-facts .hk-playback-meta__tag {
color: var(--text);
}
:where(html[data-hk-page-styles~="playback"]) .hk-playback-facts a.hk-playback-meta__tag:hover {
color: var(--accent);
}
/* 简介标题较长时,“展开”按钮保持单行不被挤压 */
:where(html[data-hk-page-styles~="playback"]) .hk-playback-synopsis__head {
gap: 12px;
}
:where(html[data-hk-page-styles~="playback"]) .hk-playback-synopsis__more {
flex-shrink: 0;
white-space: nowrap;
}
/* 简介标签:描边胶囊,无井号 */
:where(html[data-hk-page-styles~="playback"]) .hk-playback-synopsis {
border-top-style: solid;
border-top-color: var(--line-soft);
}
:where(html[data-hk-page-styles~="playback"]) .hk-playback-synopsis__tag {
padding: 2px 10px;
border: 1px solid var(--line);
border-radius: 999px;
background: transparent;
}
:where(html[data-hk-page-styles~="playback"]) .hk-playback-synopsis__tag::before {
content: none;
}
:where(html[data-hk-page-styles~="playback"]) a.hk-playback-synopsis__tag:hover {
border-color: rgba(255, 15, 83, 0.5);
background: transparent;
color: var(--accent);
}
/* EP 选集格:上方小号 EP,下方两位集号;当前集为描边强调 */
:where(html[data-hk-page-styles~="playback"]) .hk-playback-episodes__grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(64px, 1fr));
gap: 8px;
}
:where(html[data-hk-page-styles~="playback"]) .hk-playback-ep {
width: auto;
height: 54px;
flex-direction: column;
gap: 1px;
border-radius: 6px;
border: 1px solid var(--line-soft);
background: rgba(255, 255, 255, 0.03);
transition: border-color 160ms ease, background 160ms ease;
}
:where(html[data-hk-page-styles~="playback"]) .hk-playback-ep__label {
color: var(--text-dim);
font-family: Inter, var(--font);
font-size: 9px;
font-weight: 700;
line-height: 12px;
letter-spacing: 1.5px;
}
:where(html[data-hk-page-styles~="playback"]) .hk-playback-ep__num {
font-family: Inter, var(--font);
font-size: 17px;
font-weight: 700;
line-height: 20px;
}
:where(html[data-hk-page-styles~="playback"]) a.hk-playback-ep:hover {
border-color: rgba(255, 255, 255, 0.25);
}
:where(html[data-hk-page-styles~="playback"]) .hk-playback-ep.hk-is-active {
color: var(--accent);
background: rgba(255, 15, 83, 0.08);
border-color: var(--accent);
box-shadow: none;
}
:where(html[data-hk-page-styles~="playback"]) .hk-playback-ep.hk-is-active .hk-playback-ep__label {
color: var(--accent);
}
/* 主栏内的相似短剧网格 */
:where(html[data-hk-page-styles~="playback"]) .hk-playback-body__main > .hk-playback-like {
padding: 4px 0 0;
}
:where(html[data-hk-page-styles~="playback"]) .hk-playback-body__main .hk-playback-like__grid {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 20px 14px;
}
:where(html[data-hk-page-styles~="playback"]) .hk-playback-body__main .hk-drama-card--play {
width: auto;
min-width: 0;
}
/* 人气排行:大号斜体序号,前三名渐变字 */
:where(html[data-hk-page-styles~="playback"]) .hk-playback-hot {
border-radius: 10px;
border: 1px solid var(--line-soft);
}
:where(html[data-hk-page-styles~="playback"]) .hk-playback-hot__no {
width: 28px;
height: auto;
margin-right: 0;
color: var(--text-dim);
font-family: Inter, var(--font);
font-size: 20px;
font-style: italic;
font-weight: 800;
background: none;
}
:where(html[data-hk-page-styles~="playback"]) .hk-playback-hot__no.hk-is-top {
color: transparent;
background: linear-gradient(135deg, var(--gold), var(--accent));
-webkit-background-clip: text;
background-clip: text;
}
@media (max-width: 1100px) {
:where(html[data-hk-page-styles~="playback"]) .hk-playback-body {
grid-template-columns: minmax(0, 1fr) 300px;
gap: 20px;
}
:where(html[data-hk-page-styles~="playback"]) .hk-playback-body__main .hk-playback-like__grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
@media (max-width: 768px) {
:where(html[data-hk-page-styles~="playback"]) .hk-playback-body {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 16px;
}
:where(html[data-hk-page-styles~="playback"]) .hk-playback-body__aside {
position: static;
}
:where(html[data-hk-page-styles~="playback"]) .hk-playback-body__aside .hk-playback-hot {
margin: 0 var(--h5-gutter);
}
:where(html[data-hk-page-styles~="playback"]) .hk-playback-body__main > .hk-playback-like {
padding: 0 var(--h5-gutter);
}
:where(html[data-hk-page-styles~="playback"]) .hk-playback-body__main .hk-playback-like__grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px 8px;
}
:where(html[data-hk-page-styles~="playback"]) .hk-playback-meta__title {
font-size: 20px;
line-height: 28px;
}
:where(html[data-hk-page-styles~="playback"]) .hk-playback-facts__item {
flex-basis: 30%;
padding: 7px 10px;
}
:where(html[data-hk-page-styles~="playback"]) .hk-playback-facts__item--wide {
flex-basis: 60%;
}
:where(html[data-hk-page-styles~="playback"]) .hk-playback-facts dd {
font-size: 13px;
}
:where(html[data-hk-page-styles~="playback"]) .hk-playback-episodes__grid {
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 6px;
}
:where(html[data-hk-page-styles~="playback"]) .hk-playback-ep {
height: 46px;
}
:where(html[data-hk-page-styles~="playback"]) .hk-playback-ep__num {
font-size: 15px;
}
:where(html[data-hk-page-styles~="playback"]) :is(.hk-playback-synopsis__title, .hk-playback-episodes__title, .hk-playback-like__title, .hk-playback-hot__title, .hk-playback-related__title) {
font-size: 16px;
line-height: 24px;
}
}
+7 -2
View File
@@ -57,9 +57,14 @@ test("React card variants preserve watch URLs, episode gates, badges and linked
card, card,
/href="\/drama\/都市%20剧\/"|href="\/drama\/%E9%83%BD%E5%B8%82%20%E5%89%A7\/"/, /href="\/drama\/都市%20剧\/"|href="\/drama\/%E9%83%BD%E5%B8%82%20%E5%89%A7\/"/,
); );
// Episode count and serial state sit in the cover corner; the line under the title lists genres.
assert.match( assert.match(
card, card,
/<p class="hk-drama-card__meta hk-drama-card__meta--ep"><span class="hk-drama-card__meta-ep" data-status="serial">连载 · 8 集<\/span><span class="hk-drama-card__meta-tags">/, /<span class="hk-drama-card__eps" data-status="serial"><b>8<\/b> 集 · 连更<\/span><\/a>/,
);
assert.match(
card,
/<p class="hk-drama-card__meta hk-drama-card__genres"><a href="\/tags\/romance\/">爱情<\/a><span class="hk-drama-card__genre-sep" aria-hidden="true">\/<\/span>/,
); );
assert.doesNotMatch(card, /hk-drama-card__ep"/); assert.doesNotMatch(card, /hk-drama-card__ep"/);
assert.match(card, /hk-badge hk-badge--hot/); assert.match(card, /hk-badge hk-badge--hot/);
@@ -72,7 +77,7 @@ test("React card variants preserve watch URLs, episode gates, badges and linked
<PlayRecommendationCard item={item} />, <PlayRecommendationCard item={item} />,
); );
assert.match(recommendation, /hk-drama-card hk-drama-card--play/); assert.match(recommendation, /hk-drama-card hk-drama-card--play/);
assert.match(recommendation, /hk-drama-card__meta-ep" data-status="serial">连载 · 8 集</); assert.match(recommendation, /hk-drama-card__eps" data-status="serial"><b>8<\/b> 集 · 连更</);
assert.doesNotMatch(recommendation, /hk-badge--hot/); assert.doesNotMatch(recommendation, /hk-badge--hot/);
const video = renderToStaticMarkup( const video = renderToStaticMarkup(
<SeriesCard <SeriesCard
+9 -4
View File
@@ -90,10 +90,15 @@ test("React tags and list pages preserve the original Go catalog content and nav
const actual = main(renderPage(Component, data)), const actual = main(renderPage(Component, data)),
expected = main(fixture(name + "-go.html")); expected = main(fixture(name + "-go.html"));
assert.ok(actual, name); assert.ok(actual, name);
// The episode label moved from the cover link into the meta-row chip. // Both sites label episodes on the cover link, in different wording; compare those links without the label.
const coverEpisode = /^(?:全 \d+ 集|更新至 \d+ 集|连载中|已完结)$/; const coverEpisode = /^(?:全 \d+ 集|更新至 \d+ 集|连载中|已完结)$/,
coverBadge = /^(?:\d+ 集 · )?(?:连更|完结)$/;
assert.deepEqual( assert.deepEqual(
anchors(actual), anchors(actual).map((link) =>
link.href.startsWith("/drama/") && coverBadge.test(link.text)
? { ...link, text: "" }
: link,
),
anchors(expected).map((link) => anchors(expected).map((link) =>
link.href.startsWith("/drama/") && coverEpisode.test(link.text) link.href.startsWith("/drama/") && coverEpisode.test(link.text)
? { ...link, text: "" } ? { ...link, text: "" }
@@ -112,7 +117,7 @@ test("React tags and list pages preserve the original Go catalog content and nav
name + " ad placement", name + " ad placement",
); );
assert.match(actual, /花魁 &amp; 第一部/); assert.match(actual, /花魁 &amp; 第一部/);
assert.match(actual, /<span class="hk-drama-card__meta-ep" data-status="done">完结 · 4 集<\/span>/); assert.match(actual, /<span class="hk-drama-card__eps" data-status="done"><b>4<\/b> 集 · 完结<\/span>/);
} }
}); });