Browse Source

feat 复审功能

ylong 1 month ago
parent
commit
a5d7297f34
29 changed files with 1175 additions and 12 deletions
  1. 1 1
      config/request.js
  2. 18 0
      pages.json
  3. 123 0
      pages/index/audit/components/reviewOrderItem.vue
  4. 77 0
      pages/index/audit/components/time-clock.vue
  5. 77 0
      pages/index/audit/review-order.vue
  6. 21 6
      pages/index/detail/components/BookItem.vue
  7. 153 0
      pages/index/detail/components/ReviewBookInfo.vue
  8. 190 0
      pages/index/detail/components/ReviewInfo.vue
  9. 8 0
      pages/index/detail/index.vue
  10. 136 0
      pages/index/detail/review-book.vue
  11. 367 0
      pages/index/detail/review-detail.vue
  12. 0 1
      unpackage/dist/build/app-plus/app-config-service.js
  13. 0 0
      unpackage/dist/build/app-plus/app-service.js
  14. 0 0
      unpackage/dist/build/app-plus/app.css
  15. 1 1
      unpackage/dist/build/app-plus/manifest.json
  16. 0 0
      unpackage/dist/build/app-plus/pages/index/audit/review-order.css
  17. 0 0
      unpackage/dist/build/app-plus/pages/index/detail/index.css
  18. 0 0
      unpackage/dist/build/app-plus/pages/index/detail/review-book.css
  19. 0 0
      unpackage/dist/build/app-plus/pages/index/detail/review-detail.css
  20. 0 0
      unpackage/dist/build/app-plus/pages/index/express/weight-modify.css
  21. 0 0
      unpackage/dist/build/app-plus/pages/index/wms/order-query-list.css
  22. 0 0
      unpackage/dist/build/app-plus/pages/order/stat/pending-audit.css
  23. 0 0
      unpackage/dist/build/app-plus/pages/order/stat/pending-confirm.css
  24. 0 0
      unpackage/dist/build/app-plus/pages/order/stat/pending-payment.css
  25. 0 0
      unpackage/dist/build/app-plus/pages/order/stat/pending-pick.css
  26. 0 0
      unpackage/dist/build/app-plus/pages/order/stat/pending-review.css
  27. 0 0
      unpackage/dist/build/app-plus/pages/order/stat/pending-sign.css
  28. 0 0
      unpackage/dist/build/app-plus/uni-app-view.umd.js
  29. 3 3
      unpackage/dist/cache/.vite/deps/_metadata.json

+ 1 - 1
config/request.js

@@ -1,7 +1,7 @@
 /**
  * 文档地址:https://uiadmin.net/uview-plus/js/http.html
  */
-const baseUrl = "https://bpi.shuhi.com";
+const baseUrl = "https://bk.shuhi.com";
 export function initRequest() {
     console.log("初始化了 http 请求代码");
     // 初始化请求配置

+ 18 - 0
pages.json

@@ -100,6 +100,12 @@
 						"navigationBarTitleText": "根据快递单号或订单号查询"
 					}
 				},
+				{
+					"path": "audit/review-order",
+					"style": {
+						"navigationBarTitleText": "复审任务"
+					}
+				},
 				{
 					"path": "detail/user-orders",
 					"style": {
@@ -112,6 +118,18 @@
 						"navigationBarTitleText": "订单审核"
 					}
 				},
