🎨 优化点餐页面&完善分类和菜品管理
This commit is contained in:
@@ -9,7 +9,7 @@ const API = {
|
||||
login: data => net.post('/user/login', data, false), // 登录
|
||||
getUserInfo: () => net.get('/user/info', {}, true), // 获取用户信息
|
||||
updateUserInfo: data => net.put('/user/update', data, true), // 更新用户信息
|
||||
upload: data => net.post('/user/upload', data, true), // 上传头像
|
||||
upload: data => net.post('/user/upload', data, true), // 通用上传接口
|
||||
getUserInfoById: (id) => net.get(`/user/info/${id}`, {}, true), // 根据ID获取用户信息
|
||||
getLoverInfo: () => net.get('/user/lover', {}, true), // 获取情侣关系信息(包含 startDate 与 loverId)
|
||||
|
||||
|
||||
@@ -76,6 +76,34 @@
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/order/confirm",
|
||||
"style": {
|
||||
"navigationBarTitleText": "确认订单",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/order/result",
|
||||
"style": {
|
||||
"navigationBarTitleText": "订单结果",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/food/category-manage",
|
||||
"style": {
|
||||
"navigationBarTitleText": "分类管理",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/food/food-manage",
|
||||
"style": {
|
||||
"navigationBarTitleText": "菜品管理",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/diary/diary",
|
||||
"style": {
|
||||
|
||||
107
src/pages/food/category-manage.vue
Normal file
107
src/pages/food/category-manage.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="header">
|
||||
<text class="title">分类管理</text>
|
||||
<button class="add" @click="startAdd">新增分类</button>
|
||||
</view>
|
||||
|
||||
<view v-for="item in list" :key="item.ID" class="row">
|
||||
<text class="name">{{ item.name }}</text>
|
||||
<view class="ops">
|
||||
<button size="mini" class="op-btn edit" @click="startEdit(item)">编辑</button>
|
||||
<button size="mini" class="op-btn delete" @click="remove(item)">删除</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<uni-popup ref="popup" type="center">
|
||||
<view class="dialog">
|
||||
<text class="dialog-title">{{ current?.ID ? '编辑分类' : '新增分类' }}</text>
|
||||
<uni-easyinput v-model="form.name" placeholder="请输入分类名称"></uni-easyinput>
|
||||
<view class="dialog-actions">
|
||||
<button @click="close">取消</button>
|
||||
<button type="primary" @click="save">保存</button>
|
||||
</view>
|
||||
</view>
|
||||
</uni-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import api from '@/api'
|
||||
|
||||
const list = ref([])
|
||||
const popup = ref(null)
|
||||
const current = ref(null)
|
||||
const form = ref({ name: '' })
|
||||
|
||||
const load = async () => {
|
||||
const res = await api.getFoodCategoryList({ page: 1, pageSize: 100 })
|
||||
if (res?.code === 0) list.value = res.data?.list || []
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
|
||||
const startAdd = () => { current.value = null; form.value = { name: '' }; popup.value.open() }
|
||||
const startEdit = (item) => { current.value = item; form.value = { name: item.name }; popup.value.open() }
|
||||
const close = () => popup.value.close()
|
||||
|
||||
const save = async () => {
|
||||
if (!form.value.name.trim()) { uni.showToast({ title: '请输入分类名称', icon: 'none' }); return }
|
||||
if (current.value?.ID) {
|
||||
await api.updateFoodCategory({ ID: current.value.ID, name: form.value.name })
|
||||
} else {
|
||||
await api.addFoodCategory({ name: form.value.name })
|
||||
}
|
||||
close(); load()
|
||||
}
|
||||
|
||||
const remove = (item) => {
|
||||
uni.showModal({
|
||||
title: '删除确认', content: `确定删除分类“${item.name}”吗?`,
|
||||
success: async (res) => {
|
||||
if (res.confirm) { await api.deleteFoodCategory({ id: item.ID }); load() }
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
padding: 24rpx;
|
||||
background: linear-gradient(135deg, #FFE4F0 0%, #F3E8FF 50%, #EDE7FF 100%);
|
||||
}
|
||||
.header {
|
||||
display: flex; justify-content: space-between; align-items: center; margin-bottom: 24rpx;
|
||||
}
|
||||
.title {
|
||||
font-weight: 700; font-size: 34rpx; color: #6A4C93;
|
||||
}
|
||||
.add {
|
||||
background: linear-gradient(135deg, #FF7EB3 0%, #FF758C 100%);
|
||||
color:#fff; border:none; border-radius: 28rpx; padding: 16rpx 26rpx; font-size: 26rpx;
|
||||
box-shadow: 0 6rpx 16rpx rgba(255, 117, 140, 0.35);
|
||||
}
|
||||
.row {
|
||||
background: rgba(255,255,255,0.9);
|
||||
padding: 26rpx; border-radius: 18rpx; display:flex; justify-content: space-between; align-items:center; margin-bottom: 18rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(140, 82, 255, 0.12);
|
||||
}
|
||||
.name { font-size: 30rpx; color: #3C2A4D; font-weight: 600; }
|
||||
.ops { display:flex; gap: 12rpx; }
|
||||
.op-btn {
|
||||
border: none; padding: 12rpx 20rpx; border-radius: 22rpx; font-size: 24rpx; color:#fff;
|
||||
}
|
||||
.op-btn.edit {
|
||||
background: linear-gradient(135deg, #8A63F3 0%, #B089FF 100%);
|
||||
box-shadow: 0 4rpx 12rpx rgba(138, 99, 243, 0.3);
|
||||
}
|
||||
.op-btn.delete {
|
||||
background: linear-gradient(135deg, #FF8FA3 0%, #FF6B9D 100%);
|
||||
box-shadow: 0 4rpx 12rpx rgba(255, 107, 157, 0.3);
|
||||
}
|
||||
.dialog { background:#fff; padding: 30rpx; border-radius: 16rpx; width: 620rpx; }
|
||||
.dialog-title { font-size: 30rpx; font-weight: 700; margin-bottom: 20rpx; display:block; color:#6A4C93; }
|
||||
.dialog-actions { display:flex; justify-content:flex-end; gap: 12rpx; margin-top: 20rpx; }
|
||||
</style>
|
||||
181
src/pages/food/food-manage.vue
Normal file
181
src/pages/food/food-manage.vue
Normal file
@@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="header">
|
||||
<text class="title">菜品管理</text>
|
||||
<button class="add" @click="startAdd">新增菜品</button>
|
||||
</view>
|
||||
|
||||
<view v-for="item in list" :key="item.ID" class="row">
|
||||
<view class="left">
|
||||
<image :src="item.img" class="thumb" mode="aspectFill" />
|
||||
<view class="meta">
|
||||
<text class="name">{{ item.name }}</text>
|
||||
<text class="desc">分类:{{ getCategoryName(item.categoryId) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="ops">
|
||||
<button size="mini" class="op-btn edit" @click="startEdit(item)">编辑</button>
|
||||
<button size="mini" class="op-btn delete" @click="remove(item)">删除</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<uni-popup ref="popup" type="center">
|
||||
<view class="dialog">
|
||||
<text class="dialog-title">{{ current?.ID ? '编辑菜品' : '新增菜品' }}</text>
|
||||
<uni-easyinput v-model="form.name" placeholder="名称"></uni-easyinput>
|
||||
<uni-easyinput v-model="form.desc" placeholder="描述" type="textarea"></uni-easyinput>
|
||||
<view class="uploader">
|
||||
<view class="uploader-title">菜品图片</view>
|
||||
<view class="uploader-box" @click="chooseImage">
|
||||
<image v-if="form.img" :src="form.img" class="uploader-preview" mode="aspectFill" />
|
||||
<view v-else class="uploader-placeholder">
|
||||
<uni-icons type="camera" size="28" color="#999"></uni-icons>
|
||||
<text class="txt">点击上传</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="form.img" class="uploader-actions">
|
||||
<button size="mini" class="op-btn delete" @click.stop="form.img = ''">移除</button>
|
||||
<button size="mini" class="op-btn edit" @click.stop="chooseImage">更换</button>
|
||||
</view>
|
||||
</view>
|
||||
<picker :range="categoryList" range-key="name" @change="onCatChange">
|
||||
<view class="picker">分类:{{ pickCategoryName }}</view>
|
||||
</picker>
|
||||
<view class="dialog-actions">
|
||||
<button @click="close">取消</button>
|
||||
<button type="primary" @click="save">保存</button>
|
||||
</view>
|
||||
</view>
|
||||
</uni-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import api from '@/api'
|
||||
import config from '@/config'
|
||||
|
||||
const list = ref([])
|
||||
const categoryList = ref([])
|
||||
const popup = ref(null)
|
||||
const current = ref(null)
|
||||
const form = ref({ name: '', desc: '', img: '', categoryId: null })
|
||||
const selectedCatIdx = ref(-1)
|
||||
|
||||
const chooseImage = () => {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
sizeType: ['compressed','original'],
|
||||
success: async (res) => {
|
||||
const tempPath = res.tempFilePaths?.[0]
|
||||
if (!tempPath) return
|
||||
try {
|
||||
uni.showLoading({ title: '上传中' })
|
||||
// H5/APP/小程序通用上传
|
||||
const uploadRes = await new Promise((resolve, reject) => {
|
||||
const fullUrl = (config.baseUrl || '').replace(/\/$/, '') + '/user/upload'
|
||||
uni.uploadFile({
|
||||
url: fullUrl,
|
||||
filePath: tempPath,
|
||||
name: 'file',
|
||||
formData: {},
|
||||
header: { 'Authorization': uni.getStorageSync('token') || '' },
|
||||
success: (r) => {
|
||||
try { resolve(JSON.parse(r.data)) } catch (e) { resolve({ code: -1 }) }
|
||||
},
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
if (uploadRes?.code === 0 && uploadRes.data?.file?.url) {
|
||||
form.value.img = uploadRes.data.file.url
|
||||
} else {
|
||||
// 兜底:仍显示临时路径,避免影响表单交互
|
||||
form.value.img = tempPath
|
||||
uni.showToast({ title: uploadRes?.msg || '上传失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
form.value.img = tempPath
|
||||
uni.showToast({ title: '上传失败', icon: 'none' })
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const load = async () => {
|
||||
const [catsRes, foodsRes] = await Promise.all([
|
||||
api.getFoodCategoryList({ page: 1, pageSize: 100 }),
|
||||
api.getFoodList({ page: 1, pageSize: 200 })
|
||||
])
|
||||
if (catsRes?.code === 0) categoryList.value = catsRes.data?.list || []
|
||||
if (foodsRes?.code === 0) list.value = foodsRes.data?.list || []
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
|
||||
const startAdd = () => { current.value = null; form.value = { name: '', desc: '', img: '', categoryId: null }; selectedCatIdx.value = -1; popup.value.open() }
|
||||
const startEdit = (item) => {
|
||||
current.value = item
|
||||
form.value = { name: item.name, desc: item.desc, img: item.img, categoryId: item.categoryId }
|
||||
selectedCatIdx.value = categoryList.value.findIndex(c => c.ID === item.categoryId)
|
||||
popup.value.open()
|
||||
}
|
||||
const close = () => popup.value.close()
|
||||
|
||||
const onCatChange = (e) => {
|
||||
selectedCatIdx.value = Number(e.detail.value)
|
||||
form.value.categoryId = categoryList.value[selectedCatIdx.value]?.ID || null
|
||||
}
|
||||
|
||||
const pickCategoryName = computed(() => {
|
||||
if (selectedCatIdx.value < 0) return '未选择'
|
||||
return categoryList.value[selectedCatIdx.value]?.name || '未选择'
|
||||
})
|
||||
|
||||
const getCategoryName = (id) => categoryList.value.find(c => c.ID === id)?.name || '—'
|
||||
|
||||
const save = async () => {
|
||||
if (!form.value.name.trim()) { uni.showToast({ title: '请输入菜品名称', icon: 'none' }); return }
|
||||
if (!form.value.categoryId) { uni.showToast({ title: '请选择分类', icon: 'none' }); return }
|
||||
if (current.value?.ID) {
|
||||
await api.updateFood({ ID: current.value.ID, ...form.value })
|
||||
} else {
|
||||
await api.addFood(form.value)
|
||||
}
|
||||
close(); load()
|
||||
}
|
||||
|
||||
const remove = (item) => {
|
||||
uni.showModal({ title: '删除确认', content: `确定删除“${item.name}”吗?`, success: async (res) => { if (res.confirm) { await api.deleteFood({ id: item.ID }); load() } } })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
padding: 24rpx;
|
||||
background: linear-gradient(135deg, #FFE4F0 0%, #F3E8FF 50%, #EDE7FF 100%);
|
||||
}
|
||||
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24rpx; }
|
||||
.title { font-weight: 700; font-size: 34rpx; color: #6A4C93; }
|
||||
.add { background: linear-gradient(135deg, #FF7EB3 0%, #FF758C 100%); color:#fff; border:none; border-radius: 28rpx; padding: 16rpx 26rpx; font-size: 26rpx; box-shadow: 0 6rpx 16rpx rgba(255, 117, 140, 0.35); }
|
||||
.row { background: rgba(255,255,255,0.9); padding: 26rpx; border-radius: 18rpx; display:flex; justify-content: space-between; align-items:center; margin-bottom: 18rpx; box-shadow: 0 8rpx 24rpx rgba(140, 82, 255, 0.12); }
|
||||
.left { display:flex; align-items:center; }
|
||||
.thumb { width: 120rpx; height: 120rpx; border-radius: 12rpx; margin-right: 16rpx; }
|
||||
.meta { .name{display:block; font-size:28rpx; font-weight:600; color:#3C2A4D;} .desc{color:#8c8c8c; margin-top:6rpx;} }
|
||||
.ops { display:flex; gap: 12rpx; }
|
||||
.op-btn { border:none; padding: 12rpx 20rpx; border-radius: 22rpx; font-size: 24rpx; color:#fff; }
|
||||
.op-btn.edit { background: linear-gradient(135deg, #8A63F3 0%, #B089FF 100%); box-shadow: 0 4rpx 12rpx rgba(138,99,243,.3); }
|
||||
.op-btn.delete { background: linear-gradient(135deg, #FF8FA3 0%, #FF6B9D 100%); box-shadow: 0 4rpx 12rpx rgba(255,107,157,.3); }
|
||||
.dialog { background:#fff; padding: 30rpx; border-radius: 16rpx; width: 620rpx; }
|
||||
.dialog-title { font-size: 30rpx; font-weight: 700; margin-bottom: 20rpx; display:block; color:#6A4C93; }
|
||||
.uploader { margin: 16rpx 0; }
|
||||
.uploader-title { color:#6A4C93; font-size: 26rpx; margin-bottom: 10rpx; }
|
||||
.uploader-box { width: 100%; height: 200rpx; border: 2rpx dashed #d8c7ff; border-radius: 14rpx; display:flex; align-items:center; justify-content:center; background:#faf7ff; }
|
||||
.uploader-placeholder { display:flex; align-items:center; gap: 10rpx; color:#999; }
|
||||
.uploader-preview { width: 100%; height: 100%; border-radius: 12rpx; }
|
||||
.uploader-actions { display:flex; gap: 12rpx; justify-content:flex-end; margin-top: 10rpx; }
|
||||
.picker { padding: 20rpx 0; color:#666; }
|
||||
.dialog-actions { display:flex; justify-content:flex-end; gap: 12rpx; margin-top: 20rpx; }
|
||||
</style>
|
||||
@@ -122,6 +122,13 @@ const create = async () => {
|
||||
box-shadow: 0 -4rpx 20rpx rgba(0,0,0,0.06);
|
||||
|
||||
.sum { font-size: 28rpx; .price{ color:#FF6B9D; font-weight:700; }}
|
||||
.submit { background: linear-gradient(135deg, #FF6B9D 0%, #FF8E9E 100%); color:#fff; border:none; border-radius: 30rpx; padding: 16rpx 28rpx; font-size: 28rpx; }
|
||||
.submit {
|
||||
background: linear-gradient(135deg, #FF6B9D 0%, #FF8E9E 100%);
|
||||
color:#fff;
|
||||
border:none;
|
||||
border-radius: 30rpx;
|
||||
padding: 16rpx 28rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -30,10 +30,14 @@
|
||||
>
|
||||
<text class="name">{{ cat.name }}</text>
|
||||
</view>
|
||||
<view class="category-manage">
|
||||
<view class="category-manage" @click="openCategoryManage">
|
||||
<uni-icons type="gear" size="16" color="#999"></uni-icons>
|
||||
<text class="manage-text">分类管理</text>
|
||||
</view>
|
||||
<view class="food-manage" @click="openFoodManage">
|
||||
<uni-icons type="list" size="16" color="#999"></uni-icons>
|
||||
<text class="manage-text">菜品管理</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<scroll-view class="food-pane" scroll-y @scrolltolower="loadMore">
|
||||
@@ -126,7 +130,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { onPullDownRefresh } from '@dcloudio/uni-app'
|
||||
import { onPullDownRefresh, onShow } from '@dcloudio/uni-app'
|
||||
import api from '@/api'
|
||||
|
||||
// 搜索与筛选
|
||||
@@ -218,6 +222,14 @@ const doSearch = () => {
|
||||
loadFoods(true)
|
||||
}
|
||||
|
||||
const openCategoryManage = () => {
|
||||
uni.navigateTo({ url: '/pages/food/category-manage' })
|
||||
}
|
||||
|
||||
const openFoodManage = () => {
|
||||
uni.navigateTo({ url: '/pages/food/food-manage' })
|
||||
}
|
||||
|
||||
const filteredFoodList = computed(() => foodList.value)
|
||||
|
||||
const loadMore = () => {
|
||||
@@ -295,6 +307,12 @@ onMounted(() => {
|
||||
init()
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
// 从管理页返回后自动刷新
|
||||
loadCategories(true)
|
||||
loadFoods(true)
|
||||
})
|
||||
|
||||
const submitOrder = () => {
|
||||
if (cart.value.length === 0) {
|
||||
uni.showToast({ title: '请先选择菜品', icon: 'none' })
|
||||
@@ -372,7 +390,7 @@ const submitOrder = () => {
|
||||
}
|
||||
}
|
||||
|
||||
.category-manage {
|
||||
.category-manage, .food-manage {
|
||||
padding: 24rpx 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user