Просмотр исходного кода

折扣功能组件化:折扣项支持新增、删除与排序

shizhongqi 3 месяцев назад
Родитель
Сommit
d88e6915ea

+ 418 - 0
ghsPad/src/pages/home/components/discount-panel.vue

@@ -0,0 +1,418 @@
+<template>
+    <view class="discount-panel" @tap.stop>
+        <view class="discount-header">
+            <text class="discount-title">选择折扣</text>
+            <view class="header-actions">
+                <view class="edit-btn" :class="{ active: editMode }" @tap.stop="toggleEdit">
+                    {{editMode ? '完成' : '编辑'}}
+                </view>
+            </view>
+        </view>
+
+        <view v-if="editMode && showAdd" class="add-box">
+            <view class="picker-row">
+                <picker mode="selector" :range="numberOptions" :value="integerIndex" @change="changeInteger">
+                    <view class="picker-value">{{numberOptions[integerIndex]}}</view>
+                </picker>
+                <text class="dot">.</text>
+                <picker mode="selector" :range="numberOptions" :value="decimalIndex" @change="changeDecimal">
+                    <view class="picker-value">{{numberOptions[decimalIndex]}}</view>
+                </picker>
+                <text class="discount-unit">折</text>
+            </view>
+            <button class="add-confirm" @tap.stop="addDiscount">添加</button>
+        </view>
+
+        <view class="discount-list">
+            <view
+                v-for="(item, index) in innerList"
+                :key="item.value"
+                class="discount-item"
+                :class="{
+                    active: !editMode && Number(selectedValue) === Number(item.value),
+                    dragging: draggingIndex === index,
+                    'add-discount-item': editMode && isNoDiscount(item)
+                }"
+                @tap.stop="selectDiscount(item)"
+            >
+                <text class="discount-name">{{editMode && isNoDiscount(item) ? '+' : item.name}}</text>
+                <view v-if="editMode && !isNoDiscount(item)" class="item-actions">
+                    <view class="delete-btn" @tap.stop="deleteDiscount(index)">×</view>
+                    <view
+                        class="drag-handle"
+                        @tap.stop
+                        @touchstart.stop="startDrag(index, $event)"
+                        @touchmove.stop.prevent="moveDrag"
+                        @touchend.stop="endDrag"
+                        @touchcancel.stop="endDrag"
+                    >
+                        <text></text>
+                        <text></text>
+                        <text></text>
+                    </view>
+                </view>
+            </view>
+        </view>
+        <view v-if="editMode" class="tips">点击x按钮删除,按住☰按钮拖动排序</view>
+    </view>
+</template>
+
+<script>
+const DEFAULT_LIST = [
+    { name: '7折', value: 0.7 },
+    { name: '8折', value: 0.8 },
+    { name: '9折', value: 0.9 },
+    { name: '不打折', value: 1 },
+]
+
+export default {
+    name: 'DiscountPanel',
+    props: {
+        value: {
+            type: Array,
+            default: () => DEFAULT_LIST
+        },
+        selectedValue: {
+            type: [Number, String],
+            default: 0
+        }
+    },
+    data() {
+        return {
+            innerList: [],
+            numberOptions: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
+            integerIndex: 8,
+            decimalIndex: 5,
+            showAdd: false,
+            editMode: false,
+            draggingIndex: -1,
+            itemRects: []
+        }
+    },
+    mounted() {
+        this.initDiscountList()
+    },
+    watch: {
+        value: {
+            handler(list) {
+                if (Array.isArray(this.innerList) && this.innerList.length > 0) return
+                this.innerList = this.normalizeList(list)
+            },
+            immediate: true,
+            deep: true
+        }
+    },
+    methods: {
+        normalizeList(list) {
+            const source = Array.isArray(list) && list.length > 0 ? list : DEFAULT_LIST
+            return this.keepNoDiscountLast(source.map(item => ({
+                name: item.name,
+                value: Number(item.value)
+            })))
+        },
+        keepNoDiscountLast(list) {
+            const noDiscount = list.find(item => this.isNoDiscount(item)) || { name: '不打折', value: 1 }
+            return list.filter(item => !this.isNoDiscount(item)).concat([noDiscount])
+        },
+        isNoDiscount(item) {
+            return item && Number(item.value) === 1
+        },
+        initDiscountList() {
+            let cacheList = []
+            if (this.$store && this.$store.getters) {
+                cacheList = this.$store.getters.getDiscountList
+            }
+            const nextList = this.normalizeList(Array.isArray(cacheList) && cacheList.length > 0 ? cacheList : DEFAULT_LIST)
+            this.innerList = nextList
+            this.$emit('input', nextList.slice())
+            this.$emit('change-list', nextList.slice())
+        },
+        emitList() {
+            this.innerList = this.keepNoDiscountLast(this.innerList)
+            if (this.$store && this.$store.dispatch) {
+                this.$store.dispatch('setDiscountListCache', this.innerList.slice())
+            }
+            this.$emit('input', this.innerList.slice())
+            this.$emit('change-list', this.innerList.slice())
+        },
+        toggleEdit() {
+            this.editMode = !this.editMode
+            if (!this.editMode) {
+                this.showAdd = false
+                this.endDrag()
+            }
+        },
+        changeInteger(e) {
+            this.integerIndex = Number(e.detail.value)
+        },
+        changeDecimal(e) {
+            this.decimalIndex = Number(e.detail.value)
+        },
+        addDiscount() {
+            const integer = Number(this.numberOptions[this.integerIndex])
+            const decimal = Number(this.numberOptions[this.decimalIndex])
+            const value = Number(((integer * 10 + decimal) / 100).toFixed(2))
+            if (value <= 0) {
+                uni.showToast({ title: '折扣不能为0', icon: 'none' })
+                return
+            }
+            if (this.innerList.some(item => Number(item.value) === value)) {
+                uni.showToast({ title: '该折扣已存在', icon: 'none' })
+                return
+            }
+            const name = decimal === 0 ? `${integer}折` : `${integer}.${decimal}折`
+            const noDiscountIndex = this.innerList.findIndex(item => this.isNoDiscount(item))
+            if (noDiscountIndex === -1) {
+                this.innerList.push({ name, value }, { name: '不打折', value: 1 })
+            } else {
+                this.innerList.splice(noDiscountIndex, 0, { name, value })
+            }
+            this.showAdd = false
+            this.emitList()
+        },
+        deleteDiscount(index) {
+            if (this.isNoDiscount(this.innerList[index])) {
+                return
+            }
+            if (this.innerList.length <= 1) {
+                uni.showToast({ title: '至少保留一个折扣', icon: 'none' })
+                return
+            }
+            uni.showModal({
+                title: '删除折扣',
+                content: `确定删除${this.innerList[index].name}吗?`,
+                success: (res) => {
+                    if (!res.confirm) return
+                    const deleted = this.innerList.splice(index, 1)[0]
+                    this.emitList()
+                    if (deleted && Number(deleted.value) === Number(this.selectedValue)) {
+                        this.$emit('select', this.innerList[0].value)
+                    }
+                }
+            })
+        },
+        selectDiscount(item) {
+            if (this.draggingIndex !== -1) return
+            if (this.editMode && this.isNoDiscount(item)) {
+                this.showAdd = !this.showAdd
+                return
+            }
+            this.$emit('select', item.value)
+        },
+        startDrag(index) {
+            if (!this.editMode || this.isNoDiscount(this.innerList[index])) return
+            this.draggingIndex = index
+            this.queryItemRects()
+        },
+        moveDrag(e) {
+            if (this.draggingIndex === -1) return
+            const touch = e.touches && e.touches[0]
+            if (!touch || this.itemRects.length === 0) return
+            const targetIndex = this.itemRects.findIndex(rect => (
+                touch.clientX >= rect.left &&
+                touch.clientX <= rect.right &&
+                touch.clientY >= rect.top &&
+                touch.clientY <= rect.bottom
+            ))
+            if (
+                targetIndex === -1 ||
+                targetIndex === this.draggingIndex ||
+                this.isNoDiscount(this.innerList[targetIndex])
+            ) return
+            const current = this.innerList.splice(this.draggingIndex, 1)[0]
+            this.innerList.splice(targetIndex, 0, current)
+            this.innerList = this.keepNoDiscountLast(this.innerList)
+            this.draggingIndex = targetIndex
+            this.$nextTick(() => {
+                this.queryItemRects()
+            })
+        },
+        endDrag() {
+            if (this.draggingIndex !== -1) {
+                this.emitList()
+            }
+            this.draggingIndex = -1
+            this.itemRects = []
+        },
+        queryItemRects() {
+            this.$nextTick(() => {
+                uni.createSelectorQuery()
+                    .in(this)
+                    .selectAll('.discount-item')
+                    .boundingClientRect((rects) => {
+                        this.itemRects = Array.isArray(rects) ? rects : []
+                    })
+                    .exec()
+            })
+        }
+    }
+}
+</script>
+
+<style lang="scss" scoped>
+.discount-panel {
+    position: absolute;
+    top: 0;
+    left: 50%;
+    transform: translateX(-50%);
+    width: 190upx;
+    padding: 12upx;
+    background-color: #ffffff;
+    border: 1upx solid #eeeeee;
+    border-radius: 6upx;
+    z-index: 99999999;
+    box-shadow: 0 4upx 16upx rgba(0, 0, 0, 0.12);
+}
+.discount-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 10upx;
+}
+.discount-title {
+    font-size: 12upx;
+    color: #333333;
+    font-weight: bold;
+}
+.header-actions {
+    display: flex;
+    align-items: center;
+}
+.edit-btn {
+    min-width: 34upx;
+    height: 24upx;
+    line-height: 24upx;
+    margin-left: 8upx;
+    padding: 0 6upx;
+    text-align: center;
+    border-radius: 12upx;
+    border: 1upx solid #09C567;
+    color: #09C567;
+    font-size: 10upx;
+}
+.edit-btn.active {
+    color: #ffffff;
+    background-color: #09C567;
+}
+.add-box {
+    padding: 8upx;
+    margin-bottom: 10upx;
+    background-color: #f8f8f8;
+    border-radius: 4upx;
+}
+.picker-row {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+.picker-value {
+    width: 36upx;
+    height: 28upx;
+    line-height: 28upx;
+    text-align: center;
+    border: 1upx solid #dddddd;
+    border-radius: 4upx;
+    background-color: #ffffff;
+    font-size: 12upx;
+}
+.dot,
+.discount-unit {
+    font-size: 12upx;
+    color: #333333;
+    margin-left: 4upx;
+}
+.add-confirm {
+    height: 28upx;
+    line-height: 28upx;
+    margin-top: 8upx;
+    padding: 0;
+    font-size: 12upx;
+    color: #ffffff;
+    background-color: #09C567;
+    border-radius: 4upx;
+}
+.discount-list {
+    display: flex;
+    align-items: center;
+    flex-wrap: wrap;
+}
+.discount-item {
+    width: 48upx;
+    height: 48upx;
+    margin-left: 8upx;
+    margin-bottom: 8upx;
+    border: 1upx solid #999999;
+    border-radius: 24upx;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    position: relative;
+    background-color: #ffffff;
+}
+.discount-item:nth-child(3n + 1) {
+    margin-left: 0;
+}
+.discount-item.active {
+    border-color: #09C567;
+    color: #09C567;
+}
+.discount-item.dragging {
+    opacity: 0.7;
+    background-color: #f0fff8;
+}
+.discount-item.add-discount-item {
+    border-color: #09C567;
+    color: #09C567;
+    font-size: 18upx;
+}
+.discount-name {
+    font-size: 10upx;
+}
+.add-discount-item .discount-name {
+    font-size: 20upx;
+    line-height: 20upx;
+}
+.item-actions {
+    position: absolute;
+    top: -4upx;
+    right: -12upx;
+    display: flex;
+    align-items: center;
+}
+.delete-btn,
+.drag-handle {
+    width: 18upx;
+    height: 18upx;
+    border-radius: 9upx;
+}
+.delete-btn {
+    line-height: 16upx;
+    text-align: center;
+    margin-right: 10upx;
+    background-color: #ff4d4f;
+    color: #ffffff;
+    font-size: 14upx;
+}
+.drag-handle {
+    background-color: #666666;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    margin-right: 12upx;
+}
+.drag-handle text {
+    width: 8upx;
+    height: 1upx;
+    margin-top: 2upx;
+    background-color: #ffffff;
+}
+.drag-handle text:first-child {
+    margin-top: 0;
+}
+.tips {
+    margin-top: 2upx;
+    font-size: 9upx;
+    color: #999999;
+}
+</style>

+ 23 - 14
ghsPad/src/pages/home/components/settlePop.vue

@@ -174,16 +174,12 @@
         </view>
         <view class="line"></view>
         <view class="select-right">
-
-            <view style="position: absolute;background-color: white;z-index: 99999999;" v-if="showDiscount">
-                <view style="display:flex;padding:20upx;justify-content: space-between;align-items:center;flex-wrap:wrap;">
-                    <text v-for="(item, index) in discountList" :key="index" @click="setDiscount(item.value)" 
-                    style="margin-left:10upx;border:1upx solid #999999;font-size:11upx;
-                    width:40upx;height:40upx;border-radius:20upx;justify-content:center;align-items:center;display:flex;">
-                    {{item.name}}
-                    </text>
-                </view>
-            </view>
+            <DiscountPanel
+                v-if="showDiscount"
+                v-model="discountList"
+                :selected-value="discountValue"
+                @select="setDiscount"
+            />
 
             <view class="input-list">
                 <text>商品金额</text>
@@ -196,9 +192,9 @@
             </view>
             <view class="input-list">
                 <text>总金额</text>
-                <text @click="viewDiscount()">
-                    点击打折<text v-if="Number(discountValue)>0 && Number(discountValue)<1">({{ Number(discountValue)*10 }}折)</text>
-                </text>
+                <view class="discount-button" @click.stop="viewDiscount()">
+                    打折<text v-if="Number(discountValue)>0 && Number(discountValue)<1">({{ Number(discountValue)*10 }}折)</text>
+                </view>
                 <text><text>{{totalPrice}}</text></text>
             </view>
 
@@ -234,6 +230,7 @@ import VKeyboard from '@/components/vKeyboard/VKeyboard.vue'
 import TimePicker from '@/components/TimePicker';
 import WeightInput from '@/components/delivery-input/weight-input.vue';
 import RemarkInput from '@/components/delivery-input/remark-input.vue';
+import DiscountPanel from './discount-panel.vue'
 import productMins from "@/mixins/product"
 import { getAllStaff} from '@/api/shop-admin'
 import { quickOrderAllDeliveryQuotes } from '@/api/delivery'
@@ -320,7 +317,7 @@ export default {
             default:0
         }
     },