+				{
+					"path": "detail/review-book",
+					"style": {
+						"navigationBarTitleText": "订单图书复审"
+					}
+				},
+				{
+					"path": "detail/review-detail",
+					"style": {
+						"navigationBarTitleText": "订单复审"
+					}
+				},
 				{
 					"path": "detail/book-audit",
 					"style": {

+ 123 - 0
pages/index/audit/components/reviewOrderItem.vue

@@ -0,0 +1,123 @@
+<template>
+    <view class="review-order-card">
+        <!-- 订单信息 -->
+        <view class="order-info">
+            <view class="info-row">
+                <text class="label">订单号:</text>
+                <text class="value">{{ item.orderId }}</text>
+            </view>
+            <view class="info-row">
+                <text class="label">仓库:</text>
+                <text class="value">{{ item.godownName }}</text>
+            </view>
+            <view class="info-row">
+                <text class="label">不良数量:</text>
+                <text class="value">{{ item.reviewNum || 0 }}</text>
+            </view>
+            <view class="info-row">
+                <text class="label">是否入库:</text>
+                <text class="value">{{ stockStatusText }}</text>
+                <text v-if="item.positionCode" class="position-code">{{ item.positionCode }}</text>
+            </view>
+            <view class="info-row">
+                <text class="label">申请复审时间:</text>
+                <text class="value">{{ item.applyTime }}</text>
+            </view>
+            <view class="info-row">
+                <text class="label">已用时长:</text>
+                <TimeClock :initialSeconds="item.waitingTime" />
+            </view>
+        </view>
+    </view>
+</template>
+
+<script setup>
+import { computed } from "vue";
+import TimeClock from "./time-clock.vue";
+
+const props = defineProps({
+    item: {
+        type: Object,
+        default: () => { },
+    },
+});
+
+// 库存状态文本
+const stockStatusText = computed(() => {
+    if (props.item.stockStatus === 1) {
+        return "已入库";
+    } else if (props.item.stockStatus === 0) {
+        return "未入库";
+    }
+    return "-";
+});
+
+// 库存状态颜色
+const stockStatusColor = computed(() => {
+    if (props.item.stockStatus === 1) {
+        return "#FF4D4F"; // 红色 - 已入库
+    } else if (props.item.stockStatus === 0) {
+        return "#333333"; // 黑色 - 未入库
+    }
+    return "#999999";
+});
+
+// 格式化等待时长(毫秒转为时:分:秒)
+const formatWaitingTime = (milliseconds) => {
+    if (!milliseconds) return "00:00:00";
+
+    const totalSeconds = Math.floor(milliseconds / 1000);
+    const hours = Math.floor(totalSeconds / 3600);
+    const minutes = Math.floor((totalSeconds % 3600) / 60);
+    const seconds = totalSeconds % 60;
+
+    const pad = (num) => String(num).padStart(2, '0');
+
+    return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
+};
+</script>
+
+<style lang="scss" scoped>
+.review-order-card {
+    background: #ffffff;
+    border-radius: 8rpx;
+    padding: 24rpx;
+    margin-bottom: 20rpx;
+    box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
+
+    .order-info {
+        .info-row {
+            display: flex;
+            align-items: center;
+            margin-bottom: 16rpx;
+            font-size: 28rpx;
+            line-height: 40rpx;
+
+            &:last-child {
+                margin-bottom: 0;
+            }
+
+            .label {
+                color: #333333;
+                font-weight: 400;
+            }
+
+            .value {
+                color: #333333;
+                flex: 1;
+
+                &.time-duration {
+                    color: #FF4D4F;
+                    font-weight: 500;
+                }
+            }
+
+            .position-code {
+                color: #E87A20FF;
+                margin-left: 16rpx;
+                font-weight: 500;
+            }
+        }
+    }
+}
+</style>

+ 77 - 0
pages/index/audit/components/time-clock.vue

@@ -0,0 +1,77 @@
+<template>
+    <div class="time-counter">
+        {{ formattedTime }}
+    </div>
+</template>
+
+<script>
+export default {
+    name: 'TimeClock',
+    // props 定义(与 Vue3 逻辑一致,保留非负整数校验)
+    props: {
+        initialSeconds: {
+            type: Number,
+            required: true,
+            validator(value) {
+                // 校验:必须是非负整数
+                return Number.isInteger(value) && value >= 0
+            }
+        }
+    },
+    data() {
+        return {
+            // 当前累计秒数(对应 Vue3 的 ref 响应式变量)
+            currentSeconds: this.initialSeconds,
+            // 定时器实例(用于清理)
+            timer: null
+        }
+    },
+    computed: {
+        // 格式化时间:秒数 → 00:00:00 格式(逻辑与 Vue3 完全一致)
+        formattedTime() {
+            const hours = String(Math.floor(this.currentSeconds / 3600)).padStart(2, '0')
+            const minutes = String(Math.floor((this.currentSeconds % 3600) / 60)).padStart(2, '0')
+            const seconds = String(this.currentSeconds % 60).padStart(2, '0')
+            return `${hours}:${minutes}:${seconds}`
+        }
+    },
+    watch: {
+        // 监听初始秒数变化(对应 Vue3 的 watch 函数)
+        initialSeconds: {
+            handler(newVal) {
+                this.currentSeconds = Math.floor(newVal / 1000); // 更新当前秒数为新初始值
+                this.startTimer() // 重启定时器
+            },
+            immediate: true
+        }
+    },
+    methods: {
+        // 启动计时(逻辑与 Vue3 一致)
+        startTimer() {
+            // 清除已有定时器,避免重复计时
+            if (this.timer) clearInterval(this.timer)
+            // 每秒递增 1 秒
+            this.timer = setInterval(() => {
+                this.currentSeconds += 1
+            }, 1000)
+        }
+    },
+    // 初始化时启动计时(对应 Vue3 的 setup 初始化逻辑)
+    mounted() {
+        this.startTimer()
+    },
+    // 组件卸载时清理定时器(避免内存泄漏,与 Vue3 一致)
+    beforeDestroy() {
+        if (this.timer) clearInterval(this.timer)
+    }
+}
+</script>
+
+<style scoped>
+.time-counter {
+    font-size: 14px;
+    font-weight: 600;
+    color: #ff0000;
+    letter-spacing: 2px;
+}
+</style>

+ 77 - 0
pages/index/audit/review-order.vue

@@ -0,0 +1,77 @@
+<template>
+    <view class="common-page" style="padding: 0" @click="playGlobalSound">
+        <PageScroll requestStr="/app/orderreview/reviewOrderList" @updateList="updateList" ref="scrollRef"
+            :otherParams="otherParams" :immediate="true">
+            <view class="list-con" v-if="dataList.length">
+                <ReviewOrderItem v-for="cell in dataList" :key="cell.orderId" :item="cell" class="mt-20" @click="handleReviewOrder(cell)">
+                </ReviewOrderItem>
+            </view>
+
+            <!-- 底部统计 -->
+            <view class="fixed-bottom total-count" v-if="total > 0">
+                共计{{ total }}条
+            </view>
+        </PageScroll>
+    </view>
+</template>
+
+<script setup>
+import { ref } from "vue";
+import { onShow } from "@dcloudio/uni-app";
+import PageScroll from "@/components/pageScroll/index.vue";
+import ReviewOrderItem from "./components/reviewOrderItem.vue";
+
+//点击全局音效
+function playGlobalSound() {
+    uni.$u.playClickSound();
+}
+
+const otherParams = ref({});
+const scrollRef = ref(null);
+const refreshList = () => {
+    scrollRef.value?.resetUpScroll();
+};
+
+let dataList = ref([]);
+let total = ref(0);
+
+const updateList = (data, page) => {
+    dataList.value = data;
+    // 假设API返回的数据结构中包含total字段
+    // 如果API响应结构不同,需要根据实际情况调整
+    if (page && page.total !== undefined) {
+        total.value = page.total;
+    } else {
+        total.value = data.length;
+    }
+};
+
+const handleReviewOrder = (item) => {
+    uni.navigateTo({
+        url: `/pages/index/detail/review-detail?id=${item.orderId}`
+    });
+};
+
+onShow(() => {
+    refreshList();
+});
+</script>
+
+<style lang="scss" scoped>
+.list-con {
+    padding: 10rpx 30rpx;
+    gap: 30rpx;
+}
+
+.total-count {
+    background-color: #4caf50;
+    color: #ffffff;
+    text-align: center;
+    padding: 24rpx;
+    font-size: 32rpx;
+    font-weight: 500;
+    margin-top: 20rpx;
+    display: flex;
+    justify-content: center;
+}
+</style>

+ 21 - 6
pages/index/detail/components/BookItem.vue

@@ -3,7 +3,7 @@
         <view class="flex w100">
             <view class="flex-d">
                 <image style="width: 70px;height: 90px;" :src="item.cover" mode="aspectFill"></image>
-                <view class="quantity mt-24" v-if="!showClose">数量: {{ item.num }}</view>
+                <view class="quantity mt-24" v-if="!showClose && !type">数量: {{ item.num }}</view>
             </view>
             <view class="book-info ml-20 flex-1">
                 <view class="common-title mb-20">{{ item.bookName }}</view>
@@ -20,11 +20,22 @@
                     <view class="discount">回收折扣: {{ item.recycleDiscount }}</view>
                     <view class="review">审核金额: <text class="color-red">{{ item.finalMoney || 0 }}</text></view>
                 </view>
-                <view class="quality mb-10">
-                    品相: 良好({{ goodNum }}) 、 一般({{ item.averageNum || 0 }}) 、 <text
-                        :class="item.badNum > 0 ? 'color-red' : ''">极差({{ badNum }})</text>
-                </view>
-                <view class="color-red" v-if="reasons">原因:{{ reasons }}</view>
+                <template v-if="type=='review'">
+                    <view class="">
+                        <view>不良原因: <text :class="item.auditComment?.sts == 3 ? 'color-red' : ''">{{
+                                item.auditComment?.com }}</text></view>
+                        <view v-if="item.auditReviewComment?.com">
+                            复审结果:<text class="color-red">{{ item.auditReviewComment?.com }}</text>
+                        </view>
+                    </view>
+                </template>
+                <template v-else>
+                    <view class="quality mb-10">
+                        品相: 良好({{ goodNum }}) 、 一般({{ item.averageNum || 0 }}) 、 <text
+                            :class="item.badNum > 0 ? 'color-red' : ''">极差({{ badNum }})</text>
+                    </view>
+                    <view class="color-red" v-if="reasons">原因:{{ reasons }}</view>
+                </template>
             </view>
         </view>
     </view>
@@ -38,6 +49,10 @@ const props = defineProps({
     showClose: {
         type: Boolean,
         default: false
+    },
+    type: {
+        type: String,
+        default: ''
     }
 })
 

+ 153 - 0
pages/index/detail/components/ReviewBookInfo.vue

@@ -0,0 +1,153 @@
+<template>
+    <view class="book-info">
+        <template v-if="isShow">
+            <view class="font-bold mt-30 mb-30" style="padding: 0 20rpx">
+                {{ bookList.length }}条记录,共{{ bookList.length }}本,已审{{ auditNum || 0 }}本
+            </view>
+        </template>
+
+        <view class="book-info-item bg-white mt-12" :style="{ backgroundColor: item.backgroundColor }"
+            v-for="item in formatList" :key="item.title" :id="item.id" v-show="item?.title">
+            <view class="book-info-item-title">{{ item.title }}</view>
+            <view class="book-info-item-list">
+                <BookItem v-for="book in item.list" :key="book.bookId" :item="book" @click="handleBookClick(book)"
+                    type="review" />
+            </view>
+        </view>
+    </view>
+</template>
+
+<script setup>
+import { ref, watch, computed } from "vue";
+import BookItem from "./BookItem.vue";
+import toPinyin from "@/utils/toPinyin.js";
+const props = defineProps({
+    bookList: {
+        type: Array,
+        default: () => [],
+    },
+    detail: {
+        type: Object,
+        default: () => ({}),
+    },
+    isShow: {
+        type: Boolean,
+        default: true,
+    },
+});
+
+const emit = defineEmits(["get-all-firstLetter"]);
+
+//计算已审书籍数量
+const auditNum = computed(() => {
+    return props.bookList.filter((v) => !!v.auditReviewComment?.sts).length || 0;
+});
+
+function handleBookClick(book) {
+    uni.navigateTo({
+        url: `/pages/index/detail/review-book?isbn=${book.isbn}&orderId=${props.detail.orderId}&auditReviewId=${book.auditReviewComment.auditReviewId}&&index=${book.auditReviewComment.index}`,
+    });
+    uni.setStorageSync("auditBook", book);
+}
+
+function getAuditCateNum(cate) {
+    let auditList = [];
+    props.bookList.forEach((v) => {
+        auditList.push(...(v.detailReviewVoList || []));
+    });
+    return auditList.filter((v) => v.sts == cate).length;
+}
+
+//格式化书籍列表
+const formatBookList = (list) => {
+    const poorBooks = { title: "极差", list: [], backgroundColor: "#de868f", id: "poor" };
+    const goodBooks = { title: "良好", list: [], backgroundColor: "#81b337", id: "good" };
+    const otherBooks = [];
+    //获取所有的首字母
+    const allFirstLetter = [];
+
+    const handleBookCategorization = (book) => {
+        let firstLetter;
+        // 找到第一个中文
+        let firstChineseChar = null;
+        for (let i = 0; i < book.bookName.length; i++) {
+            const char = book.bookName.charAt(i);
+            if (/^[\u4E00-\u9FA5]$/.test(char)) {
+                // 检查是否为中文
+                firstChineseChar = char;
+                break;
+            }
+        }
+
+        // 如果找到中文,使用其拼音首字母
+        if (firstChineseChar) {
+            firstLetter = toPinyin.chineseToInitials(toPinyin.chineseToPinYin(firstChineseChar));
+        } else {
+            // 如果没有找到中文,使用第一个字符
+            const char = book.bookName.charAt(0);
+            if (/^[A-Za-z]$/.test(char)) {
+                firstLetter = char.toUpperCase();
+            } else {
+                firstLetter = char;
+            }
+        }
+
+        let bool = otherBooks.some((item) => item.title == firstLetter);
+        if (!bool) {
+            otherBooks.push({ id: firstLetter, title: firstLetter, list: [{ ...book }] });
+            allFirstLetter.push(firstLetter);
+            emit("get-all-firstLetter", allFirstLetter);
+        } else {
+            otherBooks.find((item) => item.title == firstLetter).list.push(book);
+        }
+    };
+
+    list.forEach((book) => {
+        book.goodNum = list?.filter((v) => v.sts == 1).length || 0;
+        book.badNum = list?.filter((v) => v.sts == 3).length || 0;
+
+        // Check if book is fully audited (all items have been reviewed)
+        const totalAudited = list?.filter((v) => v.sts !== 0).length || 0;
+        const isFullyAudited = totalAudited === list.length;
+
+        if (isFullyAudited) {
+            if (book.auditReviewComment?.sts === 3) {
+                poorBooks.list.push(book);
+            } else if (book.auditReviewComment?.sts === 1) {
+                goodBooks.list.push(book);
+            } else {
+                handleBookCategorization(book);
+            }
+        } else {
+            handleBookCategorization(book);
+        }
+    });
+
+    return { poorBooks, goodBooks, otherBooks };
+};
+
+const formatList = ref([]);
+watch(
+    () => props.bookList,
+    (newVal) => {
+        let { poorBooks, goodBooks, otherBooks } = formatBookList(newVal);
+
+        // 按照首字母排序 A-Z
+        otherBooks.sort((a, b) => a.id.localeCompare(b.id));
+
+        let poor = poorBooks.list.length > 0 ? poorBooks : {};
+        let good = goodBooks.list.length > 0 ? goodBooks : {};
+        formatList.value = [...otherBooks, poor, good];
+    },
+    { immediate: true, deep: true }
+);
+</script>
+
+<style scoped>
+.book-info-item-title {
+    padding: 10rpx 30rpx;
+    font-size: 28rpx;
+    font-weight: 600;
+    border-bottom: 1px solid #e5e5e5;
+}
+</style>

+ 190 - 0
pages/index/detail/components/ReviewInfo.vue

@@ -0,0 +1,190 @@
+<template>
+    <view class="file-info bg-white">
+        <view class="file-info-container">
+            <view class="origin-result" style="margin-bottom: 30rpx;">
+                <text class="origin-result-item-title">原审核结果:</text>
+                <text class="origin-result-item-content">{{ detail.oldAudit }}</text>
+            </view>
+            <cy-upload v-model:filename="form.reviewImg" :maxCount="3" />
+
+            <view class="mt-20 bg-white flex flex-j-a flex-a-c" style="padding: 20rpx; border-radius: 10rpx">
+                <text>复审结果:</text>
+                <view class="u-page__tag-item" v-for="(item, idx) in auditList" :key="idx">
+                    <u-tag :text="item.label" :plain="form.reviewAuditSts != item.sts" type="primary" :name="item.sts"
+                        :class="{ disabled: item.sts == 2 }" @click="radioClick(item.sts)">
+                    </u-tag>
+                </view>
+            </view>
+            <view class="flex flex-a-c flex-wrap mb-20" v-if="form.reviewAuditSts == 3">
+                <view class="mr-10 reason-item" :class="{ selected: form.auditList.includes(item.label) }"
+                    v-for="(item, idx) in reasonList2" :key="idx" @click="toggleReason2(item)">
+                    {{ item.label }}
+                </view>
+            </view>
+
+            <u-textarea v-model="form.reviewRemark" placeholder="上传说明..." autoHeight
+                style="min-height: 60px;"></u-textarea>
+
+            <!-- 快速填入的功能 -->
+            <view class="quick-input">
+                <view class="quick-input-item">快速填入</view>
+                <view class="flex flex-a-c flex-wrap">
+                    <view class="mr-10 reason-item" v-for="(item, idx) in reasonList" :key="idx"
+                        @click="toggleReason(item)">
+                        {{ item.label }}
+                    </view>
+                </view>
+            </view>
+
+            <view class="flex flex-a-c flex-j-c">
+                <u-button class="mt-24" type="primary">上传(最多上传3张)</u-button>
+            </view>
+        </view>
+
+        <view class="file-info-list">
+            <view class="file-info-list-title text-center">已上传记录</view>
+            <view class="file-info-list-content mt-24 mb-24 text-center" v-if="!detail.imgList?.length">还没有上传记录</view>
+            <view class="file-info-item" v-for="item in detail.imgList" :key="item.id" v-else>
+                <view class="file-info-item-img flex flex-a-c" v-if="item.reviewImg">
+                    <u-image v-for="(img, index) in item.reviewImg" :src="img" width="90" height="90"
+                        style="margin-right: 10px;" :key="img" @click="handlePreview(item.reviewImg, index)"></u-image>
+                </view>
+                <view class="file-info-item-info mt-10 font-14">
+                    <view class="file-info-item-info-name">上传说明:{{ item.reviewRemark }}</view>
+                    <view class="file-info-item-info-name mt-10">
+                        <text>{{ item.createTime }} </text>
+                        <text class="ml-20">{{ item.createName }}</text>
+                    </view>
+                </view>
+            </view>
+        </view>
+    </view>
+</template>
+
+<script setup>
+import { ref } from 'vue';
+import cyUpload from '@/components/cy-upload/index.vue';
+
+const form = ref({
+    reviewImg: [],
+    reviewRemark: '',
+    reviewAudit: '',
+    auditList: [],
+    reviewAuditSts: 3,
+})
+
+const props = defineProps({
+    detail: {
+        type: Object,
+        default: () => ({})
+    }
+})
+
+let reasonList = ref([
+    { sts: 3, label: "无防伪标/假防伪标" },
+    { sts: 3, label: "无防伪页/衬页" },
+    { sts: 3, label: "复印书" },
+    { sts: 3, label: "印刷呈锯齿状/断墨" },
+]);
+
+let reasonList2 = ref([
+    { sts: 3, label: "明显泛黄水印/发霉/明显异味" },
+    { sts: 3, label: "老化泛黄褪色/污渍" },
+    { sts: 3, label: "明显破损/脱胶缺页/书籍变型" },
+    { sts: 3, label: "无此书" },
+    { sts: 3, label: "印刷不清晰" },
+    { sts: 3, label: "无防伪" },
+    { sts: 3, label: "无外护封/无ISBN" },
+    { sts: 3, label: "无此书多其他书" },
+    { sts: 3, label: "笔记杂乱/习题做完" },
+    { sts: 3, label: "有笔记贴" },
+    { sts: 3, label: "套装缺册" },
+    { sts: 3, label: "听力无光盘" },
+    { sts: 3, label: "图书馆/藏书印章" },
+]);
+
+function toggleReason2(item) {
+    if (form.value.auditList.includes(item.label)) {
+        form.value.auditList = form.value.auditList.filter((v) => v != item.label);
+    } else {
+        form.value.auditList.push(item.label);
+    }
+
+    form.value.reviewAudit = form.value.auditList.join(',');
+}
+
+//预览图片
+const handlePreview = (imgList, index) => {
+    uni.previewImage({
+        urls: imgList,
+        current: index
+    })
+}
+
+const toggleReason = (item) => {
+    form.value.reviewAuditSts = item.sts;
+    form.value.reviewRemark = item.label;
+}
+
+const auditList = ref([
+    { sts: 1, label: "品相良好" },
+    { sts: 2, label: "品相一般", disabled: true },
+    { sts: 3, label: "品相极差" },
+]);
+
+// 定义方法,注意在 setup 中不需要 this,直接访问响应式引用  
+function radioClick(sts) {
+    if (sts == 2) return;
+    form.value.reviewAuditSts = sts;
+}
+
+defineExpose({
+    form: form.value,
+})
+
+</script>
+<style lang="scss" scoped>
+.file-info-container {
+    padding: 20rpx 30rpx;
+    box-sizing: border-box;
+
+}
+
+.disabled {
+    background-color: #d0d0d0;
+    cursor: not-allowed;
+}
+
+.quick-input-item {
+    margin: 30rpx 0;
+    font-weight: 600;
+    font-size: 15px;
+}
+
+.reason-item {
+    background-color: #f0f0f0;
+    border-radius: 4rpx;
+    padding: 14rpx 20rpx;
+    margin-bottom: 16rpx;
+
+    &.selected {
+        background-color: #d0e8ff;
+    }
+}
+
+.file-info-item {
+    padding: 20rpx;
+    border-bottom: 1px solid #e5e5e5;
+}
+
+.file-info-list-title {
+    background-color: #e5e5e5;
+    padding: 10rpx 0;
+    margin-top: 20px;
+}
+
+.file-info-list-content {
+    color: #999;
+    min-height: 100px;
+}
+</style>

+ 8 - 0
pages/index/detail/index.vue

@@ -80,6 +80,14 @@ const handleAuditorSelected = (auditor) => {
         userName: auditor.userName,
         userId: auditor.userId,
     });
+
+    uni.$u.http.post("/app/orderinfo/setCheckUser", {
+        checkUserId: auditor.userId,
+    }).then(res => {
+        if (res.code == 200) {
+            uni.$u.toast("绑定成功");
+        }
+    })
 };
 
 //点击全局音效

