فهرست منبع

feat(mallOrder/refund): 新增退款订单备注管理功能,调整开发环境API地址

- 更新开发环境接口地址为 https://bpi.shuhi.com
- 新增退款备注弹窗和历史时间线组件,支持标签选择与备注提交
- 在退款列表项和详情页添加备注入口,点击即可管理备注
- 提交备注后自动刷新订单列表与备注历史记录
- 优化退款详情弹窗的模板代码格式
ylong 1 ماه پیش
والد
کامیت
fe52192717

+ 1 - 1
.env.development

@@ -1,3 +1,3 @@
 # 开发环境接口地址
-VITE_API_URL=https://bk.shuhi.com
+VITE_API_URL=https://bpi.shuhi.com
 #VITE_API_URL=http://localhost:8080

+ 148 - 369
src/views/mallOrder/refund/components/refund-detail-dialog.vue

@@ -1,11 +1,6 @@
 <template>
-    <ele-modal
-        :width="1200"
-        v-model="visible"
-        title="退款详情"
-        fullscreen
-        :body-style="{ padding: '0 20px', height: 'calc(100vh - 120px)' }"
-    >
+    <ele-modal :width="1200" v-model="visible" title="退款详情" fullscreen
+        :body-style="{ padding: '0 20px', height: 'calc(100vh - 120px)' }">
         <div v-loading="loading" class="h-full flex flex-col">
             <!-- 顶部状态栏 -->
             <div class="status-bar mb-6">
@@ -16,42 +11,29 @@
                         </div>
 
                         <!-- 倒计时组件 -->
-                        <div
-                            v-if="deadline > 0"
-                            class="flex items-center text-red-500 text-sm mb-3"
-                        >
+                        <div v-if="deadline > 0" class="flex items-center text-red-500 text-sm mb-3">
                             <span class="mr-2">剩余</span>
-                            <el-countdown
-                                format="DD [天] HH [时] mm [分]"
-                                :value="deadline"
-                                :value-style="{
-                                    fontSize: '14px',
-                                    color: '#ef4444',
-                                    lineHeight: '1'
-                                }"
-                            />
+                            <el-countdown format="DD [天] HH [时] mm [分]" :value="deadline" :value-style="{
+                                fontSize: '14px',
+                                color: '#ef4444',
+                                lineHeight: '1'
+                            }" />
                         </div>
 
                         <div class="text-sm text-gray-500 space-y-1">
-                            <template
-                                v-if="['1', '3'].includes(String(form.status))"
-                            >
+                            <template v-if="['1', '3'].includes(String(form.status))">
                                 <div>{{
                                     isRefundOnly
                                         ? '请及时联系买家协商退款事宜'
                                         : '请及时联系买家协商退货事宜'
                                 }}</div>
-                                <div v-if="form.refundType == 1"
-                                    >该退款为卖家原因退款,若您同意买家退货,退货运费将由您承担。</div
-                                >
+                                <div v-if="form.refundType == 1">该退款为卖家原因退款,若您同意买家退货,退货运费将由您承担。</div>
                             </template>
 
-                            <template
-                                v-else-if="
-                                    String(form.status) === '7' &&
-                                    String(form.refundType) === '1'
-                                "
-                            >
+                            <template v-else-if="
+                                String(form.status) === '7' &&
+                                String(form.refundType) === '1'
+                            ">
                                 <div>买家已发货,请及时验货并处理退款</div>
                             </template>
 
@@ -61,53 +43,37 @@
 
                             <template v-else-if="String(form.status) === '2'">
                                 <div>买家修改退款申请后,需要您重新处理</div>
-                                <div
-                                    >如果买家超时未响应,退款申请将自动关闭</div
-                                >
+                                <div>如果买家超时未响应,退款申请将自动关闭</div>
                             </template>
 
                             <template v-else-if="String(form.status) === '10'">
                                 <div>退款成功时间:{{ form.finishTime }}</div>
-                                <div
-                                    >退款金额:¥
+                                <div>退款金额:¥
                                     {{
                                         form.refundMoneyFinal ||
                                         form.refundMoney
-                                    }}元</div
-                                >
+                                    }}元</div>
                                 <div>退款规则:符合未发货秒退 </div>
                             </template>
                         </div>
                     </div>
                     <div style="min-width: 600px" class="pt-2">
-                        <el-steps
-                            :active="currentStep"
-                            align-center
-                            finish-status="success"
-                            :process-status="processStatus"
-                        >
+                        <el-steps :active="currentStep" align-center finish-status="success"
+                            :process-status="processStatus">
                             <template v-if="isRefundOnly">
