338 行
19 KiB
PHP
338 行
19 KiB
PHP
<?php
|
||
require __DIR__ . '/includes/init.php';
|
||
requireLogin('settings.php');
|
||
$pageTitle = '用户设置';
|
||
$stmt = db()->prepare('SELECT id, username, email, nickname, verified, is_admin, status, avatar FROM ' . tn('users') . ' WHERE id = ?');
|
||
$stmt->execute([$_SESSION['user_id']]);
|
||
$user = $stmt->fetch();
|
||
$msg = '';
|
||
$msgOk = false;
|
||
$activeTab = 'profile';
|
||
if (isset($_POST['save_profile'])) {
|
||
verifyCsrf();
|
||
$activeTab = 'profile';
|
||
$u = trim($_POST['username'] ?? '');
|
||
$em = trim($_POST['email'] ?? '');
|
||
$nk = trim($_POST['nickname'] ?? '');
|
||
if (mb_strlen($u) < 3) $msg = '用户名至少 3 个字符';
|
||
elseif (!filter_var($em, FILTER_VALIDATE_EMAIL)) $msg = '邮箱格式不正确';
|
||
elseif ($nk !== '' && mb_strlen($nk) > 30) $msg = '昵称过长(最多 30 字)';
|
||
if ($msg === '') {
|
||
$chk = db()->prepare('SELECT id FROM ' . tn('users') . ' WHERE username = ? AND id <> ?');
|
||
$chk->execute([$u, $user['id']]);
|
||
if ($chk->fetch()) {
|
||
$msg = '该用户名已被占用';
|
||
} else {
|
||
$emailChanged = ($em !== $user['email']);
|
||
db()->prepare('UPDATE ' . tn('users') . ' SET username = ?, email = ?, nickname = ? WHERE id = ?')
|
||
->execute([$u, $em, $nk, $user['id']]);
|
||
if ($emailChanged && !empty($user['verified'])) {
|
||
db()->prepare('UPDATE ' . tn('users') . ' SET verified = 0 WHERE id = ?')->execute([$user['id']]);
|
||
}
|
||
$stmt->execute([$user['id']]);
|
||
$user = $stmt->fetch();
|
||
$msg = $emailChanged ? '资料已保存,邮箱已变更需重新验证' : '资料已保存';
|
||
$msgOk = true;
|
||
}
|
||
}
|
||
}
|
||
if (isset($_POST['change_password'])) {
|
||
verifyCsrf();
|
||
$activeTab = 'password';
|
||
$cur = $_POST['current_password'] ?? '';
|
||
$pw = $_POST['new_password'] ?? '';
|
||
$pw2 = $_POST['new_password2'] ?? '';
|
||
if ($cur === '' || $pw === '' || $pw2 === '') {
|
||
$msg = '请填写完整密码信息';
|
||
} elseif (!password_verify($cur, $user['password'])) {
|
||
$msg = '当前密码不正确';
|
||
} elseif (strlen($pw) < 6) {
|
||
$msg = '新密码至少 6 位';
|
||
} elseif ($pw !== $pw2) {
|
||
$msg = '两次输入的新密码不一致';
|
||
} else {
|
||
db()->prepare('UPDATE ' . tn('users') . ' SET password = ? WHERE id = ?')
|
||
->execute([password_hash($pw, PASSWORD_DEFAULT), $user['id']]);
|
||
$msg = '密码修改成功,请使用新密码登录';
|
||
$msgOk = true;
|
||
}
|
||
}
|
||
if (isset($_POST['save_notify'])) {
|
||
verifyCsrf();
|
||
$activeTab = 'notify';
|
||
saveNotifyPrefs($user['id'], [
|
||
'email_enabled' => $_POST['email_enabled'] ?? 0,
|
||
'order_shipped' => $_POST['order_shipped'] ?? 0,
|
||
'expiry_reminder' => $_POST['expiry_reminder'] ?? 0,
|
||
'promotions' => $_POST['promotions'] ?? 0,
|
||
]);
|
||
$msg = '通知偏好已保存';
|
||
$msgOk = true;
|
||
}
|
||
$notify = getNotifyPrefs($user['id']);
|
||
$realname = getUserRealname($user['id']);
|
||
$realnameVerified = $realname !== null && (int) $realname['status'] === 1;
|
||
$vapidPublic = getSetting('pwa_vapid_public', '');
|
||
require 'includes/header.php';
|
||
?>
|
||
<section class="section">
|
||
<h2 class="sec-title"><i class="fas fa-gear"></i> 用户设置</h2>
|
||
<?php if ($msg): ?>
|
||
<p class="<?= $msgOk ? 'banner-ok' : 'form-err' ?>"><?= h($msg) ?></p>
|
||
<?php endif; ?>
|
||
<div class="settings-tabs">
|
||
<button class="stab <?= $activeTab==='profile'?'active':'' ?>" data-tab="profile">资料</button>
|
||
<button class="stab <?= $activeTab==='password'?'active':'' ?>" data-tab="password">密码</button>
|
||
<button class="stab <?= $activeTab==='realname'?'active':'' ?>" data-tab="realname">实名认证</button>
|
||
<button class="stab <?= $activeTab==='notify'?'active':'' ?>" data-tab="notify">通知偏好</button>
|
||
<button class="stab <?= $activeTab==='pwa'?'active':'' ?>" data-tab="pwa">桌面通知</button>
|
||
</div>
|
||
<!-- 资料 -->
|
||
<div class="spanel" id="panel-profile" style="<?= $activeTab==='profile'?'':'display:none' ?>">
|
||
<form method="post" action="" class="grid-form settings-form">
|
||
<?= csrfField() ?>
|
||
<div class="span2" style="margin-bottom:18px">
|
||
<h3 style="color:var(--primary);font-size:1.05rem;margin-bottom:12px"><i class="fas fa-camera"></i> 头像</h3>
|
||
<div style="display:flex;align-items:center;gap:16px;flex-wrap:wrap">
|
||
<div id="avatarPreviewWrap" style="width:80px;height:80px;border-radius:50%;overflow:hidden;background:var(--primary-container);display:flex;align-items:center;justify-content:center;border:3px solid var(--outline);flex-shrink:0">
|
||
<?php if (!empty($user['avatar'])): ?>
|
||
<img src="<?= h($user['avatar']) ?>" alt="" style="width:100%;height:100%;object-fit:cover">
|
||
<?php else: ?>
|
||
<span style="font-size:1.8rem;font-weight:700;color:var(--on-primary-container)"><?= h(avatarText($user['nickname'] ?: $user['username'])) ?></span>
|
||
<?php endif; ?>
|
||
</div>
|
||
<div>
|
||
<label for="avatarInput" class="btn btn-sm btn-ghost" style="cursor:pointer;display:inline-flex;align-items:center;gap:6px">
|
||
<i class="fas fa-upload"></i> 选择图片
|
||
</label>
|
||
<input type="file" id="avatarInput" accept="image/jpeg,image/png,image/webp,image/gif" style="display:none">
|
||
<p style="font-size:.8rem;color:var(--muted);margin-top:4px">支持 JPG/PNG/WebP/GIF,≤2MB</p>
|
||
</div>
|
||
</div>
|
||
<p id="avatarMsg" style="font-size:.88rem;margin-top:6px"></p>
|
||
</div>
|
||
<label>用户名<input type="text" name="username" value="<?= h($user['username']) ?>" required></label>
|
||
<label>邮箱<input type="email" name="email" value="<?= h($user['email']) ?>" required></label>
|
||
<label class="span2">昵称<input type="text" name="nickname" value="<?= h($user['nickname']) ?>" placeholder="选填,最多 30 字" maxlength="30"></label>
|
||
<div class="form-actions span2">
|
||
<button type="submit" name="save_profile" class="btn btn-primary"><i class="fas fa-save"></i> 保存资料</button>
|
||
<a href="myorders.php" class="btn btn-ghost">我的订单</a>
|
||
</div>
|
||
</form>
|
||
<div class="panel verify-panel" style="margin-top:18px">
|
||
<h3><i class="fas fa-envelope-circle-check"></i> 邮箱验证</h3>
|
||
<?php if (!empty($user['verified'])): ?>
|
||
<p class="ok-line"><i class="fas fa-circle-check"></i> 邮箱已验证</p>
|
||
<?php else: ?>
|
||
<p class="warn-line"><i class="fas fa-triangle-exclamation"></i> 邮箱未验证,<a href="verify.php?resend=1">点击重发验证邮件</a></p>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
<!-- 密码 -->
|
||
<div class="spanel" id="panel-password" style="<?= $activeTab==='password'?'':'display:none' ?>">
|
||
<form method="post" action="" class="grid-form" style="max-width:420px">
|
||
<?= csrfField() ?>
|
||
<label class="span2">当前密码<input type="password" name="current_password" autocomplete="current-password" required></label>
|
||
<label class="span2">新密码<input type="password" name="new_password" autocomplete="new-password" required></label>
|
||
<label class="span2">确认新密码<input type="password" name="new_password2" autocomplete="new-password" required></label>
|
||
<div class="form-actions span2">
|
||
<button type="submit" name="change_password" class="btn btn-primary"><i class="fas fa-key"></i> 修改密码</button>
|
||
<a href="login.php?mode=login" class="btn btn-ghost">重新登录</a>
|
||
</div>
|
||
</form>
|
||
<p class="muted" style="font-size:.85rem;max-width:420px">忘记密码?可在登录页点击「忘记密码」通过邮箱重置。</p>
|
||
</div>
|
||
<!-- 实名认证 -->
|
||
<div class="spanel" id="panel-realname" style="<?= $activeTab==='realname'?'':'display:none' ?>">
|
||
<div class="panel">
|
||
<h3><i class="fas fa-id-card"></i> 实名认证</h3>
|
||
<?php if ($realnameVerified): ?>
|
||
<p class="ok-line"><i class="fas fa-circle-check"></i> 你已完成实名认证(信息已加密保管)。</p>
|
||
<p class="muted" style="font-size:.85rem">姓名:<?= h(maskName(rnDecrypt($realname['real_name_enc']))) ?> 身份证号:<?= h(maskIdno(rnDecrypt($realname['idno_enc']))) ?></p>
|
||
<p style="font-size:.85rem">如需更换实名信息,<a href="realname.php">点此重新提交</a>。</p>
|
||
<?php else: ?>
|
||
<p class="warn-line"><i class="fas fa-triangle-exclamation"></i> 你尚未完成实名认证。部分商品(标注「需实名」)购买前必须完成实名。</p>
|
||
<a href="realname.php" class="btn btn-primary"><i class="fas fa-id-card"></i> 去实名认证</a>
|
||
<?php endif; ?>
|
||
<p class="muted" style="font-size:.82rem;margin-top:10px">实名信息仅用于核对「姓名 + 身份证号」是否合法,采用 AES 加密存储,严格保管,不会向第三方泄露。</p>
|
||
</div>
|
||
</div>
|
||
<!-- 通知偏好 -->
|
||
<div class="spanel" id="panel-notify" style="<?= $activeTab==='notify'?'':'display:none' ?>">
|
||
<form method="post" action="" class="notify-form">
|
||
<?= csrfField() ?>
|
||
<div class="notify-row">
|
||
<div><strong>接收邮件通知</strong><small>总开关,关闭后不再收到任何邮件</small></div>
|
||
<label class="switch"><input type="checkbox" name="email_enabled" value="1" <?= !empty($notify['email_enabled'])?'checked':'' ?>><span class="slider"></span></label>
|
||
</div>
|
||
<div class="notify-row">
|
||
<div><strong>订单/发货通知</strong><small>下单成功、商品发货等状态变更</small></div>
|
||
<label class="switch"><input type="checkbox" name="order_shipped" value="1" <?= !empty($notify['order_shipped'])?'checked':'' ?>><span class="slider"></span></label>
|
||
</div>
|
||
<div class="notify-row">
|
||
<div><strong>到期前提醒</strong><small>周期性商品到期前发送续费提醒</small></div>
|
||
<label class="switch"><input type="checkbox" name="expiry_reminder" value="1" <?= !empty($notify['expiry_reminder'])?'checked':'' ?>><span class="slider"></span></label>
|
||
</div>
|
||
<div class="notify-row">
|
||
<div><strong>营销/活动通知</strong><small>新品上架、优惠活动(默认关闭)</small></div>
|
||
<label class="switch"><input type="checkbox" name="promotions" value="1" <?= !empty($notify['promotions'])?'checked':'' ?>><span class="slider"></span></label>
|
||
</div>
|
||
<div class="form-actions">
|
||
<button type="submit" name="save_notify" class="btn btn-primary"><i class="fas fa-save"></i> 保存偏好</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<!-- 桌面通知 / PWA -->
|
||
<div class="spanel" id="panel-pwa" style="<?= $activeTab==='pwa'?'':'display:none' ?>">
|
||
<div class="panel">
|
||
<h3><i class="fas fa-bell"></i> 桌面通知(PWA)</h3>
|
||
<p>将本站「安装」到桌面/手机主屏,可像 App 一样打开;开启通知后,订单、到期等重要提醒将推送到你的设备。</p>
|
||
<div class="pwa-actions">
|
||
<button id="pwaInstallBtn" class="btn btn-primary" style="display:none"><i class="fas fa-download"></i> 安装到桌面</button>
|
||
<button id="pwaSubscribeBtn" class="btn btn-ghost"><i class="fas fa-bell"></i> 启用浏览器通知</button>
|
||
<button id="pwaUnsubscribeBtn" class="btn btn-ghost" style="display:none"><i class="fas fa-bell-slash"></i> 取消订阅</button>
|
||
</div>
|
||
<p id="pwaMsg" class="muted" style="font-size:.85rem;margin-top:8px"></p>
|
||
<?php if (empty($vapidPublic)): ?>
|
||
<p class="warn-line" style="font-size:.82rem"><i class="fas fa-circle-info"></i> 当前未配置推送 VAPID 公钥,仅支持「安装为应用」;如需推送,请在后台「设置」中填入 pwa_vapid_public。</p>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
<script>
|
||
(function () {
|
||
var btns = document.querySelectorAll('.stab');
|
||
btns.forEach(function (b) {
|
||
b.addEventListener('click', function () {
|
||
var t = b.getAttribute('data-tab');
|
||
btns.forEach(function (x) { x.classList.toggle('active', x === b); });
|
||
document.querySelectorAll('.spanel').forEach(function (p) {
|
||
p.style.display = (p.id === 'panel-' + t) ? '' : 'none';
|
||
});
|
||
});
|
||
});
|
||
})();
|
||
(function () {
|
||
var input = document.getElementById('avatarInput');
|
||
var msg = document.getElementById('avatarMsg');
|
||
var wrap = document.getElementById('avatarPreviewWrap');
|
||
if (!input) return;
|
||
input.addEventListener('change', function () {
|
||
if (!input.files || !input.files[0]) return;
|
||
var file = input.files[0];
|
||
if (file.size > 2 * 1024 * 1024) { msg.textContent = '图片过大(≤2MB)'; msg.style.color = 'var(--danger)'; return; }
|
||
var reader = new FileReader();
|
||
reader.onload = function (e) { wrap.innerHTML = '<img src="' + e.target.result + '" alt="" style="width:100%;height:100%;object-fit:cover;border-radius:50%">'; };
|
||
reader.readAsDataURL(file);
|
||
msg.textContent = '上传中…'; msg.style.color = 'var(--muted)';
|
||
var fd = new FormData();
|
||
fd.append('avatar', file);
|
||
fd.append('csrf_token', Fnw.csrf());
|
||
var xhr = new XMLHttpRequest();
|
||
xhr.open('POST', 'avatar_upload.php', true);
|
||
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
|
||
xhr.onload = function () {
|
||
try { var res = JSON.parse(xhr.responseText); } catch (e) { res = { ok:false, msg:'响应异常' }; }
|
||
if (res.ok) { msg.textContent = res.msg || '上传成功'; msg.style.color = 'var(--ok)'; updateAvatars(res.data.url); }
|
||
else { msg.textContent = res.msg || '上传失败'; msg.style.color = 'var(--danger)'; }
|
||
};
|
||
xhr.onerror = function () { msg.textContent = '网络错误'; msg.style.color = 'var(--danger)'; };
|
||
xhr.send(fd);
|
||
});
|
||
function updateAvatars(url) {
|
||
document.querySelectorAll('.avatar, .uc-avatar').forEach(function (el) {
|
||
el.innerHTML = '<img src="' + url + '" alt="" style="width:100%;height:100%;object-fit:cover;border-radius:50%">';
|
||
});
|
||
}
|
||
})();
|
||
(function () {
|
||
var installBtn = document.getElementById('pwaInstallBtn');
|
||
var subBtn = document.getElementById('pwaSubscribeBtn');
|
||
var unsubBtn = document.getElementById('pwaUnsubscribeBtn');
|
||
var pwaMsg = document.getElementById('pwaMsg');
|
||
var deferredPrompt = null;
|
||
window.addEventListener('beforeinstallprompt', function (e) {
|
||
e.preventDefault();
|
||
deferredPrompt = e;
|
||
installBtn.style.display = '';
|
||
});
|
||
if (installBtn) {
|
||
installBtn.addEventListener('click', function () {
|
||
if (!deferredPrompt) { pwaMsg.textContent = '当前浏览器不支持直接安装,可手动通过「菜单 → 添加到主屏幕」。'; return; }
|
||
deferredPrompt.prompt();
|
||
deferredPrompt.userChoice.then(function () { deferredPrompt = null; installBtn.style.display = 'none'; });
|
||
});
|
||
}
|
||
function regSw() {
|
||
if (!('serviceWorker' in navigator)) return Promise.reject('no-sw');
|
||
return navigator.serviceWorker.register('sw.js').catch(function () {});
|
||
}
|
||
function urlBase64ToUint8Array(base64String) {
|
||
var padding = '='.repeat((4 - base64String.length % 4) % 4);
|
||
var base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
|
||
var raw = atob(base64);
|
||
var arr = new Uint8Array(raw.length);
|
||
for (var i = 0; i < raw.length; i++) arr[i] = raw.charCodeAt(i);
|
||
return arr;
|
||
}
|
||
function postSub(action, sub) {
|
||
return fetch('push_subscribe.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
|
||
body: JSON.stringify({ csrf_token: Fnw.csrf(), action: action, subscription: sub })
|
||
}).then(function (r) { return r.json(); });
|
||
}
|
||
if (subBtn) {
|
||
subBtn.addEventListener('click', function () {
|
||
if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
|
||
pwaMsg.textContent = '当前浏览器不支持 Web 推送通知。'; return;
|
||
}
|
||
regSw().then(function () {
|
||
return navigator.serviceWorker.ready;
|
||
}).then(function (reg) {
|
||
return reg.pushManager.getSubscription().then(function (existing) {
|
||
if (existing) return existing;
|
||
var vapid = '<?= h($vapidPublic) ?>';
|
||
if (!vapid) { pwaMsg.textContent = '推送未配置(VAPID 公钥缺失),但仍可将本站安装为应用。'; throw 'no-vapid'; }
|
||
return reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(vapid) });
|
||
});
|
||
}).then(function (sub) {
|
||
if (!sub) return;
|
||
return postSub('subscribe', sub).then(function (res) {
|
||
if (res.ok) {
|
||
pwaMsg.textContent = '已启用浏览器通知,重要消息将推送到此设备。';
|
||
subBtn.style.display = 'none'; unsubBtn.style.display = '';
|
||
} else { pwaMsg.textContent = res.msg || '订阅失败'; }
|
||
});
|
||
}).catch(function (e) {
|
||
if (e === 'no-vapid') return;
|
||
pwaMsg.textContent = '订阅失败:' + (typeof e === 'string' ? e : '浏览器拒绝或网络异常');
|
||
});
|
||
});
|
||
}
|
||
if (unsubBtn) {
|
||
unsubBtn.addEventListener('click', function () {
|
||
regSw().then(function () { return navigator.serviceWorker.ready; }).then(function (reg) {
|
||
return reg.pushManager.getSubscription();
|
||
}).then(function (sub) {
|
||
if (!sub) { unsubBtn.style.display = 'none'; subBtn.style.display = ''; return; }
|
||
return sub.unsubscribe().then(function () {
|
||
return postSub('unsubscribe', sub);
|
||
});
|
||
}).then(function () {
|
||
pwaMsg.textContent = '已取消订阅。';
|
||
unsubBtn.style.display = 'none'; subBtn.style.display = '';
|
||
}).catch(function () { pwaMsg.textContent = '取消订阅失败'; });
|
||
});
|
||
}
|
||
if ('serviceWorker' in navigator && 'PushManager' in window) {
|
||
regSw().then(function () { return navigator.serviceWorker.ready; }).then(function (reg) {
|
||
return reg.pushManager.getSubscription();
|
||
}).then(function (sub) {
|
||
if (sub) { subBtn.style.display = 'none'; unsubBtn.style.display = ''; }
|
||
}).catch(function () {});
|
||
}
|
||
})();
|
||
</script>
|
||
<?php require 'includes/footer.php'; ?>
|