+ 136 - 0
pages/index/detail/review-book.vue

@@ -0,0 +1,136 @@
+<template>
+    <view class="book-audit" @click="playGlobalSound">
+        <view class="bg-white flex flex-a-c" style="padding: 20rpx; border-radius: 10rpx">
+            <image :src="detail.cover" mode="aspectFill" style="width: 80px; height: 100px"></image>
+            <view class="flex flex-d flex-1 ml-20">
+                <text class="common-title mb-20">{{ detail.bookName }}</text>
+                <text class="text-sm">ISBN: <text class="color-primary">{{ detail.isbn }}</text>
+                </text>
+                <text class="text-sm mt-6">作者: <text>{{ detail.author }}</text>
+                </text>
+                <text class="text-sm mt-6">出版社: <text>{{ detail.publish }}</text>
+                </text>
+            </view>
+        </view>
+
+        <view class="mt-20 bg-white price-info" style="border-radius: 10rpx">
+            <view class="flex">
+                <view class="flex flex-a-c flex-1">
+                    <text class="label">定价</text>
+                    <text class="content">¥{{ detail.price }}</text>
+                </view>
+                <view class="flex flex-a-c flex-1">
+                    <text class="label">回收折扣</text>
+                    <text class="content">{{ detail.recycleDiscount }}折</text>
+                </view>
+            </view>
+            <view class="flex flex-a-c">
+                <view class="flex flex-a-c flex-1">
+                    <text class="label">预估金额</text>
+                    <text class="content">¥{{ detail.expectPrice }}</text>
+                </view>
+                <view class="flex flex-a-c flex-1">
+                    <text class="label">审核金额</text>
+                    <text class="content">¥0.00</text>
+                </view>
+            </view>
+        </view>
+
+        <view class="mt-20">
+            <ReviewInfo :detail="reviewInfo" ref="reviewInfoRef" />
+        </view>
+
+        <view class="fixed-bottom">
+            <u-button type="primary" size="large" @click="handleAudit">确定</u-button>
+        </view>
+    </view>
+</template>
+
+<script setup>
+import { ref, nextTick, onUnmounted } from "vue";
+import { onLoad, onShow } from "@dcloudio/uni-app";
+import ReviewInfo from "./components/ReviewInfo.vue";
+
+const detail = ref({});
+
+function playGlobalSound() {
+    uni.$u.playClickSound()
+}
+
+// 获取图书详情 /app/book/getSimpleBookInfoByIsbn/{isbn}
+const getBookInfo = (isbn) => {
+    uni.$u.http.get(`/app/book/getBookByIsbn/${isbn}`).then((res) => {
+        if (res.code == 200) {
+            detail.value = res.data;
+        }
+    });
+};
+//获取图书信息和审核信息
+const reviewInfo = ref({});
+const getBookInfoAndAuditInfo = (opts) => {
+    uni.$u.http.get(`/app/orderreview/getBookReviewInfo?isbn=${opts.isbn}&&orderId=${opts.orderId}&index=${opts.index}`).then((res) => {
+        if (res.code == 200) {
+            reviewInfo.value = res.data;
+        }
+    });
+};
+
+//复审 
+const reviewInfoRef = ref(null)
+const handleAudit = () => {
+    let form = reviewInfoRef.value.form
+    uni.$u.http
+        .post("/app/orderreview/reviewBook", {
+            auditReviewId: auditReviewId.value,
+            ...form
+        })
+        .then((res) => {
+            if (res.code == 200) {
+                uni.showToast({ title: "复审成功", icon: "none" });
+                uni.$u.ttsModule.speak("复审成功");
+                uni.navigateBack();
+            } else {
+                uni.$u.toast(res.msg);
+            }
+        });
+};
+
+const auditReviewId = ref();
+const index = ref();
+let isbnScan = ref("");
+onLoad((options) => {
+    options.isbn && getBookInfo(options.isbn);
+    isbnScan.value = options.isbn;
+    auditReviewId.value = options.auditReviewId;
+    index.value = options.index;
+    getBookInfoAndAuditInfo(options);
+});
+
+</script>
+
+<style lang="scss" scoped>
+.book-audit {
+    padding: 20rpx;
+    box-sizing: border-box;
+    padding-bottom: 140rpx;
+
+    .price-info {
+        border-radius: 10rpx;
+
+        .label {
+            width: 150rpx;
+            background-color: #cecece;
+            padding: 16rpx 10rpx;
+            text-align: center;
+            border: 1rpx solid #e6e6e6;
+        }
+
+        .content {
+            flex: 1;
+            text-align: center;
+            padding: 16rpx 10rpx;
+            border: 1rpx solid #e6e6e6;
+        }
+    }
+}
+</style>