-                                <el-step
-                                    :title="`买家申请${getRefundTypeText(form.refundType)}`"
-                                />
-                                <el-step
-                                    :title="
-                                        String(form.status) === '11'
-                                            ? '退款已撤销'
-                                            : '卖家处理退款申请'
-                                    "
-                                />
+                                <el-step :title="`买家申请${getRefundTypeText(form.refundType)}`" />
+                                <el-step :title="String(form.status) === '11'
+                                        ? '退款已撤销'
+                                        : '卖家处理退款申请'
+                                    " />
                                 <el-step title="退款完毕" />
                             </template>
                             <template v-else>
                                 <el-step title="买家申请退货退款" />
-                                <el-step
-                                    :title="
-                                        String(form.status) === '11'
-                                            ? '退款已撤销'
-                                            : '卖家处理退货申请'
-                                    "
-                                />
+                                <el-step :title="String(form.status) === '11'
+                                        ? '退款已撤销'
+                                        : '卖家处理退货申请'
+                                    " />
                                 <el-step title="买家退货" />
                                 <el-step title="退款完毕" />
                             </template>
@@ -124,10 +90,7 @@
                         <span class="label">退款金额:</span>
                         <span class="value text-red-500 font-bold">
                             ¥{{ form.refundMoney }}
-                            <span
-                                v-if="form.refundMoneyFinal"
-                                class="text-xs text-gray-500 ml-1"
-                            >
+                            <span v-if="form.refundMoneyFinal" class="text-xs text-gray-500 ml-1">
                                 (实退: ¥{{ form.refundMoneyFinal }})
                             </span>
                         </span>
@@ -140,13 +103,13 @@
                         <span class="label">要求:</span>
                         <span class="value">{{
                             getRefundTypeText(form.refundType)
-                        }}</span>
+                            }}</span>
                     </div>
                     <div class="info-item">
                         <span class="label">货物状态:</span>
                         <span class="value">{{
                             getShopStatusText(form.shopStatus)
-                        }}</span>
+                            }}</span>
                     </div>
                     <div class="info-item">
                         <span class="label">买家留言:</span>
@@ -155,10 +118,7 @@
                     <div class="info-item">
                         <span class="label">退款编号:</span>
                         <span class="value">{{ form.refundOrderId }}</span>
-                        <el-icon
-                            class="copy-icon"
-                            @click="handleCopy(form.refundOrderId)"
-                        >
+                        <el-icon class="copy-icon" @click="handleCopy(form.refundOrderId)">
                             <CopyDocument />
                         </el-icon>
                     </div>
@@ -168,76 +128,40 @@
                             <div>
                                 {{ form.expressName || '-' }}
                                 {{ form.waybillCode }}
-                                <el-popover
-                                    placement="bottom"
-                                    title="物流动态"
-                                    :width="400"
-                                    trigger="hover"
-                                >
+                                <el-popover placement="bottom" title="物流动态" :width="400" trigger="hover">
                                     <template #reference>
-                                        <el-button
-                                            link
-                                            type="success"
-                                            class="ml-2"
-                                            >查看</el-button
-                                        >
+                                        <el-button link type="success" class="ml-2">查看</el-button>
                                     </template>
-                                    <el-timeline
-                                        style="
+                                    <el-timeline style="
                                             max-height: 400px;
                                             overflow-y: auto;
-                                        "
-                                        v-if="
+                                        " v-if="
                                             form.trackingVoList &&
                                             form.trackingVoList.length > 0
-                                        "
-                                    >
-                                        <el-timeline-item
-                                            v-for="(
-                                                activity, index
-                                            ) in form.trackingVoList"
-                                            :key="index"
-                                            :timestamp="activity.ftime"
-                                            :type="index === 0 ? 'success' : ''"
-                                            :color="index === 0 ? '#0bbd87' : ''"
-                                        >
+                                        ">
+                                        <el-timeline-item v-for="(
+activity, index
+                                            ) in form.trackingVoList" :key="index" :timestamp="activity.ftime"
+                                            :type="index === 0 ? 'success' : ''" :color="index === 0 ? '#0bbd87' : ''">
                                             <div>
-                                                <div
-                                                    style="margin-bottom: 5px"
-                                                    v-if="activity.status"
-                                                >
-                                                    <el-tag
-                                                        size="small"
-                                                        :type="
-                                                            index === 0
-                                                                ? 'success'
-                                                                : 'info'
-                                                        "
-                                                        >{{ activity.status }}</el-tag
-                                                    >
+                                                <div style="margin-bottom: 5px" v-if="activity.status">
+                                                    <el-tag size="small" :type="index === 0
+                                                            ? 'success'
+                                                            : 'info'
+                                                        ">{{ activity.status }}</el-tag>
                                                 </div>
