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/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/types/root-params.d.ts";
import "./.next-dev/dev/types/routes.d.ts";
import "./.next-dev/dev/types/root-params.d.ts";
// NOTE: This file should not be edited
// 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",
);
assert.equal(await page.locator(".hk-playback-comments, .hk-playback-composer").count(), 0);
assert.deepEqual(
await page.locator(".hk-playback-dual > *").evaluateAll((nodes) => nodes.map((node) => node.className)),
["hk-playback-like", "hk-playback-hot"],
);
// Recommendations stay in the main column; the trending list sits in the right sidebar.
assert.equal(await page.locator(".hk-playback-body__main > .hk-playback-like").count(), 1);
assert.equal(await page.locator(".hk-playback-body__aside > .hk-playback-hot").count(), 1);
assert.match(
(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/home-road.css";
import "@/styles/pages/playback.css";
import "@/styles/overrides/drama-tiles.css";
import "@/styles/overrides/pwa-safe-area.css";
import type { ReactNode } from "react";
import { PwaProvider } from "@/components/pwa/PwaProvider";
+41 -15
View File
@@ -9,7 +9,6 @@ import {
cardTags,
categoryURL,
displayName,
episodeChip,
episodeLabel,
episodeStatus,
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 }) {
const chip = episodeChip(item);
const tags = cardTags(item);
if (!tags.length) return null;
return (
<p className="hk-drama-card__meta hk-drama-card__meta--ep">
{chip && (
<span
className="hk-drama-card__meta-ep"
data-status={num(item, "UpdateStatus") === 1 ? "serial" : "done"}
>
{chip}
</span>
)}
<span className="hk-drama-card__meta-tags">
<CardTags item={item} />
</span>
<p className="hk-drama-card__meta hk-drama-card__genres">
{tags.map((tag, index) => (
<Fragment key={tag.Name}>
{index > 0 && (
<span className="hk-drama-card__genre-sep" aria-hidden="true">
/
</span>
)}
{tag.URL ? (
<a href={tag.URL} {...externalProps(tag.URL)}>
{tag.Name}
</a>
) : (
<span>{tag.Name}</span>
)}
</Fragment>
))}
</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({
item,
recent = false,
@@ -94,6 +118,7 @@ export function DramaCard({
) : str(item, "Anchor") === "原创" ? (
<span className="hk-badge hk-badge--original">原创</span>
) : null}
<DramaCardEpisodes item={item} />
</EncryptedBackground>
<h3 className="hk-drama-card__title">
<a href={href}>{name}</a>
@@ -278,6 +303,7 @@ export function PlayRecommendationCard({ item }: { item: CatalogRecord }) {
href={href}
src={str(item, "Cover")}
>
<DramaCardEpisodes item={item} />
</EncryptedBackground>
<h3 className="hk-drama-card__title">
<a href={href}>{displayName(item)}</a>
+211 -166
View File
@@ -25,7 +25,14 @@ import {
import { coinGate } from "./model";
import { PlaybackStage } from "./PlaybackStage";
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 }) {
const p = data.playlet || {},
id = num(p, "ID"),
@@ -109,7 +116,7 @@ export function DramaWatchPage({ data }: { data: Data }) {
</a>
<Icon name="chevron-right" className="hk-breadcrumb__sep" />
<a className="hk-breadcrumb__item" href="/dramas/">
短剧
短剧库
</a>
<Icon name="chevron-right" className="hk-breadcrumb__sep" />
<a className="hk-breadcrumb__item" href={playURL(get(p, "Slug"), id)}>
@@ -128,178 +135,216 @@ export function DramaWatchPage({ data }: { data: Data }) {
onPlaying={onPlaying}
autoPlay={autoPlay}
/>
<section className="hk-playback-info" aria-label="剧集信息">
<div className="hk-playback-meta">
<div className="hk-playback-meta__title-row">
<h1 className="hk-playback-meta__title">{name}</h1>
{get(p, "IsFeatured") ? (
<span className="hk-badge hk-badge--hot">爆剧</span>
) : anchor === "原创" ? (
<span className="hk-badge hk-badge--original">原创</span>
) : null}
</div>
<div className="hk-playback-meta__chips">
{total > 0 && <span className="hk-playback-chip">全 {total} 集</span>}
<span className="hk-playback-chip">{status}</span>
{year && <span className="hk-playback-chip">{year}</span>}
<span className="hk-playback-chip">1080P</span>
</div>
{["hk-desktop-only", "hk-mobile-only"].map((device) => (
<p className={`hk-playback-meta__stats ${device}`} key={device}>
{anchor && !["原创", "已注销"].includes(anchor) && (
<span className="hk-playback-meta__stat">主演 {anchor}</span>
)}
{!!themes.length && (
<span className="hk-playback-meta__stat">
{device === "hk-desktop-only" ? "题材 " : ""}
{themes.map((tag, index) => (
<span key={tag.Name}>
{index ? " " : ""}
{tag.URL ? (
<a
className="hk-playback-meta__tag"
href={tag.URL}
{...externalProps(tag.URL)}
>
{tag.Name}
</a>
) : (
<span className="hk-playback-meta__tag">{tag.Name}</span>
)}
</span>
<div className="hk-playback-body">
<div className="hk-playback-body__main">
<section className="hk-playback-info" aria-label="剧集信息">
<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">
<h1 className="hk-playback-meta__title">{name}</h1>
{get(p, "IsFeatured") ? (
<span className="hk-badge hk-badge--hot">爆剧</span>
) : anchor === "原创" ? (
<span className="hk-badge hk-badge--original">原创</span>
) : null}
</div>
<dl className="hk-playback-facts">
{total > 0 && (
<div className="hk-playback-facts__item">
<dt>集数</dt>
<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>
{anchor && !["原创", "已注销"].includes(anchor) && (
<div className="hk-playback-facts__item">
<dt>主演</dt>
<dd>{anchor}</dd>
</div>
)}
{!!themes.length && (
<div className="hk-playback-facts__item hk-playback-facts__item--wide">
<dt>题材</dt>
<dd>
{themes.map((tag, index) => (
<span key={tag.Name}>
{index ? " / " : ""}
{tag.URL ? (
<a
className="hk-playback-meta__tag"
href={tag.URL}
{...externalProps(tag.URL)}
>
{tag.Name}
</a>
) : (
<span className="hk-playback-meta__tag">{tag.Name}</span>
)}
</span>
))}
</dd>
</div>
)}
<div className="hk-playback-facts__item">
<dt>热度</dt>
<dd>{formatLikeCount(get(p, "HeatValue"))}</dd>
</div>
<div className="hk-playback-facts__item">
<dt>收藏</dt>
<dd>{formatLikeCount(get(p, "CountCollect"))}</dd>
</div>
</dl>
</div>
<div className="hk-playback-actions">
<button
className="hk-button hk-button--primary hk-playback-actions__btn hk-playback-actions__btn--play"
type="button"
onClick={play}
>
<Icon name="play_hollow" />
<span>
{mode === "preview"
? "播放预告片"
: data.canWatch
? "立即播放"
: "去 App 看"}
</span>
</button>
<PlaybackShare
className="hk-button hk-button--ghost hk-playback-actions__btn hk-playback-actions__btn--icon"
label
/>
<a
className="hk-button hk-button--ghost hk-playback-actions__btn hk-playback-actions__btn--icon"
href="/feedback/"
aria-label="反馈"
>
<Icon name="message-circle" />
<span className="hk-desktop-only">反馈</span>
</a>
</div>
<Synopsis
name={name}
text={first(get(p, "Desc"), get(p, "Description"))}
tags={namedTags(get(p, "TagIDs"), get(p, "TagNames"), get(p, "TagSlugs"))}
/>
</section>
<section className="hk-playback-episodes" aria-label="全集列表">
<div className="hk-playback-episodes__head">
<div className="hk-playback-episodes__head-left">
<div className="hk-section-heading">
<SectionEyebrow>EPISODES</SectionEyebrow>
<h2 className="hk-playback-episodes__title">全集列表</h2>
</div>
<span className="hk-playback-episodes__count">
共 {total || chapters.length} 集 {status}
</span>
<nav
className="hk-playback-episodes__sort hk-desktop-only"
aria-label="分集排序"
>
{[false, true].map((value) => (
<button
key={String(value)}
className={`hk-playback-episodes__sort-item${descending === value ? " hk-is-active" : ""}`}
type="button"
aria-pressed={descending === value}
onClick={() => setDescending(value)}
>
{value ? "倒序" : "正序"}
</button>
))}
</nav>
</div>
<button
className="hk-playback-episodes__order hk-mobile-only"
type="button"
aria-pressed={descending}
onClick={() => setDescending(!descending)}
>
<span>{descending ? "正序" : "倒序"}</span>
<Icon name="chevron-right" />
</button>
</div>
<div className="hk-playback-episodes__grid">
{(descending ? [...chapters].reverse() : chapters).map(
(chapter, index) => {
const chid = num(chapter, "ID"),
ep =
num(chapter, "CurrentEpisode") ||
(descending ? chapters.length - index : index + 1);
return (
<a
key={chid}
className={`hk-playback-ep${chid === chapterID ? " hk-is-active" : ""}${coinGate(chapter) ? " hk-playback-ep--coin" : ""}`}
href={chapterPlayURL(get(p, "Slug"), id, chid)}
aria-label={`第 ${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>
);
},
)}
{!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>
)}
<span className="hk-playback-meta__stat">
热度 {formatLikeCount(get(p, "HeatValue"))}
</div>
<div className="hk-playback-episodes__legend">
<span className="hk-playback-legend">
<i
className="hk-playback-legend__swatch hk-playback-legend__swatch--free"
aria-hidden="true"
/>
<span className="hk-desktop-only">免费集 在线即看</span>
<span className="hk-mobile-only">免费集在线看</span>
</span>
<span className="hk-playback-meta__stat">
收藏 <span>{formatLikeCount(get(p, "CountCollect"))}</span>
<span className="hk-playback-legend">
<Icon
name="lock"
className="hk-playback-legend__swatch hk-playback-legend__swatch--coin"
/>
<span className="hk-desktop-only">App 集 前往 App 免费看</span>
<span className="hk-mobile-only">App 集</span>
</span>
</p>
))}
</div>
<div className="hk-playback-actions">
<button
className="hk-button hk-button--primary hk-playback-actions__btn hk-playback-actions__btn--play"
type="button"
onClick={play}
>
<Icon name="play_hollow" />
<span>
{mode === "preview"
? "播放预告片"
: data.canWatch
? "立即播放"
: "去 App 看"}
</span>
</button>
<PlaybackShare
className="hk-button hk-button--ghost hk-playback-actions__btn hk-playback-actions__btn--icon"
label
/>
<a
className="hk-button hk-button--ghost hk-playback-actions__btn hk-playback-actions__btn--icon"
href="/feedback/"
aria-label="反馈"
>
<Icon name="message-circle" />
<span className="hk-desktop-only">反馈</span>
</a>
</div>
<Synopsis
text={first(get(p, "Desc"), get(p, "Description"))}
tags={namedTags(get(p, "TagIDs"), get(p, "TagNames"), get(p, "TagSlugs"))}
/>
</section>
<section className="hk-playback-episodes" aria-label="分集">
<div className="hk-playback-episodes__head">
<div className="hk-playback-episodes__head-left">
<h2 className="hk-playback-episodes__title">分集</h2>
<span className="hk-playback-episodes__count">
共 {total || chapters.length} 集 {status}
</span>
<nav
className="hk-playback-episodes__sort hk-desktop-only"
aria-label="分集排序"
>
{[false, true].map((value) => (
<button
key={String(value)}
className={`hk-playback-episodes__sort-item${descending === value ? " hk-is-active" : ""}`}
type="button"
aria-pressed={descending === value}
onClick={() => setDescending(value)}
>
{value ? "倒序" : "正序"}
</button>
))}
</nav>
</div>
</section>
<PlayLinks data={data} />
<div className="hk-playback-promo">
<AdBanners
items={array(get(data.advitise, "DetailBannerList")).slice(0, 2)}
/>
</div>
<button
className="hk-playback-episodes__order hk-mobile-only"
type="button"
aria-pressed={descending}
onClick={() => setDescending(!descending)}
>
<span>{descending ? "正序" : "倒序"}</span>
<Icon name="chevron-right" />
</button>
</div>
<div className="hk-playback-episodes__grid">
{(descending ? [...chapters].reverse() : chapters).map(
(chapter, index) => {
const chid = num(chapter, "ID"),
ep =
num(chapter, "CurrentEpisode") ||
(descending ? chapters.length - index : index + 1);
return (
<a
key={chid}
className={`hk-playback-ep${chid === chapterID ? " hk-is-active" : ""}${coinGate(chapter) ? " hk-playback-ep--coin" : ""}`}
href={chapterPlayURL(get(p, "Slug"), id, chid)}
>
{ep}
</a>
);
},
)}
{!chapters.length && <span className="hk-playback-ep hk-is-active">1</span>}
</div>
<div className="hk-playback-episodes__legend">
<span className="hk-playback-legend">
<i
className="hk-playback-legend__swatch hk-playback-legend__swatch--free"
aria-hidden="true"
<Related data={data} />
<GuessLike data={data} playletId={id} />
<div className="hk-playback-promo hk-playback-promo--bottom">
<AdBanners
items={array(get(data.advitise, "DetailBannerList")).slice(2, 4)}
/>
<span className="hk-desktop-only">免费集 在线即看</span>
<span className="hk-mobile-only">免费集在线看</span>
</span>
<span className="hk-playback-legend">
<Icon
name="lock"
className="hk-playback-legend__swatch hk-playback-legend__swatch--coin"
/>
<span className="hk-desktop-only">App 集 前往 App 免费看</span>
<span className="hk-mobile-only">App 集</span>
</span>
</div>
</div>
<div className="hk-playback-body__aside">
<HotList data={data} />
</div>
</section>
<PlayLinks data={data} />
<div className="hk-playback-promo">
<AdBanners
items={array(get(data.advitise, "DetailBannerList")).slice(0, 2)}
/>
</div>
<Related data={data} />
<section className="hk-playback-dual" aria-label="推荐与热榜">
<GuessLike data={data} playletId={id} />
<HotList data={data} />
</section>
<div className="hk-playback-promo hk-playback-promo--bottom">
<AdBanners
items={array(get(data.advitise, "DetailBannerList")).slice(2, 4)}
/>
</div>
</main>
</div>
+29 -8
View File
@@ -66,12 +66,22 @@ export function PlaybackShare({
</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({
text,
tags = [],
name = "",
}: {
text: string;
tags?: NamedTag[];
name?: string;
}) {
const [expanded, setExpanded] = useState(false),
[overflow, setOverflow] = useState(false),
@@ -94,7 +104,12 @@ export function Synopsis({
return (
<div className={`hk-playback-synopsis${expanded ? " hk-is-expanded" : ""}`}>
<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) && (
<button
className="hk-playback-synopsis__more"
@@ -131,9 +146,12 @@ export function Synopsis({
export function HotList({ data }: { data: Data }) {
const items = array(data.hotList);
return (
<aside className="hk-playback-hot" aria-label="热播榜单">
<aside className="hk-playback-hot" aria-label="人气排行">
<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
className="hk-playback-hot__more"
href={String(data.hotMoreURL || "/dramas/all/0/2/")}
@@ -238,11 +256,11 @@ export function GuessLike({
}
};
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__titles">
<h2 className="hk-playback-like__title">同类推荐</h2>
<span className="hk-playback-like__desc hk-desktop-only">与本剧题材相近的好剧</span>
<div className="hk-playback-like__titles hk-section-heading">
<SectionEyebrow>MORE LIKE THIS</SectionEyebrow>
<h2 className="hk-playback-like__title">相似短剧推荐</h2>
</div>
<button
type="button"
@@ -268,7 +286,10 @@ export function Related({ data }: { data: Data }) {
return (
<section className="hk-playback-related hk-mobile-only" aria-label="大家都在看">
<div className="hk-playback-related__head">
<h2 className="hk-playback-related__title">大家都在看</h2>
<div className="hk-section-heading">
<SectionEyebrow>ALSO WATCHING</SectionEyebrow>
<h2 className="hk-playback-related__title">大家都在看</h2>
</div>
<a className="hk-playback-related__more" href="/featured/">
更多
<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;
}
}
/* ===== 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,
/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(
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.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} />,
);
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/);
const video = renderToStaticMarkup(
<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)),
expected = main(fixture(name + "-go.html"));
assert.ok(actual, name);
// The episode label moved from the cover link into the meta-row chip.
const coverEpisode = /^(?:全 \d+ 集|更新至 \d+ 集|连载中|已完结)$/;
// Both sites label episodes on the cover link, in different wording; compare those links without the label.
const coverEpisode = /^(?:全 \d+ 集|更新至 \d+ 集|连载中|已完结)$/,
coverBadge = /^(?:\d+ 集 · )?(?:连更|完结)$/;
assert.deepEqual(
anchors(actual),
anchors(actual).map((link) =>
link.href.startsWith("/drama/") && coverBadge.test(link.text)
? { ...link, text: "" }
: link,
),
anchors(expected).map((link) =>
link.href.startsWith("/drama/") && coverEpisode.test(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",
);
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>/);
}
});