This commit is contained in:
2026-07-06 16:46:07 +08:00
parent ae914b5c24
commit 21d11697a0
8 changed files with 899 additions and 1378 deletions
+97 -144
View File
@@ -1,153 +1,115 @@
import { useUserStore } from '@/store'
// TODO: 替换为实际的 API 地址
export const baseUrl = 'https://lygcgs.com.cn:8078/shjapi/api/'
export const baseFileUrl = 'https://lygcgs.com.cn:8078/shj/'
// ===== 防止重复请求 =====
const pendingRequests = new Map()
/**
* 生成请求唯一标识
* 将参数对象序列化为 URL query string
* 手动构建确保跨平台(小程序 / H5 / App)行为一致
* - 过滤 undefined 和 null
* - encodeURIComponent 编码中文和特殊字符
*/
function getRequestKey(config) {
const { url = '', method = 'GET', data = {} } = config
const paramStr = JSON.stringify(data)
return `${method}:${url}:${paramStr}`
function toQueryString(params) {
if (!params) return ''
const parts = Object.keys(params)
.filter((k) => params[k] !== undefined && params[k] !== null)
.map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`)
return parts.length ? '?' + parts.join('&') : ''
}
/**
* 检查是否为重复请求
* 核心请求方法(兼容 小程序 / H5 / App)
*
* @param {string} url - API 路径(不含 baseUrl
* @param {object} options - 可选配置
* {string} method - GET | POST,默认 GET
* {object} data - GET 时自动拼入 query stringPOST 时作为请求体
* {object} header - 自定义请求头
* {boolean} loading - 是否显示 loading,默认 true
* {boolean} toast - 是否在失败时弹 toast,默认 true
*/
function checkDuplicate(config) {
const key = getRequestKey(config)
if (pendingRequests.has(key)) {
const meta = config.meta || {}
if (meta.toast !== false) {
uni.showToast({ title: '请求过于频繁,请稍后再试', icon: 'none' })
}
return new Error('duplicate request')
export function request(url, options = {}) {
const {
method = 'GET',
data,
header = {},
loading = true,
toast = true
} = options
const userStore = useUserStore()
const headers = {
'content-type': 'application/json',
...header
}
if (userStore.token) {
headers.token = userStore.token
}
pendingRequests.set(key, true)
return null
}
/**
* 移除请求标识
*/
function removePending(config) {
const key = getRequestKey(config)
pendingRequests.delete(key)
}
// ========================
// GET: 手动拼 query string(不依赖 uni.request 各平台差异)
let fullUrl = `${baseUrl}${url}`
if (method === 'GET' && data) {
fullUrl += toQueryString(data)
}
// uview-pro 全局请求配置
export const httpRequestConfig = {
baseUrl,
header: {
'content-type': 'application/json'
},
meta: {
originalData: true,
toast: true,
loading: true
}
}
if (loading) {
uni.showLoading({ title: '加载中...', mask: true })
}
// 全局请求/响应拦截器
export const httpInterceptor = {
request: (config) => {
// 检查重复请求
const dupError = checkDuplicate(config)
if (dupError) throw dupError
const meta = config.meta || {}
if (meta.loading) {
uni.showLoading({ title: '加载中...', mask: true })
}
const userStore = useUserStore()
if (userStore.token) {
config.header.token = userStore.token
}
return config
},
response: (response) => {
const meta = response.config?.meta || {}
if (meta.loading) {
uni.hideLoading()
}
// 请求完成,移除标识
removePending(response.config)
// 根据业务处理错误、例如登录失效等处理接口返回错误码
if (response.data.code !== 1) {
if (meta.toast) {
uni.showToast({
title: response.data.message || '请求失败',
icon: 'none'
})
}
// 401 登录失效,跳转登录页
if (response.data.code === 401) {
const userStore = useUserStore()
userStore.logout()
uni.reLaunch({ url: '/pages/login/index' })
}
throw new Error(response.data.message || '请求失败')
}
uni.showToast({
title: response.data.message || '请求成功',
icon: 'none'
return new Promise((resolve, reject) => {
uni.request({
url: fullUrl,
method,
data: method === 'POST' ? data : undefined,
header: headers,
success: (res) => {
if (loading) uni.hideLoading()
const body = res.data
if (body.code !== 1) {
if (toast) {
uni.showToast({
title: body.message || '请求失败',
icon: 'none'
})
}
if (body.code === 401) {
userStore.logout()
uni.reLaunch({ url: '/pages/login/index' })
}
reject(new Error(body.message || '请求失败'))
return
}
resolve(body)
},
fail: (err) => {
if (loading) uni.hideLoading()
uni.showToast({ title: '网络连接失败', icon: 'none' })
reject(err)
}
})
return response.data
}
})
}
/**
* 便捷请求方法(基于 uni.request 封装)
* 也可用 uni.$http.get / uni.$http.postuview-pro
* GET 快捷方法
* 兼容两种调用方式:
* get('/path?key=val') 旧风格(URL 自带参数)
* get('/path', { key: 'val' }) 新风格(参数对象,自动编码)
*/
export function request(options) {
return new Promise((resolve, reject) => {
// 运行请求拦截器
const config = { ...httpRequestConfig, ...options }
const finalConfig = httpInterceptor.request({
...config,
header: { ...httpRequestConfig.header, ...options.header }
})
uni.request({
...finalConfig,
url: `${finalConfig.baseUrl}${finalConfig.url}`,
success: (res) => {
try {
const data = httpInterceptor.response({ data: res.data, config: finalConfig })
resolve(data)
} catch (err) {
reject(err)
}
},
fail: (err) => {
removePending(finalConfig) // 网络失败也要清理标识
uni.hideLoading()
uni.showToast({ title: '网络连接失败', icon: 'none' })
reject(err)
}
})
})
export const get = (url, params) => {
if (params) {
url += toQueryString(params)
}
return request(url, { method: 'GET' })
}
// GET 快捷方法
export const get = (url, params = {}) => {
return request({ url, method: 'GET', data: params })
/**
* POST 快捷方法
*/
export const post = (url, data) => {
return request(url, { method: 'POST', data })
}
// POST 快捷方法
export const post = (url, data = {}) => {
return request({ url, method: 'POST', data })
}
/**
* 解析附件 URL 字符串为可展示的照片列表
* @param {string} urlStr - 逗号分隔的附件路径,如 "FileUpLoad/xxx.png,FileUpLoad/yyy.png"
@@ -157,7 +119,7 @@ export const post = (url, data = {}) => {
export const parseAttachUrls = (urlStr, startId = 0) => {
if (!urlStr) return []
let id = startId
return urlStr.split(',').filter(Boolean).map(url => ({
return urlStr.split(',').filter(Boolean).map((url) => ({
_id: ++id,
src: `${baseFileUrl}${url}`,
_attachUrl: url,
@@ -166,20 +128,11 @@ export const parseAttachUrls = (urlStr, startId = 0) => {
}
export const getUrlParam = (url, name) => {
if (!url || !name) return null;
// 构造正则:匹配 ?name= 或 &name= 后面的值
// [?&] : 匹配起始分隔符
// ${name}: 匹配具体的参数名
// = : 匹配等号
// ([^&]*) : 捕获组,匹配直到下一个 & 或字符串结束的内容
const regex = new RegExp(`[?&]${name}=([^&]*)`, 'i'); // 'i' 表示忽略大小写
const match = url.match(regex);
if (match && match[1]) {
return decodeURIComponent(match[1]);
}
return null;
};
if (!url || !name) return null
const regex = new RegExp(`[?&]${name}=([^&]*)`, 'i')
const match = url.match(regex)
if (match && match[1]) {
return decodeURIComponent(match[1])
}
return null
}