* 覆盖默认值。主站是多域名轮换的(防封),配置化后换域名只改注入、不改本文件。
* 没有注入时回退到内置默认域名 GATEWAY_DEFAULT。
*
* ⚠ 安全红线:这里**只能是对外网关的域名**。严禁写后端源站 IP、内网地址或
* 直连端口(:8080 之类)—— 一旦出现在前端,任何人都能照着直连、绕过
* CDN / 防盗链 / WAF,源站就此裸奔。配置注入侧同样必须守这条。
* ================================================================== */
var CFG = (typeof window !== 'undefined' && window.__SHELL_CFG__) ? window.__SHELL_CFG__ : {};
var GATEWAY_DEFAULT = "https://www.021888002.xyz"; // 兜底:一个对外域名,非源站 IP
var API_BASE = String(CFG.gateway || CFG.api_base || GATEWAY_DEFAULT).replace(/\/+$/, '');
var ON_PAGES = /(^|\.)pages\.dev$/i.test(location.hostname);
// 取值:显式配了网关就用它;否则同源(主站自托,随域名轮换走),仅 pages.dev 跨域时才用绝对域名。
var API = (CFG.gateway || CFG.api_base) ? API_BASE : (ON_PAGES ? API_BASE : '');
/* ------------------------------------------------------------------
* 紧凑参数。
*
* entry.html:210 的导航公式是 shell_url + '?' + rawParams + '='
* 结尾那个 '=' 是它加的,必须在这里剥掉:AgentLinkService::pickShortQuery()
* 按 & 拆段后会跳过任何含 '=' 的段(那是键值对),不剥就一定判成非法链接。
* ------------------------------------------------------------------ */
function readQ(){
var s = (location.search || '').replace(/^\?/, '');
if(!s) return '';
// 只取第一段(后面可能被别的东西追加了 &utm_xxx=)
var seg = s.split('&')[0];
return seg.replace(/=+$/, '');
}
var Q = readQ();
var grid = document.getElementById('grid');
var deadEl = document.getElementById('dead');
var LIMIT = 40;
var page = 0, loading = false, done = false;
var curCate = '', curSort = 'hot', mode = 'feed';
var SEED = Math.floor(Math.random()*2147483000)+1;
var TOTAL = 0;
var SITE = ''; // 主站基址,bootstrap 回的 site_base
var BKEY = 'ds_seen_' + Q; // 看过的(按设备,localStorage)
function dead(){ deadEl.classList.add('show'); }
function esc(s){ return (s==null?'':String(s)).replace(/[&<>"]/g,function(m){
return {'&':'&','<':'<','>':'>','"':'"'}[m]; }); }
function fmtViews(n){ n=parseInt(n,10)||0;
if(n>=10000) return (n/10000).toFixed(1).replace(/\.0$/,'')+'万';
return n.toLocaleString('en-US'); }
/* 内置 Base64 SVG 占位图:封面加载失败/缺省时的**最终兜底**。
* 用 data URI 而不是网络图(原来的 /public/moren.jpg)的原因:
* · 壳页托在 pages.dev 外站时,任何指回主站的封面地址都可能因主站被墙/换域名
* 而拿不到;接口回的第三方封面(doubanio 之类)更是随时防盗链 403。
* · data URI 内联在文件里,零请求、永不 403、离线也在,是唯一稳的兜底。
* 挂到 window.DS_PH 上,好让每个
的内联 onerror(运行在全局作用域)够得着,
* 又不必把这串 ~800 字节重复塞进每张卡片。 */
var DS_PH = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MDAiIGhlaWdodD0iMzAwIiB2aWV3Qm94PSIwIDAgNDAwIDMwMCI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJnIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMWUyOTNiIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMGIxNDI0Ii8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHJlY3Qgd2lkdGg9IjQwMCIgaGVpZ2h0PSIzMDAiIGZpbGw9InVybCgjZykiLz48Y2lyY2xlIGN4PSIyMDAiIGN5PSIxMzIiIHI9IjMzIiBmaWxsPSJub25lIiBzdHJva2U9IiMzMzQxNTUiIHN0cm9rZS13aWR0aD0iMyIvPjxwYXRoIGQ9Ik0xOTEgMTE2bDI3IDE2LTI3IDE2eiIgZmlsbD0iIzQ3NTU2OSIvPjx0ZXh0IHg9IjIwMCIgeT0iMjA1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iLWFwcGxlLXN5c3RlbSxQaW5nRmFuZyBTQyxzYW5zLXNlcmlmIiBmb250LXNpemU9IjE1IiBmaWxsPSIjNDc1NTY5Ij7lsIHpnaLliqDovb3kuK08L3RleHQ+PC9zdmc+';
if (typeof window !== 'undefined') { window.DS_PH = DS_PH; }
function fallbackCover(){ return DS_PH; }
function cardHTML(it,i){
var photo = it.photo && it.photo!=='1' ? it.photo : fallbackCover();
var title = esc(it.name||'未命名视频');
var cate = it.cate ? esc(it.cate) : '';
var price = (it.money!=null && it.money!=='') ? it.money : it.sj;
var href = it.dwz || '#';
var payload = esc(JSON.stringify({id:it.id,zykey:it.zykey,name:it.name,photo:it.photo,
cate:it.cate,cs:it.cs,money:price,sj:it.sj,dwz:href}));
return ''
+''
+'
+')
'
+(cate?'
'+cate+'':'')
+'
'
+'
'
+'';
}
function setState(html){
grid.querySelectorAll('.state,.spinner').forEach(function(e){e.remove();});
if(html) grid.insertAdjacentHTML('beforeend', html);
}
function showSpinner(){ setState('
'); }
/* 走原生 pathinfo 而不是 /shell/list 那种短路径:短路径依赖 nginx 重写,
* 原生地址少一层依赖。注意翻页的方法名是 lists 不是 list ——
* list 是 PHP 保留字,控制器里只能叫 lists。写成 list 会 500(方法不存在),
* 而且因为 app_debug 关着,线上只看得到一句「页面错误!请稍后再试~」。 */
var EP = { boot:'bootstrap', page:'lists' };
function apiURL(path, params){
var qs = ['q=' + encodeURIComponent(Q)];
for(var k in params){
if(Object.prototype.hasOwnProperty.call(params,k) && params[k] !== '' && params[k] != null){
qs.push(k + '=' + encodeURIComponent(params[k]));
}
}
return API + '/index.php/index/shell/' + path + '?' + qs.join('&');
}
function ask(path, params){
return fetch(apiURL(path, params), {headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){ return r.json(); })
.then(function(res){
if(!res || res.code !== 1) throw new Error(res && res.msg ? res.msg : 'bad response');
return res.data;
});
}
/* ===== 分类 ===== */
function renderCats(cats){
var box = document.getElementById('tags');
var frag = '';
(cats||[]).forEach(function(c){
frag += '';
});
box.innerHTML = frag;
var n = (cats||[]).length;
document.getElementById('catCount').textContent = n ? ('共 '+n+' 个分类') : '';
}
/* ===== 价格条 ===== */
function renderPlans(p){
if(!p) return;
document.getElementById('prTime').textContent = p.time_price;
document.getElementById('prDay').textContent = p.day_price;
document.getElementById('labTime').textContent = '包时' + (p.time_hours||0) + '小时';
document.getElementById('pricebar').classList.remove('hide');
}
/* 单片价随片子走,取第一屏里最低的那个当“起”价 */
function renderSinglePrice(list){
var min = null;
(list||[]).forEach(function(it){
var v = parseFloat(it.money!=null && it.money!=='' ? it.money : it.sj);
if(!isNaN(v) && v > 0 && (min===null || v < min)) min = v;
});
if(min !== null) document.getElementById('prSingle').textContent = min;
}
function updHead(){
var h2 = document.getElementById('feedTitle');
if(mode==='mine'){ h2.innerHTML = '看过的'+readSeen().length+' 支'; }
else { h2.innerHTML = '最多人看'+TOTAL+' 支视频'; }
}
function appendList(list){
if(!list || !list.length){
done = true;
setState(page===0 ? '该分类下暂无视频
'
: '— 已经到底啦 —
');
return;
}
page++;
var frag = '';
list.forEach(function(it,idx){ frag += cardHTML(it, idx); });
grid.insertAdjacentHTML('beforeend', frag);
if(list.length < LIMIT){ done = true; setState('— 已经到底啦 —
'); }
}
function loadMore(){
if(loading || done) return;
loading = true; showSpinner();
ask(EP.page, {page:page+1, cate:curCate, sort:curSort, seed:SEED})
.then(function(d){
setState('');
TOTAL = d.count;
appendList(d.list);
if(page===1) renderSinglePrice(d.list);
updHead();
loading = false;
})
.catch(function(){
setState('加载失败,点此重试
');
var s = grid.querySelector('.state');
if(s) s.onclick = function(){ setState(''); loading=false; loadMore(); };
loading = false;
});
}
function resetFeed(){ page=0; loading=false; done=false; grid.innerHTML=''; loadMore(); }
/* ===== 分类切换 ===== */
document.getElementById('tags').addEventListener('click', function(e){
var b = e.target.closest('.tag'); if(!b) return;
document.querySelectorAll('.tag').forEach(function(x){x.classList.remove('active');});
b.classList.add('active');
curCate = b.getAttribute('data-cate') || '';
document.getElementById('q').value = '';
mode = 'feed';
document.querySelectorAll('.nav').forEach(function(x){
x.classList.toggle('active', x.getAttribute('data-nav')==='hot');
});
window.scrollTo({top:0,behavior:'smooth'});
resetFeed();
});
document.getElementById('toggleMatrix').addEventListener('click', function(){
document.getElementById('tags').classList.toggle('collapsed');
});
/* ===== 搜索:只过滤已加载的卡片,和主站落地页同样口径 ===== */
function doSearch(){
var q = document.getElementById('q').value.trim();
var cnt = document.getElementById('count');
if(!q){ grid.querySelectorAll('.card').forEach(function(c){c.style.display='';}); updHead(); return; }
var n = 0;
grid.querySelectorAll('.card').forEach(function(c){
var t = c.querySelector('.title').textContent;
var hit = t.indexOf(q) >= 0;
c.style.display = hit ? '' : 'none';
if(hit) n++;
});
if(cnt) cnt.textContent = n + ' 个结果';
if(n===0) toast('当前已加载列表无匹配,可下拉加载更多');
}
document.getElementById('doSearch').addEventListener('click', doSearch);
document.getElementById('q').addEventListener('keydown', function(e){ if(e.key==='Enter') doSearch(); });
/* ===== 看过的(本机记录)=====
* 注意这里不是“已购买”:壳页在外站,拿不到主站的付费 cookie,
* 也无从知道用户到底付没付。只记“点开过”,别拿购买状态骗人。 */
function readSeen(){ try{ return JSON.parse(localStorage.getItem(BKEY)||'[]')||[]; }catch(e){ return []; } }
function pushSeen(it){
if(!it) return;
try{
var arr = readSeen().filter(function(x){ return x.dwz !== it.dwz; });
arr.unshift(it);
if(arr.length > 60) arr = arr.slice(0,60);
localStorage.setItem(BKEY, JSON.stringify(arr));
}catch(e){}
}
function renderSeen(){
var arr = readSeen();
setState(''); grid.innerHTML = '';
if(!arr.length){
grid.insertAdjacentHTML('beforeend',
'还没有看过的视频
点开任意视频后会出现在这里
');
return;
}
var frag=''; arr.forEach(function(it,i){ frag += cardHTML(it,i); });
grid.insertAdjacentHTML('beforeend', frag);
}
/* ===== 卡片点击 → 回主站 =====
* 壳页不收款、不放片。dwz 是主站 ff.html 的绝对地址,落到主站的打赏页,
* 由主站按 PlanPricingService 解析金额、走 dunpay 下单、过 shipinok 放行。
* 这里只做本机“看过的”记录,然后原样跳走。 */
grid.addEventListener('click', function(e){
var a = e.target.closest('.card'); if(!a) return;
var raw = a.getAttribute('data-it');
if(raw){ try{ pushSeen(JSON.parse(raw)); }catch(err){} }
// 不 preventDefault: 自己跳,JS 挂了也还能点
});
/* ===== 底部导航 ===== */
document.querySelectorAll('.nav').forEach(function(n){
var k = n.getAttribute('data-nav');
if(k==='rand') return;
n.addEventListener('click', function(){
document.querySelectorAll('.nav').forEach(function(x){x.classList.remove('active');});
n.classList.add('active');
if(k==='hot'){
mode='feed'; curSort='hot';
updHead(); resetFeed(); window.scrollTo({top:0,behavior:'smooth'});
} else if(k==='mine'){
mode='mine'; done=true; loading=false;
updHead(); renderSeen(); window.scrollTo({top:0,behavior:'smooth'});
}
});
});
document.getElementById('randBtn').addEventListener('click', function(){
this.classList.remove('spin'); void this.offsetWidth; this.classList.add('spin');
if(mode==='mine'){
var arr = readSeen().slice().sort(function(){ return Math.random()-0.5; });
grid.innerHTML=''; var frag=''; arr.forEach(function(it,i){ frag+=cardHTML(it,i); });
grid.insertAdjacentHTML('beforeend', frag);
window.scrollTo({top:0,behavior:'smooth'});
} else {
// 换新种子 → 服务端重做一次热度加权随机,拿到不一样的一批
SEED = Math.floor(Math.random()*2147483000)+1;
window.scrollTo({top:0,behavior:'smooth'});
resetFeed();
}
});
/* ===== toast ===== */
var toastEl = document.getElementById('toast'), toastT;
function toast(msg){
toastEl.textContent = msg; toastEl.classList.add('show');
clearTimeout(toastT); toastT = setTimeout(function(){ toastEl.classList.remove('show'); }, 2000);
}
/* ===== 无限滚动 ===== */
window.addEventListener('scroll', function(){
if(loading || done || mode==='mine') return;
if(window.innerHeight + window.scrollY >= document.body.offsetHeight - 400) loadMore();
}, {passive:true});
/* ===== 首屏 ===== */
if(!Q){ dead(); return; }
showSpinner();
ask(EP.boot, {sort:curSort, seed:SEED})
.then(function(d){
SITE = (d.site_base || '').replace(/\/+$/, '');
TOTAL = d.count;
if(d.seed) SEED = d.seed;
if(d.title) document.title = d.title;
renderCats(d.cats);
renderPlans(d.plans);
setState('');
appendList(d.list);
renderSinglePrice(d.list);
updHead();
})
.catch(function(){ dead(); });
})();