| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221 |
- <template>
- <view class="page">
- <u-navbar :title="isEdit ? '编辑菜品' : '新建菜品'" :autoBack="true" :placeholder="true"></u-navbar>
- <view class="card">
- <view class="section-title">基础信息</view>
- <view class="row">
- <text class="label required">菜品归属</text>
- <u-radio-group v-model="form.belong" :disabled="isEdit" @change="onBelongChange">
- <u-radio name="store" active-color="#449AFF">门店</u-radio>
- <u-radio v-if="showGroupBelong" name="group" active-color="#449AFF">团餐</u-radio>
- </u-radio-group>
- </view>
- <view class="row" @click="pickCategory">
- <text class="label required">菜品分类</text>
- <text class="value">{{ categoryName || '请选择' }}</text>
- <image class="arrow-down" src="/static/merchantDish/selete.png" mode="aspectFit"></image>
- </view>
- <view class="row">
- <text class="label required">菜品名称</text>
- <input class="input" v-model="form.name" placeholder="请输入" placeholder-class="ph" />
- </view>
- <view class="row" @click="pickProductType">
- <text class="label required">菜品类型</text>
- <text class="value">{{ productTypeName || '请选择' }}</text>
- <image class="arrow-down" src="/static/merchantDish/selete.png" mode="aspectFit"></image>
- </view>
- <view class="upload-row">
- <text class="label required">菜品封面图</text>
- <view class="tip">支持jpg、png格式,尺寸为750px * 750px</view>
- <u-upload
- :action="uploadAction"
- :file-list="fileList.cover"
- :form-data="uploadFormData"
- :max-count="4"
- :multiple="true"
- :deletable="true"
- :custom-btn="true"
- del-bg-color="#fa3534"
- width="140"
- height="140"
- preview-full-image
- @on-success="onCoverSuccess"
- @on-remove="onCoverRemove"
- >
- <view slot="addBtn" class="upload-btn">
- <image class="upload-icon" src="/static/merchantDish/photo.png" mode="aspectFit"></image>
- <text>上传</text>
- </view>
- </u-upload>
- </view>
- <view class="upload-row last">
- <text class="label required">菜品图片详情</text>
- <view class="tip">支持jpg、png格式,尺寸为750px * 750px</view>
- <u-upload
- :action="uploadAction"
- :file-list="fileList.detail"
- :form-data="uploadFormData"
- :max-count="20"
- :multiple="true"
- :deletable="true"
- :custom-btn="true"
- del-bg-color="#fa3534"
- width="140"
- height="140"
- preview-full-image
- @on-success="onDetailSuccess"
- @on-remove="onDetailRemove"
- >
- <view slot="addBtn" class="upload-btn">
- <image class="upload-icon" src="/static/merchantDish/photo.png" mode="aspectFit"></image>
- <text>上传</text>
- </view>
- </u-upload>
- </view>
- </view>
- <view class="card">
- <view class="section-title">其他信息</view>
- <view class="row">
- <text class="label required">是否为套餐</text>
- <u-radio-group v-model="form.isCombo" @change="onComboChange">
- <u-radio :name="1" active-color="#449AFF">是</u-radio>
- <u-radio :name="0" active-color="#449AFF">否</u-radio>
- </u-radio-group>
- </view>
- <view class="row" v-if="Number(form.isCombo) === 1" @click="goCombo">
- <text class="label">套餐</text>
- <text class="value link">{{ comboCount ? `共${comboCount}项` : '去添加' }}</text>
- <u-icon name="arrow-right" color="#C7C6CA"></u-icon>
- </view>
- <view class="row">
- <text class="label required">销售规格</text>
- <SpecModeSwitch
- ref="specSwitch"
- :locked="specLocked"
- :value="specMode"
- @change="onSpecModeChange"
- @locked="onSpecLockedTip"
- />
- </view>
- <view class="spec-tip" v-if="specLocked && specMode !== 'multi'">当前归属为团餐或已设为套餐,仅支持单规格</view>
- <!-- panelMode 延后切换,避免与下一步按钮抢同一帧渲染 -->
- <view class="spec-panels" :class="panelMode">
- <view class="panel panel-single">
- <view class="row">
- <text class="label required">售价 (元)</text>
- <input class="input" type="digit" :value="form.price" @input="onPriceInput('price', $event)" placeholder="请输入" placeholder-class="ph" />
- </view>
- <view class="row">
- <text class="label required">原价 (元)</text>
- <input class="input" type="digit" :value="form.sellingPrice" @input="onPriceInput('sellingPrice', $event)" placeholder="请输入" placeholder-class="ph" />
- </view>
- <view class="row">
- <text class="label required">可售库存</text>
- <input class="input" type="number" :value="form.stock" @input="onPriceInput('stock', $event)" placeholder="请输入" placeholder-class="ph" />
- </view>
- <view class="row">
- <text class="label">成本价 (元)</text>
- <input class="input" type="digit" :value="form.costPrice" @input="onPriceInput('costPrice', $event)" placeholder="请输入 (选填)" placeholder-class="ph" />
- </view>
- </view>
- <view class="panel panel-multi">
- <view class="spec-block" v-for="(spec, si) in form.specs" :key="spec.id">
- <view class="spec-head">
- <text>{{ spec.name }}</text>
- <text class="more" @click="openSpecMenu(si)">···</text>
- </view>
- <view class="tags">
- <view
- v-for="(v, vi) in spec.values"
- :key="v.id"
- :class="['tag', v.selected ? 'on' : '']"
- @click="toggleSpecValue(v)"
- >
- {{ v.name }}
- <image v-if="v.selected" class="tag-check" src="/static/merchantDish/selete-true.png" mode="aspectFit"></image>
- </view>
- </view>
- </view>
- <view class="add-spec" @click="addSpec">+ 添加规格</view>
- </view>
- </view>
- </view>
- <FormNextBtn ref="nextBtn" @click="onNext" />
- <!-- 规格操作菜单 -->
- <u-action-sheet :list="specMenuList" v-model="specMenuShow" @click="onSpecMenu"></u-action-sheet>
- <!-- 编辑规格名 -->
- <view class="mask" v-if="nameModal" @click="nameModal = false">
- <view class="spec-dialog" @click.stop>
- <view class="spec-dialog-title">编辑规格名</view>
- <input
- class="spec-dialog-input"
- v-model="editingName"
- placeholder="请输入规格名称"
- placeholder-class="spec-ph"
- />
- <view class="spec-dialog-footer">
- <view class="spec-dialog-btn cancel" @click="nameModal = false">取消</view>
- <view class="spec-dialog-btn save" @click="saveSpecName">保存</view>
- </view>
- </view>
- </view>
- <!-- 编辑规格值 -->
- <view class="mask" v-if="valueListShow" @click="valueListShow = false">
- <view class="spec-dialog spec-value-dialog" @click.stop>
- <view class="spec-dialog-title">编辑规格值</view>
- <view class="sv-list">
- <view class="sv-row" v-for="(v, i) in editingValues" :key="v.id">
- <input
- class="spec-dialog-input sv-input"
- v-model="v.name"
- placeholder="请输入规格值"
- placeholder-class="spec-ph"
- />
- <view class="sv-circle minus" @click="removeSpecValue(i)">−</view>
- </view>
- <view class="sv-row">
- <input
- class="spec-dialog-input sv-input"
- v-model="valueDraft"
- placeholder="请输入规格值"
- placeholder-class="spec-ph"
- />
- <view class="sv-circle plus" @click="addSpecValue">+</view>
- </view>
- </view>
- <view class="spec-dialog-footer">
- <view class="spec-dialog-btn cancel" @click="valueListShow = false">取消</view>
- <view class="spec-dialog-btn save" @click="confirmValues">保存</view>
- </view>
- </view>
- </view>
- <!-- 菜品分类 / 菜品类型:底部弹层 -->
- <view class="pick-mask" v-if="pickShow" @click="closePickPopup">
- <view class="pick-sheet" @click.stop>
- <view class="pick-sheet-title">{{ pickTitle }}</view>
- <view class="pick-options">
- <view
- v-for="item in pickOptions"
- :key="item.value"
- :class="['pick-opt', String(pickTemp) === String(item.value) ? 'on' : '']"
- @click="pickTemp = item.value"
- >
- {{ item.label }}
- </view>
- </view>
- <view class="pick-sheet-footer">
- <view class="pick-btn cancel" @click="closePickPopup">取消</view>
- <view class="pick-btn confirm" @click="confirmPick">确定</view>
- </view>
- </view>
- </view>
- </view>
- </template>
- <script>
- import { buildSpecCombos } from '../utils.js';
- import session from '../session.js';
- import configService from '@/common/service/config.service';
- import merchantDishApi, {
- belongingToKey,
- CATEGORY_TO_PRODUCT_TYPE,
- COMBO_API_TO_CAT,
- PRODUCT_TYPE_TO_CATEGORY
- } from '@/api/merchantDish.js';
- import SpecModeSwitch from './components/SpecModeSwitch.vue';
- import FormNextBtn from './components/FormNextBtn.vue';
- const PRODUCT_TYPES = [
- { label: '早餐', value: 'breakfast' },
- { label: '午餐', value: 'lunch' },
- { label: '晚餐', value: 'dinner' }
- ];
- const DEFAULT_SPECS = [
- {
- id: 's1',
- name: '份量',
- values: [
- { id: 'v1', name: '小份', selected: true },
- { id: 'v2', name: '中份', selected: true },
- { id: 'v3', name: '大份', selected: false }
- ]
- },
- {
- id: 's2',
- name: '口味',
- values: [
- { id: 'v4', name: '微辣', selected: false },
- { id: 'v5', name: '中辣', selected: true },
- { id: 'v6', name: '特辣', selected: false }
- ]
- },
- {
- id: 's3',
- name: '温度',
- values: [
- { id: 'v7', name: '温', selected: false },
- { id: 'v8', name: '冰', selected: false },
- { id: 'v9', name: '热', selected: false }
- ]
- }
- ];
- const EMPTY_COMBO = [
- { id: 'meat', name: '荤菜', items: [] },
- { id: 'veg', name: '素菜', items: [] },
- { id: 'staple', name: '主食', items: [] },
- { id: 'soup', name: '汤', items: [] },
- { id: 'other', name: '其他', items: [] }
- ];
- export default {
- components: {
- SpecModeSwitch,
- FormNextBtn
- },
- data() {
- return {
- isEdit: false,
- editId: null,
- groupMealOpened: false,
- uploadAction: configService.apiUrl + '/sys/common/upload',
- imageHttp: configService.apiUrl + '/',
- uploadFormData: { biz: 'temp' },
- fileList: {
- cover: [],
- detail: []
- },
- specialCategories: [],
- form: {
- belong: 'store',
- category: '',
- name: '',
- productType: 'breakfast',
- cover: '',
- detail: '',
- isCombo: 0,
- comboItems: [],
- specMode: 'single',
- price: '',
- sellingPrice: '',
- stock: '',
- costPrice: '',
- specs: JSON.parse(JSON.stringify(DEFAULT_SPECS))
- },
- // 顶层字段:面板延后切换;单选/下一步在子组件内更新,避免整页重渲染卡顿
- specMode: 'single',
- panelMode: 'single',
- _panelTimer: null,
- pickShow: false,
- pickKind: '', // category | type
- pickTemp: '',
- productTypes: PRODUCT_TYPES,
- specMenuShow: false,
- specMenuList: [{ text: '编辑规格名' }, { text: '编辑规格值' }, { text: '删除规格', color: '#FF4D4F' }],
- activeSpecIndex: 0,
- nameModal: false,
- editingName: '',
- valueListShow: false,
- editingValues: [],
- valueDraft: ''
- };
- },
- computed: {
- showGroupBelong() {
- return this.groupMealOpened;
- },
- categoryName() {
- const hit = this.specialCategories.find((i) => String(i.value) === String(this.form.category));
- return hit ? hit.label : '';
- },
- productTypeName() {
- const hit = PRODUCT_TYPES.find((i) => i.value === this.form.productType);
- return hit ? hit.label : '';
- },
- specLocked() {
- return this.form.belong === 'group' || Number(this.form.isCombo) === 1;
- },
- comboCount() {
- return (this.form.comboItems || []).reduce((n, c) => n + (c.items || []).filter((i) => i.name).length, 0);
- },
- pickTitle() {
- return this.pickKind === 'category' ? '菜品分类' : '菜品类型';
- },
- pickOptions() {
- return this.pickKind === 'category' ? this.specialCategories : this.productTypes;
- }
- },
- watch: {
- 'form.name'() { this.refreshNextEnabled(); },
- 'form.category'() { this.refreshNextEnabled(); },
- 'form.cover'() { this.refreshNextEnabled(); },
- 'form.detail'() { this.refreshNextEnabled(); },
- 'form.isCombo'() { this.refreshNextEnabled(); },
- 'form.comboItems': {
- deep: true,
- handler() { this.refreshNextEnabled(); }
- }
- },
- onLoad(query) {
- session.clearDishDraft();
- this.initGroupMealFlag();
- this.loadSpecialCategories().then(() => {
- if (query.id) {
- this.isEdit = true;
- this.editId = query.id;
- this.loadEdit();
- } else {
- this.form.belong = 'store';
- this.applySpecLock();
- }
- this.$nextTick(() => this.refreshNextEnabled());
- });
- },
- onShow() {
- this.initGroupMealFlag();
- this.refreshNextEnabled();
- },
- methods: {
- /** 返回未填项提示文案,通过则返回空字符串 */
- getNextTip(mode) {
- const f = this.form;
- const specMode = mode || this.specMode;
- if (!f.belong) return '请选择商品归属';
- if (!f.category) return '请选择商品分类';
- if (!f.name) return '请输入商品名称';
- if (!f.productType) return '请选择商品类型';
- if (!f.cover) return '请上传商品封面图';
- if (!f.detail) return '请上传商品详情图';
- if (Number(f.isCombo) === 1 && !this.comboCount) return '请添加套餐菜品';
- if (this.specLocked && specMode !== 'single') return '当前仅支持单规格';
- if (specMode === 'single') {
- if (f.price === '') return '请填写售价';
- if (f.sellingPrice === '') return '请填写原价';
- if (f.stock === '') return '请填写可售库存';
- return '';
- }
- const hasSelected = (f.specs || []).some((s) => (s.values || []).some((v) => v.selected));
- if (!hasSelected) return '请至少选择一个规格值';
- return '';
- },
- refreshNextEnabled(mode) {
- const enabled = !this.getNextTip(mode);
- const apply = () => {
- if (this.$refs.nextBtn && this.$refs.nextBtn.setEnabled) {
- this.$refs.nextBtn.setEnabled(enabled);
- }
- };
- if (this.$refs.nextBtn) apply();
- else this.$nextTick(apply);
- return enabled;
- },
- syncSpecMode(mode, immediatePanel) {
- const nextMode = mode || 'single';
- const touchChildren = () => {
- if (this.$refs.specSwitch && this.$refs.specSwitch.setMode) {
- this.$refs.specSwitch.setMode(nextMode);
- }
- this.refreshNextEnabled(nextMode);
- };
- const applyParent = () => {
- this.specMode = nextMode;
- this.form.specMode = nextMode;
- this.panelMode = nextMode;
- };
- if (immediatePanel) {
- applyParent();
- this.$nextTick(touchChildren);
- return;
- }
- touchChildren();
- if (this._panelTimer) clearTimeout(this._panelTimer);
- this._panelTimer = setTimeout(() => {
- applyParent();
- this._panelTimer = null;
- }, 0);
- },
- onSpecLockedTip() {
- uni.showToast({ title: '当前仅支持单规格', icon: 'none' });
- },
- onPriceInput(key, e) {
- const val = e && e.detail ? e.detail.value : e;
- this.form[key] = val == null ? '' : String(val);
- this.refreshNextEnabled();
- },
- toggleSpecValue(v) {
- v.selected = !v.selected;
- this.refreshNextEnabled();
- },
- /** 供套餐页回写 */
- applyComboItems(categories) {
- this.form.comboItems = categories || [];
- this.refreshNextEnabled();
- },
- initGroupMealFlag() {
- merchantDishApi.isApply().then((res) => {
- if (res.data.code !== 200) return;
- const result = res.data.result;
- let localStatus = 0;
- if (result) {
- const apiStatus = result.status;
- if (apiStatus == 0) localStatus = 3;
- else if (apiStatus == 1) localStatus = 1;
- else if (apiStatus == 2) localStatus = 2;
- }
- this.groupMealOpened = localStatus === 1;
- if (!this.groupMealOpened && this.form.belong === 'group' && !this.isEdit) {
- this.form.belong = 'store';
- }
- });
- },
- loadSpecialCategories() {
- return merchantDishApi.specialCategoryList().then((res) => {
- if (res.data.code !== 200) return;
- const rows = res.data.result || [];
- this.specialCategories = rows.map((i) => ({
- label: i.categoryName,
- value: String(i.id)
- }));
- if (!this.form.category && this.specialCategories.length) {
- this.form.category = this.specialCategories[0].value;
- }
- });
- },
- mapDetailsToCombo(details) {
- const cats = JSON.parse(JSON.stringify(EMPTY_COMBO));
- (details || []).forEach((d, idx) => {
- const catId = COMBO_API_TO_CAT[d.category] || 'other';
- const cat = cats.find((c) => c.id === catId);
- if (!cat) return;
- cat.items.push({
- id: d.id || `d_${idx}`,
- name: d.name || '',
- price: d.price != null ? String(d.price) : '',
- description: d.description || ''
- });
- });
- return cats;
- },
- mapSpecsFromApi(specs) {
- if (!specs || !specs.length) return JSON.parse(JSON.stringify(DEFAULT_SPECS));
- const groupMap = {};
- specs.forEach((s, idx) => {
- const key = s.specName || '规格';
- if (!groupMap[key]) {
- groupMap[key] = {
- id: `g_${key}`,
- name: key,
- values: []
- };
- }
- // isSelected: 1 选中,0 未选中;兼容旧数据无该字段时视为选中
- const selected = s.isSelected == null ? true : Number(s.isSelected) === 1;
- groupMap[key].values.push({
- id: s.id || `${key}_${s.optionName || idx}`,
- persistId: s.id || null,
- name: s.optionName,
- selected
- });
- });
- return Object.values(groupMap);
- },
- loadEdit() {
- uni.showLoading({ title: '加载中' });
- merchantDishApi
- .packageDetail(this.editId)
- .then((res) => {
- if (res.data.code !== 200 || !res.data.result) {
- this.$tip.toast(res.data.message || '加载失败');
- return;
- }
- const item = res.data.result;
- this.form.belong = belongingToKey(item.productBelonging);
- this.form.category = item.specialCategoryId != null ? String(item.specialCategoryId) : '';
- this.form.name = item.name || '';
- this.form.productType = CATEGORY_TO_PRODUCT_TYPE[item.category] || 'breakfast';
- this.form.cover = item.productImage || '';
- this.form.detail = item.detailImages || '';
- this.form.isCombo = Number(item.isPackage) == 1 ? 1 : 0;
- // 售价 price,原价 sellingPrice
- this.form.price = item.price != null ? String(item.price) : '';
- this.form.sellingPrice =
- item.sellingPrice != null
- ? String(item.sellingPrice)
- : item.originalPrice != null
- ? String(item.originalPrice)
- : '';
- this.form.stock = item.dailyStock != null ? String(item.dailyStock) : '';
- this.form.costPrice = item.costPrice != null ? String(item.costPrice) : '';
- this.form.comboItems = this.mapDetailsToCombo(item.gmMerchantPackageDetails);
- // 以接口 salesSpecType 为准:1 单规格,2 多规格(编辑时不强制改回单规格)
- const salesSpecType = Number(item.salesSpecType);
- const specMode = salesSpecType === 2 ? 'multi' : 'single';
- if (specMode === 'multi') {
- this.form.specs = this.mapSpecsFromApi(item.specs);
- }
- this.syncSpecMode(specMode, true);
- this.fileList = {
- cover: this.toFileList(this.form.cover),
- detail: this.toFileList(this.form.detail)
- };
- // 编辑详情中的 skus 放内存,多规格下一步回填价格
- session.setDishEditExtra({
- skus: item.skus || [],
- saleTimes: item.saleTimes || [],
- saleDateType: item.saleDateType
- });
- this.$nextTick(() => this.refreshNextEnabled(specMode));
- })
- .finally(() => uni.hideLoading());
- },
- toFileList(value) {
- if (!value) return [];
- return String(value)
- .split(',')
- .filter(Boolean)
- .map((url) => ({ url }));
- },
- processImageList(lists, formKey) {
- const urls = lists
- .map((item) => {
- if (item.response && item.response.message) {
- return this.imageHttp + item.response.message;
- }
- if (item.url) return item.url;
- return '';
- })
- .filter(Boolean);
- this.form[formKey] = urls.join(',');
- },
- onCoverSuccess(res, index, lists) {
- this.fileList.cover = lists;
- this.processImageList(lists, 'cover');
- },
- onCoverRemove(index, lists) {
- this.fileList.cover = lists;
- this.processImageList(lists, 'cover');
- },
- onDetailSuccess(res, index, lists) {
- this.fileList.detail = lists;
- this.processImageList(lists, 'detail');
- },
- onDetailRemove(index, lists) {
- this.fileList.detail = lists;
- this.processImageList(lists, 'detail');
- },
- onBelongChange(val) {
- const belong = val || this.form.belong;
- this.form.belong = belong;
- this.applySpecLock();
- },
- onComboChange(val) {
- this.form.isCombo = val === undefined ? this.form.isCombo : val;
- this.applySpecLock();
- },
- onSpecModeChange(val) {
- const mode = val || 'single';
- if (mode === this.specMode && mode === this.panelMode) return;
- if (this.specLocked && mode === 'multi') {
- this.onSpecLockedTip();
- return;
- }
- // 先瞬时更新按钮置灰状态(子组件),再延后切换面板
- this.refreshNextEnabled(mode);
- if (this._panelTimer) clearTimeout(this._panelTimer);
- this._panelTimer = setTimeout(() => {
- this.specMode = mode;
- this.form.specMode = mode;
- this.panelMode = mode;
- this._panelTimer = null;
- }, 0);
- },
- applySpecLock() {
- if (this.specLocked) {
- this.syncSpecMode('single', true);
- }
- },
- pickCategory() {
- if (!this.specialCategories.length) {
- uni.showToast({ title: '暂无分类', icon: 'none' });
- return;
- }
- this.pickKind = 'category';
- this.pickTemp = this.form.category || '';
- this.pickShow = true;
- },
- pickProductType() {
- this.pickKind = 'type';
- this.pickTemp = this.form.productType || 'breakfast';
- this.pickShow = true;
- },
- closePickPopup() {
- this.pickShow = false;
- },
- confirmPick() {
- if (this.pickTemp === '' || this.pickTemp == null) {
- uni.showToast({
- title: this.pickKind === 'category' ? '请选择菜品分类' : '请选择菜品类型',
- icon: 'none'
- });
- return;
- }
- if (this.pickKind === 'category') {
- this.form.category = String(this.pickTemp);
- } else {
- this.form.productType = this.pickTemp;
- }
- this.pickShow = false;
- },
- goCombo() {
- uni.navigateTo({ url: '/pages/merchantDish/dish/combo' });
- },
- openSpecMenu(si) {
- this.activeSpecIndex = si;
- this.specMenuShow = true;
- },
- onSpecMenu(e) {
- const index = typeof e === 'object' ? e.index : e;
- const spec = this.form.specs[this.activeSpecIndex];
- if (!spec) return;
- if (index === 0) {
- this.editingName = spec.name || '';
- this.$nextTick(() => {
- this.nameModal = true;
- });
- } else if (index === 1) {
- this.editingValues = JSON.parse(JSON.stringify(spec.values || []));
- this.valueDraft = '';
- this.$nextTick(() => {
- this.valueListShow = true;
- });
- } else if (index === 2) {
- this.deleteSpec();
- }
- },
- deleteSpec() {
- if ((this.form.specs || []).length <= 1) {
- uni.showToast({ title: '至少保留一个规格', icon: 'none' });
- return;
- }
- uni.showModal({
- content: '确定删除该规格?',
- confirmColor: '#FF4D4F',
- success: (res) => {
- if (!res.confirm) return;
- this.form.specs.splice(this.activeSpecIndex, 1);
- this.refreshNextEnabled();
- }
- });
- },
- saveSpecName() {
- if (!this.editingName.trim()) {
- uni.showToast({ title: '请输入规格名称', icon: 'none' });
- return;
- }
- this.form.specs[this.activeSpecIndex].name = this.editingName.trim();
- this.nameModal = false;
- },
- addSpecValue() {
- const name = (this.valueDraft || '').trim();
- if (!name) {
- uni.showToast({ title: '请输入规格值', icon: 'none' });
- return;
- }
- this.editingValues.push({
- id: `v${Date.now()}`,
- name,
- selected: true
- });
- this.valueDraft = '';
- },
- removeSpecValue(i) {
- if (this.editingValues.length <= 1 && !(this.valueDraft || '').trim()) {
- uni.showToast({ title: '至少保留一个规格值', icon: 'none' });
- return;
- }
- this.editingValues.splice(i, 1);
- },
- confirmValues() {
- const draft = (this.valueDraft || '').trim();
- const list = this.editingValues
- .map((v) => ({ ...v, name: (v.name || '').trim() }))
- .filter((v) => v.name);
- if (draft) {
- list.push({
- id: `v${Date.now()}`,
- name: draft,
- selected: true
- });
- }
- if (!list.length) {
- uni.showToast({ title: '请至少填写一个规格值', icon: 'none' });
- return;
- }
- this.form.specs[this.activeSpecIndex].values = list;
- this.valueDraft = '';
- this.valueListShow = false;
- this.refreshNextEnabled();
- },
- addSpec() {
- const id = `s${Date.now()}`;
- this.form.specs.push({
- id,
- name: '新规格',
- values: [
- { id: `${id}_1`, name: '选项1', selected: true },
- { id: `${id}_2`, name: '选项2', selected: false }
- ]
- });
- this.refreshNextEnabled();
- },
- /** 编辑多规格时,用接口 skus 回填价格/库存 */
- mergeSkusIntoCombos(combos) {
- const extra = session.getDishEditExtra();
- const skus = extra.skus || [];
- if (!skus.length) return combos;
- const skuMap = {};
- skus.forEach((s) => {
- const key = (s.specOptionNames || '').replace(/\//g, ' | ');
- skuMap[key] = s;
- skuMap[s.specOptionNames || ''] = s;
- });
- return (combos || []).map((c) => {
- const hit = skuMap[c.label] || skuMap[(c.label || '').replace(/\s*\|\s*/g, '/')];
- if (!hit) return c;
- return {
- ...c,
- price: hit.price != null ? String(hit.price) : hit.sellingPrice != null ? String(hit.sellingPrice) : c.price,
- sellingPrice:
- hit.sellingPrice != null
- ? String(hit.sellingPrice)
- : hit.originalPrice != null
- ? String(hit.originalPrice)
- : c.sellingPrice,
- stock: hit.stock != null ? String(hit.stock) : c.stock,
- costPrice: hit.costPrice != null ? String(hit.costPrice) : c.costPrice
- };
- });
- },
- onNext() {
- const tip = this.getNextTip(this.specMode);
- if (tip) {
- this.refreshNextEnabled();
- uni.showToast({ title: tip, icon: 'none' });
- return;
- }
- this.form.specMode = this.specMode;
- const coverUrls = (this.form.cover || '').split(',').filter(Boolean);
- let combos = [];
- if (this.specMode === 'multi') {
- combos = buildSpecCombos(this.form.specs);
- if (this.isEdit) combos = this.mergeSkusIntoCombos(combos);
- }
- const draft = {
- ...this.form,
- specMode: this.specMode,
- coverImages: this.form.cover,
- cover: coverUrls[0] || '',
- detailImages: this.form.detail,
- isEdit: this.isEdit,
- editId: this.editId,
- mealCategory: PRODUCT_TYPE_TO_CATEGORY[this.form.productType] || '1',
- combos
- };
- session.setDishDraft(draft);
- if (this.specMode === 'multi') {
- uni.navigateTo({ url: '/pages/merchantDish/dish/spec-detail' });
- } else {
- uni.navigateTo({ url: '/pages/merchantDish/dish/sale-time?from=single' });
- }
- }
- }
- };
- </script>
- <style lang="scss" scoped>
- .page {
- min-height: 100vh;
- background: #f7f8fa;
- padding-bottom: 160rpx;
- }
- .card {
- background: #fff;
- margin: 20rpx 24rpx;
- border-radius: 16rpx;
- padding: 8rpx 24rpx 16rpx;
- }
- .section-title {
- position: relative;
- display: inline-block;
- font-size: 30rpx;
- font-weight: 600;
- color: #222;
- padding: 24rpx 0 16rpx;
- &::after {
- content: '';
- position: absolute;
- left: 0;
- bottom: 8rpx;
- width: 100%;
- height: 4rpx;
- background: #449aff;
- border-radius: 2rpx;
- }
- }
- .row {
- display: flex;
- align-items: center;
- justify-content: space-between;
- min-height: 96rpx;
- border-bottom: 1rpx solid #f0f0f0;
- .label {
- width: 200rpx;
- font-size: 28rpx;
- color: #333;
- flex-shrink: 0;
- &.required::before {
- content: '*';
- color: #ff4d4f;
- margin-right: 4rpx;
- }
- }
- .value {
- flex: 1;
- text-align: right;
- font-size: 28rpx;
- color: #333;
- &.link {
- color: #449aff;
- }
- }
- .input {
- flex: 1;
- text-align: right;
- font-size: 28rpx;
- color: #333;
- }
- .arrow-down {
- width: 25rpx;
- height: 25rpx;
- margin-left: 10rpx;
- flex-shrink: 0;
- }
- }
- .ph {
- color: #c0c4cc;
- }
- .spec-tip {
- font-size: 22rpx;
- color: #999;
- padding: 0 0 16rpx;
- }
- .upload-row {
- padding: 20rpx 0;
- border-bottom: 1rpx solid #f0f0f0;
- &.last {
- border-bottom: none;
- }
- .label {
- font-size: 28rpx;
- color: #333;
- &.required::before {
- content: '*';
- color: #ff4d4f;
- margin-right: 4rpx;
- }
- }
- .tip {
- font-size: 22rpx;
- color: #999;
- margin: 8rpx 0 16rpx;
- }
- }
- .upload-btn {
- width: 140rpx;
- height: 140rpx;
- background: #f5f6f8;
- border-radius: 8rpx;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- color: #999;
- font-size: 24rpx;
- .upload-icon {
- width: 48rpx;
- height: 48rpx;
- margin-bottom: 8rpx;
- }
- }
- .spec-block {
- padding: 16rpx 0;
- border-bottom: 1rpx solid #f5f5f5;
- .spec-head {
- display: flex;
- justify-content: space-between;
- font-size: 28rpx;
- color: #333;
- margin-bottom: 16rpx;
- .more {
- letter-spacing: 2rpx;
- color: #999;
- padding: 0 8rpx;
- }
- }
- .tags {
- display: flex;
- flex-wrap: wrap;
- gap: 16rpx;
- }
- .tag {
- position: relative;
- min-width: 120rpx;
- padding: 16rpx 28rpx;
- border-radius: 8rpx;
- font-size: 26rpx;
- text-align: center;
- background: #fff;
- color: #666;
- border: 1rpx solid #e8e8e8;
- box-sizing: border-box;
- overflow: hidden;
- &.on {
- background: rgba(68, 154, 255, 0.08);
- color: #449aff;
- border-color: #449aff;
- }
- .tag-check {
- position: absolute;
- right: 0;
- bottom: 0;
- width: 28rpx;
- height: 28rpx;
- }
- }
- }
- .add-spec {
- padding: 24rpx 0;
- text-align: center;
- color: #449aff;
- font-size: 28rpx;
- }
- .spec-panels {
- .panel {
- display: none;
- }
- &.single .panel-single {
- display: block;
- }
- &.multi .panel-multi {
- display: block;
- }
- }
- .modal-body {
- padding: 30rpx;
- }
- .modal-input {
- border: 1rpx solid #eee;
- border-radius: 8rpx;
- padding: 16rpx 20rpx;
- font-size: 28rpx;
- }
- .mask {
- position: fixed;
- left: 0;
- right: 0;
- top: 0;
- bottom: 0;
- background: rgba(0, 0, 0, 0.45);
- z-index: 999;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .spec-dialog {
- width: 600rpx;
- background: #fff;
- border-radius: 20rpx;
- padding: 36rpx 32rpx 32rpx;
- box-sizing: border-box;
- }
- .spec-dialog-title {
- text-align: center;
- font-size: 32rpx;
- font-weight: 600;
- color: #222;
- margin-bottom: 28rpx;
- }
- .spec-dialog-input {
- width: 100%;
- height: 80rpx;
- padding: 0 24rpx;
- background: #f5f6f8;
- border-radius: 12rpx;
- font-size: 28rpx;
- color: #333;
- box-sizing: border-box;
- }
- .spec-ph {
- color: #c0c4cc;
- }
- .spec-dialog-footer {
- display: flex;
- gap: 20rpx;
- margin-top: 32rpx;
- }
- .spec-dialog-btn {
- flex: 1;
- height: 80rpx;
- line-height: 80rpx;
- text-align: center;
- font-size: 30rpx;
- border-radius: 40rpx;
- &.cancel {
- color: #666;
- background: #fff;
- border: 2rpx solid #e5e5e5;
- box-sizing: border-box;
- line-height: 76rpx;
- }
- &.save {
- color: #fff;
- background: linear-gradient( 89deg, #10ABFE 0%, #2260F6 100%);
- }
- }
- .spec-value-dialog {
- max-height: 70vh;
- display: flex;
- flex-direction: column;
- }
- .sv-list {
- overflow-y: auto;
- max-height: 48vh;
- }
- .sv-row {
- display: flex;
- align-items: center;
- gap: 16rpx;
- margin-bottom: 20rpx;
- &:last-child {
- margin-bottom: 0;
- }
- }
- .sv-input {
- flex: 1;
- width: auto;
- margin: 0;
- }
- .sv-circle {
- width: 48rpx;
- height: 48rpx;
- border-radius: 50%;
- flex-shrink: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 36rpx;
- line-height: 1;
- color: #fff;
- font-weight: 500;
- &.minus,
- &.plus {
- background: #7eb6ff;
- }
- }
- .value-popup {
- width: 600rpx;
- padding: 30rpx;
- }
- .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;
- max-height: 70vh;
- display: flex;
- flex-direction: column;
- }
- .pick-sheet-title {
- text-align: center;
- font-size: 32rpx;
- font-weight: 600;
- color: #222;
- padding: 36rpx 0 32rpx;
- flex-shrink: 0;
- }
- .pick-options {
- display: flex;
- flex-wrap: wrap;
- gap: 20rpx;
- margin-bottom: 40rpx;
- overflow-y: auto;
- max-height: 46vh;
- }
- .pick-opt {
- width: calc((100% - 40rpx) / 3);
- height: 72rpx;
- line-height: 70rpx;
- text-align: center;
- font-size: 28rpx;
- color: #666;
- background: #fff;
- border: 1rpx solid #e5e5e5;
- border-radius: 12rpx;
- box-sizing: border-box;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- padding: 0 8rpx;
- &.on {
- color: #449aff;
- background: rgba(68, 154, 255, 0.08);
- border-color: #449aff;
- }
- }
- .pick-sheet-footer {
- display: flex;
- gap: 24rpx;
- padding-bottom: 12rpx;
- flex-shrink: 0;
- }
- .pick-btn {
- flex: 1;
- height: 88rpx;
- line-height: 88rpx;
- text-align: center;
- font-size: 30rpx;
- border-radius: 44rpx;
- &.cancel {
- color: #666;
- background: #f2f3f5;
- }
- &.confirm {
- color: #fff;
- background: linear-gradient( 89deg, #10ABFE 0%, #2260F6 100%);
- }
- }
- ::v-deep .u-radio {
- margin-right: 24rpx;
- }
- </style>
|