-                                                <div
-                                                    style="
+                                                <div style="
                                                         color: #606266;
                                                         font-size: 13px;
                                                         line-height: 1.5;
-                                                    "
-                                                    >{{ activity.context }}</div
-                                                >
+                                                    ">{{ activity.context }}</div>
                                             </div>
                                         </el-timeline-item>
                                     </el-timeline>
-                                    <el-empty
-                                        v-else
-                                        description="暂无物流信息"
-                                        :image-size="60"
-                                    />
+                                    <el-empty v-else description="暂无物流信息" :image-size="60" />
                                 </el-popover>
                             </div>
-                            <div
-                                v-if="returnLatestTrack"
-                                class="text-xs text-gray-500 mt-1"
-                            >
+                            <div v-if="returnLatestTrack" class="text-xs text-gray-500 mt-1">
                                 {{ returnLatestTrack.context }}
                                 {{ returnLatestTrack.ftime }}
                             </div>
@@ -246,32 +170,17 @@
                 </div>
 
                 <!-- 中间:交易信息 -->
-                <div
-                    class="section transaction-info flex-[1.2] overflow-y-auto"
-                >
+                <div class="section transaction-info flex-[1.2] overflow-y-auto">
                     <div class="section-title">交易信息</div>
                     <div class="product-list mb-4">
-                        <div
-                            v-for="(prod, idx) in form.detailList"
-                            :key="idx"
-                            class="product-item flex mb-2"
-                        >
-                            <el-image
-                                :src="prod.cover"
-                                class="w-16 h-16 rounded mr-2"
-                                fit="cover"
-                            />
+                        <div v-for="(prod, idx) in form.detailList" :key="idx" class="product-item flex mb-2">
+                            <el-image :src="prod.cover" class="w-16 h-16 rounded mr-2" fit="cover" />
                             <div class="flex-1">
-                                <div
-                                    class="text-sm font-bold truncate-2-lines"
-                                    >{{ prod.bookName }}</div
-                                >
+                                <div class="text-sm font-bold truncate-2-lines">{{ prod.bookName }}</div>
                                 <div class="text-xs text-gray-500 mt-1">{{
                                     prod.isbn
-                                }}</div>
-                                <div class="text-xs text-gray-500 mt-1"
-                                    >{{ prod.payPrice }} × {{ prod.num }}</div
-                                >
+                                    }}</div>
+                                <div class="text-xs text-gray-500 mt-1">{{ prod.payPrice }} × {{ prod.num }}</div>
                             </div>
                         </div>
                     </div>
@@ -279,35 +188,21 @@
                     <div class="info-item">
                         <span class="label">买家:</span>
                         <div class="flex items-center">
-                            <el-avatar
-                                :size="20"
-                                :src="form.avatar"
-                                class="mr-2"
-                            />
-                            <span class="value text-blue-500"
-                                >{{ form.userNick }}/{{ form.userId }}</span
-                            >
+                            <el-avatar :size="20" :src="form.avatar" class="mr-2" />
+                            <span class="value text-blue-500">{{ form.userNick }}/{{ form.userId }}</span>
                         </div>
                     </div>
                     <div class="info-item">
                         <span class="label">申请退款:</span>
-                        <span class="value"
-                            >¥{{ form.refundMoney || '0.00' }}</span
-                        >
+                        <span class="value">¥{{ form.refundMoney || '0.00' }}</span>
                     </div>
                     <div class="info-item">
                         <span class="label">邮费:</span>
-                        <span class="value"
-                            >¥{{ form.expressMoney || '0.00' }}</span
-                        >
+                        <span class="value">¥{{ form.expressMoney || '0.00' }}</span>
                     </div>
                     <div class="info-item">
                         <span class="label">订单标签:</span>
-                        <dict-data
-                            code="shop_refund_status"
-                            v-model="form.status"
-                            type="tag"
-                        />
+                        <dict-data code="shop_refund_status" v-model="form.status" type="tag" />
                     </div>
                     <div class="info-item">
                         <span class="label">成交时间:</span>
