4419c9265f
- Header: 登录后头像+用户名玻璃下拉(个人中心/我的报名/我的任务/我的园区/认证中心/退出);未登录显示登录/注册;移动端同步 - Profile: 玻璃用户卡(头像上传/身份标签) + 我的服务宫格(实时状态) + 报名资料 + 登录绑定 - 新增 my-bookings/my-tasks/my-park/my-cert 四页;auth.pickUser 保留 role/account_type;booking/park 新增 myBookings/myParkAdmission - 新增 user-center.css 玻璃样式;vite build 通过
144 lines
5.6 KiB
JavaScript
144 lines
5.6 KiB
JavaScript
/**
|
||
* 认证服务(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; }
|
||
}
|