🎨 优化点餐页面&完善分类和菜品管理

This commit is contained in:
2025-08-19 18:41:48 +08:00
parent 4e141ff951
commit 6353eeed0b
6 changed files with 346 additions and 5 deletions

View File

@@ -9,7 +9,7 @@ const API = {
login: data => net.post('/user/login', data, false), // 登录 login: data => net.post('/user/login', data, false), // 登录
getUserInfo: () => net.get('/user/info', {}, true), // 获取用户信息 getUserInfo: () => net.get('/user/info', {}, true), // 获取用户信息
updateUserInfo: data => net.put('/user/update', data, 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获取用户信息 getUserInfoById: (id) => net.get(`/user/info/${id}`, {}, true), // 根据ID获取用户信息
getLoverInfo: () => net.get('/user/lover', {}, true), // 获取情侣关系信息(包含 startDate 与 loverId getLoverInfo: () => net.get('/user/lover', {}, true), // 获取情侣关系信息(包含 startDate 与 loverId

View File

@@ -76,6 +76,34 @@
"enablePullDownRefresh": false "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", "path": "pages/diary/diary",
"style": { "style": {

View 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>

View 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>

View File

@@ -122,6 +122,13 @@ const create = async () => {
box-shadow: 0 -4rpx 20rpx rgba(0,0,0,0.06); box-shadow: 0 -4rpx 20rpx rgba(0,0,0,0.06);
.sum { font-size: 28rpx; .price{ color:#FF6B9D; font-weight:700; }} .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> </style>

View File

@@ -30,10 +30,14 @@
> >
<text class="name">{{ cat.name }}</text> <text class="name">{{ cat.name }}</text>
</view> </view>
<view class="category-manage"> <view class="category-manage" @click="openCategoryManage">
<uni-icons type="gear" size="16" color="#999"></uni-icons> <uni-icons type="gear" size="16" color="#999"></uni-icons>
<text class="manage-text">分类管理</text> <text class="manage-text">分类管理</text>
</view> </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>
<scroll-view class="food-pane" scroll-y @scrolltolower="loadMore"> <scroll-view class="food-pane" scroll-y @scrolltolower="loadMore">
@@ -126,7 +130,7 @@
<script setup> <script setup>
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { onPullDownRefresh } from '@dcloudio/uni-app' import { onPullDownRefresh, onShow } from '@dcloudio/uni-app'
import api from '@/api' import api from '@/api'
// 搜索与筛选 // 搜索与筛选
@@ -218,6 +222,14 @@ const doSearch = () => {
loadFoods(true) 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 filteredFoodList = computed(() => foodList.value)
const loadMore = () => { const loadMore = () => {
@@ -295,6 +307,12 @@ onMounted(() => {
init() init()
}) })
onShow(() => {
// 从管理页返回后自动刷新
loadCategories(true)
loadFoods(true)
})
const submitOrder = () => { const submitOrder = () => {
if (cart.value.length === 0) { if (cart.value.length === 0) {
uni.showToast({ title: '请先选择菜品', icon: 'none' }) uni.showToast({ title: '请先选择菜品', icon: 'none' })
@@ -372,7 +390,7 @@ const submitOrder = () => {
} }
} }
.category-manage { .category-manage, .food-manage {
padding: 24rpx 20rpx; padding: 24rpx 20rpx;
display: flex; display: flex;
align-items: center; align-items: center;