+ 367 - 0
pages/index/detail/review-detail.vue

@@ -0,0 +1,367 @@
+<template>
+    <view class="order-detail" :class="{ 'fixed-bottom-2': type == 2 }" @click="playGlobalSound">
+        <scroll-view class="scroller" :scroll-into-view="toView" scroll-y="true" scroll-with-animation="true">
+            <view class="flex flex-a-c flex-j-b bg-white bind-audit mb-16">
+                <text>绑定复审员</text>
+                <text class="text-center flex-1">{{
+                    orderDetail.auditUserName || selectedAuditor?.userName || ""
+                    }}</text>
+                <text @click="showAuditorSelector = true" class="color-primary">绑定</text>
+            </view>
+
+            <OrderInfo :detail="orderDetail" @refresh="() => getOrderDetail(true)" />
+
+            <UserInfoCard :detail="orderDetail" />
+
+            <view class="mt-16" style="padding: 0 6rpx">
+                <u-subsection :list="list" mode="subsection" :current="current"
+                    @change="handleSectionChange"></u-subsection>
+                <ReviewBookInfo v-if="current == 0" :bookList="orderDetail.detailReviewVoList" :detail="orderDetail"
+                    @get-all-firstLetter="onGetAllFirstLetter" />
+                <LogisticsTimeline v-if="current == 1" :list="orderDetail.trackingVoList" />
+                <FileInfo v-if="current == 2" :orderId="orderDetail.orderId" />
+            </view>
+        </scroll-view>
+
+        <view class="fixed-left">
+            <view class="bind-code common-bg" style="margin-bottom: 30px; padding: 20rpx" @click="handleBindCode">绑码
+            </view>
+
+            <view class="common-bg flex flex-a-c flex-j-c flex-d">
+                <view class="book-status-item" @click="scrollToView('good')">良好</view>
+                <view class="book-status-item item-center" @click="scrollToView('average')">一般</view>
+                <view class="book-status-item" @click="scrollToView('poor')">极差</view>
+            </view>
+        </view>
+
+        <view class="fixed-right">
+            <view class="letter-bg flex flex-a-c flex-j-c flex-d">
+                <view class="letter-item"><u-icon name="arrow-up-fill" size="20" color="#ffffff"
+                        @click="scrollToTop"></u-icon></view>
+                <view class="letter-item" v-for="(item, index) in allLetters" :key="index" @click="scrollToView(item)">
+                    {{ item }}</view>
+            </view>
+        </view>
+
+        <view class="common-bg fixed-bottom">
+            <u-button type="primary" size="large" @click="handleComplete">完成</u-button>
+        </view>
+
+        <AuditorSelector :show="showAuditorSelector" @update:show="showAuditorSelector = $event"
+            @auditor-selected="handleAuditorSelected" />
+    </view>
+</template>
+
+<script setup>
+import { ref, onUnmounted } from "vue";
+import { onLoad, onShow } from "@dcloudio/uni-app";
+import AuditorSelector from "./components/AuditorSelector.vue";
+import OrderInfo from "./components/OrderInfo.vue";
+import UserInfoCard from "./components/UserInfoCard.vue";
+import LogisticsTimeline from "../express/components/LogisticsTimeline.vue";
+import ReviewBookInfo from "./components/ReviewBookInfo.vue";
+import FileInfo from "./components/FileInfo.vue";
+
+const showAuditorSelector = ref(false);
+const selectedAuditor = ref({});
+const handleAuditorSelected = (auditor) => {
+    selectedAuditor.value = auditor;
+    orderDetail.value.auditUserId = auditor.userId;
+    orderDetail.value.auditUserName = auditor.userName;
+
+    uni.setStorageSync("checkUserInfo", {
+        userName: auditor.userName,
+        userId: auditor.userId,
+    });
+};
+
+//点击全局音效
+function playGlobalSound() {
+    uni.$u.playClickSound();
+}
+
+//点击滚动的位置
+const toView = ref("");
+function scrollToView(to) {
+    toView.value = to;
+
+    uni.pageScrollTo({
+        selector: "#" + to,
+        duration: 200,
+    });
+}
+//回到顶部
+function scrollToTop() {
+    uni.pageScrollTo({
+        top: 0,
+        duration: 200,
+    });
+}
+
+//绑码
+function handleBindCode() {
+    uni.$u.toast("暂无开发");
+}
+
+const list = ref(["图书清单", "物流信息", "上传附件"]);
+const current = ref(0);
+const handleSectionChange = (index) => {
+    current.value = index;
+};
+
+//监听书籍的首字母
+const allLetters = ref([]);
+function onGetAllFirstLetter(data) {
+    allLetters.value = data;
+}
+
+//完成
+function handleComplete() {
+    let bookList = orderDetail.value.detailReviewVoList;
+    let bool = bookList.some((item) => !item.auditReviewComment?.sts);
+    if (bool) {
+        let text = "还有未复审的书";
+        uni.$u.toast(text);
+        uni.$u.ttsModule.speak(text);
+        return;
+    } else {
+        uni.$u.http
+            .post("/app/orderreview/reviewOrderFinish", {
+                reviewId: orderDetail.value.reviewId,
+            })
+            .then((res) => {
+                if (res.code == 200) {
+                    uni.showToast({ title: "复审完成", icon: "none" });
+                    uni.$u.ttsModule.speak("复审完成");
+                    uni.navigateBack();
+                }else{
+                    uni.$u.ttsModule.speak(res.msg);
+                }
+            });
+    }
+}
+
+const orderDetail = ref({ status: 0 });
+//获取订单详情
+function getOrderDetail(forceRefresh = false) {
+    if (!orderId.value) return;
+
+    // 如果是强制刷新,重置标志位
+    if (forceRefresh) {
+        hasLoadedOrderDetail.value = false;
+        hasLoadedCheckUser.value = false;
+    }
+
+    uni.showLoading({
+        title: "加载中...",
+        mask: true,
+    });
+    uni.$u.http
+        .get("/app/orderreview/getOrderInfoForReview", {
+            params: {
+                searchType: 1,
+                search: orderId.value,
+            },
+        })
+        .then((res) => {
+            if (res.code == 200) {
+                orderDetail.value = res.data;
+
+                // 只有在首次加载且没有复审员信息时才调用getCheckUserInfo
+                if (!hasLoadedCheckUser.value && !res.data.auditUserId) {
+                    getCheckUserInfo();
+                }
+
+                if (res.data.auditUserId) {
+                    let auditUserInfo = {
+                        userName: res.data.auditUserName,
+                        userId: res.data.auditUserId,
+                    };
+                    uni.setStorageSync("checkUserInfo", auditUserInfo);
+                } else {
+                    let userInfo = uni.getStorageSync("checkUserInfo");
+                    if (userInfo && userInfo.userName) {
+                        orderDetail.value.auditUserName = userInfo.userName;
+                        orderDetail.value.auditUserId = userInfo.userId;
+                    }
+                }
+                if (isOnLoad.value) {
+                    if (res.data.manageRemark.length > 0 && res.data.status < 10) {
+                        uni.$u.ttsModule.speak("此订单有备注信息,请注意查看");
+                    }
+                    if (res.data.warnArea && res.data.warnArea.length > 0) {
+                        let text = `此订单来源于${res.data.warnArea}`;
+                        uni.$u.ttsModule.speak(text);
+                    }
+                    isOnLoad.value = false;
+                }
+                hasLoadedOrderDetail.value = true; // 标记订单详情已加载
+            } else {
+                uni.$u.toast(res.msg);
+            }
+        })
+        .finally(() => {
+            uni.hideLoading();
+        });
+}
+//获取上一次绑定的复审员信息
+function getCheckUserInfo() {
+    uni.$u.http.get("/app/orderinfo/getCheckUser").then((res) => {
+        if (res.code == 200) {
+            let userInfo = res.data || {};
+            orderDetail.value.auditUserName = userInfo.userName || '';
+            orderDetail.value.auditUserId = userInfo.userId || '';
+            uni.setStorageSync("checkUserInfo", {
+                userName: userInfo.userName,
+                userId: userInfo.userId,
+            });
+        }
+        hasLoadedCheckUser.value = true; // 标记复审员信息已加载
+    });
+}
+
+//isbn正则校验是否符合
+function checkIsbn(isbn) {
+    const isbn13Regex = /^(?:97[89]-?\d{1,5}-?\d{1,7}-?\d{1,6}-?\d)$/;
+    if (isbn13Regex.test(isbn)) {
+        return true;
+    }
+    return false;
+}
+
+//扫码之后的逻辑
+function handleScan(isbn) {
+    if (!checkIsbn(isbn)) {
+        let text = `不是正确的ISBN码`;
+        uni.$u.ttsModule.speak(text);
+        return;
+    }
+
+    if (orderDetail.value.status >= 10) {
+        uni.$u.ttsModule.speak("订单已复审完成");
+        return;
+    }
+
+    //取 isbn 的后四位字符串进行播报
+    let isbnStr = `${isbn.slice(-4)}`;
+
+    let isbnList = orderDetail.value.detailVoList.map((item) => item.isbn);
+
+    if (isbnList.includes(isbn)) {
+        let book = orderDetail.value.detailVoList.find((item) => item.isbn == isbn);
+
+        //扫描到套装书
+        if (book.suit == 1) {
+            let text = `${isbnStr}请注意套装书是否齐全`;
+            uni.$u.ttsModule.speak(text);
+        }
+        //扫描到需要取出的书
+        if (book.bookWarn == 1) {
+            let text = `请注意${isbnStr}需要取出`;
+            uni.$u.ttsModule.speak(text);
+        }
+
+        uni.navigateTo({
+            url: `/pages/index/detail/book-audit?isbn=${isbn}&orderId=${orderDetail.value.orderId}`,
+        });
+        uni.setStorageSync("auditBook", book);
+        uni.setStorageSync("orderDetail", orderDetail.value);
+    } else {
+        let text = `此订单中不存在${isbnStr}这本书 `;
+        uni.$u.ttsModule.speak(text);
+    }
+}
+
+//扫码
+function handleScanCode() {
+    uni.scanCode({
+        success: (res) => {
+            res.result && handleScan(res.result);
+        },
+    });
+}
+
+const orderId = ref("");
+const isOnLoad = ref(false);
+const hasLoadedOrderDetail = ref(false); // 添加标志位控制订单详情是否已加载
+const hasLoadedCheckUser = ref(false); // 添加标志位控制复审员信息是否已加载
+
+// 1 表示到货复审 2 表示查看订单
+const type = ref(1);
+onLoad((options) => {
+    isOnLoad.value = true;
+    orderId.value = options.id;
+    orderDetail.value = uni.getStorageSync("orderDetail") || {};
+    type.value = options.type || 1;
+
+    uni.removeStorageSync("scannedBooks");
+});
+
+
+onShow(() => {
+    // 只有在首次加载时才调用getOrderDetail,避免重复请求
+    getOrderDetail();
+});
+</script>
+
+<style>
+page {
+    background-color: #f5f5f5;
+}
+</style>
+<style lang="scss" scoped>
+.order-detail {
+    font-size: 30rpx;
+    padding-bottom: 140rpx;
+    position: relative;
+
+    &.fixed-bottom-2 {
+        padding-bottom: 30rpx;
+    }
+
+    .bind-audit {
+        padding: 20rpx 30rpx;
+        border-bottom: 1px solid #e5e5e5;
+    }
+
+    .fixed-left {
+        position: fixed;
+        left: 0;
+        top: 11%;
+        width: 100rpx;
+
+        .common-bg {
+            background-color: rgba(34, 172, 56, 0.7);
+            border-radius: 0 30rpx 30rpx 0;
+            font-weight: 500;
+            color: #ffffff;
+        }
+
+        .book-status-item {
+            padding: 20rpx;
+            border-top: 1rpx solid #ffffff;
+            border-bottom: 1rpx solid #ffffff;
+        }
+    }
+
+    .fixed-right {
+        position: fixed;
+        right: 0;
+        top: 12%;
+        width: 70rpx;
+
+        .letter-bg {
+            background-color: rgba(34, 172, 56, 0.7);
+            border-radius: 10rpx 0 0 10rpx;
+            font-weight: 500;
+            color: #ffffff;
+            padding: 12rpx 0;
+            padding-bottom: 6rpx;
+        }
+
+        .letter-item {
+            padding-bottom: 12rpx;
+        }
+    }
+}
+</style>

