This commit is contained in:
2025-03-21 21:42:33 +08:00
commit 2c1f20572e
33 changed files with 5271 additions and 0 deletions

184
src/views/dish/add.vue Normal file
View File

@@ -0,0 +1,184 @@
<template>
<div class="dish-form">
<h2>新增菜品</h2>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="100px"
class="form"
>
<el-form-item label="菜品名称" prop="dishName">
<el-input v-model="form.dishName" placeholder="请输入菜品名称" />
</el-form-item>
<el-form-item label="菜品分类" prop="dishType">
<el-select v-model="form.dishType" placeholder="请选择分类">
<el-option
v-for="item in dishTypes"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="价格" prop="dishPrice">
<el-input-number
v-model="form.dishPrice"
:precision="2"
:step="0.1"
:min="0"
/>
</el-form-item>
<el-form-item label="配料" prop="dishIngredients">
<el-input
v-model="form.dishIngredients"
type="textarea"
:rows="3"
placeholder="请输入配料"
/>
</el-form-item>
<el-form-item label="图片" prop="dishImage">
<el-upload
class="avatar-uploader"
:http-request="handleUpload"
:show-file-list="false"
:before-upload="beforeUpload"
>
<img v-if="form.dishImage" :src="form.dishImage" class="avatar" />
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSubmit">提交</el-button>
<el-button @click="$router.back()">取消</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import { addDish } from '@/api/dish'
const router = useRouter()
const formRef = ref(null)
const dishTypes = [
{ label: '热菜过油', value: '1' },
{ label: '热菜不过油', value: '2' },
{ label: '素菜', value: '3' },
{ label: '凉菜', value: '4' },
{ label: '汤', value: '5' }
]
const form = ref({
dishName: '',
dishType: '',
dishPrice: 0,
dishIngredients: '',
dishImage: ''
})
const rules = {
dishName: [
{ required: true, message: '请输入菜品名称', trigger: 'blur' }
],
dishType: [
{ required: true, message: '请选择菜品分类', trigger: 'change' }
],
dishPrice: [
{ required: true, message: '请输入价格', trigger: 'blur' }
]
}
const handleUpload = async (options) => {
const file = options.file
// 这里可以使用FileReader读取文件并预览
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = () => {
form.value.dishImage = reader.result // 保存为base64格式
}
// 如果需要上传到服务器,可以在提交表单时一起处理
// 现在我们直接在本地预览
}
const beforeUpload = (file) => {
const isImage = file.type.startsWith('image/')
const isLt2M = file.size / 1024 / 1024 < 2
if (!isImage) {
ElMessage.error('只能上传图片文件!')
return false
}
if (!isLt2M) {
ElMessage.error('图片大小不能超过 2MB!')
return false
}
return true
}
const handleSubmit = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
await addDish(form.value)
ElMessage.success('添加成功')
router.push('/dish/list')
} catch (error) {
console.error(error)
}
}
</script>
<style scoped>
.dish-form {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.form {
margin-top: 20px;
}
.avatar-uploader {
text-align: center;
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
width: 178px;
height: 178px;
}
.avatar-uploader:hover {
border-color: #409EFF;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
text-align: center;
line-height: 178px;
}
.avatar {
width: 178px;
height: 178px;
display: block;
object-fit: cover;
}
</style>

231
src/views/dish/edit.vue Normal file
View File

@@ -0,0 +1,231 @@
<template>
<div class="dish-form">
<h2>编辑菜品</h2>
<el-form
v-loading="loading"
ref="formRef"
:model="form"
:rules="rules"
label-width="100px"
class="form"
>
<el-form-item label="菜品名称" prop="dishName">
<el-input v-model="form.dishName" placeholder="请输入菜品名称" />
</el-form-item>
<el-form-item label="菜品分类" prop="dishType">
<el-select v-model="form.dishType" placeholder="请选择分类">
<el-option
v-for="item in dishTypes"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="价格" prop="dishPrice">
<el-input-number
v-model="form.dishPrice"
:precision="2"
:step="0.1"
:min="0"
/>
</el-form-item>
<el-form-item label="配料" prop="dishIngredients">
<el-input
v-model="form.dishIngredients"
type="textarea"
:rows="3"
placeholder="请输入配料"
/>
</el-form-item>
<el-form-item label="图片" prop="dishImage">
<el-upload
class="avatar-uploader"
:http-request="handleUpload"
:show-file-list="false"
:before-upload="beforeUpload"
>
<img v-if="form.dishImage" :src="form.dishImage" class="avatar" />
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSubmit">保存</el-button>
<el-button @click="$router.back()">取消</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import { getDishList, updateDish } from '@/api/dish'
const router = useRouter()
const route = useRoute()
const formRef = ref(null)
const loading = ref(false)
const dishTypes = [
{ label: '热菜过油', value: '1' },
{ label: '热菜不过油', value: '2' },
{ label: '素菜', value: '3' },
{ label: '凉菜', value: '4' },
{ label: '汤', value: '5' }
]
const form = ref({
id: '',
dishName: '',
dishType: '',
dishPrice: 0,
dishIngredients: '',
dishImage: ''
})
const rules = {
dishName: [
{ required: true, message: '请输入菜品名称', trigger: 'blur' }
],
dishType: [
{ required: true, message: '请选择菜品分类', trigger: 'change' }
],
dishPrice: [
{ required: true, message: '请输入价格', trigger: 'blur' }
]
}
const handleUpload = async (options) => {
const file = options.file
// 这里可以使用FileReader读取文件并预览
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = () => {
form.value.dishImage = reader.result // 保存为base64格式
}
// 如果需要上传到服务器,可以在提交表单时一起处理
// 现在我们直接在本地预览
}
const beforeUpload = (file) => {
const isImage = file.type.startsWith('image/')
const isLt2M = file.size / 1024 / 1024 < 2
if (!isImage) {
ElMessage.error('只能上传图片文件!')
return false
}
if (!isLt2M) {
ElMessage.error('图片大小不能超过 2MB!')
return false
}
return true
}
const fetchDishDetail = async () => {
loading.value = true
try {
const res = await getDishList()
console.log('获取到的菜品列表:', res.data)
console.log('当前要查找的ID', route.params.id)
// 确保ID是字符串类型进行比较因为route.params.id是字符串
const targetId = route.params.id
const dish = res.data.find(item => String(item.id) === targetId)
console.log('找到的菜品:', dish)
if (dish) {
// 确保价格是数字类型
form.value = {
...dish,
dishPrice: Number(dish.dishPrice)
}
} else {
ElMessage.error('菜品不存在')
router.push('/dish/list')
}
} catch (error) {
console.error('获取菜品详情失败:', error)
ElMessage.error('获取菜品详情失败')
router.push('/dish/list')
} finally {
loading.value = false
}
}
const handleSubmit = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
// 显示提交的数据,用于调试
console.log('提交的表单数据:', form.value)
// 不需要在这里转换为FormData在API中已处理
await updateDish(form.value)
ElMessage.success('更新成功')
router.push('/dish/list')
} catch (error) {
console.error('更新失败:', error)
ElMessage.error(error.message || '更新失败')
}
}
onMounted(() => {
fetchDishDetail()
})
</script>
<style scoped>
.dish-form {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.form {
margin-top: 20px;
}
.avatar-uploader {
text-align: center;
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
width: 178px;
height: 178px;
}
.avatar-uploader:hover {
border-color: #409EFF;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
text-align: center;
line-height: 178px;
}
.avatar {
width: 178px;
height: 178px;
display: block;
object-fit: cover;
}
</style>

345
src/views/dish/list.vue Normal file
View File

@@ -0,0 +1,345 @@
<template>
<div class="dish-list">
<div class="header">
<el-button type="primary" @click="$router.push('/dish/add')">
新增菜品
</el-button>
<el-select v-model="selectedType" placeholder="选择分类" clearable>
<el-option
v-for="item in dishTypes"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
<!-- PC端表格 -->
<el-table
v-if="!isMobile"
v-loading="loading"
:data="filteredDishList"
style="width: 100%"
:max-height="tableHeight"
:border="true"
class="dish-table"
>
<el-table-column prop="dishImage" label="图片" width="100" fixed="left">
<template #default="scope">
<el-image
:src="scope.row.dishImage"
:preview-src-list="[scope.row.dishImage]"
fit="cover"
style="width: 80px; height: 80px"
/>
</template>
</el-table-column>
<el-table-column prop="dishName" label="菜品名称" min-width="120" />
<el-table-column prop="dishType" label="分类" min-width="100">
<template #default="scope">
{{ getDishTypeLabel(scope.row.dishType) }}
</template>
</el-table-column>
<el-table-column prop="dishPrice" label="价格" min-width="80" />
<el-table-column prop="dishIngredients" label="配料" min-width="500" show-overflow-tooltip>
<template #default="scope">
<div style="white-space: normal; word-break: break-all; line-height: 1.5;">
{{ scope.row.dishIngredients }}
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template #default="scope">
<el-button
type="primary"
link
@click="$router.push(`/dish/edit/${scope.row.id}`)"
>
编辑
</el-button>
<el-button
type="danger"
link
@click="handleDelete(scope.row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 移动端卡片列表 -->
<div v-else class="mobile-dish-list">
<div v-loading="loading" class="dish-cards">
<div v-for="dish in filteredDishList" :key="dish.id" class="dish-card">
<div class="dish-card-header">
<el-image
:src="dish.dishImage"
:preview-src-list="[dish.dishImage]"
fit="cover"
class="dish-image"
/>
<div class="dish-basic-info">
<h3>{{ dish.dishName }}</h3>
<p class="dish-type">{{ getDishTypeLabel(dish.dishType) }}</p>
<p class="dish-price">¥{{ dish.dishPrice }}</p>
</div>
</div>
<div class="dish-ingredients" v-if="dish.dishIngredients">
<p class="label">配料</p>
<p>{{ dish.dishIngredients }}</p>
</div>
<div class="dish-actions">
<el-button
type="primary"
size="small"
@click="$router.push(`/dish/edit/${dish.id}`)"
>
编辑
</el-button>
<el-button
type="danger"
size="small"
@click="handleDelete(dish)"
>
删除
</el-button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getDishList, deleteDish } from '@/api/dish'
import { useWindowSize } from '@vueuse/core'
const loading = ref(false)
const dishList = ref([])
const selectedType = ref('')
const tableHeight = ref('calc(100vh - 150px)')
const { width, height } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const dishTypes = [
{ label: '热菜过油', value: '1' },
{ label: '热菜不过油', value: '2' },
{ label: '素菜', value: '3' },
{ label: '凉菜', value: '4' },
{ label: '汤', value: '5' }
]
const filteredDishList = computed(() => {
if (!selectedType.value) return dishList.value
return dishList.value.filter(item => item.dishType === selectedType.value)
})
const getDishTypeLabel = (type) => {
const found = dishTypes.find(item => item.value === type)
return found ? found.label : type
}
const fetchDishList = async () => {
loading.value = true
try {
const res = await getDishList()
dishList.value = res.data
} catch (error) {
console.error(error)
} finally {
loading.value = false
}
}
const handleDelete = async (row) => {
try {
await ElMessageBox.confirm('确认删除该菜品吗?', '提示', {
type: 'warning'
})
await deleteDish({ id: row.id })
ElMessage.success('删除成功')
fetchDishList()
} catch (error) {
if (error !== 'cancel') {
console.error(error)
}
}
}
// 监听窗口大小变化,调整表格高度
const updateTableHeight = () => {
// 减去头部的高度和边距,确保表格填充剩余空间
tableHeight.value = isMobile.value ? 'calc(100vh - 230px)' : 'calc(100vh - 150px)'
}
onMounted(() => {
fetchDishList()
updateTableHeight()
window.addEventListener('resize', updateTableHeight)
})
onUnmounted(() => {
window.removeEventListener('resize', updateTableHeight)
})
</script>
<style scoped>
.dish-list {
width: 100%;
height: 100%;
box-sizing: border-box;
overflow-x: hidden;
display: flex;
flex-direction: column;
}
.header {
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}
/* PC端表格样式优化 */
.dish-table {
width: 100% !important;
}
:deep(.el-table) {
flex: 1;
width: 100% !important;
--el-table-border-color: #ebeef5;
--el-table-header-bg-color: #f5f7fa;
}
:deep(.el-table__inner-wrapper) {
height: 100%;
}
:deep(.el-table th) {
background-color: #f5f7fa;
color: #606266;
font-weight: bold;
font-size: 14px;
padding: 12px 10px;
height: 50px;
}
:deep(.el-table td) {
padding: 15px 10px;
height: 90px; /* 增加行高 */
}
:deep(.el-table--border th), :deep(.el-table--border td) {
border-right: 1px solid #ebeef5;
}
:deep(.el-table__fixed-right) {
height: auto !important;
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.05);
}
:deep(.el-table__fixed-right-patch) {
background-color: #f5f7fa;
}
/* 增加移动端卡片样式 */
.mobile-dish-list {
margin-bottom: 70px;
flex: 1;
width: 100%;
}
.dish-cards {
display: flex;
flex-direction: column;
gap: 15px;
}
.dish-card {
background: #fff;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.dish-card-header {
display: flex;
margin-bottom: 10px;
}
.dish-image {
width: 80px;
height: 80px;
border-radius: 4px;
margin-right: 12px;
}
.dish-basic-info {
display: flex;
flex-direction: column;
justify-content: space-between;
}
.dish-basic-info h3 {
margin: 0 0 5px 0;
font-size: 16px;
}
.dish-type {
color: #909399;
margin: 0 0 5px 0;
font-size: 14px;
}
.dish-price {
color: #f56c6c;
font-weight: bold;
margin: 0;
}
.dish-ingredients {
margin-bottom: 10px;
font-size: 14px;
display: flex;
}
.dish-ingredients .label {
color: #909399;
margin: 0;
margin-right: 5px;
}
.dish-ingredients p {
margin: 0;
}
.dish-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
}
@media screen and (max-width: 768px) {
.dish-list {
padding: 10px;
}
.header {
flex-direction: column;
gap: 10px;
margin-bottom: 15px;
}
.header > * {
width: 100%;
}
}
</style>