@@ -317,116 +212,67 @@
                         <span class="label">订单编号:</span>
                         <span class="value text-blue-500">{{
                             form.originOrderId
-                        }}</span>
-                        <el-icon
-                            class="copy-icon"
-                            @click="handleCopy(form.originOrderId)"
-                        >
+                            }}</span>
+                        <el-icon class="copy-icon" @click="handleCopy(form.originOrderId)">
                             <CopyDocument />
                         </el-icon>
                     </div>
 
                     <div class="logistics-info mt-4 p-3 bg-gray-50 rounded">
                         <div class="font-bold mb-2">原始订单物流信息:</div>
-                        <template
-                            v-if="
-                                form.originTrackingList &&
-                                form.originTrackingList.length
-                            "
-                        >
-                            <div
-                                v-for="(item, idx) in form.originTrackingList"
-                                :key="idx"
-                                class="mb-3"
-                            >
+                        <template v-if="
+                            form.originTrackingList &&
+                            form.originTrackingList.length
+                        ">
+                            <div v-for="(item, idx) in form.originTrackingList" :key="idx" class="mb-3">
                                 <div class="text-sm mb-1 flex items-center">
                                     <span>
                                         {{ item.originExpressName || '-' }}
                                         {{ item.originWaybillCode || '-' }}
                                     </span>
-                                    <el-icon
-                                        v-if="item.originWaybillCode"
-                                        class="copy-icon ml-1"
-                                        @click="handleCopy(item.originWaybillCode)"
-                                    >
+                                    <el-icon v-if="item.originWaybillCode" class="copy-icon ml-1"
+                                        @click="handleCopy(item.originWaybillCode)">
                                         <CopyDocument />
                                     </el-icon>
-                                    <el-popover
-                                        placement="bottom"
-                                        title="物流动态"
-                                        :width="400"
-                                        trigger="hover"
-                                    >
+                                    <el-popover placement="bottom" title="物流动态" :width="400" trigger="hover">
                                         <template #reference>
-                                            <el-button
-                                                link
-                                                type="success"
-                                                class="ml-2"
-                                                v-if="item.originWaybillCode"
-                                                >查看</el-button
-                                            >
+                                            <el-button link type="success" class="ml-2"
+                                                v-if="item.originWaybillCode">查看</el-button>
                                         </template>
-                                        <el-timeline
-                                            style="
+                                        <el-timeline style="
                                                 max-height: 400px;
                                                 overflow-y: auto;
-                                            "
-                                            v-if="
+                                            " v-if="
                                                 item.originTrackingVoList &&
                                                 item.originTrackingVoList.length > 0
-                                            "
-                                        >
-                                            <el-timeline-item
-                                                v-for="(
-                                                    activity, index
-                                                ) in item.originTrackingVoList"
-                                                :key="index"
-                                                :timestamp="activity.ftime"
-                                                :type="
-                                                    index === 0 ? 'success' : ''
-                                                "
-                                                :color="index === 0 ? '#0bbd87' : ''"
-                                            >
+                                            ">
+                                            <el-timeline-item v-for="(
+activity, index
+                                                ) in item.originTrackingVoList" :key="index"
+                                                :timestamp="activity.ftime" :type="index === 0 ? 'success' : ''
+                                                    " :color="index === 0 ? '#0bbd87' : ''">
                                                 <div>
-                                                    <div
-                                                        style="margin-bottom: 5px"
-                                                        v-if="activity.status"
-                                                    >
-                                                        <el-tag
-                                                            size="small"
-                                                            :type="
-                                                                index === 0
-                                                                    ? 'success'
-                                                                    : 'info'
-                                                            "
-                                                            >{{ activity.status }}</el-tag
-                                                        >
+                                                    <div style="margin-bottom: 5px" v-if="activity.status">
+                                                        <el-tag size="small" :type="index === 0
+                                                                ? 'success'
+                                                                : 'info'
+                                                            ">{{ activity.status }}</el-tag>
                                                     </div>
-                                                    <div
-                                                        style="
+                                                    <div style="
                                                             color: #606266;
                                                             font-size: 13px;
                                                             line-height: 1.5;
-                                                        "
-                                                        >{{ activity.context }}</div
-                                                    >
+                                                        ">{{ activity.context }}</div>
                                                 </div>
                                             </el-timeline-item>
                                         </el-timeline>
-                                        <el-empty
-                                            v-else
-                                            description="暂无物流信息"
-                                            :image-size="60"
-                                        />
+                                        <el-empty v-else description="暂无物流信息" :image-size="60" />
                                     </el-popover>
                                 </div>
