Parcourir la source

feat 复审列表功能

ylong il y a 2 mois
Parent
commit
37ae3aa0fa

+ 48 - 13
src/views/recycleOrder/components/order-page-all.vue

@@ -13,16 +13,26 @@
                 </template>
 
                 <template #status="{ row }">
-                    <el-tag type="danger" v-if="row.cancelStatus == 1">已取消</el-tag>
-                    <dict-data v-else code="order_status" type="tag" :model-value="row.status" />
-                    <div v-if="row.status == 4 && row.auditRejectReason">
-                        原因:
-                        <el-text type="info">{{ row.auditRejectReason }}</el-text>
-                    </div>
-                    <div class v-if="row.cancelReason && row.cancelStatus == 1">
-                        取消原因:
-                        <el-text type="info">{{ row.cancelReason }}</el-text>
-                    </div>
+                    <template v-if="permissionKey === 'review'">
+                        <span>
+                            {{ row.reviewStatus === 1 ? '未复审' : row.reviewStatus === 2 ? '已复审' : '-' }}
+                        </span>
+                        <div style="font-size: 12px;display: flex;">
+                            已用时长: <time-clock :start-time="row.applyTime" :end-time="row.finishTime" />
+                        </div>
+                    </template>
+                    <template v-else>
+                        <el-tag type="danger" v-if="row.cancelStatus == 1">已取消</el-tag>
+                        <dict-data v-else code="order_status" type="tag" :model-value="row.status" />
+                        <div v-if="row.status == 4 && row.auditRejectReason">
+                            原因:
+                            <el-text type="info">{{ row.auditRejectReason }}</el-text>
+                        </div>
+                        <div class v-if="row.cancelReason && row.cancelStatus == 1">
+                            取消原因:
+                            <el-text type="info">{{ row.cancelReason }}</el-text>
+                        </div>
+                    </template>
                 </template>
                 <template #orderNumber="{ row }">
                     <order-number :row="row" />
@@ -34,7 +44,17 @@
                     <order-amount :row="row" />
                 </template>
                 <template #time="{ row }">
-                    <order-time :row="row" />
+                    <div class="recycle-order-number" v-if="permissionKey === 'review'">
+                        <div class="common-text">
+                            <el-text>申请复审:</el-text>
+                            <el-text>{{ row.applyTime }}</el-text>
+                        </div>
+                        <div class="common-text">
+                            <el-text>复审完成:</el-text>
+                            <el-text>{{ row.finishTime || '-' }}</el-text>
+                        </div>
+                    </div>
+                    <order-time :row="row" v-else />
                 </template>
                 <template #remarks="{ row }">
                     <el-popover trigger="hover" width="240px" @show="handleShowPopover(row)" @hide="showOrderId = ''">
@@ -95,6 +115,10 @@
                             v-permission="usePermission('auditScreenshot')" @click="handleAuditScreenshot(row)">
                             [审核截图]
                         </el-button>
+                        <el-button type="danger" link v-if="permissionKey == 'review'"
+                            v-permission="usePermission('reviewScreenshot')" @click="handleAuditScreenshot(row)">
+                            [复审截图]
+                        </el-button>
                         <el-button type="danger" link v-if="row.status == 10" v-permission="usePermission('payment')"
                             @click="handleBatchPayment(row)">
                             [支付书款]
@@ -170,6 +194,8 @@
         <orderDetail ref="orderDetailRef" @refresh="reload()" />
         <!-- 审核截图 -->
         <auditScreenshot ref="auditScreenshotRef" />
+        <!-- 复审截图 -->
+        <reviewScreenshot ref="reviewScreenshotRef" />
         <!-- 申请订单理赔 -->
         <applyForOrderClaim ref="applyForOrderClaimRef" />
         <!-- 售后补款 -->
@@ -190,6 +216,7 @@ import { EleMessage } from "ele-admin-plus/es";
 import { DownloadOutlined } from "@/components/icons";
 import { Flag, ChatDotSquare } from "@element-plus/icons-vue";
 import OrderSearch from "../components/order-search.vue";
+import TimeClock from "@/views/recycleOrder/components/time-clock.vue";
 import OrderNumber from "@/views/recycleOrder/components/order-number.vue";
 import OrderCustomer from "@/views/recycleOrder/components/order-customer.vue";
 import OrderAmount from "@/views/recycleOrder/components/order-amount.vue";
@@ -210,6 +237,8 @@ import userBindTag from "@/views/recycleOrder/components/user-bind-tag.vue";
 import orderDetail from "@/views/recycleOrder/components/order-detail.vue";
 //审核截图
 import auditScreenshot from "@/views/recycleOrder/components/audit-screenshot.vue";