222
src/views/dish/random.vue Normal file
View File

@@ -0,0 +1,222 @@
<template>
<div class="random-dish">
<div class="header">
<h2>随机选择菜品</h2>
<div class="controls">
<el-input-number
v-model="num"
:min="1"
:max="10"
placeholder="选择数量"
style="width: 120px"
/>
<el-button type="primary" @click="handleRandom" :loading="loading">
随机选择
</el-button>
</div>
</div>
<div class="result" v-if="randomDishes.length">
<h3>随机结果</h3>
<div class="dish-grid" :class="{ 'mobile-grid': isMobile }">
<div v-for="dish in randomDishes" :key="dish.id" class="dish-card">
<el-image
:src="dish.dishImage"
:preview-src-list="[dish.dishImage]"
fit="cover"
class="dish-image"
/>
<div class="dish-info">
<h4>{{ dish.dishName }}</h4>
<p class="type">{{ getDishTypeLabel(dish.dishType) }}</p>
<p class="price">¥{{ dish.dishPrice }}</p>
</div>
</div>
</div>
</div>
<el-empty v-else description="点击上方按钮开始随机选择" />
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { getRandomDish } from '@/api/dish'
import { useWindowSize } from '@vueuse/core'
const loading = ref(false)
const num = ref(1)
const randomDishes = ref([])
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const dishTypes = [
{ label: '热菜过油', value: '1' },
{ label: '热菜不过油', value: '2' },
{ label: '素菜', value: '3' },
{ label: '凉菜', value: '4' },
{ label: '汤', value: '5' }
]
const getDishTypeLabel = (type) => {
const found = dishTypes.find(item => item.value === type)
return found ? found.label : type
}
const handleRandom = async () => {
if (num.value < 1) {
ElMessage.warning('请选择至少1个菜品')
return
}
loading.value = true
try {
const res = await getRandomDish(num.value)
randomDishes.value = res.data
} catch (error) {
console.error(error)
} finally {
loading.value = false
}
}
</script>
<style scoped>
.random-dish {
width: 100%;
height: 100%;
box-sizing: border-box;
overflow-y: auto;
overflow-x: hidden;
display: flex;
flex-direction: column;
}
.header {
margin-bottom: 30px;
flex-shrink: 0;
}
.header h2 {
margin-bottom: 20px;
color: #303133;
font-size: 20px;
}
.controls {
display: flex;
gap: 15px;
align-items: center;
}
.result {
margin-top: 20px;
margin-bottom: 30px;
flex: 1;
width: 100%;
}
.result h3 {
margin-bottom: 20px;
color: #303133;
font-size: 18px;
}
.dish-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 30px;
width: 100%;
}
.mobile-grid {
grid-template-columns: 1fr;
gap: 15px;
}
.dish-card {
border: 1px solid #ebeef5;
border-radius: 8px;
overflow: hidden;
transition: all 0.3s;
background-color: #fff;
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.05);
width: 100%;
}
.dish-card:hover {
transform: translateY(-5px);
box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
}
.dish-image {
width: 100%;
height: 200px;
object-fit: cover;
}
.dish-info {
padding: 15px;
}
.dish-info h4 {
margin: 0 0 10px;
font-size: 16px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #303133;
}
.type {
color: #909399;
margin: 5px 0;
font-size: 14px;
}
.price {
color: #f56c6c;
font-size: 16px;
font-weight: bold;
margin: 8px 0 0;
}
@media screen and (max-width: 768px) {
.random-dish {
padding: 15px;
}
.controls {
flex-direction: column;
align-items: stretch;
gap: 10px;
}
.dish-image {
height: 140px;
}
.dish-info {
padding: 10px;
}
.dish-info h4 {
font-size: 15px;
}
.type {
font-size: 12px;
margin: 3px 0;
}
.price {
font-size: 15px;
}
.result {
margin-bottom: 80px; /* 为底部导航留出空间 */
}
}
</style>