-                                <div
-                                    v-if="
-                                        item.originTrackingVoList &&
-                                        item.originTrackingVoList.length
-                                    "
-                                    class="text-xs text-gray-500"
-                                >
+                                <div v-if="
+                                    item.originTrackingVoList &&
+                                    item.originTrackingVoList.length
+                                " class="text-xs text-gray-500">
                                     {{ item.originTrackingVoList[0].context }}
                                     {{ item.originTrackingVoList[0].ftime }}
                                 </div>
@@ -439,77 +285,44 @@
                 </div>
 
                 <!-- 右侧:协商历史 -->
-                <div
-                    class="section history-info flex-[1.5] flex flex-col overflow-hidden"
-                >
-                    <div
-                        class="flex justify-between items-center mb-4 flex-shrink-0"
-                    >
+                <div class="section history-info flex-[1.5] flex flex-col overflow-hidden">
+                    <div class="flex justify-between items-center mb-4 flex-shrink-0">
                         <div class="section-title mb-0">协商历史</div>
-                        <el-button
-                            link
-                            type="primary"
-                            @click="handleLeaveMessage"
-                            >我要留言</el-button
-                        >
+                        <el-button link type="primary" @click="handleLeaveMessage">我要留言</el-button>
                     </div>
 
                     <div class="history-list flex-1 overflow-y-auto pr-2">
-                        <div
-                            v-for="(activity, index) in historyList"
-                            :key="index"
-                            class="history-item flex mb-4 pb-4 border-b border-gray-100 last:border-0"
-                        >
+                        <div v-for="(activity, index) in historyList" :key="index"
+                            class="history-item flex mb-4 pb-4 border-b border-gray-100 last:border-0">
                             <div class="avatar-wrapper mr-3">
-                                <el-avatar
-                                    :size="40"
-                                    :src="activity.imgPath"
-                                    shape="square"
-                                    class="rounded-lg"
-                                />
+                                <el-avatar :size="40" :src="activity.imgPath" shape="square" class="rounded-lg" />
                             </div>
                             <div class="content-wrapper flex-1">
-                                <div
-                                    class="header flex justify-between items-start mb-1"
-                                >
-                                    <span
-                                        class="name font-bold text-gray-800"
-                                        >{{
-                                            activity.userName ||
-                                            (activity.userType === '2'
-                                                ? '客服'
-                                                : '用户')
-                                        }}</span
-                                    >
+                                <div class="header flex justify-between items-start mb-1">
+                                    <span class="name font-bold text-gray-800">{{
+                                        activity.userName ||
+                                        (activity.userType === '2'
+                                            ? '客服'
+                                            : '用户')
+                                    }}</span>
                                     <span class="time text-xs text-gray-400">{{
                                         activity.createTime
-                                    }}</span>
+                                        }}</span>
                                 </div>
                                 <div class="main-content text-sm text-gray-600">
-                                    <div
-                                        v-if="activity.title"
-                                        class="font-bold mb-1 text-gray-900"
-                                        >{{ activity.title }}
+                                    <div v-if="activity.title" class="font-bold mb-1 text-gray-900">{{ activity.title }}
                                     </div>
                                     <div class="whitespace-pre-wrap mb-2">{{
                                         activity.content
-                                    }}</div>
+                                        }}</div>
 
-                                    <div
-                                        v-if="
-                                            activity.imgList &&
-                                            activity.imgList.length
-                                        "
-                                        class="mt-2 flex flex-wrap gap-2"
-                                    >
-                                        <el-image
-                                            v-for="(img, i) in activity.imgList"
-                                            :key="i"
-                                            :src="img"
+                                    <div v-if="
+                                        activity.imgList &&
+                                        activity.imgList.length
+                                    " class="mt-2 flex flex-wrap gap-2">
+                                        <el-image v-for="(img, i) in activity.imgList" :key="i" :src="img"
                                             class="w-20 h-20 rounded border border-gray-200"
-                                            :preview-src-list="activity.imgList"
-                                            fit="cover"
-                                        />
+                                            :preview-src-list="activity.imgList" fit="cover" />
                                     </div>
                                 </div>
                             </div>
@@ -535,77 +348,32 @@
 
         <template #footer>
             <div class="flex justify-center gap-4">
