restore gpt seo copy pipeline and stabilize seo tkd output

This commit is contained in:
root
2026-04-16 22:45:00 +08:00
parent 09b2137fa6
commit 2a905bbf0f
18 changed files with 3587 additions and 85 deletions

View File

@@ -26,8 +26,10 @@ class SeoCopyFallbackBuilder
{ {
$strSiteName = self::siteName($arrFacts); $strSiteName = self::siteName($arrFacts);
$strScopes = self::joinItems((array)($arrFacts['content_scopes'] ?? []), 3, '影视内容'); $strScopes = self::joinItems((array)($arrFacts['content_scopes'] ?? []), 3, '影视内容');
$strLanding = $strSiteName . '首页当前覆盖' . $strScopes . '等常见内容入口,适合先看首屏推荐,再按分类、榜单或搜索继续缩小范围。';
return [ return [
'search_landing' => $strLanding,
'intro_text' => $strSiteName . '首页当前覆盖' . $strScopes . '等常见内容入口,建议先看首屏内容卡片,再继续按分类、榜单或搜索缩小范围。', 'intro_text' => $strSiteName . '首页当前覆盖' . $strScopes . '等常见内容入口,建议先看首屏内容卡片,再继续按分类、榜单或搜索缩小范围。',
'intro_meta' => '首页更适合先发现值得点开的内容,说明性提示只做辅助,不建议压住首屏主体内容。', 'intro_meta' => '首页更适合先发现值得点开的内容,说明性提示只做辅助,不建议压住首屏主体内容。',
'guide_cards' => [ 'guide_cards' => [
@@ -94,8 +96,10 @@ class SeoCopyFallbackBuilder
private static function buildRankIndex(array $arrFacts): array private static function buildRankIndex(array $arrFacts): array
{ {
$strPeriods = self::joinItems((array)($arrFacts['rank_periods'] ?? []), 3, '常见榜单周期'); $strPeriods = self::joinItems((array)($arrFacts['rank_periods'] ?? []), 3, '常见榜单周期');
$strLanding = '当前榜单首页汇总了' . $strPeriods . '等常见周期入口,适合先确定周期,再继续进入具体榜单列表页。';
return [ return [
'search_landing' => $strLanding,
'intro_text' => '当前榜单首页汇总了' . $strPeriods . '等常见周期入口,适合先确定要看的榜单范围再继续下钻。', 'intro_text' => '当前榜单首页汇总了' . $strPeriods . '等常见周期入口,适合先确定要看的榜单范围再继续下钻。',
'intro_meta' => '如果你更关心具体周期,可以直接进入对应榜单列表页,再继续看详情和播放路径。', 'intro_meta' => '如果你更关心具体周期,可以直接进入对应榜单列表页,再继续看详情和播放路径。',
'guide_cards' => [ 'guide_cards' => [
@@ -111,8 +115,10 @@ class SeoCopyFallbackBuilder
$strPeriod = self::fallback((string)($arrFacts['rank_period_name'] ?? ''), '当前周期'); $strPeriod = self::fallback((string)($arrFacts['rank_period_name'] ?? ''), '当前周期');
$strScope = self::fallback((string)($arrFacts['rank_scope_name'] ?? ''), '当前范围'); $strScope = self::fallback((string)($arrFacts['rank_scope_name'] ?? ''), '当前范围');
$intVisible = (int)($arrFacts['page_stats']['visible'] ?? 0); $intVisible = (int)($arrFacts['page_stats']['visible'] ?? 0);
$strLanding = '当前榜单列表页展示的是' . $strScope . '下的' . $strPeriod . '结果,当前页可继续浏览的条目约为' . max(1, $intVisible) . '条。';
return [ return [
'search_landing' => $strLanding,
'intro_text' => '当前榜单列表页展示的是' . $strScope . '下的' . $strPeriod . '结果,当前页可继续浏览的条目约为' . max(1, $intVisible) . '条。', 'intro_text' => '当前榜单列表页展示的是' . $strScope . '下的' . $strPeriod . '结果,当前页可继续浏览的条目约为' . max(1, $intVisible) . '条。',
'intro_meta' => '如果你想横向对比结果,可以切到其他周期榜单,或者直接进入详情页继续确认内容。', 'intro_meta' => '如果你想横向对比结果,可以切到其他周期榜单,或者直接进入详情页继续确认内容。',
'guide_cards' => [ 'guide_cards' => [

View File

@@ -24,7 +24,93 @@ class SeoCopyStore
return trim($strTemplate); return trim($strTemplate);
} }
return trim(strtr($strTemplate, $arrReplace)); foreach ($arrTokens as $strKey => $mValue) {
$strKey = trim((string)$strKey);
if ($strKey === '' || trim((string)$mValue) !== '') {
continue;
}
$strTemplate = self::stripEmptyTokenArtifacts($strTemplate, $strKey);
}
return self::normalizeInterpolatedText(strtr($strTemplate, $arrReplace));
}
private static function stripEmptyTokenArtifacts(string $strTemplate, string $strKey): string
{
$strToken = preg_quote('{' . $strKey . '}', '/');
$arrLabelMap = [
'video_alias' => ['别名'],
'year' => ['年份'],
'area_name' => ['地区'],
'lang_name' => ['语言'],
'actor_names' => ['演员'],
'director_names' => ['导演'],
'remarks' => ['备注'],
'play_line' => ['播放线路', '线路'],
'episode_name' => ['集数', '期数', '剧集'],
];
foreach ((array)($arrLabelMap[$strKey] ?? []) as $strLabel) {
$strLabel = preg_quote($strLabel, '/');
$arrLabelPatterns = [
'/' . $strLabel . '\s*' . $strToken . '\s*[、,,]/u',
'/[、,,]\s*' . $strLabel . '\s*' . $strToken . '/u',
'/' . $strLabel . '\s*' . $strToken . '/u',
];
foreach ($arrLabelPatterns as $strPattern) {
$strTemplate = preg_replace($strPattern, '', $strTemplate) ?? $strTemplate;
}
}
$arrPatterns = [
// 删掉前置连词 + 空 token例如 “和{remarks}”
'/\s*(?:和|及|与|并|或)\s*' . $strToken . '/u',
// 删掉空 token + 后置连词,例如 “{remarks}和”
'/' . $strToken . '\s*(?:和|及|与|并|或)\s*/u',
// 删掉与空 token 紧邻的顿号/逗号
'/[、,,]\s*' . $strToken . '/u',
'/' . $strToken . '\s*[、,,]/u',
// 默认兜底:直接删掉空 token
'/' . $strToken . '/u',
];
foreach ($arrPatterns as $strPattern) {
$strTemplate = preg_replace($strPattern, '', $strTemplate) ?? $strTemplate;
}
return $strTemplate;
}
private static function normalizeInterpolatedText(string $strValue): string
{
$strValue = preg_replace('/\{[a-z0-9_]+\}/iu', '', $strValue) ?? $strValue;
$strValue = str_replace(['高清高清播放', '高清高清'], ['高清播放', '高清'], $strValue);
$strValue = preg_replace('/\s{2,}/u', ' ', $strValue) ?? $strValue;
$strValue = preg_replace('/\s*[_|]+\s*/u', ' - ', $strValue) ?? $strValue;
$strValue = preg_replace('/\s*-\s*-\s*/u', ' - ', $strValue) ?? $strValue;
$strValue = preg_replace('/(会把|把)([^,。;!?、\\s]{1,24})和([^,。;!?、\\s]{1,24})(等资料|这类线索)/u', '$1$2、$3$4', $strValue) ?? $strValue;
$strValue = preg_replace('/(年份|地区|语言|演员|导演|备注|别名|线路|集数)(?=[A-Za-z0-9])/u', '$1 ', $strValue) ?? $strValue;
$strValue = preg_replace('/(地区|语言|演员|导演|备注|别名)([\p{Han}A-Za-z])/u', '$1 $2', $strValue) ?? $strValue;
$strValue = preg_replace('/地区\s*([^,。;!?、]+?)和语言\s*([^,。;!?、]+)/u', '地区 $1语言 $2', $strValue) ?? $strValue;
$strValue = preg_replace('/(会把)([^,。;!?、\\s]{1,24})、([^,。;!?、\\s]{1,24})(这类线索)/u', '$1$2和$3$4', $strValue) ?? $strValue;
$strValue = preg_replace('/(会把)([^,。;!?、\\s]{1,24})、([^,。;!?、\\s]{1,24})(等资料)/u', '$1$2、$3$4', $strValue) ?? $strValue;
$strValue = preg_replace('/补看(\d{4}[^、,。;!?]*?)、([\p{Han}A-Za-z]{1,20})、([\p{Han}A-Za-z]{1,20})与([\p{Han}A-Za-z·、,]{1,40})/u', '补看 $1、$2、$3以及$4', $strValue) ?? $strValue;
$strValue = preg_replace('/(别名|年份|地区|语言|演员|导演|备注)\s+与/u', '$1与', $strValue) ?? $strValue;
$strValue = preg_replace('/本页,建议/u', '本页建议', $strValue) ?? $strValue;
$strValue = preg_replace('/本页,适合/u', '本页适合', $strValue) ?? $strValue;
$strValue = preg_replace('/本页,方便/u', '本页方便', $strValue) ?? $strValue;
$strValue = preg_replace('/([、,,])(?:\s*\\1)+/u', '$1', $strValue) ?? $strValue;
$strValue = preg_replace('/([(【\[])\s*[、,,\-]+\s*/u', '$1', $strValue) ?? $strValue;
$strValue = preg_replace('/\s*[、,,\-]+\s*([)】\]])/u', '$1', $strValue) ?? $strValue;
$strValue = preg_replace('/(会把|把|将|用|看)、/u', '$1', $strValue) ?? $strValue;
$strValue = preg_replace('/(\d{4})-\d{2}-\d{2}(?:\s+\d{2}:\d{2}:\d{2})?/u', '$1', $strValue) ?? $strValue;
$strValue = preg_replace('/\s*([,。;!?、])/u', '$1', $strValue) ?? $strValue;
$strValue = preg_replace('/([(【\[])\s+/u', '$1', $strValue) ?? $strValue;
$strValue = preg_replace('/\s+([)】\]])/u', '$1', $strValue) ?? $strValue;
return trim($strValue);
} }
public static function buildPageKey(array|string $mValue, string $strFallback = 'index'): string public static function buildPageKey(array|string $mValue, string $strFallback = 'index'): string

View File

@@ -4,6 +4,31 @@ namespace app\common\helper;
class SeoWordsHelper class SeoWordsHelper
{ {
/**
* Resolve a stable forge keyword from video seo words.
*/
public static function resolveForgeWord(array $video = [], int $intForgeId = 1): string
{
$intForgeId = max(1, $intForgeId);
$arrWords = array_values(array_filter(array_map(
static fn($mVal): string => trim((string)$mVal),
(array)($video['v_seo_words'] ?? [])
), static fn(string $strVal): bool => $strVal !== ''));
if (!empty($arrWords)) {
$intIndex = ($intForgeId - 1) % count($arrWords);
return $arrWords[$intIndex];
}
$strName = trim((string)($video['v_name'] ?? ''));
if ($strName !== '') {
return $strName;
}
return trim((string)($video['v_name_en'] ?? ''));
}
/** /**
* 根据 $intRewrite 处理 SeoWords * 根据 $intRewrite 处理 SeoWords
*/ */
@@ -22,32 +47,39 @@ class SeoWordsHelper
// 0原序全部 // 0原序全部
case 0: case 0:
$arrSeoWords = $words; $arrSeoWords = $words;
break;
// 1前 6 // 1前 6
case 1: case 1:
$arrSeoWords = array_slice($words, 0, 6); $arrSeoWords = array_slice($words, 0, 6);
break;
// 2随机 8 // 2随机 8
case 2: case 2:
shuffle($words); shuffle($words);
$arrSeoWords = array_slice($words, 0, 8); $arrSeoWords = array_slice($words, 0, 8);
break;
// 3插年份 // 3插年份
case 3: case 3:
$arrSeoWords = self::appendYear($words, $video); $arrSeoWords = self::appendYear($words, $video);
break;
// 4插“在线观看 / 免费 / 高清” // 4插“在线观看 / 免费 / 高清”
case 4: case 4:
$arrSeoWords = self::appendSuffix($words); $arrSeoWords = self::appendSuffix($words);
break;
// 5重排 + 去重 // 5重排 + 去重
case 5: case 5:
shuffle($words); shuffle($words);
$arrSeoWords = array_values(array_unique($words)); $arrSeoWords = array_values(array_unique($words));
break;
// 兜底 // 兜底
default: default:
$arrSeoWords = array_slice($words, 0, 6); $arrSeoWords = array_slice($words, 0, 6);
break;
} }
$arrNewsSeoWords = []; $arrNewsSeoWords = [];

View File

@@ -83,7 +83,7 @@ class UrlBuilder
return $this->replacePattern($pattern, [ return $this->replacePattern($pattern, [
'strParentCategory' => $strParentCategory, 'strParentCategory' => $strParentCategory,
'strCategory' => $strCategory, 'strCategory' => $strCategory,
'intPage' => max(1, (int)$intPage), 'intPage' => $this->normalizePageValue($intPage),
]); ]);
} }
@@ -226,4 +226,13 @@ class UrlBuilder
} }
return $this->trimSlash($pattern); return $this->trimSlash($pattern);
} }
protected function normalizePageValue(int|string $page): string
{
if (is_string($page) && strpos($page, '{page}') !== false) {
return '{page}';
}
return (string)max(1, (int)$page);
}
} }

View File

@@ -51,6 +51,11 @@ if ($strTmpCode == 'videoGpt1') {
return $SiteContext->getSiteMapByCode('VIDEO'); return $SiteContext->getSiteMapByCode('VIDEO');
})->ext('xml'); })->ext('xml');
// 榜单首页历史深链优先硬绑定,避免被通用 rank_list 模式误吞到列表页模板。
Route::get('/phb-index', function () {
return view('video/getRankIndex.html');
});
// ========== 1⃣ 取当前域名冻结的 family ========== // ========== 1⃣ 取当前域名冻结的 family ==========
$tpStyle = SiteStyle::getConfig(); $tpStyle = SiteStyle::getConfig();
$family = $tpStyle['template_cfg']['url_family']; $family = $tpStyle['template_cfg']['url_family'];
@@ -75,6 +80,21 @@ if ($strTmpCode == 'videoGpt1') {
} }
$registered[$route] = true; $registered[$route] = true;
// 兼容带 .html 的冻结路径。
// ThinkPHP 在旧路由里普遍使用 "/path" + ->ext('html')
// 而当前 family 里有一部分站点会直接冻结成 "/get.html"、"/record.html"。
// 这里同时补一个无后缀别名,避免这类路径在 GPT 新路由分支下落不到视图。
if (str_ends_with($route, '.html')) {
$routeWithoutExt = substr($route, 0, -5);
if ($routeWithoutExt !== '' && !isset($registered[$routeWithoutExt])) {
$alias = Route::get($routeWithoutExt, fn() => view($view))->ext('html');
if ($pattern) {
$alias->pattern($pattern);
}
$registered[$routeWithoutExt] = true;
}
}
}; };
// ========== 2.5️⃣ 冻结前按 sort 排序 ========== // ========== 2.5️⃣ 冻结前按 sort 排序 ==========
@@ -126,7 +146,7 @@ if ($strTmpCode == 'videoGpt1') {
break; break;
case 'category_home': case 'category_home':
$register($route, 'video/getCategory.html'); $register($route, 'video/getMap.html');
break; break;
case 'category_parent': case 'category_parent':
$register($route, 'video/getCategoryType.html',[ $register($route, 'video/getCategoryType.html',[
@@ -167,7 +187,7 @@ if ($strTmpCode == 'videoGpt1') {
$register($route, 'video/getRankIndex.html'); $register($route, 'video/getRankIndex.html');
break; break;
case 'rank_list': case 'rank_list':
$register($route, 'video/getRankIndex.html',[ $register($route, 'video/getRankList.html',[
'strParentCategory' => '[a-z\-]*', // 允许空值 'strParentCategory' => '[a-z\-]*', // 允许空值
'strCategory' => '[a-z\-]*', // 允许空值 'strCategory' => '[a-z\-]*', // 允许空值
'strSortType' => '[a-z\-]*', // 允许空值 'strSortType' => '[a-z\-]*', // 允许空值
@@ -851,7 +871,7 @@ if ($strTmpCode == 'videoGpt1') {
], ],
//排行榜 //排行榜
'video_rank_index' => ['/paihang/index', '/rank/index', '/paihang-quan', '/top/index', '/paihang-bang', '/ranks/index', '/sort/index', '/order/index', '/phb/index'], 'video_rank_index' => ['/paihang/index', '/rank/index', '/paihang-quan', '/top/index', '/paihang-bang', '/ranks/index', '/sort/index', '/order/index', '/phb/index', '/phb-index'],
//排行榜 //排行榜
'video_rank_list' => [ 'video_rank_list' => [

View File

@@ -1,12 +1,98 @@
{extend name="base" /} {extend name="base" /}
{block name="get-data"} {block name="get-data"}
{video:seocopy scene="home" export_name="seoCopy" /}
{php}
$strHomeIntroTitle = trim((string)($DomainModel->d_name ?? ''));
if ($strHomeIntroTitle === '') {
$strHomeIntroTitle = '影视内容推荐';
}
$strHomeIntroText = trim((string)($seoCopy['intro_text'] ?? $seoCopy['category_intro'] ?? ''));
if ($strHomeIntroText === '') {
$strHomeIntroText = trim((string)($seoCopy['description'] ?? ''));
}
if ($strHomeIntroText === '') {
$strHomeIntroText = $strHomeIntroTitle . '首页整理了电影、剧集、综艺、动漫等常见入口,适合先看首屏推荐,再继续按分类、榜单和搜索缩小范围。';
}
$strHomeIntroMeta = trim((string)($seoCopy['intro_meta'] ?? $seoCopy['faq_content'] ?? ''));
if ($strHomeIntroMeta === '') {
$strHomeIntroMeta = '建议先看首屏推荐,再根据题材、热度和搜索词继续深入浏览。';
}
$arrHomeCategoryNames = array_values(array_filter(array_map(static function ($arrCategory): string {
return trim((string)($arrCategory['name'] ?? ''));
}, (array)($TpStyle['template_cfg']['pages']['home']['categories'] ?? [])), static function (string $strName): bool {
return $strName !== '';
}));
$strHomeCategorySummary = implode('、', array_slice($arrHomeCategoryNames, 0, 3));
if ($strHomeCategorySummary === '') {
$strHomeCategorySummary = '电影、剧集、综艺';
}
$arrHomeSlotKeys = array_values(array_filter(array_map(static function ($arrSlot): string {
return trim((string)($arrSlot['layout_key'] ?? ''));
}, (array)($TpStyle['template_cfg']['pages']['home']['slots'] ?? [])), static function (string $strKey): bool {
return $strKey !== '';
}));
$strHomePrimarySlot = $arrHomeSlotKeys[0] ?? 'news';
$arrHomePrimarySlotLabelMap = [
'news' => '更新区',
'rank' => '榜单区',
'tuijian' => '推荐区',
'update' => '更新区',
'piaofang' => '热度区',
'lunli' => '内容区',
];
$strHomePrimarySlotLabel = $arrHomePrimarySlotLabelMap[$strHomePrimarySlot] ?? '推荐区';
$intHomeTopSlotCount = count((array)($TpStyle['template_cfg']['pages']['home']['slots'] ?? []));
$intHomeCategoryCount = count((array)($TpStyle['template_cfg']['pages']['home']['categories'] ?? []));
$arrHomeQuickSignals = [
['label' => '首屏主区', 'text' => '先看' . $strHomePrimarySlotLabel],
['label' => '频道方向', 'text' => $strHomeCategorySummary],
['label' => '内容覆盖', 'text' => '当前约有' . (1 + $intHomeTopSlotCount + $intHomeCategoryCount) . '组入口区块'],
['label' => '浏览节奏', 'text' => '首屏吸引 -> 详情判断 -> 播放选择'],
];
$arrHomeGuideCards = array_values(array_filter((array)($seoCopy['guide_cards'] ?? []), static function ($arrCard): bool {
return is_array($arrCard)
&& (
trim((string)($arrCard['title'] ?? '')) !== ''
|| trim((string)($arrCard['text'] ?? '')) !== ''
);
}));
if (empty($arrHomeGuideCards)) {
$arrHomeGuideCards = [
[
'title' => '首屏入口',
'text' => '首页第一屏更适合先看' . $strHomePrimarySlotLabel . '和刚刚更新,快速判断当前最值得点开的内容。',
],
[
'title' => '内容范围',
'text' => '当前首页已经把' . $strHomeCategorySummary . '等入口串起来,适合横向切换不同内容类型。',
],
[
'title' => '搜索建议',
'text' => '如果你已经知道片名、演员或题材词,直接用搜索通常会比翻列表更快。',
],
[
'title' => '后续路径',
'text' => '找到感兴趣的内容后,建议先看详情页确认题材和线路,再决定是否直接进入播放页。',
],
];
}
{/php}
{/block} {/block}
{block name="title"}{site:replace code="VIDEO@INDEX@INDEX@TITLE"}{/block} {block name="title"}{site:seotkd code="title" page="home" /}{/block}
{block name="keywords"}{site:replace code="VIDEO@INDEX@INDEX@KEYWORDS"}{/block} {block name="keywords"}{site:seotkd code="keywords" page="home" /}{/block}
{block name="description"}{site:replace code="VIDEO@INDEX@INDEX@DESCRIPTION"}{/block} {block name="description"}{site:seotkd code="description" page="home" /}{/block}
{block name="head"} {block name="head"}
<meta name="robots" content="index,follow"> <meta name="robots" content="index,follow">
@@ -14,8 +100,8 @@
{// 社交媒体标签 } {// 社交媒体标签 }
<meta property="og:title" content='{site:replace code="VIDEO@INDEX@INDEX@TITLE"}' /> <meta property="og:title" content='{site:seotkd code="title" page="home" /}' />
<meta property="og:description" content='{site:replace code="VIDEO@INDEX@INDEX@DESCRIPTION"}' /> <meta property="og:description" content='{site:seotkd code="description" page="home" /}' />
<meta property="og:url" content="https://{$DomainModel->d_domain}" /> <meta property="og:url" content="https://{$DomainModel->d_domain}" />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
{// 结构化数据 } {// 结构化数据 }
@@ -26,7 +112,7 @@
"name": "{$DomainModel->d_name}", "name": "{$DomainModel->d_name}",
"url": "https://{$DomainModel->d_domain}", "url": "https://{$DomainModel->d_domain}",
"alternateName": "{$DomainModel->d_domain}", "alternateName": "{$DomainModel->d_domain}",
"description": "{site:replace code="VIDEO@INDEX@INDEX@DESCRIPTION"}", "description": "{site:seotkd code='description' page='home' /}",
"potentialAction": { "potentialAction": {
"@type": "SearchAction", "@type": "SearchAction",
"target": 'https://{$DomainModel->d_domain}{site:vsurl key="search_term_string" p="1"/}', "target": 'https://{$DomainModel->d_domain}{site:vsurl key="search_term_string" p="1"/}',
@@ -42,6 +128,25 @@
{block name="main"} {block name="main"}
<main class="page-home" style="max-width:{$TpStyle.template_cfg.global.page_max_width_pc}px;margin:0 auto;"> <main class="page-home" style="max-width:{$TpStyle.template_cfg.global.page_max_width_pc}px;margin:0 auto;">
<section class="{$TpStyle.dom_prefix}-home-intro" style="margin:0 0 18px;padding:20px 22px;border:1px solid var(--border-color);border-radius:24px;background:linear-gradient(135deg,rgba(255,255,255,.98),var(--bg-soft-color));box-shadow:0 12px 28px rgba(0,0,0,.04);">
<div style="display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:10px;">
<span style="display:inline-flex;width:28px;height:3px;border-radius:999px;background:linear-gradient(90deg,var(--color-primary),var(--color-secondary));"></span>
<span style="display:inline-flex;align-items:center;padding:4px 10px;border-radius:999px;background:rgba(0,0,0,.04);color:var(--text-muted-color);font-size:12px;line-height:1.5;">首页导览</span>
</div>
<h1 style="margin:0 0 10px;color:var(--text-color);font-size:24px;line-height:1.35;">{$strHomeIntroTitle}</h1>
<p style="margin:0 0 10px;color:var(--text-color);font-size:15px;line-height:1.9;">{$strHomeIntroText}</p>
<p style="margin:0;color:var(--text-muted-color);font-size:13px;line-height:1.8;">{$strHomeIntroMeta}</p>
</section>
<section class="{$TpStyle.dom_prefix}-home-quick-signals" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:10px;margin:0 0 18px;">
{foreach $arrHomeQuickSignals as $arrSignal}
<article style="padding:12px 14px;border-radius:16px;background:rgba(255,255,255,.92);border:1px solid rgba(0,0,0,.06);box-shadow:0 8px 18px rgba(0,0,0,.03);">
<strong style="display:block;margin:0 0 6px;color:var(--text-color);font-size:12px;line-height:1.5;">{$arrSignal.label}</strong>
<span style="display:block;color:var(--text-muted-color);font-size:13px;line-height:1.7;">{$arrSignal.text}</span>
</article>
{/foreach}
</section>
{// lunli } {// lunli }
{assign name="Slot" value="$TpStyle.template_cfg.pages.home.lunliSlots[0]"} {assign name="Slot" value="$TpStyle.template_cfg.pages.home.lunliSlots[0]"}
{assign name="title" value="$Slot.title_text"} {assign name="title" value="$Slot.title_text"}
@@ -74,6 +179,8 @@
{/foreach} {/foreach}
{include file="module/seo_copy/collection" /}
{// 分类} {// 分类}
{foreach $TpStyle.template_cfg.pages.home.categories as $HomeCategory} {foreach $TpStyle.template_cfg.pages.home.categories as $HomeCategory}
@@ -105,4 +212,3 @@
</main> </main>
{/block} {/block}

View File

@@ -1,80 +1,207 @@
{// ===================== Desc Variants ===================== } {// ===================== Desc Variants ===================== }
{php}
$intCurrentForgeId = (int)(request()->route('intVForgeId') ?? 0);
$strSeoCopyScene = $intCurrentForgeId > 0 ? 'forge' : 'detail';
{/php}
{video:seocopy scene="$strSeoCopyScene" video="$arrVideo" export_name="seoCopy" /}
{php}
$strDetailMainDescription = trim(preg_replace('/\s+/u', ' ', strip_tags((string)($arrVideo['v_description'] ?? ''))));
if ($strDetailMainDescription === '') {
$strDetailMainDescription = '当前内容暂无完整剧情简介,建议结合详情信息与播放入口继续判断。';
}
$arrPlayGroups = is_array($arrVideo['v_play_url'] ?? null) ? $arrVideo['v_play_url'] : [];
$strPrimaryPlayType = '';
$intPrimaryPlayIndex = 1;
if (!empty($arrPlayGroups)) {
$strPrimaryPlayType = (string)array_key_first($arrPlayGroups);
}
$strDetailSlug = trim((string)($arrVideo['v_name_en'] ?? request()->route('strPinyin') ?? ''));
if ($strDetailSlug === '') {
$strDetailSlug = (string)($arrVideo['v_id'] ?? '');
}
$strPreferredPlayUrl = '';
if ((int)($arrVideo['v_id'] ?? 0) > 0 && $strPrimaryPlayType !== '') {
$strPreferredPlayUrl = app(\app\services\VideoService::class)->getVideoPlayUrl(
(int)$arrVideo['v_id'],
$strDetailSlug,
$strPrimaryPlayType,
$intPrimaryPlayIndex
);
}
$boolIsForgeDetail = $intCurrentForgeId > 0;
$strOriginVideoName = trim((string)($arrVideo['v_name'] ?? ''));
$strMainVideoInfoUrl = '';
if ((int)($arrVideo['v_id'] ?? 0) > 0) {
$strMainVideoInfoUrl = app(\app\services\VideoService::class)->getVideoInfoUrl(
(int)$arrVideo['v_id'],
$strDetailSlug
);
}
$strDetailBody = trim((string)($seoCopy['detail_faq'] ?? ''));
$strDetailBodyLead = trim((string)($seoCopy['detail_body_lead'] ?? ''));
$strDetailPlayLinkLead = trim((string)($seoCopy['detail_play_link_lead'] ?? ''));
$strDetailBodyTail = trim((string)($seoCopy['detail_body_tail'] ?? ''));
$strForgeBodyLead = trim((string)($seoCopy['forge_body_lead'] ?? ''));
$strForgeReturnLead = trim((string)($seoCopy['forge_return_lead'] ?? ''));
if ($strForgeReturnLead === '') {
$strForgeReturnLead = '如果想回到标准详情,可返回';
}
$strForgePlayLead = trim((string)($seoCopy['forge_play_lead'] ?? ''));
$strForgeDetailNote = trim((string)($seoCopy['forge_detail_note'] ?? ''));
$arrGuideCards = array_values(array_filter((array)($seoCopy['guide_cards'] ?? []), static function ($arrCard): bool {
return is_array($arrCard)
&& (
trim((string)($arrCard['title'] ?? '')) !== ''
|| trim((string)($arrCard['text'] ?? '')) !== ''
);
}));
$arrForgeEntryLinks = [];
foreach ($arrGuideCards as $arrGuideCard) {
$strGuideTitle = trim((string)($arrGuideCard['title'] ?? ''));
$strGuideText = trim((string)($arrGuideCard['text'] ?? ''));
$strGuideLine = trim($strGuideTitle . ($strGuideText !== '' ? '' . $strGuideText : ''));
if ($strGuideLine !== '') {
$arrForgeEntryLinks[] = [
'url' => $strMainVideoInfoUrl !== '' ? $strMainVideoInfoUrl : '/',
'text' => $strGuideLine,
];
}
}
{/php}
<style>
.{$TpStyle.dom_prefix}-dm-desc,
.{$TpStyle.dom_prefix}-dm-desc-body,
.{$TpStyle.dom_prefix}-dm-desc-guide,
.{$TpStyle.dom_prefix}-dm-forge-note {
min-width: 0;
word-break: break-word;
}
.{$TpStyle.dom_prefix}-dm-desc p,
.{$TpStyle.dom_prefix}-dm-desc li,
.{$TpStyle.dom_prefix}-dm-desc blockquote,
.{$TpStyle.dom_prefix}-dm-desc summary,
.{$TpStyle.dom_prefix}-dm-desc-body p,
.{$TpStyle.dom_prefix}-dm-desc-guide p,
.{$TpStyle.dom_prefix}-dm-forge-note p {
font-size: 14px;
line-height: 1.78;
}
.{$TpStyle.dom_prefix}-dm-desc header,
.{$TpStyle.dom_prefix}-dm-desc summary {
font-size: 16px;
line-height: 1.45;
}
@media (max-width: 768px) {
.{$TpStyle.dom_prefix}-dm-desc p,
.{$TpStyle.dom_prefix}-dm-desc li,
.{$TpStyle.dom_prefix}-dm-desc blockquote,
.{$TpStyle.dom_prefix}-dm-desc summary,
.{$TpStyle.dom_prefix}-dm-desc-body p,
.{$TpStyle.dom_prefix}-dm-desc-guide p,
.{$TpStyle.dom_prefix}-dm-forge-note p {
font-size: 13px !important;
line-height: 1.74 !important;
}
.{$TpStyle.dom_prefix}-dm-desc header,
.{$TpStyle.dom_prefix}-dm-desc summary {
font-size: 15px !important;
}
.{$TpStyle.dom_prefix}-dm-desc-body,
.{$TpStyle.dom_prefix}-dm-desc-guide,
.{$TpStyle.dom_prefix}-dm-forge-note {
margin-top: 8px !important;
padding: 10px 10px !important;
border-radius: 12px !important;
}
}
</style>
{if $variant == 0} {if $variant == 0}
<p class="{$TpStyle.dom_prefix}-dm-desc v0"> <p class="{$TpStyle.dom_prefix}-dm-desc v0">
{$arrVideo.v_description} {$strDetailMainDescription}
</p> </p>
{elseif $variant == 1} {elseif $variant == 1}
<div class="{$TpStyle.dom_prefix}-dm-desc v1"> <div class="{$TpStyle.dom_prefix}-dm-desc v1">
<p>{$arrVideo.v_description|mb_substr=0,120}</p> <p>{$strDetailMainDescription|mb_substr=0,120}</p>
</div> </div>
{elseif $variant == 2} {elseif $variant == 2}
<section class="{$TpStyle.dom_prefix}-dm-desc v2"> <section class="{$TpStyle.dom_prefix}-dm-desc v2">
<p>{$arrVideo.v_description|mb_substr=0,200}</p> <p>{$strDetailMainDescription|mb_substr=0,200}</p>
</section> </section>
{elseif $variant == 3} {elseif $variant == 3}
<article class="{$TpStyle.dom_prefix}-dm-desc v3"> <article class="{$TpStyle.dom_prefix}-dm-desc v3">
<p>{$arrVideo.v_description}</p> <p>{$strDetailMainDescription}</p>
</article> </article>
{elseif $variant == 4} {elseif $variant == 4}
<div class="{$TpStyle.dom_prefix}-dm-desc v4"> <div class="{$TpStyle.dom_prefix}-dm-desc v4">
<p> <p>
剧情简介:{$arrVideo.v_description|mb_substr=0,150} 剧情简介:{$strDetailMainDescription|mb_substr=0,150}
</p> </p>
</div> </div>
{elseif $variant == 5} {elseif $variant == 5}
<details class="{$TpStyle.dom_prefix}-dm-desc v5"> <details class="{$TpStyle.dom_prefix}-dm-desc v5">
<summary>剧情介绍</summary> <summary>剧情介绍</summary>
<p>{$arrVideo.v_description}</p> <p>{$strDetailMainDescription}</p>
</details> </details>
{elseif $variant == 6} {elseif $variant == 6}
<div class="{$TpStyle.dom_prefix}-dm-desc v6"> <div class="{$TpStyle.dom_prefix}-dm-desc v6">
<p>{$arrVideo.v_description|mb_substr=0,100}...</p> <p>{$strDetailMainDescription|mb_substr=0,100}...</p>
<a href="#desc">查看详情</a> <a href="#desc">查看详情</a>
</div> </div>
{elseif $variant == 7} {elseif $variant == 7}
<section class="{$TpStyle.dom_prefix}-dm-desc v7"> <section class="{$TpStyle.dom_prefix}-dm-desc v7">
<header>影片简介</header> <header>影片简介</header>
<p>{$arrVideo.v_description|mb_substr=0,180}</p> <p>{$strDetailMainDescription|mb_substr=0,180}</p>
</section> </section>
{elseif $variant == 8} {elseif $variant == 8}
<div class="{$TpStyle.dom_prefix}-dm-desc v8"> <div class="{$TpStyle.dom_prefix}-dm-desc v8">
<blockquote> <blockquote>
{$arrVideo.v_description|mb_substr=0,160} {$strDetailMainDescription|mb_substr=0,160}
</blockquote> </blockquote>
</div> </div>
{elseif $variant == 9} {elseif $variant == 9}
<p class="{$TpStyle.dom_prefix}-dm-desc v9"> <p class="{$TpStyle.dom_prefix}-dm-desc v9">
{$arrVideo.v_name}{$arrVideo.v_year})剧情: {$arrVideo.v_name}{$arrVideo.v_year})剧情:
{$arrVideo.v_description|mb_substr=0,140} {$strDetailMainDescription|mb_substr=0,140}
</p> </p>
{elseif $variant == 10} {elseif $variant == 10}
<div class="{$TpStyle.dom_prefix}-dm-desc v10"> <div class="{$TpStyle.dom_prefix}-dm-desc v10">
<p itemprop="description"> <p itemprop="description">
{$arrVideo.v_description|mb_substr=0,160} {$strDetailMainDescription|mb_substr=0,160}
</p> </p>
</div> </div>
{elseif $variant == 11} {elseif $variant == 11}
<div class="{$TpStyle.dom_prefix}-dm-desc v11" <div class="{$TpStyle.dom_prefix}-dm-desc v11"
data-desc="{$arrVideo.v_description|mb_substr=0,200}"> data-desc="{$strDetailMainDescription|mb_substr=0,200}">
<p>{$arrVideo.v_description|mb_substr=0,120}</p> <p>{$strDetailMainDescription|mb_substr=0,120}</p>
</div> </div>
{elseif $variant == 12} {elseif $variant == 12}
<section class="{$TpStyle.dom_prefix}-dm-desc v12"> <section class="{$TpStyle.dom_prefix}-dm-desc v12">
<p> <p>
{$arrVideo.v_description|mb_substr=0,100} {$strDetailMainDescription|mb_substr=0,100}
<span class="{$TpStyle.dom_prefix}-dm-desc-more"> <span class="{$TpStyle.dom_prefix}-dm-desc-more">
{$arrVideo.v_name} 在线观看 {$arrVideo.v_name} 在线观看
</span> </span>
@@ -84,7 +211,7 @@
{elseif $variant == 13} {elseif $variant == 13}
<div class="{$TpStyle.dom_prefix}-dm-desc v13"> <div class="{$TpStyle.dom_prefix}-dm-desc v13">
<p> <p>
{$arrVideo.v_description|mb_substr=0,130} {$strDetailMainDescription|mb_substr=0,130}
高清完整版内容介绍。 高清完整版内容介绍。
</p> </p>
</div> </div>
@@ -92,7 +219,7 @@
{elseif $variant == 14} {elseif $variant == 14}
<div class="{$TpStyle.dom_prefix}-dm-desc v14"> <div class="{$TpStyle.dom_prefix}-dm-desc v14">
<p> <p>
{$arrVideo.v_description|mb_substr=0,150} {$strDetailMainDescription|mb_substr=0,150}
</p> </p>
<p class="{$TpStyle.dom_prefix}-dm-desc-note"> <p class="{$TpStyle.dom_prefix}-dm-desc-note">
本片支持在线播放 本片支持在线播放
@@ -101,7 +228,7 @@
{elseif $variant == 15} {elseif $variant == 15}
<p class="{$TpStyle.dom_prefix}-dm-desc v15"> <p class="{$TpStyle.dom_prefix}-dm-desc v15">
{$arrVideo.v_description|mb_substr=0,120} {$strDetailMainDescription|mb_substr=0,120}
免费观看{$arrVideo.v_name} 免费观看{$arrVideo.v_name}
</p> </p>
@@ -109,30 +236,59 @@
<div class="{$TpStyle.dom_prefix}-dm-desc v16"> <div class="{$TpStyle.dom_prefix}-dm-desc v16">
<p> <p>
<strong>{$arrVideo.v_name}</strong> <strong>{$arrVideo.v_name}</strong>
{$arrVideo.v_description|mb_substr=0,140} {$strDetailMainDescription|mb_substr=0,140}
</p> </p>
</div> </div>
{elseif $variant == 17} {elseif $variant == 17}
<section class="{$TpStyle.dom_prefix}-dm-desc v17"> <section class="{$TpStyle.dom_prefix}-dm-desc v17">
<p>{$arrVideo.v_description|mb_substr=0,180}</p> <p>{$strDetailMainDescription|mb_substr=0,180}</p>
</section> </section>
{elseif $variant == 18} {elseif $variant == 18}
<div class="{$TpStyle.dom_prefix}-dm-desc v18"> <div class="{$TpStyle.dom_prefix}-dm-desc v18">
<p class="{$TpStyle.dom_prefix}-dm-desc-hidden"> <p class="{$TpStyle.dom_prefix}-dm-desc-hidden">
{$arrVideo.v_description} {$strDetailMainDescription}
</p> </p>
</div> </div>
{elseif $variant == 19} {elseif $variant == 19}
<article class="{$TpStyle.dom_prefix}-dm-desc v19"> <article class="{$TpStyle.dom_prefix}-dm-desc v19">
<header>剧情简介</header> <header>剧情简介</header>
<p>{$arrVideo.v_description|mb_substr=0,160}</p> <p>{$strDetailMainDescription|mb_substr=0,160}</p>
</article> </article>
{/if} {/if}
{notempty name="$strDetailBody"}
<div class="{$TpStyle.dom_prefix}-dm-desc-body" style="margin-top:10px;padding:12px 12px;border:1px solid var(--border-color);border-radius:14px;background:var(--bg-soft-color);">
<p style="margin:0;color:var(--text-color);line-height:1.8;">{$strDetailBody}</p>
</div>
{/notempty}
{if empty($boolIsForgeDetail)}
{include file="module/seo_copy/detail" /}
{/if}
{if !empty($boolIsForgeDetail)}
<div class="{$TpStyle.dom_prefix}-dm-desc-guide" style="margin-top:10px;padding:12px 12px;border:1px solid var(--border-color);border-radius:14px;background:var(--bg-soft-color);">
<p style="margin:0;color:var(--text-color);line-height:1.8;">{$strForgeBodyLead|default='当前页围绕延伸词补充主片相关信息,适合先确认内容归属。'}</p>
<p style="margin:8px 0 0;color:var(--text-muted-color);line-height:1.7;">
{notempty name="$strMainVideoInfoUrl"}
{$strForgeReturnLead|default='如果想回到标准详情,可返回'} <a href="{$strMainVideoInfoUrl}">《{$strOriginVideoName}》详情页</a>
{/notempty}
{notempty name="$strPreferredPlayUrl"}
{$strForgePlayLead|default='如果准备继续观看,也可以直接进入'} <a href="{$strPreferredPlayUrl}">播放页</a>
{/notempty}
</p>
</div>
<div class="{$TpStyle.dom_prefix}-dm-forge-note" style="margin-top:10px;padding:12px 12px;border:1px solid var(--border-color);border-radius:14px;background:var(--bg-soft-color);">
<p style="margin:0;color:var(--text-color);line-height:1.8;">{$strForgeDetailNote|default='当前页提供的是延伸词视角,适合先确认内容归属,再决定是否回到标准详情或播放页。'}</p>
{notempty name="$strMainVideoInfoUrl"}
<p style="margin:8px 0 0;color:var(--text-muted-color);line-height:1.7;">如果你想继续查看主内容页,可以回到 <a href="{$strMainVideoInfoUrl}">《{$strOriginVideoName}》详情页</a></p>
{/notempty}
</div>
{/if}
{video:seoaddon video="$arrVideo" export_name="arrAddon" /} {video:seoaddon video="$arrVideo" export_name="arrAddon" /}
{include file="module/detail/_seo_addon" /} {include file="module/detail/_seo_addon" /}

View File

@@ -0,0 +1,57 @@
{php}
$arrGuideLines = [];
foreach ([
$seoCopy['search_landing'] ?? '',
$seoCopy['category_intro'] ?? '',
$seoCopy['intro_text'] ?? '',
$seoCopy['intro_meta'] ?? '',
$seoCopy['faq_content'] ?? '',
] as $strGuideLine) {
$strGuideLine = trim((string)$strGuideLine);
if ($strGuideLine !== '' && !in_array($strGuideLine, $arrGuideLines, true)) {
$arrGuideLines[] = $strGuideLine;
}
}
$arrGuideCards = array_values(array_filter((array)($seoCopy['guide_cards'] ?? []), static function ($arrCard): bool {
return is_array($arrCard)
&& (
trim((string)($arrCard['title'] ?? '')) !== ''
|| trim((string)($arrCard['text'] ?? '')) !== ''
);
}));
{/php}
{if !empty($arrGuideLines) || !empty($arrGuideCards)}
<section class="{$TpStyle.dom_prefix}-guide {$TpStyle.dom_prefix}-guide-collection"
style="margin:28px 0 18px;padding:20px 22px;border:1px solid var(--border-color);border-radius:22px;background:linear-gradient(180deg,var(--bg-soft-color),rgba(255,255,255,.96));box-shadow:0 12px 28px rgba(0,0,0,.04);">
{notempty name="arrGuideLines"}
<div style="display:grid;gap:10px;margin-bottom:{notempty name='arrGuideCards'}16px{else/}0{/notempty};">
{foreach $arrGuideLines as $strGuideLine}
<p style="margin:0;line-height:1.9;color:var(--text-color);font-size:15px;">
{$strGuideLine}
</p>
{/foreach}
</div>
{/notempty}
{notempty name="arrGuideCards"}
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px;">
{foreach $arrGuideCards as $arrGuideCard}
<article style="padding:14px 16px;border-radius:16px;background:rgba(255,255,255,.9);border:1px solid rgba(0,0,0,.06);">
{notempty name="$arrGuideCard.title"}
<h3 style="margin:0 0 8px;font-size:15px;line-height:1.4;color:var(--text-color);">
{$arrGuideCard.title}
</h3>
{/notempty}
{notempty name="$arrGuideCard.text"}
<p style="margin:0;line-height:1.8;color:var(--text-muted-color);font-size:13px;">
{$arrGuideCard.text}
</p>
{/notempty}
</article>
{/foreach}
</div>
{/notempty}
</section>
{/if}

View File

@@ -0,0 +1,55 @@
{php}
$arrGuideLines = [];
foreach ([
$seoCopy['detail_body_lead'] ?? '',
$seoCopy['detail_play_link_lead'] ?? '',
$seoCopy['detail_body_tail'] ?? '',
$seoCopy['detail_faq'] ?? '',
] as $strGuideLine) {
$strGuideLine = trim((string)$strGuideLine);
if ($strGuideLine !== '' && !in_array($strGuideLine, $arrGuideLines, true)) {
$arrGuideLines[] = $strGuideLine;
}
}
$arrGuideCards = array_values(array_filter((array)($seoCopy['guide_cards'] ?? []), static function ($arrCard): bool {
return is_array($arrCard)
&& (
trim((string)($arrCard['title'] ?? '')) !== ''
|| trim((string)($arrCard['text'] ?? '')) !== ''
);
}));
{/php}
{if !empty($arrGuideLines) || !empty($arrGuideCards)}
<section class="{$TpStyle.dom_prefix}-guide {$TpStyle.dom_prefix}-guide-detail"
style="margin-top:16px;padding:18px 20px;border:1px solid rgba(0,0,0,.08);border-radius:20px;background:rgba(255,255,255,.92);">
{notempty name="arrGuideLines"}
<div style="display:grid;gap:10px;margin-bottom:{notempty name='arrGuideCards'}16px{else/}0{/notempty};">
{foreach $arrGuideLines as $strGuideLine}
<p style="margin:0;line-height:1.9;color:var(--text-color);font-size:14px;">
{$strGuideLine}
</p>
{/foreach}
</div>
{/notempty}
{notempty name="arrGuideCards"}
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:12px;">
{foreach $arrGuideCards as $arrGuideCard}
<article style="padding:12px 14px;border-radius:14px;background:var(--bg-soft-color);border:1px solid rgba(0,0,0,.05);">
{notempty name="$arrGuideCard.title"}
<h3 style="margin:0 0 8px;font-size:14px;line-height:1.4;color:var(--text-color);">
{$arrGuideCard.title}
</h3>
{/notempty}
{notempty name="$arrGuideCard.text"}
<p style="margin:0;line-height:1.8;color:var(--text-muted-color);font-size:13px;">
{$arrGuideCard.text}
</p>
{/notempty}
</article>
{/foreach}
</div>
{/notempty}
</section>
{/if}

View File

@@ -0,0 +1,55 @@
{php}
$arrGuideLines = [];
foreach ([
$seoCopy['play_intro'] ?? '',
$seoCopy['play_meta_note'] ?? '',
$seoCopy['play_body_lead'] ?? '',
$seoCopy['play_body_next'] ?? '',
] as $strGuideLine) {
$strGuideLine = trim((string)$strGuideLine);
if ($strGuideLine !== '' && !in_array($strGuideLine, $arrGuideLines, true)) {
$arrGuideLines[] = $strGuideLine;
}
}
$arrGuideCards = array_values(array_filter((array)($seoCopy['guide_cards'] ?? []), static function ($arrCard): bool {
return is_array($arrCard)
&& (
trim((string)($arrCard['title'] ?? '')) !== ''
|| trim((string)($arrCard['text'] ?? '')) !== ''
);
}));
{/php}
{if !empty($arrGuideLines) || !empty($arrGuideCards)}
<section class="{$TpStyle.dom_prefix}-guide {$TpStyle.dom_prefix}-guide-play"
style="margin:20px 0 6px;padding:18px 20px;border:1px solid rgba(0,0,0,.08);border-radius:20px;background:linear-gradient(180deg,rgba(255,255,255,.98),var(--bg-soft-color));">
{notempty name="arrGuideLines"}
<div style="display:grid;gap:10px;margin-bottom:{notempty name='arrGuideCards'}16px{else/}0{/notempty};">
{foreach $arrGuideLines as $strGuideLine}
<p style="margin:0;line-height:1.9;color:var(--text-color);font-size:14px;">
{$strGuideLine}
</p>
{/foreach}
</div>
{/notempty}
{notempty name="arrGuideCards"}
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:12px;">
{foreach $arrGuideCards as $arrGuideCard}
<article style="padding:12px 14px;border-radius:14px;background:rgba(255,255,255,.92);border:1px solid rgba(0,0,0,.05);">
{notempty name="$arrGuideCard.title"}
<h3 style="margin:0 0 8px;font-size:14px;line-height:1.4;color:var(--text-color);">
{$arrGuideCard.title}
</h3>
{/notempty}
{notempty name="$arrGuideCard.text"}
<p style="margin:0;line-height:1.8;color:var(--text-muted-color);font-size:13px;">
{$arrGuideCard.text}
</p>
{/notempty}
</article>
{/foreach}
</div>
{/notempty}
</section>
{/if}

View File

@@ -2,9 +2,9 @@
{block name="get-data"} {block name="get-data"}
{if $Request.route.intVId && $Request.route.intVForgeId} {if $Request.route.intVId && $Request.route.intVForgeId}
{video:info v_id="$Request.route.intVId" v_key="arrVideo" v_fid="$Request.route.intVForgeId" export_name="arrVideo"/} {video:info v_id="$Request.route.intVId" v_pinyin="$Request.route.strPinyin" v_key="arrVideo" v_fid="$Request.route.intVForgeId" export_name="arrVideo"/}
{elseif $Request.route.intVId} {elseif $Request.route.intVId || $Request.route.strPinyin}
{video:info v_id="$Request.route.intVId" v_key="arrVideo" export_name="arrVideo"/} {video:info v_id="$Request.route.intVId" v_pinyin="$Request.route.strPinyin" v_key="arrVideo" export_name="arrVideo"/}
{else} {else}
{video:info v_id="$DomainModel->info_id" n_key="arrVideo" export_name="arrVideo" /} {video:info v_id="$DomainModel->info_id" n_key="arrVideo" export_name="arrVideo" /}
{/if} {/if}
@@ -15,12 +15,83 @@
area="all" lang="all" year="all" order="all" page="1" area="all" lang="all" year="all" order="all" page="1"
export_name="strVclurl" /} export_name="strVclurl" /}
{video:seocopy scene="play"
video="$arrVideo"
play_type="$Request.route.strPlayType"
play_index="$Request.route.intPlayIndex"
export_name="seoCopy" /}
{php} {php}
$arrBreadcrumb = [ $arrBreadcrumb = [
['title'=>'首页','url'=>'/'], ['title'=>'首页','url'=>'/'],
['title'=>$arrVideo['v_parent_category'],'url'=>$strVciurl], ['title'=>$arrVideo['v_parent_category'],'url'=>$strVciurl],
['title'=>$arrVideo['v_category'],'url'=>$strVclurl], ['title'=>$arrVideo['v_category'],'url'=>$strVclurl],
]; ];
$strPlaySlug = trim((string)($arrVideo['v_name_en'] ?? request()->route('strPinyin') ?? ''));
if ($strPlaySlug === '') {
$strPlaySlug = (string)($arrVideo['v_id'] ?? '');
}
$strCurrentPlayType = trim((string)(request()->route('strPlayType') ?? ''));
$intCurrentPlayIndex = max(1, (int)(request()->route('intPlayIndex') ?? 1));
$arrPlayGroups = is_array($arrVideo['v_play_url'] ?? null) ? $arrVideo['v_play_url'] : [];
if ($strCurrentPlayType === '' && !empty($arrPlayGroups)) {
$strCurrentPlayType = (string)array_key_first($arrPlayGroups);
}
$strCurrentPlayLine = trim((string)app(\app\services\VideoService::class)->getConverterPlayLineVal($strCurrentPlayType));
if ($strCurrentPlayLine === '') {
$strCurrentPlayLine = strtoupper($strCurrentPlayType !== '' ? $strCurrentPlayType : 'default');
}
$strCurrentEpisodeName = '第' . $intCurrentPlayIndex . '集';
if (!empty($arrPlayGroups[$strCurrentPlayType][$intCurrentPlayIndex - 1]['name'])) {
$strCurrentEpisodeName = trim((string)$arrPlayGroups[$strCurrentPlayType][$intCurrentPlayIndex - 1]['name']);
}
$strPlayDetailUrl = '';
if ((int)($arrVideo['v_id'] ?? 0) > 0) {
$strPlayDetailUrl = app(\app\services\VideoService::class)->getVideoInfoUrl(
(int)$arrVideo['v_id'],
$strPlaySlug
);
}
$strPlayVisibleIntro = trim((string)($seoCopy['play_intro'] ?? ''));
if ($strPlayVisibleIntro === '') {
$strPlayVisibleIntro = '当前页围绕《' . (string)($arrVideo['v_name'] ?? '当前内容') . '》' . $strCurrentEpisodeName . '整理在线播放入口,并补充剧情与观看线索,方便直接续看。';
}
$strPlayMetaNote = trim((string)($seoCopy['play_meta_note'] ?? ''));
if ($strPlayMetaNote === '') {
$strPlayMetaNote = '当前线路为' . $strCurrentPlayLine . ',如果加载较慢可以切换线路,或先回详情页补看剧情资料。';
}
$strPlayBodyGuideLead = trim((string)($seoCopy['play_body_lead'] ?? ''));
if ($strPlayBodyGuideLead === '') {
$strPlayBodyGuideLead = '播放页更适合先确认当前线路、集数和回详情路径,再决定是否继续完整观看。';
}
$strPlayBodyGuideNext = trim((string)($seoCopy['play_body_next'] ?? ''));
if ($strPlayBodyGuideNext === '') {
$strPlayBodyGuideNext = '如果想先了解完整剧情和人物资料,可以回到详情页继续浏览。';
}
$arrPlayGuideCards = array_values(array_filter((array)($seoCopy['guide_cards'] ?? []), static function ($arrCard): bool {
return is_array($arrCard)
&& (
trim((string)($arrCard['title'] ?? '')) !== ''
|| trim((string)($arrCard['text'] ?? '')) !== ''
);
}));
if (empty($arrPlayGuideCards)) {
$arrPlayGuideCards = [
['title' => '当前剧集', 'text' => $strCurrentEpisodeName],
['title' => '当前线路', 'text' => $strCurrentPlayLine],
['title' => '观看建议', 'text' => '如果当前线路不够稳定,可先切换线路,或返回详情页继续浏览剧情资料。'],
];
}
{/php} {/php}
{/block} {/block}
@@ -181,6 +252,8 @@
{include file="module/page/page_router" /} {include file="module/page/page_router" /}
{include file="module/seo_copy/play" /}
<script> <script>
const strMaxPlayHeight = '480px' const strMaxPlayHeight = '480px'
const strVideoId = `{$arrVideo.v_id}`; const strVideoId = `{$arrVideo.v_id}`;

View File

@@ -458,10 +458,14 @@ class SiteContext
$strCategory = $this->Request->param('strCategory'); $strCategory = $this->Request->param('strCategory');
$strSortType = $this->Request->param('strSortType'); $strSortType = $this->Request->param('strSortType');
$intPage = $this->Request->param('intPage'); $intPage = $this->Request->param('intPage');
$strOrderName = '';
if (!empty($strSortType) && $strSortType !== 'all' && isset(StaticConfig::$arrVideoRankSortType[$strSortType])) {
$strOrderName = StaticConfig::$arrVideoRankSortType[$strSortType];
}
ConverterMovel::setVal([ ConverterMovel::setVal([
'strVideoParentCategoryName' => empty($strParentCategory) || $strParentCategory == 'all' ? '' : StaticConfig::$arrVideoClass[$strParentCategory], 'strVideoParentCategoryName' => empty($strParentCategory) || $strParentCategory == 'all' ? '' : StaticConfig::$arrVideoClass[$strParentCategory],
'strVideoCategoryName' => empty($strCategory) || $strCategory == 'all' ? '' : VideoCategoryModel::$arrCategory[$strCategory], 'strVideoCategoryName' => empty($strCategory) || $strCategory == 'all' ? '' : VideoCategoryModel::$arrCategory[$strCategory],
'strOrderName' => empty($strSortType) || $strSortType == 'all' ? '' : StaticConfig::$arrVideoRankSortType[$strSortType], 'strOrderName' => $strOrderName,
'intPage' => $intPage ?? 1, 'intPage' => $intPage ?? 1,
]); ]);
} }
@@ -746,23 +750,377 @@ class SiteContext
*/ */
public function getSeoTkd(string $strCode, string $strPage): string public function getSeoTkd(string $strCode, string $strPage): string
{ {
// TpStyle 已在 initCfg() 中注入 $strCode = strtolower(trim($strCode));
$TpStyle = $this->TpStyle; $strPage = strtolower(trim($strPage));
if (empty($TpStyle)) { if (!in_array($strCode, ['title', 'keywords', 'description'], true)) {
return ''; return '';
} }
// 1⃣ 选 SEO 模板 $arrCandidates = [];
$renderer = new SeoRenderer($TpStyle);
$tpl = $renderer->getTemplate($strCode, $strPage);
// 1. 新 GPT SEO 池
$arrCandidates[] = $this->renderSeoFromPool($strCode, $strPage);
// 2. 旧 SubjectFomart key 兼容层
$arrCandidates[] = $this->renderSeoFromLegacyKey($strCode, $strPage);
// 3. domain 表兜底
$arrCandidates[] = $this->renderSeoFromDomainRecord($strCode, $strPage);
foreach ($arrCandidates as $strCandidate) {
$strCandidate = $this->normalizeSeoText($strCandidate);
if ($strCode === 'keywords') {
$strCandidate = $this->normalizeSeoKeywords($strCandidate);
} elseif ($strCode === 'description') {
$strCandidate = $this->normalizeSeoDescription($strCandidate, $strPage);
}
if ($strCandidate !== '') {
return $strCandidate;
}
}
return '';
}
protected function renderSeoFromPool(string $strCode, string $strPage): string
{
$TpStyle = $this->TpStyle;
if (empty($TpStyle) || !is_array($TpStyle)) {
return '';
}
$renderer = new SeoRenderer($TpStyle);
$tpl = trim((string)$renderer->getTemplate($strCode, $strPage));
if ($tpl === '') { if ($tpl === '') {
return ''; return '';
} }
// var_dump($tpl);
// 2⃣ 统一走 Converter你现有系统 return $this->normalizeSeoText(ConverterMovel::convert($tpl));
return ConverterMovel::convert($tpl); }
protected function renderSeoFromLegacyKey(string $strCode, string $strPage): string
{
$strLegacyKey = $this->resolveLegacySeoKey($strCode, $strPage);
if ($strLegacyKey === '') {
return '';
}
try {
return $this->normalizeSeoText($this->converterTemplate($strLegacyKey));
} catch (\Throwable $Throwable) {
return '';
}
}
protected function renderSeoFromDomainRecord(string $strCode, string $strPage): string
{
if (!$this->DomainModel instanceof DomainModel) {
return '';
}
$strSiteName = trim((string)($this->DomainModel->d_name ?? ''));
$strHost = trim((string)($this->DomainModel->d_domain ?? ''));
if ($strPage === 'home') {
return match ($strCode) {
'title' => trim((string)($this->DomainModel->d_index_title ?? $strSiteName)),
'keywords' => trim((string)($this->DomainModel->d_index_keywords ?? $this->DomainModel->d_keywords ?? $strSiteName)),
'description' => trim((string)($this->DomainModel->d_index_description ?? $this->DomainModel->d_description ?? $strSiteName)),
default => '',
};
}
if ($strCode === 'title') {
$strEntityTitle = trim((string)(
ConverterMovel::getVal('strVideoName')
?? ConverterMovel::getVal('strVideoCategoryName')
?? ConverterMovel::getVal('strVideoParentCategoryName')
?? ''
));
if ($strEntityTitle !== '' && $strSiteName !== '') {
return $strEntityTitle . ' - ' . $strSiteName;
}
}
if ($strCode === 'keywords') {
$arrKeywords = array_filter([
trim((string)(ConverterMovel::getVal('strVideoName') ?? '')),
trim((string)(ConverterMovel::getVal('strVideoCategoryName') ?? '')),
trim((string)($this->DomainModel->d_index_keywords ?? '')),
trim((string)($this->DomainModel->d_keywords ?? '')),
], static function (string $strValue): bool {
return $strValue !== '';
});
if (!empty($arrKeywords)) {
return implode(',', array_values(array_unique($arrKeywords)));
}
}
if ($strCode === 'description') {
$strEntityDescription = trim((string)(
ConverterMovel::getVal('strVideoDescription')
?? ConverterMovel::getVal('strVideoCategoryName')
?? ConverterMovel::getVal('strVideoParentCategoryName')
?? ''
));
if ($strEntityDescription !== '') {
return $strEntityDescription;
}
$strIndexDescription = trim((string)($this->DomainModel->d_index_description ?? ''));
if ($strIndexDescription !== '') {
return $strIndexDescription;
}
}
$arrGeneric = [
'title' => $strSiteName !== '' ? $strSiteName : $strHost,
'keywords' => trim((string)($this->DomainModel->d_keywords ?? $this->DomainModel->d_index_keywords ?? $strSiteName)),
'description' => trim((string)($this->DomainModel->d_description ?? $this->DomainModel->d_index_description ?? $strSiteName)),
];
return trim((string)($arrGeneric[$strCode] ?? ''));
}
protected function normalizeSeoText(mixed $mValue): string
{
$strValue = trim((string)$mValue);
if ($strValue === '') {
return '';
}
$strValue = html_entity_decode($strValue, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$strValue = strip_tags($strValue);
$strValue = preg_replace('/\s+/u', ' ', $strValue) ?? $strValue;
$strValue = preg_replace('/\{[A-Z0-9@_:-]+\}/iu', '', $strValue) ?? $strValue;
$strValue = preg_replace('/^[\\s\\-|_,,。;:]+/u', '', $strValue) ?? $strValue;
$strValue = preg_replace('/[\\s\\-|_,,。;:]+$/u', '', $strValue) ?? $strValue;
if ($strValue === '' || preg_match('/^[-|_,,。;:\\s]+$/u', $strValue)) {
return '';
}
return trim($strValue);
}
protected function normalizeSeoKeywords(string $strValue): string
{
$strValue = str_replace(['', '、', ';', '', '|', ''], ',', $strValue);
$arrRawParts = preg_split('/\s*,\s*/u', $strValue) ?: [];
$arrNoiseSuffixes = [
'叙事模型',
'英雄之旅',
'成长母题',
'精神图腾',
'精神内核',
'热血传奇',
'荒诞美学',
'流浪美学',
'怀旧元素',
'世界观构建',
'文化符号',
'末日隐喻',
'双重叙事结构',
'叙事策略',
'氛围营造手法',
'角色塑造',
'诗化表达手法',
'史诗建构',
'疗愈力',
'英雄塑造',
'人性挖掘',
];
$arrStopWords = [
'在线',
'播放',
'观看',
'免费',
'高清',
'热播',
'推荐',
'完整',
'完整版',
'全集',
'中字',
'未删减',
'在线播放',
'在线观看',
'免费高清',
'高清免费视频',
'高清免费',
'免费播放',
];
$arrParts = [];
foreach ($arrRawParts as $strPart) {
$strPart = trim((string)$strPart);
if ($strPart === '') {
continue;
}
foreach ($arrNoiseSuffixes as $strSuffix) {
if ($strSuffix !== '' && str_ends_with($strPart, $strSuffix)) {
$strPart = trim(mb_substr($strPart, 0, mb_strlen($strPart) - mb_strlen($strSuffix)));
break;
}
}
$strPart = trim($strPart, " \t\n\r\0\x0B-_|,。;、,;");
if (in_array($strPart, $arrStopWords, true)) {
continue;
}
if (mb_strlen($strPart) <= 2 && !preg_match('/^\d+$/u', $strPart)) {
continue;
}
if (!in_array($strPart, $arrParts, true)) {
$arrParts[] = $strPart;
}
}
return implode(',', $arrParts);
}
protected function normalizeSeoDescription(string $strValue, string $strPage): string
{
$strValue = trim($strValue);
if ($strValue === '') {
return '';
}
$strSiteName = trim((string)($this->DomainModel->d_name ?? ''));
$strSiteKeywords = trim((string)(ConverterMovel::getVal('strSiteKeywords') ?? ''));
$strVideoName = trim((string)(ConverterMovel::getVal('strVideoName') ?? ''));
$strVideoDescription = trim((string)(ConverterMovel::getVal('strVideoDescription') ?? ''));
$strVideoCategoryName = trim((string)(ConverterMovel::getVal('strVideoCategoryName') ?? ''));
if ($strSiteKeywords !== '') {
$strValue = str_replace($strSiteKeywords, '', $strValue);
}
if ($strVideoName !== '') {
$strQuotedVideoName = preg_quote($strVideoName, '/');
// 合并连续重复片名,如 “片名……片名”
$strValue = preg_replace('/(' . $strQuotedVideoName . ')(?:\s*[,。;、,\-]?\s*\1){1,}/u', '$1', $strValue) ?? $strValue;
// 清掉 “片名的热血传奇片名” 这类硬拼句式
$strValue = preg_replace('/' . $strQuotedVideoName . '(的)?(热血传奇|精神图腾|叙事模型|英雄之旅|成长母题)' . $strQuotedVideoName . '/u', $strVideoName, $strValue) ?? $strValue;
}
$arrNoisePhrases = [
'的热血传奇',
'的精神图腾',
'的叙事模型',
'的英雄之旅',
'的成长母题',
];
$strValue = str_replace($arrNoisePhrases, '', $strValue);
$strValue = preg_replace('/([,。;、])\1+/u', '$1', $strValue) ?? $strValue;
$strValue = preg_replace('/\s+/u', ' ', $strValue) ?? $strValue;
$strValue = preg_replace('/\s*([,。;、])/u', '$1', $strValue) ?? $strValue;
$strValue = preg_replace('/([,。;、])(?=[,。;、])/u', '', $strValue) ?? $strValue;
$strValue = trim($strValue, " \t\n\r\0\x0B,。;、,;");
$boolDescriptionLooksWeak = false;
if ($strVideoName !== '') {
$intNameCount = preg_match_all('/' . preg_quote($strVideoName, '/') . '/u', $strValue);
if ($intNameCount >= 2) {
$boolDescriptionLooksWeak = true;
}
}
if ($strVideoDescription !== '' && $strVideoName !== '' && $strVideoDescription === $strVideoName) {
$boolDescriptionLooksWeak = true;
}
if (mb_strlen($strValue) < 20) {
$boolDescriptionLooksWeak = true;
}
if ($boolDescriptionLooksWeak) {
return $this->buildSeoDescriptionFallback($strPage, $strSiteName, $strVideoName, $strVideoCategoryName);
}
return $strValue;
}
protected function buildSeoDescriptionFallback(string $strPage, string $strSiteName, string $strVideoName, string $strVideoCategoryName): string
{
$strSiteName = trim($strSiteName);
$strVideoName = trim($strVideoName);
$strVideoCategoryName = trim($strVideoCategoryName);
if ($strVideoName === '') {
return trim((string)($this->DomainModel->d_index_description ?? $strSiteName));
}
$strCategorySuffix = $strVideoCategoryName !== '' ? $strVideoCategoryName . '内容' : '相关内容';
return match ($strPage) {
'play' => $strSiteName . '提供' . $strVideoName . '播放入口与剧集浏览信息,方便继续查看' . $strCategorySuffix . '与线路安排。',
'detail' => $strSiteName . '整理' . $strVideoName . '的详情信息、演员资料与观看线索,方便继续了解' . $strCategorySuffix . '与播放入口。',
default => $strSiteName . '提供' . $strVideoName . '相关介绍与浏览入口,方便继续查看' . $strCategorySuffix . '。',
};
}
protected function resolveLegacySeoKey(string $strCode, string $strPage): string
{
$arrMap = [
'home' => [
'title' => 'VIDEO@INDEX@INDEX@TITLE',
'keywords' => 'VIDEO@INDEX@INDEX@KEYWORDS',
'description' => 'VIDEO@INDEX@INDEX@DESCRIPTION',
],
'detail' => [
'title' => 'VIDEO@GETVIDEOINFO@TITLE',
'keywords' => 'VIDEO@GETVIDEOINFO@KEYWORDS',
'description' => 'VIDEO@GETVIDEOINFO@DESCRIPTION',
],
'play' => [
'title' => 'VIDEO@GETVIDEOPLAY@TITLE',
'keywords' => 'VIDEO@GETVIDEOPLAY@KEYWORDS',
'description' => 'VIDEO@GETVIDEOPLAY@DESCRIPTION',
],
'search' => [
'title' => 'VIDEO@GETSEARCHVIDEO@TITLE',
'keywords' => 'VIDEO@GETSEARCHVIDEO@KEYWORDS',
'description' => 'VIDEO@GETSEARCHVIDEO@DESCRIPTION',
],
'rank_index' => [
'title' => 'VIDEO@GETVIDEORANKINDEX@TITLE',
'keywords' => 'VIDEO@GETVIDEORANKINDEX@KEYWORDS',
'description' => 'VIDEO@GETVIDEORANKINDEX@DESCRIPTION',
],
'rank_list' => [
'title' => 'VIDEO@GETVIDEORANKLIST@TITLE',
'keywords' => 'VIDEO@GETVIDEORANKLIST@KEYWORDS',
'description' => 'VIDEO@GETVIDEORANKLIST@DESCRIPTION',
],
'category_home' => [
'title' => 'VIDEO@GETSEARCHVIDEO@TITLE',
'keywords' => 'VIDEO@GETSEARCHVIDEO@KEYWORDS',
'description' => 'VIDEO@GETSEARCHVIDEO@DESCRIPTION',
],
'category_index' => [
'title' => 'VIDEO@GETCATEGORYINDEX@TITLE',
'keywords' => 'VIDEO@GETCATEGORYINDEX@KEYWORDS',
'description' => 'VIDEO@GETCATEGORYINDEX@DESCRIPTION',
],
'category_list' => [
'title' => 'VIDEO@GETCATEGORY@TITLE',
'keywords' => 'VIDEO@GETCATEGORY@KEYWORDS',
'description' => 'VIDEO@GETCATEGORY@DESCRIPTION',
],
];
return (string)($arrMap[$strPage][$strCode] ?? '');
} }
public function getTemplate(): string public function getTemplate(): string

View File

@@ -2,6 +2,9 @@
namespace app\services; namespace app\services;
use app\common\helper\SeoCopyFallbackBuilder;
use app\common\helper\SeoCopySchema;
use app\common\helper\SeoCopyStore;
use app\model\CategoryModel; use app\model\CategoryModel;
use app\model\ChapterModel; use app\model\ChapterModel;
use app\model\ConverterMovel; use app\model\ConverterMovel;
@@ -48,14 +51,18 @@ class VideoService
/** /**
* 获取视频分类首页 URl * 获取视频分类首页 URl
* *
* @param string $strParentCategory * @param string|null $strParentCategory
* @return string * @return string
*/ */
public function getVideoCategoryIndexUrl(string $strParentCategory): string public function getVideoCategoryIndexUrl(?string $strParentCategory): string
{ {
$strParentCategory = trim((string)$strParentCategory);
$strTmpCode = $this->SiteContext->TemplatesModel['t_code']; $strTmpCode = $this->SiteContext->TemplatesModel['t_code'];
if ($strTmpCode == 'videoGpt1') { if ($strTmpCode == 'videoGpt1') {
if ($strParentCategory === '') {
return $this->SiteContext->UrlBuilder->categoryHome();
}
return $this->SiteContext->UrlBuilder->categoryParent($strParentCategory); return $this->SiteContext->UrlBuilder->categoryParent($strParentCategory);
} else { } else {
$strKey = 'VIDEO_CATEGORY_INDEX_URL'; $strKey = 'VIDEO_CATEGORY_INDEX_URL';
@@ -184,17 +191,23 @@ class VideoService
/** /**
* 排行榜 列表 url * 排行榜 列表 url
* *
* @param string $strCategory * @param string|null $strCategory
* @param string $strParentCategory * @param string|null $strParentCategory
* @param string $strSortType * @param string|null $strSortType
* @param integer $intPage * @param integer|null $intPage
* @return string * @return string
*/ */
public function getVideoRankUrl(string $strCategory, string $strParentCategory, string $strSortType, int $intPage): string public function getVideoRankUrl(?string $strCategory, ?string $strParentCategory, ?string $strSortType, ?int $intPage): string
{ {
$strCategory = trim((string)$strCategory);
$strParentCategory = trim((string)$strParentCategory);
$strSortType = trim((string)$strSortType);
$strTmpCode = $this->SiteContext->TemplatesModel['t_code']; $strTmpCode = $this->SiteContext->TemplatesModel['t_code'];
if ($strTmpCode == 'videoGpt1') { if ($strTmpCode == 'videoGpt1') {
if ($strSortType === '') {
return $this->SiteContext->UrlBuilder->rankIndex();
}
return $this->SiteContext->UrlBuilder->rankList($strSortType); return $this->SiteContext->UrlBuilder->rankList($strSortType);
} else { } else {
$strKey = 'VIDEO_RANK_LIST_URL'; $strKey = 'VIDEO_RANK_LIST_URL';
@@ -244,7 +257,17 @@ class VideoService
$strTmpCode = $this->SiteContext->TemplatesModel['t_code']; $strTmpCode = $this->SiteContext->TemplatesModel['t_code'];
if ($strTmpCode == 'videoGpt1') { if ($strTmpCode == 'videoGpt1') {
return $this->SiteContext->UrlBuilder->searchResult($strKeyWords); $strUrl = $this->SiteContext->UrlBuilder->searchResult($strKeyWords);
if (is_string($intPage) && strpos($intPage, '{page}') !== false) {
return $strUrl . '&page={page}';
}
$intPage = max(1, (int)$intPage);
if ($intPage <= 1) {
return $strUrl;
}
return $strUrl . '&page=' . $intPage;
} else { } else {
$strKey = 'VIDEO_SEARCH_LIST_URL'; $strKey = 'VIDEO_SEARCH_LIST_URL';
$strUrl = $this->SiteContext->DomainModel->getFomartUrl($strKey, [ $strUrl = $this->SiteContext->DomainModel->getFomartUrl($strKey, [
@@ -791,6 +814,11 @@ class VideoService
throw new HttpException(404, '视频不存在'); throw new HttpException(404, '视频不存在');
} }
return $this->hydrateResolvedVideo($arrVideo, $intVId, $intVForgeId);
}
protected function hydrateResolvedVideo(array $arrVideo, int $intRequestedVId, int|NULL $intVForgeId): array
{
if ($intVForgeId >= 0 && !empty($arrVideo['v_seo_words'])) { if ($intVForgeId >= 0 && !empty($arrVideo['v_seo_words'])) {
$intForgeOffset = max(0, $intVForgeId - 1); $intForgeOffset = max(0, $intVForgeId - 1);
if (!empty($arrVideo['v_seo_words'][$intForgeOffset])) { if (!empty($arrVideo['v_seo_words'][$intForgeOffset])) {
@@ -809,14 +837,23 @@ class VideoService
$strTopLang = '2025'; $strTopLang = '2025';
// 点击数 // 点击数
$arrVideoClicks = $this->VideoClicksModel->getStats((int)($arrVideo['v_id'] ?? $intVId)); $arrVideoClicks = $this->VideoClicksModel->getStats((int)($arrVideo['v_id'] ?? $intRequestedVId));
$strVideoParentCategoryName = trim((string)($arrVideo['v_parent_category'] ?? ''));
$strVideoCategoryName = trim((string)($arrVideo['v_category'] ?? ''));
// 一部分视频数据里父分类与子分类会被写成同一个值,例如“伦理片 / 伦理片”。
// 这里在注入 SEO/TKD 变量前做一次去重,避免标题拼出“伦理片伦理片”。
if ($strVideoParentCategoryName !== '' && $strVideoParentCategoryName === $strVideoCategoryName) {
$strVideoParentCategoryName = '';
}
// 更新值 // 更新值
ConverterMovel::setVal([ ConverterMovel::setVal([
'intPage' => 1, 'intPage' => 1,
'strVideoName' => $arrVideo['v_name'], 'strVideoName' => $arrVideo['v_name'],
'strVideoParentCategoryName' => $arrVideo['v_parent_category'], 'strVideoParentCategoryName' => $strVideoParentCategoryName,
'strVideoCategoryName' => $arrVideo['v_category'], 'strVideoCategoryName' => $strVideoCategoryName,
'strVideoStatus' => $arrVideo['v_isend'], 'strVideoStatus' => $arrVideo['v_isend'],
'strVideoDescription' => $arrVideo['v_description'], 'strVideoDescription' => $arrVideo['v_description'],
'strLangName' => $strTopLang, 'strLangName' => $strTopLang,
@@ -830,6 +867,26 @@ class VideoService
return $arrVideo; return $arrVideo;
} }
public function getVideoByRouteContext(int|string|null $intVId, int|string|null $intVForgeId, string|null $strPinyin = '')
{
$intVId = (int)$intVId;
$intVForgeId = $intVForgeId === null ? -1 : (int)$intVForgeId;
$strPinyin = trim((string)$strPinyin);
if ($intVId > 0) {
return $this->getVideoByVId($intVId, $intVForgeId);
}
if ($strPinyin !== '') {
$arrVideo = $this->VideoModel->getVideoByNameEn($strPinyin);
if (!empty($arrVideo)) {
return $this->hydrateResolvedVideo($arrVideo, (int)($arrVideo['v_id'] ?? 0), $intVForgeId);
}
}
return $this->getVideoByVId($intVId, $intVForgeId);
}
protected function resolveFallbackVideoByRequest(int $requestedVId, bool $requirePlayable = false): ?array protected function resolveFallbackVideoByRequest(int $requestedVId, bool $requirePlayable = false): ?array
{ {
$strHost = (string)($this->SiteContext->DomainModel->d_domain ?? 'default'); $strHost = (string)($this->SiteContext->DomainModel->d_domain ?? 'default');
@@ -1103,6 +1160,82 @@ class VideoService
return SiteStyle::buildDetailSeoAddon($arrTpStyle, $arrVideo); return SiteStyle::buildDetailSeoAddon($arrTpStyle, $arrVideo);
} }
/**
* 统一获取页面补料块
*
* 优先读取 seo_copy 发布/本地文件;
* 缺失时退回 fallback builder保证页面不空。
*/
public function getSeoCopyBlock(string $strScene, array $arrOptions = []): array
{
$strScene = trim(strtolower($strScene));
if ($strScene === '') {
return [];
}
$strHost = trim((string)($this->SiteContext->DomainModel->d_domain ?? ''));
if ($strHost === '') {
return [];
}
$arrPageKeys = $this->buildSeoCopyPageKeys($strScene, $arrOptions);
if ($this->shouldSuppressDefaultOnlySeoCopy($strScene, $strHost, $arrPageKeys)) {
return [];
}
$arrData = !empty($arrPageKeys)
? SeoCopyStore::getPreferredPageData($strHost, $strScene, $arrPageKeys)
: [];
if (!empty($arrData)) {
$arrData = $this->interpolateSeoCopyData($arrData, $this->buildSeoCopyTokens($strScene, $arrOptions));
}
if (empty($arrData)) {
$arrData = SeoCopyFallbackBuilder::build($strScene, $this->buildSeoCopyFacts($strScene, $arrOptions));
}
return $this->normalizeSeoCopyBlock($strScene, $arrData);
}
private function shouldSuppressDefaultOnlySeoCopy(string $strScene, string $strHost, array $arrPageKeys): bool
{
if (!in_array($strScene, ['detail', 'play'], true)) {
return false;
}
$arrSpecificKeys = array_values(array_filter($arrPageKeys, static function ($strPageKey): bool {
$strPageKey = trim((string)$strPageKey);
return $strPageKey !== '' && $strPageKey !== 'default';
}));
if (empty($arrSpecificKeys)) {
return false;
}
foreach (SeoCopyStore::preferredRoots() as $strRoot) {
$strRoot = rtrim(trim((string)$strRoot), '/');
if ($strRoot === '' || !is_dir($strRoot)) {
continue;
}
foreach ($arrSpecificKeys as $strPageKey) {
$strPath = SeoCopyStore::resolvePagePathFromRoot($strRoot, $strHost, $strScene, $strPageKey);
if ($strPath !== '' && is_file($strPath)) {
return false;
}
}
$strDefaultPath = SeoCopyStore::resolvePagePathFromRoot($strRoot, $strHost, $strScene, 'default');
if ($strDefaultPath !== '' && is_file($strDefaultPath)) {
return true;
}
}
return false;
}
/** /**
* BSONDocument / 对象 / array 统一转数组(递归) * BSONDocument / 对象 / array 统一转数组(递归)
*/ */
@@ -1144,4 +1277,435 @@ class VideoService
} }
return $mixVal; return $mixVal;
} }
private function buildSeoCopyPageKeys(string $strScene, array $arrOptions): array
{
$arrKeys = [];
switch ($strScene) {
case 'home':
case 'rank_index':
$arrKeys[] = SeoCopySchema::buildScenePageKey($strScene, 'index');
break;
case 'category_index':
$strParentCategory = trim((string)($arrOptions['parent_category'] ?? request()->route('strParentCategory')));
if ($strParentCategory !== '') {
$arrKeys[] = SeoCopySchema::buildScenePageKey($strScene, [$strParentCategory]);
}
$arrKeys[] = 'index';
break;
case 'category_list':
$strParentCategory = trim((string)($arrOptions['parent_category'] ?? request()->route('strParentCategory')));
$strCategory = trim((string)($arrOptions['category'] ?? request()->route('strCategory')));
$arrKeys[] = SeoCopySchema::buildScenePageKey($strScene, [$strParentCategory, $strCategory]);
$arrKeys[] = 'default';
break;
case 'search':
$strKeyword = trim((string)($arrOptions['keyword'] ?? request()->get('keyword', '')));
if ($strKeyword !== '') {
$arrKeys[] = SeoCopySchema::buildScenePageKey($strScene, [$strKeyword]);
}
$arrKeys[] = 'landing';
break;
case 'rank_list':
$strSortType = trim((string)($arrOptions['sort_type'] ?? request()->route('strSortType')));
$strParentCategory = trim((string)($arrOptions['parent_category'] ?? request()->route('strParentCategory')));
$strCategory = trim((string)($arrOptions['category'] ?? request()->route('strCategory')));
if ($strSortType !== '') {
$arrKeys[] = SeoCopySchema::buildScenePageKey($strScene, [$strSortType, $strParentCategory, $strCategory]);
$arrKeys[] = SeoCopySchema::buildScenePageKey($strScene, [$strSortType]);
}
$arrKeys[] = 'default';
break;
case 'detail':
$arrVideo = $this->toArraySafe($arrOptions['video'] ?? []);
$intVideoId = (int)($arrVideo['v_id'] ?? ($arrOptions['video_id'] ?? 0));
if ($intVideoId > 0) {
$arrKeys[] = SeoCopySchema::buildScenePageKey($strScene, [$intVideoId]);
}
$arrKeys[] = 'default';
break;
case 'forge':
$arrVideo = $this->toArraySafe($arrOptions['video'] ?? []);
$intVideoId = (int)($arrVideo['v_id'] ?? ($arrOptions['video_id'] ?? 0));
$intForgeId = (int)($arrOptions['forge_id'] ?? request()->route('intVForgeId'));
if ($intVideoId > 0 && $intForgeId > 0) {
$arrKeys[] = SeoCopySchema::buildScenePageKey($strScene, [$intVideoId, $intForgeId]);
}
$arrKeys[] = 'default';
break;
case 'play':
$arrVideo = $this->toArraySafe($arrOptions['video'] ?? []);
$intVideoId = (int)($arrVideo['v_id'] ?? ($arrOptions['video_id'] ?? 0));
$strPlayType = trim((string)($arrOptions['play_type'] ?? request()->route('strPlayType')));
$intPlayIndex = (int)($arrOptions['play_index'] ?? request()->route('intPlayIndex'));
if ($intVideoId > 0) {
$arrKeys[] = SeoCopySchema::buildScenePageKey($strScene, [$intVideoId, $strPlayType, $intPlayIndex]);
}
$arrKeys[] = 'default';
break;
}
return array_values(array_unique(array_filter($arrKeys, static function ($strKey): bool {
return trim((string)$strKey) !== '';
})));
}
private function buildSeoCopyFacts(string $strScene, array $arrOptions): array
{
$arrVideo = $this->toArraySafe($arrOptions['video'] ?? []);
$arrPagerData = $this->toArraySafe($arrOptions['pager_data'] ?? []);
$arrPageStats = [
'page' => (int)($arrPagerData['page'] ?? ($arrOptions['page'] ?? 1)),
'pages' => (int)($arrPagerData['pages'] ?? ($arrOptions['pages'] ?? 1)),
'total' => (int)($arrPagerData['total'] ?? ($arrOptions['total'] ?? 0)),
'visible' => (int)($arrOptions['visible'] ?? 0),
];
return match ($strScene) {
'home' => [
'site_name' => (string)($this->SiteContext->DomainModel->d_name ?? ''),
'content_scopes' => $this->extractHomeContentScopes(),
'navigation_paths' => ['首页 -> 分类频道 -> 详情页', '首页 -> 搜索页 -> 详情页 -> 播放页'],
],
'category_index' => [
'parent_category_name' => $this->resolveCategoryLabel((string)($arrOptions['parent_category'] ?? request()->route('strParentCategory'))),
'sub_category_examples' => $this->extractCategoryExamples((string)($arrOptions['parent_category'] ?? request()->route('strParentCategory'))),
'search_path' => '如果题材仍然不够精确,可继续走站内搜索。',
],
'category_list' => [
'parent_category_name' => $this->resolveCategoryLabel((string)($arrOptions['parent_category'] ?? request()->route('strParentCategory'))),
'category_name' => $this->resolveCategoryLabel((string)($arrOptions['category'] ?? request()->route('strCategory'))),
'page_stats' => $arrPageStats,
],
'search' => [
'keyword' => (string)($arrOptions['keyword'] ?? request()->get('keyword', '')),
'result_stats' => $arrPageStats,
],
'rank_index' => [
'rank_periods' => $this->extractRankPeriods(),
'rank_path' => '榜单首页 -> 榜单列表 -> 详情页 -> 播放页',
],
'rank_list' => [
'rank_period_name' => $this->resolveRankSortLabel((string)($arrOptions['sort_type'] ?? request()->route('strSortType'))),
'rank_scope_name' => $this->resolveRankScopeName(
(string)($arrOptions['parent_category'] ?? request()->route('strParentCategory')),
(string)($arrOptions['category'] ?? request()->route('strCategory'))
),
'page_stats' => $arrPageStats,
],
'detail' => [
'video_name' => (string)($arrVideo['v_name'] ?? ''),
'category_name' => (string)($arrVideo['v_category'] ?? ''),
'year' => $this->normalizeSeoCopyYear($arrVideo),
'core_plot_or_positioning' => (string)($arrVideo['v_description'] ?? ''),
],
'play' => [
'video_name' => (string)($arrVideo['v_name'] ?? ''),
'play_type' => $this->getConverterPlayLineVal((string)($arrOptions['play_type'] ?? request()->route('strPlayType'))),
'episode_label_or_index' => (string)($arrOptions['play_index'] ?? request()->route('intPlayIndex') ?? 1),
'play_index' => (int)($arrOptions['play_index'] ?? request()->route('intPlayIndex') ?? 1),
'core_plot_or_positioning' => (string)($arrVideo['v_description'] ?? ''),
],
default => [],
};
}
private function normalizeSeoCopyBlock(string $strScene, array $arrData): array
{
$arrTemplate = SeoCopySchema::getSceneTemplate($strScene);
$arrMerged = array_merge($arrTemplate, $arrData);
$arrGuideCards = [];
foreach ((array)($arrMerged['guide_cards'] ?? []) as $arrCard) {
if (!is_array($arrCard)) {
continue;
}
$strTitle = trim((string)($arrCard['title'] ?? ''));
$strText = trim((string)($arrCard['text'] ?? ''));
$strHref = trim((string)($arrCard['href'] ?? ''));
if ($strTitle === '' && $strText === '') {
continue;
}
$arrItem = [
'title' => $strTitle,
'text' => $strText,
];
if ($strHref !== '') {
$arrItem['href'] = $strHref;
}
$arrGuideCards[] = $arrItem;
}
$arrMerged['guide_cards'] = $arrGuideCards;
$arrMerged['_scene'] = $strScene;
return $arrMerged;
}
private function interpolateSeoCopyData(array $arrData, array $arrTokens): array
{
foreach ($arrData as $strKey => $mValue) {
$arrData[$strKey] = $this->interpolateSeoCopyValue($mValue, $arrTokens);
}
return $arrData;
}
private function interpolateSeoCopyValue($mValue, array $arrTokens)
{
if (is_array($mValue)) {
foreach ($mValue as $strKey => $mChildValue) {
$mValue[$strKey] = $this->interpolateSeoCopyValue($mChildValue, $arrTokens);
}
return $mValue;
}
if (!is_string($mValue)) {
return $mValue;
}
return SeoCopyStore::interpolateTemplate($mValue, $arrTokens);
}
private function buildSeoCopyTokens(string $strScene, array $arrOptions): array
{
$arrVideo = $this->toArraySafe($arrOptions['video'] ?? []);
$strPlayType = trim((string)($arrOptions['play_type'] ?? request()->route('strPlayType')));
$intPlayIndex = (int)($arrOptions['play_index'] ?? request()->route('intPlayIndex') ?? 1);
$arrFacts = $this->buildSeoCopyFacts($strScene, $arrOptions);
$strSiteName = trim((string)($this->SiteContext->DomainModel->d_name ?? ''));
$strVideoName = trim((string)($arrVideo['v_name'] ?? ''));
$strYear = $this->normalizeSeoCopyYear($arrVideo);
$strAreaName = $this->implodeSeoCopyScalar($arrVideo['v_area'] ?? '');
$strLangName = $this->implodeSeoCopyScalar($arrVideo['v_lang'] ?? '');
$strDirectorNames = $this->implodeSeoCopyScalar($arrVideo['v_director'] ?? '', 3);
$strActorNames = $this->implodeSeoCopyScalar($arrVideo['v_actor'] ?? '', 4);
$strRemarks = trim((string)($arrVideo['v_remarks'] ?? ''));
$strVideoAlias = $this->implodeSeoCopyScalar($arrVideo['v_alias'] ?? ($arrVideo['v_aliases'] ?? ''), 3);
$strPlayLine = $this->getConverterPlayLineVal($strPlayType);
$strEpisodeName = $this->buildSeoCopyEpisodeLabel($arrVideo, $intPlayIndex);
$arrTokens = [
'site_name' => $strSiteName,
'video_name' => $strVideoName,
'video_alias' => $strVideoAlias,
'year' => $strYear,
'area_name' => $strAreaName,
'lang_name' => $strLangName,
'director_names' => $strDirectorNames,
'actor_names' => $strActorNames,
'remarks' => $strRemarks,
'play_line' => $strPlayLine,
'play_type' => $strPlayType,
'episode_name' => $strEpisodeName,
'episode_label_or_index' => $strEpisodeName,
'category_name' => trim((string)($arrVideo['v_category'] ?? '')),
'scene' => $strScene,
];
$arrKnownFactKeys = [
'keyword',
'parent_category_name',
'category_name',
'rank_period_name',
'rank_scope_name',
'search_path',
'rank_path',
];
foreach ($arrKnownFactKeys as $strFactKey) {
if (!isset($arrFacts[$strFactKey])) {
continue;
}
$arrTokens[$strFactKey] = $this->implodeSeoCopyScalar($arrFacts[$strFactKey]);
}
return array_map(static function ($mValue): string {
return trim((string)$mValue);
}, $arrTokens);
}
private function normalizeSeoCopyYear(array $arrVideo): string
{
$strYear = trim((string)($arrVideo['v_year'] ?? ''));
if ($strYear !== '') {
if (preg_match('/\b(\d{4})\b/u', $strYear, $arrMatches) === 1) {
return $arrMatches[1];
}
return $strYear;
}
$strPublishDate = trim((string)($arrVideo['v_publish_date'] ?? ''));
if ($strPublishDate !== '' && preg_match('/\b(\d{4})\b/u', $strPublishDate, $arrMatches) === 1) {
return $arrMatches[1];
}
return '';
}
private function implodeSeoCopyScalar($mValue, int $intLimit = 0): string
{
if (is_string($mValue)) {
return trim($mValue);
}
if (!is_array($mValue)) {
return trim((string)$mValue);
}
$arrItems = array_values(array_filter(array_map(static function ($mItem): string {
return trim((string)$mItem);
}, $mValue), static function (string $strVal): bool {
return $strVal !== '';
}));
if ($intLimit > 0) {
$arrItems = array_slice($arrItems, 0, $intLimit);
}
return implode('、', $arrItems);
}
private function buildSeoCopyEpisodeLabel(array $arrVideo, int $intPlayIndex): string
{
$intPlayIndex = max(1, $intPlayIndex);
$arrPlayFrom = (array)($arrVideo['v_play_from'] ?? []);
$arrPlayUrl = (array)($arrVideo['v_play_url'] ?? []);
$strPlayType = trim((string)(request()->route('strPlayType') ?? ''));
if ($strPlayType !== '' && isset($arrPlayFrom[$strPlayType]) && !empty($arrPlayUrl[$strPlayType][$intPlayIndex - 1]['p_title'])) {
return trim((string)$arrPlayUrl[$strPlayType][$intPlayIndex - 1]['p_title']);
}
return '第' . $intPlayIndex . '集';
}
private function extractHomeContentScopes(): array
{
$arrCategories = (array)($this->SiteContext->TpStyle['template_cfg']['pages']['home']['categories'] ?? []);
$arrOut = [];
foreach ($arrCategories as $arrCategory) {
$strTitle = $this->resolveDisplayText($arrCategory['title_text'] ?? ($arrCategory['name'] ?? $arrCategory['key'] ?? ''));
if ($strTitle !== '') {
$arrOut[] = $strTitle;
}
}
return !empty($arrOut) ? array_slice(array_values(array_unique($arrOut)), 0, 5) : ['电影', '电视剧', '综艺'];
}
private function extractCategoryExamples(string $strParentCategory): array
{
$arrMap = (array)($this->SiteContext->TpStyle['template_cfg']['pages']['category']['cat1_map'][$strParentCategory]['subcat_slots'] ?? []);
$arrOut = [];
foreach ($arrMap as $arrItem) {
$strTitle = $this->resolveDisplayText($arrItem['title_text'] ?? ($arrItem['name'] ?? $arrItem['key'] ?? ''));
if ($strTitle !== '') {
$arrOut[] = $strTitle;
}
}
return !empty($arrOut) ? array_slice(array_values(array_unique($arrOut)), 0, 4) : [];
}
private function extractRankPeriods(): array
{
$arrSlots = (array)($this->SiteContext->TpStyle['template_cfg']['pages']['rank_home']['slots'] ?? []);
$arrOut = [];
foreach ($arrSlots as $arrSlot) {
$strSortType = trim((string)($arrSlot['sort_type'] ?? ''));
if ($strSortType !== '') {
$arrOut[] = $this->resolveRankSortLabel($strSortType);
}
}
return !empty($arrOut) ? array_slice(array_values(array_unique($arrOut)), 0, 4) : ['日榜', '周榜', '月榜'];
}
private function resolveCategoryLabel(string $strCategory): string
{
$strCategory = trim($strCategory);
if ($strCategory === '') {
return '';
}
$arrFlatMap = (array)VideoCategoryModel::$arrCategory;
if (isset($arrFlatMap[$strCategory])) {
return trim((string)$arrFlatMap[$strCategory]);
}
foreach ((array)VideoCategoryModel::getCustomCategory('ONE') as $arrParentCategory) {
if ((string)($arrParentCategory['v_category_en'] ?? '') === $strCategory) {
return trim((string)($arrParentCategory['v_category'] ?? $strCategory));
}
foreach ((array)($arrParentCategory['children'] ?? []) as $arrChildCategory) {
if ((string)($arrChildCategory['v_category_en'] ?? '') === $strCategory) {
return trim((string)($arrChildCategory['v_category'] ?? $strCategory));
}
}
}
return $strCategory;
}
private function resolveRankSortLabel(string $strSortType): string
{
return match (trim(strtolower($strSortType))) {
'daily' => '日榜',
'weekly' => '周榜',
'monthly' => '月榜',
default => $strSortType !== '' ? $strSortType : '榜单',
};
}
private function resolveRankScopeName(string $strParentCategory, string $strCategory): string
{
$strParentLabel = $this->resolveCategoryLabel($strParentCategory);
$strCategoryLabel = $this->resolveCategoryLabel($strCategory);
if ($strParentLabel !== '' && $strCategoryLabel !== '' && $strCategoryLabel !== 'all') {
return $strParentLabel . ' / ' . $strCategoryLabel;
}
if ($strParentLabel !== '') {
return $strParentLabel;
}
return '当前范围';
}
private function resolveDisplayText($mValue): string
{
if (is_array($mValue)) {
foreach (['primary', 'seo', 'secondary', 'name', 'title', 'text'] as $strKey) {
$strVal = trim((string)($mValue[$strKey] ?? ''));
if ($strVal !== '') {
return $strVal;
}
}
foreach ($mValue as $mItem) {
$strVal = $this->resolveDisplayText($mItem);
if ($strVal !== '') {
return $strVal;
}
}
return '';
}
return trim((string)$mValue);
}
} }

View File

@@ -0,0 +1,8 @@
{
"detail_body": "本页围绕《色降2之血玫瑰》整理剧情简介、题材信息与播放线索方便先确认内容方向。",
"detail_body_lead": "本页围绕《色降2之血玫瑰》整理剧情简介、题材信息与播放线索方便先确认内容方向。",
"detail_play_link_lead": "如果准备继续观看,可直接进入",
"detail_body_tail": "如果想换个切口继续了解这部内容,可以继续浏览延伸词入口或返回分类页。",
"detail_faq": "常见问题:如果你已经确认要看这部内容,可以先看剧情简介,再进入播放页;如果还想补充检索角度,也可以先回到分类页继续筛选。",
"main_description": "《色降2之血玫瑰》当前归类到伦理片参考年份为1992。泰国一名邪恶女巫绑架并洗脑美丽女性强迫她们加入她的邪教。一名男子和他的女友为了寻找失踪的妹妹与一名牧师和一名当地警察联手对抗邪教。"
}

View File

@@ -0,0 +1,6 @@
{
"play_intro": "当前播放页提供《色降2之血玫瑰》的douban线路入口当前可继续浏览的集数位置为正片。",
"play_meta_note": "如果当前线路不合适,可以先回详情页确认内容归属,再切换其他播放入口继续浏览。",
"play_body_lead": "播放器下方正文主要帮助补充《色降2之血玫瑰》的剧情、题材和返回路径信息。",
"play_body_next": "如果想继续确认内容信息,可以回详情页;如果想换范围继续看,也可以返回题材页。"
}

View File

@@ -11,7 +11,7 @@ class Video extends TagLib
'listexp' => ['attr' => 'count,v_category_en,v_parent_category_en,sort_type,v_status,v_sex,diff_key,cache_life,d_key,d_val,export_name', 'close' => 0], 'listexp' => ['attr' => 'count,v_category_en,v_parent_category_en,sort_type,v_status,v_sex,diff_key,cache_life,d_key,d_val,export_name', 'close' => 0],
'pager' => ['attr' => 'page,limit,v_category_en,v_parent_category_en,sort_type,v_status,v_sex,key,diff_key,cache_life,d_key,d_val,p_val,func,export_name', 'close' => 1], 'pager' => ['attr' => 'page,limit,v_category_en,v_parent_category_en,sort_type,v_status,v_sex,key,diff_key,cache_life,d_key,d_val,p_val,func,export_name', 'close' => 1],
'pagerexp' => ['attr' => 'page,limit,v_class,v_category_en,v_parent_category_en,sort_type,v_status,v_sex,key,diff_key,cache_life,d_key,d_val,p_val,func,export_name', 'close' => 0], 'pagerexp' => ['attr' => 'page,limit,v_class,v_category_en,v_parent_category_en,sort_type,v_status,v_sex,key,diff_key,cache_life,d_key,d_val,p_val,func,export_name', 'close' => 0],
'info' => ['attr' => 'v_id,v_forge_id,v_key', 'close' => 0], 'info' => ['attr' => 'v_id,v_forge_id,v_key,v_pinyin', 'close' => 0],
'sort' => ['attr' => 'd_key,d_val'], 'sort' => ['attr' => 'd_key,d_val'],
'ranksort' => ['attr' => 'd_key,d_val'], 'ranksort' => ['attr' => 'd_key,d_val'],
'status' => ['attr' => 'd_key,d_val'], 'status' => ['attr' => 'd_key,d_val'],
@@ -29,6 +29,7 @@ class Video extends TagLib
'imgalt' => ['attr' => 'video,tp_style,slot,item_type,export_name', 'close' => 0], 'imgalt' => ['attr' => 'video,tp_style,slot,item_type,export_name', 'close' => 0],
'seoaddon' => ['attr' => 'video,tp_style,export_name', 'close' => 0], 'seoaddon' => ['attr' => 'video,tp_style,export_name', 'close' => 0],
'seocopy' => ['attr' => 'scene,video,parent_category,category,keyword,sort_type,page,total,pages,visible,play_type,play_index,export_name', 'close' => 0],
]; ];
@@ -132,6 +133,72 @@ EOD;
return $strParse; return $strParse;
} }
/**
* 页面补料块(优先 seo_copy缺失走 fallback
*
* 用法:
* {video:seocopy scene="home" export_name="seoCopy" /}
* {video:seocopy scene="detail" video="$arrVideo" export_name="seoCopy" /}
*/
public function tagSeoCopy($tag)
{
$strScene = $this->customBuildVar($tag['scene'] ?? 'home');
$strVideo = $this->customBuildVar($tag['video'] ?? '[]');
$strParentCategory = $this->customBuildVar($tag['parent_category'] ?? '');
$strCategory = $this->customBuildVar($tag['category'] ?? '');
$strKeyword = $this->customBuildVar($tag['keyword'] ?? '');
$strSortType = $this->customBuildVar($tag['sort_type'] ?? '');
$intPage = $this->customBuildVar($tag['page'] ?? 1);
$intTotal = $this->customBuildVar($tag['total'] ?? 0);
$intPages = $this->customBuildVar($tag['pages'] ?? 1);
$intVisible = $this->customBuildVar($tag['visible'] ?? 0);
$strPlayType = $this->customBuildVar($tag['play_type'] ?? '');
$intPlayIndex = $this->customBuildVar($tag['play_index'] ?? 1);
$export_name = $tag['export_name'] ?? '';
$strDataName = randomString(8);
if (!empty($export_name) && !preg_match('/^[A-Za-z_]\w*$/', $export_name)) {
$export_name = '';
}
$strParse = <<<EOD
<?php
\${$strDataName} = app(\\app\\services\\VideoService::class)->getSeoCopyBlock({$strScene}, [
'video' => {$strVideo},
'parent_category' => {$strParentCategory},
'category' => {$strCategory},
'keyword' => {$strKeyword},
'sort_type' => {$strSortType},
'page' => {$intPage},
'total' => {$intTotal},
'pages' => {$intPages},
'visible' => {$intVisible},
'play_type' => {$strPlayType},
'play_index' => {$intPlayIndex},
'pager_data' => [
'page' => {$intPage},
'total' => {$intTotal},
'pages' => {$intPages},
],
]);
EOD;
if (empty($export_name)) {
$strParse .= <<<EOD
echo json_encode(\${$strDataName}, JSON_UNESCAPED_UNICODE);
?>
EOD;
} else {
$strParse .= <<<EOD
\${$export_name} = \${$strDataName};
?>
EOD;
}
return $strParse;
}
/** /**
* tagRankListExp * tagRankListExp
@@ -439,16 +506,20 @@ EOT;
$v_key = $tag['v_key'] ?? 'arrVideo'; $v_key = $tag['v_key'] ?? 'arrVideo';
$v_pinyin = $tag['v_pinyin'] ?? '';
$v_id = $this->customBuildVar($v_id); $v_id = $this->customBuildVar($v_id);
$v_fid = $this->customBuildVar($v_fid); $v_fid = $this->customBuildVar($v_fid);
$v_pinyin = $this->customBuildVar($v_pinyin);
$arrDataName = randomString(5); $arrDataName = randomString(5);
$strParse = <<<EOT $strParse = <<<EOT
<?php <?php
\${$arrDataName} = app(\\app\\services\\VideoService::class)->getVideoByVId( \${$arrDataName} = app(\\app\\services\\VideoService::class)->getVideoByRouteContext(
{$v_id}, {$v_id},
{$v_fid} {$v_fid},
{$v_pinyin}
); );
\${$v_key} = \${$arrDataName} ?? []; \${$v_key} = \${$arrDataName} ?? [];

View File

@@ -9,12 +9,36 @@
* ===================================================== * =====================================================
*/ */
function initDPlayer(strPlayUrl) { function ensurePrefix(root) {
if (!root || root.dataset.prefix) {
return;
}
var cls = root.className || '';
var m = cls.match(/([a-z0-9]{4,8})-player/);
if (m) {
root.dataset.prefix = m[1];
}
}
function resolveContainer(root) {
if (root && root.querySelector) {
return root.querySelector('.dplayer, [id="dplayer"]');
}
return document.getElementById('dplayer') || document.querySelector('.dplayer');
}
function initDPlayer(root, strPlayUrl) {
var DomDPlayer = resolveContainer(root);
if (!DomDPlayer) {
showError(root, '播放器容器不存在');
return null;
}
let DomDPlayer = document.getElementById('dplayer')
// ---- 创建 DPlayer 实例 ---- // ---- 创建 DPlayer 实例 ----
try { try {
videoPlayer = new DPlayer({ var videoPlayer = new DPlayer({
container: DomDPlayer, container: DomDPlayer,
autoplay: true, autoplay: true,
screenshot: false, screenshot: false,
@@ -37,13 +61,22 @@
showError(root, '播放失败,请尝试切换线路'); showError(root, '播放失败,请尝试切换线路');
}); });
return videoPlayer;
} catch (e) { } catch (e) {
console.log(e) console.log(e)
showError(root, '播放器初始化失败'); showError(root, '播放器初始化失败');
return null;
} }
} }
function showError(root, msg) { function showError(root, msg) {
if (!root || !root.querySelector) {
console.warn(msg);
return;
}
ensurePrefix(root);
var err = root.querySelector('.' + root.dataset.prefix + '-player-status'); var err = root.querySelector('.' + root.dataset.prefix + '-player-status');
if (!err) { if (!err) {
err = document.createElement('div'); err = document.createElement('div');
@@ -58,16 +91,7 @@
if (!players.length) return; if (!players.length) return;
players.forEach(function (root) { players.forEach(function (root) {
// dom_prefix 注入(防串) ensurePrefix(root);
if (!root.dataset.prefix) {
var cls = root.className || '';
var m = cls.match(/([a-z0-9]{4,8})-player/);
if (m) {
root.dataset.prefix = m[1];
}
}
console.log(root)
initDPlayer(root);
}); });
} }
@@ -100,6 +124,10 @@
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
try { try {
if (typeof strVideoId == "undefined") return; if (typeof strVideoId == "undefined") return;
var root = document.querySelector('[data-engine="dplayer"]');
if (!root) return;
ensurePrefix(root);
console.log(strPlayType) console.log(strPlayType)
console.log(boolIsPlayPage) console.log(boolIsPlayPage)
@@ -118,7 +146,7 @@
// checkLine(defaultLine); // checkLine(defaultLine);
} }
console.log('strPlayUrlstrPlayUrl') console.log('strPlayUrlstrPlayUrl')
initDPlayer(strPlayUrl); initDPlayer(root, strPlayUrl);
} else if (strPlayType == "default" && !boolIsPlayPage) { } else if (strPlayType == "default" && !boolIsPlayPage) {
@@ -126,13 +154,12 @@
strPlayUrl = getFirstPlayUrl(defaultLine); strPlayUrl = getFirstPlayUrl(defaultLine);
// checkLine(defaultLine); // checkLine(defaultLine);
initDPlayer(strPlayUrl); initDPlayer(root, strPlayUrl);
} }
} catch (error) { } catch (error) {
console.log(error);
} }
}) })
})(); })();

File diff suppressed because it is too large Load Diff