fix(food-encyclopedia): 后台食物百科列表/编辑页接入并修复图片URL双前缀

- 新增 FoodEncyclopediaController 及 ToolFoodAdminService,提供 /api/admin/tool/food/* CRUD
- ToolFoodAdminServiceImpl 在保存前 clearPrefix 并正则修复历史脏数据中的多层 host 前缀
- 前端 list.vue/edit.vue 修复二次解包导致 listData.list 渲染崩溃
- edit.vue 加载详情时兜底归一化 image 字段,处理 https://host//https://host//crmebimage/... 形式
- content.js 注册 foodManager / foodEdit 路由

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
msh-agent
2026-05-03 01:08:34 +08:00
parent d7c870ced7
commit a3b609e70a
7 changed files with 1013 additions and 0 deletions

View File

@@ -0,0 +1,114 @@
package com.zbkj.admin.controller;
import com.zbkj.common.model.tool.V2Food;
import com.zbkj.common.page.CommonPage;
import com.zbkj.common.request.PageParamRequest;
import com.zbkj.common.result.CommonResult;
import com.zbkj.service.service.tool.ToolFoodAdminService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 食物百科管理 后台控制器
* +----------------------------------------------------------------------
* | 提供食物百科 CRUD 管理接口,含图片维护
* +----------------------------------------------------------------------
*/
@Slf4j
@RestController
@RequestMapping("api/admin/tool/food")
@Api(tags = "食物百科管理")
public class FoodEncyclopediaController {
@Autowired
private ToolFoodAdminService toolFoodAdminService;
/**
* 分页列表
*/
@ApiOperation(value = "食物列表")
@GetMapping("/list")
public CommonResult<CommonPage<V2Food>> getList(
@RequestParam(required = false) String keyword,
@RequestParam(required = false) String category,
@RequestParam(required = false) String status,
@Validated PageParamRequest pageParamRequest) {
return CommonResult.success(CommonPage.restPage(
toolFoodAdminService.getAdminList(keyword, category, status, pageParamRequest)));
}
/**
* 食物详情
*/
@ApiOperation(value = "食物详情")
@GetMapping("/info/{id}")
public CommonResult<V2Food> info(@PathVariable Long id) {
return CommonResult.success(toolFoodAdminService.getById(id));
}
/**
* 新增食物
*/
@ApiOperation(value = "新增食物")
@PostMapping("/save")
public CommonResult<String> save(@RequestBody @Validated V2Food food) {
if (toolFoodAdminService.create(food)) {
return CommonResult.success();
}
return CommonResult.failed();
}
/**
* 修改食物
*/
@ApiOperation(value = "修改食物")
@PostMapping("/update/{id}")
public CommonResult<String> update(@PathVariable Long id,
@RequestBody @Validated V2Food food) {
food.setFoodId(id);
if (toolFoodAdminService.updateFood(food)) {
return CommonResult.success();
}
return CommonResult.failed();
}
/**
* 删除食物
*/
@ApiOperation(value = "删除食物")
@GetMapping("/delete/{id}")
public CommonResult<String> delete(@PathVariable Long id) {
if (toolFoodAdminService.deleteById(id)) {
return CommonResult.success();
}
return CommonResult.failed();
}
/**
* 修改状态(上架/下架)
*/
@ApiOperation(value = "修改上下架状态")
@PostMapping("/changeStatus/{id}")
public CommonResult<String> changeStatus(@PathVariable Long id,
@RequestParam String status) {
if (toolFoodAdminService.changeStatus(id, status)) {
return CommonResult.success();
}
return CommonResult.failed();
}
/**
* 获取分类列表(用于下拉筛选)
*/
@ApiOperation(value = "食物分类列表")
@GetMapping("/categories")
public CommonResult<List<String>> getCategories() {
return CommonResult.success(toolFoodAdminService.getAllCategories());
}
}

View File

