重构:移动源码到 src/ 目录,支持 CLI 构建
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
/**
|
||||
* 用户授权工具
|
||||
* 登录检查、授权跳转、回调处理、绑定手机号
|
||||
*
|
||||
* 授权相关接口(auth/url、auth/callback、sms/send、bindPhone)走网关 apiGatewayUrl
|
||||
* 业务接口(商品列表等)走 apiBaseUrl
|
||||
*/
|
||||
import config from '@/config/env.js';
|
||||
import { gatewayGet, gatewayPost } from './request.js';
|
||||
|
||||
// 场景提示文案
|
||||
const SCENE_TIPS = {
|
||||
order: '登录后即可查看订单',
|
||||
mine: '登录后即可查看个人信息',
|
||||
pay: '登录后即可下单购买',
|
||||
bindPhone: '需要绑定手机号完成登录',
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查是否已登录
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isLoggedIn() {
|
||||
return !!uni.getStorageSync('token');
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查登录状态,未登录时弹窗引导授权
|
||||
* @param {string} scene - 触发场景:'order' | 'mine' | 'pay' | 'bindPhone'
|
||||
* @returns {boolean} 是否已登录
|
||||
*/
|
||||
export function checkLoginWithPrompt(scene = '') {
|
||||
if (isLoggedIn()) return true;
|
||||
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: SCENE_TIPS[scene] || '需要登录后操作',
|
||||
confirmText: '去登录',
|
||||
cancelText: '暂不',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
startAuth();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查登录状态,未登录时静默跳转授权(不弹窗)
|
||||
* @returns {boolean} 是否已登录
|
||||
*/
|
||||
export function requireLogin() {
|
||||
if (isLoggedIn()) return true;
|
||||
startAuth();
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起授权跳转
|
||||
* 获取 auth_url 后跳转到微信/支付宝授权页
|
||||
*/
|
||||
export async function startAuth() {
|
||||
const appNo = uni.getStorageSync('app_no') || '';
|
||||
|
||||
if (!appNo) {
|
||||
uni.showToast({ title: '当前环境不支持授权', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await gatewayGet('/api/v1/user/auth/url', {
|
||||
app_no: appNo,
|
||||
scopes: '',
|
||||
redirect_uri: config.authRedirectUri || '',
|
||||
});
|
||||
|
||||
const data = res.data || {};
|
||||
if (data.code === 0 && data.data && data.data.auth_url) {
|
||||
// #ifdef H5
|
||||
window.location.href = data.data.auth_url;
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
// 小程序环境走其他授权方式
|
||||
uni.setStorageSync('pending_auth', true);
|
||||
// #endif
|
||||
} else {
|
||||
uni.showToast({ title: data.message || '获取授权地址失败', icon: 'none' });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Auth] startAuth error:', e);
|
||||
uni.showToast({ title: '网络请求失败', icon: 'none' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理授权回调
|
||||
* 解析 URL 中的 code/auth_code 参数,调用后端回调接口
|
||||
* @returns {Promise<Object>} 回调结果
|
||||
*/
|
||||
export async function handleAuthCallback() {
|
||||
// #ifdef H5
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const code = urlParams.get('code') || urlParams.get('auth_code') || '';
|
||||
const appNo = urlParams.get('state') || uni.getStorageSync('app_no') || '';
|
||||
|
||||
if (!code) return null;
|
||||
|
||||
try {
|
||||
const res = await gatewayGet('/api/v1/user/auth/callback', {
|
||||
app_no: appNo,
|
||||
code,
|
||||
state: appNo,
|
||||
});
|
||||
|
||||
const data = res.data || {};
|
||||
if (data.code === 0 && data.data) {
|
||||
return data.data;
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifndef H5
|
||||
return null;
|
||||
// #endif
|
||||
}
|
||||
|
||||
/**
|
||||
* 授权成功后处理
|
||||
* 缴存 token 和用户信息
|
||||
* @param {Object} authResult - 后端返回的授权结果
|
||||
*/
|
||||
export function onAuthSuccess(authResult) {
|
||||
if (!authResult) return;
|
||||
|
||||
const { token, user_info, auth_info } = authResult;
|
||||
|
||||
if (token) {
|
||||
uni.setStorageSync('token', token);
|
||||
}
|
||||
|
||||
if (user_info) {
|
||||
uni.setStorageSync('user_info', user_info);
|
||||
}
|
||||
|
||||
if (auth_info) {
|
||||
uni.setStorageSync('auth_info', auth_info);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送短信验证码
|
||||
* @param {string} phone - 手机号
|
||||
* @param {string} tempToken - 临时 token
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export async function sendSmsCode(phone, tempToken) {
|
||||
const res = await gatewayPost('/api/v1/user/sms/send', {
|
||||
phone,
|
||||
temp_token: tempToken,
|
||||
purpose: 'bindPhone',
|
||||
});
|
||||
|
||||
const data = res.data || {};
|
||||
return data.code === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定手机号
|
||||
* @param {string} tempToken - 临时 token
|
||||
* @param {string} phone - 手机号
|
||||
* @param {string} code - 验证码
|
||||
* @returns {Promise<Object>} 绑定结果(含 token 和 user_info)
|
||||
*/
|
||||
export async function bindPhone(tempToken, phone, code) {
|
||||
const res = await gatewayPost('/api/v1/user/bindPhone', {
|
||||
temp_token: tempToken,
|
||||
phone,
|
||||
code,
|
||||
});
|
||||
|
||||
const data = res.data || {};
|
||||
if (data.code === 0 && data.data) {
|
||||
// 绑定成功,缓存 token 和用户信息
|
||||
onAuthSuccess(data.data);
|
||||
uni.removeStorageSync('temp_token');
|
||||
return data.data;
|
||||
}
|
||||
|
||||
uni.showToast({ title: data.message || '绑定失败', icon: 'none' });
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
*/
|
||||
export function logout() {
|
||||
uni.removeStorageSync('token');
|
||||
uni.removeStorageSync('user_info');
|
||||
uni.removeStorageSync('auth_info');
|
||||
uni.removeStorageSync('temp_token');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存的用户信息
|
||||
* @returns {Object|null}
|
||||
*/
|
||||
export function getCachedUserInfo() {
|
||||
try {
|
||||
return uni.getStorageSync('user_info') || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示绑定手机号弹窗
|
||||
* @param {string} tempToken - 临时 token
|
||||
* @param {Function} onSuccess - 绑定成功回调
|
||||
*/
|
||||
export function showBindPhoneDialog(tempToken, onSuccess) {
|
||||
let phone = '';
|
||||
let code = '';
|
||||
let countdown = 0;
|
||||
let timer = null;
|
||||
|
||||
// 使用 uni.showModal 不够灵活,这里用自定义弹窗
|
||||
// 简化实现:先弹出手机号输入
|
||||
uni.showModal({
|
||||
title: '绑定手机号',
|
||||
content: '请先绑定手机号完成注册',
|
||||
confirmText: '去绑定',
|
||||
cancelText: '取消',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
// 跳转到绑定手机号页面(预留)
|
||||
// 后续可实现专门的绑定手机号页面
|
||||
uni.showToast({ title: '绑定手机号功能开发中', icon: 'none' });
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* 环境检测工具
|
||||
* 检测当前运行平台(微信/支付宝/普通浏览器),映射到对应的 app_no
|
||||
*/
|
||||
|
||||
// app_no 编码表:platform + terminalType → app_no
|
||||
const APP_NO_MAP = {
|
||||
'wechat_h5': '60000001102001',
|
||||
'wechat_mp': '600102101',
|
||||
'alipay_h5': '600112001',
|
||||
'alipay_mp': '600112101',
|
||||
};
|
||||
|
||||
/**
|
||||
* 检测当前运行环境
|
||||
* @returns {'wechat' | 'alipay' | 'browser'}
|
||||
*/
|
||||
export function detectPlatform() {
|
||||
// #ifdef H5
|
||||
const ua = navigator.userAgent;
|
||||
|
||||
if (/MicroMessenger/i.test(ua)) {
|
||||
return 'wechat';
|
||||
}
|
||||
if (/AlipayClient/i.test(ua)) {
|
||||
return 'alipay';
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
return 'wechat';
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-ALIPAY
|
||||
return 'alipay';
|
||||
// #endif
|
||||
|
||||
return 'browser';
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测当前终端类型
|
||||
* @returns {'h5' | 'mp'}
|
||||
*/
|
||||
export function detectTerminalType() {
|
||||
// #ifdef H5
|
||||
return 'h5';
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN || MP-ALIPAY
|
||||
return 'mp';
|
||||
// #endif
|
||||
|
||||
return 'h5';
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据平台和终端类型获取 app_no
|
||||
* @param {string} platform - 'wechat' | 'alipay' | 'browser'
|
||||
* @param {string} terminalType - 'h5' | 'mp'
|
||||
* @returns {string} app_no
|
||||
*/
|
||||
export function getAppNo(platform, terminalType = 'h5') {
|
||||
if (platform === 'browser') return '';
|
||||
return APP_NO_MAP[`${platform}_${terminalType}`] || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化环境检测并缓存到 storage
|
||||
* 应在 App.vue onLaunch 中调用
|
||||
*/
|
||||
export function initEnv() {
|
||||
const platform = detectPlatform();
|
||||
const terminalType = detectTerminalType();
|
||||
const appNo = getAppNo(platform, terminalType);
|
||||
|
||||
uni.setStorageSync('platform', platform);
|
||||
uni.setStorageSync('terminal_type', terminalType);
|
||||
if (appNo) {
|
||||
uni.setStorageSync('app_no', appNo);
|
||||
}
|
||||
|
||||
return { platform, terminalType, appNo };
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* 统一请求封装
|
||||
* 自动注入 Authorization header,处理 401
|
||||
* app_no 通过接口参数传递,不在 header 中
|
||||
*/
|
||||
import config from '@/config/env.js';
|
||||
|
||||
/**
|
||||
* 发起请求
|
||||
* @param {Object} options - uni.request 选项
|
||||
* @param {string} options.url - 请求地址(可以是相对路径,会拼接 baseUrl)
|
||||
* @param {string} [options.method='GET'] - 请求方法
|
||||
* @param {Object} [options.data] - 请求参数
|
||||
* @param {Object} [options.header] - 额外请求头
|
||||
* @param {string} [options.baseUrl] - 基础地址,默认 apiBaseUrl;传 'gateway' 使用 apiGatewayUrl
|
||||
* @returns {Promise<Object>} 响应数据
|
||||
*/
|
||||
export function request(options = {}) {
|
||||
const token = uni.getStorageSync('token') || '';
|
||||
|
||||
// 确定 baseUrl
|
||||
let baseUrl = config.apiBaseUrl;
|
||||
if (options.baseUrl === 'gateway') {
|
||||
baseUrl = config.apiGatewayUrl;
|
||||
} else if (options.baseUrl) {
|
||||
baseUrl = options.baseUrl;
|
||||
}
|
||||
|
||||
// 拼接完整 URL
|
||||
let url = options.url || '';
|
||||
if (url && !url.startsWith('http')) {
|
||||
url = baseUrl + url;
|
||||
}
|
||||
|
||||
// 构建 header(app_no 不在 header 中传,通过参数传递)
|
||||
const header = {
|
||||
...options.header,
|
||||
};
|
||||
|
||||
// 公开接口(授权相关)不带 Authorization,避免 CORS 预检和 JWT 拦截
|
||||
const publicPaths = ['/api/v1/user/auth/url', '/api/v1/user/auth/callback'];
|
||||
const isPublic = publicPaths.some((p) => url.includes(p));
|
||||
|
||||
if (token && !isPublic) {
|
||||
header['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
// 开发环境请求日志
|
||||
if (config.enableRequestLog) {
|
||||
console.log(`[API] ${options.method || 'GET'} ${url}`, options.data || '');
|
||||
console.log('[API] Headers:', header);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url,
|
||||
method: options.method || 'GET',
|
||||
data: options.data,
|
||||
header,
|
||||
timeout: config.requestTimeout,
|
||||
success: (res) => {
|
||||
if (config.enableRequestLog) {
|
||||
console.log('[API] StatusCode:', res.statusCode);
|
||||
console.log('[API] Response Headers:', res.header || res.headers);
|
||||
console.log('[API] Response Body:', res.data);
|
||||
}
|
||||
|
||||
// 401 → 清除登录状态
|
||||
if (res.statusCode === 401) {
|
||||
uni.removeStorageSync('token');
|
||||
uni.removeStorageSync('user_info');
|
||||
uni.removeStorageSync('temp_token');
|
||||
}
|
||||
|
||||
resolve(res);
|
||||
},
|
||||
fail: (err) => {
|
||||
if (config.enableRequestLog) {
|
||||
console.error('[API] Error:', err);
|
||||
}
|
||||
reject(err);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* GET 请求(默认走业务 API)
|
||||
*/
|
||||
export function get(url, data, options = {}) {
|
||||
return request({ url, method: 'GET', data, ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* POST 请求(默认走业务 API)
|
||||
*/
|
||||
export function post(url, data, options = {}) {
|
||||
return request({ url, method: 'POST', data, ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* GET 请求(走网关)
|
||||
*/
|
||||
export function gatewayGet(url, data, options = {}) {
|
||||
return request({ url, method: 'GET', data, baseUrl: 'gateway', ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* POST 请求(走网关)
|
||||
*/
|
||||
export function gatewayPost(url, data, options = {}) {
|
||||
return request({ url, method: 'POST', data, baseUrl: 'gateway', ...options });
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* vConsole 调试工具管理
|
||||
*
|
||||
* 使用方式:
|
||||
* 1. URL 参数:?vconsole=1 开启,?vconsole=0 关闭
|
||||
* 2. localStorage:设置 'vconsole' 为 '1' 开启,'0' 关闭
|
||||
* 3. 代码调用:import { showVConsole, hideVConsole, toggleVConsole } from '@/utils/vconsole'
|
||||
*/
|
||||
|
||||
let vConsoleInstance = null;
|
||||
|
||||
// 检测是否为开发环境(兼容多种方式)
|
||||
const isDev = process.env.NODE_ENV !== 'production';
|
||||
|
||||
/**
|
||||
* 获取 vConsole 是否开启
|
||||
*/
|
||||
export function isVConsoleEnabled() {
|
||||
if (!isDev) return false;
|
||||
|
||||
// 优先从 URL 参数获取(H5 环境直接读 window.location)
|
||||
// #ifdef H5
|
||||
try {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const vconsoleParam = urlParams.get('vconsole');
|
||||
if (vconsoleParam !== null) {
|
||||
return vconsoleParam === '1' || vconsoleParam === 'true';
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifndef H5
|
||||
try {
|
||||
const pages = getCurrentPages();
|
||||
if (pages.length > 0) {
|
||||
const currentPage = pages[pages.length - 1];
|
||||
const options = currentPage.$page?.options || currentPage.options || {};
|
||||
if (options.vconsole !== undefined) {
|
||||
return options.vconsole === '1' || options.vconsole === 'true';
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
// #endif
|
||||
|
||||
// 其次从 localStorage 获取
|
||||
try {
|
||||
return uni.getStorageSync('vconsole') === '1';
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开启 vConsole
|
||||
*/
|
||||
export function showVConsole() {
|
||||
if (vConsoleInstance) return;
|
||||
|
||||
// 动态加载 vConsole
|
||||
import('vconsole').then((module) => {
|
||||
const VConsole = module.default || module;
|
||||
vConsoleInstance = new VConsole({
|
||||
theme: 'dark',
|
||||
disableLogScrolling: false,
|
||||
buttonSize: 48,
|
||||
});
|
||||
|
||||
// 增强 vConsole 按钮样式,确保不被遮挡
|
||||
setTimeout(() => {
|
||||
const btn = document.querySelector('#__vconsole');
|
||||
if (btn) {
|
||||
btn.style.zIndex = '999999';
|
||||
btn.style.position = 'fixed';
|
||||
btn.style.bottom = '80px';
|
||||
btn.style.right = '10px';
|
||||
}
|
||||
}, 500);
|
||||
|
||||
uni.setStorageSync('vconsole', '1');
|
||||
}).catch((err) => {
|
||||
console.error('[vConsole] 加载失败:', err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭 vConsole
|
||||
*/
|
||||
export function hideVConsole() {
|
||||
if (vConsoleInstance) {
|
||||
vConsoleInstance.destroy();
|
||||
vConsoleInstance = null;
|
||||
}
|
||||
try {
|
||||
uni.setStorageSync('vconsole', '0');
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换 vConsole 状态
|
||||
*/
|
||||
export function toggleVConsole() {
|
||||
if (vConsoleInstance) {
|
||||
hideVConsole();
|
||||
} else {
|
||||
showVConsole();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据配置初始化 vConsole
|
||||
*/
|
||||
export function initVConsole() {
|
||||
if (!isDev) return;
|
||||
|
||||
if (isVConsoleEnabled()) {
|
||||
showVConsole();
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
isVConsoleEnabled,
|
||||
showVConsole,
|
||||
hideVConsole,
|
||||
toggleVConsole,
|
||||
initVConsole,
|
||||
};
|
||||
Reference in New Issue
Block a user