first commit
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import { createRequest } from '@/utils/request'
|
||||
import type { ResponseStructure } from '@/utils/request'
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
/** 图片资源 */
|
||||
export interface ApiImage {
|
||||
preview_url: string
|
||||
file_name: string
|
||||
id: number
|
||||
}
|
||||
|
||||
/** 轮播图项 */
|
||||
export interface CarouselItem {
|
||||
id: number
|
||||
title: string
|
||||
image: ApiImage
|
||||
link: string
|
||||
status: number // 0: 启用
|
||||
sort: number
|
||||
}
|
||||
|
||||
/** 宫格导航项 */
|
||||
export interface GridNavItem {
|
||||
id: number
|
||||
title: string
|
||||
image: ApiImage
|
||||
link: string
|
||||
status: number // 0: 启用
|
||||
sort: number
|
||||
}
|
||||
|
||||
/** 首页数据 */
|
||||
export interface HomeData {
|
||||
carousel: CarouselItem[]
|
||||
gridNav: GridNavItem[]
|
||||
}
|
||||
|
||||
// ==================== API ====================
|
||||
|
||||
/**
|
||||
* 获取首页数据(无需登录)
|
||||
*/
|
||||
export async function getHomeData(): Promise<ResponseStructure<HomeData>> {
|
||||
return createRequest<HomeData>({
|
||||
url: '/api/home',
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export default defineAppConfig({
|
||||
pages: [
|
||||
'pages/index/index',
|
||||
],
|
||||
window: {
|
||||
backgroundTextStyle: 'light',
|
||||
navigationBarBackgroundColor: '#fff',
|
||||
navigationBarTitleText: 'WeChat',
|
||||
navigationBarTextStyle: 'black',
|
||||
},
|
||||
animation: false,
|
||||
})
|
||||
@@ -0,0 +1,45 @@
|
||||
@import '@antmjs/vantui/es/style/var.less';
|
||||
@import '@antmjs/vantui/lib/index.less';
|
||||
|
||||
page {
|
||||
background: @page-back;
|
||||
font-size: 28px;
|
||||
font-family: @base-font-family;
|
||||
color: @black;
|
||||
}
|
||||
|
||||
view,
|
||||
div {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body,
|
||||
html {
|
||||
// NOTE: taro h5 ios上拉遮挡底部fixed元素
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.van-cell-group--inset {
|
||||
background: @white;
|
||||
box-shadow: 0 14px 80px 0 rgba(138, 149, 158, 0.2);
|
||||
}
|
||||
|
||||
.van-cell__label {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
// 可以用的类
|
||||
// .van-hairline,
|
||||
// .van-hairline--top,
|
||||
// .van-hairline--left,
|
||||
// .van-hairline--right,
|
||||
// .van-hairline--bottom,
|
||||
// .van-hairline--top-bottom,
|
||||
// .van-hairline--surround
|
||||
// .van-ellipsis
|
||||
// .van-multi-ellipsis--l2
|
||||
// .van-multi-ellipsis--l3
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { Component } from 'react'
|
||||
import './app.less'
|
||||
|
||||
class App extends Component {
|
||||
|
||||
componentDidMount () {}
|
||||
|
||||
componentDidShow () {}
|
||||
|
||||
componentDidHide () {}
|
||||
|
||||
componentDidCatchError () {}
|
||||
|
||||
// this.props.children 是将要会渲染的页面
|
||||
render () {
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
|
||||
export default App
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
||||
<meta content="width=device-width,initial-scale=1,user-scalable=no" name="viewport">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-touch-fullscreen" content="yes">
|
||||
<meta name="format-detection" content="telephone=no,address=no">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="white">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" >
|
||||
<title>antmjs</title>
|
||||
<script crossorigin="anonymous" src="https://polyfill.alicdn.com/polyfill.min.js?features=es2015%2Ces2016%2Ces2017%2Ces2018%2Ces2019%2Ces2020%2Ces2021%2Ces2022"></script>
|
||||
<script><%= htmlWebpackPlugin.options.script %></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,5 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '校园美食',
|
||||
navigationBarBackgroundColor: '#ff6b35',
|
||||
navigationBarTextStyle: 'white',
|
||||
})
|
||||
@@ -0,0 +1,153 @@
|
||||
.index {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
|
||||
// 背景图层
|
||||
.bg-image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
// 内容层
|
||||
.index-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
// ========== 1. 搜索栏 ==========
|
||||
.search-wrapper {
|
||||
padding: 24px 32px 16px;
|
||||
|
||||
.van-search {
|
||||
padding: 0;
|
||||
border-radius: 36px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 16px rgba(255, 107, 53, 0.15);
|
||||
}
|
||||
|
||||
.van-search__content {
|
||||
border-radius: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 2. 轮播图 ==========
|
||||
.swiper-wrapper {
|
||||
margin: 8px 32px 20px;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
|
||||
.swiper-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 3. 宫格导航 ==========
|
||||
.grid-wrapper {
|
||||
margin: 16px 32px 20px;
|
||||
background: rgb(255 255 255);
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
|
||||
padding-bottom: 24px;
|
||||
|
||||
.grid-title {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.van-grid-item__content {
|
||||
padding-top: 24px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.van-icon--image {
|
||||
width: 60px !important;
|
||||
height: 60px !important;
|
||||
}
|
||||
|
||||
.van-grid-item__text {
|
||||
font-size: 22px;
|
||||
color: #555;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 4. 底部推荐卡片 ==========
|
||||
.bottom-section {
|
||||
padding: 0 32px;
|
||||
|
||||
.section-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 18px;
|
||||
padding: 24px 28px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 48px;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 18px;
|
||||
margin-right: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.card-info {
|
||||
flex: 1;
|
||||
|
||||
.card-title {
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 24px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.card-arrow {
|
||||
font-size: 40px;
|
||||
color: #ccc;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 每张卡片图标不同底色
|
||||
.card-hot .card-icon {
|
||||
background: #fff0ed;
|
||||
}
|
||||
.card-discount .card-icon {
|
||||
background: #fff7e6;
|
||||
}
|
||||
.card-new .card-icon {
|
||||
background: #e6f7ff;
|
||||
}
|
||||
.card-order .card-icon {
|
||||
background: #f0edff;
|
||||
}
|
||||
}
|
||||
|
||||
// 底部留白
|
||||
.bottom-spacer {
|
||||
height: 60px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import { View, Image } from '@tarojs/components'
|
||||
import { Search, Swiper, SwiperItem, Grid, GridItem } from '@antmjs/vantui'
|
||||
import { useState, useEffect } from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { getHomeData } from '@/api/home'
|
||||
import type { CarouselItem, GridNavItem } from '@/api/home'
|
||||
import './index.less'
|
||||
|
||||
// 背景图
|
||||
import bgImg from '../../../static/img/bg.png'
|
||||
|
||||
// ==================== 本地兜底数据 ====================
|
||||
|
||||
// ==================== 工具函数 ====================
|
||||
|
||||
/** 排序 + 过滤启用的项 */
|
||||
function prepareItems<T extends { status: number; sort: number }>(items: T[]): T[] {
|
||||
return items
|
||||
.filter((item) => item.status === 0)
|
||||
.sort((a, b) => a.sort - b.sort)
|
||||
}
|
||||
|
||||
/** 处理导航跳转 */
|
||||
function handleNavigate(link: string): void {
|
||||
if (!link) return
|
||||
if(link.startsWith('http')) {
|
||||
window.location.href = link
|
||||
} else {
|
||||
Taro.navigateTo({ url: link }).catch(() => {
|
||||
Taro.switchTab({ url: link }).catch(() => {
|
||||
console.warn('导航失败:', link)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 页面组件 ====================
|
||||
|
||||
export default function Index() {
|
||||
const [searchValue, setSearchValue] = useState('')
|
||||
const [carousel, setCarousel] = useState<CarouselItem[]>([])
|
||||
const [gridNav, setGridNav] = useState<GridNavItem[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
// 获取首页数据
|
||||
useEffect(() => {
|
||||
async function fetchHomeData() {
|
||||
try {
|
||||
const res = await getHomeData()
|
||||
if (res.success && res.data) {
|
||||
// 轮播图:API 有数据则用,否则兜底
|
||||
if (res.data.carousel?.length > 0) {
|
||||
setCarousel(prepareItems(res.data.carousel))
|
||||
}
|
||||
// 宫格导航:API 有数据则用,否则兜底
|
||||
if (res.data.gridNav?.length > 0) {
|
||||
setGridNav(prepareItems(res.data.gridNav))
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('获取首页数据失败,使用本地兜底数据', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchHomeData()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<View className='index'>
|
||||
{/* 背景图层 */}
|
||||
<Image className='bg-image' src={bgImg} mode='widthFix' />
|
||||
|
||||
{/* 内容层 */}
|
||||
<View className='index-content'>
|
||||
{/* 1. 顶部搜索栏 */}
|
||||
<View className='search-wrapper'>
|
||||
<Search
|
||||
value={searchValue}
|
||||
onChange={(e) => setSearchValue(e.detail)}
|
||||
shape='round'
|
||||
placeholder='搜索美食、餐厅...'
|
||||
background='rgba(255,255,255,0.9)'
|
||||
onSearch={() => {
|
||||
// 搜索逻辑
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 2. 中间轮播图 */}
|
||||
<View className='swiper-wrapper'>
|
||||
{carousel.length > 0 ? (
|
||||
<Swiper
|
||||
autoPlay={3000}
|
||||
loop
|
||||
paginationVisible
|
||||
paginationColor='#ffffff'
|
||||
duration={500}
|
||||
height={220}
|
||||
>
|
||||
{carousel.map((item) => (
|
||||
<SwiperItem key={item.id}>
|
||||
<Image
|
||||
className='swiper-img'
|
||||
src={item.image.preview_url}
|
||||
mode='aspectFill'
|
||||
onClick={() => handleNavigate(item.link)}
|
||||
/>
|
||||
</SwiperItem>
|
||||
))}
|
||||
</Swiper>
|
||||
) : (
|
||||
<View className='swiper-placeholder'>暂无轮播图</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 3. 宫格导航 */}
|
||||
<View className='grid-wrapper'>
|
||||
{gridNav.length > 0 ? (
|
||||
<Grid columnNum={5} border={false} gutter={12} iconSize={48}>
|
||||
{gridNav.map((item) => (
|
||||
<GridItem
|
||||
key={item.id}
|
||||
icon={item.image.preview_url}
|
||||
text={item.title}
|
||||
onClick={() => handleNavigate(item.link)}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
) : (
|
||||
<View className='grid-placeholder'>暂无分类</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 4. 底部推荐卡片 */}
|
||||
<View className='bottom-section'>
|
||||
<View className='section-card card-hot'>
|
||||
<View className='card-icon'>🔥</View>
|
||||
<View className='card-info'>
|
||||
<View className='card-title'>热门推荐</View>
|
||||
<View className='card-desc'>查看同学们都在点什么</View>
|
||||
</View>
|
||||
<View className='card-arrow'>›</View>
|
||||
</View>
|
||||
|
||||
<View className='section-card card-discount'>
|
||||
<View className='card-icon'>💰</View>
|
||||
<View className='card-info'>
|
||||
<View className='card-title'>限时优惠</View>
|
||||
<View className='card-desc'>超值套餐低至9.9元</View>
|
||||
</View>
|
||||
<View className='card-arrow'>›</View>
|
||||
</View>
|
||||
|
||||
<View className='section-card card-new'>
|
||||
<View className='card-icon'>⭐</View>
|
||||
<View className='card-info'>
|
||||
<View className='card-title'>新品上架</View>
|
||||
<View className='card-desc'>尝尝校园新味道</View>
|
||||
</View>
|
||||
<View className='card-arrow'>›</View>
|
||||
</View>
|
||||
|
||||
<View className='section-card card-order'>
|
||||
<View className='card-icon'>📋</View>
|
||||
<View className='card-info'>
|
||||
<View className='card-title'>我的订单</View>
|
||||
<View className='card-desc'>查看订单状态与历史</View>
|
||||
</View>
|
||||
<View className='card-arrow'>›</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 底部留白 */}
|
||||
<View className='bottom-spacer' />
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
@import '@antmjs/vantui/es/style/var.less';
|
||||
|
||||
// 这里可以重写主题
|
||||
//@black: #1a1a1a;
|
||||
//@white: #f7f7f7;
|
||||
//@gray-1: #f7f8fa;
|
||||
//@gray-2: #f2f3f5;
|
||||
//@gray-3: #ededed;
|
||||
//@gray-4: #dcdee0;
|
||||
//@gray-5: #c8c9cc;
|
||||
//@gray-6: #969799;
|
||||
//@gray-7: #646566;
|
||||
//@gray-8: #323233;
|
||||
//@red: #ee0a24;
|
||||
//@blue: #1989fa;
|
||||
//@orange: #ff976a;
|
||||
//@orange-dark: #ed6a0c;
|
||||
//@orange-light: #fffbe8;
|
||||
//@green: #0a4d2b;
|
||||
//
|
||||
//@pageBack: @gray-3;
|
||||
//@navBack: rgba(237, 237, 237, 0.9);
|
||||
//@popup-background-color: @gray-3;
|
||||
//@backDropFilter: blur(20px);
|
||||
//
|
||||
//@popup-close-icon-color: @gray-5;
|
||||
//@popup-close-icon-size: 40px;
|
||||
//@popup-close-icon-margin: 24px;
|
||||
//@button-plain-background-color: @gray-4;
|
||||
//
|
||||
// z-index
|
||||
//@sticky-z-index: 800;
|
||||
//@tabbar-z-index: 805;
|
||||
//@navbar-z-index: 805;
|
||||
//@goods-action-z-index: 806;
|
||||
//@submit-bar-z-index: 806;
|
||||
//@overlay-z-index: 1000;
|
||||
//@dropdown-z-index: 1000;
|
||||
//@popup-z-index: 1010;
|
||||
//@popup-close-icon-z-index: 1010;
|
||||
//@notify-z-index: 1500;
|
||||
//
|
||||
// Padding or Margin
|
||||
//@padding-base: 8px;
|
||||
//@padding-xs: @padding-base * 2;
|
||||
//@padding-sm: @padding-base * 3;
|
||||
//@padding-md: @padding-base * 4;
|
||||
//@padding-lg: @padding-base * 6;
|
||||
//@padding-xl: @padding-base * 8;
|
||||
//
|
||||
// Font
|
||||
//@font-size-xs: 20px;
|
||||
//@font-size-sm: 24px;
|
||||
//@font-size-md: 28px;
|
||||
//@font-size-lg: 32px;
|
||||
//@font-weight-bold: 500;
|
||||
//@line-height-xs: 28px;
|
||||
//@line-height-sm: 36px;
|
||||
//@line-height-md: 40px;
|
||||
//@line-height-lg: 44px;
|
||||
//@base-font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue',
|
||||
// Helvetica, Segoe UI, Arial, Roboto, 'PingFang SC', 'miui', 'Hiragino Sans GB',
|
||||
// 'Microsoft Yahei', sans-serif;
|
||||
//@price-integer-font-family: Avenir-Heavy, PingFang SC, Helvetica Neue, Arial,
|
||||
// sans-serif;
|
||||
//
|
||||
// Border
|
||||
//@border-color: @gray-3;
|
||||
//@border-width-base: 2px;
|
||||
//@border-radius-sm: 4px;
|
||||
//@border-radius-md: 8px;
|
||||
//@border-radius-lg: 16px;
|
||||
//@border-radius-max: 999px;
|
||||
@@ -0,0 +1,221 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
/** 后端统一响应结构 */
|
||||
export interface ResponseStructure<T = any> {
|
||||
success: boolean
|
||||
msg?: string
|
||||
showType?: number // 0-2: Toast 提示; 3-5: Modal 通知; 99: 静默处理
|
||||
description?: string
|
||||
placement?: string
|
||||
data?: T
|
||||
}
|
||||
|
||||
/** 请求配置(兼容 Taro.request.Option) */
|
||||
export interface RequestConfig {
|
||||
url: string
|
||||
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS' | 'HEAD' | 'TRACE' | 'CONNECT'
|
||||
data?: any
|
||||
header?: Record<string, any>
|
||||
timeout?: number
|
||||
dataType?: 'json' | 'text' | 'base64' | 'arraybuffer'
|
||||
responseType?: 'text' | 'arraybuffer'
|
||||
}
|
||||
|
||||
// ==================== 配置常量 ====================
|
||||
/** 请求超时时间 */
|
||||
const REQUEST_TIMEOUT = 10000
|
||||
/** 登录页面 */
|
||||
const LOGIN_PATH = '/pages/login/index'
|
||||
/** API 基础地址 */
|
||||
const BASE_URL = 'http://localhost:8000'
|
||||
|
||||
/**
|
||||
* 解析请求 URL,自动拼接 BASE_URL
|
||||
*/
|
||||
function resolveUrl(url: string): string {
|
||||
// 已经是完整 URL
|
||||
if (/^https?:\/\//i.test(url)) {
|
||||
return url
|
||||
}
|
||||
return BASE_URL + url
|
||||
}
|
||||
|
||||
// ==================== 错误处理 ====================
|
||||
|
||||
/**
|
||||
* 处理网络错误(HTTP 状态码错误)
|
||||
*/
|
||||
async function handleNetworkError(statusCode?: number): Promise<void> {
|
||||
if (!statusCode) {
|
||||
Taro.showToast({ title: '无法连接到服务器!', icon: 'none', duration: 2000 })
|
||||
return
|
||||
}
|
||||
|
||||
// 401 未授权
|
||||
if (statusCode === 401) {
|
||||
Taro.showToast({ title: '您未登录,或者登录已经超时,请先登录!', icon: 'none', duration: 2500 })
|
||||
|
||||
// 清除本地认证信息
|
||||
Taro.removeStorageSync('token')
|
||||
Taro.removeStorageSync('refresh_token')
|
||||
Taro.removeStorageSync('auth-storage')
|
||||
|
||||
// 避免在登录页重复跳转
|
||||
const pages = Taro.getCurrentPages()
|
||||
const currentRoute = pages.length > 0 ? pages[pages.length - 1].route : ''
|
||||
if (currentRoute !== LOGIN_PATH.replace(/^\//, '')) {
|
||||
Taro.redirectTo({ url: LOGIN_PATH })
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const errorMessages: Record<number, string> = {
|
||||
302: '接口重定向了!',
|
||||
400: '参数不正确!',
|
||||
403: '您没有权限操作!',
|
||||
404: '请求错误,未找到该资源!',
|
||||
408: '请求超时!',
|
||||
409: '系统已存在相同数据!',
|
||||
500: '服务器内部错误!',
|
||||
501: '服务未实现!',
|
||||
502: '网关错误!',
|
||||
503: '服务不可用!',
|
||||
504: '服务暂时无法访问,请稍后再试!',
|
||||
505: 'HTTP 版本不受支持!',
|
||||
}
|
||||
|
||||
const message = errorMessages[statusCode] || `连接错误 (状态码: ${statusCode})`
|
||||
Taro.showToast({ title: message, icon: 'none', duration: 2500 })
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理业务错误(接口返回的业务逻辑错误)
|
||||
*/
|
||||
async function handleBusinessError(data: ResponseStructure): Promise<void> {
|
||||
const { msg = '', showType = 0, description = '' } = data
|
||||
|
||||
switch (showType) {
|
||||
case 99: // 静默处理
|
||||
break
|
||||
case 0: // Toast 成功
|
||||
if (msg) Taro.showToast({ title: msg, icon: 'success', duration: 2000 })
|
||||
break
|
||||
case 1: // Toast 警告
|
||||
if (msg) Taro.showToast({ title: msg, icon: 'none', duration: 2500 })
|
||||
break
|
||||
case 2: // Toast 错误
|
||||
if (msg) Taro.showToast({ title: msg, icon: 'error', duration: 2500 })
|
||||
break
|
||||
case 3: // Modal 成功
|
||||
if (msg) Taro.showModal({ title: msg, content: description || '', showCancel: false })
|
||||
break
|
||||
case 4: // Modal 警告
|
||||
if (msg) Taro.showModal({ title: msg, content: description || '', showCancel: false })
|
||||
break
|
||||
case 5: // Modal 错误
|
||||
if (msg) Taro.showModal({ title: msg, content: description || '', showCancel: false })
|
||||
break
|
||||
default:
|
||||
if (msg) Taro.showToast({ title: msg, icon: 'none', duration: 2500 })
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 核心请求函数 ====================
|
||||
|
||||
/**
|
||||
* 创建请求
|
||||
* @param config - 请求配置
|
||||
* @returns Promise<ResponseStructure<Data>>
|
||||
*/
|
||||
export async function createRequest<Data = any>(
|
||||
config: RequestConfig,
|
||||
): Promise<ResponseStructure<Data>> {
|
||||
|
||||
// 2. 构建 headers
|
||||
const header: Record<string, any> = { ...(config.header || {}) }
|
||||
|
||||
// 附加国际化语言
|
||||
try {
|
||||
const language = Taro.getStorageSync('i18nextLng')
|
||||
if (language) {
|
||||
header['User-Language'] = language
|
||||
}
|
||||
} catch {
|
||||
// storage 读取失败时忽略
|
||||
}
|
||||
|
||||
// 附加 Token(排除登录接口)
|
||||
try {
|
||||
const token = Taro.getStorageSync('token')
|
||||
if (token) {
|
||||
header['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
} catch {
|
||||
// storage 读取失败时忽略
|
||||
}
|
||||
|
||||
try {
|
||||
// 4. 发起请求
|
||||
const task = Taro.request<ResponseStructure<Data>>({
|
||||
url: resolveUrl(config.url),
|
||||
method: config.method || 'GET',
|
||||
data: config.data,
|
||||
header,
|
||||
timeout: config.timeout || REQUEST_TIMEOUT,
|
||||
dataType: config.dataType || 'json',
|
||||
responseType: config.responseType,
|
||||
})
|
||||
|
||||
const response = await task
|
||||
|
||||
const body = response.data as ResponseStructure<Data>
|
||||
|
||||
if (body?.success) {
|
||||
return body
|
||||
}
|
||||
|
||||
// 8. 业务失败:显示错误提示并 reject
|
||||
if (body) {
|
||||
await handleBusinessError(body)
|
||||
}
|
||||
|
||||
return Promise.reject(body)
|
||||
} catch (err: any) {
|
||||
|
||||
// 请求被取消(去重或手动取消)
|
||||
if (err?.errMsg?.includes('abort') || err?.errMsg?.includes('cancel')) {
|
||||
console.warn('请求已取消:', err.errMsg)
|
||||
return Promise.reject(err)
|
||||
}
|
||||
|
||||
// 有 HTTP 状态码但非 2xx
|
||||
if (err?.statusCode) {
|
||||
await handleNetworkError(err.statusCode)
|
||||
return Promise.reject(err)
|
||||
}
|
||||
|
||||
// 网络超时
|
||||
if (err?.errMsg?.includes('timeout')) {
|
||||
Taro.showToast({ title: '请求超时,请稍后重试!', icon: 'none', duration: 2500 })
|
||||
return Promise.reject(err)
|
||||
}
|
||||
|
||||
// 网络连接失败
|
||||
if (err?.errMsg?.includes('fail') || err?.message?.includes('Network Error')) {
|
||||
Taro.showToast({ title: '网络连接失败,请检查网络!', icon: 'none', duration: 2500 })
|
||||
return Promise.reject(err)
|
||||
}
|
||||
|
||||
// 其他未知错误
|
||||
Taro.showToast({
|
||||
title: `网络错误: ${err?.errMsg || err?.message || '未知错误'}`,
|
||||
icon: 'none',
|
||||
duration: 2500,
|
||||
})
|
||||
return Promise.reject(err)
|
||||
}
|
||||
}
|
||||
|
||||
export default createRequest
|
||||
Reference in New Issue
Block a user