@@ -0,0 +1,134 @@
package com.zbkj.service.service.impl.tool;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.github.pagehelper.PageHelper;
import com.zbkj.common.model.tool.V2Food;
import com.zbkj.common.request.PageParamRequest;
import com.zbkj.service.dao.tool.V2FoodDao;
import com.zbkj.service.service.SystemAttachmentService;
import com.zbkj.service.service.tool.ToolFoodAdminService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
/**
* 食物百科管理后台服务实现类
* +----------------------------------------------------------------------
* | 提供后台 CRUD 操作,图片维护等功能
* +----------------------------------------------------------------------
*/
@Slf4j
@Service
public class ToolFoodAdminServiceImpl implements ToolFoodAdminService {
@Resource
private V2FoodDao v2FoodDao;
@Resource
private SystemAttachmentService systemAttachmentService;
@Override
public List<V2Food> getAdminList(String keyword, String category, String status,
PageParamRequest pageParamRequest) {
PageHelper.startPage(pageParamRequest.getPage(), pageParamRequest.getLimit());
LambdaQueryWrapper<V2Food> wrapper = new LambdaQueryWrapper<>();
// 关键词搜索(名称或别名)
if (StrUtil.isNotBlank(keyword)) {
wrapper.and(w -> w.like(V2Food::getName, keyword)
.or().like(V2Food::getAlias, keyword));
}
// 分类筛选
if (StrUtil.isNotBlank(category)) {
wrapper.eq(V2Food::getCategory, category);
}
// 状态筛选
if (StrUtil.isNotBlank(status)) {
wrapper.eq(V2Food::getStatus, status);
}
wrapper.orderByDesc(V2Food::getSortOrder)
.orderByDesc(V2Food::getUpdatedAt);
return v2FoodDao.selectList(wrapper);
}
@Override
public V2Food getById(Long id) {
return v2FoodDao.selectById(id);
}
@Override
public boolean create(V2Food food) {
Date now = new Date();
food.setImage(normalizeImage(food.getImage()));
food.setCreatedAt(now);
food.setUpdatedAt(now);
if (food.getStatus() == null) {
food.setStatus("active");
}
if (food.getSortOrder() == null) {
food.setSortOrder(0);
}
if (food.getViewCount() == null) {
food.setViewCount(0);
}
return v2FoodDao.insert(food) > 0;
}
@Override
public boolean updateFood(V2Food food) {
food.setImage(normalizeImage(food.getImage()));
food.setUpdatedAt(new Date());
return v2FoodDao.updateById(food) > 0;
}
/**
* 规范化图片字段:
* 1. 先 clearPrefix 剥掉单层 CDN 前缀
* 2. 再用正则去掉残留的「http(s)://xxx/」段,修复历史脏数据中的双前缀
*/
private String normalizeImage(String image) {
if (StrUtil.isBlank(image)) {
return image;
}
String cleaned = systemAttachmentService.clearPrefix(image);
// 处理历史脏数据:形如 "https://.../crmebimage/..." 或 "https://.../https://.../crmebimage/..."
// 去除 "crmebimage/" 之前的所有 "http(s)://...//?" 段
return cleaned.replaceAll("^(https?://[^/]+/+)+(?=crmebimage/)", "");
}
@Override
public boolean deleteById(Long id) {
return v2FoodDao.deleteById(id) > 0;
}
@Override
public boolean changeStatus(Long id, String status) {
V2Food food = new V2Food();
food.setFoodId(id);
food.setStatus(status);
food.setUpdatedAt(new Date());
return v2FoodDao.updateById(food) > 0;
}
@Override
public List<String> getAllCategories() {
LambdaQueryWrapper<V2Food> wrapper = new LambdaQueryWrapper<>();
wrapper.select(V2Food::getCategory)
.isNotNull(V2Food::getCategory)
.ne(V2Food::getCategory, "")
.groupBy(V2Food::getCategory);
return v2FoodDao.selectList(wrapper).stream()
.map(V2Food::getCategory)
.filter(StrUtil::isNotBlank)
.distinct()
.sorted()
.collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,51 @@
package com.zbkj.service.service.tool;
import com.zbkj.common.model.tool.V2Food;
import com.zbkj.common.request.PageParamRequest;
import java.util.List;
/**
* 食物百科管理后台服务接口
* +----------------------------------------------------------------------
* | 提供后台 CRUD 操作,图片维护等功能
* +----------------------------------------------------------------------
*/
public interface ToolFoodAdminService {
/**
* 后台分页列表(支持关键词、分类、状态筛选)
*/
List<V2Food> getAdminList(String keyword, String category, String status,
PageParamRequest pageParamRequest);
/**
* 根据ID获取食物<E9A39F><E789A9><EFBFBD>
*/
V2Food getById(Long id);
/**
* 新增食物
*/
boolean create(V2Food food);
/**
* 修改食物
*/
boolean updateFood(V2Food food);
/**
* 删除食物
*/
boolean deleteById(Long id);
/**
* 修改状态(上架/下架)
*/
boolean changeStatus(Long id, String status);
/**
* 获取所有分类列表
*/
List<String> getAllCategories();
}

View File

@@ -0,0 +1,93 @@
// +----------------------------------------------------------------------
// | 食物百科管理 API
// +----------------------------------------------------------------------
import request from '@/utils/request';
/**
* 食物列表
* @param {Object} params - { keyword, category, status, page, limit }
*/
export function ListFood(params) {
return request({
url: '/admin/tool/food/list',
method: 'GET',
params: {
keyword: params.keyword,
category: params.category,
status: params.status,
page: params.page,
limit: params.limit,
},
});
}
/**
* 食物详情
* @param {Number} id - 食物ID
*/
export function InfoFood(id) {
return request({
url: `/admin/tool/food/info/${id}`,
method: 'GET',
});
}
/**
* 新增食物
* @param {Object} data - 食物数据
*/
export function AddFood(data) {
return request({
url: '/admin/tool/food/save',
method: 'POST',
data,
});
}
/**
* 修改食物
* @param {Number} id - 食物ID
* @param {Object} data - 食物数据
*/
export function UpdateFood(id, data) {
return request({
url: `/admin/tool/food/update/${id}`,
method: 'POST',
data,
});
}
/**
* 删除食物
* @param {Number} id - 食物ID
*/
export function DeleteFood(id) {
return request({
url: `/admin/tool/food/delete/${id}`,
method: 'GET',
});
}
/**
* 修改上下架状态
* @param {Number} id - 食物ID
* @param {String} status - active/inactive
*/
export function ChangeStatusFood(id, status) {
return request({
url: `/admin/tool/food/changeStatus/${id}`,
method: 'POST',
params: { status },
});
}
/**
* 获取食物分类列表
*/
export function GetFoodCategories() {
return request({
url: '/admin/tool/food/categories',
method: 'GET',
});
}

View File

@@ -48,6 +48,25 @@ const contentRouter = {
icon: 'clipboard',
},
},
{
path: 'foodManager',
name: 'foodManager',
component: () => import('@/views/content/food/list'),
meta: {
title: '食物百科',
icon: 'clipboard',
},
},
{
path: 'foodEdit/:id?',
name: 'foodEdit',
component: () => import('@/views/content/food/edit'),
meta: {
title: '编辑食物',
noCache: true,
activeMenu: '/content/foodManager',
},
},
],
};