-                <el-button
-                    :disabled="
-                        !canOperate && !['4', '5'].includes(String(form.status))
-                    "
-                    >备注</el-button
-                >
+                <el-button @click="handleRemark">备注</el-button>
                 <el-button @click="handleRefuse">{{
                     isRefundOnly ? '拒绝退款申请' : '拒绝退货申请'
-                }}</el-button>
-                <el-button @click="handleAgree" :disabled="!canOperate">{{
+                    }}</el-button>
+                <el-button @click="handleAgree">{{
                     isRefundOnly ? '同意退款' : '同意退货'
-                }}</el-button>
-                <el-button
-                    v-if="isPlatformConfirmReceipt"
-                    type="primary"
-                    @click="handleConfirmReceiptRefund"
-                    >确认收货,同意退款</el-button
-                >
-                <el-button
-                    v-if="isRefundOnly"
-                    type="primary"
-                    @click="handleExpressIntercept"
-                    :disabled="form.interceptStatus === '1'"
-                    >快递拦截</el-button
-                >
-                <el-button
-                    v-if="isRefundOnly"
-                    type="primary"
-                    @click="handleNegotiateReturn"
-                    :disabled="
-                        !canOperate && !['4', '5'].includes(String(form.status))
-                    "
-                    >协商退货退款</el-button
-                >
-                <el-button
-                    type="primary"
-                    @click="handleNegotiate"
-                    :disabled="
-                        !canOperate && !['4', '5'].includes(String(form.status))
-                    "
-                    >与买家协商</el-button
-                >
+                    }}</el-button>
+                <el-button v-if="isPlatformConfirmReceipt" type="primary"
+                    @click="handleConfirmReceiptRefund">确认收货,同意退款</el-button>
+                <el-button v-if="isRefundOnly" type="primary" @click="handleExpressIntercept"
+                    :disabled="form.interceptStatus === '1'">快递拦截</el-button>
+                <el-button v-if="isRefundOnly" type="primary" @click="handleNegotiateReturn" :disabled="!canOperate && !['4', '5'].includes(String(form.status))
+                    ">协商退货退款</el-button>
+                <el-button type="primary" @click="handleNegotiate" :disabled="!canOperate && !['4', '5'].includes(String(form.status))
+                    ">与买家协商</el-button>
             </div>
         </template>
-        <negotiation-apply-dialog
-            v-model="negotiationDialogVisible"
-            :refund-order-id="form.refundOrderId"
-            :refund-type="Number(form.refundType)"
-            :max-refund-amount="Number(form.refundMoney) || 0"
-            @success="handleNegotiationSuccess"
-        />
-        <refuse-dialog
-            ref="refuseDialogRef"
-            @success="handleNegotiationSuccess"
-        />
-        <agree-dialog
-            ref="agreeDialogRef"
-            @success="handleNegotiationSuccess"
-        />
-        <confirm-refund-dialog
-            ref="confirmRefundDialogRef"
-            @success="handleNegotiationSuccess"
-        />
-        <negotiate-return-dialog
-            ref="negotiateReturnDialogRef"
-            @success="handleNegotiationSuccess"
-        />
-        <leave-message-dialog
-            ref="leaveMessageDialogRef"
-            @success="handleNegotiationSuccess"
-        />
+        <negotiation-apply-dialog v-model="negotiationDialogVisible" :refund-order-id="form.refundOrderId"
+            :refund-type="Number(form.refundType)" :max-refund-amount="Number(form.refundMoney) || 0"
+            @success="handleNegotiationSuccess" />
+        <refuse-dialog ref="refuseDialogRef" @success="handleNegotiationSuccess" />
+        <agree-dialog ref="agreeDialogRef" @success="handleNegotiationSuccess" />
+        <confirm-refund-dialog ref="confirmRefundDialogRef" @success="handleNegotiationSuccess" />
+        <negotiate-return-dialog ref="negotiateReturnDialogRef" @success="handleNegotiationSuccess" />
+        <leave-message-dialog ref="leaveMessageDialogRef" @success="handleNegotiationSuccess" />
+        <refund-remarks ref="remarkRef" @refresh="handleRemarkRefresh" />
     </ele-modal>
 </template>
 
@@ -622,8 +390,10 @@
     import ConfirmRefundDialog from './confirm-refund-dialog.vue';
     import NegotiateReturnDialog from './negotiate-return-dialog.vue';
     import LeaveMessageDialog from './leave-message-dialog.vue';
+    import RefundRemarks from './refund-remarks.vue';
 
     const visible = defineModel({ type: Boolean });
+    const emit = defineEmits(['refresh']);
     const loading = ref(false);
     const form = ref({});
     const historyList = ref([]);
@@ -634,6 +404,7 @@
     const confirmRefundDialogRef = ref(null);
     const negotiateReturnDialogRef = ref(null);
     const leaveMessageDialogRef = ref(null);
+    const remarkRef = ref(null);
 
     const deadline = ref(0);
 
