第46批:修复账号安全卡二维码扫不出(换Arase SVG带静区方案,提取qrcode-svg.js复用twofa_qr库);用户端&管理员端背景系统升级三模式(纯色/图片URL/必应API壁纸,遮罩强度可调,新增api/wallpaper.php代理带缓存与SSRF白名单)

This commit is contained in:
ypsdf1
2026-10-22 20:59:32 +08:00
parent 9dcf5dcb93
commit 3e2bd64a70
4 changed files with 2839 additions and 59 deletions
+199 -27
View File
@@ -2109,47 +2109,217 @@ function showModal(title, message, defaultValue, customHtml) {
});
}
// ===== 主题选择器 =====
// ===== 自定义背景系统(第46批):纯色 / 图片URL / API壁纸(必应) 三模式 =====
let _wpPendingUrl = null; // 待应用的图片URL(预览/获取后暂存,点应用才生效)
function wpEsc(s) {
return String(s).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
// 读取已保存的图片背景配置 {url, dim},非法数据返回 null
function getAdminBgImg() {
try {
const s = localStorage.getItem('sdf1_admin_bgimg');
if (!s) return null;
const o = JSON.parse(s);
if (o && typeof o.url === 'string' && /^https?:\/\//i.test(o.url)) {
return { url: o.url, dim: (typeof o.dim === 'number' ? o.dim : 0.35) };
}
} catch (e) {}
return null;
}
// 应用图片背景:linear-gradient 遮罩 + url 图片,cover 铺满,fixed 不随滚动
function applyAdminBgImage(url, dim, silent) {
dim = Math.max(0, Math.min(0.8, parseFloat(dim)));
if (isNaN(dim)) dim = 0.35;
const safe = url.replace(/"/g, '%22');
const b = document.body;
b.style.backgroundImage = 'linear-gradient(rgba(13,17,23,' + dim + '),rgba(13,17,23,' + dim + ')), url("' + safe + '")';
b.style.backgroundSize = 'cover';
b.style.backgroundPosition = 'center center';
b.style.backgroundRepeat = 'no-repeat';
b.style.backgroundAttachment = 'fixed';
document.documentElement.style.setProperty('--bg', 'rgba(13,17,23,' + Math.min(0.9, dim + 0.25) + ')');
localStorage.setItem('sdf1_admin_bgimg', JSON.stringify({ type: 'img', url: url, dim: dim }));
if (!silent) toast('背景图片已应用', 'ok');
}
function wpShowPreview(url) {
const pv = document.getElementById('wpPreviewBox');
if (!pv) return;
pv.style.backgroundImage = 'url("' + url.replace(/"/g, '%22') + '")';
pv.style.display = 'block';
}
// 选项卡切换:color / url / api
function wpTab(m) {
const paneColor = document.getElementById('wpPaneColor');
const paneUrl = document.getElementById('wpPaneUrl');
const paneApi = document.getElementById('wpPaneApi');
const dimRow = document.getElementById('wpDimRow');
if (!paneColor) return;
paneColor.style.display = (m === 'color') ? '' : 'none';
paneUrl.style.display = (m === 'url') ? '' : 'none';
paneApi.style.display = (m === 'api') ? '' : 'none';
dimRow.style.display = (m === 'color') ? 'none' : '';
document.getElementById('wpTabColor').className = 'btn' + (m === 'color' ? ' btn-blue' : '');
document.getElementById('wpTabUrl').className = 'btn' + (m === 'url' ? ' btn-blue' : '');
document.getElementById('wpTabApi').className = 'btn' + (m === 'api' ? ' btn-blue' : '');
}
// 图片URL预览:new Image 预检,失败不入暂存
function wpPreviewUrl() {
const input = document.getElementById('bgImgUrl');
const url = (input ? input.value : '').trim();
if (!/^https?:\/\//i.test(url)) { toast('请输入以 http(s):// 开头的图片地址', 'err'); return; }
const probe = new Image();
probe.onload = function () {
_wpPendingUrl = url;
wpShowPreview(url);
toast('预览就绪,点击应用生效', 'ok');
};
probe.onerror = function () { toast('图片加载失败,请检查地址或防盗链设置', 'err'); };
probe.src = url;
}
// API壁纸:取必应每日壁纸,成功后预览
function wpFetchBing() {
const info = document.getElementById('wpApiInfo');
if (info) info.textContent = '获取中...';
fetch('api/wallpaper.php?action=bing', { credentials: 'same-origin' })
.then(function (r) { return r.json(); })
.then(function (j) {
if (!j.success || !j.data || !j.data.url) {
if (info) info.textContent = '获取失败,可稍后重试或改用图片URL';
toast(j.message || '壁纸获取失败', 'err');
return;
}
_wpPendingUrl = j.data.url;
wpShowPreview(j.data.url);
const meta = [j.data.title, j.data.copyright].filter(Boolean).join(' — ');
if (info) info.textContent = meta || ('日期 ' + (j.data.date || ''));
toast('壁纸已预览,点击应用生效', 'ok');
})
.catch(function () {
if (info) info.textContent = '网络错误,可稍后重试或改用图片URL';
toast('壁纸获取失败', 'err');
});
}
// 应用图片背景(读遮罩滑条 + 暂存URL)
function wpApplyBg() {
if (!_wpPendingUrl) { toast('请先预览或获取一张图片', 'err'); return; }
const dimEl = document.getElementById('bgDim');
const dim = dimEl ? (parseInt(dimEl.value, 10) / 100) : 0.35;
applyAdminBgImage(_wpPendingUrl, dim, false);
const ov = document.getElementById('themePickerOverlay');
if (ov) ov.remove();
}
// 恢复默认背景(清纯色+图片配置)
function resetBg() {
localStorage.removeItem('sdf1_theme');
localStorage.removeItem('sdf1_admin_bgimg');
const b = document.body;
b.style.backgroundImage = 'none';
b.style.backgroundSize = '';
b.style.backgroundPosition = '';
b.style.backgroundRepeat = '';
b.style.backgroundAttachment = '';
document.documentElement.style.removeProperty('--bg');
b.style.background = '';
const ov = document.getElementById('themePickerOverlay');
if (ov) ov.remove();
toast('已恢复默认背景', 'ok');
}
// ===== 主题选择器(第46批:三模式背景) =====
function showThemePicker() {
const savedImg = getAdminBgImg();
_wpPendingUrl = savedImg ? savedImg.url : null;
const dimPct = savedImg ? Math.round(savedImg.dim * 100) : 35;
const overlay = document.createElement('div');
overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.6);display:flex;justify-content:center;align-items:center;z-index:2000';
overlay.id = 'themePickerOverlay';
overlay.innerHTML = `
<div style="background:var(--card);border:1px solid var(--border);border-radius:12px;padding:24px;width:500px;max-width:90%">
<h3 style="margin-bottom:12px">🎨 选择背景颜色</h3>
<div class="theme-picker">
<div class="color-btn" style="background:#0d1117" onclick="setTheme('#0d1117')"></div>
<div class="color-btn" style="background:#1a1e2e" onclick="setTheme('#1a1e2e')"></div>
<div class="color-btn" style="background:#0f4c75" onclick="setTheme('#0f4c75')"></div>
<div class="color-btn" style="background:#1b2631" onclick="setTheme('#1b2631')"></div>
<div class="color-btn" style="background:#2c3e50" onclick="setTheme('#2c3e50')"></div>
<div class="color-btn" style="background:#23272a" onclick="setTheme('#23272a')"></div>
<div id="themePickerModal" style="background:var(--card);border:1px solid var(--border);border-radius:12px;padding:24px;width:520px;max-width:94%;position:relative;max-height:90vh;overflow-y:auto">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
<h3 style="margin:0">🎨 自定义背景</h3>
<button onclick="document.getElementById('themePickerOverlay').remove()" style="background:none;border:none;color:var(--dim);font-size:20px;cursor:pointer">✕</button>
</div>
<div style="margin-top:16px">
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<span style="color:var(--dim);font-size:12px;white-space:nowrap">R</span>
<input type="number" id="rgbR" min="0" max="255" placeholder="0-255" style="width:70px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:13px;text-align:center">
<span style="color:var(--dim);font-size:12px;white-space:nowrap">G</span>
<input type="number" id="rgbG" min="0" max="255" placeholder="0-255" style="width:70px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:13px;text-align:center">
<span style="color:var(--dim);font-size:12px;white-space:nowrap">B</span>
<input type="number" id="rgbB" min="0" max="255" placeholder="0-255" style="width:70px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:13px;text-align:center">
<span style="color:var(--dim);font-size:11px">或</span>
<input type="text" id="customColor" placeholder="#1a237e" class="color-input" style="flex:1;min-width:120px">
<div style="display:flex;gap:6px;margin-bottom:14px">
<button class="btn" id="wpTabColor" onclick="wpTab('color')">纯色</button>
<button class="btn" id="wpTabUrl" onclick="wpTab('url')">图片URL</button>
<button class="btn" id="wpTabApi" onclick="wpTab('api')">API壁纸</button>
</div>
<div id="wpPaneColor">
<div class="theme-picker" style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:16px">
<div class="color-btn" style="background:#0d1117" onclick="setTheme('#0d1117')"></div>
<div class="color-btn" style="background:#1a1e2e" onclick="setTheme('#1a1e2e')"></div>
<div class="color-btn" style="background:#0f4c75" onclick="setTheme('#0f4c75')"></div>
<div class="color-btn" style="background:#1b2631" onclick="setTheme('#1b2631')"></div>
<div class="color-btn" style="background:#2c3e50" onclick="setTheme('#2c3e50')"></div>
<div class="color-btn" style="background:#23272a" onclick="setTheme('#23272a')"></div>
</div>
<div>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<span style="color:var(--dim);font-size:12px;white-space:nowrap">R</span>
<input type="number" id="rgbR" min="0" max="255" placeholder="0-255" style="width:70px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:13px;text-align:center">
<span style="color:var(--dim);font-size:12px;white-space:nowrap">G</span>
<input type="number" id="rgbG" min="0" max="255" placeholder="0-255" style="width:70px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:13px;text-align:center">
<span style="color:var(--dim);font-size:12px;white-space:nowrap">B</span>
<input type="number" id="rgbB" min="0" max="255" placeholder="0-255" style="width:70px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:13px;text-align:center">
<span style="color:var(--dim);font-size:11px">或</span>
<input type="text" id="customColor" placeholder="#1a237e" class="color-input" style="flex:1;min-width:120px">
</div>
<div style="margin-top:10px"><button class="btn btn-blue" onclick="applyCustomColor()">应用纯色</button></div>
</div>
</div>
<div style="display:flex;gap:8px;justify-content:flex-end;margin-top:16px">
<button class="btn" id="themeCancel">取消</button>
<button class="btn btn-blue" onclick="applyCustomColor()">应用</button>
<div id="wpPaneUrl" style="display:none">
<div style="display:flex;gap:8px;align-items:center">
<input type="text" id="bgImgUrl" placeholder="https://example.com/wallpaper.jpg" value="${wpEsc(savedImg ? savedImg.url : '')}" style="flex:1;padding:8px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:13px">
<button class="btn" onclick="wpPreviewUrl()">预览</button>
</div>
<div style="color:var(--dim);font-size:11px;margin-top:6px">支持任意直链 jpg/png/webp;对方站点防盗链可能导致加载失败</div>
</div>
<div id="wpPaneApi" style="display:none">
<button class="btn btn-blue" onclick="wpFetchBing()">获取必应每日壁纸</button>
<div id="wpApiInfo" style="margin-top:10px;color:var(--dim);font-size:12px">点击获取后自动预览,再点应用生效</div>
</div>
<div id="wpDimRow" style="display:none;margin-top:14px">
<div style="display:flex;align-items:center;gap:10px">
<span style="color:var(--dim);font-size:12px;white-space:nowrap">遮罩强度</span>
<input type="range" id="bgDim" min="0" max="80" value="${dimPct}" oninput="document.getElementById('bgDimVal').textContent=this.value+'%';" style="flex:1">
<span id="bgDimVal" style="color:var(--dim);font-size:12px;width:44px;text-align:right">${dimPct}%</span>
</div>
</div>
<div id="wpPreviewBox" style="display:none;margin-top:14px;border-radius:8px;height:120px;background-size:cover;background-position:center;border:1px solid var(--border)"></div>
<div style="display:flex;gap:8px;justify-content:space-between;margin-top:16px;flex-wrap:wrap">
<button class="btn" onclick="resetBg()">恢复默认背景</button>
<div style="display:flex;gap:8px">
<button class="btn" onclick="document.getElementById('themePickerOverlay').remove()">取消</button>
<button class="btn btn-blue" onclick="wpApplyBg()">应用图片</button>
</div>
</div>
</div>`;
document.body.appendChild(overlay);
overlay.querySelector('#themeCancel').onclick = () => overlay.remove();
overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
wpTab(savedImg ? 'url' : 'color');
if (savedImg) wpShowPreview(savedImg.url);
}
function setTheme(color) {
const b = document.body;
b.style.backgroundImage = 'none';
b.style.backgroundSize = '';
b.style.backgroundPosition = '';
b.style.backgroundRepeat = '';
b.style.backgroundAttachment = '';
document.documentElement.style.setProperty('--bg', color);
document.body.style.background = color;
b.style.background = color;
localStorage.removeItem('sdf1_admin_bgimg');
localStorage.setItem('sdf1_theme', color);
_wpPendingUrl = null;
toast('主题已应用', 'ok');
}
@@ -2197,8 +2367,10 @@ function applyCustomColor() {
}
}
// 加载保存的主题
// 加载保存的主题(第46批:图片背景优先,其次纯色)
(function() {
const savedImg = getAdminBgImg();
if (savedImg) { applyAdminBgImage(savedImg.url, savedImg.dim, true); return; }
const savedTheme = localStorage.getItem('sdf1_theme');
if (savedTheme) setTheme(savedTheme);
})();
+124
View File
@@ -0,0 +1,124 @@
<?php
/**
* 自定义背景壁纸源代理 —— 第46批
*
* 用户端(player.php)与管理员端(admin.php)的自定义背景系统共用:
* action=bing 返回必应每日壁纸元数据(图片URL/标题/版权/日期),6小时文件缓存
* action=image 按 u 参数转发图片二进制(仅白名单 bing.com 域,防SSRF/防盗链),浏览器缓存6小时
*
* 自定义图片URL模式为浏览器直连(前端 background-image),不经过本代理。
* 自包含轻量实现,不依赖 core.php/DB。
*/
while (ob_get_level() > 0) { @ob_end_clean(); }
ob_start();
error_reporting(E_ALL);
ini_set('display_errors', '0');
ini_set('log_errors', '1');
/** 必应结果缓存时长(秒)—— 每日换图,6小时足够 */
define('WPJ_BING_TTL', 21600);
function wpjOut($ok, $msg, $data = null) {
header('Content-Type: application/json; charset=utf-8');
header('X-Content-Type-Options: nosniff');
header('Cache-Control: no-store');
echo json_encode(array('success' => $ok, 'message' => $msg, 'data' => $data),
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
/** 缓存文件放系统临时目录 —— Web 根可能只读,临时目录必可写 */
function wpjCacheFile($key) {
return rtrim(sys_get_temp_dir(), '/\\') . DIRECTORY_SEPARATOR . 'sdf1_wp_' . $key . '.json';
}
function wpjCacheGet($key, $ttl) {
$f = wpjCacheFile($key);
if (is_file($f) && (time() - filemtime($f)) < $ttl) {
$j = json_decode((string)@file_get_contents($f), true);
if (is_array($j)) { return $j; }
}
return null;
}
function wpjCacheSet($key, $arr) {
@file_put_contents(wpjCacheFile($key),
json_encode($arr, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
}
function wpjFetch($url, $timeout, $referer) {
if (function_exists('curl_init')) {
$ch = curl_init($url);
curl_setopt_array($ch, array(
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_CONNECTTIMEOUT => 4,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) sdf1-wallpaper',
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_REFERER => $referer,
));
$body = curl_exec($ch);
curl_close($ch);
return is_string($body) ? $body : '';
}
$ctx = stream_context_create(array('http' => array(
'timeout' => $timeout,
'header' => "User-Agent: Mozilla/5.0 sdf1-wallpaper\r\nReferer: " . $referer . "\r\n",
)));
$body = @file_get_contents($url, false, $ctx);
return is_string($body) ? $body : '';
}
$act = isset($_GET['action']) ? $_GET['action'] : '';
/* ===== 必应每日壁纸元数据 ===== */
if ($act === 'bing') {
$cached = wpjCacheGet('bing', WPJ_BING_TTL);
if ($cached !== null) { wpjOut(true, 'ok', $cached); }
$api = 'https://www.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1&mkt=zh-CN';
$body = wpjFetch($api, 6, 'https://www.bing.com/');
$j = json_decode($body, true);
if (!is_array($j) || empty($j['images'][0]['urlbase'])) {
wpjOut(false, '壁纸源暂不可用,请稍后重试或改用图片URL');
}
$img = $j['images'][0];
$direct = 'https://www.bing.com' . $img['urlbase'] . '_1920x1080.jpg';
$data = array(
// 前端优先用代理地址(防防盗链),direct 作兜底直连
'url' => 'wallpaper.php?action=image&u=' . rawurlencode($direct),
'direct' => $direct,
'title' => isset($img['title']) ? $img['title'] : '',
'copyright' => isset($img['copyright']) ? $img['copyright'] : '',
'date' => isset($img['startdate']) ? $img['startdate'] : '',
);
wpjCacheSet('bing', $data);
wpjOut(true, 'ok', $data);
}
/* ===== 图片转发(仅 bing 白名单,防 SSRF) ===== */
if ($act === 'image') {
$u = isset($_GET['u']) ? (string)$_GET['u'] : '';
$host = @parse_url($u, PHP_URL_HOST);
if ($u === '' || !is_string($host) || $host === '') { wpjOut(false, '参数无效'); }
$host = strtolower($host);
$allow = array('www.bing.com', 'bing.com', 'th.bing.com', 'cn.bing.com');
if (!in_array($host, $allow, true)) { wpjOut(false, '不支持的图片源'); }
$bin = wpjFetch($u, 10, 'https://www.bing.com/');
if (!is_string($bin) || strlen($bin) < 1000) { wpjOut(false, '图片下载失败'); }
// 粗校验图片魔数,防返回HTML错误页被当图片缓存
$head = substr($bin, 0, 3);
if ($head !== "\xFF\xD8\xFF" && substr($bin, 0, 4) !== "\x89PNG" && stripos(substr($bin, 0, 32), 'RIFF') !== 0) {
wpjOut(false, '图片源返回内容异常');
}
header('Content-Type: image/jpeg');
header('Cache-Control: public, max-age=21600');
echo $bin;
exit;
}
wpjOut(false, '未知 action');
+214 -32
View File
@@ -370,7 +370,7 @@ if ($currentVersion !== $BUILD_VERSION) {
</div>
</div>
<script src="qrcode.min.js?v=<?php echo filemtime(__DIR__ . '/qrcode.min.js'); ?>"></script>
<script src="qrcode-svg.js?v=<?php echo filemtime(__DIR__ . '/qrcode-svg.js'); ?>"></script>
<script>
// ★ 人机验证挂件HTML:由 config.php 的 CAPTCHA_PROVIDER 单点决定 CF/VA(全项目只用一家,不混用)。
// 未启用(off / 密钥未配)时为空串,前端不挂件、服务端同步放行,
@@ -2680,50 +2680,217 @@ if ($currentVersion !== $BUILD_VERSION) {
toast(isLight ? '浅色主题' : '深色主题', 'success');
}
// ===== 自定义背景系统(第46批):纯色 / 图片URL / API壁纸(必应) 三模式 =====
let _wpPendingUrl = null; // 待应用的图片URL(预览/获取后暂存,点应用才生效)
function wpEsc(s) {
return String(s).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
// 读取已保存的图片背景配置 {url, dim},非法数据返回 null
function getPlayerBgImg() {
try {
const s = localStorage.getItem('sdf1_player_bgimg');
if (!s) return null;
const o = JSON.parse(s);
if (o && typeof o.url === 'string' && /^https?:\/\//i.test(o.url)) {
return { url: o.url, dim: (typeof o.dim === 'number' ? o.dim : 0.35) };
}
} catch (e) {}
return null;
}
// 应用图片背景:linear-gradient 遮罩 + url 图片,cover 铺满,fixed 不随滚动
function applyPlayerBgImage(url, dim, silent) {
dim = Math.max(0, Math.min(0.8, parseFloat(dim)));
if (isNaN(dim)) dim = 0.35;
const safe = url.replace(/"/g, '%22');
const b = document.body;
b.style.backgroundImage = 'linear-gradient(rgba(13,17,23,' + dim + '),rgba(13,17,23,' + dim + ')), url("' + safe + '")';
b.style.backgroundSize = 'cover';
b.style.backgroundPosition = 'center center';
b.style.backgroundRepeat = 'no-repeat';
b.style.backgroundAttachment = 'fixed';
document.documentElement.style.setProperty('--bg', 'rgba(13,17,23,' + Math.min(0.9, dim + 0.25) + ')');
localStorage.setItem('sdf1_player_bgimg', JSON.stringify({ type: 'img', url: url, dim: dim }));
if (!silent) toast('背景图片已应用', 'success');
}
function wpShowPreview(url) {
const pv = document.getElementById('wpPreviewBox');
if (!pv) return;
pv.style.backgroundImage = 'url("' + url.replace(/"/g, '%22') + '")';
pv.style.display = 'block';
}
// 选项卡切换:color / url / api
function wpTab(m) {
const paneColor = document.getElementById('wpPaneColor');
const paneUrl = document.getElementById('wpPaneUrl');
const paneApi = document.getElementById('wpPaneApi');
const dimRow = document.getElementById('wpDimRow');
if (!paneColor) return;
paneColor.style.display = (m === 'color') ? '' : 'none';
paneUrl.style.display = (m === 'url') ? '' : 'none';
paneApi.style.display = (m === 'api') ? '' : 'none';
dimRow.style.display = (m === 'color') ? 'none' : '';
document.getElementById('wpTabColor').className = 'btn' + (m === 'color' ? ' btn-blue' : '');
document.getElementById('wpTabUrl').className = 'btn' + (m === 'url' ? ' btn-blue' : '');
document.getElementById('wpTabApi').className = 'btn' + (m === 'api' ? ' btn-blue' : '');
}
// 图片URL预览:new Image 预检,失败不入暂存
function wpPreviewUrl() {
const input = document.getElementById('bgImgUrl');
const url = (input ? input.value : '').trim();
if (!/^https?:\/\//i.test(url)) { toast('请输入以 http(s):// 开头的图片地址', 'err'); return; }
const probe = new Image();
probe.onload = function () {
_wpPendingUrl = url;
wpShowPreview(url);
toast('预览就绪,点击应用生效', 'success');
};
probe.onerror = function () { toast('图片加载失败,请检查地址或防盗链设置', 'err'); };
probe.src = url;
}
// API壁纸:取必应每日壁纸,成功后预览
function wpFetchBing() {
const info = document.getElementById('wpApiInfo');
if (info) info.textContent = '获取中...';
fetch('api/wallpaper.php?action=bing', { credentials: 'same-origin' })
.then(function (r) { return r.json(); })
.then(function (j) {
if (!j.success || !j.data || !j.data.url) {
if (info) info.textContent = '获取失败,可稍后重试或改用图片URL';
toast(j.message || '壁纸获取失败', 'err');
return;
}
_wpPendingUrl = j.data.url;
wpShowPreview(j.data.url);
const meta = [j.data.title, j.data.copyright].filter(Boolean).join(' — ');
if (info) info.textContent = meta || ('日期 ' + (j.data.date || ''));
toast('壁纸已预览,点击应用生效', 'success');
})
.catch(function () {
if (info) info.textContent = '网络错误,可稍后重试或改用图片URL';
toast('壁纸获取失败', 'err');
});
}
// 应用图片背景(读遮罩滑条 + 暂存URL)
function wpApplyBg() {
if (!_wpPendingUrl) { toast('请先预览或获取一张图片', 'err'); return; }
const dimEl = document.getElementById('bgDim');
const dim = dimEl ? (parseInt(dimEl.value, 10) / 100) : 0.35;
applyPlayerBgImage(_wpPendingUrl, dim, false);
const ov = document.getElementById('themePickerOverlay');
if (ov) ov.remove();
}
// 恢复默认背景(清纯色+图片配置)
function resetPlayerBg() {
localStorage.removeItem('sdf1_player_bg');
localStorage.removeItem('sdf1_player_bgimg');
const b = document.body;
b.style.backgroundImage = 'none';
b.style.backgroundSize = '';
b.style.backgroundPosition = '';
b.style.backgroundRepeat = '';
b.style.backgroundAttachment = '';
document.documentElement.style.removeProperty('--bg');
b.style.background = '';
const ov = document.getElementById('themePickerOverlay');
if (ov) ov.remove();
toast('已恢复默认背景', 'success');
}
function showThemePicker() {
const savedImg = getPlayerBgImg();
_wpPendingUrl = savedImg ? savedImg.url : null;
const dimPct = savedImg ? Math.round(savedImg.dim * 100) : 35;
const overlay = document.createElement('div');
overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.6);display:flex;justify-content:center;align-items:center;z-index:2000';
overlay.id = 'themePickerOverlay';
overlay.innerHTML = `
<div id="themePickerModal" style="background:var(--card);border:1px solid var(--border);border-radius:12px;padding:24px;width:500px;max-width:90%;position:relative">
<div id="themePickerModal" style="background:var(--card);border:1px solid var(--border);border-radius:12px;padding:24px;width:520px;max-width:94%;position:relative;max-height:90vh;overflow-y:auto">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
<h3 style="margin:0">🎨 选择背景颜色</h3>
<h3 style="margin:0">🎨 自定义背景</h3>
<button onclick="document.getElementById('themePickerOverlay').remove()" style="background:none;border:none;color:var(--dim);font-size:20px;cursor:pointer">✕</button>
</div>
<div class="theme-picker" style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:16px">
<div class="color-btn" style="width:36px;height:36px;border-radius:6px;background:#0d1117;border:2px solid var(--border);cursor:pointer" onclick="setPlayerTheme('#0d1117')"></div>
<div class="color-btn" style="width:36px;height:36px;border-radius:6px;background:#1a1e2e;border:2px solid var(--border);cursor:pointer" onclick="setPlayerTheme('#1a1e2e')"></div>
<div class="color-btn" style="width:36px;height:36px;border-radius:6px;background:#0f4c75;border:2px solid var(--border);cursor:pointer" onclick="setPlayerTheme('#0f4c75')"></div>
<div class="color-btn" style="width:36px;height:36px;border-radius:6px;background:#1b2631;border:2px solid var(--border);cursor:pointer" onclick="setPlayerTheme('#1b2631')"></div>
<div class="color-btn" style="width:36px;height:36px;border-radius:6px;background:#2c3e50;border:2px solid var(--border);cursor:pointer" onclick="setPlayerTheme('#2c3e50')"></div>
<div class="color-btn" style="width:36px;height:36px;border-radius:6px;background:#23272a;border:2px solid var(--border);cursor:pointer" onclick="setPlayerTheme('#23272a')"></div>
<div class="color-btn" style="width:36px;height:36px;border-radius:6px;background:#ffffff;border:2px solid var(--border);cursor:pointer" onclick="setPlayerTheme('#ffffff')"></div>
<div style="display:flex;gap:6px;margin-bottom:14px">
<button class="btn" id="wpTabColor" onclick="wpTab('color')">纯色</button>
<button class="btn" id="wpTabUrl" onclick="wpTab('url')">图片URL</button>
<button class="btn" id="wpTabApi" onclick="wpTab('api')">API壁纸</button>
</div>
<div style="margin-top:16px">
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<span style="color:var(--dim);font-size:12px;white-space:nowrap">R</span>
<input type="number" id="rgbR" min="0" max="255" placeholder="0-255" style="width:70px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:13px;text-align:center">
<span style="color:var(--dim);font-size:12px;white-space:nowrap">G</span>
<input type="number" id="rgbG" min="0" max="255" placeholder="0-255" style="width:70px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:13px;text-align:center">
<span style="color:var(--dim);font-size:12px;white-space:nowrap">B</span>
<input type="number" id="rgbB" min="0" max="255" placeholder="0-255" style="width:70px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:13px;text-align:center">
<span style="color:var(--dim);font-size:11px">或</span>
<input type="text" id="customColor" placeholder="#1a237e" class="color-input" style="flex:1;min-width:120px">
<div id="wpPaneColor">
<div class="theme-picker" style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:16px">
<div class="color-btn" style="width:36px;height:36px;border-radius:6px;background:#0d1117;border:2px solid var(--border);cursor:pointer" onclick="setPlayerTheme('#0d1117')"></div>
<div class="color-btn" style="width:36px;height:36px;border-radius:6px;background:#1a1e2e;border:2px solid var(--border);cursor:pointer" onclick="setPlayerTheme('#1a1e2e')"></div>
<div class="color-btn" style="width:36px;height:36px;border-radius:6px;background:#0f4c75;border:2px solid var(--border);cursor:pointer" onclick="setPlayerTheme('#0f4c75')"></div>
<div class="color-btn" style="width:36px;height:36px;border-radius:6px;background:#1b2631;border:2px solid var(--border);cursor:pointer" onclick="setPlayerTheme('#1b2631')"></div>
<div class="color-btn" style="width:36px;height:36px;border-radius:6px;background:#2c3e50;border:2px solid var(--border);cursor:pointer" onclick="setPlayerTheme('#2c3e50')"></div>
<div class="color-btn" style="width:36px;height:36px;border-radius:6px;background:#23272a;border:2px solid var(--border);cursor:pointer" onclick="setPlayerTheme('#23272a')"></div>
<div class="color-btn" style="width:36px;height:36px;border-radius:6px;background:#ffffff;border:2px solid var(--border);cursor:pointer" onclick="setPlayerTheme('#ffffff')"></div>
</div>
<div>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<span style="color:var(--dim);font-size:12px;white-space:nowrap">R</span>
<input type="number" id="rgbR" min="0" max="255" placeholder="0-255" style="width:70px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:13px;text-align:center">
<span style="color:var(--dim);font-size:12px;white-space:nowrap">G</span>
<input type="number" id="rgbG" min="0" max="255" placeholder="0-255" style="width:70px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:13px;text-align:center">
<span style="color:var(--dim);font-size:12px;white-space:nowrap">B</span>
<input type="number" id="rgbB" min="0" max="255" placeholder="0-255" style="width:70px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:13px;text-align:center">
<span style="color:var(--dim);font-size:11px">或</span>
<input type="text" id="customColor" placeholder="#1a237e" class="color-input" style="flex:1;min-width:120px">
</div>
<div style="margin-top:10px"><button class="btn btn-blue" onclick="applyPlayerCustomColor()">应用纯色</button></div>
</div>
</div>
<div style="display:flex;gap:8px;justify-content:flex-end;margin-top:16px">
<button class="btn" onclick="document.getElementById('themePickerOverlay').remove()">取消</button>
<button class="btn btn-blue" onclick="applyPlayerCustomColor()">应用</button>
<div id="wpPaneUrl" style="display:none">
<div style="display:flex;gap:8px;align-items:center">
<input type="text" id="bgImgUrl" placeholder="https://example.com/wallpaper.jpg" value="${wpEsc(savedImg ? savedImg.url : '')}" style="flex:1;padding:8px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:13px">
<button class="btn" onclick="wpPreviewUrl()">预览</button>
</div>
<div style="color:var(--dim);font-size:11px;margin-top:6px">支持任意直链 jpg/png/webp;对方站点防盗链可能导致加载失败</div>
</div>
<div id="wpPaneApi" style="display:none">
<button class="btn btn-blue" onclick="wpFetchBing()">获取必应每日壁纸</button>
<div id="wpApiInfo" style="margin-top:10px;color:var(--dim);font-size:12px">点击获取后自动预览,再点应用生效</div>
</div>
<div id="wpDimRow" style="display:none;margin-top:14px">
<div style="display:flex;align-items:center;gap:10px">
<span style="color:var(--dim);font-size:12px;white-space:nowrap">遮罩强度</span>
<input type="range" id="bgDim" min="0" max="80" value="${dimPct}" oninput="document.getElementById('bgDimVal').textContent=this.value+'%';" style="flex:1">
<span id="bgDimVal" style="color:var(--dim);font-size:12px;width:44px;text-align:right">${dimPct}%</span>
</div>
</div>
<div id="wpPreviewBox" style="display:none;margin-top:14px;border-radius:8px;height:120px;background-size:cover;background-position:center;border:1px solid var(--border)"></div>
<div style="display:flex;gap:8px;justify-content:space-between;margin-top:16px;flex-wrap:wrap">
<button class="btn" onclick="resetPlayerBg()">恢复默认背景</button>
<div style="display:flex;gap:8px">
<button class="btn" onclick="document.getElementById('themePickerOverlay').remove()">取消</button>
<button class="btn btn-blue" onclick="wpApplyBg()">应用图片</button>
</div>
</div>
</div>`;
document.body.appendChild(overlay);
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
wpTab(savedImg ? 'url' : 'color');
if (savedImg) wpShowPreview(savedImg.url);
}
function setPlayerTheme(color) {
const b = document.body;
b.style.backgroundImage = 'none';
b.style.backgroundSize = '';
b.style.backgroundPosition = '';
b.style.backgroundRepeat = '';
b.style.backgroundAttachment = '';
document.documentElement.style.setProperty('--bg', color);
document.body.style.background = color;
b.style.background = color;
localStorage.removeItem('sdf1_player_bgimg');
localStorage.setItem('sdf1_player_bg', color);
_wpPendingUrl = null;
toast('背景颜色已应用', 'success');
}
@@ -2781,8 +2948,10 @@ if ($currentVersion !== $BUILD_VERSION) {
}
}
// 加载保存的主题
// 加载保存的主题(第46批:图片背景优先,其次纯色)
(function() {
const savedImg = getPlayerBgImg();
if (savedImg) { applyPlayerBgImage(savedImg.url, savedImg.dim, true); return; }
const savedBg = localStorage.getItem('sdf1_player_bg');
if (savedBg) {
document.documentElement.style.setProperty('--bg', savedBg);
@@ -3085,16 +3254,29 @@ if ($currentVersion !== $BUILD_VERSION) {
function acctRenderQr(text) {
const box = document.getElementById('acctQr');
if (!box) return;
if (typeof QRCode === 'undefined') {
if (typeof qrcode === 'undefined') {
box.innerHTML = '<div style="color:var(--red);font-size:12px;padding:6px">二维码组件未加载,请手动复制下方密钥录入认证器</div>';
return;
}
box.innerHTML = '';
try {
new QRCode(box, {text: text, width: 180, height: 180, colorDark: '#000000', colorLight: '#ffffff', correctLevel: QRCode.CorrectLevel.M});
} catch (e) {
box.innerHTML = '<div style="color:var(--red);font-size:12px">二维码渲染失败,请手动复制下方密钥</div>';
// Arase SVG 方案:与 twofa_qr.php 同款。SVG + margin:4 静区 + 白底容器 + 型号自适应,手机扫描可靠
let svg = '';
for (let n = 1; n <= 12; n++) { // 二维码型号自适应:按数据长度逐级尝试
try {
const q = qrcode(n, 'M');
q.addData(text);
q.make();
svg = q.createSvgTag({ cellSize: 5, margin: 4, scalable: true, alt: 'TOTP 绑定二维码' });
break;
} catch (e) { /* 型号容量不够,换下一个 */ }
}
if (!svg) {
box.innerHTML = '<div style="color:var(--red);font-size:12px">二维码渲染失败,请手动复制下方密钥</div>';
return;
}
// 白底容器保证四周留白(静区),固定 220px 清晰渲染,不随容器发虚
box.innerHTML = '<div style="display:inline-block;background:#fff;padding:10px;border-radius:8px">' + svg + '</div>';
const sv = box.querySelector('svg');
if (sv) { sv.style.width = '220px'; sv.style.height = '220px'; sv.style.display = 'block'; }
}
async function acctDoBind(ticket) {
+2302
View File
File diff suppressed because it is too large Load Diff