View File

@@ -0,0 +1,365 @@
<template>
<div class="divBox">
<el-card class="box-card">
<div slot="header">
<span>{{ isEdit ? '编辑食物' : '新增食物' }}</span>
</div>
<el-form ref="form" :model="form" :rules="rules" label-width="120px" size="small">
<!-- 基本信息 -->
<el-divider content-position="left">基本信息</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="食物名称" prop="name">
<el-input v-model="form.name" placeholder="如:西兰花" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="别名">
<el-input v-model="form.alias" placeholder="多个别名用逗号分隔" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="分类" prop="category">
<el-select
v-model="form.category"
filterable
allow-create
default-first-option
placeholder="选择或输入分类"
style="width: 100%;"
>
<el-option
v-for="cat in categoryList"
:key="cat"
:label="cat"
:value="cat"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="排序">
<el-input-number v-model="form.sortOrder" :min="0" :max="9999" style="width: 100%;" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="状态">
<el-radio-group v-model="form.status">
<el-radio label="active">上架</el-radio>
<el-radio label="inactive">下架</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<!-- 食物图片 -->
<el-divider content-position="left">食物图片</el-divider>
<el-form-item label="食物图片">
<div class="image-upload-wrap">
<div class="upLoadPicBox" @click="modalPicTap">
<div v-if="form.image" class="pictrue">
<img :src="form.image" />
</div>
<div v-else class="upLoad">
<i class="el-icon-camera cameraIconfont" />
</div>
</div>
<div class="image-url-input">
<el-input
v-model="form.image"
placeholder="或直接粘贴图片URL"
clearable
style="width: 400px;"
>
<template slot="prepend">URL</template>
</el-input>
<span class="upload-tip">建议尺寸 400×400px支持 JPG/PNG不超过 2MB</span>
</div>
</div>
</el-form-item>
<!-- 营养成分 -->
<el-divider content-position="left">营养成分每100g</el-divider>
<el-row :gutter="16">
<el-col :span="6">
<el-form-item label="能量(kcal)">
<el-input-number v-model="form.energy" :min="0" :precision="0" style="width: 100%;" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="蛋白质(g)">
<el-input-number v-model="form.protein" :min="0" :precision="2" style="width: 100%;" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="脂肪(g)">
<el-input-number v-model="form.fat" :min="0" :precision="2" style="width: 100%;" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="碳水(g)">
<el-input-number v-model="form.carbohydrate" :min="0" :precision="2" style="width: 100%;" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="6">
<el-form-item label="钾(mg)">
<el-input-number v-model="form.potassium" :min="0" :precision="0" style="width: 100%;" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="磷(mg)">
<el-input-number v-model="form.phosphorus" :min="0" :precision="0" style="width: 100%;" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="钠(mg)">
<el-input-number v-model="form.sodium" :min="0" :precision="0" style="width: 100%;" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="钙(mg)">
<el-input-number v-model="form.calcium" :min="0" :precision="0" style="width: 100%;" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="6">
<el-form-item label="铁(mg)">
<el-input-number v-model="form.iron" :min="0" :precision="2" style="width: 100%;" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="维生素C(mg)">
<el-input-number v-model="form.vitaminC" :min="0" :precision="2" style="width: 100%;" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="嘌呤(mg)">
<el-input-number v-model="form.purine" :min="0" :precision="2" style="width: 100%;" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="参考基准">
<el-input v-model="form.servingSize" placeholder="每100g" />
</el-form-item>
</el-col>
</el-row>
<!-- 适宜性与建议 -->
<el-divider content-position="left">适宜性与饮食建议</el-divider>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="适宜性等级">
<el-select v-model="form.suitabilityLevel" placeholder="请选择" style="width: 100%;">
<el-option label="适宜" value="suitable" />
<el-option label="适量" value="moderate" />
<el-option label="限制" value="restricted" />
<el-option label="禁止" value="forbidden" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="注意等级">
<el-select v-model="form.cautionLevel" placeholder="请选择" style="width: 100%;">
<el-option label="无" :value="0" />
<el-option label="低" :value="1" />
<el-option label="中" :value="2" />
<el-option label="高" :value="3" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="推荐用量">
<el-input v-model="form.recommendedAmount" placeholder="如每日50-100g" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="适宜性说明">
<el-input v-model="form.suitabilityDesc" type="textarea" :rows="2" placeholder="适宜性详细说明" />
</el-form-item>
<el-form-item label="注意事项">
<el-input v-model="form.cautionDesc" type="textarea" :rows="2" placeholder="需要注意的事项说明" />
</el-form-item>
<el-form-item label="烹饪建议">
<el-input v-model="form.cookingTips" type="textarea" :rows="3" placeholder="烹饪方式建议" />
</el-form-item>
<!-- 提交按钮 -->
<el-form-item>
<el-button type="primary" :loading="submitting" @click="handleSubmit"> </el-button>
<el-button @click="$router.back()"> </el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script>
import { InfoFood, AddFood, UpdateFood, GetFoodCategories } from '@/api/foodEncyclopedia';
export default {
name: 'FoodEdit',
data() {
return {
isEdit: false,
submitting: false,
categoryList: [],
form: {
name: '',
alias: '',
category: '',
image: '',
energy: null,
protein: null,
fat: null,
carbohydrate: null,
potassium: null,
phosphorus: null,
sodium: null,
calcium: null,
iron: null,
vitaminC: null,
purine: null,
servingSize: '每100g',
nutrientsJson: '',
suitabilityLevel: 'suitable',
suitabilityDesc: '',
cautionLevel: 0,
cautionDesc: '',
recommendedAmount: '',
cookingTips: '',
status: 'active',
sortOrder: 0,
},
rules: {
name: [{ required: true, message: '请输入食物名称', trigger: 'blur' }],
category: [{ required: true, message: '请选择或输入分类', trigger: 'change' }],
},
};
},
mounted() {
this.getCategories();
const id = this.$route.params.id;
if (id) {
this.isEdit = true;
this.loadDetail(id);
}
},
methods: {
// 获取分类列表
getCategories() {
GetFoodCategories().then((res) => {
this.categoryList = res || [];
});
},
// 加载食物详情(编辑模式)
loadDetail(id) {
InfoFood(id).then((res) => {
const data = res || {};
Object.keys(this.form).forEach((key) => {
if (data[key] !== undefined && data[key] !== null) {
this.form[key] = data[key];
}
});
// 兜底修复历史脏数据image 字段被前缀拼接多次的情况
// 例https://host//https://host//crmebimage/... → https://host/crmebimage/...
if (this.form.image && typeof this.form.image === 'string') {
const img = this.form.image;
const hostMatch = img.match(/^(https?:\/\/[^/]+)\//);
const idx = img.indexOf('crmebimage/');
if (hostMatch && idx > 0) {
this.form.image = hostMatch[1] + '/' + img.substring(idx);
}
}
});
},
// 打开图片选择器(复用系统素材管理弹窗)
modalPicTap() {
const _this = this;
this.$modalUpload(
function (img) {
_this.form.image = img[0].sattDir;
},
'1',
'content',
);
},
// 提交表单
handleSubmit() {
this.$refs.form.validate((valid) => {
if (!valid) return;
this.submitting = true;
const action = this.isEdit
? UpdateFood(this.$route.params.id, this.form)
: AddFood(this.form);
action
.then(() => {
this.$message.success(this.isEdit ? '修改成功' : '新增成功');
this.$router.push('/content/foodManager');
})
.catch(() => {
this.$message.error('操作失败,请重试');
})
.finally(() => {
this.submitting = false;
});
});
},
},
};
</script>
<style scoped>
.image-upload-wrap {
display: flex;
align-items: flex-start;
gap: 20px;
}
.image-url-input {
display: flex;
flex-direction: column;
gap: 8px;
}
.upload-tip {
font-size: 12px;
color: #999;
}
.upLoadPicBox {
width: 120px;
height: 120px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border: 1px dashed #dcdfe6;
border-radius: 6px;
overflow: hidden;
}
.upLoadPicBox:hover {
border-color: #409eff;
}
.upLoadPicBox .pictrue {
width: 100%;
height: 100%;
}
.upLoadPicBox .pictrue img {
width: 100%;
height: 100%;
object-fit: cover;
}
.upLoadPicBox .upLoad {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.cameraIconfont {
font-size: 32px;
color: #c0c4cc;
}
</style>

View File

@@ -0,0 +1,237 @@
<template>
<div class="divBox">
<el-card class="box-card">
<div slot="header" class="clearfix">
<div class="container">
<el-form inline size="small">
<el-form-item label="分类:">
<el-select
v-model="listPram.category"
clearable
class="selWidth"
placeholder="全部分类"
@change="handleSearch"
>
<el-option
v-for="cat in categoryList"
:key="cat"
:label="cat"
:value="cat"
/>
</el-select>
</el-form-item>
<el-form-item label="状态:">
<el-select
v-model="listPram.status"
clearable
class="selWidth"
placeholder="全部状态"
@change="handleSearch"
>
<el-option label="上架" value="active" />
<el-option label="下架" value="inactive" />
</el-select>
</el-form-item>
<el-form-item label="关键词:">
<el-input
v-model="listPram.keyword"
placeholder="食物名称/别名"
class="selWidth"
size="small"
clearable
>
<el-button slot="append" icon="el-icon-search" @click="handleSearch" size="small" />
</el-input>
</el-form-item>
</el-form>
</div>
<router-link :to="{ path: '/content/foodEdit' }">
<el-button type="primary" class="mr10" v-hasPermi="['admin:tool:food:save']">添加食物</el-button>
</router-link>
</div>
<el-table
v-loading="listLoading"
:data="listData.list"
size="mini"
class="table"
highlight-current-row
:header-cell-style="{ fontWeight: 'bold' }"
>
<el-table-column prop="foodId" label="ID" min-width="60" />
<el-table-column label="图片" min-width="80">
<template slot-scope="scope">
<div class="demo-image__preview">
<el-image
style="width: 50px; height: 50px; border-radius: 4px;"
:src="scope.row.image"
:preview-src-list="scope.row.image ? [scope.row.image] : []"
fit="cover"
>
<div slot="error" class="image-slot">
<i class="el-icon-picture-outline" style="font-size: 24px; color: #c0c4cc;" />
</div>
</el-image>
</div>
</template>
</el-table-column>
<el-table-column prop="name" label="名称" min-width="120" show-overflow-tooltip />
<el-table-column prop="alias" label="别名" min-width="100" show-overflow-tooltip />
<el-table-column prop="category" label="分类" min-width="90" />
<el-table-column label="适宜性" min-width="80">
<template slot-scope="scope">
<el-tag :type="suitTagType(scope.row.suitabilityLevel)" size="mini">
{{ suitLabel(scope.row.suitabilityLevel) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="energy" label="能量(kcal)" min-width="90" />
<el-table-column prop="protein" label="蛋白质(g)" min-width="90" />
<el-table-column prop="potassium" label="钾(mg)" min-width="70" />
<el-table-column prop="phosphorus" label="磷(mg)" min-width="70" />
<el-table-column label="状态" min-width="70">
<template slot-scope="scope">
<el-switch
:value="scope.row.status === 'active'"
active-color="#13ce66"
@change="handleStatusChange(scope.row)"
/>
</template>
</el-table-column>
<el-table-column prop="sortOrder" label="排序" min-width="60" />
<el-table-column prop="updatedAt" label="更新时间" min-width="160" />
<el-table-column label="操作" min-width="120" fixed="right" align="center">
<template slot-scope="scope">
<router-link :to="{ path: '/content/foodEdit/' + scope.row.foodId }">
<el-button size="small" type="text" class="mr10" v-hasPermi="['admin:tool:food:info']">编辑</el-button>
</router-link>
<el-button
type="text"
size="small"
style="color: #F56C6C;"
@click="handleDelete(scope.row)"
v-hasPermi="['admin:tool:food:delete']"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
:current-page="listPram.page"
:page-sizes="constants.page.limit"
:layout="constants.page.layout"
:total="listData.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</el-card>
</div>
</template>
<script>
import { ListFood, DeleteFood, ChangeStatusFood, GetFoodCategories } from '@/api/foodEncyclopedia';
export default {
name: 'FoodList',
data() {
return {
constants: this.$constants,
listPram: {
keyword: null,
category: null,
status: null,
page: 1,
limit: this.$constants.page.limit[0],
},
listData: { list: [], total: 0 },
listLoading: false,
categoryList: [],
};
},
mounted() {
this.getList();
this.getCategories();
},
methods: {
// 获取食物列表
getList() {
this.listLoading = true;
ListFood(this.listPram)
.then((res) => {
this.listData = res || { list: [], total: 0 };
})
.finally(() => {
this.listLoading = false;
});
},
// 获取分类列表
getCategories() {
GetFoodCategories().then((res) => {
this.categoryList = res || [];
});
},
// 搜索
handleSearch() {
this.listPram.page = 1;
this.getList();
},
// 分页 - 每页条数变化
handleSizeChange(val) {
this.listPram.limit = val;
this.getList();
},
// 分页 - 页码变化
handleCurrentChange(val) {
this.listPram.page = val;
this.getList();
},
// 修改上下架状态
handleStatusChange(row) {
const newStatus = row.status === 'active' ? 'inactive' : 'active';
const actionText = newStatus === 'active' ? '上架' : '下架';
this.$confirm(`确定${actionText}${row.name}」吗?`, '提示', { type: 'warning' })
.then(() => ChangeStatusFood(row.foodId, newStatus))
.then(() => {
this.$message.success(`${actionText}成功`);
this.getList();
})
.catch(() => {});
},
// 删除食物
handleDelete(row) {
this.$confirm(`确定删除「${row.name}」吗?删除后不可恢复。`, '提示', { type: 'warning' })
.then(() => DeleteFood(row.foodId))
.then(() => {
this.$message.success('删除成功');
this.getList();
})
.catch(() => {});
},
// 适宜性等级标签
suitLabel(level) {
const map = { suitable: '适宜', moderate: '适量', restricted: '限制', forbidden: '禁止' };
return map[level] || level || '-';
},
// 适宜性等级标签类型
suitTagType(level) {
const map = { suitable: 'success', moderate: '', restricted: 'warning', forbidden: 'danger' };
return map[level] || 'info';
},
},
};
</script>
<style scoped>
.selWidth {
width: 180px;
}
.image-slot {
display: flex;
justify-content: center;
align-items: center;
width: 50px;
height: 50px;
background: #f5f7fa;
border-radius: 4px;
}
</style>