You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
crmeb/app/pages/supply_chain/day_menu/index.vue

804 lines
19 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<view class="page">
<view class="date-card">
<view class="date-row">
<view class="date-btn" @click="changeDateBy(-1)"></view>
<view class="date-picker-container">
<view class="date-text" @click="openCalendar">
<text>{{ selectedDate }}</text>
<text class="iconfont icon-xiangxia"></text>
</view>
</view>
<view class="date-btn" @click="changeDateBy(1)"></view>
</view>
<view class="date-tip">默认当天可切换查看不同日期菜单</view>
</view>
<!-- 食堂筛选 -->
<view class="canteen-tabs" v-if="canteens && canteens.length > 1">
<view class="canteen-tabs-container">
<view
v-for="canteen in canteens"
:key="canteen.value"
class="canteen-tab"
:class="selectedCanteen === canteen.value ? 'active' : ''"
@click="switchCanteen(canteen.value)"
>
{{ canteen.label }}
</view>
</view>
</view>
<!-- 日历组件 -->
<uni-calendar
ref="calendar"
:insert="false"
:range="false"
:start-date="'2026-01-01'"
:end-date="'2099-12-31'"
@confirm="confirm"
></uni-calendar>
<!-- 排行榜日期范围选择器 -->
<uni-calendar
ref="rankingCalendar"
:insert="false"
:range="true"
:start-date="'2026-01-01'"
:end-date="'2099-12-31'"
@confirm="confirmRankingDate"
></uni-calendar>
<view class="section">
<view class="section-title">当日菜单</view>
<view class="meal-card" v-for="meal in mealSections" :key="meal.key">
<view class="meal-title">{{ meal.label }}</view>
<view v-if="meal.list.length">
<view class="dish-item" v-for="item in meal.list" :key="item.id">
<view class="dish-main">
<view class="dish-info">
<view class="dish-name">{{ item.itemName || '未命名菜品' }}</view>
<view class="dish-price" v-if="item.itemPrice !== undefined && item.itemPrice !== null">
¥{{ item.itemPrice }}
</view>
</view>
</view>
<view class="action-row">
<view class="action like" @click="submitLike(item, '1')">
<image :src="item.isLiked == '1' ? '/static/images/wg/like_.png' : '/static/images/wg/like.png'" class="action-icon" />
<span>{{ item.likeCount || 0 }}</span>
</view>
<view class="action dislike" @click="submitLike(item, '2')">
<image :src="item.isDisliked == '1' ? '/static/images/wg/dislike_.png' : '/static/images/wg/dislike.png'" class="action-icon" />
<span>{{ item.dislikeCount || 0 }}</span>
</view>
</view>
</view>
</view>
<view v-else class="empty">暂无{{ meal.label }}菜品</view>
</view>
</view>
<view class="section">
<view class="rank-header">
<view class="section-title">菜品排行</view>
<view class="rank-tabs">
<view
class="rank-tab"
:class="rankingType === 'like' ? 'active' : ''"
@click="switchRanking('like')"
>
点赞排行
</view>
<view
class="rank-tab"
:class="rankingType === 'dislike' ? 'active' : ''"
@click="switchRanking('dislike')"
>
点踩排行
</view>
</view>
</view>
<view class="rank-date-range">
<view class="date-range-container">
<view class="date-range-btn">
<view v-if="rankingDateRange.startDate && rankingDateRange.endDate" class="clear-btn" @click.stop="clearRankingDate">
</view>
<view class="date-text" @click="openRankingDatePicker">
{{ rankingDateRange.startDate && rankingDateRange.endDate ? `${rankingDateRange.startDate} 至 ${rankingDateRange.endDate}` : '选择日期范围' }}
</view>
<view class="icon" @click="openRankingDatePicker">
<text class="iconfont icon-xiangxia"></text>
</view>
</view>
</view>
</view>
<view v-if="rankingList.length">
<view class="rank-item" v-for="(item, index) in rankingList" :key="item.id" @click="viewLikeRecords(item)">
<view class="rank-left">
<text class="rank-no" :class="index < 3 ? 'top' : ''">{{ index + 1 }}</text>
<text class="rank-name">{{ item.itemName || '未命名菜品' }}</text>
</view>
<view class="rank-right" :class="rankingType">
<image v-if="rankingType === 'like'" :src="'/static/images/wg/like.png'" class="rank-icon" />
<image v-else :src="'/static/images/wg/dislike.png'" class="rank-icon" />
<span>{{ rankingType === 'like' ? (item.likeCount || 0) : (item.dislikeCount || 0) }}</span>
</view>
</view>
</view>
<view v-else class="empty">暂无排行数据</view>
</view>
<!-- 点赞点踩记录弹窗 -->
<LikeRecordPopup
ref="likeRecordPopup"
:menu-dtl-id="currentMenuDtlId"
:item-name="currentMenuItemName"
:like-type="currentLikeType"
/>
</view>
</template>
<script>
import {
listDailyMenuDetails,
likeDailyMenuItem,
cancelLikeDailyMenuItem,
getDailyMenuRanking
} from '@/api/property.js';
import { checkPermi } from '@/utils/auth/permission.js';
import LikeRecordPopup from './LikeRecordPopup.vue';
export default {
components: {
LikeRecordPopup
},
dicts: ['canteen_name', 'meal_type'],
data() {
return {
checkPermi,
selectedDate: '',
menuList: [],
rankingType: 'like',
rankingList: [],
mealSections: [],
canteens: [],
selectedCanteen: '',
rankingDateRange: {
startDate: '',
endDate: ''
},
showRankingDatePicker: false,
currentMenuDtlId: '',
currentMenuItemName: '',
currentLikeType: '1'
};
},
onLoad() {
this.selectedDate = this.formatDate(new Date());
this.rankingDateRange.startDate = this.selectedDate;
this.rankingDateRange.endDate = this.selectedDate;
this.loadCanteens();
this.loadPageData();
},
mounted() {
// 监听字典数据变化
this.$on('dictChange', () => {
this.loadCanteens();
this.loadMealTypes();
// 如果菜单数据已加载,则重新分组
if (this.menuList && this.menuList.length > 0) {
this.groupMeals(this.menuList);
}
});
},
beforeUnmount() {
this.$off('dictChange');
},
methods: {
async promptDislikeRemark() {
return new Promise((resolve) => {
uni.showModal({
title: '请输入点踩原因',
editable: true,
placeholderText: '请填写点踩原因',
success: (res) => {
if (!res.confirm) {
resolve(null);
return;
}
const remark = (res.content || '').trim();
if (!remark) {
uni.showToast({
title: '请输入点踩原因',
icon: 'none'
});
resolve('');
return;
}
resolve(remark);
},
fail: () => resolve(null)
});
});
},
async loadPageData() {
await Promise.all([this.loadMenuByDate(), this.loadRanking()]);
},
loadCanteens() {
const canteenDict = this.dict.get('canteen_name') || [];
this.canteens = canteenDict.map(item => ({
value: item.dictValue,
label: item.dictLabel
}));
if (this.canteens.length > 0 && !this.selectedCanteen) {
this.selectedCanteen = this.canteens[0].value;
}
},
loadMealTypes() {
const mealTypeDict = this.dict.get('meal_type') || [];
this.mealSections = mealTypeDict.map(item => ({
key: item.dictValue,
label: item.dictLabel,
list: []
}));
},
async loadMenuByDate() {
try {
uni.showLoading({ title: '加载菜单中...', mask: true });
const res = await listDailyMenuDetails({
menuDate: this.selectedDate,
canteenName: this.selectedCanteen
});
const list = res?.data || [];
this.menuList = list;
this.groupMeals(list);
} catch (e) {
this.menuList = [];
this.groupMeals([]);
uni.showToast({
title: typeof e === 'string' ? e : '菜单加载失败',
icon: 'none'
});
} finally {
uni.hideLoading();
}
},
groupMeals(list) {
// 重新从字典获取餐次类型,确保数据最新
const mealTypeDict = this.dict.get('meal_type') || [];
const mealTypeMap = {};
mealTypeDict.forEach(item => {
mealTypeMap[item.dictValue] = item.dictLabel;
});
// 按餐次类型分组
const grouped = {};
list.forEach((item) => {
const mealType = this.normalizeMealType(item.mealType);
if (mealType && !grouped[mealType]) {
grouped[mealType] = [];
}
if (mealType) {
grouped[mealType].push(item);
}
});
// 根据字典顺序重新构建 mealSections
this.mealSections = mealTypeDict.map(item => ({
key: item.dictValue,
label: item.dictLabel,
list: grouped[item.dictValue] || []
}));
},
normalizeMealType(type) {
if (!type) return '';
const v = String(type).trim();
// 直接返回 dictValue
const mealTypeDict = this.dict.get('meal_type') || [];
for (const item of mealTypeDict) {
// 匹配 dictValue 或 dictLabel
if (String(item.dictValue).trim() === v ||
String(item.dictLabel).trim() === v ||
String(item.dictValue).toLowerCase() === v.toLowerCase()) {
return item.dictValue;
}
}
return v;
},
async submitLike(item, likeType) {
if (!item || !item.id) return;
try {
// 判断是否需要取消点赞/点踩
const isLiked = likeType === '1' && item.isLiked === '1';
const isDisliked = likeType === '2' && item.isDisliked === '1';
if (isLiked || isDisliked) {
uni.showLoading({ title: likeType === '1' ? '取消点赞中...' : '取消点踩中...', mask: true });
await cancelLikeDailyMenuItem({
menuDtlId: item.id,
likeType
});
uni.showToast({
title: likeType === '1' ? '取消点赞成功' : '取消点踩成功',
icon: 'success'
});
// 立即更新本地状态,提供即时反馈
if (likeType === '1') {
item.isLiked = '0';
item.likeCount = Math.max(0, (item.likeCount || 0) - 1);
} else {
item.isDisliked = '0';
item.dislikeCount = Math.max(0, (item.dislikeCount || 0) - 1);
}
} else {
let remark = '';
if (likeType === '2') {
remark = await this.promptDislikeRemark();
if (remark === null || remark === '') return;
}
uni.showLoading({ title: likeType === '1' ? '点赞中...' : '点踩中...', mask: true });
await likeDailyMenuItem({
menuDtlId: item.id,
likeType,
remark
});
uni.showToast({
title: likeType === '1' ? '点赞成功' : '点踩成功',
icon: 'success'
});
// 立即更新本地状态,提供即时反馈
if (likeType === '1') {
item.isLiked = '1';
item.likeCount = (item.likeCount || 0) + 1;
} else {
item.isDisliked = '1';
item.dislikeCount = (item.dislikeCount || 0) + 1;
}
}
// await Promise.all([this.loadMenuByDate(), this.loadRanking()]);
} catch (e) {
uni.showToast({
title: typeof e === 'string' ? e : '操作失败',
icon: 'none'
});
} finally {
uni.hideLoading();
}
},
async loadRanking() {
try {
const res = await getDailyMenuRanking({
limit: 20,
rankingType: this.rankingType,
startDate: this.rankingDateRange.startDate,
endDate: this.rankingDateRange.endDate
});
this.rankingList = res?.data?.list || [];
} catch (e) {
this.rankingList = [];
}
},
async switchRanking(type) {
if (this.rankingType === type) return;
this.rankingType = type;
await this.loadRanking();
},
// 打开日历
openCalendar() {
this.$refs.calendar.open();
},
// 确认日期
async confirm(e) {
this.selectedDate = e.fulldate;
await this.loadMenuByDate();
},
async changeDateBy(step) {
const d = new Date(this.selectedDate.replace(/-/g, '/'));
d.setDate(d.getDate() + step);
this.selectedDate = this.formatDate(d);
await this.loadMenuByDate();
},
formatDate(date) {
const y = date.getFullYear();
const m = `${date.getMonth() + 1}`.padStart(2, '0');
const d = `${date.getDate()}`.padStart(2, '0');
return `${y}-${m}-${d}`;
},
async switchCanteen(canteenValue) {
if (this.selectedCanteen === canteenValue) return;
this.selectedCanteen = canteenValue;
await this.loadPageData();
},
openRankingDatePicker() {
this.$refs.rankingCalendar.open();
},
async confirmRankingDate(e) {
this.rankingDateRange.startDate = e.range.before;
this.rankingDateRange.endDate = e.range.after;
await this.loadRanking();
},
async clearRankingDate() {
this.rankingDateRange.startDate = '';
this.rankingDateRange.endDate = '';
await this.loadRanking();
},
// 查看点赞/点踩记录
viewLikeRecords(item) {
if (!checkPermi('daily_menu_record')) {
return;
}
this.currentMenuDtlId = item.id;
this.currentMenuItemName = item.itemName || '未命名菜品';
this.currentLikeType = this.rankingType === 'like' ? '1' : '2';
this.$nextTick(() => {
this.$refs.likeRecordPopup.open();
});
}
}
};
</script>
<style lang="scss">
.page {
min-height: 100vh;
background: #f6f7fb;
padding: 24rpx;
}
/* */
.canteen-tabs {
margin-bottom: 20rpx;
overflow-x: auto;
white-space: nowrap;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
.canteen-tabs-container {
display: inline-flex;
background: #fff;
border-radius: 16rpx;
padding: 8rpx;
}
.canteen-tab {
padding: 12rpx 24rpx;
margin: 0 4rpx;
border-radius: 999rpx;
font-size: 26rpx;
color: #6b7280;
transition: all 0.3s;
&.active {
background: #3b82f6;
color: #fff;
font-weight: 600;
}
}
}
/* */
.rank-date-range {
margin-bottom: 18rpx;
display: flex;
justify-content: flex-end;
}
.date-range-container {
display: flex;
align-items: center;
gap: 12rpx;
}
.date-range-btn {
display: flex;
align-items: center;
padding: 10rpx 16rpx;
background: #f3f4f6;
border-radius: 999rpx;
font-size: 24rpx;
color: #6b7280;
cursor: pointer;
gap: 12rpx;
.clear-btn {
font-size: 20rpx;
color: #9ca3af;
cursor: pointer;
padding: 0 4rpx;
&:hover {
color: #ef4444;
}
}
.date-text {
flex: 1;
font-size: 24rpx;
color: #6b7280;
font-weight: normal;
}
.icon {
.iconfont {
font-size: 18rpx;
}
}
}
.rank-tabs {
display: flex;
background: #f3f4f6;
border-radius: 999rpx;
padding: 4rpx;
}
.date-card,
.section,
.meal-card {
background: #fff;
border-radius: 16rpx;
}
.date-card {
padding: 24rpx;
margin-bottom: 20rpx;
}
.date-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.date-btn {
width: 160rpx;
text-align: center;
height: 64rpx;
line-height: 64rpx;
background: #f2f4f8;
color: #333;
border-radius: 10rpx;
font-size: 26rpx;
}
.date-picker-container {
display: flex;
align-items: center;
}
.date-text {
min-width: 260rpx;
text-align: center;
font-size: 30rpx;
color: #1f2d3d;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
text {
margin-right: 10rpx;
}
.iconfont {
font-size: 20rpx;
color: #666;
}
}
.date-tip {
margin-top: 16rpx;
color: #8a94a6;
font-size: 24rpx;
}
.section {
padding: 24rpx;
margin-bottom: 20rpx;
}
.section-title {
font-size: 32rpx;
color: #222;
font-weight: 600;
margin-bottom: 18rpx;
}
.meal-card {
border: 1rpx solid #edf0f5;
padding: 20rpx;
margin-bottom: 16rpx;
}
.meal-title {
font-size: 28rpx;
color: #3b82f6;
font-weight: 600;
margin-bottom: 14rpx;
}
.dish-item {
padding: 16rpx 0;
border-bottom: 1rpx solid #f2f3f7;
display: flex;
align-items: flex-start;
gap: 16rpx;
}
.dish-item:last-child {
border-bottom: none;
}
.dish-main {
flex: 1;
min-width: 0;
}
.dish-info {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 4rpx;
}
.dish-name {
font-size: 28rpx;
color: #1f2937;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.dish-desc {
color: #6b7280;
font-size: 20rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.dish-price {
color: #ef4444;
font-size: 24rpx;
white-space: nowrap;
}
.action-row {
display: flex;
gap: 16rpx;
white-space: nowrap;
align-self: center;
}
.action {
height: 52rpx;
line-height: 52rpx;
padding: 0 20rpx;
border-radius: 26rpx;
font-size: 24rpx;
display: flex;
align-items: center;
gap: 8rpx;
}
.action-icon {
width: 28rpx;
height: 28rpx;
vertical-align: middle;
}
.like {
background: #ecfdf3;
color: #16a34a;
}
.dislike {
background: #fef2f2;
color: #dc2626;
}
.rank-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 18rpx;
}
.rank-tabs {
display: flex;
background: #f3f4f6;
border-radius: 999rpx;
padding: 4rpx;
}
.rank-tab {
padding: 10rpx 20rpx;
font-size: 24rpx;
color: #6b7280;
border-radius: 999rpx;
}
.rank-tab.active {
background: #fff;
color: #2563eb;
font-weight: 600;
}
.rank-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 18rpx 0;
border-bottom: 1rpx solid #f2f3f7;
}
.rank-item:last-child {
border-bottom: none;
}
.rank-left {
display: flex;
align-items: center;
}
.rank-no {
width: 40rpx;
text-align: center;
font-size: 26rpx;
color: #6b7280;
margin-right: 14rpx;
}
.rank-no.top {
color: #f97316;
font-weight: 700;
}
.rank-name {
font-size: 27rpx;
color: #1f2937;
}
.rank-right {
font-size: 26rpx;
color: #6b7280;
display: flex;
align-items: center;
gap: 8rpx;
border-radius: 4rpx;
padding: 2rpx 4rpx;
}
.rank-right.like {
color: #16a34a;
}
.rank-right.dislike {
color: #dc2626;
}
.rank-icon {
width: 28rpx;
height: 28rpx;
vertical-align: middle;
}
.empty {
color: #9ca3af;
font-size: 24rpx;
padding: 20rpx 0;
text-align: center;
}
</style>