@@ -852,7 +623,7 @@
                         loading.value = false;
                     });
             })
-            .catch(() => {});
+            .catch(() => { });
     };
 
     const handleRefuse = () => {
@@ -888,6 +659,14 @@
         }
     };
 
+    const handleRemark = () => {
+        remarkRef.value?.open(form.value.refundOrderId);
+    };
+
+    const handleRemarkRefresh = () => {
+        emit('refresh');
+    };
+
     defineExpose({
         handleOpen
     });

+ 14 - 2
src/views/mallOrder/refund/components/refund-item.vue

@@ -18,6 +18,17 @@
             <span class="mr-4 text-black-600" v-if="item.cancelStatus == 2">
                 <el-tag type="info" size="small">已撤销</el-tag>
             </span>
+            <span
+                class="flag-icon cursor-pointer"
+                @click="$emit('add-remark', item)"
+            >
+                <el-icon
+                    size="23"
+                    :color="item.remarkLabel || '#777777'"
+                >
+                    <Flag />
+                </el-icon>
+            </span>
         </div>
 
         <!-- Body -->
@@ -168,7 +179,7 @@
 </template>
 
 <script setup>
-    import { CopyDocument } from '@element-plus/icons-vue';
+    import { CopyDocument, Flag } from '@element-plus/icons-vue';
     import { EleMessage } from 'ele-admin-plus/es';
     import { useClipboard } from '@vueuse/core';
 
@@ -184,7 +195,8 @@
         'send-packet',
         'view-detail',
         'view-logistics',
-        'confirm-refund'
+        'confirm-refund',
+        'add-remark'
     ]);
 
     const { copy } = useClipboard();

+ 73 - 0
src/views/mallOrder/refund/components/refund-remark-timeline.vue

@@ -0,0 +1,73 @@
+<template>
+    <div class="demo-timeline">
+        <div v-if="loading" v-loading="loading" style="height: 180px"></div>
+        <template v-else>
+            <div v-if="records.length" style="margin-bottom: 10px">{{
+                title
+            }}</div>
+            <el-timeline v-if="records.length">
+                <el-timeline-item
+                    v-for="(item, index) in records"
+                    :key="index"
+                    :timestamp="item.createTime"
+                    :color="item.label"
+                >
+                    <div
+                        style="
+                            display: flex;
+                            align-items: center;
+                            flex-wrap: wrap;
+                        "
+                    >
+                        <el-icon :size="14" :color="item.label">
+                            <Flag />
+                        </el-icon>
+                        <span>{{ item.createName }}:</span>
+                        <span>{{ item.remark }}</span>
+                    </div>
+                </el-timeline-item>
+            </el-timeline>
+            <el-empty v-else description="暂无数据" :image-size="80" />
+        </template>
+    </div>
+</template>
+
+<script setup>
+    import { ref } from 'vue';
+    import request from '@/utils/request';
+    import { Flag } from '@element-plus/icons-vue';
+
+    const props = defineProps({
+        refundOrderId: {
+            type: [String, Number],
+            default: ''
+        },
+        title: {
+            type: String,
+            default: ''
+        }
+    });
+
+    const records = ref([]);
+    const loading = ref(false);
+
+    //获取备注
+    const getRemarks = async (refundOrderId) => {
+        if (!refundOrderId || loading.value) return;
+        loading.value = true;
+        try {
+            const res = await request.get('/shop/shopOrderRefundRemark/list', {
+                params: { refundOrderId }
+            });
+            records.value = res.data.data;
+        } catch (error) {
+            console.error('Failed to fetch remarks:', error);
+        } finally {
+            loading.value = false;
+        }
+    };
+
+    defineExpose({
+        getRemarks
+    });
+</script>

+ 129 - 0
src/views/mallOrder/refund/components/refund-remarks.vue

