Files
web_hgcg_h5/src/lib/client/detail.ts
T
谢宇宁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

79 lines
8.2 KiB
TypeScript

import { contentPath } from '../routing';
import type { Chapter, Config, DramaDetail } from "../api/types";
import { formatCount, recordList, safeUrl } from "../api/protocol";
import { escape, imageAttributes, title } from "./render";
// Original HTML action icons, kept as SVG paths so active colors follow the stylesheet.
const actionPaths = {
"save": "M857.6 327.68l-104.494545-34.210909a178.036364 178.036364 0 0 1-90.763637-66.56l-64-89.367273a104.96 104.96 0 0 0-23.272727-23.272727 106.123636 106.123636 0 0 0-148.48 23.272727l-64.930909 89.367273a179.665455 179.665455 0 0 1-90.763637 66.792727l-104.494545 33.978182a106.821818 106.821818 0 0 0-53.061818 162.909091l93.090909 128.930909a35.84 35.84 0 0 1 6.981818 21.643636v158.952728a109.381818 109.381818 0 0 0 5.12 33.512727 105.658182 105.658182 0 0 0 133.818182 67.956364l128-39.33091a107.985455 107.985455 0 0 1 64.232727 0l127.534546 39.33091a106.123636 106.123636 0 0 0 139.636363-101.469091v-158.254546a35.84 35.84 0 0 1 6.981818-21.643636l93.09091-128.930909a104.494545 104.494545 0 0 0 15.127272-29.556364 106.821818 106.821818 0 0 0-69.352727-134.050909z m-192 83.549091a34.909091 34.909091 0 0 1-48.174545 10.938182l-46.545455-29.090909a34.909091 34.909091 0 0 1 37.003636-59.112728l46.545455 29.090909a34.909091 34.909091 0 0 1 11.170909 48.174546z",
"like": "M855.738182 198.283636A252.043636 252.043636 0 0 0 670.254545 116.363636a225.28 225.28 0 0 0-160.814545 63.069091 248.32 248.32 0 0 0-152.901818-53.294545A251.578182 251.578182 0 0 0 171.287273 209.454545 287.883636 287.883636 0 0 0 162.909091 588.567273l256 275.083636a122.181818 122.181818 0 0 0 90.065454 39.563636 121.716364 121.716364 0 0 0 89.832728-39.563636l256-272.989091 2.792727-3.723636a290.443636 290.443636 0 0 0-1.861818-388.654546z",
"share": "M910.661818 117.992727a114.734545 114.734545 0 0 0-113.803636-16.756363L134.050909 363.752727a138.472727 138.472727 0 0 0-15.592727 250.181818l97.978182 54.225455 1.861818 162.909091a87.272727 87.272727 0 0 0 137.309091 69.818182L482.210909 814.545455l180.363636 99.374545a138.472727 138.472727 0 0 0 203.869091-101.236364L954.181818 224.814545a113.803636 113.803636 0 0 0-43.52-106.821818z",
"feedback": "M505.949091 38.4a465.454545 465.454545 0 1 0 465.454545 465.454545 465.454545 465.454545 0 0 0-465.454545-465.454545z m0 721.454545a46.545455 46.545455 0 1 1 46.545454-46.545454 46.545455 46.545455 0 0 1-46.545454 46.545454z m43.287273-188.276363a11.636364 11.636364 0 0 0-8.378182 11.170909v25.832727a34.909091 34.909091 0 0 1-69.818182 0V581.818182a81.454545 81.454545 0 0 1 58.88-78.196364 87.04 87.04 0 0 0 60.741818-102.865454A86.807273 86.807273 0 0 0 433.803636 372.363636a90.298182 90.298182 0 0 0-11.403636 24.436364 34.909091 34.909091 0 0 1-67.025455-19.549091 162.909091 162.909091 0 0 1 20.712728-43.985454 156.625455 156.625455 0 1 1 173.149091 238.312727z",
"phone": "M756.363636 69.818182h-488.727272A128.232727 128.232727 0 0 0 139.636364 197.818182v628.363636a128.232727 128.232727 0 0 0 128 128h488.727272a128 128 0 0 0 128-128v-628.363636A128 128 0 0 0 756.363636 69.818182z m-139.636363 698.181818h-209.454546a34.909091 34.909091 0 0 1 0-69.818182h209.454546a34.909091 34.909091 0 0 1 0 69.818182z"
} as const;
export function detailActionIcon(kind: keyof typeof actionPaths): string {
return `<svg class="play-action-icon" viewBox="0 0 1024 1024" aria-hidden="true"><path d="${actionPaths[kind]}"/></svg>`;
}
export function overviewMarkup(drama: DramaDetail, first: Chapter | undefined, actions: string): string {
const genres = (Array.isArray(drama.tagNames) ? drama.tagNames : Array.isArray(drama.themeNames) ? drama.themeNames : []).filter(tag => typeof tag === 'string');
const badge = drama.isFeatured ? '<span class="series-badge series-badge--hot">爆剧</span>' : drama.isNew ? '<span class="series-badge series-badge--new">新剧</span>' : '';
return `<div class="series-overview">
<img class="series-poster" ${imageAttributes(drama.cover || drama.coverImg)} width="640" height="914" alt="${escape(title(drama))}封面">
<div class="series-info">
<div class="series-title-row"><h1 id="play-detail-title">${escape(title(drama))}</h1><span class="series-heat"><img class="series-heat-icon" src="/assets/9df0afe903e8d542.svg" width="16" height="16" alt="剧集热度图标" aria-hidden="true">${formatCount(drama.heatValue)}热度</span></div>
<div class="series-tags" aria-label="剧集标签">${badge}<span class="series-stat" data-engage-count="collect" aria-live="polite">${formatCount(drama.countCollect)}收藏</span><span class="series-stat" data-engage-count="like" aria-live="polite">${formatCount(drama.countLike)}点赞</span></div>
${genres.length ? `<p class="series-genres">${escape(genres.join(' · '))}</p>` : ''}
<div class="series-description-wrap"><p class="series-description" id="series-description-text">${escape(drama.desc || drama.description)}</p><button class="series-description-toggle" type="button" aria-expanded="false" aria-controls="series-description-text" hidden>展开</button></div>
<div class="series-action-bar"><div class="series-actions">
${first ? `<a class="primary detail-play" href="${contentPath('play',drama,first.id)}"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg><span>立即播放</span></a>` : '<span class="series-unavailable">暂无可用剧集</span>'}
<a class="series-download" data-app-download-entry data-app-download target="_blank" rel="sponsored nofollow noopener noreferrer" hidden><svg class="series-download-phone" viewBox="0 0 24 24" aria-hidden="true"><rect x="6" y="2" width="12" height="20" rx="3"/><path d="M10 5h4M11 19h2"/></svg><span>下载 App</span></a>
</div>${actions}</div>
</div>
</div>`;
}
export function officialLinks(config: Config): string {
const settings = {...config.newAddressInfo,...config.config};
const primary = safeUrl(settings.newUrlAddr) || recordList<{url?:unknown}>(config.navPageList).map(item => safeUrl(item.url)).find(Boolean) || '';
const permanent = safeUrl(settings.foreverUrlAddr);
const group = safeUrl(settings.tgGroup);
const contact = safeUrl(settings.telegram);
const external = (url: string, label = url) => `<a href="${escape(url)}" target="_blank" rel="sponsored nofollow noopener noreferrer">${escape(label)}</a>`;
return `<section class="play-official" aria-label="官方链接">
${primary ? `<div class="play-official-line play-official-inline"><span>官方入口:</span>${external(primary)}</div>` : ''}
${permanent ? `<div class="play-official-line play-official-inline"><span>永久地址:</span>${external(permanent)}</div>` : ''}
${group ? `<div class="play-official-line">${external(group,'点击加入91短剧官方 Telegram 群')}</div>` : ''}
<div class="play-official-line play-official-inline"><span>官方客服:</span>${contact ? external(contact,'立即联系') : '<a href="/info/feedback/">立即联系</a>'}</div>
</section>`;
}
export function bindSeriesDescription(root: HTMLElement, signal: AbortSignal): () => void {
const text = root.querySelector<HTMLElement>('#series-description-text');
const toggle = root.querySelector<HTMLButtonElement>('.series-description-toggle');
if (!text || !toggle) return () => {};
const update = () => {
if (signal.aborted || !text.clientWidth) return;
const lineHeight = Number.parseFloat(getComputedStyle(text).lineHeight) || 26.6;
const overflow = text.scrollHeight > lineHeight*3 + 1;
toggle.hidden = !overflow;
if (!overflow) {
text.classList.remove('is-expanded');
toggle.setAttribute('aria-expanded','false');toggle.textContent = '展开';
}
};
toggle.addEventListener('click',() => {
const expanded = toggle.getAttribute('aria-expanded') !== 'true';
text.classList.toggle('is-expanded',expanded);
toggle.setAttribute('aria-expanded',String(expanded));toggle.textContent = expanded ? '收起' : '展开';
},{signal});
const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(update);
observer?.observe(text);
if (!observer) window.addEventListener('resize',update,{signal});
const dispose = () => observer?.disconnect();
signal.addEventListener('abort',dispose,{once:true});
update();
return dispose;
}