xiaohaizhao 8 mēneši atpakaļ
vecāks
revīzija
00830d6b0e

+ 20 - 0
package-lock.json

@@ -10,6 +10,21 @@
       "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
       "dev": true
     },
+    "@zxing/library": {
+      "version": "0.21.3",
+      "resolved": "https://registry.npmmirror.com/@zxing/library/-/library-0.21.3.tgz",
+      "integrity": "sha512-hZHqFe2JyH/ZxviJZosZjV+2s6EDSY0O24R+FQmlWZBZXP9IqMo7S3nb3+2LBWxodJQkSurdQGnqE7KXqrYgow==",
+      "requires": {
+        "@zxing/text-encoding": "~0.9.0",
+        "ts-custom-error": "^3.2.1"
+      }
+    },
+    "@zxing/text-encoding": {
+      "version": "0.9.0",
+      "resolved": "https://registry.npmmirror.com/@zxing/text-encoding/-/text-encoding-0.9.0.tgz",
+      "integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==",
+      "optional": true
+    },
     "ajv": {
       "version": "6.12.6",
       "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz",
@@ -632,6 +647,11 @@
         "is-number": "^7.0.0"
       }
     },
+    "ts-custom-error": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmmirror.com/ts-custom-error/-/ts-custom-error-3.3.1.tgz",
+      "integrity": "sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A=="
+    },
     "uri-js": {
       "version": "4.4.1",
       "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz",

+ 1 - 0
package.json

@@ -4,6 +4,7 @@
   "description": "组件库: https://uiadmin.net/uview-plus/components/intro.html",
   "main": "main.js",
   "dependencies": {
+    "@zxing/library": "^0.21.3",
     "axios": "^1.11.0",
     "clipboard": "^2.0.11",
     "dayjs": "^1.11.13"

+ 21 - 15
pages/bookingService/index.vue

@@ -319,22 +319,28 @@ function changeItem(item) {
     closePopup();
 }
 
+import { BrowserMultiFormatReader } from '@zxing/library';
 function openScan() {
-    uni.scanCode({
-        onlyFromCamera: true,
-        scanType: ['qrCode', 'barCode'],
-        success: (res) => {
-            console.log("扫码结果", res);
-            if (res.result) {
-                form.sku = res.result;
-                skuConfirm();
-            } else {
-                uni.showToast({ title: '扫码失败,请重试', icon: 'none' });
-            }
-        },
-        fail: (err) => {
-            console.error("扫码失败", err);
-            uni.showToast({ title: '扫码失败,请重试', icon: 'none' });
+    uni.chooseImage({
+        count: 1,
+        success: imgRes => {
+            const imagePath = imgRes.tempFilePaths[0];
+
+            const img = new Image();
+            img.src = imagePath;
+
+            img.onload = async () => {
+                const codeReader = new BrowserMultiFormatReader();
+                try {
+                    const result = await codeReader.decodeFromImageElement(img);
+                    form.sku = result.text;
+                    skuConfirm();
+                    uni.showToast({ title: `识别成功: ${result.text}`, icon: 'none' });
+                } catch (err) {
+                    console.log("未识别出二维码或条形码");
+                    uni.showToast({ title: '未识别出二维码或条码', icon: 'none' });
+                }
+            };
         }
     });
 }

+ 340 - 0
pages/select/accessories.vue

@@ -0,0 +1,340 @@
+<template>
+    <block v-if="!isBom">
+        <view class="search-box">
+            <up-search placeholder="搜索关键词" v-model="keyword" height="35" @blur="onSearch" :clearabled="false"
+                :showAction="false" />
+            <view v-if="content.where.condition" class="clear" @click.stop="onSearch('')">
+                <up-icon name="close-circle-fill" size="20" />
+            </view>
+        </view>
+        <view style="height: 20rpx;" />
+    </block>
+
+    <My_listbox v-if="!isBom" ref="listBox" :empty="!list.length" :pullDown="!isBom" @getlist="getList">
+        <showList :result="resultIds" :list='list' @onClick="onSelect" />
+        <view style="height: 200rpx;" />
+    </My_listbox>
+
+    <view v-else-if="bomList.length" class="bom">
+        <view class="left">
+            <view class="class1" :class="active.class1 == index ? 'class1active' : ''" @click="changeClass1(index)"
+                v-for="(item, index) in bomList" :key="item.plm_bomid" hover-class="navigator-hover">
+                {{ item.bomname }}
+            </view>
+        </view>
+
+        <view class="right">
+            <My_listbox ref="listBox" :pullDown="false">
+                <up-collapse :value="collapse1" @change="changeCollapse1($event)">
+                    <up-collapse-item :ref="el => {
+                        if (el) {
+                            collapseRefs['collapse' + item.plm_bomid] = el
+                        }
+                    }" :cellCustomStyle="{
+                        backgroundColor: '#fff',
+                    }" v-for="item in bomList[active.class1].subdep" :key="item.plm_bomid" :title="item.bomname"
+                        :name="item.plm_bomid">
+                        <up-collapse @change="changeCollapse($event, item.plm_bomid)" v-if="item.subdep.length"
+                            :name="item.plm_bomid">
+                            <up-collapse-item :cellCustomStyle="{
+                                backgroundColor: '#fff',
+                            }" :ref="el => {
+                                if (el) {
+                                    collapseRefs['collapse' + item.plm_bomid] = el
+                                }
+                            }" v-for="item1 in item.subdep" :key="item1.plm_bomid" :title="item1.bomname"
+                                :name="item1.plm_bomid">
+                                <showList v-if="item1.items.length" size="small" :result="resultIds" :list='item1.items'
+                                    @onClick="onSelect" />
+                            </up-collapse-item>
+                        </up-collapse>
+
+                        <showList v-if="item.items.length" size="small" :result="resultIds" :list='item.items'
+                            @onClick="onSelect" />
+                    </up-collapse-item>
+                </up-collapse>
+                <view style="height: 200rpx;" />
+            </My_listbox>
+        </view>
+    </view>
+
+    <My_listbox v-else ref="listBox" :empty="true" @getlist="getBomList" />
+    <view class="footer">
+        <My-button :text="`确定添加(${resultIds.length})`" @onClick="onAdd" />
+    </view>
+</template>
+
+<script setup>
+import { ref, reactive, getCurrentInstance, nextTick } from 'vue';
+import { onLoad } from '@dcloudio/uni-app';
+import showList from "./accessoriesList.vue"
+const { $Http } = getCurrentInstance().proxy;
+
+const listBox = ref(null);
+const isBom = ref(false);
+let result = reactive([]);
+const resultIds = ref([]);
+
+const content = reactive({
+    loading: false,
+    "pageNumber": 1,
+    "pageSize": 20,
+    "where": {
+        "condition": ""
+    }
+})
+
+function onAdd() {
+    $Http.selectAcc(result)
+}
+
+function onSelect(e) {
+    if (result.some(item => item.itemid == e.itemid)) {
+        result = result.filter(item => item.itemid != e.itemid);
+    } else {
+        result.push(e);
+    }
+    resultIds.value = result.map(item => item.itemid);
+}
+
+const list = ref([]);
+
+onLoad((options) => {
+    console.log("options", options)
+    console.log("$Http", $Http)
+
+    result = result.concat(JSON.parse(options.list || '[]'));
+    resultIds.value = result.map(item => item.itemid);
+    console.log("初始选中", result, resultIds.value)
+    let content1 = $Http.content1;
+    content.sys_enterpriseid = content1.sys_enterpriseid;
+    content.sa_workorderid = content1.sa_workorderid;
+
+    if (content1.sku) $Http.basic({
+        "id": 2025080610424703,
+        "content": content1
+    }).then(res => {
+        console.log("查询产品是否存在BOM", res)
+        if (res.data == 0) {
+            // 不存在BOM
+            getList(true);
+        } else {
+            // 存在BOM
+            isBom.value = true;
+            getBomList();
+        }
+    })
+})
+
+const bomList = ref([]);
+
+let active = reactive({
+    class1: 0
+});
+
+function changeClass1(index) {
+    active.class1 = index;
+}
+
+// 折叠面板
+const collapseRefs = ref({})
+const collapse1 = ref([])
+
+function changeCollapse1(e) {
+    collapse1.value = e.filter(v => v.status == 'open').map(v => v.name)
+}
+
+function changeCollapse(e, id) {
+    nextTick(() => {
+        collapseRefs.value['collapse' + id].init()
+    });
+    setTimeout(() => {
+        nextTick(() => {
+            collapseRefs.value['collapse' + id].init()
+        });
+    }, 330);
+}
+
+// 有bom
+function getBomList() {
+    $Http.basic({
+        "id": "2025080610425503",
+        content: {
+            "sa_aftersalesbomid": content.sa_workorderid,
+            "sys_enterpriseid": content.sys_enterpriseid,
+        }
+    }).then(res => {
+        console.log("获取bom配件列表", res)
+        listBox.value.refreshToComplete();
+        listBox.value.setHeight();
+        if (res.code == 1) {
+            bomList.value = processBomData(res.data)
+            console.log("bomList", bomList.value);
+        } else {
+            if (res.msg) uni.showToast({
+                title: res.msg,
+                icon: 'none'
+            });
+        }
+    })
+}
+
+function processBomData(originalData) {
+    // 1. 提取所有一级分类节点
+    const topLevelNodes = [];
+
+    // 遍历原始数据
+    originalData.forEach(dataSet => {
+        dataSet.bom?.forEach(rootNode => {
+            // 提取根节点下的一级分类
+            if (rootNode.subdep && rootNode.subdep.length > 0) {
+                topLevelNodes.push(...rootNode.subdep);
+            }
+        });
+    });
+
+    // 2. 递归处理节点
+    const processNode = (node) => {
+        node.items = node.items || [];
+        node.subdep = node.subdep || [];
+        // 如果当前节点没有items和subdep,直接返回null
+        if (node.items.length === 0 && node.subdep.length === 0) return null;
+
+        try {
+            if (node.items.length) node.items = node.items.map(item => {
+                item.imageUrl = item.attinfos.length ? $Http.getSpecifiedImage(item.attinfos[0]) : ''
+                return item;
+            });
+        } catch (error) {
+
+        }
+        // 创建新节点副本
+        const newNode = { ...node };
+        // 处理子节点
+        if (node.subdep.length) newNode.subdep = node.subdep.map(subNode => processNode(subNode)).filter(subNode => subNode !== null);
+        return newNode;
+    };
+
+    // 3. 处理所有一级分类节点
+    return topLevelNodes.map(node => processNode(node)).filter(node => node && (node.subdep.length || node.items.length));
+}
+
+
+// 无BOM
+
+const keyword = ref('');
+
+function onSearch(e) {
+    if (content.where.condition == e) return;
+    content.where.condition = e;
+    keyword.value = e;
+    getList(true);
+}
+
+function getList(init = false) {
+    if (isBom.value) return;
+    if (content.loading) return;
+    if (init) content.pageNumber = 1;
+    content.loading = true;
+    $Http.basic({
+        "id": "2025080610425103",
+        content
+    }).then(res => {
+        console.log("获取配件列表", res)
+        content.loading = false;
+        listBox.value.refreshToComplete();
+        listBox.value.setHeight();
+        res.data = res.data.map(item => {
+            item.imageUrl = item.attinfos.length ? $Http.getSpecifiedImage(item.attinfos[0]) : ''
+            return item;
+        });
+        if (res.code == 1) {
+            list.value = reactive(res.firstPage ? res.data : list.value.concat(res.data));
+            content.pageTotal = res.pageTotal;
+            content.pageNumber = res.pageNumber;
+        } else {
+            if (res.msg) uni.showToast({
+                title: res.msg,
+                icon: 'none'
+            });
+        }
+    })
+}
+</script>
+
+<style lang="scss" scoped>
+.bom {
+    width: 100vw;
+    display: flex;
+    min-height: 100vh;
+
+    .left {
+        width: 250rpx;
+        background: #fff;
+        flex-shrink: 0;
+
+        .class1 {
+            padding: 30rpx 30rpx;
+            width: 250rpx;
+            box-sizing: border-box;
+            background: #FFFFFF;
+            border-radius: 0rpx 8rpx 0rpx 0rpx;
+            font-family: PingFang SC, PingFang SC;
+            font-size: 28rpx;
+            color: #999999;
+        }
+
+        .class1active {
+            position: relative;
+            background: #F7F7FF;
+            color: #3774F6;
+        }
+
+        .class1active::after {
+            content: '';
+            position: absolute;
+            left: 0;
+            top: 0;
+            width: 8rpx;
+            height: 100%;
+            background: #3774F6;
+        }
+    }
+
+    .right {
+        flex: 1;
+
+
+    }
+}
+
+.search-box {
+    position: relative;
+    padding: 20rpx;
+    background: #fff;
+
+    .clear {
+        position: absolute;
+        display: flex;
+        align-items: center;
+        right: 0;
+        top: 50%;
+        transform: translateY(-50%);
+        width: 80rpx;
+        padding-left: 10rpx;
+        height: 70rpx;
+        z-index: 2;
+    }
+}
+
+.footer {
+    position: fixed;
+    bottom: 0;
+    left: 0;
+    width: 100%;
+    height: 120rpx;
+    padding: 10rpx 20rpx;
+    background: #fff;
+    box-sizing: border-box;
+}
+</style>

+ 161 - 0
pages/select/accessoriesList.vue

@@ -0,0 +1,161 @@
+<template>
+    <view :class="size">
+        <view v-for="item in list" :key="item.itemid" hover-class="navigator-hover" class="item"
+            :class="result.includes(item.itemid) ? 'radio' : ''" @click="click(item)">
+            <view class="left" @click.stop="previewImge(item.imageUrl)">
+                <up-image :show-loading="true" :src="item.imageUrl" width="100%" height="100%" />
+            </view>
+            <view class="right">
+                <view class="itemname">
+                    {{ item.itemname || '--' }}
+                </view>
+                <view class="row">
+                    型号:{{ item.model || '--' }}
+                </view>
+                <view class="row">
+                    分类:{{ item.bomfullname || '--' }}
+                </view>
+                <view class="row">
+                    售价:<text class="price">{{ item.price }}</text>
+                </view>
+            </view>
+        </view>
+    </view>
+</template>
+
+<script setup>
+import { defineProps, defineEmits } from 'vue';
+const emit = defineEmits(['uploadCallback'])
+
+const props = defineProps({
+    list: {
+        type: Array
+    },
+    result: {
+        type: Array,
+        default: () => []
+    },
+    onClick: {
+        type: Function,
+        default: () => { }
+    },
+    size: {
+        type: String,
+        default: 'large' // small, large
+    }
+});
+
+function click(item) {
+    emit('onClick', item);
+}
+
+function previewImge(url) {
+    if (!url) return;
+    uni.previewImage({
+        urls: [url],
+        current: url
+    });
+}
+</script>
+
+<style lang="scss" scoped>
+.item {
+    display: flex;
+    width: 100%;
+    background: #FFFFFF;
+    border-radius: 20rpx;
+    padding: 16rpx;
+    box-sizing: border-box;
+    margin-top: 16rpx;
+
+    .left {
+        flex-shrink: 0;
+        width: 148rpx;
+        height: 148rpx;
+        background: #FFFFFF;
+        border-radius: 8rpx;
+        border: 2rpx solid #707070;
+        margin-right: 16rpx;
+        overflow: hidden;
+    }
+
+    .right {
+        right: 1;
+
+        .itemname {
+            line-height: 34rpx;
+            font-family: PingFang SC, PingFang SC;
+            font-weight: bold;
+            font-size: 24rpx;
+            color: #333333;
+        }
+
+        .row {
+            line-height: 28rpx;
+            font-family: PingFang SC, PingFang SC;
+            font-size: 20rpx;
+            color: #999999;
+
+            .price {
+                color: #FA5151;
+            }
+        }
+
+    }
+}
+
+.large {
+    width: 690rpx;
+    margin: 0 auto;
+
+    .item {
+        padding: 20rpx;
+        margin-top: 20rpx;
+
+        .left {
+            width: 180rpx;
+            height: 180rpx;
+            background: #FFFFFF;
+            margin-right: 20rpx;
+        }
+
+        .right {
+
+            .itemname {
+                line-height: 38rpx;
+                font-size: 34rpx;
+            }
+
+            .row {
+                font-size: 28rpx;
+                margin-top: 12rpx;
+            }
+
+        }
+    }
+}
+
+.radio {
+    background: #3774F6;
+
+    .right {
+        right: 1;
+
+        .itemname {
+            color: #fff;
+        }
+
+        .row {
+            color: #fff;
+
+            .price {
+                color: #fff;
+            }
+        }
+    }
+}
+
+.item:first-child {
+    margin-top: 0;
+}
+</style>