|
|
@@ -12,7 +12,14 @@
|
|
|
</u-navbar>
|
|
|
|
|
|
<view class="search-bar" v-if="showSearch && !batchMode">
|
|
|
- <u-search v-model="keyword" placeholder="请输入菜品/套餐名称" :show-action="true" action-text="搜索" @search="noop" @custom="noop"></u-search>
|
|
|
+ <u-search
|
|
|
+ v-model="keyword"
|
|
|
+ placeholder="请输入菜品/套餐名称"
|
|
|
+ :show-action="true"
|
|
|
+ action-text="搜索"
|
|
|
+ @search="onSearch"
|
|
|
+ @custom="onSearch"
|
|
|
+ ></u-search>
|
|
|
</view>
|
|
|
|
|
|
<view class="filters">
|
|
|
@@ -32,47 +39,59 @@
|
|
|
<text>{{ bannerText }}</text>
|
|
|
</view>
|
|
|
|
|
|
- <scroll-view scroll-y class="list" :style="{ paddingBottom: batchMode ? '140rpx' : '160rpx' }">
|
|
|
- <view class="card" v-for="item in filteredList" :key="item.id" @click="toggleSelect(item)">
|
|
|
- <view class="check" v-if="batchMode">
|
|
|
- <view :class="['dot', selectedIds.includes(item.id) ? 'on' : '']"></view>
|
|
|
- </view>
|
|
|
- <view class="thumb-wrap">
|
|
|
- <image class="thumb" :src="item.image" mode="aspectFill"></image>
|
|
|
- <view class="tag" :class="item.belong">{{ item.belong === 'group' ? '团餐' : '门店' }}</view>
|
|
|
- </view>
|
|
|
- <view class="info">
|
|
|
- <view class="row-top">
|
|
|
- <text class="name">{{ item.name }}</text>
|
|
|
- <text class="status">{{ statusText[item.status] }}</text>
|
|
|
- </view>
|
|
|
- <view class="desc">{{ item.desc }}</view>
|
|
|
- <view class="price-line">
|
|
|
- <text class="price">¥{{ item.price }}</text>
|
|
|
- <text class="sub">原价¥{{ item.originPrice }}</text>
|
|
|
- <text class="sub">库存{{ item.stock }}</text>
|
|
|
- </view>
|
|
|
- <view class="row-bottom">
|
|
|
- <text class="sales">合计销量: {{ item.sales }}</text>
|
|
|
- <view class="actions" v-if="!batchMode" @click.stop>
|
|
|
- <template v-if="item.status === 'selling' || item.status === 'soldout'">
|
|
|
- <view class="btn primary" @click="onOffShelf(item)">下架</view>
|
|
|
- </template>
|
|
|
- <template v-else-if="item.status === 'pending'">
|
|
|
- <view class="btn" @click="onDelete(item)">删除</view>
|
|
|
- <view class="btn" @click="goEdit(item)">编辑</view>
|
|
|
- <view class="btn primary" @click="onOnShelf(item)">上架</view>
|
|
|
- </template>
|
|
|
- <template v-else-if="item.status === 'rejected'">
|
|
|
- <view class="btn" @click="onDelete(item)">删除</view>
|
|
|
- <view class="btn" @click="goEdit(item)">编辑</view>
|
|
|
- </template>
|
|
|
+ <view class="list-wrap" :style="{ paddingBottom: batchMode ? '140rpx' : '160rpx' }">
|
|
|
+ <ListScrollView
|
|
|
+ ref="listScroll"
|
|
|
+ :api="api"
|
|
|
+ :init="initParams"
|
|
|
+ :pageSize="20"
|
|
|
+ emptyText="暂无菜品"
|
|
|
+ @afterFetch="afterFetch"
|
|
|
+ >
|
|
|
+ <template v-slot:list="{ data }">
|
|
|
+ <view class="list">
|
|
|
+ <view class="card" v-for="item in data" :key="item.id" @click="toggleSelect(item)">
|
|
|
+ <view class="check" v-if="batchMode">
|
|
|
+ <view :class="['dot', selectedIds.includes(item.id) ? 'on' : '']"></view>
|
|
|
+ </view>
|
|
|
+ <view class="thumb-wrap">
|
|
|
+ <image class="thumb" :src="item.image" mode="aspectFill"></image>
|
|
|
+ <view class="tag" :class="item.belong">{{ item.belong === 'group' ? '团餐' : '门店' }}</view>
|
|
|
+ </view>
|
|
|
+ <view class="info">
|
|
|
+ <view class="row-top">
|
|
|
+ <text class="name">{{ item.name }}</text>
|
|
|
+ <text class="status">{{ statusText[item.status] || item.statusName }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="desc">{{ item.desc }}</view>
|
|
|
+ <view class="price-line">
|
|
|
+ <text class="price">¥{{ item.price }}</text>
|
|
|
+ <text class="sub">原价¥{{ item.originPrice }}</text>
|
|
|
+ <text class="sub">库存{{ item.stock }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="row-bottom">
|
|
|
+ <text class="sales">合计销量: {{ item.sales }}</text>
|
|
|
+ <view class="actions" v-if="!batchMode" @click.stop>
|
|
|
+ <template v-if="item.status === 'selling' || item.status === 'soldout'">
|
|
|
+ <view class="btn primary" @click="onOffShelf(item)">下架</view>
|
|
|
+ </template>
|
|
|
+ <template v-else-if="item.status === 'pending'">
|
|
|
+ <view class="btn" @click="onDelete(item)">删除</view>
|
|
|
+ <view class="btn" @click="goEdit(item)">编辑</view>
|
|
|
+ <view class="btn primary" @click="onOnShelf(item)">上架</view>
|
|
|
+ </template>
|
|
|
+ <template v-else-if="item.status === 'rejected'">
|
|
|
+ <view class="btn" @click="onDelete(item)">删除</view>
|
|
|
+ <view class="btn" @click="goEdit(item)">编辑</view>
|
|
|
+ </template>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
</view>
|
|
|
- </view>
|
|
|
- </view>
|
|
|
- <u-empty v-if="!filteredList.length" text="暂无菜品" mode="list"></u-empty>
|
|
|
- </scroll-view>
|
|
|
+ </template>
|
|
|
+ </ListScrollView>
|
|
|
+ </view>
|
|
|
|
|
|
<!-- 底部:新建 / 批量操作 -->
|
|
|
<view class="footer" v-if="!batchMode">
|
|
|
@@ -107,9 +126,14 @@
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
-import { createMockDishes } from '../mock.js';
|
|
|
-
|
|
|
-const DISH_KEY = 'merchantDish_list';
|
|
|
+import ListScrollView from '@/components/list-scroll-view/index.vue';
|
|
|
+import merchantDishApi, {
|
|
|
+ STATUS_KEY_TO_TYPE,
|
|
|
+ STATUS_TEXT,
|
|
|
+ mapPackageToListItem,
|
|
|
+ belongingToApi,
|
|
|
+ getMerchantId
|
|
|
+} from '@/api/merchantDish.js';
|
|
|
|
|
|
const STATUS_OPTIONS = [
|
|
|
{ label: '全部', value: '' },
|
|
|
@@ -126,25 +150,16 @@ const TYPE_OPTIONS = [
|
|
|
{ label: '门店', value: 'store' }
|
|
|
];
|
|
|
|
|
|
-const CATEGORY_OPTIONS = [
|
|
|
- { label: '全部品类', value: '' },
|
|
|
- { label: '时令美味', value: 'seasonal' },
|
|
|
- { label: '家常菜', value: 'home' },
|
|
|
- { label: '主食', value: 'staple' }
|
|
|
-];
|
|
|
-
|
|
|
-const STATUS_TEXT = {
|
|
|
- selling: '销售中',
|
|
|
- soldout: '已售完',
|
|
|
- pending: '待上架',
|
|
|
- reviewing: '审核中',
|
|
|
- rejected: '未通过'
|
|
|
-};
|
|
|
-
|
|
|
export default {
|
|
|
+ components: {
|
|
|
+ ListScrollView
|
|
|
+ },
|
|
|
data() {
|
|
|
return {
|
|
|
- list: [],
|
|
|
+ api: {
|
|
|
+ url: '/groupmeal/gmMerchantPackage/pageList',
|
|
|
+ method: 'POST'
|
|
|
+ },
|
|
|
keyword: '',
|
|
|
showSearch: false,
|
|
|
batchMode: false,
|
|
|
@@ -152,12 +167,14 @@ export default {
|
|
|
filterStatus: '',
|
|
|
filterType: '',
|
|
|
filterCategory: '',
|
|
|
+ categoryOptions: [{ label: '全部品类', value: '' }],
|
|
|
pickerShow: false,
|
|
|
pickerType: '',
|
|
|
pickerList: [],
|
|
|
statusText: STATUS_TEXT,
|
|
|
- // 团餐报名:0未报名 1已通过 2驳回 3审核中
|
|
|
- applyStatus: 0
|
|
|
+ applyStatus: 0,
|
|
|
+ listVersion: 0,
|
|
|
+ listReady: false
|
|
|
};
|
|
|
},
|
|
|
computed: {
|
|
|
@@ -168,19 +185,27 @@ export default {
|
|
|
return (TYPE_OPTIONS.find((i) => i.value === this.filterType) || {}).label || '全部商品';
|
|
|
},
|
|
|
categoryLabel() {
|
|
|
- return (CATEGORY_OPTIONS.find((i) => i.value === this.filterCategory) || {}).label || '全部品类';
|
|
|
+ return (this.categoryOptions.find((i) => i.value === this.filterCategory) || {}).label || '全部品类';
|
|
|
},
|
|
|
- filteredList() {
|
|
|
- return this.list.filter((item) => {
|
|
|
- if (this.filterStatus && item.status !== this.filterStatus) return false;
|
|
|
- if (this.filterType && item.belong !== this.filterType) return false;
|
|
|
- if (this.filterCategory && item.category !== this.filterCategory) return false;
|
|
|
- if (this.keyword && !item.name.includes(this.keyword)) return false;
|
|
|
- return true;
|
|
|
- });
|
|
|
+ initParams() {
|
|
|
+ const params = {
|
|
|
+ merchantId: getMerchantId(),
|
|
|
+ statusType: STATUS_KEY_TO_TYPE[this.filterStatus] != null ? STATUS_KEY_TO_TYPE[this.filterStatus] : 0
|
|
|
+ };
|
|
|
+ if (this.keyword) params.name = this.keyword;
|
|
|
+ if (this.filterType) params.productBelonging = belongingToApi(this.filterType);
|
|
|
+ if (this.filterCategory) params.specialCategoryIds = [this.filterCategory];
|
|
|
+ return params;
|
|
|
+ },
|
|
|
+ currentList() {
|
|
|
+ // listVersion 用于操作后触发重新读取
|
|
|
+ void this.listVersion;
|
|
|
+ const scroll = this.$refs.listScroll;
|
|
|
+ return (scroll && scroll.dataList) || [];
|
|
|
},
|
|
|
isAllSelected() {
|
|
|
- const ids = this.filteredList.map((i) => i.id);
|
|
|
+ const list = this.currentList;
|
|
|
+ const ids = list.map((i) => i.id);
|
|
|
return ids.length > 0 && ids.every((id) => this.selectedIds.includes(id));
|
|
|
},
|
|
|
bannerVisible() {
|
|
|
@@ -193,22 +218,28 @@ export default {
|
|
|
return '';
|
|
|
}
|
|
|
},
|
|
|
+ onLoad() {
|
|
|
+ this.loadSpecialCategories();
|
|
|
+ },
|
|
|
onShow() {
|
|
|
- this.loadList();
|
|
|
+ // 首次进入交给 ListScrollView 自动加载,避免重复请求;返回本页时再刷新
|
|
|
+ if (this.listReady) {
|
|
|
+ this.reloadList();
|
|
|
+ } else {
|
|
|
+ this.listReady = true;
|
|
|
+ }
|
|
|
this.fetchApplyStatus();
|
|
|
},
|
|
|
methods: {
|
|
|
- noop() {},
|
|
|
goBack() {
|
|
|
uni.navigateBack({ delta: 1 });
|
|
|
},
|
|
|
- // 从接口同步报名状态(本地:0未报名 1已通过 2驳回 3审核中;接口:0审核中 1通过 2驳回)
|
|
|
fetchApplyStatus() {
|
|
|
const cache = uni.getStorageSync('merchantDish_applyStatus');
|
|
|
if (cache !== '' && cache !== undefined && cache !== null) {
|
|
|
this.applyStatus = Number(cache);
|
|
|
}
|
|
|
- this.$http.post('/groupmeal/gmMerchantApplicationRecord/isApply', {}).then((res) => {
|
|
|
+ merchantDishApi.isApply().then((res) => {
|
|
|
if (res.data.code !== 200) return;
|
|
|
const result = res.data.result;
|
|
|
let localStatus = 0;
|
|
|
@@ -222,32 +253,55 @@ export default {
|
|
|
uni.setStorageSync('merchantDish_applyStatus', localStatus);
|
|
|
});
|
|
|
},
|
|
|
- loadList() {
|
|
|
- const cache = uni.getStorageSync(DISH_KEY);
|
|
|
- if (cache && cache.length) {
|
|
|
- this.list = cache;
|
|
|
- } else {
|
|
|
- this.list = createMockDishes();
|
|
|
- uni.setStorageSync(DISH_KEY, this.list);
|
|
|
- }
|
|
|
+ loadSpecialCategories() {
|
|
|
+ merchantDishApi.specialCategoryList().then((res) => {
|
|
|
+ if (res.data.code !== 200) return;
|
|
|
+ const rows = res.data.result || [];
|
|
|
+ this.categoryOptions = [
|
|
|
+ { label: '全部品类', value: '' },
|
|
|
+ ...rows.map((i) => ({
|
|
|
+ label: i.categoryName,
|
|
|
+ value: String(i.id)
|
|
|
+ }))
|
|
|
+ ];
|
|
|
+ });
|
|
|
+ },
|
|
|
+ afterFetch(result) {
|
|
|
+ const records = Array.isArray(result.records) ? result.records : [];
|
|
|
+ records.forEach((item) => {
|
|
|
+ Object.assign(item, mapPackageToListItem(item));
|
|
|
+ });
|
|
|
+ this.listVersion += 1;
|
|
|
},
|
|
|
- saveList() {
|
|
|
- uni.setStorageSync(DISH_KEY, this.list);
|
|
|
+ reloadList() {
|
|
|
+ this.$nextTick(() => {
|
|
|
+ const scroll = this.$refs.listScroll;
|
|
|
+ if (scroll && scroll.mescroll) {
|
|
|
+ scroll.reloadList(this.initParams);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+ onSearch() {
|
|
|
+ this.reloadList();
|
|
|
},
|
|
|
openPicker(type) {
|
|
|
if (this.batchMode) return;
|
|
|
this.pickerType = type;
|
|
|
- const map = { status: STATUS_OPTIONS, type: TYPE_OPTIONS, category: CATEGORY_OPTIONS };
|
|
|
+ const map = {
|
|
|
+ status: STATUS_OPTIONS,
|
|
|
+ type: TYPE_OPTIONS,
|
|
|
+ category: this.categoryOptions
|
|
|
+ };
|
|
|
this.pickerList = (map[type] || []).map((i) => ({ text: i.label, value: i.value }));
|
|
|
this.pickerShow = true;
|
|
|
},
|
|
|
onPick(e) {
|
|
|
- // u-action-sheet 回调为 { text, value, index }
|
|
|
const value = e && e.value !== undefined ? e.value : '';
|
|
|
if (this.pickerType === 'status') this.filterStatus = value;
|
|
|
if (this.pickerType === 'type') this.filterType = value;
|
|
|
if (this.pickerType === 'category') this.filterCategory = value;
|
|
|
this.selectedIds = [];
|
|
|
+ this.reloadList();
|
|
|
},
|
|
|
enterBatch() {
|
|
|
this.batchMode = true;
|
|
|
@@ -265,7 +319,7 @@ export default {
|
|
|
},
|
|
|
toggleAll() {
|
|
|
if (this.isAllSelected) this.selectedIds = [];
|
|
|
- else this.selectedIds = this.filteredList.map((i) => i.id);
|
|
|
+ else this.selectedIds = this.currentList.map((i) => i.id);
|
|
|
},
|
|
|
goCreate() {
|
|
|
uni.navigateTo({ url: '/pages/merchantDish/dish/form' });
|
|
|
@@ -292,29 +346,28 @@ export default {
|
|
|
},
|
|
|
async onOffShelf(item) {
|
|
|
let content = '确定下架当前商品';
|
|
|
- if (item.belong === 'group') {
|
|
|
- if (item.unformedOrders || item.formedOrders) {
|
|
|
- content = `该商品当前有未成团订单${item.unformedOrders}份,已成团${item.formedOrders}份,是否要确定下架`;
|
|
|
- }
|
|
|
- } else if (item.pendingPayOrders) {
|
|
|
- content = '已选商品有待支付订单,是否要确定下架';
|
|
|
+ if (item.belong === 'group' && item.hasInProgressOrder) {
|
|
|
+ content = '该商品当前有进行中的团餐订单,是否确定下架';
|
|
|
}
|
|
|
if (!(await this.confirmModal(content))) return;
|
|
|
- item.status = 'pending';
|
|
|
- this.saveList();
|
|
|
- uni.showToast({ title: '已下架', icon: 'none' });
|
|
|
+ merchantDishApi.packageLaunch(item.id, 0).then((res) => {
|
|
|
+ this.$tip.toast(res.data.message || (res.data.code === 200 ? '已下架' : '操作失败'));
|
|
|
+ if (res.data.code === 200) this.reloadList();
|
|
|
+ });
|
|
|
},
|
|
|
async onOnShelf(item) {
|
|
|
if (!(await this.confirmModal('确定上架当前商品'))) return;
|
|
|
- item.status = item.stock > 0 ? 'selling' : 'soldout';
|
|
|
- this.saveList();
|
|
|
- uni.showToast({ title: '已上架', icon: 'none' });
|
|
|
+ merchantDishApi.packageLaunch(item.id, 1).then((res) => {
|
|
|
+ this.$tip.toast(res.data.message || (res.data.code === 200 ? '已上架' : '操作失败'));
|
|
|
+ if (res.data.code === 200) this.reloadList();
|
|
|
+ });
|
|
|
},
|
|
|
async onDelete(item) {
|
|
|
if (!(await this.confirmModal('确定删除当前商品'))) return;
|
|
|
- this.list = this.list.filter((i) => i.id !== item.id);
|
|
|
- this.saveList();
|
|
|
- uni.showToast({ title: '已删除', icon: 'none' });
|
|
|
+ merchantDishApi.packageDelete(item.id).then((res) => {
|
|
|
+ this.$tip.toast(res.data.message || (res.data.code === 200 ? '已删除' : '操作失败'));
|
|
|
+ if (res.data.code === 200) this.reloadList();
|
|
|
+ });
|
|
|
},
|
|
|
ensureSelected() {
|
|
|
if (!this.selectedIds.length) {
|
|
|
@@ -326,30 +379,35 @@ export default {
|
|
|
async batchOff() {
|
|
|
if (!this.ensureSelected()) return;
|
|
|
if (!(await this.confirmModal('确定批量下架选中商品?'))) return;
|
|
|
- this.list.forEach((i) => {
|
|
|
- if (this.selectedIds.includes(i.id)) i.status = 'pending';
|
|
|
+ merchantDishApi.packageBatchLaunch(this.selectedIds, 0).then((res) => {
|
|
|
+ this.$tip.toast(res.data.message || (res.data.code === 200 ? '批量下架成功' : '操作失败'));
|
|
|
+ if (res.data.code === 200) {
|
|
|
+ this.exitBatch();
|
|
|
+ this.reloadList();
|
|
|
+ }
|
|
|
});
|
|
|
- this.saveList();
|
|
|
- this.exitBatch();
|
|
|
- uni.showToast({ title: '批量下架成功', icon: 'none' });
|
|
|
},
|
|
|
async batchOn() {
|
|
|
if (!this.ensureSelected()) return;
|
|
|
if (!(await this.confirmModal('确定批量上架选中商品?'))) return;
|
|
|
- this.list.forEach((i) => {
|
|
|
- if (this.selectedIds.includes(i.id)) i.status = i.stock > 0 ? 'selling' : 'soldout';
|
|
|
+ merchantDishApi.packageBatchLaunch(this.selectedIds, 1).then((res) => {
|
|
|
+ this.$tip.toast(res.data.message || (res.data.code === 200 ? '批量上架成功' : '操作失败'));
|
|
|
+ if (res.data.code === 200) {
|
|
|
+ this.exitBatch();
|
|
|
+ this.reloadList();
|
|
|
+ }
|
|
|
});
|
|
|
- this.saveList();
|
|
|
- this.exitBatch();
|
|
|
- uni.showToast({ title: '批量上架成功', icon: 'none' });
|
|
|
},
|
|
|
async batchDelete() {
|
|
|
if (!this.ensureSelected()) return;
|
|
|
if (!(await this.confirmModal('确定批量删除选中商品?'))) return;
|
|
|
- this.list = this.list.filter((i) => !this.selectedIds.includes(i.id));
|
|
|
- this.saveList();
|
|
|
- this.exitBatch();
|
|
|
- uni.showToast({ title: '批量删除成功', icon: 'none' });
|
|
|
+ merchantDishApi.packageDeleteBatch(this.selectedIds).then((res) => {
|
|
|
+ this.$tip.toast(res.data.message || (res.data.code === 200 ? '批量删除成功' : '操作失败'));
|
|
|
+ if (res.data.code === 200) {
|
|
|
+ this.exitBatch();
|
|
|
+ this.reloadList();
|
|
|
+ }
|
|
|
+ });
|
|
|
}
|
|
|
}
|
|
|
};
|
|
|
@@ -357,8 +415,11 @@ export default {
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
.page {
|
|
|
- min-height: 100vh;
|
|
|
+ height: 100vh;
|
|
|
background: #f7f8fc;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ overflow: hidden;
|
|
|
}
|
|
|
.nav-title {
|
|
|
font-size: 40rpx;
|
|
|
@@ -378,11 +439,13 @@ export default {
|
|
|
.search-bar {
|
|
|
padding: 12rpx 24rpx;
|
|
|
background: #fff;
|
|
|
+ flex-shrink: 0;
|
|
|
}
|
|
|
.filters {
|
|
|
display: flex;
|
|
|
background: #fff;
|
|
|
padding: 16rpx 24rpx 20rpx;
|
|
|
+ flex-shrink: 0;
|
|
|
.filter-item {
|
|
|
margin-right: 36rpx;
|
|
|
font-size: 26rpx;
|
|
|
@@ -399,9 +462,14 @@ export default {
|
|
|
font-size: 24rpx;
|
|
|
padding: 16rpx 20rpx;
|
|
|
border-radius: 8rpx;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+.list-wrap {
|
|
|
+ flex: 1;
|
|
|
+ min-height: 0;
|
|
|
+ box-sizing: border-box;
|
|
|
}
|
|
|
.list {
|
|
|
- height: calc(100vh - 280rpx);
|
|
|
padding: 16rpx 24rpx;
|
|
|
box-sizing: border-box;
|
|
|
}
|