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:
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
93
msh_single_admin/src/api/foodEncyclopedia.js
Normal file
93
msh_single_admin/src/api/foodEncyclopedia.js
Normal 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',
|
||||
});
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
365
msh_single_admin/src/views/content/food/edit.vue
Normal file
365
msh_single_admin/src/views/content/food/edit.vue
Normal 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>
|
||||
237
msh_single_admin/src/views/content/food/list.vue
Normal file
237
msh_single_admin/src/views/content/food/list.vue
Normal 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>
|
||||
Reference in New Issue
Block a user