Procházet zdrojové kódy

Merge remote-tracking branch 'origin/红德堂' into 红德堂

Long před 3 dny
rodič
revize
24acfeb829

+ 29 - 0
src/api/crm/orderAuditMsg.js

@@ -0,0 +1,29 @@
+import request from '@/utils/request'
+
+export function getLatestUnreadMsg() {
+  return request({
+    url: '/crm/orderAuditMsg/getLatestUnread',
+    method: 'get'
+  })
+}
+
+export function getUnreadCount() {
+  return request({
+    url: '/crm/orderAuditMsg/getUnreadCount',
+    method: 'get'
+  })
+}
+
+export function markAsRead(msgId) {
+  return request({
+    url: '/crm/orderAuditMsg/markAsRead/' + msgId,
+    method: 'post'
+  })
+}
+
+export function markAllAsRead() {
+  return request({
+    url: '/crm/orderAuditMsg/markAllAsRead',
+    method: 'post'
+  })
+}

+ 29 - 0
src/api/crm/redPacketBalanceMsg.js

@@ -0,0 +1,29 @@
+import request from '@/utils/request'
+
+export function getLatestUnreadMsg() {
+  return request({
+    url: '/crm/redPacketBalanceMsg/getLatestUnread',
+    method: 'get'
+  })
+}
+
+export function getUnreadCount() {
+  return request({
+    url: '/crm/redPacketBalanceMsg/getUnreadCount',
+    method: 'get'
+  })
+}
+
+export function markAsRead(msgId) {
+  return request({
+    url: '/crm/redPacketBalanceMsg/markAsRead/' + msgId,
+    method: 'post'
+  })
+}
+
+export function markAllAsRead() {
+  return request({
+    url: '/crm/redPacketBalanceMsg/markAllAsRead',
+    method: 'post'
+  })
+}

+ 22 - 0
src/api/store/externalOrder.js

@@ -23,6 +23,28 @@ export function getExternalOrder(id) {
   })
 }
 