+//复审截图
+import reviewScreenshot from "@/views/recycleOrder/components/review-screenshot.vue";
 //推送短信
 import sendSMS from "@/views/recycleOrder/components/send-SMS.vue";
 //短信记录
@@ -231,6 +260,7 @@ let props = defineProps({
     exportUrl: { type: String, default: "/system/post/export" },
     permissionKey: { type: String, default: "search" },
     propColumns: { type: Array, default: () => [] },
+    formatterData: { type: Function, default: (data) => data },
 });
 const usePermission = computed(() => (opts) => {
     return `recycleOrder:${props.permissionKey}:${opts}`;
@@ -293,7 +323,7 @@ async function queryPage(params) {
     if (!props.pageConfig.pageUrl) return EleMessage.error("未配置页面请求URL");
     const res = await proxy.$http.get(props.pageConfig.pageUrl, { params });
     if (res.data.code === 200) {
-        return res.data;
+        return props.formatterData(res.data);
     }
     return Promise.reject(new Error(res.data.msg));
 }
@@ -601,8 +631,13 @@ function fallbackOrder(row) {
 }
 //审核截图
 const auditScreenshotRef = ref(null);
+const reviewScreenshotRef = ref(null);
 function handleAuditScreenshot(row) {
-    auditScreenshotRef.value?.handleOpen(row);
+    if (props.permissionKey == 'review') {
+        reviewScreenshotRef.value?.handleOpen(row);
+    } else {
+        auditScreenshotRef.value?.handleOpen(row);
+    }
 }
 
 //申请恢复订单

+ 81 - 0
src/views/recycleOrder/components/review-screenshot.vue

@@ -0,0 +1,81 @@
+<!-- 编辑弹窗 -->
+<template>
+    <ele-modal :width="700" v-model="visible" title="复审截图">
+        <div v-if="imageList.length" class="flex flex-row gap-6 flex-wrap">
+            <div
+                class="demo-image__preview"
+                v-for="(item, index) in imageList"
+                :key="index"
+            >
+                <div class="demo-image__info" style="margin-bottom: 6px">
+                    说明:{{ item.remark || '--' }}
+                </div>
+                <div class="no-pic" v-if="!item.imgInfo.length">
+                    暂无图片
+                </div>
+
+                <el-image
+                    v-else
+                    v-for="(url, idx) in item.imgInfo"
+                    :key="idx"
+                    style="width: 100px; height: 100px; margin-right: 10px"
+                    :src="url"
+                    :preview-src-list="item.imgInfo"
+                    :initial-index="idx"
+                    fit="cover"
+                />
+
+
+            </div>
+        </div>
+        <el-empty v-else description="暂无复审截图" />
+
+        <template #footer>
+            <el-button @click="handleCancel">关闭</el-button>
+        </template>
+    </ele-modal>
+</template>
+
+<script setup>
+    import { ref, getCurrentInstance, nextTick } from 'vue';
+    const { proxy } = getCurrentInstance();
+
+    /** 弹窗是否打开 */
+    const visible = defineModel({ type: Boolean });
+
+    /** 关闭弹窗 */
+    const handleCancel = () => {
+        visible.value = false;
+    };
+
+    //获取审核截图
+    const imageList = ref([]);
+    function getAuditScreenshot(id) {
+        proxy.$http
+            .get(`/order/recycleOrderReview/getImgInfo?id=${id}`)
+            .then((res) => {
+                imageList.value = res.data.data || [];
+            });
+    }
+
+    /** 弹窗打开事件 */
+    const handleOpen = (row) => {
+        visible.value = true;
+        row.id && getAuditScreenshot(row.id);
+    };
+
+    defineExpose({
+        handleOpen
+    });
+</script>
+<style>
+.no-pic {
+    width: 100px;
+    height: 100px;
+    background-color: #f5f7fa;
+    border-radius: 4px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+</style>

+ 109 - 0
src/views/recycleOrder/components/time-clock.vue

@@ -0,0 +1,109 @@
+<template>
+    <div class="time-clock">
+        {{ formattedTime }}
+    </div>
+</template>
+
+<script setup>
+import { ref, watch, onUnmounted, computed } from 'vue'
+
+/**
+ * 组件 props 定义
+ * @param {string|Date} startTime - 开始时间(必填,格式:YYYY-MM-DD HH:mm:ss 或 Date 对象)
+ * @param {string|Date} endTime - 结束时间(可选,格式同上,传则定格,不传则计时)
+ */
+const props = defineProps({
+    startTime: {
+        type: [String, Date],
+        required: true,
+        validator: (value) => {
+            const date = new Date(value)
+            return !isNaN(date.getTime())
+        }
+    },
+    endTime: {
+        type: [String, Date],
+        required: false,
+        validator: (value) => {
+            if (!value) return true
+            const date = new Date(value)
+            return !isNaN(date.getTime())
+        }
+    }
+})
+
+// 累计秒数(核心:基于开始时间与当前时间的差值初始化,再每秒递增)
+const totalSeconds = ref(0)
+
+// 格式化时间:将累计秒数转为 00:00:00 格式
+const formattedTime = computed(() => {
+    const hours = String(Math.floor(totalSeconds.value / 3600)).padStart(2, '0')
+    const minutes = String(Math.floor((totalSeconds.value % 3600) / 60)).padStart(2, '0')
+    const seconds = String(totalSeconds.value % 60).padStart(2, '0')
+    return `${hours}:${minutes}:${seconds}`
+})
+
+let timer = null
+
+// 初始化时间逻辑(核心优化点)
+const initTime = () => {
+    if (timer) clearInterval(timer)
+
+    const startDate = new Date(props.startTime)
+    const endDate = props.endTime ? new Date(props.endTime) : null
+    const now = new Date()
+
+    // 基础校验
+    if (endDate && endDate <= startDate) {
+        // 结束时间早于开始时间:直接显示开始时间的时分秒
+        totalSeconds.value = calculateSecondsFromDate(startDate)
+        return
+    }
+
+    // 有结束时间:计算开始到结束的总秒数,定格显示
+    if (endDate) {
+        const diffMs = endDate - startDate
+        totalSeconds.value = Math.max(0, Math.floor(diffMs / 1000)) // 避免负数
+        return
+    }
+
+    // 无结束时间:初始化时计算「当前时间 - 开始时间」的差值,作为初始秒数
+    const diffMs = now - startDate
+    totalSeconds.value = Math.max(0, Math.floor(diffMs / 1000)) // 若开始时间在未来,初始为 0
+
+    // 继续每秒递增计时
+    timer = setInterval(() => {
+        totalSeconds.value += 1
+    }, 1000)
+}
+
+// 辅助函数:将 Date 对象转为「当天0点到当前时间的总秒数」(用于定格场景)
+const calculateSecondsFromDate = (date) => {
+    const hours = date.getHours()
+    const minutes = date.getMinutes()
+    const seconds = date.getSeconds()
+    return hours * 3600 + minutes * 60 + seconds
+}
+
+// 初始化
+initTime()
+
+// 监听 props 变化(支持动态修改开始/结束时间)
+watch([() => props.startTime, () => props.endTime], initTime, {
+    deep: true
+})
+
+// 卸载清理
+onUnmounted(() => {
+    if (timer) clearInterval(timer)
+})
+</script>
+
+<style scoped>
+.time-clock {
+    font-size: 13px;
+    color: #ff0000;
+    letter-spacing: 2px;
+    font-weight: 500;
+}
+</style>

+ 165 - 0
src/views/recycleService/review/components/order-search.vue

@@ -0,0 +1,165 @@
+<!-- 搜索表单 -->
+<template>
+    <ele-card :body-style="{ paddingBottom: '8px' }">
+        <ProSearch :items="columns" ref="searchRef" :isShowLabel="false" @search="search">
+            <el-col :span="6" style="min-width: 160px">
+                <el-button style="width: 80px" type="primary" plain @click="search">查询</el-button>
+                <el-button style="width: 80px" type="info" @click="reset">重置</el-button>
+            </el-col>
+        </ProSearch>
+    </ele-card>
+</template>
+
+<script setup>
+import { reactive, ref, defineEmits } from 'vue';
+import { useFormData } from '@/utils/use-form-data';
+import ProSearch from '@/components/CommonPage/ProSearch2.vue';
+import request from '@/utils/request';
+
+const emit = defineEmits(['search']);
+const columns = ref([
+    {
+        type: 'input',
+        label: '发件人名称',
+        prop: 'sendNameLike'
+    },
+    {
+        type: 'input',
+        label: '发件人电话',
+        prop: 'sendMobileLike'
+    },
+    {
+        type: 'input',
+        label: '发件人地址(仅查询7天内数据)',
+        prop: 'sendAddressLike'
+    },
+    {
+        type: 'input',
+        label: '用户名',
+        prop: 'userName'
+    },
+    {
+        type: 'dictSelect',
+        label: '售后状态',
+        prop: 'refundStatus',
+        props: { code: 'recycle_refund_status' }
+    },
+    {
+        type: 'autocomplete',
+        label: '全部备注',
+        prop: 'remark',
+        props: {
+            fetchSuggestions: (queryString, cb) => {
+                cb([
+                    { value: '无内容', label: '0' },
+                    { value: '有内容', label: '1' }
+                ]);
+            }
+        }
+    },
+    {
+        type: 'select',
+        label: '收货仓库',
+        prop: 'godownId',
+        options: []
+    },
+    {
+        type: 'dictSelect',
+        label: '订单类型',
+        prop: 'orderType',
+        props: { code: 'order_type' }
+    },
+    {
+        type: 'dictSelect',
+        label: '物流公司',
+        prop: 'finalExpress',
+        props: { code: 'final_express' }
+    },
+    {
+        type: 'dictSelect',
+        label: '全部方式',
+        prop: 'expressType',
+        props: { code: 'express_type' }
+    },
+    {
+        type: 'datetime',
+        label: '建单时间(开始时间)',
+        prop: 'createTimeStart',
+        props: {
+            format: 'YYYY-MM-DD HH:mm:ss',
+            valueFormat: 'YYYY-MM-DD HH:mm:ss'
+        }
+    },
+    {
+        type: 'datetime',
+        label: '建单时间(结束时间)',
+        prop: 'createTimeEnd',
+        props: {
+            valueFormat: 'YYYY-MM-DD HH:mm:ss'
+        }
+    },
+    {
+        type: 'datetime',
+        label: '提交时间(开始时间)',
+        prop: 'orderTimeStart',
+        props: {
+            valueFormat: 'YYYY-MM-DD HH:mm:ss'
+        }
+    },
+    {
+        type: 'datetime',
+        label: '提交时间(结束时间)',
+        prop: 'orderTimeEnd',
+        props: {
+            type: 'datetime',
+            valueFormat: 'YYYY-MM-DD HH:mm:ss'
+        }
+    },
+    {
+        type: 'input',
+        label: '订单编号(多个以中文逗号、空格或换行分割)',
+        prop: 'orderIds',
+        colProps: { span: 8 }
+    },
+    {
+        type: 'input',
+        label: '物流单号(多个以中文逗号、空格或换行分割)',
+        prop: 'waybillCodes',
+        colProps: { span: 8 }
+    }
+]);
+
+//获取仓库数据
+function getGodownList() {
+    request.post('/baseinfo/godown/searchGodown?name=').then((res) => {
+        if (res.data.code === 200) {
+            let item = columns.value.find(
+                (item) => item.prop === 'godownId'
+            );
+            item.options = res.data.data.map((item) => ({
+                label: item.godownName,
+                value: item.id
+            }));
+        }
+    });
+}
+getGodownList();
+
+const searchRef = ref(null);
+/** 搜索 */
+const search = (data) => {
+    data.remark =
+        data.remark === '无内容'
+            ? '0'
+            : data.remark === '有内容'
+                ? '1'
+                : data.remark;
+    emit('search', { ...data });
+};
+
+/** 重置 */
+const reset = () => {
+    resetFields();
+    search();
+};
+</script>

+ 75 - 0
src/views/recycleService/review/index.vue

@@ -0,0 +1,75 @@
+<template>
+    <order-page
+        ref="pageRef"
+        :pageConfig="pageConfig"
+        permissionKey="review"
+        :formatterData="formatterData"
+    >
+        <template #toolbar>
+            <el-button
+                type="success"
+                plain
+                class="ele-btn-icon"
+                :icon="DownloadOutlined"
+                v-permission="'recycleOrder:review:export'"
+                @click="exportData"
+            >
+                导出订单明细
+            </el-button>
+
+            <el-radio-group
+                v-model="status"
+                @change="handleExpressTypeChange"
+                style="position: relative; top: -3px; margin-left: 20px"
+            >
+                <el-radio-button label="全部" value="" />
+                <el-radio-button label="普通上门取件" value="1" />
+            </el-radio-group>
+        </template>
+    </order-page>
+</template>
+
+<script setup>
+    import { ref, reactive } from 'vue';
+    import { ElMessageBox } from 'element-plus/es';
+    import { DownloadOutlined } from '@/components/icons';
+    import OrderPage from '@/views/recycleOrder/components/order-page-all.vue';
+    import { useRouter } from 'vue-router';
+
+    defineOptions({ name: 'RecycleOrderReview' });
+
+    let router = useRouter();
+    /** 页面组件实例 */
+    const pageRef = ref(null);
+    const status = ref('');
+
+    const pageConfig = reactive({
+        pageUrl: '/order/recycleOrderReview/pagelist',
+        exportUrl: '/order/recycleOrderReview/export',
+        fileName: '复审订单',
+        cacheKey: 'awaitReviewTable',
+        where: {
+            expressType: ''
+        }
+    });
+
+    /** 格式化数据 */
+    const formatterData = (data) => {
+        data.rows.forEach((item) => {
+            for (let key in item.orderInfoResult) {
+                item[key] = item.orderInfoResult[key];
+            }
+        });
+        return data;
+    };
+
+    function handleExpressTypeChange(val) {
+        pageConfig.where.expressType = val;
+        pageRef.value?.reload({ expressType: val });
+    }
+
+    //导出数据
+    function exportData() {
+        pageRef.value?.exportData('复审订单');
+    }
+</script>