feat(web): 作品详情头图改为剧照铺顶渐隐到页面底色(方案 A)
- MediaHero 去掉深色圆角卡片:剧照铺在内容区顶部,上下与右缘渐隐成透明;左侧文字区压近乎不透明的页面底色、右侧露出剧照,窄屏改为自上而下压底色;文字用常规前景色,日夜同一结构;没有背景图时不渲染剧照层 - 新增材质参数 --mat-hero-*(浅 / 深两套)与 .media-hero-backdrop - 「正常」/ Emby / 库存等徽标并进「连载中」所在的事实行 - 操作按钮(订阅 / 搜索资源 / 查看订阅 / 合并作品 / 插槽入口如补字幕 / 命名覆盖)移进头图区外链行下方,库作品页与 TMDB 详情页一致 - 订阅改为主色实心(Button variant="solid",.pill-solid / --mat-pill-solid-*),作为头图区唯一主操作;设计文档记为例外 - 简介折叠:去掉省略号 + 单独一行「展开」的重复,改为最后一行行尾渐隐、「展开」压在渐隐处;按实际排版测量是否需要折叠 - 补 MediaHero / ClampText 渲染测试 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
1 parent
6549ab92c3
commit
caf5a1f2f4
8 files changed
+370
-118
No files matched your search
@@ -0,0 +1,120 @@
|
||||
import { createElement, Fragment } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { StatusBadge } from "~/components/console/status-badge.tsx";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { ClampText, type MediaFacts, MediaHero } from "./media-detail.tsx";
|
||||
|
||||
// 详情页头图(方案 A):背景图渐隐到页面底色、无背景图退化为普通头部;状态徽标并进事实行;
|
||||
// 操作按钮并进头图区且只有一个实心主操作;简介折叠只留一个「展开」入口。
|
||||
|
||||
const facts = (over: Partial<MediaFacts> = {}): MediaFacts => ({
|
||||
title: "人生切割术",
|
||||
originalTitle: "Severance",
|
||||
year: 2022,
|
||||
kind: "series",
|
||||
kindLabel: "剧集",
|
||||
genres: ["剧情"],
|
||||
voteAverage: 8.4,
|
||||
voteCount: 1200,
|
||||
airStatus: "Returning Series",
|
||||
runtime: 55,
|
||||
seasonCount: 2,
|
||||
episodeCount: 19,
|
||||
releaseDate: "2022-02-17",
|
||||
lastAirDate: "2025-03-20",
|
||||
platform: "Apple TV",
|
||||
originCountry: ["US"],
|
||||
overview: "故事发生在一家名为卢蒙的超级大公司内。",
|
||||
tagline: null,
|
||||
posterPath: "/sev-poster.jpg",
|
||||
backdropPath: "/severance-bg.jpg",
|
||||
externalLinks: [{ label: "TMDB", href: "https://www.themoviedb.org/tv/95396" }],
|
||||
...over,
|
||||
});
|
||||
|
||||
const render = (props: Parameters<typeof MediaHero>[0]) => renderToStaticMarkup(createElement(MediaHero, props));
|
||||
|
||||
/** 取 data-slot 标记的元素起始处之后的一段 HTML(足够断言其中的内容) */
|
||||
const slotHtml = (html: string, slot: string) => {
|
||||
const i = html.indexOf(`data-slot="${slot}"`);
|
||||
return i < 0 ? null : html.slice(i, html.indexOf("</div>", i) + 6);
|
||||
};
|
||||
|
||||
describe("MediaHero 头图", () => {
|
||||
it("有背景图:剧照层经图片代理、带渐隐材质,不再是深色圆角卡片;文字用常规前景色", () => {
|
||||
const html = render({ facts: facts() });
|
||||
expect(html).toContain('data-backdrop="image"');
|
||||
expect(html).toContain("media-hero-backdrop");
|
||||
expect(html).toContain('src="/api/v1/img/tmdb/w1280/severance-bg.jpg"');
|
||||
expect(html).not.toMatch(/bg-scrim|text-on-media|rounded-2xl/);
|
||||
expect(html).toContain("text-foreground");
|
||||
});
|
||||
|
||||
it("没有背景图:退化为普通头部,不渲染剧照层和空的深色块", () => {
|
||||
const html = render({ facts: facts({ backdropPath: null }) });
|
||||
expect(html).toContain('data-backdrop="none"');
|
||||
expect(html).not.toContain("media-hero-backdrop");
|
||||
expect(html).not.toContain("w1280");
|
||||
expect(html).not.toMatch(/bg-scrim/);
|
||||
expect(html).toContain("人生切割术");
|
||||
});
|
||||
|
||||
it("状态徽标(正常 / Emby 等)与播出状态在同一行", () => {
|
||||
const html = render({
|
||||
facts: facts(),
|
||||
badges: createElement(StatusBadge, { tone: "success" }, "正常"),
|
||||
});
|
||||
const row = slotHtml(html, "media-hero-badges");
|
||||
expect(row).toContain("连载中");
|
||||
expect(row).toContain("正常");
|
||||
});
|
||||
|
||||
it("操作按钮并进头图区、在外链之后;只有订阅是实心主操作,其余是描边次操作,没有棱镜描边", () => {
|
||||
const html = render({
|
||||
facts: facts(),
|
||||
actions: createElement(
|
||||
Fragment,
|
||||
null,
|
||||
createElement(Button, { variant: "solid", size: "sm" }, "订阅"),
|
||||
createElement(Button, { variant: "outline", size: "sm" }, "搜索资源"),
|
||||
createElement(Button, { variant: "outline", size: "sm" }, "查看订阅"),
|
||||
),
|
||||
});
|
||||
const actions = slotHtml(html, "media-hero-actions");
|
||||
expect(actions).not.toBeNull();
|
||||
expect(html.indexOf('data-slot="media-hero-actions"')).toBeGreaterThan(html.indexOf("TMDB"));
|
||||
expect(actions!.match(/pill-solid/g)).toHaveLength(1);
|
||||
expect(actions!.match(/pill-secondary/g)).toHaveLength(2);
|
||||
expect(actions).not.toContain("pill-primary");
|
||||
expect(actions!.indexOf("pill-solid")).toBeLessThan(actions!.indexOf("订阅"));
|
||||
});
|
||||
|
||||
it("没有操作时不渲染操作行;标语有值才显示", () => {
|
||||
const plain = render({ facts: facts() });
|
||||
expect(plain).not.toContain("media-hero-actions");
|
||||
expect(plain).not.toContain("media-hero-tagline");
|
||||
const tagged = render({ facts: facts({ tagline: "工作与生活,一刀两断" }) });
|
||||
expect(slotHtml(tagged, "media-hero-tagline")).toContain("工作与生活,一刀两断");
|
||||
});
|
||||
});
|
||||
|
||||
describe("ClampText 折叠", () => {
|
||||
const long = "很长的简介。".repeat(60);
|
||||
|
||||
it("超出行数:折叠成行尾渐隐 + 一个「展开」入口,不再另加省略号", () => {
|
||||
const html = renderToStaticMarkup(createElement(ClampText, { text: long, lines: 4 }));
|
||||
expect(html).toContain("clamp-fade");
|
||||
expect(html).toContain("--clamp-lines:4");
|
||||
expect(html).not.toContain("line-clamp");
|
||||
expect(html).not.toMatch(/…|\.\.\./);
|
||||
expect(html.match(/展开/g)).toHaveLength(1);
|
||||
expect(html).toContain('aria-expanded="false"');
|
||||
});
|
||||
|
||||
it("放得下时不折叠、不给「展开」", () => {
|
||||
const html = renderToStaticMarkup(createElement(ClampText, { text: "短简介", lines: 4 }));
|
||||
expect(html).not.toContain("clamp-fade");
|
||||
expect(html).not.toContain("展开");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user