Files
shaguabijia-app-server/h5/settings/index.html
T

542 lines
36 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="../shared/fonts.css">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>设置 · 傻瓜比价</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
/* 【去框 + 不滚动】页面本身不滚动(固定屏),内部 set-scroll 才滚;不用 min-height:100vh(避免 vh 与 WebView 视口差导致整页能下滑一点) */
html, body { height: 100%; overflow: hidden; }
body { font-family: 'PuHuiTi', -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue', sans-serif; background: #F5F5F5; color: #1A1A1A; }
.device { position: relative; width: 100%; height: 100%; background: #F5F5F5; overflow: hidden; }
.set-screen { position: absolute; inset: 0; display: flex; flex-direction: column; background: #F5F5F5; }
/* header */
.set-header { height: 48px; flex-shrink: 0; display: flex; align-items: center; padding: 0 8px; position: relative; background: #F5F5F5; }
.set-back { width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; background: none; border: none; cursor: pointer; padding: 0; }
.set-back:active { opacity: .5; }
.set-back svg { width: 24px; height: 24px; display: block; }
.set-title { position: absolute; left: 50%; transform: translateX(-50%); font-size: 17px; font-weight: 600; color: #1A1A1A; }
/* panels */
.set-body { flex: 1; min-height: 0; display: flex; flex-direction: column; }
.set-body[hidden] { display: none; }
.set-scroll { flex: 1; min-height: 0; overflow-y: auto; -webkit-overflow-scrolling: touch; padding: 12px 14px; display: flex; flex-direction: column; gap: 12px; }
.set-scroll::-webkit-scrollbar { display: none; }
.set-bottom-group { flex-shrink: 0; padding: 8px 14px 20px; display: flex; flex-direction: column; gap: 6px; }
/* card + row */
.set-card { background: #FFFFFF; border-radius: 14px; padding: 0 16px; box-shadow: 0 1px 2px rgba(0,0,0,.03); }
.set-row { display: flex; align-items: center; min-height: 56px; padding: 12px 0; cursor: pointer; }
.set-row:active { background: rgba(0,0,0,.02); }
.set-row.set-static { cursor: default; }
.set-row.set-static:active { background: transparent; }
.set-row-label { flex: 1; font-size: 15px; font-weight: 400; color: #1A1A1A; line-height: 1.5; }
.set-row-right { display: flex; align-items: center; gap: 8px; min-width: 0; }
.set-value { font-size: 15px; color: #999999; max-width: 52vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.set-chevron { display: flex; align-items: center; flex-shrink: 0; }
.set-chevron svg { width: 8px; height: 14px; display: block; }
.set-divider { height: .5px; background: #E5E5E5; }
/* 头像缩略(对齐原型 .set-avatar-thumb:渐变圆 + 卡通头像 SVG */
.set-avatar-thumb { width: 40px; height: 40px; border-radius: 50%; overflow: hidden; background: radial-gradient(circle at 42% 32%, #FFE0B2 0%, #FFE066 43%, #FFEB3B 100%); border: 2px solid #fff; display: flex; align-items: center; justify-content: center; flex-shrink: 0; box-shadow: 0 6px 16px rgba(255,137,32,.20); }
.set-avatar-thumb svg { width: 38px; height: 38px; display: block; }
.set-avatar-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* 退出 / 注销 */
.set-logout-row { justify-content: center; }
.set-logout-row .set-row-label { flex: 0 0 auto; font-size: 15px; color: #666666; text-align: center; white-space: nowrap; }
.set-delete { font-size: 12px; color: #999999; padding: 6px 12px; border-radius: 6px; cursor: pointer; align-self: center; }
.set-delete:active { background: rgba(0,0,0,.04); }
/* 关于 hero */
.set-about-hero { padding: 28px 0 34px; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; }
.set-about-brand { display: inline-flex; align-items: center; justify-content: center; gap: 12px; }
.set-about-logo { width: 48px; height: 48px; border-radius: 12px; display: block; box-shadow: 0 5px 14px rgba(0,0,0,.08); }
.set-about-name { font-size: 30px; font-weight: 900; line-height: 1.05; color: #000000; white-space: nowrap; }
.set-about-version { margin-top: 10px; font-size: 13px; font-weight: 400; line-height: 1.35; color: #999999; }
/* 个性化广告 假开关 */
.set-switch-row { display: flex; align-items: flex-start; gap: 12px; padding: 14px 0; }
.set-switch-text { flex: 1; min-width: 0; }
.set-switch-label { font-size: 15px; color: #1A1A1A; line-height: 22px; }
.set-switch-desc { font-size: 12px; color: #999999; line-height: 18px; margin-top: 4px; }
.set-switch { width: 50px; height: 30px; border-radius: 15px; background: #E5E5E5; position: relative; flex-shrink: 0; cursor: pointer; transition: background .2s; margin-top: 2px; }
.set-switch.on { background: #34C759; }
.set-switch-knob { position: absolute; top: 2px; left: 2px; width: 26px; height: 26px; border-radius: 50%; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.2); transition: transform .2s; }
.set-switch.on .set-switch-knob { transform: translateX(20px); }
/* ===== 昵称编辑弹窗(对齐原型 .nick-modal:按钮 radius 10 四角圆弧,输入 #FAFAFA,含字数) ===== */
.nick-modal { display: none; position: fixed; inset: 0; z-index: 285; background: rgba(0,0,0,.45); align-items: center; justify-content: center; padding: 0 32px; }
.nick-modal.open { display: flex; }
.nick-modal-card { width: 100%; background: #FFFFFF; border-radius: 14px; padding: 20px 18px 16px; }
.nick-modal-title { font-size: 16px; font-weight: 600; line-height: 1.5; color: #1A1A1A; text-align: center; margin-bottom: 14px; }
.nick-modal-input { width: 100%; height: 40px; padding: 0 12px; border: none; background: #FAFAFA; border-radius: 10px; font-size: 15px; font-weight: 400; color: #1A1A1A; font-family: inherit; outline: none; margin-bottom: 6px; }
.nick-modal-count { text-align: right; font-size: 12px; font-weight: 400; line-height: 1.5; color: #999999; margin: 0 2px 14px; }
.nick-modal-actions { display: flex; gap: 12px; }
.nick-modal-btn { flex: 1; height: 40px; border-radius: 10px; border: none; font-size: 15px; font-weight: 700; line-height: 1; cursor: pointer; font-family: inherit; }
.nick-modal-btn.cancel { background: #FAFAFA; color: #666666; }
.nick-modal-btn.confirm { background: #FFD600; color: #1A1A1A; }
/* ===== 退出 / 注销确认弹窗(对齐原型:radius 18 卡 + 按钮 radius 12 四角圆弧) ===== */
.cfm-mask { display: none; position: fixed; inset: 0; z-index: 260; background: rgba(0,0,0,.45); align-items: center; justify-content: center; padding: 40px 32px; }
.cfm-mask.open { display: flex; }
.cfm-card { background: #fff; border-radius: 18px; max-width: 295px; width: 100%; padding: 24px 24px 16px; text-align: center; }
.cfm-warn { font-size: 48px; line-height: 1; margin-bottom: 12px; }
.cfm-title { font-size: 18px; font-weight: 700; margin-bottom: 8px; color: #1A1A1A; }
.cfm-title.danger { color: #E53935; }
.cfm-msg { font-size: 14px; color: #999999; line-height: 22px; margin-bottom: 24px; }
.cfm-msg.tight { font-size: 13px; margin-bottom: 8px; white-space: nowrap; }
.cfm-msg .danger { color: #E53935; font-weight: 600; }
.cfm-databox { text-align: left; background: #FAFAFA; border-radius: 8px; padding: 12px 16px; margin-bottom: 24px; font-size: 13px; color: #666666; line-height: 22px; }
.cfm-actions { display: flex; gap: 12px; }
.cfm-btn { flex: 1; padding: 12px; border-radius: 12px; font-size: 15px; font-weight: 600; cursor: pointer; font-family: inherit; }
.cfm-btn.outline { border: 1px solid #CCCCCC; background: #fff; color: #4D4D4D; }
.cfm-btn.cta { border: none; background: linear-gradient(180deg, #FFE066 0%, #FFD600 100%); color: #1A1A1A; box-shadow: inset 0 1px 0 rgba(255,255,255,.7), 0 6px 18px rgba(255,179,0,.35); }
/* ===== 头像底部 sheet(对齐原型 .avatar-sheet:设置页下方弹出) ===== */
.avatar-sheet-mask { display: none; position: fixed; inset: 0; z-index: 290; background: rgba(0,0,0,.45); }
.avatar-sheet-mask.open { display: block; }
.avatar-sheet { position: fixed; left: 0; right: 0; bottom: 0; z-index: 291; transform: translateY(100%); transition: transform .26s cubic-bezier(.32,.72,.27,1); padding: 0 8px 8px; pointer-events: none; }
.avatar-sheet.open { transform: translateY(0); pointer-events: auto; }
.avatar-sheet-group { background: rgba(255,255,255,.98); border-radius: 14px; overflow: hidden; }
.avatar-sheet-grip { width: 36px; height: 4px; background: rgba(0,0,0,.18); border-radius: 2px; margin: 8px auto 4px; }
.avatar-sheet-btn { width: 100%; height: 56px; border: none; background: transparent; font-size: 17px; font-weight: 400; line-height: 1.5; color: #1A1A1A; cursor: pointer; font-family: inherit; }
.avatar-sheet-btn:active { background: rgba(0,0,0,.05); }
.avatar-sheet-btn + .avatar-sheet-btn { border-top: .5px solid #E5E5E5; }
.avatar-sheet-cancel { margin-top: 8px; background: rgba(255,255,255,.98); border-radius: 14px; font-weight: 700; }
/* ===== 头像裁剪(移动和缩放) ===== */
.crop-screen { display: none; position: fixed; inset: 0; z-index: 300; background: #000; flex-direction: column; }
.crop-screen.open { display: flex; }
.crop-header { height: 48px; flex-shrink: 0; display: flex; align-items: center; justify-content: space-between; padding: 0 16px; }
.crop-header-btn { background: none; border: none; color: #fff; font-size: 16px; font-weight: 400; cursor: pointer; font-family: inherit; padding: 8px 0; }
.crop-header-btn.confirm { color: #FFD600; font-weight: 700; }
.crop-header-title { color: #fff; font-size: 16px; font-weight: 500; }
.crop-stage { flex: 1; position: relative; overflow: hidden; touch-action: none; }
.crop-stage img { position: absolute; left: 0; top: 0; transform-origin: 0 0; will-change: transform; -webkit-user-drag: none; user-select: none; pointer-events: none; }
.crop-mask { position: absolute; inset: 0; pointer-events: none; }
.crop-tips { flex-shrink: 0; text-align: center; color: rgba(255,255,255,.7); font-size: 13px; padding: 14px 0 18px; }
/* toast */
.set-toast { position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); background: rgba(0,0,0,.78); color: #FFFFFF; padding: 12px 20px; border-radius: 10px; font-size: 14px; line-height: 1.5; max-width: 70%; text-align: center; z-index: 320; opacity: 0; pointer-events: none; transition: opacity .2s; }
.set-toast.show { opacity: 1; }
/* 忙碌遮罩(上传中) */
.busy-mask { display: none; position: fixed; inset: 0; z-index: 310; background: rgba(0,0,0,.2); align-items: center; justify-content: center; }
.busy-mask.open { display: flex; }
.busy-spin { width: 38px; height: 38px; border: 3px solid rgba(255,255,255,.5); border-top-color: #FFD600; border-radius: 50%; animation: busySpin .8s linear infinite; }
@keyframes busySpin { to { transform: rotate(360deg); } }
</style>
</head>
<body>
<div class="device">
<div class="set-screen">
<div class="set-header">
<button class="set-back" onclick="onBack()" aria-label="返回">
<svg viewBox="0 0 24 24" fill="none"><path d="M15 6L9 12L15 18" stroke="#1A1A1A" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<div class="set-title" id="setTitle">设置</div>
</div>
<!-- ===== 主面板 ===== -->
<div class="set-body" id="panel-main">
<div class="set-scroll">
<div class="set-card">
<div class="set-row" onclick="openAvatarSheet()">
<span class="set-row-label">头像</span>
<div class="set-row-right">
<span class="set-avatar-thumb" id="avatarThumb"></span>
<span class="set-chevron"><svg viewBox="0 0 8 14" fill="none"><path d="M1 1L7 7L1 13" stroke="#C7C7CC" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
</div>
</div>
<div class="set-divider"></div>
<div class="set-row" onclick="openNickname()">
<span class="set-row-label">昵称</span>
<div class="set-row-right"><span class="set-value" id="nicknameVal"></span><span class="set-chevron"><svg viewBox="0 0 8 14" fill="none"><path d="M1 1L7 7L1 13" stroke="#C7C7CC" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg></span></div>
</div>
<div class="set-divider"></div>
<div class="set-row" onclick="showPanel('phone')">
<span class="set-row-label">手机号</span>
<div class="set-row-right"><span class="set-value" id="phoneVal"></span><span class="set-chevron"><svg viewBox="0 0 8 14" fill="none"><path d="M1 1L7 7L1 13" stroke="#C7C7CC" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg></span></div>
</div>
</div>
<div class="set-card">
<div class="set-row" onclick="nav('notificationSettings')">
<span class="set-row-label">通知设置</span>
<div class="set-row-right"><span class="set-chevron"><svg viewBox="0 0 8 14" fill="none"><path d="M1 1L7 7L1 13" stroke="#C7C7CC" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg></span></div>
</div>
</div>
<div class="set-card">
<div class="set-row" onclick="showPanel('about')">
<span class="set-row-label">关于傻瓜比价</span>
<div class="set-row-right"><span class="set-chevron"><svg viewBox="0 0 8 14" fill="none"><path d="M1 1L7 7L1 13" stroke="#C7C7CC" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg></span></div>
</div>
</div>
</div>
<div class="set-bottom-group">
<div class="set-card">
<div class="set-row set-logout-row" onclick="openLogout()"><span class="set-row-label">退出登录</span></div>
</div>
</div>
</div>
<!-- ===== 手机号 ===== -->
<div class="set-body" id="panel-phone" hidden>
<div class="set-scroll">
<div class="set-card">
<div class="set-row set-static"><span class="set-row-label">手机号</span><div class="set-row-right"><span class="set-value" id="phoneVal2"></span></div></div>
</div>
</div>
<div class="set-bottom-group"><span class="set-delete" onclick="openDelete()">注销账号</span></div>
</div>
<!-- ===== 关于傻瓜比价 ===== -->
<div class="set-body" id="panel-about" hidden>
<div class="set-scroll">
<div class="set-about-hero">
<div class="set-about-brand">
<img class="set-about-logo" src="assets/shared/logos/sb-brand.png" alt="傻瓜比价">
<span class="set-about-name">傻瓜比价</span>
</div>
<div class="set-about-version" id="aboutVersion">version</div>
</div>
<div class="set-card">
<div class="set-row" onclick="nav('checkUpdate')"><span class="set-row-label">检查更新</span><div class="set-row-right"><span class="set-value" id="updVersion"></span></div></div>
</div>
<div class="set-card">
<div class="set-row" onclick="showPanel('agreements')"><span class="set-row-label">相关协议</span><div class="set-row-right"><span class="set-chevron"><svg viewBox="0 0 8 14" fill="none"><path d="M1 1L7 7L1 13" stroke="#C7C7CC" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg></span></div></div>
<div class="set-divider"></div>
<div class="set-row" onclick="nav('permissions')"><span class="set-row-label">权限管理</span><div class="set-row-right"><span class="set-chevron"><svg viewBox="0 0 8 14" fill="none"><path d="M1 1L7 7L1 13" stroke="#C7C7CC" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg></span></div></div>
<div class="set-divider"></div>
<div class="set-row" onclick="showPanel('ads')"><span class="set-row-label">个性化广告</span><div class="set-row-right"><span class="set-chevron"><svg viewBox="0 0 8 14" fill="none"><path d="M1 1L7 7L1 13" stroke="#C7C7CC" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg></span></div></div>
<div class="set-divider"></div>
<div class="set-row" onclick="nav('devSettings')"><span class="set-row-label">开发设置</span><div class="set-row-right"><span class="set-chevron"><svg viewBox="0 0 8 14" fill="none"><path d="M1 1L7 7L1 13" stroke="#C7C7CC" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg></span></div></div>
</div>
</div>
</div>
<!-- ===== 相关协议 ===== -->
<div class="set-body" id="panel-agreements" hidden>
<div class="set-scroll">
<div class="set-card">
<div class="set-row" onclick="nav('agreement:user')"><span class="set-row-label">用户服务协议</span><div class="set-row-right"><span class="set-chevron"><svg viewBox="0 0 8 14" fill="none"><path d="M1 1L7 7L1 13" stroke="#C7C7CC" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg></span></div></div>
<div class="set-divider"></div>
<div class="set-row" onclick="nav('agreement:privacy')"><span class="set-row-label">隐私政策</span><div class="set-row-right"><span class="set-chevron"><svg viewBox="0 0 8 14" fill="none"><path d="M1 1L7 7L1 13" stroke="#C7C7CC" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg></span></div></div>
</div>
</div>
</div>
<!-- ===== 个性化广告(假开关,纯 H5) ===== -->
<div class="set-body" id="panel-ads" hidden>
<div class="set-scroll">
<div class="set-card">
<div class="set-switch-row">
<div class="set-switch-text">
<div class="set-switch-label">个性化广告推荐</div>
<div class="set-switch-desc">关闭个性化推荐不会减少广告数量,但广告与你的相关性可能降低。</div>
</div>
<div class="set-switch on" id="adsSwitch" onclick="toggleAds()"><span class="set-switch-knob"></span></div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 头像底部 sheet -->
<div class="avatar-sheet-mask" id="avatarSheetMask" onclick="closeAvatarSheet()"></div>
<div class="avatar-sheet" id="avatarSheet">
<div class="avatar-sheet-group">
<div class="avatar-sheet-grip"></div>
<button class="avatar-sheet-btn" onclick="pickAvatar(true)">拍照</button>
<button class="avatar-sheet-btn" onclick="pickAvatar(false)">从相册中选择</button>
</div>
<button class="avatar-sheet-btn avatar-sheet-cancel" onclick="closeAvatarSheet()">取消</button>
</div>
<input type="file" id="avCam" accept="image/*" capture="environment" style="display:none">
<input type="file" id="avAlbum" accept="image/*" style="display:none">
<!-- 头像裁剪(移动和缩放) -->
<div class="crop-screen" id="cropScreen">
<div class="crop-header">
<button class="crop-header-btn" onclick="closeCrop()">取消</button>
<span class="crop-header-title">移动和缩放</span>
<button class="crop-header-btn confirm" onclick="confirmCrop()">选取</button>
</div>
<div class="crop-stage" id="cropStage">
<img id="cropImg" alt="">
<canvas class="crop-mask" id="cropMask"></canvas>
</div>
<div class="crop-tips">捏合手势可缩放,拖动可调整位置</div>
</div>
<!-- 昵称编辑弹窗 -->
<div class="nick-modal" id="nickModal" onclick="closeNickname(event)">
<div class="nick-modal-card" onclick="event.stopPropagation()">
<div class="nick-modal-title">修改昵称</div>
<input type="text" id="nickInput" class="nick-modal-input" maxlength="20" placeholder="请输入昵称" oninput="updateNickCount()">
<div id="nickCount" class="nick-modal-count">0/20</div>
<div class="nick-modal-actions">
<button class="nick-modal-btn cancel" onclick="closeNickname()">取消</button>
<button class="nick-modal-btn confirm" onclick="saveNickname()">保存</button>
</div>
</div>
</div>
<!-- 退出确认 -->
<div class="cfm-mask" id="logoutModal">
<div class="cfm-card">
<div class="cfm-title">退出登录</div>
<div class="cfm-msg">退出后将无法继续累积金币收益</div>
<div class="cfm-actions">
<button class="cfm-btn outline" onclick="doLogout()">退出</button>
<button class="cfm-btn outline" onclick="closeLogout()">取消</button>
</div>
</div>
</div>
<!-- 注销确认 -->
<div class="cfm-mask" id="deleteModal">
<div class="cfm-card">
<div class="cfm-warn">⚠️</div>
<div class="cfm-title danger">确认注销账号?</div>
<div class="cfm-msg tight">注销后以下数据将<span class="danger">永久删除且无法恢复</span></div>
<div class="cfm-databox">· 全部金币余额和现金收益<br>· 比价记录和购买战绩<br>· 邀请好友关系和返佣收益</div>
<div class="cfm-actions">
<button class="cfm-btn outline" onclick="doDelete()">确认注销</button>
<button class="cfm-btn cta" onclick="closeDelete()">取消</button>
</div>
</div>
</div>
<div class="busy-mask" id="busyMask"><div class="busy-spin"></div></div>
<div class="set-toast" id="toast"></div>
<script src="../shared/bridge.js"></script>
<script src="../shared/api.js"></script>
<script>
(function () {
var DEFAULT_AVATAR = '<svg viewBox="0 0 80 80" fill="none"><defs><radialGradient id="sgAvBg" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(31 24) rotate(58) scale(52)"><stop offset="0" stop-color="#FFF5B5"/><stop offset=".58" stop-color="#FFD45F"/><stop offset="1" stop-color="#FFB43A"/></radialGradient><linearGradient id="sgAvFace" x1="39" y1="25" x2="39" y2="55" gradientUnits="userSpaceOnUse"><stop stop-color="#FFE0B0"/><stop offset="1" stop-color="#FFC783"/></linearGradient><linearGradient id="sgAvHood" x1="40" y1="45" x2="40" y2="78" gradientUnits="userSpaceOnUse"><stop stop-color="#FFD742"/><stop offset="1" stop-color="#FFA923"/></linearGradient></defs><circle cx="40" cy="40" r="40" fill="url(#sgAvBg)"/><path d="M15 72c2.5-13 12.2-21 25-21s22.5 8 25 21H15z" fill="url(#sgAvHood)"/><path d="M26 62c2.8 4.8 7.4 7.4 14 7.4 6.5 0 11.2-2.6 14-7.4-2.6-7.2-7.2-10.8-14-10.8S28.6 54.8 26 62z" fill="#FFE789" opacity=".8"/><path d="M20.5 37.5c0-13.2 8.3-22.2 20.3-22.2 11.7 0 19.7 8.8 19.7 22.1 0 2.4-.3 4.6-.8 6.8H21.3c-.5-2.2-.8-4.5-.8-6.7z" fill="#3B2416"/><path d="M23.5 36.3c.8-11.4 7.7-18 17.1-18 9.8 0 16.4 6.9 16.8 18.3-3.3-5.2-8.1-7.7-14.4-7.5-3.1.1-5.2 1.6-8 1.6-3.8 0-7.7 1.5-11.5 5.6z" fill="#5B3520"/><path d="M22.8 39.6c-2.9.1-5.2 2.5-5.1 5.7.1 3.1 2.4 5.3 5.5 5.4l.5-11.1h-.9z" fill="#FFC783"/><path d="M57.2 39.6c2.9.1 5.2 2.5 5.1 5.7-.1 3.1-2.4 5.3-5.5 5.4l-.5-11.1h.9z" fill="#FFC783"/><path d="M22.5 38.4c0-10.5 7.2-17.7 17.5-17.7s17.5 7.2 17.5 17.7v5.8c0 10.2-7.2 17.2-17.5 17.2s-17.5-7-17.5-17.2v-5.8z" fill="url(#sgAvFace)"/><path d="M22.7 35.8c2.5-8.7 9.7-13.2 18-13.2 7.7 0 13.7 4.4 16.1 11.9-4.8-.3-8.5-2.5-11-6.5-3.2 4.7-10 7.8-23.1 7.8z" fill="#3B2416"/><circle cx="31.3" cy="42.5" r="2.1" fill="#3A2113"/><circle cx="48.7" cy="42.5" r="2.1" fill="#3A2113"/><circle cx="28.2" cy="48.4" r="3.2" fill="#FF916C" opacity=".54"/><circle cx="51.8" cy="48.4" r="3.2" fill="#FF916C" opacity=".54"/><path d="M35.8 50.8c2.7 2.1 5.7 2.1 8.4 0" stroke="#6B2C19" stroke-width="1.8" stroke-linecap="round"/><path d="M27 34.2c1.8-3.2 3.9-5.7 6.3-7.3-1.7 3.3-2.5 6.6-2.4 9.8-1.2-.8-2.5-1.6-3.9-2.5zM39.5 22c-4.1 1.7-7.2 4.1-9.4 7.3 5.8-.1 10.5-1.6 14-4.5L39.5 22z" fill="#2A170E"/><path d="M33 63.7c3.7 1.8 10.3 1.8 14 0" stroke="#FFF2B3" stroke-width="2.2" stroke-linecap="round" opacity=".75"/></svg>';
var toastEl = document.getElementById('toast'), toastTimer = null;
function showToast(m) { toastEl.textContent = m; toastEl.classList.add('show'); if (toastTimer) clearTimeout(toastTimer); toastTimer = setTimeout(function () { toastEl.classList.remove('show'); }, 1600); }
function escAttr(s) { return String(s == null ? '' : s).replace(/"/g, '&quot;').replace(/</g, '&lt;'); }
function bridgeReady() { return window.SGBridge && SGBridge.hasNative && typeof SGBridge.getToken === 'function'; }
function busy(on) { document.getElementById('busyMask').classList.toggle('open', !!on); }
// ===== 面板切换 =====
var PARENT = { main: null, phone: 'main', about: 'main', agreements: 'about', ads: 'about' };
var TITLE = { main: '设置', phone: '手机号', about: '', agreements: '相关协议', ads: '个性化广告' };
var curPanel = 'main';
function showPanel(name) {
['main', 'phone', 'about', 'agreements', 'ads'].forEach(function (p) { document.getElementById('panel-' + p).hidden = (p !== name); });
document.getElementById('setTitle').textContent = TITLE[name] || '';
curPanel = name;
}
window.showPanel = showPanel;
// 返回:裁剪>弹窗>面板>退出,逐级
window.onBack = function () {
if (document.getElementById('cropScreen').classList.contains('open')) { closeCrop(); return; }
if (document.getElementById('avatarSheet').classList.contains('open')) { closeAvatarSheet(); return; }
var modals = ['nickModal', 'logoutModal', 'deleteModal'];
for (var i = 0; i < modals.length; i++) { var m = document.getElementById(modals[i]); if (m && m.classList.contains('open')) { m.classList.remove('open'); return; } }
var p = PARENT[curPanel];
if (p) { showPanel(p); return; }
if (window.SGBridge && SGBridge.closePage) { SGBridge.closePage(); } else if (history.length > 1) { history.back(); }
};
// ===== 跳原生 =====
window.nav = function (route) {
if (route === 'permissions' || route === 'devSettings' || route.indexOf('agreement:') === 0) {
try { localStorage.setItem('sg_set_panel', curPanel); } catch (e) {}
}
if (window.SGBridge && SGBridge.navigate) { SGBridge.navigate(route); } else { console.log('[nav]', route); }
};
// ===== 用户资料 =====
function loadProfile() {
var a = (window.SGBridge && SGBridge.getAuthState) ? (SGBridge.getAuthState() || {}) : {};
document.getElementById('nicknameVal').textContent = a.nickname || '傻瓜用户';
var phone = a.phone || '未绑定';
document.getElementById('phoneVal').textContent = phone;
document.getElementById('phoneVal2').textContent = phone;
setAvatar(a.avatarUrl);
var ver = (window.SGBridge && SGBridge.getAppVersion) ? (SGBridge.getAppVersion() || '') : '';
ver = ver || '1.0.0';
document.getElementById('aboutVersion').textContent = 'version v' + ver;
document.getElementById('updVersion').textContent = 'v' + ver;
}
function setAvatar(url) {
var el = document.getElementById('avatarThumb');
// 任意非空 url 都用 <img>http(s) 绝对、/media 同源相对、blob: 本地预览均可
if (url) { el.innerHTML = '<img src="' + escAttr(url) + '" alt="头像">'; }
else { el.innerHTML = DEFAULT_AVATAR; }
}
// ===== 头像 sheet =====
window.openAvatarSheet = function () { document.getElementById('avatarSheetMask').classList.add('open'); document.getElementById('avatarSheet').classList.add('open'); };
window.closeAvatarSheet = function () { document.getElementById('avatarSheetMask').classList.remove('open'); document.getElementById('avatarSheet').classList.remove('open'); };
window.pickAvatar = function (useCamera) { closeAvatarSheet(); document.getElementById(useCamera ? 'avCam' : 'avAlbum').click(); };
['avCam', 'avAlbum'].forEach(function (id) {
document.getElementById(id).addEventListener('change', function () {
var f = this.files && this.files[0]; this.value = '';
if (f && /^image\//.test(f.type)) openCrop(f);
});
});
// ===== 裁剪(移动和缩放)=====
var crop = { img: null, natW: 0, natH: 0, base: 1, scale: 1, x: 0, y: 0, D: 0, cx: 0, cy: 0, pts: {}, startDist: 0, startScale: 1, midX: 0, midY: 0, file: null };
var cropImgEl = document.getElementById('cropImg'), cropStage = document.getElementById('cropStage'), cropMask = document.getElementById('cropMask');
function openCrop(file) {
crop.file = file;
var url = URL.createObjectURL(file);
var im = new Image();
im.onload = function () {
crop.img = im; crop.natW = im.naturalWidth; crop.natH = im.naturalHeight;
document.getElementById('cropScreen').classList.add('open');
// 等布局生效后按 stage 实际尺寸初始化
setTimeout(function () { initCrop(); }, 30);
};
im.onerror = function () { URL.revokeObjectURL(url); showToast('图片读取失败'); };
cropImgEl.src = url; im.src = url;
}
function initCrop() {
var sw = cropStage.clientWidth, sh = cropStage.clientHeight;
crop.D = Math.min(sw, sh) * 0.86;
crop.cx = (sw - crop.D) / 2; crop.cy = (sh - crop.D) / 2;
crop.base = Math.max(crop.D / crop.natW, crop.D / crop.natH); // cover 裁剪框
crop.scale = crop.base;
crop.x = (sw - crop.natW * crop.scale) / 2;
crop.y = (sh - crop.natH * crop.scale) / 2;
drawMaskRing(sw, sh);
applyCrop();
}
function drawMaskRing(sw, sh) {
var dpr = window.devicePixelRatio || 1;
cropMask.width = sw * dpr; cropMask.height = sh * dpr; cropMask.style.width = sw + 'px'; cropMask.style.height = sh + 'px';
var ctx = cropMask.getContext('2d'); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); ctx.clearRect(0, 0, sw, sh);
ctx.fillStyle = 'rgba(0,0,0,.55)'; ctx.fillRect(0, 0, sw, sh);
ctx.globalCompositeOperation = 'destination-out';
ctx.beginPath(); ctx.arc(crop.cx + crop.D / 2, crop.cy + crop.D / 2, crop.D / 2, 0, Math.PI * 2); ctx.fill();
ctx.globalCompositeOperation = 'source-over';
ctx.strokeStyle = 'rgba(255,255,255,.9)'; ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.arc(crop.cx + crop.D / 2, crop.cy + crop.D / 2, crop.D / 2, 0, Math.PI * 2); ctx.stroke();
}
function clampCrop() {
var w = crop.natW * crop.scale, h = crop.natH * crop.scale;
if (crop.x > crop.cx) crop.x = crop.cx;
if (crop.x + w < crop.cx + crop.D) crop.x = crop.cx + crop.D - w;
if (crop.y > crop.cy) crop.y = crop.cy;
if (crop.y + h < crop.cy + crop.D) crop.y = crop.cy + crop.D - h;
}
function applyCrop() { clampCrop(); cropImgEl.style.transform = 'translate(' + crop.x + 'px,' + crop.y + 'px) scale(' + crop.scale + ')'; }
cropStage.addEventListener('pointerdown', function (e) { cropStage.setPointerCapture(e.pointerId); crop.pts[e.pointerId] = { x: e.clientX, y: e.clientY }; var ids = Object.keys(crop.pts); if (ids.length === 2) { var a = crop.pts[ids[0]], b = crop.pts[ids[1]]; crop.startDist = Math.hypot(a.x - b.x, a.y - b.y); crop.startScale = crop.scale; var r = cropStage.getBoundingClientRect(); crop.midX = (a.x + b.x) / 2 - r.left; crop.midY = (a.y + b.y) / 2 - r.top; } });
cropStage.addEventListener('pointermove', function (e) {
if (!crop.pts[e.pointerId]) return;
var prev = crop.pts[e.pointerId]; crop.pts[e.pointerId] = { x: e.clientX, y: e.clientY };
var ids = Object.keys(crop.pts);
if (ids.length === 1) { crop.x += e.clientX - prev.x; crop.y += e.clientY - prev.y; applyCrop(); }
else if (ids.length === 2) {
var a = crop.pts[ids[0]], b = crop.pts[ids[1]]; var dist = Math.hypot(a.x - b.x, a.y - b.y);
if (crop.startDist > 0) {
var ns = Math.max(crop.base, Math.min(crop.base * 5, crop.startScale * dist / crop.startDist));
// 以双指中点为锚缩放
var k = ns / crop.scale;
crop.x = crop.midX - (crop.midX - crop.x) * k;
crop.y = crop.midY - (crop.midY - crop.y) * k;
crop.scale = ns; applyCrop();
}
}
});
function endPt(e) { delete crop.pts[e.pointerId]; var ids = Object.keys(crop.pts); if (ids.length === 1) { } else if (ids.length === 0) { crop.startDist = 0; } }
cropStage.addEventListener('pointerup', endPt);
cropStage.addEventListener('pointercancel', endPt);
window.closeCrop = function () { document.getElementById('cropScreen').classList.remove('open'); if (crop.img) { try { URL.revokeObjectURL(crop.img.src); } catch (e) {} } crop.img = null; crop.pts = {}; };
window.confirmCrop = function () {
if (!crop.img) return;
var out = 512, cv = document.createElement('canvas'); cv.width = out; cv.height = out;
var sx = (crop.cx - crop.x) / crop.scale, sy = (crop.cy - crop.y) / crop.scale, sd = crop.D / crop.scale;
try { cv.getContext('2d').drawImage(crop.img, sx, sy, sd, sd, 0, 0, out, out); } catch (e) { showToast('裁剪失败'); return; }
cv.toBlob(function (blob) { document.getElementById('cropScreen').classList.remove('open'); uploadAvatar(blob || crop.file); }, 'image/jpeg', 0.9);
};
// ===== 上传头像 =====
function uploadAvatar(blob) {
var thumb = document.getElementById('avatarThumb');
var prevHtml = thumb.innerHTML;
setAvatar(URL.createObjectURL(blob)); // 乐观:裁剪完立即显示本地图,不等服务端(修「重进才变」)
if (!bridgeReady()) { showToast('(预览)头像已更新'); return; }
busy(true);
(async function () {
try {
var fd = new FormData(); fd.append('file', blob, 'avatar.jpg');
var res = await fetch('/api/v1/user/avatar', { method: 'POST', headers: { 'Authorization': 'Bearer ' + SGBridge.getToken() }, body: fd });
if (res.ok) {
showToast('头像已更新'); // 本地图已显示,保持不动(避免再拉服务端图闪一下)
if (SGBridge.navigate) SGBridge.navigate('profileChanged'); // 同步原生缓存(我的页一致)
} else if (res.status === 401) { thumb.innerHTML = prevHtml; if (SGBridge.requestLogin) SGBridge.requestLogin(); }
else { thumb.innerHTML = prevHtml; var d = ''; try { var e = await res.json(); d = (e && e.detail) ? e.detail : ''; } catch (x) {} showToast(d || '头像上传失败,请重试'); }
} catch (e) { thumb.innerHTML = prevHtml; showToast('网络异常,请重试'); } finally { busy(false); if (crop.img) { try { URL.revokeObjectURL(crop.img.src); } catch (e) {} crop.img = null; } }
})();
}
// ===== 昵称 =====
window.openNickname = function () { var v = document.getElementById('nicknameVal').textContent || ''; document.getElementById('nickInput').value = v; updateNickCount(); document.getElementById('nickModal').classList.add('open'); setTimeout(function () { document.getElementById('nickInput').focus(); }, 60); };
window.closeNickname = function (ev) { if (ev && ev.target && ev.target.id !== 'nickModal') return; document.getElementById('nickModal').classList.remove('open'); };
window.updateNickCount = function () { var v = document.getElementById('nickInput').value || ''; document.getElementById('nickCount').textContent = v.length + '/20'; };
window.saveNickname = function () {
var name = (document.getElementById('nickInput').value || '').trim();
if (!name) { showToast('昵称不能为空'); return; }
if (!bridgeReady()) { document.getElementById('nicknameVal').textContent = name; document.getElementById('nickModal').classList.remove('open'); showToast('昵称已更新'); return; }
(async function () {
try {
var res = await fetch('/api/v1/user/profile', { method: 'PATCH', headers: { 'Authorization': 'Bearer ' + SGBridge.getToken(), 'Content-Type': 'application/json' }, body: JSON.stringify({ nickname: name }) });
if (res.ok) {
var j = null; try { j = await res.json(); } catch (e) {}
document.getElementById('nicknameVal').textContent = (j && j.nickname) || name;
document.getElementById('nickModal').classList.remove('open');
showToast('昵称已更新');
if (SGBridge.navigate) SGBridge.navigate('profileChanged');
} else if (res.status === 401) { if (SGBridge.requestLogin) SGBridge.requestLogin(); }
else { var d = ''; try { var e = await res.json(); d = (e && e.detail) ? e.detail : ''; } catch (x) {} showToast(d || '更新失败,请重试'); }
} catch (e) { showToast('网络异常,请重试'); }
})();
};
// ===== 退出 / 注销 =====
window.openLogout = function () { document.getElementById('logoutModal').classList.add('open'); };
window.closeLogout = function () { document.getElementById('logoutModal').classList.remove('open'); };
window.doLogout = function () { closeLogout(); if (window.SGBridge && SGBridge.navigate) { SGBridge.navigate('logout'); } else { showToast('(预览)已退出'); } };
window.openDelete = function () { document.getElementById('deleteModal').classList.add('open'); };
window.closeDelete = function () { document.getElementById('deleteModal').classList.remove('open'); };
window.doDelete = function () {
if (!bridgeReady()) { closeDelete(); showToast('(预览)已注销'); return; }
closeDelete();
(async function () {
try {
var res = await fetch('/api/v1/user', { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + SGBridge.getToken() } });
if (res.ok) { if (SGBridge.navigate) SGBridge.navigate('logout'); }
else if (res.status === 401) { if (SGBridge.requestLogin) SGBridge.requestLogin(); }
else { var d = ''; try { var e = await res.json(); d = (e && e.detail) ? e.detail : ''; } catch (x) {} showToast(d || '注销失败,请重试'); }
} catch (e) { showToast('网络异常,请重试'); }
})();
};
window.toggleAds = function () { document.getElementById('adsSwitch').classList.toggle('on'); };
// ===== init =====
(function () { var p = 'main'; try { var sp = localStorage.getItem('sg_set_panel'); if (sp) { p = sp; localStorage.removeItem('sg_set_panel'); } } catch (e) {} showPanel(p); })();
loadProfile();
if (window.SGBridge && SGBridge.on) { SGBridge.on('onResume', loadProfile); SGBridge.on('onAuthChange', loadProfile); }
})();
</script>
</body>
</html>