|
|
@@ -0,0 +1,1228 @@
|
|
|
+<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格式,大小不超过2M</view>
|
|
|
+ <u-upload
|
|
|
+ :action="uploadAction"
|
|
|
+ :file-list="fileList.cover"
|
|
|
+ :form-data="uploadFormData"
|
|
|
+ :max-count="4"
|
|
|
+ :max-size="2 * 1024 * 1024"
|
|
|
+ :multiple="true"
|
|
|
+ :deletable="true"
|
|
|
+ :custom-btn="true"
|
|
|
+ del-bg-color="#fa3534"
|
|
|
+ width="140"
|
|
|
+ height="140"
|
|
|
+ preview-full-image
|
|
|
+ @on-success="onCoverSuccess"
|
|
|
+ @on-remove="onCoverRemove"
|
|
|
+ @on-oversize="onUploadOversize"
|
|
|
+ >
|
|
|
+ <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格式,大小不超过2M</view>
|
|
|
+ <u-upload
|
|
|
+ :action="uploadAction"
|
|
|
+ :file-list="fileList.detail"
|
|
|
+ :form-data="uploadFormData"
|
|
|
+ :max-count="20"
|
|
|
+ :max-size="2 * 1024 * 1024"
|
|
|
+ :multiple="true"
|
|
|
+ :deletable="true"
|
|
|
+ :custom-btn="true"
|
|
|
+ del-bg-color="#fa3534"
|
|
|
+ width="140"
|
|
|
+ height="140"
|
|
|
+ preview-full-image
|
|
|
+ @on-success="onDetailSuccess"
|
|
|
+ @on-remove="onDetailRemove"
|
|
|
+ @on-oversize="onUploadOversize"
|
|
|
+ >
|
|
|
+ <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(',');
|
|
|
+ },
|
|
|
+ onUploadOversize() {
|
|
|
+ uni.showToast({ title: '图片大小不能超过2M', icon: 'none' });
|
|
|
+ },
|
|
|
+ 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>
|