+export function auditExternalOrder(id) {
+  return request({
+    url: '/store/externalOrder/audit/' + id,
+    method: 'post'
+  })
+}
+
+export function cancelExternalOrder(id) {
+  return request({
+    url: '/store/externalOrder/cancel/' + id,
+    method: 'post'
+  })
+}
+
+export function approveExternalOrder(data) {
+  return request({
+    url: '/store/externalOrder/approveOrder',
+    method: 'post',
+    data: data
+  })
+}
+
 export function exportExternalOrder(query) {
   return request({
     url: '/store/externalOrder/export',

+ 272 - 0
src/components/OrderAuditMsgDialog/index.vue

@@ -0,0 +1,272 @@
+<template>
+  <el-dialog
+    title="订单审批提醒"
+    :visible.sync="visible"
+    width="480px"
+    :close-on-click-modal="false"
+    :show-close="true"
+    custom-class="order-audit-msg-dialog"
+    @close="handleClose"
+    center
+  >
+    <div class="msg-content">
+      <div class="msg-icon audit">
+        <i class="el-icon-document-checked"></i>
+      </div>
+      <div class="msg-text">
+        <h3>{{ msgData.title }}</h3>
+        <p>{{ msgData.content }}</p>
+        <div class="msg-stats">
+          <div class="stat-item">
+            <span class="stat-label">未处理消息</span>
+            <span class="stat-value">{{ unreadCount }}条</span>
+          </div>
+          <div class="stat-item">
+            <span class="stat-label">未处理率</span>
+            <span class="stat-value percent">{{ unreadPercent }}%</span>
+          </div>
+        </div>
+        <div class="msg-time" v-if="msgData.createTime">
+          <i class="el-icon-time"></i>
+          {{ msgData.createTime }}
+        </div>
+      </div>
+    </div>
+    <span slot="footer" class="dialog-footer">
+      <el-button @click="handleKnow" size="medium">
+        <i class="el-icon-check"></i> 知道了
+      </el-button>
+      <el-button type="primary" @click="handleGoProcess" size="medium">
+        <i class="el-icon-right"></i> 去处理
+      </el-button>
+    </span>
+  </el-dialog>
+</template>
+
+<script>
+import { getLatestUnreadMsg, markAsRead } from '@/api/crm/orderAuditMsg'
+
+export default {
+  name: 'OrderAuditMsgDialog',
+  data() {
+    return {
+      visible: false,
+      msgData: {
+        msgId: null,
+        title: '',
+        content: '',
+        actionUrl: '',
+        createTime: ''
+      },
+      unreadCount: 0,
+      totalCount: 0
+    }
+  },
+  computed: {
+    unreadPercent() {
+      if (this.totalCount === 0) return 0
+      return Math.round((this.unreadCount / this.totalCount) * 100)
+    }
+  },
+  methods: {
+    checkNewMsg() {
+      getLatestUnreadMsg().then(res => {
+        if (res.code === 200 && res.data) {
+          if (res.data.msg) {
+            this.msgData = res.data.msg
+            this.unreadCount = res.data.unreadCount || 0
+            this.totalCount = res.data.totalCount || 0
+            this.visible = true
+          }
+        }
+      }).catch(err => {
+        console.error('获取订单审批提醒消息失败:', err)
+      })
+    },
+    handleKnow() {
+      this.visible = false
+    },
+    handleGoProcess() {
+      if (this.msgData.msgId) {
+        markAsRead(this.msgData.msgId).then(() => {
+          this.visible = false
+          this.$emit('msg-read')
+          if (this.msgData.actionUrl) {
+            this.navigateToUrl(this.msgData.actionUrl)
+          }
+          setTimeout(() => {
+            this.checkNewMsg()
+          }, 500)
+        })
+      } else {
+        this.visible = false
+        if (this.msgData.actionUrl) {
+          this.navigateToUrl(this.msgData.actionUrl)
+        }
+      }
+    },
+    navigateToUrl(url) {
+      if (url.endsWith('/index')) {
+        url = url.slice(0, -6)
+      }
+      if (!url.startsWith('/')) {
+        url = '/' + url
+      }
+      this.$router.push(url).catch(err => {
+        console.error('路由跳转失败:', err)
+      })
+    },
+    handleClose() {
+      this.visible = false
+    },
+    show() {
+      this.checkNewMsg()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.order-audit-msg-dialog {
+  border-radius: 12px;
+  overflow: hidden;
+}
+
+.msg-content {
+  display: flex;
+  align-items: flex-start;
+  padding: 24px 16px;
+}
+
+.msg-icon {
+  flex-shrink: 0;
+  width: 56px;
+  height: 56px;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 20px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+}
+
+.msg-icon.audit {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+}
+
+.msg-icon i {
+  font-size: 28px;
+  color: #fff;
+  animation: bell-ring 1s ease-in-out infinite;
+}
+
+@keyframes bell-ring {
+  0%, 100% {
+    transform: rotate(0);
+  }
+  10%, 30% {
+    transform: rotate(10deg);
+  }
+  20%, 40% {
+    transform: rotate(-10deg);
+  }
+  50% {
+    transform: rotate(0);
+  }
+}
+
+.msg-text {
+  flex: 1;
+}
+
+.msg-text h3 {
+  margin: 0 0 12px 0;
+  font-size: 18px;
+  font-weight: 600;
+  color: #303133;
+}
+
+.msg-text p {
+  margin: 0 0 12px 0;
+  font-size: 14px;
+  color: #606266;
+  line-height: 1.6;
+}
+
+.msg-stats {
+  display: flex;
+  gap: 20px;
+  margin-bottom: 12px;
+  padding: 12px;
+  background: #f0f9eb;
+  border-radius: 8px;
+}
+
+.stat-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.stat-label {
+  font-size: 12px;
+  color: #909399;
+  margin-bottom: 4px;
+}
+
+.stat-value {
+  font-size: 20px;
+  font-weight: 600;
+  color: #67c23a;
+}
+
+.stat-value.percent {
+  color: #e6a23c;
+}
+
+.msg-time {
+  font-size: 12px;
+  color: #909399;
+  display: flex;
+  align-items: center;
+}
+
+.msg-time i {
+  margin-right: 4px;
+}
+
+.dialog-footer {
+  text-align: center;
+  padding: 10px 20px 20px;
+}
+
+.dialog-footer .el-button {
+  min-width: 100px;
+}
+
+.dialog-footer .el-button i {
+  margin-right: 4px;
+}
+</style>
+
+<style>
+.order-audit-msg-dialog .el-dialog__header {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  padding: 16px 20px;
+}
+
+.order-audit-msg-dialog .el-dialog__title {
+  color: #fff;
+  font-size: 16px;
+  font-weight: 600;
+}
+
+.order-audit-msg-dialog .el-dialog__headerbtn .el-dialog__close {
+  color: #fff;
+  font-size: 18px;
+}
+
+.order-audit-msg-dialog .el-dialog__body {
+  padding: 0;
+}
+</style>

+ 233 - 0
src/components/RedPacketBalanceMsgDialog/index.vue

@@ -0,0 +1,233 @@
+<template>
+  <el-dialog
+    title="红包余额不足提醒"
+    :visible.sync="visible"
+    width="480px"
+    :close-on-click-modal="false"
+    :show-close="true"
+    custom-class="red-packet-balance-msg-dialog"
+    @close="handleClose"
+    center
+  >
+    <div class="msg-content">
+      <div class="msg-icon warning">
+        <i class="el-icon-warning"></i>
+      </div>
+      <div class="msg-text">
+        <h3>{{ msgData.title }}</h3>
+        <p>{{ msgData.content }}</p>
+        <div class="msg-stats">
+          <div class="stat-item">
+            <span class="stat-label">未处理消息</span>
+            <span class="stat-value">{{ unreadCount }}条</span>
+          </div>
+          <div class="stat-item">
+            <span class="stat-label">未处理率</span>
+            <span class="stat-value percent">{{ unreadPercent }}%</span>
+          </div>
+        </div>
+        <div class="msg-time" v-if="msgData.createTime">
+          <i class="el-icon-time"></i>
+          {{ msgData.createTime }}
+        </div>
+      </div>
+    </div>
+    <span slot="footer" class="dialog-footer">
+      <el-button type="primary" @click="handleKnow" size="medium">
+        <i class="el-icon-check"></i> 知道了
+      </el-button>
+    </span>
+  </el-dialog>
+</template>
+
+<script>
+import { getLatestUnreadMsg } from '@/api/crm/redPacketBalanceMsg'
+
+export default {
+  name: 'RedPacketBalanceMsgDialog',
+  data() {
+    return {
+      visible: false,
+      msgData: {
+        msgId: null,
+        title: '',
+        content: '',
+        actionUrl: '',
+        createTime: ''
+      },
+      unreadCount: 0,
+      totalCount: 0
+    }
+  },
+  computed: {
+    unreadPercent() {
+      if (this.totalCount === 0) return 0
+      return Math.round((this.unreadCount / this.totalCount) * 100)
+    }
+  },
+  methods: {
+    checkNewMsg() {
+      getLatestUnreadMsg().then(res => {
+        if (res.code === 200 && res.data) {
+          if (res.data.msg) {
+            this.msgData = res.data.msg
+            this.unreadCount = res.data.unreadCount || 0
+            this.totalCount = res.data.totalCount || 0
+            this.visible = true
+          }
+        }
+      }).catch(err => {
+        console.error('获取红包余额不足消息失败:', err)
+      })
+    },
+    handleKnow() {
+      this.visible = false
+    },
+    handleClose() {
+      this.visible = false
+    },
+    show() {
+      this.checkNewMsg()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.red-packet-balance-msg-dialog {
+  border-radius: 12px;
+  overflow: hidden;
+}
+
+.msg-content {
+  display: flex;
+  align-items: flex-start;
+  padding: 24px 16px;
+}
+
+.msg-icon {
+  flex-shrink: 0;
+  width: 56px;
+  height: 56px;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 20px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+}
+
+.msg-icon.warning {
+  background: linear-gradient(135deg, #f5af19 0%, #f12711 100%);
+}
+
+.msg-icon i {
+  font-size: 28px;
+  color: #fff;
+  animation: pulse 1.5s ease-in-out infinite;
+}
+
+@keyframes pulse {
+  0%, 100% {
+    transform: scale(1);
+  }
+  50% {
+    transform: scale(1.1);
+  }
+}
+
+.msg-text {
+  flex: 1;
+}
+
+.msg-text h3 {
+  margin: 0 0 12px 0;
+  font-size: 18px;
+  font-weight: 600;
+  color: #303133;
+}
+
+.msg-text p {
+  margin: 0 0 12px 0;
+  font-size: 14px;
+  color: #606266;
+  line-height: 1.6;
+}
+
+.msg-stats {
+  display: flex;
+  gap: 20px;
+  margin-bottom: 12px;
+  padding: 12px;
+  background: #fef0f0;
+  border-radius: 8px;
+}
+
+.stat-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.stat-label {
+  font-size: 12px;
+  color: #909399;
+  margin-bottom: 4px;
+}
+
+.stat-value {
+  font-size: 20px;
+  font-weight: 600;
+  color: #f56c6c;
+}
+
+.stat-value.percent {
+  color: #e6a23c;
+}
+
+.msg-time {
+  font-size: 12px;
+  color: #909399;
+  display: flex;
+  align-items: center;
+}
+
+.msg-time i {
+  margin-right: 4px;
+}
+
+.dialog-footer {
+  text-align: center;
+  padding: 10px 20px 20px;
+}
+
+.dialog-footer .el-button {
+  min-width: 100px;
+}
+
+.dialog-footer .el-button i {
+  margin-right: 4px;
+}
+</style>
+
+<style>
+.red-packet-balance-msg-dialog .el-dialog__header {
+  background: linear-gradient(135deg, #f5af19 0%, #f12711 100%);
+  padding: 16px 20px;
+}
+
+.red-packet-balance-msg-dialog .el-dialog__title {
+  color: #fff;
+  font-size: 16px;
+  font-weight: 600;
+}
+
+.red-packet-balance-msg-dialog .el-dialog__headerbtn .el-dialog__close {
+  color: #fff;
+  font-size: 18px;
+}
+
+.red-packet-balance-msg-dialog .el-dialog__body {
+  padding: 0;
+}
+</style>

+ 24 - 20
src/layout/components/Navbar.vue

@@ -56,6 +56,9 @@
           <msg    ref="msg"  @update-count="getMsgCount" />
       </el-drawer>
 
+    <red-packet-balance-msg-dialog ref="redPacketBalanceMsgDialog" @msg-read="getMsgCount" />
+    <order-audit-msg-dialog ref="orderAuditMsgDialog" @msg-read="getMsgCount" />
+
   </div>
 </template>
 
@@ -70,6 +73,8 @@ import Search from '@/components/HeaderSearch'
 
 import msg from "@/views/crm/components/msg";
 import { getMsg,getMsgList,getMsgCount,setRead } from "@/api/crm/msg";
+import RedPacketBalanceMsgDialog from '@/components/RedPacketBalanceMsgDialog'
+import OrderAuditMsgDialog from '@/components/OrderAuditMsgDialog'
 
 export default {
   components: {
@@ -79,7 +84,9 @@ export default {
     Screenfull,
     SizeSelect,
     Search,
-    msg
+    msg,
+    RedPacketBalanceMsgDialog,
+    OrderAuditMsgDialog
   },
   computed: {
     ...mapGetters([
@@ -107,34 +114,31 @@ export default {
   data() {
     return {
       msgCount:0,
-      previousMsgCount: 0, // 保存上一次的消息计数
+      previousMsgCount: 0,
       msg:{
         open:false,
         title:'通知消息'
       },
-      msgPollingTimer: null, // 轮询定时器
+      msgPollingTimer: null,
+      isFirstCheck: true,
     }
   },
   created() {
     this.getMsgCount();
-    // 启动消息轮询,每30秒检查一次新消息
     this.startMsgPolling();
   },
   beforeDestroy() {
-    // 组件销毁前清除定时器
     this.stopMsgPolling();
   },
   methods: {
     startMsgPolling() {
-      // 清除已存在的定时器
       if (this.msgPollingTimer) {
         clearInterval(this.msgPollingTimer);
       }
 
-      // 设置定时器,每30秒获取一次消息计数
       this.msgPollingTimer = setInterval(() => {
         this.getMsgCount();
-      }, 30000); // 30秒轮询一次
+      }, 30000);
     },
     stopMsgPolling() {
       if (this.msgPollingTimer) {
@@ -142,26 +146,28 @@ export default {
         this.msgPollingTimer = null;
       }
     },
+    checkSpecialMsgs() {
+      if (this.$refs.redPacketBalanceMsgDialog) {
+        this.$refs.redPacketBalanceMsgDialog.show();
+      }
+      if (this.$refs.orderAuditMsgDialog) {
+        this.$refs.orderAuditMsgDialog.show();
+      }
+    },
     getMsgCount(){
       getMsg().then(response => {
-        // 计算所有未读消息总数
         let totalCount = 0;
         response.counts.forEach(item => {
           totalCount += item.total;
         });
 
-        // 保存旧的消息计数
         this.previousMsgCount = this.msgCount;
-        // 更新消息计数
         this.msgCount = totalCount;
 
-        // 如果消息计数增加了,显示通知弹框
-        if (this.msgCount > this.previousMsgCount && this.msgCount > 0) {
-          this.$notify({
-            title: '提示',
-            message: '您有'+this.msgCount+"条消息通知",
-            position: 'top-right',
-            type: 'info'
+        if (this.isFirstCheck) {
+          this.isFirstCheck = false;
+          this.$nextTick(() => {
+            this.checkSpecialMsgs();
           });
         }
 
@@ -172,11 +178,9 @@ export default {
     openMsg(){
       console.log(11);
       this.msg.open=true;
-      // 每次打开消息界面时刷新数据
       if (this.$refs.msg) {
         this.$refs.msg.getMsg();
       }
-      // 打开消息后重新获取消息计数来更新角标
       this.getMsgCount();
     },
     toggleSideBar() {

+ 8 - 0
src/views/company/orderApprove/list.vue

@@ -44,6 +44,12 @@
 
     <el-table height="450" v-loading="loading" border :data="orderList">
       <el-table-column label="订单编号" align="center" prop="orderCode" width="180px"/>
+      <el-table-column label="订单类型" align="center" prop="orderType">
+        <template slot-scope="scope">
+          <span v-if="scope.row.orderType === 0">外部订单</span>
+          <span v-else-if="scope.row.orderType === 1">普通订单</span>
+        </template>
+      </el-table-column>
       <el-table-column label="审核类型" align="center" prop="auditType">
         <template slot-scope="scope">
           <span v-if="scope.row.auditType === 1">价格变更审核</span>
@@ -185,6 +191,7 @@ export default {
       auditOpen: false,
       auditForm: {
         id: null,
+        orderType: null,
         auditType: null,
         originalTotalPrice: null,
         originalPayPrice: null,
@@ -219,6 +226,7 @@ export default {
     handleAudit(row) {
       this.auditForm = {
         id: row.id,
+        orderType: row.orderType,
         auditType: row.auditType,
         originalTotalPrice: row.originalTotalPrice,
         originalPayPrice: row.originalPayPrice,

+ 280 - 17
src/views/store/externalOrder/index.vue

@@ -115,7 +115,7 @@
           <dict-tag :options="statusOptions" :value="scope.row.status" />
         </template>
       </el-table-column>
-      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" fixed="right" width="100px">
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" fixed="right" width="240px">
         <template slot-scope="scope">
           <el-button
             size="mini"
@@ -123,6 +123,27 @@
             v-hasPermi="['store:externalOrder:query']"
             @click="handleView(scope.row)"
           >查看</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            v-hasPermi="['store:externalOrder:audit']"
+            @click="handleAudit(scope.row)"
+            v-if="scope.row.status == 1"
+          >审核</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            v-hasPermi="['store:externalOrder:approveOrder']"
+            @click="handleApprove(scope.row)"
+            v-if="scope.row.status == 3 && !scope.row.isApplyAudit"
+          >申请审核</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            v-hasPermi="['store:externalOrder:cancel']"
+            @click="handleCancel(scope.row)"
+            v-if="scope.row.status != 4"
+          >取消订单</el-button>
         </template>
       </el-table-column>
     </el-table>
@@ -192,9 +213,9 @@
             clearable
             size="small"
             style="width: 300px; margin-bottom: 10px;"
-            @keyup.enter.native="getProductList"
+            @keyup.enter.native="handleProductSearch"
           />
-          <el-button type="primary" size="small" style="margin-left: 10px;" @click="getProductList">搜索</el-button>
+          <el-button type="primary" size="small" style="margin-left: 10px;" @click="handleProductSearch">搜索</el-button>
           <el-table border width="100%" style="margin-top:5px;" :data="productJson" @selection-change="handleProductSelectionChange">
             <el-table-column type="selection" width="55" align="center" />
             <el-table-column label="商品图片" align="center" width="120">
@@ -206,13 +227,38 @@
               </template>
             </el-table-column>
             <el-table-column label="商品名称" show-overflow-tooltip align="center" prop="productName" />
-            <el-table-column label="售价" align="center" prop="price" >
+            <el-table-column label="售价" align="center" prop="price" width="160">
               <template slot-scope="scope">
-                <span v-if="scope.row.price!=null">{{scope.row.price.toFixed(2)}}</span>
+                <el-input-number
+                  v-model="productPrices[scope.row.productId]"
+                  :min="0"
+                  :precision="2"
+                  :step="0.01"
+                  size="mini"
+                  @change="handleQuantityChange"
+                />
+              </template>
+            </el-table-column>
+            <el-table-column label="数量" align="center" width="160">
+              <template slot-scope="scope">
+                <el-input-number
+                  v-model="productQuantities[scope.row.productId]"
+                  :min="1"
+                  :max="getProductMax(scope.row)"
+                  size="mini"
+                  style="width: 140px;"
+                  @change="handleQuantityChange"
+                />
               </template>
             </el-table-column>
-            <el-table-column label="库存" align="center" prop="stock" />
           </el-table>
+          <pagination
+            v-show="productTotal > 0"
+            :total="productTotal"
+            :page.sync="productQueryParams.pageNum"
+            :limit.sync="productQueryParams.pageSize"
+            @pagination="getProductList"
+          />
         </el-form-item>
         <el-form-item label="支付方式" prop="payType">
           <el-select v-model="form.payType" placeholder="请选择支付方式" clearable size="small">
@@ -271,11 +317,56 @@
     <el-dialog :title="userAddress.title" v-if="userAddress.open" :visible.sync="userAddress.open" width="800px" append-to-body>
       <add-external-user-address ref="addExternalUserAddress" @addUserAddress="addUserAddress" />
     </el-dialog>
+
+    <el-dialog title="申请审核" :visible.sync="approveOpen" width="600px" append-to-body :close-on-click-modal="false">
+      <el-form ref="approveForm" :model="approveForm" :rules="approveRules" label-width="120px">
+        <el-form-item label="审核类型" prop="auditType">
+          <el-radio-group v-model="approveForm.auditType">
+            <el-radio :label="1">申请改价</el-radio>
+            <el-radio :label="2">订单完成</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <template v-if="approveForm.auditType === 1">
+          <el-form-item label="原价格">
+            <el-input v-model="approveForm.oldTotalPrice" disabled style="width: 200px;" />
+          </el-form-item>
+          <el-form-item label="更改后总价" prop="newTotalPrice">
+            <el-input-number v-model="approveForm.newTotalPrice" :precision="2" :min="0" style="width: 200px;" />
+          </el-form-item>
+          <el-form-item label="更改后实收" prop="newPayPrice">
+            <el-input-number v-model="approveForm.newPayPrice" :precision="2" :min="0" style="width: 200px;" />
+          </el-form-item>
+        </template>
+        <el-form-item label="申请原因" prop="priceChangeReason">
+          <el-input v-model="approveForm.priceChangeReason" type="textarea" :rows="3" placeholder="请输入申请原因" />
+        </el-form-item>
+        <el-form-item label="凭证图片">
+          <el-upload
+            class="avatar-uploader"
+            :action="uploadUrl"
+            :headers="headers"
+            :show-file-list="false"
+            :on-success="handleUploadSuccess"
+            accept="image/*"
+          >
+            <img v-if="approveForm.voucherImages" :src="approveForm.voucherImages" style="max-width: 200px; max-height: 200px;">
+            <el-button v-else size="small" type="primary">点击上传</el-button>
+          </el-upload>
+        </el-form-item>
+        <el-form-item label="凭证说明">
+          <el-input v-model="approveForm.voucherRemark" type="textarea" :rows="2" placeholder="请输入凭证说明" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="approveOpen = false">取 消</el-button>
+        <el-button type="primary" @click="submitApprove">确 定</el-button>
+      </div>
+    </el-dialog>
   </div>
 </template>
 
 <script>
-import { listExternalOrder, createExternalOrder, getExternalOrder, exportExternalOrder } from "@/api/store/externalOrder";
+import { listExternalOrder, createExternalOrder, getExternalOrder, auditExternalOrder, cancelExternalOrder, approveExternalOrder, exportExternalOrder } from "@/api/store/externalOrder";
 import { getCompanyList } from "@/api/company/company";
 import { listUserAddress } from "@/api/store/userAddress";
 import { listStoreProduct } from "@/api/store/storeProduct";
@@ -306,7 +397,13 @@ export default {
       address: [],
       addressloading: false,
       productJson: [],
+      productTotal: 0,
       productNameSearch: null,
+      productQueryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        productName: null
+      },
       user: {
         open: false,
         title: "创建外部订单用户"
@@ -333,13 +430,43 @@ export default {
         userId: null,
         payType: null,
         payMoney: null,
-        productIds: []
+        products: []
       },
+      productQuantities: {},
+      productPrices: {},
+      selectedProducts: [],
       rules: {
         addressId: [{ required: true, message: "请选择收货地址", trigger: "change" }],
         payType: [{ required: true, message: "支付方式不能为空", trigger: "change" }],
         payMoney: [{ required: true, message: "支付金额不能为空", trigger: "blur" }]
       },
+      approveOpen: false,
+      approveForm: {
+        orderIds: [],
+        auditType: 1,
+        oldTotalPrice: null,
+        newTotalPrice: null,
+        newPayPrice: null,
+        priceChangeReason: null,
+        voucherImages: null,
+        voucherRemark: null
+      },
+      approveRules: {
+        auditType: [
+          { required: true, message: "请选择审核类型", trigger: "change" }
+        ],
+        newTotalPrice: [
+          { required: true, message: "请输入更改后总价", trigger: "blur" }
+        ],
+        newPayPrice: [
+          { required: true, message: "请输入更改后实收", trigger: "blur" }
+        ],
+        priceChangeReason: [
+          { required: true, message: "请输入申请原因", trigger: "blur" }
+        ]
+      },
+      uploadUrl: process.env.VUE_APP_BASE_API + "/common/upload",
+      headers: { Authorization: "Bearer " + this.$store.getters.token },
       payTypeOptions: [],
       statusOptions: [],
       userStatusOptions: []
@@ -351,7 +478,7 @@ export default {
     this.getDicts("sys_package_pay_type").then(response => {
       this.payTypeOptions = response.data;
     });
-    this.getDicts("sys_order_status").then(response => {
+    this.getDicts("sys_externalOrder_status").then(response => {
       this.statusOptions = response.data;
     });
     this.getDicts("sys_company_status").then(response => {
@@ -408,9 +535,16 @@ export default {
       this.users = [];
       this.address = [];
       this.productJson = [];
+      this.productTotal = 0;
       this.productNameSearch = null;
+      this.productQueryParams.pageNum = 1;
+      this.productQueryParams.productName = null;
+      this.productQuantities = {};
+      this.productPrices = {};
+      this.selectedProducts = [];
       this.open = true;
       this.title = "创建外部订单";
+      this.getProductList();
     },
     reset() {
       this.form = {
@@ -418,8 +552,11 @@ export default {
         userId: null,
         payType: null,
         payMoney: null,
-        productIds: []
+        products: []
       };
+      this.productQuantities = {};
+      this.productPrices = {};
+      this.selectedProducts = [];
       this.resetForm("form");
     },
     cancel() {
@@ -481,18 +618,69 @@ export default {
         this.addressloading = false;
       });
     },
+    handleProductSearch() {
+      this.productQueryParams.pageNum = 1;
+      this.productQueryParams.productName = this.productNameSearch;
+      this.getProductList();
+    },
     getProductList() {
       var data = {
-        productName: this.productNameSearch,
-        pageNum: 1,
-        pageSize: 100
+        pageNum: this.productQueryParams.pageNum,
+        pageSize: this.productQueryParams.pageSize,
+        productName: this.productNameSearch
       };
       listStoreProduct(data).then(response => {
-        this.productJson = response.rows;
+        this.productJson = response.rows || [];
+        this.productJson.forEach(item => {
+          if (this.productPrices[item.productId] == null && item.price != null) {
+            this.$set(this.productPrices, item.productId, Number(item.price));
+          }
+          if (this.productQuantities[item.productId] == null) {
+            this.$set(this.productQuantities, item.productId, 1);
+          }
+        });
+        this.productTotal = response.total || 0;
       });
     },
+    getDisplayProductPrice(product) {
+      const customPrice = this.productPrices[product.productId];
+      if (customPrice !== undefined && customPrice !== null && customPrice !== "") {
+        return Number(customPrice);
+      }
+      return Number(product.price || 0);
+    },
+    getProductMax(product) {
+      const stock = Number(product.stock);
+      if (Number.isFinite(stock) && stock > 0) {
+        return stock;
+      }
+      return 999;
+    },
     handleProductSelectionChange(selection) {
-      this.form.productIds = selection.map(item => item.productId);
+      this.selectedProducts = selection;
+      // 初始化未设置数量的商品默认值为1
+      selection.forEach(item => {
+        if (this.productQuantities[item.productId] == null) {
+          this.$set(this.productQuantities, item.productId, 1);
+        }
+        if (this.productPrices[item.productId] == null) {
+          this.$set(this.productPrices, item.productId, this.getDisplayProductPrice(item));
+        }
+      });
+      this.handleQuantityChange();
+    },
+    isProductSelected(productId) {
+      return this.selectedProducts.some(item => item.productId === productId);
+    },
+    handleQuantityChange() {
+      // 数量变化时自动计算总金额
+      let total = 0;
+      this.selectedProducts.forEach(item => {
+        const quantity = this.productQuantities[item.productId] || 1;
+        const price = this.getDisplayProductPrice(item);
+        total += price * quantity;
+      });
+      this.form.payMoney = total.toFixed(2);
     },
     submitForm() {
       this.$refs["form"].validate(valid => {
@@ -501,16 +689,22 @@ export default {
             this.msgError("请选择会员");
             return;
           }
-          if (this.form.productIds.length === 0) {
+          if (this.selectedProducts.length === 0) {
             this.msgError("请选择至少一个商品");
             return;
           }
+          // 构建商品列表 [{productId, num}]
+          const products = this.selectedProducts.map(item => ({
+            productId: item.productId,
+            num: this.productQuantities[item.productId] || 1,
+            price: this.getDisplayProductPrice(item)
+          }));
           var submitData = {
             addressId: this.form.addressId,
             userId: this.form.userId,
             payType: this.form.payType,
             payMoney: parseFloat(this.form.payMoney) || 0,
-            productIds: this.form.productIds
+            products: products
           };
           createExternalOrder(submitData).then(response => {
             if (response.code === 200) {
@@ -528,6 +722,75 @@ export default {
         this.detailOpen = true;
       });
     },
+    handleAudit(row) {
+      this.$confirm("确认审核该订单?审核后订单将变为待发货状态", "提示", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        auditExternalOrder(row.orderId).then(response => {
+          if (response.code === 200) {
+            this.msgSuccess("审核成功");
+            this.getList();
+          }
+        });
+      });
+    },
+    handleCancel(row) {
+      this.$confirm("确认取消该订单?取消后订单将变为已取消状态", "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        cancelExternalOrder(row.orderId).then(response => {
+          if (response.code === 200) {
+            this.msgSuccess("取消订单成功");
+            this.getList();
+          }
+        });
+      });
+    },
+    handleApprove(row) {
+      this.approveForm = {
+        orderIds: [row.orderId],
+        auditType: 1,
+        oldTotalPrice: row.payPrice,
+        newTotalPrice: null,
+        newPayPrice: null,
+        priceChangeReason: null,
+        voucherImages: null,
+        voucherRemark: null
+      };
+      this.approveOpen = true;
+    },
+    handleUploadSuccess(response) {
+      if (response.code === 200) {
+        this.approveForm.voucherImages = response.url;
+        this.msgSuccess("上传成功");
+      } else {
+        this.msgError(response.msg);
+      }
+    },
+    submitApprove() {
+      this.$refs["approveForm"].validate(valid => {
+        if (valid) {
+          const data = {
+            orderIds: this.approveForm.orderIds,
+            auditType: this.approveForm.auditType,
+            newTotalPrice: this.approveForm.newTotalPrice,
+            newPayPrice: this.approveForm.newPayPrice,
+            priceChangeReason: this.approveForm.priceChangeReason,
+            voucherImages: this.approveForm.voucherImages,
+            voucherRemark: this.approveForm.voucherRemark
+          };
+          approveExternalOrder(data).then(response => {
+            this.msgSuccess("提交成功");
+            this.approveOpen = false;
+            this.getList();
+          });
+        }
+      });
+    },
     handleExport() {
       const queryParams = this.queryParams;
       this.$confirm("是否确认导出外部订单数据项?", "警告", {

+ 38 - 20
src/views/store/storeOrder/list.vue

@@ -419,16 +419,16 @@
         </el-form-item>
         <el-form-item label="商品信息" prop="product" >
           <el-input
-            v-model="queryParams.productName"
+            v-model="productNameSearch"
             placeholder="请输入商品名称搜索"
             clearable
             size="small"    style="width: 300px; margin-bottom: 10px;"
-            @keyup.enter.native="getProductList"
+            @keyup.enter.native="handleProductSearch"
           />
           <el-button
             type="primary"
             size="small"    style="margin-left: 10px;"
-            @click="getProductList"
+            @click="handleProductSearch"
           >搜索</el-button>
           <el-table border width="100%" style="margin-top:5px;"  :data="productJson"  @selection-change="handleProductSelectionChange">
             <el-table-column type="selection" width="55" align="center" />
@@ -453,12 +453,14 @@
             </el-table-column>
             <el-table-column label="销量" align="center" prop="sales" />
             <el-table-column label="库存" align="center" prop="stock" />
-            <el-table-column label="商品类型" align="center" prop="productType" >
-              <template slot-scope="scope">
-                <el-tag prop="status" v-for="(item, index) in productTypeOptions"    v-if="scope.row.productType==item.dictValue">{{item.dictLabel}}</el-tag>
-              </template>
-            </el-table-column>
           </el-table>
+          <pagination
+            v-show="productTotal > 0"
+            :total="productTotal"
+            :page.sync="productQueryParams.pageNum"
+            :limit.sync="productQueryParams.pageSize"
+            @pagination="getProductList"
+          />
         </el-form-item>
 
 
@@ -579,6 +581,13 @@ export default {
     return {
       payTypeArr:[],
       productJson:[],
+      productTotal: 0,
+      productNameSearch: null,
+      productQueryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        productName: null
+      },
       scheduleIdArr:[],
       buyTypeArr:[],
       channelArr:[],
@@ -844,15 +853,26 @@ export default {
       }, 500);
     },
     handleProductSelectionChange(selection) {
-      // 将选中商品的ID保存到form.productIds中
       this.form.productIds = selection.map(item => item.productId);
     },
+    handleProductSearch() {
+      this.productQueryParams.pageNum = 1;
+      this.productQueryParams.productName = this.productNameSearch;
+      this.getProductList();
+    },
     /** 查询商品列表 */
     getProductList() {
+      const data = {
+        pageNum: this.productQueryParams.pageNum,
+        pageSize: this.productQueryParams.pageSize,
+        productName: this.productNameSearch
+      };
       this.loading = true;
-      listStoreProduct(this.queryParams).then(response => {
-        this.productJson = response.rows;
-        this.total = response.total;
+      listStoreProduct(data).then(response => {
+        this.productJson = response.rows || [];
+        this.productTotal = response.total || 0;
+        this.loading = false;
+      }).catch(() => {
         this.loading = false;
       });
     },
@@ -1150,19 +1170,15 @@ export default {
     /** 新增按钮操作 */
     handleAdd() {
       this.reset();
-      // 清空商品列表
       this.productJson = [];
-      // 清空用户搜索手机号
+      this.productTotal = 0;
       this.phone = null;
-      // 清空用户列表
       this.users = [];
-      // 清空地址列表
       this.address = [];
-      // 清空就诊人列表
       this.patientList = [];
-      // 清空商品搜索名称
-      this.queryParams.productName = null;
-      // 重置表单
+      this.productNameSearch = null;
+      this.productQueryParams.pageNum = 1;
+      this.productQueryParams.productName = null;
       this.form = {
         addressId: null,
         userId: null,
@@ -1174,6 +1190,7 @@ export default {
       };
       this.open = true;
       this.title = "添加订单";
+      this.getProductList();
     },
     /** 修改按钮操作 */
     handleUpdate(row) {
@@ -1304,6 +1321,7 @@ export default {
         if (valid) {
           const data = {
             orderIds: this.approveForm.orderIds,
+            orderType: 1,
             auditType: this.approveForm.auditType,
             newTotalPrice: this.approveForm.newTotalPrice,
             newPayPrice: this.approveForm.newPayPrice,