-    components:{ VKeyboard, TimePicker, WeightInput, RemarkInput },
+    components:{ VKeyboard, TimePicker, WeightInput, RemarkInput, DiscountPanel },
 	mixins: [productMins],
     computed:{
         ...mapGetters(["getLoginInfo"]),
@@ -927,6 +924,18 @@ export default {
                 background:blue;
                 color:white;
             }
+            .discount-button {
+                min-width: 38upx;
+                height: 22upx;
+                padding: 0 8upx;
+                line-height: 22upx;
+                text-align: center;
+                color: #3385ff;
+                border: 1upx solid #3385ff;
+                border-radius: 11upx;
+                background: #eef5ff;
+                box-sizing: border-box;
+            }
         }
         & .input-bottom{
             margin-bottom:10upx;

+ 71 - 0
ghsPad/src/store/modules/discount.js

@@ -0,0 +1,71 @@
+const DISCOUNT_LIST_KEY = 'settleDiscountList'
+
+function normalizeDiscountList(list) {
+	if (!Array.isArray(list) || list.length === 0) {
+		return []
+	}
+	return list
+		.filter(item => item && item.name && Number(item.value) > 0)
+		.map(item => ({
+			name: item.name,
+			value: Number(item.value)
+		}))
+}
+
+function readDiscountList() {
+	try {
+		const cache = uni.getStorageSync(DISCOUNT_LIST_KEY)
+		if (!cache) return []
+		const list = typeof cache === 'string' ? JSON.parse(cache) : cache
+		return normalizeDiscountList(list)
+	} catch (e) {
+		return []
+	}
+}
+
+function writeDiscountList(list) {
+	try {
+		uni.setStorageSync(DISCOUNT_LIST_KEY, JSON.stringify(normalizeDiscountList(list)))
+	} catch (e) {
+		console.log('保存折扣设置失败:', e)
+	}
+}
+
+const state = {
+	discountList: []
+}
+
+const getters = {
+	getDiscountList(state) {
+		if (!Array.isArray(state.discountList) || state.discountList.length === 0) {
+			state.discountList = readDiscountList()
+		}
+		return state.discountList || []
+	}
+}
+
+const actions = {
+	getDiscountListFromCache({ commit }) {
+		const list = readDiscountList()
+		commit('SET_DISCOUNT_LIST', list)
+		return list
+	},
+	setDiscountListCache({ commit }, list) {
+		const nextList = normalizeDiscountList(list)
+		writeDiscountList(nextList)
+		commit('SET_DISCOUNT_LIST', nextList)
+	}
+}
+
+const mutations = {
+	SET_DISCOUNT_LIST(state, list = []) {
+		state.discountList = normalizeDiscountList(list)
+	}
+}
+
+export default {
+	state,
+	getters,
+	actions,
+	mutations
+}

+ 3 - 1
ghsPad/src/store/modules/index.js

@@ -4,6 +4,7 @@ import config from "./config";
 import authorization from "./authorization";
 import product from "./product";
 import merchant from "./merchant";
+import discount from "./discount";
 export default {
 	login,
 	user,
@@ -11,5 +12,6 @@ export default {
 	authorization,
 
 	product, //商品
-	merchant //商家
+	merchant, //商家
+	discount //折扣设置
 };

+ 418 - 0
hdPad/src/pages/home/components/discount-panel.vue

@@ -0,0 +1,418 @@
+<template>
+    <view class="discount-panel" @tap.stop>
+        <view class="discount-header">
+            <text class="discount-title">选择折扣</text>
+            <view class="header-actions">
+                <view class="edit-btn" :class="{ active: editMode }" @tap.stop="toggleEdit">
+                    {{editMode ? '完成' : '编辑'}}
+                </view>
+            </view>
+        </view>
+
+        <view v-if="editMode && showAdd" class="add-box">
+            <view class="picker-row">
+                <picker mode="selector" :range="numberOptions" :value="integerIndex" @change="changeInteger">
+                    <view class="picker-value">{{numberOptions[integerIndex]}}</view>
+                </picker>
+                <text class="dot">.</text>
+                <picker mode="selector" :range="numberOptions" :value="decimalIndex" @change="changeDecimal">
+                    <view class="picker-value">{{numberOptions[decimalIndex]}}</view>
+                </picker>
+                <text class="discount-unit">折</text>
+            </view>
+            <button class="add-confirm" @tap.stop="addDiscount">添加</button>
+        </view>
+
+        <view class="discount-list">
+            <view
+                v-for="(item, index) in innerList"
+                :key="item.value"
+                class="discount-item"
+                :class="{
+                    active: !editMode && Number(selectedValue) === Number(item.value),
+                    dragging: draggingIndex === index,
+                    'add-discount-item': editMode && isNoDiscount(item)
+                }"
+                @tap.stop="selectDiscount(item)"
+            >
+                <text class="discount-name">{{editMode && isNoDiscount(item) ? '+' : item.name}}</text>
+                <view v-if="editMode && !isNoDiscount(item)" class="item-actions">
+                    <view class="delete-btn" @tap.stop="deleteDiscount(index)">×</view>
+                    <view
+                        class="drag-handle"
+                        @tap.stop
+                        @touchstart.stop="startDrag(index, $event)"
+                        @touchmove.stop.prevent="moveDrag"
+                        @touchend.stop="endDrag"
+                        @touchcancel.stop="endDrag"
+                    >
+                        <text></text>
+                        <text></text>
+                        <text></text>
+                    </view>
+                </view>
+            </view>
+        </view>
+        <view v-if="editMode" class="tips">点击x按钮删除,按住☰按钮拖动排序</view>
+    </view>
+</template>
+
+<script>
+const DEFAULT_LIST = [
+    { name: '7折', value: 0.7 },
+    { name: '8折', value: 0.8 },
+    { name: '9折', value: 0.9 },
+    { name: '不打折', value: 1 },
+]
+
+export default {
+    name: 'DiscountPanel',
+    props: {
+        value: {
+            type: Array,
+            default: () => DEFAULT_LIST
+        },
+        selectedValue: {
+            type: [Number, String],
+            default: 0
+        }
+    },
+    data() {
+        return {
+            innerList: [],
+            numberOptions: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
+            integerIndex: 8,
+            decimalIndex: 5,
+            showAdd: false,
+            editMode: false,
+            draggingIndex: -1,
+            itemRects: []
+        }
+    },
+    mounted() {
+        this.initDiscountList()
+    },
+    watch: {
+        value: {
+            handler(list) {
+                if (Array.isArray(this.innerList) && this.innerList.length > 0) return
+                this.innerList = this.normalizeList(list)
+            },
+            immediate: true,
+            deep: true
+        }
+    },
+    methods: {
+        normalizeList(list) {
+            const source = Array.isArray(list) && list.length > 0 ? list : DEFAULT_LIST
+            return this.keepNoDiscountLast(source.map(item => ({
+                name: item.name,
+                value: Number(item.value)
+            })))
+        },
+        keepNoDiscountLast(list) {
+            const noDiscount = list.find(item => this.isNoDiscount(item)) || { name: '不打折', value: 1 }
+            return list.filter(item => !this.isNoDiscount(item)).concat([noDiscount])
+        },
+        isNoDiscount(item) {
+            return item && Number(item.value) === 1
+        },
+        initDiscountList() {
+            let cacheList = []
+            if (this.$store && this.$store.getters) {
+                cacheList = this.$store.getters.getDiscountList
+            }
+            const nextList = this.normalizeList(Array.isArray(cacheList) && cacheList.length > 0 ? cacheList : DEFAULT_LIST)
+            this.innerList = nextList
+            this.$emit('input', nextList.slice())
+            this.$emit('change-list', nextList.slice())
+        },
+        emitList() {
+            this.innerList = this.keepNoDiscountLast(this.innerList)
+            if (this.$store && this.$store.dispatch) {
+                this.$store.dispatch('setDiscountListCache', this.innerList.slice())
+            }
+            this.$emit('input', this.innerList.slice())
+            this.$emit('change-list', this.innerList.slice())
+        },
+        toggleEdit() {
+            this.editMode = !this.editMode
+            if (!this.editMode) {
+                this.showAdd = false
+                this.endDrag()
+            }
+        },
+        changeInteger(e) {
+            this.integerIndex = Number(e.detail.value)
+        },
+        changeDecimal(e) {
+            this.decimalIndex = Number(e.detail.value)
+        },
+        addDiscount() {
+            const integer = Number(this.numberOptions[this.integerIndex])
+            const decimal = Number(this.numberOptions[this.decimalIndex])
+            const value = Number(((integer * 10 + decimal) / 100).toFixed(2))
+            if (value <= 0) {
+                uni.showToast({ title: '折扣不能为0', icon: 'none' })
+                return
+            }
+            if (this.innerList.some(item => Number(item.value) === value)) {
+                uni.showToast({ title: '该折扣已存在', icon: 'none' })
+                return
+            }
+            const name = decimal === 0 ? `${integer}折` : `${integer}.${decimal}折`
+            const noDiscountIndex = this.innerList.findIndex(item => this.isNoDiscount(item))
+            if (noDiscountIndex === -1) {
+                this.innerList.push({ name, value }, { name: '不打折', value: 1 })
+            } else {
+                this.innerList.splice(noDiscountIndex, 0, { name, value })
+            }
+            this.showAdd = false
+            this.emitList()
+        },
+        deleteDiscount(index) {
+            if (this.isNoDiscount(this.innerList[index])) {
+                return
+            }
+            if (this.innerList.length <= 1) {
+                uni.showToast({ title: '至少保留一个折扣', icon: 'none' })
+                return
+            }
+            uni.showModal({
+                title: '删除折扣',
+                content: `确定删除${this.innerList[index].name}吗?`,
+                success: (res) => {
+                    if (!res.confirm) return
+                    const deleted = this.innerList.splice(index, 1)[0]
+                    this.emitList()
+                    if (deleted && Number(deleted.value) === Number(this.selectedValue)) {
+                        this.$emit('select', this.innerList[0].value)
+                    }
+                }
+            })
+        },
+        selectDiscount(item) {
+            if (this.draggingIndex !== -1) return
+            if (this.editMode && this.isNoDiscount(item)) {
+                this.showAdd = !this.showAdd
+                return
+            }
+            this.$emit('select', item.value)
+        },
+        startDrag(index) {
+            if (!this.editMode || this.isNoDiscount(this.innerList[index])) return
+            this.draggingIndex = index
+            this.queryItemRects()
+        },
+        moveDrag(e) {
+            if (this.draggingIndex === -1) return
+            const touch = e.touches && e.touches[0]
+            if (!touch || this.itemRects.length === 0) return
+            const targetIndex = this.itemRects.findIndex(rect => (
+                touch.clientX >= rect.left &&
+                touch.clientX <= rect.right &&
+                touch.clientY >= rect.top &&
+                touch.clientY <= rect.bottom
+            ))
+            if (
+                targetIndex === -1 ||
+                targetIndex === this.draggingIndex ||
+                this.isNoDiscount(this.innerList[targetIndex])
+            ) return
+            const current = this.innerList.splice(this.draggingIndex, 1)[0]
+            this.innerList.splice(targetIndex, 0, current)
+            this.innerList = this.keepNoDiscountLast(this.innerList)
+            this.draggingIndex = targetIndex
+            this.$nextTick(() => {
+                this.queryItemRects()
+            })
+        },
+        endDrag() {
+            if (this.draggingIndex !== -1) {
+                this.emitList()
+            }
+            this.draggingIndex = -1
+            this.itemRects = []
+        },
+        queryItemRects() {
+            this.$nextTick(() => {
+                uni.createSelectorQuery()
+                    .in(this)
+                    .selectAll('.discount-item')
+                    .boundingClientRect((rects) => {
+                        this.itemRects = Array.isArray(rects) ? rects : []
+                    })
+                    .exec()
+            })
+        }
+    }
+}
+</script>
+
+<style lang="scss" scoped>
+.discount-panel {
+    position: absolute;
+    top: 0;
+    left: 50%;
+    transform: translateX(-50%);
+    width: 190upx;
+    padding: 12upx;
+    background-color: #ffffff;
+    border: 1upx solid #eeeeee;
+    border-radius: 6upx;
+    z-index: 99999999;
+    box-shadow: 0 4upx 16upx rgba(0, 0, 0, 0.12);
+}
+.discount-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 10upx;
+}
+.discount-title {
+    font-size: 12upx;
+    color: #333333;
+    font-weight: bold;
+}
+.header-actions {
+    display: flex;
+    align-items: center;
+}
+.edit-btn {
+    min-width: 34upx;
+    height: 24upx;
+    line-height: 24upx;
+    margin-left: 8upx;
+    padding: 0 6upx;
+    text-align: center;
+    border-radius: 12upx;
+    border: 1upx solid #09C567;
+    color: #09C567;
+    font-size: 10upx;
+}
+.edit-btn.active {
+    color: #ffffff;
+    background-color: #09C567;
+}
+.add-box {
+    padding: 8upx;
+    margin-bottom: 10upx;
+    background-color: #f8f8f8;
+    border-radius: 4upx;
+}
+.picker-row {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+.picker-value {
+    width: 36upx;
+    height: 28upx;
+    line-height: 28upx;
+    text-align: center;
+    border: 1upx solid #dddddd;
+    border-radius: 4upx;
+    background-color: #ffffff;
+    font-size: 12upx;
+}
+.dot,
+.discount-unit {
+    font-size: 12upx;
+    color: #333333;
+    margin-left: 4upx;
+}
+.add-confirm {
+    height: 28upx;
+    line-height: 28upx;
+    margin-top: 8upx;
+    padding: 0;
+    font-size: 12upx;
+    color: #ffffff;
+    background-color: #09C567;
+    border-radius: 4upx;
+}
+.discount-list {
+    display: flex;
+    align-items: center;
+    flex-wrap: wrap;
+}
+.discount-item {
+    width: 48upx;
+    height: 48upx;
+    margin-left: 8upx;
+    margin-bottom: 8upx;
+    border: 1upx solid #999999;
+    border-radius: 24upx;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    position: relative;
+    background-color: #ffffff;
+}
+.discount-item:nth-child(3n + 1) {
+    margin-left: 0;
+}
+.discount-item.active {
+    border-color: #09C567;
+    color: #09C567;
+}
+.discount-item.dragging {
+    opacity: 0.7;
+    background-color: #f0fff8;
+}
+.discount-item.add-discount-item {
+    border-color: #09C567;
+    color: #09C567;
+    font-size: 18upx;
+}
+.discount-name {
+    font-size: 10upx;
+}
+.add-discount-item .discount-name {
+    font-size: 20upx;
+    line-height: 20upx;
+}
+.item-actions {
+    position: absolute;
+    top: -4upx;
+    right: -12upx;
+    display: flex;
+    align-items: center;
+}
+.delete-btn,
+.drag-handle {
+    width: 18upx;
+    height: 18upx;
+    border-radius: 9upx;
+}
+.delete-btn {
+    line-height: 16upx;
+    text-align: center;
+    margin-right: 10upx;
+    background-color: #ff4d4f;
+    color: #ffffff;
+    font-size: 14upx;
+}
+.drag-handle {
+    background-color: #666666;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    margin-right: 12upx;
+}
+.drag-handle text {
+    width: 8upx;
+    height: 1upx;
+    margin-top: 2upx;
+    background-color: #ffffff;
+}
+.drag-handle text:first-child {
+    margin-top: 0;
+}
+.tips {
+    margin-top: 2upx;
+    font-size: 9upx;
+    color: #999999;
+}
+</style>

+ 23 - 13
hdPad/src/pages/home/components/settlePop.vue

@@ -176,15 +176,12 @@
 
 
         <view class="select-right">
-            <view style="position: absolute;background-color: white;z-index: 99999999;" v-if="showDiscount">
-                <view style="display:flex;padding:20upx;justify-content: space-between;align-items:center;flex-wrap:wrap;">
-                    <text v-for="(item, index) in discountList" :key="index" @click="setDiscount(item.value)" 
-                    style="margin-left:10upx;border:1upx solid #999999;font-size:11upx;
-                    width:40upx;height:40upx;border-radius:20upx;justify-content:center;align-items:center;display:flex;">
-                    {{item.name}}
-                    </text>
-                </view>
-            </view>
+            <DiscountPanel
+                v-if="showDiscount"
+                v-model="discountList"
+                :selected-value="discountValue"
+                @select="setDiscount"
+            />
 
             <view class="input-list">
                 <text>商品金额</text>
@@ -213,9 +210,9 @@
             </view>
             <view class="input-list">
                 <text>总金额</text>
-                <text @click="viewDiscount()">
-                    点击打折<text v-if="Number(discountValue)>0 && Number(discountValue)<1">({{ Number(discountValue)*10 }}折)</text>
-                </text>
+                <view class="discount-button" @click.stop="viewDiscount()">
+                    打折<text v-if="Number(discountValue)>0 && Number(discountValue)<1">({{ Number(discountValue)*10 }}折)</text>
+                </view>
                 <text><text>{{totalPrice}}</text></text>
             </view>
 
@@ -250,6 +247,7 @@ import TimePicker from '@/components/TimePicker';
 import WeightInput from '@/components/delivery-input/weight-input.vue';
 import RemarkInput from '@/components/delivery-input/remark-input.vue';
 import HbSelect from './hb-select'
+import DiscountPanel from './discount-panel.vue'
 import productMins from "@/mixins/product";
 import { getAllStaff} from '@/api/shop-admin'
 import { merchantDetail } from '@/api/merchant' 
@@ -352,7 +350,7 @@ export default {
             default:0
         }
     },
