从零搭建移动端 H5 审批系统:Vue3 全家桶实战
以开发者视角,基于 Vite 官方脚手架快速起步,逐步集成路由、状态管理、请求层、UI 组件库和移动端适配能力,完成一个可落地的审批 H5 基础工程。
一、业务场景
企业内部办公系统的典型链路:
审批流转产生待办 → IM 推送消息 → 用户点击 → 打开 H5 页面 → 审批操作
项目产出是一个跑在 App 内置 WebView 中的 H5 页面,包含审批列表和审批详情。
二、技术选型
|
层面 |
选型 |
|
框架 |
Vue 3.4 + Composition API |
|
构建 |
Vite 5 |
|
类型 |
TypeScript 5 |
|
UI 库 |
Vant 4(移动端 Vue3 最成熟的组件库) |
|
路由 |
Vue Router 4(Hash 模式,WebView 兼容性最佳) |
|
状态管理 |
Pinia 2 |
|
请求库 |
alova 3 + Axios(声明式请求管理 + 底层网络) |
|
CSS |
UnoCSS 原子化 + Less |
|
移动适配 |
postcss-px-to-viewport-8-plugin(px → vw,375 设计稿) |
包管理器统一使用 pnpm。
三、项目初始化
实际开发中,开发者不会从空目录一步步 mkdir、npm init。Vite 官方提供了成熟的项目模板,一行命令就能生成结构完整、配置齐全的基础工程。
3.1 脚手架创建项目
pnpm create vite approval-h5 --template vue-ts
cd approval-h5
pnpm install
--template vue-ts 指定 Vue3 + TypeScript 模板。执行完即得到一个可运行的标准项目:
approval-h5/
├── index.html
├── package.json
├── tsconfig.json
├── tsconfig.app.json
├── tsconfig.node.json
├── vite.config.ts
├── public/
└── src/
├── main.ts
├── App.vue
├── style.css
└── vite-env.d.ts
验证:
pnpm dev
浏览器访问 http://localhost:5173,看到 Vue 默认页说明初始化成功。
脚手架已经帮你配好了 vite.config.ts、tsconfig 系列、package.json 的 scripts。你不必从零写 Vite 插件配置和 TypeScript 编译选项,精力留在架构设计上。
3.2 安装运行时依赖
脚手架给了 Vue3 + Vite + TypeScript 的底座,追加业务核心依赖:
# 路由 & 状态管理
pnpm add vue-router@^4.6.4 pinia@^2.3.1
# UI 组件库
pnpm add vant@^4.9.24
# 请求库(alova + Axios 做底层适配器)
pnpm add alova@^3.5.1 @alova/adapter-axios@^2.0.18 axios@^1.7.2
3.3 安装开发依赖
# 原子化 CSS
pnpm add -D unocss@^66.7.2 less@^4.2.0
# Vant 按需引入
pnpm add -D unplugin-auto-import@^21.0.0 unplugin-vue-components@^32.1.0 @vant/auto-import-resolver@^1.3.0
# 移动端 px → vw 自动转换
pnpm add -D postcss-px-to-viewport-8-plugin@^1.2.5
# Node 类型声明(vite.config.ts 中 resolve 需要)
pnpm add -D @types/node@^20.14.2
3.4 创建业务目录
脚手架提供的是通用骨架,不含业务分层。在 src/ 下追加本项目需要的目录:
mkdir -p src/{router,store/modules,api/modules,utils/platforms,views/approval-detail,views/approval-list,styles}
mkdir -p types
此时项目结构演变为:
approval-h5/
├── index.html # 脚手架生成
├── package.json # 脚手架生成 + 新增依赖
├── tsconfig.json # 脚手架生成
├── tsconfig.app.json # 脚手架生成
├── tsconfig.node.json # 脚手架生成
├── vite.config.ts # 脚手架生成(后续修改)
├── public/
├── src/
│ ├── main.ts # 脚手架生成(后续修改)
│ ├── App.vue # 脚手架生成(后续修改)
│ ├── style.css # 脚手架生成
│ ├── vite-env.d.ts # 脚手架生成
│ ├── api/modules/ # 新增
│ ├── components/ # 脚手架已有,后续替换
│ ├── router/ # 新增
│ ├── store/modules/ # 新增
│ ├── styles/ # 新增
│ ├── utils/platforms/ # 新增
│ └── views/ # 新增
│ ├── approval-detail/
│ └── approval-list/
└── types/ # 新增
到此骨架就绪,下面逐层完善配置和代码。
四、TypeScript 配置
脚手架已生成 tsconfig.json、tsconfig.app.json、tsconfig.node.json,只需两项调整。
4.1 路径别名
在 tsconfig.app.json 中添加 baseUrl 和 paths,让 TypeScript 识别 @/ 别名:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
脚手架将应用代码的 TS 配置拆分到了 tsconfig.app.json,所以路径别名加在这里。tsconfig.json 通过 references 引用它。
4.2 全局类型声明
创建 types/global.d.ts:
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
interface ImportMetaEnv {
readonly VITE_API_BASE_URL: string
readonly VITE_APP_TITLE: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
另外,unplugin-auto-import 和 unplugin-vue-components 后续会自动生成 types/auto-imports.d.ts 和 types/components.d.ts,提前创建占位文件避免首次构建类型报错。
五、Vite 配置
脚手架已生成 vite.config.ts,在此基础上追加四个插件和相关配置:
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
import UnoCSS from 'unocss/vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { VantResolver } from '@vant/auto-import-resolver'
export default defineConfig(({ mode }) => ({
plugins: [
vue(),
UnoCSS(),
AutoImport({
imports: ['vue', 'vue-router', 'pinia'],
dts: 'types/auto-imports.d.ts',
resolvers: [VantResolver()],
}),
Components({
resolvers: [VantResolver()],
dts: 'types/components.d.ts',
}),
],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
server: {
host: '0.0.0.0',
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
}))
四个插件的分工:
|
插件 |
作用 |
|
|
编译 |
|
|
原子化 CSS 按需生成 |
|
|
自动 import |
|
|
自动按需引入 Vant 组件,Tree Shaking |
VantResolver() 让你在模板里写 <van-button> 不用 import,构建时只打包用到的 Vant 组件,从 600KB 砍到 160KB。
六、入口文件与路由
6.1 index.html
脚手架已生成 index.html,调整 viewport 为移动端适配版本:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0,
minimum-scale=1.0, user-scalable=no, viewport-fit=cover"
/>
<meta name="format-detection" content="telephone=no" />
<title>审批系统</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
viewport 关键属性:
user-scalable=no— 禁止用户双指缩放viewport-fit=cover— 撑满 iPhone 刘海屏,配合safe-area-inset-bottom适配安全区format-detection=telephone=no— 禁止 iOS Safari 把数字识别成电话号码
6.2 应用入口
脚手架已生成 src/main.ts 和 src/App.vue,改造为:
// src/main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import 'uno.css'
import { initPlatform } from './utils/platform'
async function bootstrap() {
const app = createApp(App)
app.use(createPinia())
app.use(router)
// 初始化平台适配层(鉴权、JSBridge 等)
await initPlatform()
app.mount('#app')
}
bootstrap()
<!-- src/App.vue -->
<template>
<div class="min-h-screen bg-#f7f8fa">
<router-view />
</div>
</template>
6.3 路由
// src/router/index.ts
import { createRouter, createWebHashHistory } from 'vue-router'
const routes = [
{
path: '/',
redirect: '/approval-list',
},
{
path: '/approval-list',
name: 'ApprovalList',
component: () => import('@/views/approval-list/index.vue'),
meta: { title: '审批列表' },
},
{
path: '/approval-detail/:id',
name: 'ApprovalDetail',
component: () => import('@/views/approval-detail/index.vue'),
meta: { title: '审批详情' },
},
]
const router = createRouter({
history: createWebHashHistory(),
routes,
})
router.beforeEach((to) => {
document.title = (to.meta.title as string) || '审批系统'
})
export default router
三个设计决策:
createWebHashHistory()— Hash 模式,所有 WebView 环境通吃() => import(...)— 路由懒加载,按页面拆 chunkbeforeEach动态 title — 每个页面独立标题,导航栏和浏览器标签同步
七、请求层
7.1 alova 实例
// src/api/index.ts
import { createAlova } from 'alova'
import { axiosRequestAdapter } from '@alova/adapter-axios'
import axios from 'axios'
import { useUserStore } from '@/store/modules/user'
const httpInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 15000,
headers: { 'Content-Type': 'application/json' },
})
// 请求拦截:自动注入 token
httpInstance.interceptors.request.use((config) => {
const userStore = useUserStore()
if (userStore.token) {
config.headers.Authorization = `Bearer ${userStore.token}`
}
return config
})
// 响应拦截:统一错误处理
httpInstance.interceptors.response.use(
(response) => response.data,
(error) => {
const msg = error.response?.data?.message || '请求失败'
return Promise.reject(new Error(msg))
}
)
// 创建 alova 实例,用 Axios 做底层适配器
const alovaInstance = createAlova({
requestAdapter: axiosRequestAdapter({ axios: httpInstance }),
responded: {
onSuccess: async (response) => {
const res = response as { code: number; data: any; message?: string }
if (res.code === 0) return res.data
throw new Error(res.message || '请求异常')
},
onError: (error) => { throw error },
},
})
export default alovaInstance
alova 的 requestAdapter 让你在享受 alova 声明式 API 的同时,复用 Axios 成熟的拦截器、超时、取消请求等能力。
7.2 业务 API
// src/api/modules/approval.ts
import alovaInstance from '@/api'
export interface ApprovalItem {
id: string
title: string
type: string
applicant: string
status: 'pending' | 'approved' | 'rejected'
createTime: string
}
export function getApprovalList(params: { status?: string; page: number; pageSize: number }) {
return alovaInstance.Get<{ list: ApprovalItem[]; total: number }>(
'/api/approval/list',
{ params }
)
}
export function getApprovalDetail(id: string) {
return alovaInstance.Get<ApprovalItem>(`/api/approval/detail/${id}`)
}
export function submitApproval(data: { id: string; action: string; comment?: string }) {
return alovaInstance.Post('/api/approval/submit', data)
}
// src/api/modules/auth.ts
import alovaInstance from '@/api'
export function login(credential: string) {
return alovaInstance.Post<{ token: string; userId: string; userName: string }>(
'/api/auth/login',
{ credential }
)
}
八、状态管理
// src/store/modules/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { getUserCredential } from '@/utils/platform'
import { login } from '@/api/modules/auth'
export const useUserStore = defineStore('user', () => {
const token = ref('')
const userId = ref('')
const userName = ref('')
const isLoggedIn = computed(() => !!token.value)
async function initUser() {
const credential = await getUserCredential()
if (!credential) return
const userInfo = await login(credential)
token.value = userInfo.token
userId.value = userInfo.userId
userName.value = userInfo.userName
}
return { token, userId, userName, isLoggedIn, initUser }
})
初始化链路:
getUserCredential() → login(credential) → Pinia store
平台获取身份凭证(code/token) 后端换 token + 用户信息 存入全局状态
九、平台适配层
整个项目架构中最关键的设计。不同 IM 平台(钉钉/飞书/企业微信/内部 IM)的差异只在于两点:用户身份怎么获取、原生能力怎么调用。用适配器模式抽象它。
9.1 适配器接口 + 默认实现
// src/utils/platform.ts
export interface PlatformAdapter {
name: string
init(): Promise<boolean>
getUserCredential(): Promise<string>
closeWindow(): void
takePhoto?(): Promise<string>
}
// 默认适配器:纯浏览器环境,从 URL 参数取 token
const defaultAdapter: PlatformAdapter = {
name: 'default-browser',
async init() {
return true
},
async getUserCredential() {
const params = new URLSearchParams(window.location.search)
return params.get('token') || params.get('auth_code') || ''
},
closeWindow() {
if (history.length > 1) {
history.back()
} else {
window.close()
}
},
}
let currentAdapter: PlatformAdapter = defaultAdapter
export function registerPlatformAdapter(adapter: PlatformAdapter) {
currentAdapter = adapter
}
export async function initPlatform() {
await currentAdapter.init()
}
export async function getUserCredential() {
return currentAdapter.getUserCredential()
}
export function closePlatformWindow() {
currentAdapter.closeWindow()
}
9.2 钉钉适配器(示例)
// src/utils/platforms/dingtalk.ts
import type { PlatformAdapter } from '../platform'
export const dingtalkAdapter: PlatformAdapter = {
name: 'dingtalk',
async init() {
return new Promise((resolve) => {
;(window as any).dd.ready(() => resolve(true))
;(window as any).dd.error(() => resolve(false))
})
},
async getUserCredential() {
return new Promise((resolve) => {
;(window as any).dd.runtime.permission.requestAuthCode({
corpId: import.meta.env.VITE_DD_CORP_ID,
onSuccess: (result: any) => resolve(result.code),
onFail: () => resolve(''),
})
})
},
closeWindow() {
;(window as any).dd.biz.navigation.close()
},
}
9.3 接入内部平台
假设内部办公软件的 JSBridge 叫 MyIMBridge:
// src/utils/platforms/my-im.ts
import type { PlatformAdapter } from '../platform'
export const myIMAdapter: PlatformAdapter = {
name: 'my-im',
async init() {
return window.MyIMBridge.init()
},
async getUserCredential() {
return window.MyIMBridge.getToken()
},
closeWindow() {
window.MyIMBridge.close()
},
}
在入口注册:
// src/main.ts —— 加两行
import { myIMAdapter } from './utils/platforms/my-im'
import { registerPlatformAdapter } from './utils/platform'
registerPlatformAdapter(myIMAdapter)
设计原则:默认适配器能独立跑通全流程(URL 传 token),新平台只加文件不删代码——开闭原则。
十、移动端适配
10.1 px → vw
postcss.config.cjs(后缀必须是 .cjs,因为脚手架 "type": "module"):
module.exports = {
plugins: {
'postcss-px-to-viewport-8-plugin': {
viewportWidth: 375,
unitPrecision: 5,
viewportUnit: 'vw',
minPixelValue: 1,
mediaQuery: false,
},
},
}
效果:width: 100px 构建后自动变成 width: 26.66667vw。
10.2 UnoCSS
unocss.config.ts:
import { defineConfig, presetUno, presetAttributify } from 'unocss'
export default defineConfig({
presets: [presetUno(), presetAttributify()],
shortcuts: {
'flex-center': 'flex items-center justify-center',
'flex-between': 'flex items-center justify-between',
'text-ellipsis': 'overflow-hidden text-ellipsis whitespace-nowrap',
},
theme: {
colors: {
primary: '#1989fa',
success: '#07c160',
warning: '#ff976a',
danger: '#ee0a24',
},
},
})
模板里直接用原子类:
<div class="flex items-center gap-12px mb-16px">
<span class="color-primary font-500">同意</span>
</div>
10.3 全局样式 + 安全区
// src/styles/global.less
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
font-size: 14px;
color: #323233;
background: #f7f8fa;
-webkit-font-smoothing: antialiased;
}
// iPhone 刘海屏底部安全区
body {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
10.4 环境变量
# .env.development
VITE_API_BASE_URL=/api
VITE_APP_TITLE=审批系统
# .env.production
VITE_API_BASE_URL=https://api.your-domain.com
VITE_APP_TITLE=审批系统
十一、页面实现
11.1 审批列表页
<template>
<div class="approval-list-page">
<van-nav-bar title="审批列表" fixed placeholder />
<van-tabs v-model:active="activeTab" @change="onTabChange">
<van-tab title="全部" name="" />
<van-tab title="待审批" name="pending" />
<van-tab title="已通过" name="approved" />
<van-tab title="已驳回" name="rejected" />
</van-tabs>
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<div
v-for="item in list"
:key="item.id"
class="approval-card"
@click="goDetail(item.id)"
>
<div class="flex-between">
<span class="text-15px font-500">{{ item.title }}</span>
<van-tag :type="statusTagType(item.status)" size="medium">
{{ statusLabel(item.status) }}
</van-tag>
</div>
<div class="flex-between mt-8px text-13px color-#999">
<span>{{ item.applicant }}</span>
<span>{{ item.createTime }}</span>
</div>
</div>
</van-list>
</van-pull-refresh>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { showToast } from 'vant'
import { getApprovalList } from '@/api/modules/approval'
import type { ApprovalItem } from '@/api/modules/approval'
const router = useRouter()
const activeTab = ref('')
const list = ref<ApprovalItem[]>([])
const refreshing = ref(false)
const loading = ref(false)
const finished = ref(false)
let page = 1
async function onLoad() {
try {
const res = await getApprovalList({
status: activeTab.value,
page: page++,
pageSize: 20,
})
list.value.push(...res.list)
if (list.value.length >= res.total) finished.value = true
} catch {
showToast('加载失败')
}
loading.value = false
}
async function onRefresh() {
page = 1
finished.value = false
list.value = []
refreshing.value = false
await onLoad()
}
function onTabChange() {
onRefresh()
}
function goDetail(id: string) {
router.push(`/approval-detail/${id}`)
}
function statusTagType(status: string): 'warning' | 'success' | 'danger' | 'default' {
const map: Record<string, 'warning' | 'success' | 'danger'> = {
pending: 'warning',
approved: 'success',
rejected: 'danger',
}
return map[status] || 'default'
}
function statusLabel(status: string) {
const map: Record<string, string> = {
pending: '待审批',
approved: '已通过',
rejected: '已驳回',
}
return map[status] || status
}
</script>
页面组成:van-nav-bar 导航 + van-tabs 状态筛选 + van-pull-refresh 下拉刷新 + van-list 上拉加载。
11.2 审批详情页
<template>
<div class="approval-detail-page pb-70px">
<van-nav-bar
title="审批详情"
left-text="返回"
left-arrow
fixed placeholder
@click-left="router.back"
/>
<van-cell-group inset>
<van-cell
v-for="(value, key) in detail.formData"
:key="key"
:title="key"
:value="String(value)"
/>
</van-cell-group>
<van-cell-group inset>
<van-steps direction="vertical" :active="detail.approvalHistory?.length">
<van-step v-for="item in detail.approvalHistory" :key="item.time">
<template #active-icon>
<van-icon name="checked" color="#07c160" />
</template>
<div class="text-14px font-500">{{ item.nodeName }}</div>
<div class="text-12px color-#999">
{{ item.approver }} · {{ item.time }}
</div>
</van-step>
</van-steps>
</van-cell-group>
<ApprovalActions
v-if="detail.status === 'pending'"
@approve="handleApprove"
@reject="handleReject"
/>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { showToast } from 'vant'
import { getApprovalDetail, submitApproval } from '@/api/modules/approval'
import { closePlatformWindow } from '@/utils/platform'
const route = useRoute()
const router = useRouter()
const detail = ref<any>({})
onMounted(async () => {
try {
detail.value = await getApprovalDetail(route.params.id as string)
} catch {
showToast('加载失败')
}
})
async function handleApprove(comment: string) {
try {
await submitApproval({ id: route.params.id as string, action: 'approve', comment })
showToast('审批通过')
setTimeout(closePlatformWindow, 1000)
} catch {
showToast('操作失败')
}
}
async function handleReject(comment: string) {
try {
await submitApproval({ id: route.params.id as string, action: 'reject', comment })
showToast('已驳回')
setTimeout(closePlatformWindow, 1000)
} catch {
showToast('操作失败')
}
}
</script>
11.3 审批操作组件
<template>
<div class="fixed bottom-0 left-0 right-0 flex gap-12px p-16px bg-white approval-actions">
<van-button
round
block
type="danger"
:loading="loading"
@click="showRejectDialog = true"
>
驳回
</van-button>
<van-button
round
block
type="primary"
:loading="loading"
@click="$emit('approve', '')"
>
同意
</van-button>
<van-dialog
v-model:show="showRejectDialog"
title="驳回理由"
show-cancel-button
@confirm="onRejectConfirm"
>
<van-field
v-model="rejectComment"
type="textarea"
placeholder="请输入驳回理由"
rows="3"
/>
</van-dialog>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
defineProps<{ loading?: boolean }>()
const emit = defineEmits<{
approve: [comment: string]
reject: [comment: string]
}>()
const showRejectDialog = ref(false)
const rejectComment = ref('')
function onRejectConfirm() {
emit('reject', rejectComment.value)
showRejectDialog.value = false
rejectComment.value = ''
}
</script>
<style scoped>
.approval-actions {
padding-bottom: calc(16px + env(safe-area-inset-bottom));
box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.06);
}
</style>
十二、构建验证
pnpm build
输出:
✓ 35 modules transformed.
dist/index.html 0.46 kB │ gzip: 0.29 kB
dist/assets/index-CjqzWpxW.css 203.54 kB │ gzip: 54.23 kB
dist/assets/index-Bcj8A2oL.js 162.87 kB │ gzip: 63.90 kB
✓ built in 2.61s
产物分析:
|
文件 |
原始大小 |
Gzip |
|
HTML 入口 |
0.46 KB |
0.29 KB |
|
CSS(含 Vant 按需样式) |
203 KB |
54 KB |
|
JS 主包 |
162 KB |
64 KB |
|
总计 |
~366 KB |
~118 KB |
Gzip 后约 118KB,3G 网络下 1.5 秒完成传输。
十三、最终目录结构
approval-h5/
├── index.html # 脚手架生成,已修改 viewport
├── package.json # 脚手架生成,已追加依赖
├── vite.config.ts # 脚手架生成,已追加插件和配置
├── tsconfig.json # 脚手架生成
├── tsconfig.app.json # 脚手架生成,已添加路径别名
├── tsconfig.node.json # 脚手架生成
├── unocss.config.ts # 新增
├── postcss.config.cjs # 新增
├── .env.development # 新增
├── .env.production # 新增
│
├── src/
│ ├── main.ts # 脚手架生成,已重写
│ ├── App.vue # 脚手架生成,已重写
│ ├── router/index.ts # 新增
│ ├── store/
│ │ └── modules/user.ts # 新增
│ ├── api/
│ │ ├── index.ts # 新增
│ │ └── modules/
│ │ ├── auth.ts # 新增
│ │ └── approval.ts # 新增
│ ├── utils/
│ │ ├── platform.ts # 新增
│ │ └── platforms/dingtalk.ts # 新增
│ ├── views/
│ │ ├── approval-list/index.vue # 新增
│ │ └── approval-detail/index.vue # 新增
│ ├── components/
│ │ └── ApprovalActions.vue # 新增
│ └── styles/global.less # 新增
│
└── types/
├── global.d.ts # 新增
├── auto-imports.d.ts # 插件自动生成
└── components.d.ts # 插件自动生成
十四、开发命令速查
cd approval-h5
# 启动开发服务器 → http://localhost:3000
pnpm dev
# 类型检查 + 生产构建
pnpm build
# 本地预览生产构建
pnpm preview
十五、踩坑记录
|
# |
问题 |
原因 |
解决 |
|
1 |
首 |
|
在 |
|
2 |
postcss 配置不生效 |
脚手架默认 |
改为 |
|
3 |
|
alova 3.x 适配器 API 变化,需要传 |
写成 |
|
4 |
Vant Tag 的 |
Vant 把 |
返回值显式声明为 |


浙公网安备 33010602011771号