shish hai 2 meses
pai
achega
b6444dda8b

+ 11 - 4
ghsPad/src/pages/home/components/selectPrice.vue

@@ -151,7 +151,9 @@
                                 <view class="numpad-key numpad-key-op" data-action="calc-add" @tap="onNumpadTap">+</view>
                                 <view class="numpad-key numpad-key-op" data-action="calc-sub" @tap="onNumpadTap">−</view>
                                 <view class="numpad-key numpad-key-op" data-action="calc-div" @tap="onNumpadTap">÷</view>
-                                <view class="numpad-key numpad-key-op numpad-key-op-muted" data-action="calc-clear" @tap="onNumpadTap">清空</view>
+                                <view class="numpad-key numpad-key-op numpad-key-op-muted" data-action="calc-clear" @tap="onNumpadTap">
+                                    <text class="numpad-key-text numpad-key-text-clear">清空</text>
+                                </view>
                             </view>
                         </view>
                     </view>
@@ -1942,13 +1944,18 @@ export default {
 .numpad-op-col .numpad-key-op + .numpad-key-op {
     margin-top: 8upx;
 }
+.numpad-op-col .numpad-key-op.numpad-key-op-muted {
+    font-size: 14upx;
+}
 .numpad-key-op-muted {
-    font-size: 13upx;
+    font-size: 14upx;
     font-weight: 600;
     background: #e8e8e8;
-    color: #333333;
     box-shadow: 0 2upx 0 #bdbdbd;
 }
+.numpad-key-text-clear {
+    color: #d32f2f;
+}
 .keyboard-body_box .numpad-op-col .numpad-key-op {
     min-height: 0;
 }
@@ -2015,7 +2022,7 @@ export default {
 }
 .numpad-key-danger {
     background: #ffeded;
-    color: #cc4444;
+    color: #222222;
     box-shadow: 0 2upx 0 #ddb8b8;
 }
 .numpad-key-action {

+ 1968 - 0
hdApp/src/admin/tools/calculator.vue

@@ -0,0 +1,1968 @@
+<template>
+	<view class="calc-page">
+		<scroll-view class="calc-scroll" scroll-y :enable-back-to-top="false">
+			<view class="calc-sheet">
+				<view class="calc-card-wrap">
+					<view
+						class="calc-section"
+						:class="{ 'calc-section-active': keyboardTarget === 'calc' }"
+						@tap="focusCalc"
+					>
+						<view class="calc-actions-top">
+							<view class="discount-entry" @tap.stop="openDiscountPopup" hover-class="discount-entry-hover">
+								<text class="discount-entry-glyph">折</text>
+							</view>
+							<text class="calc-section-title">计算器</text>
+							<view class="history-entry" @tap.stop="openCalcHistory" hover-class="history-entry-hover">
+								<text class="iconfont iconmingxi1 history-entry-icon"></text>
+							</view>
+						</view>
+						<view class="calc-formula">
+							<block v-if="tokens.length === 0 && isEmptyCalcInput">
+								<text class="calc-placeholder">输入数字</text>
+							</block>
+							<view v-else class="calc-formula-inner">
+								<block v-for="(t, idx) in tokens" :key="'tok-' + idx">
+									<text v-if="t.type === 'num'" class="calc-num">{{ t.s }}</text>
+									<text v-else class="calc-op">{{ t.o }}</text>
+								</block>
+								<text v-if="!isEmptyCalcInput" class="calc-num">{{ calcInput }}</text>
+							</view>
+						</view>
+						<view class="calc-total-row">
+							<text class="calc-total-label">应收金额</text>
+							<text class="calc-total-val">{{ formatMoney(stackDiscountedAmount) }}</text>
+						</view>
+						<view v-if="isDiscountActive" class="calc-discount-row">
+							<text class="calc-discount-label">{{ formatDiscountZheLabel(discountZhe) }}后</text>
+							<text class="calc-discount-val">{{ formatMoney(stackDiscountedAmount) }}</text>
+						</view>
+					</view>
+
+					<view class="customer-strip" @tap.stop="pickCustomer">
+						<text class="customer-strip-label">客户</text>
+						<text class="customer-strip-name">{{ customerDisplayName }}</text>
+						<text class="customer-strip-action">修改客户</text>
+					</view>
+
+					<view class="rcv-pay-block">
+						<view class="rcv-pay-title">收款方式</view>
+						<view class="rcv-item-row">
+							<view
+								v-for="item in optHasPayList"
+								:key="'hp-' + item.id"
+								class="rcv-item-chip"
+								:data-hpid="String(item.id)"
+								:class="{ active: selHasPay === item.id }"
+								@tap.stop="onHasPayChipTap"
+							>
+								{{ item.name }}
+							</view>
+						</view>
+						<view v-if="selHasPay === 1" class="rcv-sub-block">
+							<view class="rcv-pay-title rcv-pay-title-sub">线下方式</view>
+							<view class="rcv-item-row">
+								<view
+									v-for="item in optPayWayList"
+									:key="'pw-' + item.id"
+									class="rcv-item-chip"
+									:data-pwid="String(item.id)"
+									:class="{ active: selPayWay === item.id }"
+									@tap.stop="onPayWayChipTap"
+								>
+									{{ item.name }}
+								</view>
+							</view>
+						</view>
+					</view>
+
+					<view v-if="showCashPayCols" class="pay-cash-panel">
+						<view class="pay-cash-item" @tap="checkCurrent(2)">
+							<text class="pay-cash-desc">收到现金</text>
+							<text
+								class="pay-cash-amt"
+								:class="{ 'pay-cash-amt-selected': keyboardTarget !== 'calc' && checkType == 2 }"
+							>
+								{{ formatMoney(cash) }}
+							</text>
+						</view>
+						<view class="pay-cash-item pay-cash-item-change">
+							<view class="pay-change-line">
+								<text v-if="Number(zl) > 0" class="lbl-ok">找零</text>
+								<text v-else-if="Number(zl) < 0" class="lbl-bad">还差</text>
+								<text v-else class="lbl-neu">找零</text>
+							</view>
+							<text v-if="Number(zl) > 0" class="amt-ok">{{ formatChangeAmount(zl) }}</text>
+							<text v-else-if="Number(zl) < 0" class="amt-bad">{{ formatChangeAmount(-zl) }}</text>
+							<text v-else class="amt-neu">—</text>
+						</view>
+					</view>
+
+					<view class="pay-submit-row">
+						<view class="pay-submit-btn pay-submit-btn-outline" @tap.stop="stashCurrent">暂存</view>
+						<view class="pay-submit-btn" @tap.stop="submitCashierPay">提交</view>
+					</view>
+
+					<view class="safe-bottom-spacer" />
+				</view>
+			</view>
+		</scroll-view>
+
+		<view class="numpad-dock safe-area-padding">
+				<view class="numpad">
+					<view class="numpad-grid-wrap">
+						<view class="numpad-cell"><view class="numpad-key" data-char="1" @tap="onNumpadTap">1</view></view>
+						<view class="numpad-cell"><view class="numpad-key" data-char="2" @tap="onNumpadTap">2</view></view>
+						<view class="numpad-cell"><view class="numpad-key" data-char="3" @tap="onNumpadTap">3</view></view>
+						<view class="numpad-cell numpad-cell-op numpad-cell-mul">
+							<view class="numpad-key numpad-key-op" data-action="calc-mul" @tap="onNumpadTap">×</view>
+						</view>
+
+						<view class="numpad-cell"><view class="numpad-key" data-char="4" @tap="onNumpadTap">4</view></view>
+						<view class="numpad-cell"><view class="numpad-key" data-char="5" @tap="onNumpadTap">5</view></view>
+						<view class="numpad-cell"><view class="numpad-key" data-char="6" @tap="onNumpadTap">6</view></view>
+						<view class="numpad-cell numpad-cell-op numpad-cell-add">
+							<view class="numpad-key numpad-key-op numpad-key-op-add" data-action="calc-add" @tap="onNumpadTap">
+								+
+							</view>
+						</view>
+
+						<view class="numpad-cell"><view class="numpad-key" data-char="7" @tap="onNumpadTap">7</view></view>
+						<view class="numpad-cell"><view class="numpad-key" data-char="8" @tap="onNumpadTap">8</view></view>
+						<view class="numpad-cell"><view class="numpad-key" data-char="9" @tap="onNumpadTap">9</view></view>
+
+						<view class="numpad-cell"><view class="numpad-key numpad-key-muted" data-char="." @tap="onNumpadTap">.</view></view>
+						<view class="numpad-cell"><view class="numpad-key" data-char="0" @tap="onNumpadTap">0</view></view>
+						<view class="numpad-cell"><view class="numpad-key" data-char="00" @tap="onNumpadTap">00</view></view>
+						<view class="numpad-cell numpad-cell-op numpad-cell-sub">
+							<view class="numpad-key numpad-key-op" data-action="calc-sub" @tap="onNumpadTap">−</view>
+						</view>
+
+						<view class="numpad-cell numpad-cell-actions-row">
+							<view
+								class="numpad-key numpad-key-action-dual numpad-key-action-muted"
+								@tap.stop="tapClearCalculator"
+							>
+								<text class="numpad-key-text">清空</text>
+							</view>
+							<view
+								class="numpad-key numpad-key-danger numpad-key-action-dual"
+								data-action="backspace"
+								@tap="onNumpadTap"
+							>
+								<text class="numpad-key-text">删除</text>
+							</view>
+						</view>
+						<view class="numpad-cell numpad-cell-op numpad-cell-div">
+							<view class="numpad-key numpad-key-op" data-action="calc-div" @tap="onNumpadTap">÷</view>
+						</view>
+					</view>
+				</view>
+		</view>
+
+		<uni-popup ref="calcHistoryPopup" type="center" background-color="#ffffff" @change="onCalcHistoryPopupChange">
+			<view class="mob-popup-wrap mob-popup-history">
+				<view class="mob-popup-hd">
+					<text class="mob-popup-title">暂存记录</text>
+					<text class="mob-popup-close" @tap.stop="onHistoryPopupCloseTap">关闭</text>
+				</view>
+				<scroll-view scroll-y class="mob-popup-body">
+					<view v-for="(row, idx) in calcHistory" :key="row.id || 'hist-' + idx" class="history-item">
+						<view class="history-item-body" :data-idx="idx" @tap.stop="onHistoryRowTap">
+							<view class="history-line">
+								<text class="history-time">{{ row.timeLabel }}</text>
+								<text class="history-total">合计 {{ formatMoney(row.total) }}</text>
+							</view>
+							<text class="history-detail">{{ row.detailText }}</text>
+						</view>
+						<text class="history-del" :data-idx="idx" @tap.stop="onHistoryDeleteTap">删除</text>
+					</view>
+					<view v-if="calcHistory.length === 0" class="history-empty">暂无记录</view>
+				</scroll-view>
+			</view>
+		</uni-popup>
+
+		<uni-popup ref="discountPopup" type="center" background-color="#ffffff" @change="onDiscountPopupChange">
+			<view class="mob-popup-wrap mob-popup-discount">
+				<view class="mob-popup-hd">
+					<text class="mob-popup-title">折扣</text>
+					<text class="mob-popup-close" @tap.stop="onDiscountPopupCloseTap">关闭</text>
+				</view>
+				<view class="discount-toolbar">
+					<view class="discount-custom-row">
+						<input
+							class="discount-custom-input"
+							v-model="customZheInput"
+							type="digit"
+							placeholder="如 8.8 折"
+							confirm-type="done"
+						/>
+						<view class="discount-custom-add-btn" @tap.stop="addCustomDiscountZhe">添加</view>
+					</view>
+					<text v-if="customDiscountZheList.length > 0" class="discount-toolbar-hint">
+						已存 {{ customDiscountZheList.length }} 条,点下方选用
+					</text>
+					<text v-else class="discount-toolbar-hint discount-toolbar-hint-muted">先输入折扣数字再点添加</text>
+				</view>
+				<scroll-view scroll-y class="mob-popup-body discount-options-scroll">
+					<view
+						class="discount-option"
+						:class="{ 'discount-option-active': isZheActive(10) }"
+						data-zhe="10"
+						@tap.stop="onDiscountRowPickTap"
+					>
+						<text class="discount-option-text">无折扣</text>
+						<text v-if="isZheActive(10)" class="discount-option-check">✓</text>
+					</view>
+					<view
+						v-for="(z, cidx) in customDiscountZheList"
+						:key="'d-' + cidx + '-' + z"
+						class="discount-option discount-option-with-del"
+						:class="{ 'discount-option-active': isZheActive(z) }"
+					>
+						<view class="discount-option-main" :data-zhe="String(z)" @tap.stop="onDiscountRowPickTap">
+							<text class="discount-option-text">{{ formatDiscountRowLabel(z) }}</text>
+							<text v-if="isZheActive(z)" class="discount-option-check">✓</text>
+						</view>
+						<text class="discount-option-del" :data-cidx="String(cidx)" @tap.stop="onCustomDiscountDeleteTap">
+							删除
+						</text>
+					</view>
+				</scroll-view>
+			</view>
+		</uni-popup>
+	</view>
+</template>
+
+<script>
+import { createOrder } from '@/api/order/index'
+import { mapGetters } from 'vuex'
+
+const CALC_HISTORY_STORAGE_KEY = 'ghsApp_calculator_calcHistory'
+const DISCOUNT_CUSTOM_STORAGE_KEY = 'ghsApp_calculator_customDiscountZhe'
+const DISCOUNT_SELECTED_STORAGE_KEY = 'ghsApp_calculator_discountZhe'
+/** 计算器选客回填:对应 selectCustom style=3 */
+const CALCULATOR_PICK_CUSTOMER_KEY = 'ghsApp_calculator_pick_customer'
+/** 与首页「快捷开单」一致:取自 loginInfo.skCustomId,界面展示名固定 */
+const SHORTCUT_CUSTOM_DISPLAY_NAME = '快捷开单'
+
+const SETTLE_FORM_DEFAULTS = {
+	sendType: 1,
+	hasPay: 0,
+	payWay: 0,
+	needPrint: 1,
+	freight: '',
+	groupName: '',
+	remark: '',
+	sendCost: 0,
+	customSendCost: 0,
+	labourCost: 0,
+	modifyPrice: 0,
+	getStaffId: 0,
+	getStaffName: '',
+	shopAdminId: 0,
+	shopAdminName: '',
+	cash: 0,
+	dealPrice: 0,
+	reductionRule: 0,
+	callErrand: 0,
+	weight: 1,
+	deliveryRemark: '',
+}
+
+export default {
+	name: 'ToolsCalculator',
+	data() {
+		return {
+			customId: 0,
+			customName: '',
+			price: 0,
+			checkType: 3,
+			keyboardTarget: 'calc',
+			name: '',
+			cash: 0,
+			tokens: [],
+			calcInput: '',
+			calcHistory: [],
+			maxHistory: 20,
+			_historyPopupCloseByApi: false,
+			discountZhe: 10,
+			customDiscountZheList: [],
+			customZheInput: '',
+			_discountPopupCloseByApi: false,
+			selHasPay: 0,
+			selPayWay: 0,
+			optHasPayList: [
+				{ name: '扫码', id: 0 },
+				{ name: '赊账', id: 2 },
+				{ name: '线下', id: 1 },
+			],
+			optPayWayList: [
+				{ name: '微信', id: 0 },
+				{ name: '支付宝', id: 1 },
+				{ name: '现金', id: 4 },
+				{ name: '银行卡', id: 5 },
+			],
+		}
+	},
+	computed: {
+		...mapGetters({ loginInfo: 'getLoginInfo' }),
+		/** 工作台「快捷开单」客户 id,与 workbench skKd 使用的 loginInfo.skCustomId 一致 */
+		shortcutCustomerId() {
+			const login = this.loginInfo || {}
+			const sid = login.skCustomId
+			if (this.$util.isEmpty(sid) || Number(sid) <= 0) return 0
+			return Number(sid)
+		},
+		customerDisplayName() {
+			const n = (this.customName || '').trim()
+			if (n) return n
+			const skid = this.shortcutCustomerId
+			if (Number(this.customId) > 0 && skid > 0 && Number(this.customId) === skid) {
+				return SHORTCUT_CUSTOM_DISPLAY_NAME
+			}
+			if (Number(this.customId) > 0) return '—'
+			return '未选择'
+		},
+		isEmptyCalcInput() {
+			return this.$util.isEmpty(this.calcInput)
+		},
+		stackTotal() {
+			const { nums, ops } = this.buildEvalState()
+			if (!nums.length) return 0
+			const r = this.evalExpr(nums, ops)
+			return isNaN(r) ? 0 : r
+		},
+		discountRate() {
+			const z = Number(this.discountZhe)
+			if (isNaN(z) || z <= 0) return 1
+			return Math.min(10, Math.max(0.01, z)) / 10
+		},
+		stackDiscountedAmount() {
+			const raw = Number(this.stackTotal)
+			if (isNaN(raw)) return 0
+			return parseFloat((raw * this.discountRate).toFixed(2))
+		},
+		isDiscountActive() {
+			return Number(this.discountZhe) < 10 - 1e-6
+		},
+		zl() {
+			if (!(this.selHasPay === 1 && this.selPayWay === 4)) return 0
+			let amount = 0
+			if (Number(this.cash) > 0 && Number(this.price) > 0) {
+				amount = Number(this.cash) - Number(this.price)
+				amount = parseFloat(amount.toFixed(2))
+			}
+			return Number(amount)
+		},
+		showCashPayCols() {
+			return this.selHasPay === 1 && this.selPayWay === 4
+		},
+	},
+	watch: {
+		shortcutCustomerId(n) {
+			if (n > 0 && Number(this.customId) <= 0) this.applyDefaultShortcutCustomer()
+		},
+		stackDiscountedAmount: {
+			handler(v) {
+				if (this.keyboardTarget === 'calc') {
+					const n = parseFloat(Number(v).toFixed(2))
+					this.price = isNaN(n) ? 0 : n
+				}
+			},
+			immediate: true,
+		},
+	},
+	onLoad(options) {
+		if (options && options.customId) {
+			const id = Number(options.customId)
+			if (!isNaN(id) && id > 0) this.customId = id
+		}
+		if (options && options.customName) {
+			this.customName = decodeURIComponent(String(options.customName))
+		}
+		this.applyDefaultShortcutCustomer()
+	},
+	onShow() {
+		this.applyPickCustomerFromStorage()
+		this.applyDefaultShortcutCustomer()
+	},
+	mounted() {
+		this.loadCalcHistoryFromStorage()
+		this.loadDiscountFromStorage()
+	},
+	methods: {
+		init(){
+
+		},
+		pickCustomer() {
+			this.tapVoice()
+			this.$util.pageTo({url: '/admin/custom/selectCustom?style=3'})
+		},
+		applyPickCustomerFromStorage() {
+			try {
+				const raw = uni.getStorageSync(CALCULATOR_PICK_CUSTOMER_KEY)
+				if (!raw) return
+				const obj = typeof raw === 'string' ? JSON.parse(raw) : raw
+				if (obj && obj.id != null) {
+					this.customId = Number(obj.id) || 0
+					this.customName = obj.name != null ? String(obj.name) : ''
+				}
+				uni.removeStorageSync(CALCULATOR_PICK_CUSTOMER_KEY)
+			} catch (e) {}
+		},
+		/** 未选客且无路由参数带入时,默认工作台设置的快捷客户 */
+		applyDefaultShortcutCustomer() {
+			if (Number(this.customId) > 0) return
+			const skid = this.shortcutCustomerId
+			if (!skid || skid <= 0) return
+			this.customId = skid
+			this.customName = SHORTCUT_CUSTOM_DISPLAY_NAME
+		},
+		loadCalcHistoryFromStorage() {
+			try {
+				const raw = uni.getStorageSync(CALC_HISTORY_STORAGE_KEY)
+				if (raw === undefined || raw === null || raw === '') return
+				const list = typeof raw === 'string' ? JSON.parse(raw) : raw
+				if (!Array.isArray(list) || !list.length) return
+				this.calcHistory = list.slice(0, this.maxHistory).map((row, i) => this.normalizeHistoryRow(row, i))
+			} catch (e) {
+				console.warn('[calculator] loadCalcHistoryFromStorage', e)
+			}
+		},
+		normalizeHistoryRow(row, idx) {
+			const r = row && typeof row === 'object' ? row : {}
+			const id = r.id != null && String(r.id) !== '' ? String(r.id) : 'hist-' + Date.now() + '-' + idx
+			return {
+				id,
+				tokens: Array.isArray(r.tokens) ? r.tokens : null,
+				addends: Array.isArray(r.addends) ? r.addends : null,
+				calcInput: r.calcInput != null ? String(r.calcInput) : '',
+				total: r.total,
+				timeLabel: r.timeLabel != null ? String(r.timeLabel) : '--:--',
+				detailText: r.detailText != null ? String(r.detailText) : '',
+				cashSnapshot: r.cashSnapshot,
+				priceSnapshot: r.priceSnapshot,
+				orderOnly: !!r.orderOnly,
+				source: r.source === 'stash' ? 'stash' : 'submit',
+				discountZhe:
+					r.discountZhe != null && r.discountZhe !== '' && !isNaN(Number(r.discountZhe))
+						? parseFloat(Number(r.discountZhe).toFixed(2))
+						: undefined,
+			}
+		},
+		persistCalcHistoryToStorage() {
+			try {
+				uni.setStorageSync(CALC_HISTORY_STORAGE_KEY, JSON.stringify(this.calcHistory))
+			} catch (e) {
+				console.warn('[calculator] persistCalcHistoryToStorage', e)
+			}
+		},
+		loadDiscountFromStorage() {
+			try {
+				const customRaw = uni.getStorageSync(DISCOUNT_CUSTOM_STORAGE_KEY)
+				if (customRaw !== undefined && customRaw !== null && customRaw !== '') {
+					const parsed = typeof customRaw === 'string' ? JSON.parse(customRaw) : customRaw
+					if (Array.isArray(parsed)) {
+						this.customDiscountZheList = parsed
+							.map((x) => Number(x))
+							.filter((n) => !isNaN(n) && n > 0 && n < 10 - 1e-6)
+							.map((n) => parseFloat(n.toFixed(2)))
+					}
+				}
+				const sel = uni.getStorageSync(DISCOUNT_SELECTED_STORAGE_KEY)
+				if (sel !== undefined && sel !== null && sel !== '') {
+					const z = Number(sel)
+					if (!isNaN(z) && z > 0 && z <= 10) this.discountZhe = parseFloat(z.toFixed(2))
+				}
+				this.normalizeDiscountSelectionToCustomOrNone()
+			} catch (e) {
+				console.warn('[calculator] loadDiscountFromStorage', e)
+			}
+		},
+		normalizeDiscountSelectionToCustomOrNone() {
+			const z = Number(this.discountZhe)
+			if (isNaN(z)) {
+				this.discountZhe = 10
+				this.persistDiscountSelected()
+				return
+			}
+			if (z >= 10 - 1e-6) {
+				this.discountZhe = 10
+				return
+			}
+			const ok = this.customDiscountZheList.some((x) => Math.abs(Number(x) - z) < 1e-4)
+			if (!ok) {
+				this.discountZhe = 10
+				this.persistDiscountSelected()
+			}
+		},
+		persistDiscountSelected() {
+			try {
+				uni.setStorageSync(DISCOUNT_SELECTED_STORAGE_KEY, String(this.discountZhe))
+			} catch (e) {
+				console.warn('[calculator] persistDiscountSelected', e)
+			}
+		},
+		persistCustomDiscountList() {
+			try {
+				uni.setStorageSync(DISCOUNT_CUSTOM_STORAGE_KEY, JSON.stringify(this.customDiscountZheList))
+			} catch (e) {
+				console.warn('[calculator] persistCustomDiscountList', e)
+			}
+		},
+		resetActiveDiscountToNone() {
+			this.discountZhe = 10
+			this.persistDiscountSelected()
+		},
+		formatDiscountZheLabel(z) {
+			const n = Number(z)
+			if (isNaN(n) || n >= 10 - 1e-6) return ''
+			return parseFloat(n.toFixed(2)) + '折'
+		},
+		formatDiscountRowLabel(z) {
+			const n = Number(z)
+			if (isNaN(n)) return '—'
+			return parseFloat(n.toFixed(2)) + '折'
+		},
+		isZheActive(z) {
+			const a = Number(this.discountZhe)
+			const b = Number(z)
+			if (isNaN(a) || isNaN(b)) return false
+			return Math.abs(a - b) < 1e-4
+		},
+		openDiscountPopup() {
+			this.tapVoice()
+			this.$refs.discountPopup && this.$refs.discountPopup.open()
+		},
+		onDiscountPopupCloseTap() {
+			this.tapVoice()
+			this.closeDiscountPopup()
+		},
+		closeDiscountPopup() {
+			this._discountPopupCloseByApi = true
+			this.$refs.discountPopup && this.$refs.discountPopup.close()
+		},
+		onDiscountPopupChange(e) {
+			if (!e || e.show !== false) return
+			if (this._discountPopupCloseByApi) {
+				this._discountPopupCloseByApi = false
+				return
+			}
+			this.tapVoice()
+		},
+		onDiscountRowPickTap(e) {
+			const ds = e.currentTarget.dataset || {}
+			const z = Number(ds.zhe)
+			if (isNaN(z)) return
+			this.pickDiscountZhe(z)
+		},
+		onCustomDiscountDeleteTap(e) {
+			const ds = e.currentTarget.dataset || {}
+			const idx = Number(ds.cidx)
+			if (isNaN(idx)) return
+			this.removeCustomDiscountByIndex(idx)
+		},
+		pickDiscountZhe(z) {
+			this.tapVoice()
+			const n = Number(z)
+			if (isNaN(n) || n <= 0 || n > 10) {
+				this.$msg('折扣无效')
+				return
+			}
+			if (n < 10 - 1e-6) {
+				const ok = this.customDiscountZheList.some((x) => Math.abs(Number(x) - n) < 1e-4)
+				if (!ok) {
+					this.$msg('请先从列表选择或添加')
+					return
+				}
+			}
+			this.discountZhe = parseFloat(n.toFixed(2))
+			this.persistDiscountSelected()
+			this.closeDiscountPopup()
+		},
+		addCustomDiscountZhe() {
+			this.tapVoice()
+			const raw = String(this.customZheInput || '').trim()
+			if (!raw) {
+				this.$msg('请输入折扣')
+				return
+			}
+			const n = parseFloat(raw)
+			if (isNaN(n) || n <= 0 || n > 10) {
+				this.$msg('请输入 0.1~10(如 8.8)')
+				return
+			}
+			const rounded = parseFloat(n.toFixed(2))
+			if (rounded >= 10 - 1e-6) {
+				this.$msg('不打折请选「无折扣」')
+				return
+			}
+			if (this.customDiscountZheList.some((x) => Math.abs(x - rounded) < 1e-4)) {
+				this.$msg('已在列表中')
+				return
+			}
+			this.customDiscountZheList = [rounded].concat(this.customDiscountZheList)
+			this.persistCustomDiscountList()
+			this.customZheInput = ''
+			this.$msg('已添加')
+		},
+		removeCustomDiscountByIndex(cidx) {
+			const idx = Number(cidx)
+			if (isNaN(idx) || idx < 0 || idx >= this.customDiscountZheList.length) return
+			this.tapVoice()
+			const removed = this.customDiscountZheList[idx]
+			this.customDiscountZheList = this.customDiscountZheList.filter((_, i) => i !== idx)
+			this.persistCustomDiscountList()
+			if (this.isZheActive(removed)) {
+				this.discountZhe = 10
+				this.persistDiscountSelected()
+			}
+		},
+		tapVoice() {
+			this.$util.hitRemind()
+		},
+		buildEvalState() {
+			const nums = []
+			const ops = []
+			for (let i = 0; i < this.tokens.length; i++) {
+				const t = this.tokens[i]
+				if (t.type === 'num') {
+					const v = Number(t.s)
+					if (!isNaN(v)) nums.push(v)
+				} else {
+					ops.push(t.o)
+				}
+			}
+			const hasTrailingOp = this.tokens.length > 0 && this.tokens[this.tokens.length - 1].type === 'op'
+			const ci = this.calcInput
+			const hasPartial =
+				ci &&
+				!this.$util.isEmpty(ci) &&
+				ci !== '.' &&
+				!isNaN(parseFloat(ci))
+			let evalNums = nums.slice()
+			let evalOps = ops.slice()
+			if (hasPartial) {
+				const c = parseFloat(ci)
+				if (!isNaN(c) && c > 0) evalNums.push(parseFloat(c.toFixed(2)))
+			} else if (hasTrailingOp && evalOps.length > 0) {
+				evalOps = evalOps.slice(0, -1)
+			}
+			while (evalOps.length > 0 && evalNums.length <= evalOps.length) {
+				evalOps.pop()
+			}
+			return { nums: evalNums, ops: evalOps }
+		},
+		opToInternal(o) {
+			if (o === '×') return '*'
+			if (o === '÷') return '/'
+			if (o === '−') return '-'
+			return '+'
+		},
+		evalExpr(nums, ops) {
+			if (!nums.length) return 0
+			const nArr = nums.map((x) => Number(x))
+			const oArr = ops.map((o) => this.opToInternal(o))
+			while (oArr.length > nArr.length - 1) oArr.pop()
+			if (nArr.length === 1) return parseFloat(nArr[0].toFixed(2))
+			if (oArr.length !== nArr.length - 1) return nArr[0] || 0
+			let i = 0
+			while (i < oArr.length) {
+				if (oArr[i] === '*' || oArr[i] === '/') {
+					const a = nArr[i]
+					const b = nArr[i + 1]
+					if (oArr[i] === '/' && b === 0) return NaN
+					const r = oArr[i] === '*' ? a * b : a / b
+					nArr.splice(i, 2, r)
+					oArr.splice(i, 1)
+				} else {
+					i++
+				}
+			}
+			let s = nArr[0]
+			for (let k = 0; k < oArr.length; k++) {
+				const b = nArr[k + 1]
+				if (oArr[k] === '+') s += b
+				else s -= b
+			}
+			return parseFloat(s.toFixed(2))
+		},
+		cloneTokens(arr) {
+			return arr.map((t) => (t.type === 'num' ? { type: 'num', s: t.s } : { type: 'op', o: t.o }))
+		},
+		getExprDisplayString(includePartial) {
+			let s = ''
+			this.tokens.forEach((t) => {
+				s += t.type === 'num' ? t.s : t.o
+			})
+			if (includePartial && !this.$util.isEmpty(this.calcInput)) {
+				s += this.calcInput
+			}
+			return s
+		},
+		legacyAddendsToTokens(addends) {
+			const tokens = []
+			if (!addends || !addends.length) return tokens
+			addends.forEach((raw, i) => {
+				const v = Number(raw)
+				if (i === 0) {
+					tokens.push({ type: 'num', s: String(Math.abs(v)) })
+				} else {
+					tokens.push({ type: 'op', o: v >= 0 ? '+' : '−' })
+					tokens.push({ type: 'num', s: String(Math.abs(v)) })
+				}
+			})
+			return tokens
+		},
+		pressCalcOp(displayOp) {
+			if (!this.ensureCalcModeForOps()) return
+			if (!this.$util.isEmpty(this.calcInput) && this.calcInput !== '.') {
+				const n = Number(this.calcInput)
+				if (isNaN(n) || n <= 0) {
+					this.$msg('请输入有效金额')
+					return
+				}
+				const fs = parseFloat(n.toFixed(2))
+				this.tokens.push({ type: 'num', s: String(fs) })
+				this.calcInput = ''
+			}
+			const last = this.tokens.length > 0 ? this.tokens[this.tokens.length - 1] : null
+			if (last && last.type === 'op') {
+				last.o = displayOp
+				return
+			}
+			if (last && last.type === 'num') {
+				this.tokens.push({ type: 'op', o: displayOp })
+				return
+			}
+			this.$msg('请先输入数字')
+		},
+		ensureCalcModeForOps() {
+			if (this.keyboardTarget !== 'calc') {
+				this.$msg('请先点击上方计算器区域')
+				return false
+			}
+			return true
+		},
+		/** 金额展示:最多两位小数,不保留小数末尾多余的 0 */
+		formatMoney(n) {
+			const x = Number(n)
+			if (isNaN(x)) return '0'
+			return parseFloat(x.toFixed(2)).toString()
+		},
+		formatChangeAmount(n) {
+			const x = Number(n)
+			if (isNaN(x)) return '0'
+			const s = (Math.round(x * 100) / 100).toFixed(2)
+			return parseFloat(s).toString()
+		},
+		focusCalc() {
+			this.tapVoice()
+			this.keyboardTarget = 'calc'
+			this.checkType = 3
+		},
+		checkCurrent(n) {
+			this.tapVoice()
+			this.checkType = n
+			this.keyboardTarget = n === 1 ? 'order' : 'cash'
+		},
+		onHasPayChipTap(e) {
+			const ds = e.currentTarget.dataset || {}
+			const val = ds.hpid !== undefined ? Number(ds.hpid) : NaN
+			if (isNaN(val)) return
+			this.applySelHasPay(val)
+		},
+		onPayWayChipTap(e) {
+			const ds = e.currentTarget.dataset || {}
+			const val = ds.pwid !== undefined ? Number(ds.pwid) : NaN
+			if (isNaN(val)) return
+			this.applySelPayWay(val)
+		},
+		applySelHasPay(val) {
+			this.tapVoice()
+			this.selHasPay = val
+			this.cash = 0
+			if (Number(val) !== 1) this.selPayWay = 0
+			this.blurCashIfNoColumn()
+		},
+		applySelPayWay(val) {
+			this.tapVoice()
+			this.selPayWay = val
+			if (Number(val) !== 4) {
+				this.cash = 0
+			} else {
+				this.checkCurrent(2)
+			}
+			this.blurCashIfNoColumn()
+		},
+		blurCashIfNoColumn() {
+			if (this.keyboardTarget === 'cash' && !this.showCashPayCols) this.focusCalc()
+		},
+		openCalcHistory() {
+			this.tapVoice()
+			this.$refs.calcHistoryPopup && this.$refs.calcHistoryPopup.open()
+		},
+		onHistoryPopupCloseTap() {
+			this.tapVoice()
+			this.closeCalcHistory()
+		},
+		closeCalcHistory() {
+			this._historyPopupCloseByApi = true
+			this.$refs.calcHistoryPopup && this.$refs.calcHistoryPopup.close()
+		},
+		onCalcHistoryPopupChange(e) {
+			if (!e || e.show !== false) return
+			if (this._historyPopupCloseByApi) {
+				this._historyPopupCloseByApi = false
+				return
+			}
+			this.tapVoice()
+		},
+		onHistoryRowTap(e) {
+			this.tapVoice()
+			const idx = Number(e.currentTarget.dataset.idx)
+			if (isNaN(idx) || idx < 0) return
+			const row = this.calcHistory[idx]
+			this.applyHistoryRow(row)
+			this.closeCalcHistory()
+		},
+		onHistoryDeleteTap(e) {
+			this.tapVoice()
+			const idx = Number(e.currentTarget.dataset.idx)
+			if (isNaN(idx) || idx < 0 || idx >= this.calcHistory.length) return
+			this.calcHistory.splice(idx, 1)
+			this.persistCalcHistoryToStorage()
+		},
+		pushHistorySnapshot(total, opts) {
+			opts = opts || {}
+			const now = new Date()
+			const hh = String(now.getHours())
+			const m = now.getMinutes() < 10 ? '0' + now.getMinutes() : String(now.getMinutes())
+			const timeLabel = hh + ':' + m
+			const hasCalc =
+				this.tokens.length > 0 ||
+				(this.calcInput != null && !this.$util.isEmpty(this.calcInput))
+			const orderOnly =
+				opts.orderOnly !== undefined ? !!opts.orderOnly : !hasCalc
+			const expr = this.getExprDisplayString(true)
+			let detailText = orderOnly
+				? '订单 ' + this.formatMoney(total)
+				: expr || '订单 ' + this.formatMoney(total)
+			if (!orderOnly && hasCalc && this.isDiscountActive && expr) {
+				detailText =
+					expr +
+					'(原价' +
+					this.formatMoney(this.stackTotal) +
+					' ' +
+					this.formatDiscountZheLabel(this.discountZhe) +
+					'→' +
+					this.formatMoney(total) +
+					')'
+			}
+			const row = this.normalizeHistoryRow(
+				{
+					id: Date.now() + '-' + Math.random().toString(16).slice(2, 10),
+					tokens: this.cloneTokens(this.tokens),
+					calcInput: this.calcInput != null ? String(this.calcInput) : '',
+					total,
+					timeLabel,
+					detailText,
+					cashSnapshot: Number(this.cash) || 0,
+					priceSnapshot: Number(this.price) || 0,
+					orderOnly,
+					source: opts.source === 'stash' ? 'stash' : 'submit',
+					discountZhe: parseFloat(Number(this.discountZhe).toFixed(2)),
+				},
+				0
+			)
+			this.calcHistory.unshift(row)
+			if (this.calcHistory.length > this.maxHistory) this.calcHistory.pop()
+			this.persistCalcHistoryToStorage()
+		},
+		stashCurrent() {
+			this.tapVoice()
+			const hasCalc =
+				this.tokens.length > 0 ||
+				(this.calcInput != null && !this.$util.isEmpty(this.calcInput))
+			const st = this.stackTotal
+			const orderP = Number(this.price)
+			let total = null
+			let orderOnly = false
+			if (hasCalc) {
+				if (!Number.isFinite(st) || isNaN(st) || st <= 0) {
+					this.$msg('请先完成有效算式')
+					return
+				}
+				const payable = this.stackDiscountedAmount
+				if (!Number.isFinite(payable) || isNaN(payable) || payable <= 0) {
+					this.$msg('请先完成有效算式')
+					return
+				}
+				total = payable
+				orderOnly = false
+			} else if (!isNaN(orderP) && orderP > 0) {
+				total = orderP
+				orderOnly = true
+			} else {
+				this.$msg('没有可暂存的内容')
+				return
+			}
+			this.pushHistorySnapshot(total, { source: 'stash', orderOnly })
+			this.$msg('已暂存')
+			this.clearStack()
+			this.keyboardTarget = 'calc'
+			this.checkType = 3
+		},
+		clearStack() {
+			this.tokens = []
+			this.calcInput = ''
+			this.resetActiveDiscountToNone()
+		},
+		tapClearCalculator() {
+			this.tapVoice()
+			this.clearStack()
+			this.keyboardTarget = 'calc'
+			this.checkType = 3
+			this.price = 0
+		},
+		restoreHistoryDiscountFromRow(row) {
+			let z = 10
+			const raw = row && row.discountZhe
+			if (raw != null && raw !== '' && !isNaN(Number(raw))) {
+				const n = Number(raw)
+				if (n > 0 && n <= 10) z = parseFloat(n.toFixed(2))
+			}
+			if (z >= 10 - 1e-6) {
+				this.discountZhe = 10
+				return
+			}
+			const ok = this.customDiscountZheList.some((x) => Math.abs(Number(x) - z) < 1e-4)
+			this.discountZhe = ok ? z : 10
+		},
+		applyHistoryRow(row) {
+			if (!row) return
+			this.restoreHistoryDiscountFromRow(row)
+			const ci = row.calcInput != null ? String(row.calcInput) : ''
+			const hasTok = row.tokens && row.tokens.length > 0
+			const hasLegacy = row.addends && row.addends.length
+			const restoreCash = () => {
+				this.$nextTick(() => {
+					if (row.cashSnapshot != null && !isNaN(Number(row.cashSnapshot))) {
+						this.cash = Number(row.cashSnapshot)
+					}
+				})
+			}
+			if (hasLegacy) {
+				this.tokens = this.legacyAddendsToTokens(row.addends)
+				this.calcInput = ci
+				this.keyboardTarget = 'calc'
+				this.checkType = 3
+				restoreCash()
+				return
+			}
+			if (hasTok) {
+				this.tokens = this.cloneTokens(row.tokens)
+				this.calcInput = ci
+				this.keyboardTarget = 'calc'
+				this.checkType = 3
+				restoreCash()
+				return
+			}
+			if (!this.$util.isEmpty(ci)) {
+				this.tokens = []
+				this.calcInput = ci
+				this.keyboardTarget = 'calc'
+				this.checkType = 3
+				restoreCash()
+				return
+			}
+			const orderAmt =
+				row.priceSnapshot != null &&
+				!isNaN(Number(row.priceSnapshot)) &&
+				Number(row.priceSnapshot) > 0
+					? Number(row.priceSnapshot)
+					: Number(row.total)
+			if (!isNaN(orderAmt) && orderAmt > 0) {
+				this.tokens = []
+				this.calcInput = ''
+				this.price = orderAmt
+				this.cash =
+					row.cashSnapshot != null && !isNaN(Number(row.cashSnapshot))
+						? Number(row.cashSnapshot)
+						: 0
+				this.keyboardTarget = 'order'
+				this.checkType = 1
+				return
+			}
+		},
+		onNumpadTap(e) {
+			this.tapVoice()
+			const ds = e.currentTarget.dataset || {}
+			const action = ds.action
+			const char = ds.char
+			if (action === 'calc-add') {
+				this.pressCalcOp('+')
+				return
+			}
+			if (action === 'calc-sub') {
+				this.pressCalcOp('−')
+				return
+			}
+			if (action === 'calc-mul') {
+				this.pressCalcOp('×')
+				return
+			}
+			if (action === 'calc-div') {
+				this.pressCalcOp('÷')
+				return
+			}
+			if (action === 'backspace') {
+				this.typing({ backspace: true })
+			} else if (char !== undefined && char !== null && char !== '') {
+				this.typing({ backspace: false, char: String(char) })
+			}
+		},
+		typing(e) {
+			const field = this.keyboardTarget
+			if (e.backspace) {
+				if (field === 'calc') {
+					if (this.calcInput && String(this.calcInput).length > 0) {
+						this.calcInput = String(this.calcInput).substring(0, String(this.calcInput).length - 1)
+					} else if (this.tokens.length > 0) {
+						const last = this.tokens[this.tokens.length - 1]
+						if (last.type === 'op') this.tokens.pop()
+						else if (last.s.length > 1)
+							last.s = String(last.s).substring(0, String(last.s).length - 1)
+						else this.tokens.pop()
+					}
+				} else if (field === 'order') {
+					if (this.checkType === 1) this.price = ''
+					else if (String(this.price).length > 0)
+						this.price = String(this.price).substring(0, String(this.price).length - 1)
+				} else if (field === 'cash') {
+					if (!this.showCashPayCols) return
+					if (this.checkType === 2) this.cash = ''
+					else if (String(this.cash).length > 0)
+						this.cash = String(this.cash).substring(0, String(this.cash).length - 1)
+				}
+			} else {
+				const ch = e.char
+				if (field === 'calc') {
+					if (this.$util.isEmpty(this.calcInput) || this.checkType === 3) this.calcInput = ch
+					else {
+						if (ch === '.' && String(this.calcInput).indexOf('.') >= 0) return
+						this.calcInput += ch
+					}
+				} else if (field === 'order') {
+					if (this.$util.isEmpty(this.price) || this.checkType === 1) this.price = ch
+					else {
+						if (ch === '.' && String(this.price).indexOf('.') >= 0) return
+						this.price += ch
+					}
+				} else if (field === 'cash') {
+					if (!this.showCashPayCols) return
+					if (this.$util.isEmpty(this.cash) || this.checkType === 2) this.cash = ch
+					else {
+						if (ch === '.' && String(this.cash).indexOf('.') >= 0) return
+						this.cash += ch
+					}
+				}
+			}
+			this.checkType = 0
+		},
+		submitCashierPay() {
+			this._submitCashierPayCore()
+		},
+		_resolveXjPayload() {
+			try {
+				if (Number(this.customId) > 0) {
+					return uni.getStorageSync('xj' + this.customId)
+				}
+			} catch (e) {}
+			try {
+				return uni.getStorageSync('xj')
+			} catch (e2) {}
+			return null
+		},
+		_submitCashierPayCore() {
+			this.tapVoice()
+			if (this.keyboardTarget === 'calc') {
+				const st = this.stackTotal
+				if (!Number.isFinite(st) || isNaN(st)) {
+					this.$msg('算式无效')
+					return
+				}
+				this.price = this.stackDiscountedAmount
+			}
+			const p = Number(this.price)
+			if (isNaN(p) || p <= 0) {
+				this.$msg('金额须大于 0')
+				return
+			}
+			if (this.selHasPay === 2 && (!this.customId || Number(this.customId) <= 0)) {
+				this.$msg('请先选择客户')
+				return
+			}
+			if (this.showCashPayCols) {
+				const cx = Number(this.cash) || 0
+				if (cx <= 0) {
+					this.$msg('请输入收到的现金')
+					return
+				}
+				if (Number(this.zl) < 0) {
+					this.$msg('现金不足')
+					return
+				}
+			}
+			const hasCalc =
+				this.tokens.length > 0 ||
+				(this.calcInput != null && !this.$util.isEmpty(this.calcInput))
+			const cashSubmit = this.showCashPayCols ? (Number(this.cash) || 0) : 0
+			const emitHasPay = this.selHasPay
+			const emitPayWay = this.selHasPay === 1 ? this.selPayWay : 0
+			this.pushHistorySnapshot(p, {
+				source: 'submit',
+				orderOnly: !hasCalc,
+			})
+			const orderName = (this.name != null ? String(this.name) : '').trim()
+			const formPayload = {
+				...SETTLE_FORM_DEFAULTS,
+				hasPay: emitHasPay,
+				payWay: emitPayWay,
+				modifyPrice: p,
+				cash: cashSubmit,
+				orderName,
+				gatheringPrice: p,
+			}
+			const xjRaw = this._resolveXjPayload()
+			const payload = {
+				...formPayload,
+				isCashier: 1,
+				customId: Number(this.customId) || 0,
+				xj: JSON.stringify(xjRaw),
+				version: 3,
+				zjGathering: 1,
+				dealPrice: 1,
+				getGoods: 1,
+				product: '',
+				payWay: Number(formPayload.hasPay) === 1 ? (formPayload.payWay != null ? formPayload.payWay : 0) : 0,
+			}
+			uni.showLoading({ mask: true })
+			createOrder(payload)
+				.then((res) => {
+					uni.hideLoading()
+					if (res.code != 1) {
+						return
+					}
+					if (res.data && res.data.diff && !this.$util.isEmpty(res.data.diff)) {
+						this.$msg('存在价差条目,请到开单页处理')
+						return
+					}
+					let navigatedPay = false
+					if (res.data && res.data.status == 1) {
+						const id = res.data.id
+						const orderSn = res.data.orderSn
+						const actPrice = res.data.actPrice != null ? res.data.actPrice : p
+						if (formPayload.hasPay == 0) {
+							navigatedPay = true
+							this.$util.pageTo({
+								url: '/admin/billing/toPay',
+								type: 2,
+								query: { orderId: id, orderSn: orderSn, actPrice: actPrice },
+							})
+						} else {
+							navigatedPay = true
+							this.$util.pageTo({
+								url: '/admin/billing/result',
+								type: 2,
+								query: { orderId: id, orderSn: orderSn, title: '收款成功' },
+							})
+						}
+					} else if (!navigatedPay) {
+						uni.showToast({ title: '操作成功', icon: 'success' })
+					}
+					this.price = 0
+					this.checkType = 3
+					this.keyboardTarget = 'calc'
+					this.cash = 0
+					this.selHasPay = 0
+					this.selPayWay = 0
+					this.tokens = []
+					this.calcInput = ''
+					this.resetActiveDiscountToNone()
+				})
+				.catch(() => {
+					uni.hideLoading()
+				})
+		},
+	},
+}
+</script>
+
+<style lang="scss" scoped>
+.calc-page {
+	height: 100%;
+	background: #f4f6f8;
+	box-sizing: border-box;
+	display: flex;
+	flex-direction: column;
+	overflow: hidden;
+}
+
+/* 上 4/7 内容区,下 3/7 数字键区(随屏高同比伸缩) */
+.calc-scroll {
+	flex: 4;
+	flex-basis: 0;
+	min-height: 0;
+	width: 100%;
+}
+
+.calc-sheet {
+	padding: 20upx 24upx 0;
+	box-sizing: border-box;
+}
+
+.calc-card-wrap {
+	background: transparent;
+}
+
+.calc-section {
+	position: relative;
+	border: 2upx solid #e5e9ef;
+	border-radius: 16upx;
+	padding: 20upx 20upx 24upx;
+	background: #fff;
+	box-shadow: 0 4upx 20upx rgba(0, 0, 0, 0.04);
+}
+
+.calc-section-active {
+	border-color: #09c567;
+	background: linear-gradient(180deg, #f6fff9 0%, #fff 52%);
+}
+
+.calc-actions-top {
+	display: flex;
+	flex-direction: row;
+	align-items: center;
+	justify-content: space-between;
+	margin-bottom: 16upx;
+}
+
+.calc-section-title {
+	font-size: 28upx;
+	font-weight: 600;
+	color: #1a1a1a;
+}
+
+.discount-entry {
+	width: 64upx;
+	height: 64upx;
+	border-radius: 12upx;
+	background: rgba(255, 251, 245, 0.98);
+	border: 2upx solid #f0dcc8;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+
+.discount-entry-hover {
+	opacity: 0.82;
+}
+
+.discount-entry-glyph {
+	font-size: 26upx;
+	font-weight: 700;
+	color: #d97706;
+}
+
+.history-entry {
+	width: 64upx;
+	height: 64upx;
+	border-radius: 12upx;
+	background: rgba(248, 255, 248, 0.95);
+	border: 2upx solid #c8ecd8;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+
+.history-entry-hover {
+	opacity: 0.82;
+}
+
+.history-entry-icon {
+	font-size: 34upx;
+	color: #09c567;
+}
+
+.calc-formula {
+	min-height: 88upx;
+	font-size: 38upx;
+	color: #333;
+	text-align: center;
+	line-height: 1.45;
+	word-break: break-all;
+	padding: 0 8upx;
+}
+
+.calc-formula-inner {
+	display: flex;
+	flex-direction: row;
+	flex-wrap: wrap;
+	align-items: center;
+	justify-content: center;
+}
+
+.calc-placeholder {
+	font-size: 32upx;
+	color: #9aa4b2;
+}
+
+.calc-num {
+	font-size: 42upx;
+	font-weight: 600;
+	color: #1a1a1a;
+}
+
+.calc-op {
+	font-size: 42upx;
+	font-weight: 700;
+	color: #09c567;
+}
+
+.calc-total-row {
+	display: flex;
+	flex-direction: row;
+	align-items: baseline;
+	justify-content: center;
+	margin-top: 20upx;
+}
+
+.calc-total-label {
+	font-size: 26upx;
+	color: #9aa4b2;
+	margin-right: 12upx;
+}
+
+.calc-total-val {
+	font-size: 44upx;
+	font-weight: 700;
+	color: #09c567;
+	line-height: 1.2;
+}
+
+.calc-discount-row {
+	display: flex;
+	flex-direction: row;
+	align-items: center;
+	justify-content: center;
+	margin-top: 12upx;
+}
+
+.calc-discount-label {
+	font-size: 24upx;
+	color: #d97706;
+	margin-right: 10upx;
+}
+
+.calc-discount-val {
+	font-size: 34upx;
+	font-weight: 700;
+	color: #d97706;
+}
+
+.customer-strip {
+	display: flex;
+	flex-direction: row;
+	align-items: center;
+	background: #fff;
+	border-radius: 14upx;
+	padding: 32upx 28upx;
+	min-height: 94upx;
+	box-sizing: border-box;
+	margin-top: 12upx;
+	border: 2upx solid #e5e9ef;
+}
+
+.customer-strip-label {
+	font-size: 34upx;
+	color: #9aa4b2;
+	flex-shrink: 0;
+}
+
+.customer-strip-name {
+	flex: 1;
+	min-width: 0;
+	font-size: 34upx;
+	font-weight: 600;
+	color: #1a1a1a;
+	margin-left: 16upx;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+}
+
+.customer-strip-action {
+	font-size: 32upx;
+	color: #09c567;
+	flex-shrink: 0;
+	margin-left: 12upx;
+}
+
+.rcv-pay-block {
+	margin-top: 12upx;
+	padding: 22upx 20upx;
+	background: #fff;
+	border-radius: 14upx;
+	border: 2upx solid #e8edf3;
+	box-sizing: border-box;
+}
+
+.rcv-pay-title {
+	font-size: 30upx;
+	color: #5c6573;
+	text-align: left;
+	margin-bottom: 12upx;
+	font-weight: 500;
+}
+
+.rcv-pay-title-sub {
+	margin-top: 16upx;
+}
+
+.rcv-sub-block {
+	margin-top: 4upx;
+}
+
+.rcv-item-row {
+	display: flex;
+	flex-direction: row;
+	flex-wrap: wrap;
+	align-items: center;
+}
+
+.rcv-item-chip {
+	min-width: 140upx;
+	padding: 0 20upx;
+	height: 68upx;
+	line-height: 68upx;
+	text-align: center;
+	font-size: 28upx;
+	border: 2upx solid #e8edf3;
+	color: #3d4654;
+	border-radius: 10upx;
+	background: #fafbfc;
+	box-sizing: border-box;
+	display: inline-flex;
+	align-items: center;
+	justify-content: center;
+}
+
+.rcv-item-chip {
+	margin-right: 18upx;
+	margin-bottom: 12upx;
+}
+
+.rcv-item-chip.active {
+	background: #09c567;
+	color: #fff;
+	border-color: #09c567;
+}
+
+.pay-cash-panel {
+	display: flex;
+	flex-direction: row;
+	align-items: stretch;
+	margin-top: 16upx;
+}
+
+.pay-cash-item {
+	flex: 1;
+	min-width: 0;
+	background: #fff;
+	border: 2upx solid #e8edf3;
+	border-radius: 14upx;
+	padding: 22upx 16upx;
+	box-sizing: border-box;
+	text-align: center;
+}
+
+.pay-cash-item.pay-cash-item-change {
+	border-style: dashed;
+}
+
+.pay-cash-item.pay-cash-item-change {
+	margin-left: 12upx;
+}
+
+.pay-cash-desc {
+	display: block;
+	font-size: 24upx;
+	color: #9aa4b2;
+	margin-bottom: 10upx;
+}
+
+.pay-cash-amt {
+	font-size: 32upx;
+	font-weight: 600;
+	color: #1a1a1a;
+}
+
+.pay-cash-amt-selected {
+	color: #fff;
+	background: #09c567;
+	padding: 8upx 16upx;
+	border-radius: 10upx;
+	display: inline-block;
+}
+
+.pay-change-line {
+	display: flex;
+	justify-content: center;
+	margin-bottom: 8upx;
+}
+
+.lbl-ok {
+	font-size: 24upx;
+	color: #09c567;
+}
+.lbl-bad {
+	font-size: 24upx;
+	color: #e54545;
+}
+.lbl-neu {
+	font-size: 24upx;
+	color: #c5ccd6;
+}
+
+.amt-ok {
+	font-size: 34upx;
+	font-weight: 700;
+	color: #09c567;
+	display: block;
+}
+.amt-bad {
+	font-size: 34upx;
+	font-weight: 700;
+	color: #e54545;
+	display: block;
+}
+.amt-neu {
+	font-size: 30upx;
+	color: #c5ccd6;
+	display: block;
+}
+
+.pay-submit-row {
+	display: flex;
+	flex-direction: row;
+	margin-top: 20upx;
+	margin-bottom: 8upx;
+}
+
+.pay-submit-btn {
+	flex: 1;
+	text-align: center;
+	font-size: 30upx;
+	padding: 30upx 22upx;
+	background: #09c567;
+	color: #fff;
+	border-radius: 12upx;
+	font-weight: 600;
+	box-sizing: border-box;
+	border: 2upx solid #09c567;
+}
+
+.pay-submit-btn-outline {
+	flex: 1;
+	background: #fff;
+	color: #09c567;
+	border: 2upx solid #09c567;
+	font-weight: 600;
+	border-radius: 12upx;
+	text-align: center;
+	font-size: 30upx;
+	padding: 30upx 22upx;
+	box-sizing: border-box;
+	margin-right: 12upx;
+}
+
+.safe-bottom-spacer {
+	height: 24upx;
+}
+
+.numpad-dock {
+	flex: 3;
+	flex-basis: 0;
+	min-height: 0;
+	width: 100%;
+	display: flex;
+	flex-direction: column;
+	box-sizing: border-box;
+	background: linear-gradient(180deg, transparent 0%, #eceef1 22%, #dcdfe4 100%);
+	padding: 12upx 16upx 10upx;
+	box-shadow: 0 -4upx 24upx rgba(0, 0, 0, 0.06);
+}
+
+.safe-area-padding {
+	padding-bottom: calc(10upx + constant(safe-area-inset-bottom));
+	padding-bottom: calc(10upx + env(safe-area-inset-bottom));
+}
+
+.numpad {
+	width: 100%;
+	flex: 1;
+	min-height: 0;
+	display: flex;
+	flex-direction: column;
+	background: #e2e5ea;
+	border-radius: 14upx;
+	padding: 10upx;
+	box-sizing: border-box;
+}
+
+.numpad-grid-wrap {
+	flex: 1;
+	min-width: 0;
+	min-height: 0;
+	display: grid;
+	grid-template-columns: 1fr 1fr 1fr 25%;
+	grid-template-rows: repeat(5, minmax(0, 1fr));
+	box-sizing: border-box;
+	align-items: stretch;
+	justify-items: stretch;
+	width: 100%;
+}
+
+/* 小程序端 grid 子项 margin 常与 100% 宽高组合失效 → 用 cell 的内边距留白,不配 gap,兼容微信与 App */
+.numpad-cell {
+	box-sizing: border-box;
+	min-width: 0;
+	min-height: 0;
+	overflow: visible;
+	display: flex;
+	flex-direction: column;
+	align-items: stretch;
+	padding-right: 10upx;
+	padding-bottom: 8upx;
+}
+
+.numpad-cell-op {
+	padding-right: 0;
+}
+
+.numpad-cell-mul {
+	grid-column: 4;
+	grid-row: 1;
+}
+
+.numpad-cell-add {
+	grid-column: 4;
+	grid-row: 2 / span 2;
+}
+
+.numpad-cell-sub {
+	grid-column: 4;
+	grid-row: 4;
+}
+
+.numpad-cell-div {
+	grid-column: 4;
+	grid-row: 5;
+}
+
+.numpad-cell-actions-row {
+	grid-column: 1 / span 3;
+	grid-row: 5;
+	flex-direction: row;
+	align-items: stretch;
+}
+
+.numpad-grid-wrap .numpad-cell-actions-row > .numpad-key {
+	flex: 1;
+	width: auto;
+	min-width: 0;
+	min-height: 0;
+	align-self: stretch;
+}
+
+.numpad-grid-wrap .numpad-cell-actions-row > .numpad-key + .numpad-key {
+	margin-left: 10upx;
+}
+
+.numpad-key {
+	min-width: 0;
+	min-height: 0;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	text-align: center;
+	font-size: 46upx;
+	font-weight: 600;
+	margin-left: 0;
+	color: #1a2430;
+	background: linear-gradient(180deg, #fff 0%, #f5f7fa 100%);
+	border-radius: 12upx;
+	box-shadow: 0 3upx 0 #bdc3c9;
+}
+
+.numpad-grid-wrap .numpad-cell > .numpad-key {
+	flex: 1;
+	width: 100%;
+	min-width: 0;
+	min-height: 0;
+	align-self: stretch;
+}
+
+.numpad-key:active {
+	opacity: 0.88;
+	transform: translateY(1px);
+	box-shadow: 0 1upx 0 #a8afb8;
+}
+
+.numpad-grid-wrap .numpad-key.numpad-key-op:not(.numpad-key-op-add) {
+	font-size: 46upx;
+	font-weight: 600;
+	background: linear-gradient(180deg, #fff 0%, #f0f2f5 100%);
+	color: #1a2430;
+	box-shadow: 0 3upx 0 #bdc3c9;
+}
+
+.numpad-grid-wrap .numpad-key.numpad-key-op-add {
+	font-size: 48upx;
+	font-weight: 700;
+	background: #52b384;
+	color: #063d2b;
+	box-shadow: 0 4upx 0 #388f65;
+}
+
+.numpad-key-muted {
+	color: #4a5666;
+	font-size: 50upx;
+}
+
+.numpad-key-text {
+	font-size: 34upx;
+	font-weight: 600;
+}
+
+.numpad-key-action-muted {
+	font-size: 32upx !important;
+	color: #c0392b !important;
+	background: #eac997;
+	box-shadow: 0 3upx 0 #b9c2cf !important;
+}
+
+.numpad-key-danger {
+	background: #eac997;
+	color: black !important;
+	box-shadow: 0 3upx 0 #e0abab !important;
+}
+
+.mob-popup-wrap {
+	width: 86vw;
+	max-width: 640upx;
+	max-height: 70vh;
+	display: flex;
+	flex-direction: column;
+	border-radius: 16upx;
+	overflow: hidden;
+	background: #fff;
+}
+
+.mob-popup-hd {
+	display: flex;
+	flex-direction: row;
+	align-items: center;
+	justify-content: space-between;
+	padding: 24upx 28upx;
+	border-bottom: 2upx solid #eef1f6;
+	flex-shrink: 0;
+}
+
+.mob-popup-title {
+	font-size: 32upx;
+	font-weight: bold;
+	color: #1a1a1a;
+}
+
+.mob-popup-close {
+	font-size: 28upx;
+	color: #09c567;
+	padding: 8upx 16upx;
+}
+
+.mob-popup-body {
+	max-height: 46vh;
+	box-sizing: border-box;
+	padding: 0 12upx 12upx;
+}
+
+.discount-toolbar {
+	flex-shrink: 0;
+	padding: 20upx 24upx 22upx;
+	border-bottom: 2upx solid #f0f2f5;
+	box-sizing: border-box;
+}
+
+.discount-custom-row {
+	display: flex;
+	flex-direction: row;
+	align-items: center;
+}
+
+.discount-custom-input {
+	flex: 1;
+	min-width: 0;
+	height: 72upx;
+	line-height: 72upx;
+	padding: 0 20upx;
+	font-size: 28upx;
+	border: 2upx solid #e8edf3;
+	border-radius: 10upx;
+	background: #fff;
+	box-sizing: border-box;
+}
+
+.discount-custom-add-btn {
+	flex-shrink: 0;
+	margin-left: 14upx;
+	padding: 0 28upx;
+	height: 72upx;
+	line-height: 72upx;
+	font-size: 28upx;
+	color: #fff;
+	background: #09c567;
+	border-radius: 10upx;
+	text-align: center;
+	box-sizing: border-box;
+	font-weight: 500;
+}
+
+.discount-toolbar-hint {
+	display: block;
+	font-size: 24upx;
+	color: #09c567;
+	line-height: 1.45;
+	padding: 14upx 6upx 0;
+}
+
+.discount-toolbar-hint-muted {
+	color: #9aa4b2;
+}
+
+.discount-options-scroll {
+	padding: 20upx 20upx 28upx;
+	box-sizing: border-box;
+	max-height: 44vh;
+}
+
+.discount-option {
+	display: flex;
+	flex-direction: row;
+	align-items: center;
+	justify-content: space-between;
+	padding: 24upx 20upx;
+	border: 2upx solid #e8edf3;
+	border-radius: 12upx;
+	margin-bottom: 12upx;
+	background: #fafbfc;
+	box-sizing: border-box;
+}
+
+.discount-option:last-child {
+	margin-bottom: 0;
+}
+
+.discount-option-active {
+	border-color: #09c567;
+	background: #f0fff5;
+}
+
+.discount-option-text {
+	font-size: 30upx;
+	color: #1a2430;
+}
+
+.discount-option-check {
+	font-size: 30upx;
+	color: #09c567;
+	font-weight: bold;
+}
+
+.discount-option-with-del {
+	padding-right: 12upx;
+}
+
+.discount-option-main {
+	flex: 1;
+	min-width: 0;
+	display: flex;
+	flex-direction: row;
+	align-items: center;
+	justify-content: space-between;
+	padding-right: 12upx;
+	box-sizing: border-box;
+}
+
+.discount-option-del {
+	flex-shrink: 0;
+	font-size: 26upx;
+	color: #e54545;
+	padding: 8upx 12upx;
+}
+
+.history-item {
+	display: flex;
+	flex-direction: row;
+	align-items: center;
+	padding: 18upx 12upx;
+	border-bottom: 2upx solid #f0f3f8;
+}
+
+.history-item-body {
+	flex: 1;
+	min-width: 0;
+}
+
+.history-del {
+	flex-shrink: 0;
+	font-size: 26upx;
+	color: #e54545;
+	padding: 10upx 16upx;
+	margin-left: 12upx;
+}
+
+.history-line {
+	display: flex;
+	flex-direction: row;
+	justify-content: space-between;
+	margin-bottom: 8upx;
+}
+
+.history-time {
+	font-size: 24upx;
+	color: #9aa4b2;
+}
+
+.history-total {
+	font-size: 24upx;
+	color: #09c567;
+	font-weight: 600;
+}
+
+.history-detail {
+	font-size: 24upx;
+	color: #5c6573;
+	word-break: break-all;
+	line-height: 1.35;
+}
+
+.history-empty {
+	text-align: center;
+	font-size: 26upx;
+	color: #c5ccd6;
+	padding: 60upx 0;
+}
+</style>

+ 6 - 0
hdApp/src/pages.json

@@ -47,6 +47,12 @@
 				{"path": "detail","style": {"navigationBarTitleText": "详情","enablePullDownRefresh": false}}
 			]
 		},
+		{
+			"root": "admin/tools",
+			"pages": [
+				{"path": "calculator","style": {"navigationBarTitleText": "计算器","enablePullDownRefresh": false}}
+			]
+		},
 		{
 			"root": "admin/ptItem",
 			"pages": [

+ 11 - 4
hdPad/src/pages/home/components/selectPrice.vue

@@ -152,7 +152,9 @@
                                 <view class="numpad-key numpad-key-op" data-action="calc-add" @tap="onNumpadTap">+</view>
                                 <view class="numpad-key numpad-key-op" data-action="calc-sub" @tap="onNumpadTap">−</view>
                                 <view class="numpad-key numpad-key-op" data-action="calc-div" @tap="onNumpadTap">÷</view>
-                                <view class="numpad-key numpad-key-op numpad-key-op-muted" data-action="calc-clear" @tap="onNumpadTap">清空</view>
+                                <view class="numpad-key numpad-key-op numpad-key-op-muted" data-action="calc-clear" @tap="onNumpadTap">
+                                    <text class="numpad-key-text numpad-key-text-clear">清空</text>
+                                </view>
                             </view>
                         </view>
                     </view>
@@ -1939,13 +1941,18 @@ export default {
 .numpad-op-col .numpad-key-op + .numpad-key-op {
     margin-top: 8upx;
 }
+.numpad-op-col .numpad-key-op.numpad-key-op-muted {
+    font-size: 14upx;
+}
 .numpad-key-op-muted {
-    font-size: 13upx;
+    font-size: 14upx;
     font-weight: 600;
     background: #e8e8e8;
-    color: #333333;
     box-shadow: 0 2upx 0 #bdbdbd;
 }
+.numpad-key-text-clear {
+    color: #d32f2f;
+}
 .keyboard-body_box .numpad-op-col .numpad-key-op {
     min-height: 0;
 }
@@ -2012,7 +2019,7 @@ export default {
 }
 .numpad-key-danger {
     background: #ffeded;
-    color: #cc4444;
+    color: #222222;
     box-shadow: 0 2upx 0 #ddb8b8;
 }
 .numpad-key-action {