Files
training/website/src/services/auth.js
T
Pine 4419c9265f feat(user-center): 顶栏用户下拉 + 玻璃个人中心 + 我的报名/任务/园区/认证页
- Header: 登录后头像+用户名玻璃下拉(个人中心/我的报名/我的任务/我的园区/认证中心/退出);未登录显示登录/注册;移动端同步
- Profile: 玻璃用户卡(头像上传/身份标签) + 我的服务宫格(实时状态) + 报名资料 + 登录绑定
- 新增 my-bookings/my-tasks/my-park/my-cert 四页;auth.pickUser 保留 role/account_type;booking/park 新增 myBookings/myParkAdmission
- 新增 user-center.css 玻璃样式;vite build 通过
2026-08-27 20:15:01 +08:00

144 lines
5.6 KiB
JavaScript
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.
/**
* 认证服务(auth service)——统一账号
* -------------------------------------------------------------
* 用户端:手机号 + 短信验证码登录(手机号即账号,未注册自动注册)。
* POST /auth/send-code 发送验证码(stub 环境返回 stub_code/debugCode
* POST /auth/phone-login 手机号 + 验证码 → 自动注册或登录 → 平台 JWT
* POST /auth/update-profile 更新资料 → 平台 profile
* 平台端(/pine 内部管理):账号密码登录(内置 pine/123456)。
* POST /auth/login 密码登录 → 平台 JWT + 身份列表
* 统一账号:所有登录写全局 users + 平台 JWT(与小程序一致,见 分端口同步约束.md)。
*/
const API_BASE = 'https://opc.pinesound.cn'; // 云超服 FastAPI 后端
const TOKEN_KEY = 'pine_token';
const USER_KEY = 'pine_user';
/** 平台 profile → 本地 user 契约字段(各页面读取 name/status/topics/source/phoneBound 等) */
function pickUser(d) {
return {
id: d.id || '',
username: d.username,
name: d.name || d.nickname || d.username || '用户',
nickname: d.nickname || '',
avatar: d.avatar || '',
phone: d.phone || '',
phoneBound: !!d.phoneBound,
role: d.role || '',
account_type: d.account_type || '',
account_type_label: d.account_type_label || '',
company: d.company || '',
status: d.status || '',
topics: Array.isArray(d.topics) ? d.topics : [],
source: d.source || ''
};
}
export function saveSession(data) {
if (!data || !data.token) return;
localStorage.setItem(TOKEN_KEY, data.token);
localStorage.setItem(USER_KEY, JSON.stringify(pickUser(data)));
}
async function req(path, { method = 'POST', body, auth = false } = {}) {
const headers = { 'Content-Type': 'application/json', 'X-Client': 'web' };
if (auth) { const t = getToken(); if (t) headers.Authorization = `Bearer ${t}`; }
let res;
try {
res = await fetch(`${API_BASE}${path}`, { method, headers, body: body ? JSON.stringify(body) : undefined });
} catch {
throw new Error('网络连接失败,请检查网络后重试');
}
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.detail || data.error || '请求失败,请重试');
return data; // 平台契约:token/username/name/.../auth/*
}
/* ---------------- 用户端:手机号 + 验证码 ---------------- */
export async function sendCode(phone) {
const data = await req('/auth/send-code', { body: { phone } });
return { ok: true, sent: true, debugCode: data.stub_code ?? data.debugCode };
}
export async function phoneLogin(phone, code) {
const data = await req('/auth/phone-login', { body: { phone, code } });
if (data.token) saveSession(data);
return data;
}
/* ---------------- 平台端:账号密码登录(内部管理) ---------------- */
export async function login(username, password) {
const data = await req('/auth/login', { body: { username, password } });
if (data.token) saveSession(data);
return data;
}
/* ---------------- 绑定 / 解绑(统一账号,按手机号合并,绝不新建账号) ---------------- */
export async function bindPhone(phone, code) {
const data = await req('/auth/bind-phone', { body: { phone, code } });
if (data.token) saveSession(data); // 可能合并成另一账号,切换新令牌
return data;
}
export async function unbind(type) {
return req('/auth/unbind', { body: { type }, auth: true });
}
/** 拉取当前账号(含 phoneBound/wxBound/wxMiniBound */
export async function me() {
return req('/auth/me', { method: 'GET', auth: true });
}
/* ---------------- 微信扫码 / 小程序扫码登录 ---------------- */
export async function wxQrStart() {
return req('/auth/wx-qr/start', { method: 'GET' });
}
export async function wxQrPoll(scene) {
return req(`/auth/wx-qr/poll?scene=${encodeURIComponent(scene)}`, { method: 'GET' });
}
export async function mpQrStart() {
return req('/auth/mp-qr/start', { method: 'GET' });
}
export async function mpQrPoll(scene) {
return req(`/auth/mp-qr/poll?scene=${encodeURIComponent(scene)}`, { method: 'GET' });
}
/** 发起「绑定小程序」:返回小程序码(微信扫码自动打开小程序确认),绑定到当前账号 */
export async function mpQrBindStart() {
return req('/auth/mp-qr/bind-start', { method: 'GET', auth: true });
}
/** 扫码/短信登录成功:落 session 后返回完整平台 profile */
export function applyAuthResult(data) {
if (data && data.token) saveSession(data);
return data;
}
export function logout() {
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(USER_KEY);
// 平台用 /auth/revoke-token;这里本地退出即可(与小程序一致)
}
export function getToken() {
if (typeof localStorage === 'undefined') return null;
return localStorage.getItem(TOKEN_KEY);
}
/** 用完整 user 对象覆盖本地用户缓存(含 status/topics/source 等报名资料) */
export function saveUser(user) {
if (!user) return;
localStorage.setItem(USER_KEY, JSON.stringify(user));
}
/** 更新当前用户资料(昵称/头像 + 报名资料 status/topics/source),需登录(token */
export async function updateProfile(data) {
const r = await req('/auth/update-profile', { body: data, auth: true });
if (r.username) saveUser(pickUser(r));
return { ok: true, user: pickUser(r) };
}
export function isAuthed() {
return !!getToken();
}
export function getUser() {
if (typeof localStorage === 'undefined') return null;
try { return JSON.parse(localStorage.getItem(USER_KEY) || 'null'); } catch { return null; }
}