| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755 |
- <template>
- <view class="page">
- <u-navbar title="售卖时间" :autoBack="true" :placeholder="true"></u-navbar>
- <view class="card">
- <view class="label required">菜品售卖日期</view>
- <u-radio-group v-model="mode" @change="onModeChange">
- <u-radio name="fixed" active-color="#449AFF">固定日期</u-radio>
- <u-radio name="custom" active-color="#449AFF">自定义</u-radio>
- <u-radio v-if="fromMulti" name="all" active-color="#449AFF">全部日期售卖</u-radio>
- </u-radio-group>
- </view>
- <!-- 固定日期:月历多选 -->
- <view class="card" v-if="mode === 'fixed'">
- <view class="cal-head">
- <view class="cal-month" @click="monthPickerShow = true">
- <text>{{ year }}年{{ month }}月</text>
- <u-icon name="arrow-down" color="#333" size="24"></u-icon>
- </view>
- <view class="cal-all" @click="toggleSelectAll">
- <text>全选</text>
- <view :class="['check', isMonthAllSelected ? 'on' : '']">
- <u-icon v-if="isMonthAllSelected" name="checkmark" color="#fff" size="18"></u-icon>
- </view>
- </view>
- </view>
- <view class="week">
- <text v-for="w in weeks" :key="w">{{ w }}</text>
- </view>
- <view class="days">
- <view
- v-for="(d, i) in days"
- :key="i"
- :class="['day', !d ? 'empty' : '', d && selectedDates.includes(d.full) ? 'on' : '']"
- @click="toggleDate(d)"
- >
- <text v-if="d" class="n">{{ d.day }}</text>
- </view>
- </view>
- </view>
- <!-- 自定义区间 -->
- <view class="card" v-if="mode === 'custom'">
- <view class="label">自定义时间</view>
- <view class="range-row">
- <view class="range-box" @click="openRangePicker('start')">
- {{ startDate || '开始日期' }}
- </view>
- <text class="range-sep">-</text>
- <view class="range-box" @click="openRangePicker('end')">
- {{ endDate || '结束日期' }}
- </view>
- </view>
- </view>
- <view class="card tip" v-if="mode === 'all'">
- 商品上架后,在售卖期内每日均可售卖;超出日期后自动对用户端隐藏。
- </view>
- <view class="footer">
- <view class="f-btn outline" @click="goPrev">上一步</view>
- <view
- :class="['f-btn', 'primary', !canSubmit || submitting ? 'disabled' : '']"
- @click="onSubmit"
- >
- 提交审核
- </view>
- </view>
- <!-- 年月切换 -->
- <view class="pick-mask" v-if="monthPickerShow" @click="monthPickerShow = false">
- <view class="pick-sheet" @click.stop>
- <view class="pick-title">选择月份</view>
- <picker-view class="pick-view" :value="monthPickerValue" @change="onMonthPickerChange">
- <picker-view-column>
- <view class="pick-item" v-for="y in yearList" :key="y">{{ y }}年</view>
- </picker-view-column>
- <picker-view-column>
- <view class="pick-item" v-for="m in 12" :key="m">{{ m }}月</view>
- </picker-view-column>
- </picker-view>
- <view class="pick-confirm" @click="confirmMonthPicker">确定</view>
- </view>
- </view>
- <!-- 自定义:选择时间(起止) -->
- <view class="pick-mask" v-if="rangePickerShow" @click="rangePickerShow = false">
- <view class="pick-sheet" @click.stop>
- <view class="pick-title">选择时间</view>
- <view class="range-preview">
- <view
- :class="['preview-box', rangeField === 'start' ? 'on' : '']"
- @click="switchRangeField('start')"
- >
- {{ formatCnDate(tempStart) || '开始日期' }}
- </view>
- <view
- :class="['preview-box', rangeField === 'end' ? 'on' : '']"
- @click="switchRangeField('end')"
- >
- {{ formatCnDate(tempEnd) || '结束日期' }}
- </view>
- </view>
- <picker-view class="pick-view" :value="datePickerValue" @change="onDatePickerChange">
- <picker-view-column>
- <view class="pick-item" v-for="y in yearList" :key="'dy' + y">{{ y }}年</view>
- </picker-view-column>
- <picker-view-column>
- <view class="pick-item" v-for="m in 12" :key="'dm' + m">{{ m }}月</view>
- </picker-view-column>
- <picker-view-column>
- <view class="pick-item" v-for="d in dayList" :key="'dd' + d">{{ d }}日</view>
- </picker-view-column>
- </picker-view>
- <view class="pick-confirm" @click="confirmRangePicker">确定</view>
- </view>
- </view>
- </view>
- </template>
- <script>
- import merchantDishApi, {
- belongingToApi,
- COMBO_CAT_TO_API,
- getMerchantId,
- PRODUCT_TYPE_TO_CATEGORY
- } from '@/api/merchantDish.js';
- import session from '../session.js';
- export default {
- data() {
- const now = new Date();
- const year = now.getFullYear();
- const yearList = [];
- for (let y = year - 2; y <= year + 10; y++) yearList.push(y);
- return {
- fromMulti: false,
- mode: 'fixed',
- year,
- month: now.getMonth() + 1,
- weeks: ['日', '一', '二', '三', '四', '五', '六'],
- selectedDates: [],
- startDate: '',
- endDate: '',
- submitting: false,
- yearList,
- monthPickerShow: false,
- monthPickerValue: [2, now.getMonth()],
- rangePickerShow: false,
- rangeField: 'start',
- tempStart: '',
- tempEnd: '',
- datePickerValue: [2, now.getMonth(), now.getDate() - 1],
- pickerDay: now.getDate()
- };
- },
- computed: {
- days() {
- const first = new Date(this.year, this.month - 1, 1);
- const startWeek = first.getDay();
- const total = new Date(this.year, this.month, 0).getDate();
- const arr = [];
- for (let i = 0; i < startWeek; i++) arr.push(null);
- for (let d = 1; d <= total; d++) {
- const m = String(this.month).padStart(2, '0');
- const day = String(d).padStart(2, '0');
- arr.push({ day: d, full: `${this.year}-${m}-${day}` });
- }
- return arr;
- },
- monthDateList() {
- return this.days.filter(Boolean).map((d) => d.full);
- },
- isMonthAllSelected() {
- const list = this.monthDateList;
- if (!list.length) return false;
- return list.every((d) => this.selectedDates.includes(d));
- },
- dayList() {
- const y =
- this.yearList[this.datePickerValue[0]] ||
- this.year;
- const m = (this.datePickerValue[1] || 0) + 1;
- const total = new Date(y, m, 0).getDate();
- const arr = [];
- for (let d = 1; d <= total; d++) arr.push(d);
- return arr;
- },
- canSubmit() {
- if (this.mode === 'all') return true;
- if (this.mode === 'fixed') return this.selectedDates.length > 0;
- return !!(this.startDate && this.endDate);
- }
- },
- onLoad(query) {
- this.fromMulti = query.from === 'multi';
- if (this.fromMulti) this.mode = 'fixed';
- this.syncMonthPickerValue();
- this.hydrateFromEditExtra();
- },
- methods: {
- normalizeDate(val) {
- if (!val) return '';
- const s = String(val).trim();
- const m = s.match(/^(\d{4}-\d{2}-\d{2})/);
- return m ? m[1] : s.slice(0, 10);
- },
- formatCnDate(val) {
- const d = this.normalizeDate(val);
- if (!d) return '';
- const [y, m, day] = d.split('-');
- return `${Number(y)}年${Number(m)}月${Number(day)}日`;
- },
- toYmd(y, m, d) {
- return `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
- },
- /** 编辑回显:优先根据 saleTimes 内容推断 */
- hydrateFromEditExtra() {
- const extra = session.getDishEditExtra() || {};
- const saleTimes = Array.isArray(extra.saleTimes) ? extra.saleTimes : [];
- const saleDateType = Number(extra.saleDateType);
- if (!saleTimes.length && !saleDateType) return;
- const range = saleTimes.find(
- (t) => Number(t.saleType) === 2 || t.startDate || t.endDate
- );
- if (range && (range.startDate || range.endDate)) {
- this.mode = 'custom';
- this.startDate = this.normalizeDate(range.startDate);
- this.endDate = this.normalizeDate(range.endDate);
- return;
- }
- const fixedDates = saleTimes
- .filter((t) => Number(t.saleType) === 1 || t.saleDate)
- .map((t) => this.normalizeDate(t.saleDate))
- .filter(Boolean)
- .sort();
- if (fixedDates.length || saleDateType === 1) {
- this.mode = 'fixed';
- this.selectedDates = fixedDates;
- if (fixedDates[0]) {
- const [y, m] = fixedDates[0].split('-').map(Number);
- if (y) this.year = y;
- if (m) this.month = m;
- this.syncMonthPickerValue();
- }
- return;
- }
- if ((saleDateType === 2 || !saleTimes.length) && this.fromMulti) {
- this.mode = 'all';
- }
- },
- syncMonthPickerValue() {
- const yi = this.yearList.indexOf(this.year);
- this.monthPickerValue = [yi >= 0 ? yi : 0, this.month - 1];
- },
- onModeChange() {
- this.selectedDates = [];
- this.startDate = '';
- this.endDate = '';
- },
- toggleDate(d) {
- if (!d) return;
- const i = this.selectedDates.indexOf(d.full);
- if (i >= 0) this.selectedDates.splice(i, 1);
- else this.selectedDates.push(d.full);
- },
- toggleSelectAll() {
- const list = this.monthDateList;
- if (this.isMonthAllSelected) {
- this.selectedDates = this.selectedDates.filter((d) => !list.includes(d));
- } else {
- const set = new Set(this.selectedDates.concat(list));
- this.selectedDates = Array.from(set).sort();
- }
- },
- onMonthPickerChange(e) {
- this.monthPickerValue = e.detail.value || [0, 0];
- },
- confirmMonthPicker() {
- const [yi, mi] = this.monthPickerValue;
- this.year = this.yearList[yi] || this.year;
- this.month = (mi || 0) + 1;
- this.monthPickerShow = false;
- },
- openRangePicker(field) {
- this.rangeField = field || 'start';
- this.tempStart = this.startDate;
- this.tempEnd = this.endDate;
- const base = this.normalizeDate(
- field === 'end' ? this.endDate || this.startDate : this.startDate || this.endDate
- );
- this.applyDateToPicker(base);
- // 首次打开且对应字段为空时,用当前滚轮日期预填
- const now = new Date();
- const fallback = this.toYmd(now.getFullYear(), now.getMonth() + 1, now.getDate());
- const seed = base || fallback;
- if (field === 'start' && !this.tempStart) this.tempStart = seed;
- if (field === 'end' && !this.tempEnd) this.tempEnd = seed;
- this.rangePickerShow = true;
- },
- switchRangeField(field) {
- this.rangeField = field;
- const base = this.normalizeDate(field === 'start' ? this.tempStart : this.tempEnd);
- this.applyDateToPicker(base);
- },
- applyDateToPicker(ymd) {
- const now = new Date();
- let y = now.getFullYear();
- let m = now.getMonth() + 1;
- let d = now.getDate();
- if (ymd) {
- const parts = ymd.split('-').map(Number);
- y = parts[0] || y;
- m = parts[1] || m;
- d = parts[2] || d;
- }
- let yi = this.yearList.indexOf(y);
- if (yi < 0) yi = 0;
- const total = new Date(y, m, 0).getDate();
- if (d > total) d = total;
- this.pickerDay = d;
- this.datePickerValue = [yi, m - 1, d - 1];
- this.$nextTick(() => {
- this.datePickerValue = [yi, m - 1, d - 1];
- });
- },
- onDatePickerChange(e) {
- const val = e.detail.value || [0, 0, 0];
- const y = this.yearList[val[0]] || this.year;
- const m = (val[1] || 0) + 1;
- const total = new Date(y, m, 0).getDate();
- let d = (val[2] || 0) + 1;
- if (d > total) d = total;
- this.pickerDay = d;
- this.datePickerValue = [val[0], val[1], d - 1];
- const ymd = this.toYmd(y, m, d);
- if (this.rangeField === 'start') this.tempStart = ymd;
- else this.tempEnd = ymd;
- },
- confirmRangePicker() {
- // 滚轮停住后若未触发 change,用当前索引兜底写入当前字段
- const [yi, mi, di] = this.datePickerValue;
- const y = this.yearList[yi] || this.year;
- const m = (mi || 0) + 1;
- const total = new Date(y, m, 0).getDate();
- let d = (di || 0) + 1;
- if (d > total) d = total;
- const ymd = this.toYmd(y, m, d);
- if (this.rangeField === 'start') {
- if (!this.tempStart) this.tempStart = ymd;
- } else if (!this.tempEnd) {
- this.tempEnd = ymd;
- }
- if (!this.tempStart || !this.tempEnd) {
- uni.showToast({ title: '请选择开始和结束日期', icon: 'none' });
- return;
- }
- if (this.tempStart > this.tempEnd) {
- uni.showToast({ title: '开始日期不能晚于结束日期', icon: 'none' });
- return;
- }
- this.startDate = this.tempStart;
- this.endDate = this.tempEnd;
- this.rangePickerShow = false;
- },
- goPrev() {
- uni.navigateBack();
- },
- buildSaleTimes() {
- if (this.mode === 'fixed') {
- return {
- saleDateType: 1,
- saleTimes: this.selectedDates
- .slice()
- .sort()
- .map((d, i) => ({
- saleType: 1,
- saleDate: d,
- sort: i
- }))
- };
- }
- if (this.mode === 'custom') {
- return {
- saleDateType: 2,
- saleTimes: [
- {
- saleType: 2,
- startDate: this.startDate,
- endDate: this.endDate,
- sort: 0
- }
- ]
- };
- }
- return {
- saleDateType: 2,
- saleTimes: []
- };
- },
- buildComboDetails(comboItems) {
- const details = [];
- (comboItems || []).forEach((cat) => {
- const category = COMBO_CAT_TO_API[cat.id] || '5';
- (cat.items || []).forEach((item) => {
- if (!item.name) return;
- details.push({
- category,
- name: item.name,
- price: item.price !== '' && item.price != null ? Number(item.price) : 0,
- description: item.description || ''
- });
- });
- });
- return details;
- },
- buildSpecsAndSkus(draft) {
- const specs = [];
- (draft.specs || []).forEach((spec, si) => {
- (spec.values || []).forEach((v, oi) => {
- const row = {
- specName: spec.name,
- optionName: v.name,
- specSort: si,
- optionSort: oi,
- isSelected: v.selected ? 1 : 0
- };
- if (v.persistId) row.id = v.persistId;
- specs.push(row);
- });
- });
- const skus = (draft.combos || []).map((c, i) => ({
- specOptionNames: (c.label || '').replace(/\s*\|\s*/g, '/'),
- price: Number(c.price || 0),
- sellingPrice: Number(c.sellingPrice != null ? c.sellingPrice : c.originPrice || 0),
- stock: Number(c.stock || 0),
- costPrice: c.costPrice !== '' && c.costPrice != null ? Number(c.costPrice) : undefined,
- status: 1,
- sort: i
- }));
- return { specs, skus };
- },
- buildSubmitPayload(draft) {
- const isGroup = draft.belong === 'group';
- const isMulti = draft.specMode === 'multi';
- const sale = this.buildSaleTimes();
- const payload = {
- merchantId: getMerchantId(),
- category: draft.mealCategory || PRODUCT_TYPE_TO_CATEGORY[draft.productType] || '1',
- name: draft.name,
- productImage: draft.coverImages || draft.cover || '',
- detailImages: draft.detailImages || draft.detail || '',
- specialCategoryId: draft.category,
- productBelonging: belongingToApi(draft.belong),
- isGroupMeal: isGroup ? 1 : 0,
- salesSpecType: isMulti ? 2 : 1,
- isPackage: Number(draft.isCombo) === 1 ? 1 : 0,
- saleDateType: sale.saleDateType,
- saleTimes: sale.saleTimes
- };
- if (draft.isEdit && draft.editId) {
- payload.id = draft.editId;
- }
- if (!isMulti) {
- payload.price = Number(draft.price || 0);
- payload.sellingPrice = Number(draft.sellingPrice || 0);
- payload.dailyStock = Number(draft.stock || 0);
- if (draft.costPrice !== '' && draft.costPrice != null) {
- payload.costPrice = Number(draft.costPrice);
- }
- } else {
- const { specs, skus } = this.buildSpecsAndSkus(draft);
- payload.specs = specs;
- payload.skus = skus;
- const first = (draft.combos || [])[0] || {};
- payload.price = Number(first.price || 0);
- payload.sellingPrice = Number(
- first.sellingPrice != null ? first.sellingPrice : first.originPrice || 0
- );
- payload.dailyStock = Number(first.stock || 0);
- }
- if (Number(draft.isCombo) === 1) {
- payload.gmMerchantPackageDetails = this.buildComboDetails(draft.comboItems);
- }
- return payload;
- },
- onSubmit() {
- if (!this.canSubmit || this.submitting) {
- if (!this.canSubmit) uni.showToast({ title: '请完善售卖时间', icon: 'none' });
- return;
- }
- const draft = session.getDishDraft() || {};
- if (!draft.name) {
- uni.showToast({ title: '表单已失效,请重新填写', icon: 'none' });
- return;
- }
- const payload = this.buildSubmitPayload(draft);
- this.submitting = true;
- uni.showLoading({ title: '提交中' });
- merchantDishApi
- .packageSubmit(payload)
- .then((res) => {
- const ok = res.data.code === 200;
- this.$tip[ok ? 'success' : 'error'](res.data.message || (ok ? '送审成功!' : '提交失败'));
- if (ok) {
- session.clearDishDraft();
- setTimeout(() => {
- uni.redirectTo({ url: '/pages/merchantDish/dish/list' });
- }, 500);
- }
- })
- .finally(() => {
- this.submitting = false;
- uni.hideLoading();
- });
- }
- }
- };
- </script>
- <style lang="scss" scoped>
- .page {
- min-height: 100vh;
- background: #f7f8fc;
- padding-bottom: 160rpx;
- }
- .card {
- background: #fff;
- margin: 20rpx 24rpx;
- border-radius: 16rpx;
- padding: 24rpx;
- &.tip {
- font-size: 24rpx;
- color: #666;
- line-height: 1.6;
- }
- .label {
- font-size: 28rpx;
- color: #333;
- margin-bottom: 20rpx;
- display: block;
- &.required::before {
- content: '*';
- color: #ff4d4f;
- margin-right: 4rpx;
- }
- }
- }
- .cal-head {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 24rpx;
- }
- .cal-month {
- display: flex;
- align-items: center;
- gap: 8rpx;
- font-size: 30rpx;
- font-weight: 600;
- color: #222;
- }
- .cal-all {
- display: flex;
- align-items: center;
- gap: 12rpx;
- font-size: 26rpx;
- color: #666;
- .check {
- width: 32rpx;
- height: 32rpx;
- border-radius: 6rpx;
- border: 2rpx solid #c7c6ca;
- box-sizing: border-box;
- display: flex;
- align-items: center;
- justify-content: center;
- &.on {
- background: #449aff;
- border-color: #449aff;
- }
- }
- }
- .week {
- display: flex;
- margin-bottom: 8rpx;
- text {
- width: 14.28%;
- text-align: center;
- font-size: 24rpx;
- color: #999;
- }
- }
- .days {
- display: flex;
- flex-wrap: wrap;
- .day {
- width: 14.28%;
- height: 88rpx;
- display: flex;
- align-items: center;
- justify-content: center;
- .n {
- width: 72rpx;
- height: 72rpx;
- line-height: 70rpx;
- text-align: center;
- border-radius: 12rpx;
- font-size: 28rpx;
- color: #333;
- border: 2rpx solid transparent;
- box-sizing: border-box;
- }
- &.on .n {
- background: rgba(68, 154, 255, 0.12);
- border-color: #449aff;
- color: #449aff;
- }
- &.empty {
- pointer-events: none;
- }
- }
- }
- .range-row {
- display: flex;
- align-items: center;
- gap: 16rpx;
- }
- .range-box {
- flex: 1;
- height: 80rpx;
- line-height: 80rpx;
- text-align: center;
- font-size: 28rpx;
- color: #333;
- background: #f7f8fc;
- border-radius: 12rpx;
- border: 1rpx solid #e8e8e8;
- }
- .range-sep {
- color: #999;
- font-size: 28rpx;
- }
- .footer {
- position: fixed;
- left: 0;
- right: 0;
- bottom: 0;
- display: flex;
- gap: 24rpx;
- padding: 20rpx 40rpx calc(20rpx + env(safe-area-inset-bottom));
- background: #fff;
- }
- .f-btn {
- height: 88rpx;
- line-height: 88rpx;
- text-align: center;
- font-size: 30rpx;
- border-radius: 44rpx;
- &.outline {
- width: 240rpx;
- flex-shrink: 0;
- color: #449aff;
- border: 2rpx solid #449aff;
- box-sizing: border-box;
- line-height: 84rpx;
- background: #fff;
- }
- &.primary {
- flex: 1;
- color: #fff;
- background: linear-gradient( 89deg, #10ABFE 0%, #2260F6 100%);
- }
- &.disabled {
- opacity: 0.5;
- }
- }
- .pick-mask {
- position: fixed;
- left: 0;
- right: 0;
- top: 0;
- bottom: 0;
- background: rgba(0, 0, 0, 0.45);
- z-index: 1000;
- display: flex;
- align-items: flex-end;
- }
- .pick-sheet {
- width: 100%;
- background: #fff;
- border-radius: 24rpx 24rpx 0 0;
- padding: 0 32rpx calc(24rpx + env(safe-area-inset-bottom));
- box-sizing: border-box;
- }
- .pick-title {
- text-align: center;
- font-size: 32rpx;
- font-weight: 600;
- color: #222;
- padding: 36rpx 0 24rpx;
- }
- .range-preview {
- display: flex;
- gap: 20rpx;
- margin-bottom: 12rpx;
- }
- .preview-box {
- flex: 1;
- height: 72rpx;
- line-height: 70rpx;
- text-align: center;
- font-size: 26rpx;
- color: #666;
- background: #fff;
- border: 2rpx solid #e5e5e5;
- border-radius: 12rpx;
- box-sizing: border-box;
- &.on {
- color: #449aff;
- border-color: #449aff;
- background: rgba(68, 154, 255, 0.06);
- }
- }
- .pick-view {
- height: 400rpx;
- width: 100%;
- }
- .pick-item {
- height: 80rpx;
- line-height: 80rpx;
- text-align: center;
- font-size: 30rpx;
- color: #333;
- }
- .pick-confirm {
- margin-top: 12rpx;
- height: 88rpx;
- line-height: 88rpx;
- text-align: center;
- font-size: 30rpx;
- color: #fff;
- border-radius: 44rpx;
- background: linear-gradient( 89deg, #10ABFE 0%, #2260F6 100%);
- }
- ::v-deep .u-radio {
- margin-right: 24rpx;
- margin-bottom: 12rpx;
- }
- </style>
|