File diff suppressed because it is too large
+ 0 - 1
unpackage/dist/build/app-plus/app-config-service.js


File diff suppressed because it is too large
+ 0 - 0
unpackage/dist/build/app-plus/app-service.js


File diff suppressed because it is too large
+ 0 - 0
unpackage/dist/build/app-plus/app.css


+ 1 - 1
unpackage/dist/build/app-plus/manifest.json

@@ -171,7 +171,7 @@
     "uni-app": {
       "control": "uni-v3",
       "vueVersion": "3",
-      "compilerVersion": "4.76",
+      "compilerVersion": "4.85",
       "nvueCompiler": "uni-app",
       "renderer": "auto",
       "nvue": {

File diff suppressed because it is too large
+ 0 - 0
unpackage/dist/build/app-plus/pages/index/audit/review-order.css


File diff suppressed because it is too large
+ 0 - 0
unpackage/dist/build/app-plus/pages/index/detail/index.css


File diff suppressed because it is too large
+ 0 - 0
unpackage/dist/build/app-plus/pages/index/detail/review-book.css


File diff suppressed because it is too large
+ 0 - 0
unpackage/dist/build/app-plus/pages/index/detail/review-detail.css


File diff suppressed because it is too large
+ 0 - 0
unpackage/dist/build/app-plus/pages/index/express/weight-modify.css


File diff suppressed because it is too large
+ 0 - 0
unpackage/dist/build/app-plus/pages/index/wms/order-query-list.css


+ 0 - 0
unpackage/dist/build/app-plus/pages/order/stat/pending-audit.css


+ 0 - 0
unpackage/dist/build/app-plus/pages/order/stat/pending-confirm.css


+ 0 - 0
unpackage/dist/build/app-plus/pages/order/stat/pending-payment.css


+ 0 - 0
unpackage/dist/build/app-plus/pages/order/stat/pending-pick.css


+ 0 - 0
unpackage/dist/build/app-plus/pages/order/stat/pending-review.css


+ 0 - 0
unpackage/dist/build/app-plus/pages/order/stat/pending-sign.css


File diff suppressed because it is too large
+ 0 - 0
unpackage/dist/build/app-plus/uni-app-view.umd.js


+ 3 - 3
unpackage/dist/cache/.vite/deps/_metadata.json

@@ -1,8 +1,8 @@
 {
-  "hash": "d5a0ebfd",
-  "configHash": "f0b6cb8a",
+  "hash": "3211f8c4",
+  "configHash": "194cc637",
   "lockfileHash": "e3b0c442",
-  "browserHash": "e4b9a606",
+  "browserHash": "062c5002",
   "optimized": {},
   "chunks": {}
 }

Some files were not shown because too many files changed in this diff