@@ -0,0 +1,129 @@
+<!-- 退款订单备注弹窗 -->
+<template>
+    <ele-modal
+        form
+        :width="760"
+        v-model="visible"
+        title="退款订单备注"
+        @open="handleOpen"
+    >
+        <el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
+            <el-form-item label="选择标签" prop="label">
+                <el-radio-group v-model="form.label">
+                    <el-radio v-for="item in tags" :value="item" :key="item"
+                        ><el-icon :color="item" :size="18"> <Flag /> </el-icon
+                    ></el-radio>
+                </el-radio-group>
+            </el-form-item>
+            <el-form-item label="备注信息" prop="remark">
+                <el-input
+                    :rows="4"
+                    type="textarea"
+                    v-model="form.remark"
+                    placeholder="请输入备注"
+                />
+            </el-form-item>
+        </el-form>
+
+        <RefundRemarkTimeline
+            title="备注历史记录"
+            :refundOrderId="form.refundOrderId"
+            ref="timelineRef"
+        />
+
+        <template #footer>
+            <el-button @click="handleCancel">关闭</el-button>
+            <el-button type="primary" @click="handleSubmit">确定</el-button>
+        </template>
+    </ele-modal>
+</template>
+
+<script setup>
+    import { ref, reactive, nextTick } from 'vue';
+    import { Flag } from '@element-plus/icons-vue';
+    import { EleMessage } from 'ele-admin-plus';
+    import request from '@/utils/request';
+    import RefundRemarkTimeline from './refund-remark-timeline.vue';
+
+    const tags = reactive(['green', 'blue', 'yellow', 'red', 'purple']);
+
+    /** 弹窗是否打开 */
+    const visible = defineModel({ type: Boolean });
+
+    /** 关闭弹窗 */
+    const handleCancel = () => {
+        visible.value = false;
+    };
+
+    const form = ref({
+        refundOrderId: '',
+        label: '',
+        remark: ''
+    });
+
+    const timelineRef = ref(null);
+
+    /** 弹窗打开事件 */
+    const handleOpen = () => {
+        nextTick(() => {
+            if (form.value.refundOrderId && timelineRef.value) {
+                timelineRef.value.getRemarks(form.value.refundOrderId);
+            }
+        });
+    };
+
+    const open = (refundOrderId) => {
+        form.value = {
+            refundOrderId,
+            label: '',
+            remark: ''
+        };
+        visible.value = true;
+    };
+
+    /** 刷新事件 */
+    const emit = defineEmits(['refresh']);
+
+    /** 确定 /shop/shopOrderRefundRemark/addRemark */
+    const formRef = ref(null);
+    const handleSubmit = () => {
+        formRef.value.validate((valid) => {
+            if (valid) {
+                const url = '/shop/shopOrderRefundRemark/addRemark';
+                request.post(url, form.value).then((res) => {
+                    if (res.data.code === 200) {
+                        EleMessage.success('操作成功');
+                        handleCancel();
+                        emit('refresh');
+                    } else {
+                        EleMessage.error(res.data.msg);
+                    }
+                });
+            }
+        });
+    };
+
+    /** 表单验证规则 */
+    const rules = reactive({
+        label: [
+            {
+                required: true,
+                message: '请选择标签',
+                type: 'string',
+                trigger: 'change'
+            }
+        ],
+        remark: [
+            {
+                required: true,
+                message: '请输入备注信息',
+                type: 'string',
+                trigger: 'blur'
+            }
+        ]
+    });
+
+    defineExpose({
+        open
+    });
+</script>

+ 9 - 1
src/views/mallOrder/refund/index.vue

@@ -106,6 +106,7 @@
                     @send-packet="handleSendPacket"
                     @view-detail="handleViewDetail"
                     @confirm-refund="handleConfirmRefund"
+                    @add-remark="handleAddRemark"
                 />
             </div>
 
@@ -124,7 +125,8 @@
 
         <!-- Dialogs -->
         <push-sms-dialog ref="pushSmsDialogRef" @success="fetchData" />
-        <refund-detail-dialog ref="refundDetailDialogRef" />
+        <refund-detail-dialog ref="refundDetailDialogRef" @refresh="fetchData" />
+        <refund-remarks ref="remarkRef" @refresh="fetchData" />
         <confirm-refund-dialog
             ref="confirmRefundDialogRef"
             @success="fetchData"
@@ -142,6 +144,7 @@
     import PushSmsDialog from './components/push-sms-dialog.vue';
     import RefundDetailDialog from './components/refund-detail-dialog.vue';
     import ConfirmRefundDialog from './components/confirm-refund-dialog.vue';
+    import RefundRemarks from './components/refund-remarks.vue';
 
     const activeTab = ref('');
     const loading = ref(false);
@@ -160,6 +163,7 @@
     const pushSmsDialogRef = ref(null);
     const refundDetailDialogRef = ref(null);
     const confirmRefundDialogRef = ref(null);
+    const remarkRef = ref(null);
 
     const fetchData = () => {
         loading.value = true;
@@ -241,6 +245,10 @@
         }
     };
 
+    const handleAddRemark = (item) => {
+        remarkRef.value?.open(item.refundOrderId);
+    };
+
     onMounted(() => {
         fetchData();
     });