-    components:{ VKeyboard, TimePicker, WeightInput, RemarkInput, HbSelect },
+    components:{ VKeyboard, TimePicker, WeightInput, RemarkInput, HbSelect, DiscountPanel },
 	mixins: [productMins],
     computed:{
         ...mapGetters(["getLoginInfo"]),
@@ -1077,6 +1075,18 @@ export default {
                 background:blue;
                 color:white;
             }
+            .discount-button {
+                min-width: 38upx;
+                height: 22upx;
+                padding: 0 8upx;
+                line-height: 22upx;
+                text-align: center;
+                color: #09C567;
+                border: 1upx solid #09C567;
+                border-radius: 11upx;
+                background: #F0FFF7;
+                box-sizing: border-box;
+            }
         }
         & .input-bottom{
             margin-bottom:10upx;

+ 71 - 0
hdPad/src/store/modules/discount.js

@@ -0,0 +1,71 @@
+const DISCOUNT_LIST_KEY = 'settleDiscountList'
+
+function normalizeDiscountList(list) {
+	if (!Array.isArray(list) || list.length === 0) {
+		return []
+	}
+	return list
+		.filter(item => item && item.name && Number(item.value) > 0)
+		.map(item => ({
+			name: item.name,
+			value: Number(item.value)
+		}))
+}
+
+function readDiscountList() {
+	try {
+		const cache = uni.getStorageSync(DISCOUNT_LIST_KEY)
+		if (!cache) return []
+		const list = typeof cache === 'string' ? JSON.parse(cache) : cache
+		return normalizeDiscountList(list)
+	} catch (e) {
+		return []
+	}
+}
+
+function writeDiscountList(list) {
+	try {
+		uni.setStorageSync(DISCOUNT_LIST_KEY, JSON.stringify(normalizeDiscountList(list)))
+	} catch (e) {
+		console.log('保存折扣设置失败:', e)
+	}
+}
+
+const state = {
+	discountList: []
+}
+
+const getters = {
+	getDiscountList(state) {
+		if (!Array.isArray(state.discountList) || state.discountList.length === 0) {
+			state.discountList = readDiscountList()
+		}
+		return state.discountList || []
+	}
+}
+
+const actions = {
+	getDiscountListFromCache({ commit }) {
+		const list = readDiscountList()
+		commit('SET_DISCOUNT_LIST', list)
+		return list
+	},
+	setDiscountListCache({ commit }, list) {
+		const nextList = normalizeDiscountList(list)
+		writeDiscountList(nextList)
+		commit('SET_DISCOUNT_LIST', nextList)
+	}
+}
+
+const mutations = {
+	SET_DISCOUNT_LIST(state, list = []) {
+		state.discountList = normalizeDiscountList(list)
+	}
+}
+
+export default {
+	state,
+	getters,
+	actions,
+	mutations
+}

+ 3 - 1
hdPad/src/store/modules/index.js

@@ -4,6 +4,7 @@ import config from "./config";
 import authorization from "./authorization";
 import product from "./product";
 import merchant from "./merchant";
+import discount from "./discount";
 export default {
 	login,
 	user,
@@ -11,5 +12,6 @@ export default {
 	authorization,
 
 	product, //商品
-	merchant //商家
+	merchant, //商家
+	discount //折扣设置
 };