将现有的 uniapp 订餐系统重构为基于 LINE LIFF 的 Web 应用
/orderApp/online-order-uniapp (Vue 3 + uniapp)/orderApp/line-order-app (Vue 3 + LIFF + Vite)| 模块 | 技术方案 | 说明 |
|---|---|---|
| 构建工具 | Vite 5.x | 快速、现代化 |
| HTTP请求 | Axios | 成熟稳定,拦截器逻辑可复用 |
| WebSocket | Socket.IO Client | 自动重连、更好的错误处理 |
| UI组件库 | Vant 4 | 移动端优化,完善的组件 |
| 状态管理 | Pinia 2.x | 从原项目100%迁移 |
| 国际化 | vue-i18n 9.x | 从原项目100%迁移 |
| 路由 | Vue Router 4 | SPA标准路由 |
| LINE集成 | @line/liff | LINE官方SDK |
| 工具库 | @vueuse/core、dayjs | 现代化工具集 |
line-order-app/
├── public/ # 静态资源
│ ├── favicon.ico
│ └── liff-starter.html # LIFF启动页
│
├── src/
│ ├── api/ # API接口层 [从uniapp迁移80%]
│ │ ├── request.js # Axios封装
│ │ ├── address.js
│ │ ├── auth.js
│ │ ├── coupon.js
│ │ ├── goods.js
│ │ ├── market.js
│ │ ├── merchant.js
│ │ ├── order.js
│ │ ├── score.js
│ │ ├── user.js
│ │ └── index.js
│ │
│ ├── assets/ # 资源文件
│ │ ├── images/
│ │ └── styles/
│ │ ├── reset.css
│ │ ├── variables.css
│ │ └── common.css
│ │
│ ├── components/ # 公共组件 [重写]
│ │ ├── common/
│ │ │ ├── YImage.vue # 图片组件
│ │ │ ├── YNavBar.vue # 导航栏
│ │ │ └── YTabBar.vue # 底部Tab
│ │ └── business/ # 业务组件
│ │
│ ├── composables/ # 组合式函数 [从uniapp改造]
│ │ ├── useAuth.js # 认证逻辑
│ │ ├── useLiff.js # LIFF相关
│ │ ├── useSocket.js # Socket.IO封装
│ │ └── useCart.js # 购物车逻辑
│ │
│ ├── config/ # 配置文件 [从uniapp迁移]
│ │ ├── index.js # 全局配置
│ │ └── constants.js # 常量定义
│ │
│ ├── locale/ # 国际化 [从uniapp复用100%]
│ │ ├── index.js
│ │ ├── zh-Hans.json # 简体中文
│ │ ├── zh-Hant.json # 繁体中文
│ │ ├── ja.json # 日语
│ │ └── en.json # 英语
│ │
│ ├── router/ # 路由配置 [新建]
│ │ ├── index.js
│ │ ├── routes.js
│ │ └── guards.js # 路由守卫
│ │
│ ├── store/ # Pinia状态管理 [从uniapp迁移90%]
│ │ ├── index.js
│ │ ├── modules/
│ │ │ ├── app.js # 应用状态
│ │ │ ├── user.js # 用户状态
│ │ │ ├── cart.js # 购物车
│ │ │ └── order.js # 订单状态
│ │ └── plugins/
│ │ └── persist.js # 持久化配置
│ │
│ ├── utils/ # 工具函数 [从uniapp迁移90%]
│ │ ├── index.js
│ │ ├── auth.js # 认证工具
│ │ ├── storage.js # 存储工具
│ │ ├── format.js # 格式化工具
│ │ ├── validate.js # 验证工具
│ │ ├── image.js # 图片处理
│ │ ├── price.js # 价格计算
│ │ └── logger.js # 日志工具
│ │
│ ├── views/ # 页面视图 [重写]
│ │ ├── index/ # 首页
│ │ │ └── index.vue
│ │ ├── menu/ # 菜单
│ │ │ ├── menu.vue
│ │ │ └── detail.vue
│ │ ├── order/ # 订单
│ │ │ ├── order.vue
│ │ │ └── detail.vue
│ │ ├── mine/ # 我的
│ │ │ ├── mine.vue
│ │ │ └── userinfo.vue
│ │ ├── cart/ # 购物车
│ │ │ └── cart.vue
│ │ ├── login/ # 登录
│ │ │ └── login.vue
│ │ └── payment/ # 支付
│ │ └── pay.vue
│ │
│ ├── App.vue # 根组件
│ └── main.js # 入口文件
│
├── .env.development # 开发环境变量
├── .env.production # 生产环境变量
├── .gitignore
├── index.html
├── package.json
├── vite.config.js # Vite配置
└── REFACTOR_PLAN.md # 本文档
目标: 初始化项目基础结构
cd /Users/lidefan/Develop/orderApp/line-order-app
npm create vite@latest . -- --template vue
# 核心框架
npm install vue@latest vue-router@latest pinia@latest
# HTTP & WebSocket
npm install axios socket.io-client
# UI组件库
npm install vant @vant/area-data
npm install @vant/auto-import-resolver unplugin-vue-components -D
# LINE SDK
npm install @line/liff
# 国际化
npm install vue-i18n@latest
# 工具库
npm install pinia-plugin-persistedstate
npm install dayjs
npm install @vueuse/core
mkdir -p src/{api,assets/{images,styles},components/{common,business},composables,config,locale,router,store/{modules,plugins},utils,views/{index,menu,order,mine,cart,login,payment}}
vite.config.js.env.development 和 .env.production完成标志: ✅ 项目可以运行 npm run dev
目标: 完成基础架构搭建
src/config/index.jsutils/format.js - 时间、价格格式化utils/validate.js - 表单验证utils/image.js - 图片处理utils/storage.js - localStorage封装utils/logger.js - 日志工具代码示例:
// utils/storage.js
export const storage = {
get(key, defaultValue = null) {
const value = localStorage.getItem(key)
try {
return value ? JSON.parse(value) : defaultValue
} catch {
return value || defaultValue
}
},
set(key, value) {
localStorage.setItem(key, JSON.stringify(value))
},
remove(key) {
localStorage.removeItem(key)
}
}
locale/*.json 文件(100%复用)locale/index.jscomposables/useLiff.js完成标志: ✅ 工具函数可用,国际化正常切换,LIFF可初始化
目标: 完成 Pinia Store 迁移
store/index.jsstore/modules/app.js - 应用全局状态store/modules/user.js - 用户信息store/modules/cart.js - 购物车store/modules/order.js - 订单状态代码示例:
// store/modules/user.js (从 uniapp store/store.js 改造)
import { defineStore } from 'pinia'
import { storage } from '@/utils/storage'
export const useUserStore = defineStore('user', {
state: () => ({
member: {},
token: '',
openid: '',
isMer: 0,
merchartShop: {}
}),
getters: {
isLogin: (state) => Object.keys(state.member).length > 0
},
actions: {
setMember(member) {
this.member = member
},
setToken(token) {
this.token = token
storage.set('accessToken', token)
},
logout() {
this.member = {}
this.token = ''
storage.remove('accessToken')
}
},
persist: {
enabled: true,
strategies: [
{
storage: localStorage,
paths: ['member', 'token']
}
]
}
})
完成标志: ✅ Store 可以正常存取数据,持久化生效
目标: 完成 HTTP 请求层迁移
api/request.js代码示例:
// api/request.js
import axios from 'axios'
import { storage } from '@/utils/storage'
import { showToast } from 'vant'
import router from '@/router'
const request = axios.create({
baseURL: import.meta.env.VITE_API_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
'tenant-id': import.meta.env.VITE_TENANT_ID
}
})
// 请求拦截器
request.interceptors.request.use(
config => {
const token = storage.get('accessToken')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
error => Promise.reject(error)
)
// 响应拦截器
request.interceptors.response.use(
response => {
const { data } = response
if (data.code === 401) {
showToast('未登录')
storage.remove('accessToken')
router.push('/login')
return Promise.reject(new Error('未登录'))
}
if (data.code !== 0) {
showToast(data.msg || '请求失败')
return Promise.reject(new Error(data.msg))
}
return data.data
},
error => {
showToast('网络错误')
return Promise.reject(error)
}
)
export default request
api/auth.js - 认证接口api/user.js - 用户接口api/goods.js - 商品接口api/order.js - 订单接口api/address.js - 地址接口api/coupon.js - 优惠券接口api/merchant.js - 商家接口完成标志: ✅ API 可以正常调用,错误处理正确
目标: 完成路由配置
router/routes.jspages.json 迁移路由配置代码示例:
// router/routes.js
export default [
{
path: '/',
redirect: '/index'
},
{
path: '/index',
name: 'Index',
component: () => import('@/views/index/index.vue'),
meta: {
title: 'index.home',
keepAlive: true
}
},
{
path: '/menu',
name: 'Menu',
component: () => import('@/views/menu/menu.vue'),
meta: {
title: 'menu.title',
keepAlive: true
}
},
{
path: '/order',
name: 'Order',
component: () => import('@/views/order/order.vue'),
meta: {
title: 'order.title',
requiresAuth: true
}
},
{
path: '/mine',
name: 'Mine',
component: () => import('@/views/mine/mine.vue'),
meta: {
title: 'mine.title'
}
},
{
path: '/login',
name: 'Login',
component: () => import('@/views/login/login.vue'),
meta: {
title: 'login.title'
}
}
]
router/guards.jsrouter/index.js完成标志: ✅ 路由可以正常跳转,守卫生效
目标: 完成 Socket.IO 集成
composables/useSocket.jshooks/useWebSocket.js 改造代码示例:
// composables/useSocket.js
import { ref, onBeforeUnmount } from 'vue'
import { io } from 'socket.io-client'
import { storage } from '@/utils/storage'
export function useSocket(options = {}) {
const socket = ref(null)
const connected = ref(false)
const connect = () => {
const token = storage.get('accessToken')
socket.value = io(import.meta.env.VITE_WS_URL, {
auth: { token },
transports: ['websocket'],
reconnection: true,
reconnectionDelay: 3000,
reconnectionAttempts: 5,
...options
})
socket.value.on('connect', () => {
console.log('Socket connected')
connected.value = true
options.onConnected?.()
})
socket.value.on('disconnect', () => {
console.log('Socket disconnected')
connected.value = false
options.onDisconnected?.()
})
socket.value.on('message', (data) => {
options.onMessage?.(data)
})
}
const disconnect = () => {
if (socket.value) {
socket.value.disconnect()
socket.value = null
}
}
const emit = (event, data) => {
if (socket.value?.connected) {
socket.value.emit(event, data)
}
}
onBeforeUnmount(() => {
disconnect()
})
return {
socket,
connected,
connect,
disconnect,
emit
}
}
完成标志: ✅ WebSocket 可以正常连接和通信
目标: 使用 Vant 重构所有页面
components/common/YImage.vue - 图片组件components/common/YNavBar.vue - 导航栏components/common/YTabBar.vue - 底部Tabviews/index/index.vue - 首页views/menu/menu.vue - 菜单列表views/menu/detail.vue - 菜单详情views/cart/cart.vue - 购物车views/order/order.vue - 订单列表views/order/detail.vue - 订单详情views/mine/mine.vue - 我的views/login/login.vue - 登录页| uniapp组件 | Vant组件 | 说明 |
|---|---|---|
| uv-button | van-button | 按钮 |
| uv-popup | van-popup | 弹出层 |
| uv-picker | van-picker | 选择器 |
| uv-tabs | van-tabs | 标签页 |
| uv-list | van-list | 列表 |
| uv-cell | van-cell | 单元格 |
| uv-image | van-image | 图片 |
| uv-icon | van-icon | 图标 |
| uv-navbar | van-nav-bar | 导航栏 |
| uv-tabbar | van-tabbar | 标签栏 |
完成标志: ✅ 所有页面开发完成,UI符合设计
目标: 集成 LINE 特有功能
main.js 初始化 LIFF代码示例:
// composables/useLiff.js
import { ref } from 'vue'
import liff from '@line/liff'
export function useLiff() {
const isReady = ref(false)
const isLoggedIn = ref(false)
const profile = ref(null)
const init = async () => {
try {
await liff.init({ liffId: import.meta.env.VITE_LIFF_ID })
isReady.value = true
if (liff.isLoggedIn()) {
isLoggedIn.value = true
profile.value = await liff.getProfile()
}
} catch (error) {
console.error('LIFF init error:', error)
}
}
const login = () => {
if (!liff.isLoggedIn()) {
liff.login()
}
}
const logout = () => {
liff.logout()
isLoggedIn.value = false
profile.value = null
}
const shareTargetPicker = (messages) => {
if (liff.isApiAvailable('shareTargetPicker')) {
return liff.shareTargetPicker(messages)
}
}
return {
isReady,
isLoggedIn,
profile,
init,
login,
logout,
shareTargetPicker
}
}
完成标志: ✅ LIFF 功能正常工作
目标: 测试和性能优化
完成标志: ✅ 所有功能测试通过,性能达标
# 安装依赖
npm install
# 开发模式
npm run dev
# 构建生产
npm run build
# 预览构建结果
npm run preview
# 代码检查
npm run lint
# 代码格式化
npm run format
# API地址
VITE_API_URL=https://api-dev.example.com
# WebSocket地址
VITE_WS_URL=wss://ws-dev.example.com
# LIFF ID
VITE_LIFF_ID=your-liff-id-dev
# 租户ID
VITE_TENANT_ID=1
# API地址
VITE_API_URL=https://api.example.com
# WebSocket地址
VITE_WS_URL=wss://ws.example.com
# LIFF ID
VITE_LIFF_ID=your-liff-id-prod
# 租户ID
VITE_TENANT_ID=1
| uniapp API | Web 标准 API | 说明 |
|---|---|---|
| uni.getStorageSync() | localStorage.getItem() | 本地存储 |
| uni.setStorageSync() | localStorage.setItem() | 本地存储 |
| uni.showToast() | showToast() (Vant) | 提示 |
| uni.showLoading() | showLoadingToast() (Vant) | 加载 |
| uni.navigateTo() | router.push() | 路由跳转 |
| uni.redirectTo() | router.replace() | 路由替换 |
| uni.switchTab() | router.replace() | Tab切换 |
| uni.request() | axios() | 网络请求 |
| uni.connectSocket() | io() (Socket.IO) | WebSocket |
vh、vw 替代 rpx| 阶段 | 时间 | 任务 |
|---|---|---|
| 阶段0 | 0.5天 | 环境准备 |
| 阶段1 | 1天 | 核心配置迁移 |
| 阶段2 | 1天 | 状态管理迁移 |
| 阶段3 | 1天 | API层重构 |
| 阶段4 | 1天 | 路由系统搭建 |
| 阶段5 | 1天 | WebSocket重构 |
| 阶段6 | 5天 | UI组件开发 |
| 阶段7 | 2天 | LIFF功能集成 |
| 阶段8 | 2天 | 测试与优化 |
| 总计 | 14.5天 | 约3周 |
如遇问题,请查阅本文档或相关技术文档。
开始重构: 准备好后,请从 阶段 0 开始执行!