Jelajahi Sumber

feat:更新

钱新宇 1 bulan lalu
induk
melakukan
0938b8cb4b

+ 6 - 1
.env.development

@@ -17,4 +17,9 @@ VITE_APP_API_BASEURL7='http://10.148.96.26:8001'
 
 VITE_APP_API_BASEURL8='http://10.148.96.25:8001'
 
-VITE_APP_API_BASEURL9='http://10.144.117.40:8001'
+VITE_APP_API_BASEURL9='http://10.144.117.40:8001'
+
+VITE_APP_API_BASEURL10='http://10.148.96.28:8001'
+
+VITE_APP_API_BASEURL11='http://10.148.96.27:8004'
+

+ 110 - 50
traceCodePackages/traceabilityReport/pages/blacklist/detail/index.vue

@@ -19,24 +19,40 @@
       </view>
       <view class="meta">
         <view class="dot"></view>
-        <text class="meta-text">信用代码:91370104771001730R</text>
+        <text class="meta-text">信用代码:{{ creditCode }}</text>
       </view>
     </view>
     <view
       class="list-container"
       :style="{ paddingTop: statusBarHeight - 30 + 'px' }"
     >
-      <view v-for="(prd, i) in products" :key="'prd-' + i">
-        <view class="section-title">{{
-          i == 0 ? "感冒灵" : "复方氨酚颗粒"
-        }}</view>
+      <view v-if="loading" class="empty-row">加载中...</view>
+      <view v-else-if="products.length === 0" class="empty-row">暂无数据</view>
+      <view v-else v-for="(prd, i) in products" :key="'prd-' + i">
+        <view class="section-title">{{ prd.productType }}{{ prd.pkgSpec ? '(' + prd.pkgSpec + ')' : '' }}</view>
 
         <view class="card product-card">
-          <view v-for="(c, j) in prd.customers" :key="'cust-' + j">
+          <view v-for="(c, j) in prd.upstreamDTOList" :key="'cust-' + j">
             <view class="customer-row" @click.stop="toggleCustomer(i, j)">
-              <view>
-                <text class="customer-label">上游客户:</text>
-                <text class="customer-name">{{ c.name }}</text>
+              <view class="customer-info-main">
+                <view class="main-row">
+                  <text class="customer-label">上游客户:</text>
+                  <text class="customer-name highlight">{{ c.upstreamCustomer }}</text>
+                </view>
+                <view class="info-sub-grid">
+                  <view class="sub-item">
+                    <text class="sub-label">客户级别:</text>
+                    <text class="sub-value">{{ c.customerLevel || '--' }}</text>
+                  </view>
+                  <view class="sub-item">
+                    <text class="sub-label">客户类型:</text>
+                    <text class="sub-value">{{ c.customerNature || '--' }}</text>
+                  </view>
+                  <view class="sub-item">
+                    <text class="sub-label">责任经理:</text>
+                    <text class="sub-value">{{ c.responsibleManager || '--' }}</text>
+                  </view>
+                </view>
               </view>
               <uni-icons
                 type="down"
@@ -61,17 +77,17 @@
                   <view class="blk-body">
                     <view
                       class="blk-row"
-                      v-for="(row, idx) in c.details"
+                      v-for="(row, idx) in c.detailLineDTOList"
                       :key="'row-' + idx"
                     >
-                      <view class="td col-region">{{ row.region }}</view>
+                      <view class="td col-region">{{ row.supplyArea }}</view>
                       <view class="td col-qty">{{ row.quantity }}</view>
-                      <view class="td col-batch">{{ row.batchNo }}</view>
-                      <view class="td col-sample">{{ row.sample }}</view>
-                      <view class="td col-terminal">{{ row.arrivalQty }}</view>
+                      <view class="td col-batch">{{ row.batchNumber }}</view>
+                      <view class="td col-sample">{{ row.supervisionCodeSample }}</view>
+                      <view class="td col-terminal">{{ row.quantity || '--' }}</view>
                     </view>
                     <view
-                      v-if="!c.details || c.details.length === 0"
+                      v-if="!c.detailLineDTOList || c.detailLineDTOList.length === 0"
                       class="empty-row"
                       >暂无数据</view
                     >
@@ -88,6 +104,8 @@
 
 <script>
 import Water from "@/components/water/water.vue";
+import request from "../../../../request/index";
+
 export default {
   components: {
     Water,
@@ -97,6 +115,8 @@ export default {
       statusBarHeight: 20,
       title: "黑名单详情",
       products: [],
+      creditCode: "",
+      loading: false,
     };
   },
   onLoad(options) {
@@ -105,7 +125,10 @@ export default {
     const name =
       options && options.name ? decodeURIComponent(options.name) : "";
     this.title = name || "黑名单详情";
-    this.products = this.generateMockProducts(name || "感冒灵");
+    this.creditCode = options.id || "";
+    this.dateStr = options.dateStr || ''
+
+    this.fetchDetail(this.creditCode, options.dateStr); // 使用固定日期测试,或使用 dateStr
   },
   methods: {
     onBack() {
@@ -114,34 +137,31 @@ export default {
       } catch (e) {}
     },
     toggleCustomer(i, j) {
-      const cur = this.products[i].customers[j];
-      this.$set(this.products[i].customers[j], "expanded", !cur.expanded);
+      const cur = this.products[i].upstreamDTOList[j];
+      this.$set(this.products[i].upstreamDTOList[j], "expanded", !cur.expanded);
+    },
+    fetchDetail(creditCode, billTime) {
+      if (!creditCode) return;
+      this.loading = true;
+      request("/blacklist-report/get-export-company-detail", {
+        creditCode,
+        billTime,
+        path: "traceabilityReport/pages/blacklist/detail/index.vue",
+      }).then((res) => {
+        if (res.code == 200) {
+          this.products = this.processData(res.data || []);
+        }
+        this.loading = false;
+      });
     },
-    generateMockProducts(drugName) {
-      const makeDetails = (n = 3) =>
-        Array.from({ length: n }).map((_, idx) => ({
-          region: ["广东", "江苏", "四川", "山东"][idx % 4],
-          quantity: 100 + idx * 20,
-          batchNo: `B${String(202400 + idx)}`,
-          sample: `抽样${10 + idx}枚`,
-          arrivalQty: 90 + idx * 15,
-        }));
-      return [
-        {
-          name: drugName,
-          customers: [
-            { name: "公司A", expanded: false, details: makeDetails(3) },
-            { name: "公司B", expanded: false, details: makeDetails(2) },
-          ],
-        },
-        {
-          name: drugName,
-          customers: [
-            { name: "公司A", expanded: true, details: makeDetails(4) },
-            { name: "公司C", expanded: false, details: makeDetails(3) },
-          ],
-        },
-      ];
+    processData(data) {
+      return data.map((prd, i) => ({
+        ...prd,
+        upstreamDTOList: (prd.upstreamDTOList || []).map((cust, j) => ({
+          ...cust,
+          expanded: i === 0 && j === 0, // 默认展开第一个
+        })),
+      }));
     },
   },
 };
@@ -256,26 +276,66 @@ export default {
 
 .customer-row {
   display: flex;
-  align-items: center;
-  padding: 12rpx 0;
+  align-items: flex-start;
+  padding: 24rpx 0;
   border-bottom: 1rpx solid #f0f0f0;
-  display: flex;
   justify-content: space-between;
 }
 
-.customer-row:last-child {
-  border-bottom: none;
+.customer-info-main {
+  flex: 1;
+  margin-right: 20rpx;
+}
+
+.main-row {
+  margin-bottom: 16rpx;
+  display: flex;
+  align-items: center;
 }
 
 .customer-label {
   font-size: 28rpx;
-  color: #666;
-  margin-right: 10rpx;
+  color: #999;
+  flex-shrink: 0;
 }
 
 .customer-name {
   font-size: 28rpx;
+  color: #333;
+}
+
+.customer-name.highlight {
   color: #2c69ff;
+  font-weight: 600;
+  font-size: 30rpx;
+}
+
+.info-sub-grid {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 16rpx 24rpx;
+}
+
+.sub-item {
+  display: flex;
+  align-items: center;
+  min-width: 45%;
+}
+
+.sub-label {
+  font-size: 24rpx;
+  color: #bbb;
+  flex-shrink: 0;
+}
+
+.sub-value {
+  font-size: 24rpx;
+  color: #666;
+  font-weight: 500;
+}
+
+.customer-row:last-child {
+  border-bottom: none;
 }
 
 .expand-tag {

+ 336 - 120
traceCodePackages/traceabilityReport/pages/blacklist/history/index.vue

@@ -3,89 +3,99 @@
     <view class="header-section">
       <view class="header-title">历史记录</view>
       <view class="filter-tabs">
-        <view
-          class="tab-item"
-          v-for="(tab, index) in timeFilters"
-          :key="index"
-          :class="{ active: currentFilter === tab.value }"
-          @click="switchFilter(tab.value)"
-        >
+        <view class="tab-item" v-for="(tab, index) in timeFilters" :key="index"
+          :class="{ active: currentFilter === tab.value }" @click="switchFilter(tab.value)">
           {{ tab.label }}
         </view>
       </view>
     </view>
 
-    <scroll-view
-      class="list-scroll"
-      scroll-y="true"
-      refresher-enabled
-      :refresher-triggered="isRefreshing"
-      @refresherrefresh="onRefresh"
-      @scrolltolower="onLoadMore"
-    >
+    <scroll-view class="list-scroll" scroll-y="true" refresher-enabled :refresher-triggered="isRefreshing"
+      @refresherrefresh="onRefresh">
       <view class="list-container">
-        <view
-          class="card-item"
-          v-for="(item, index) in displayRows"
-          :key="index"
-        >
-          <view class="card-header">
-            <view class="header-left">
-              <text class="date-text">{{ item.date }}</text>
-            </view>
-            <view class="header-right">
-              <text class="status-tag">黑名单</text>
+        <view class="date-group" v-for="groupItem in groupedRows" :key="groupItem.date">
+          <view class="date-group-card">
+            <view class="group-left">
+              <!-- <text class="calendar-icon">📅</text> -->
+              <text class="date-label">{{ groupItem.date }}</text>
             </view>
+            <button class="group-export-btn" @click.stop="handleExportByDate(groupItem)">
+              导出
+            </button>
           </view>
 
-          <view class="card-body">
-            <view class="row">
-              <text class="label">公司名称:</text>
-              <text class="value">{{ item.receiverName }}</text>
-            </view>
-            <view class="code-row">
-              <text class="label">信用代码:</text>
-              <text class="value code-text">{{ item.companyCode }}</text>
+          <view class="card-item" v-for="(item, index) in groupItem.list" :key="index" @click="toDetail(item)">
+            <view class="card-header">
+              <view class="header-left">
+                <text class="company-name">{{ item.queryCustomer }}</text>
+              </view>
+              <view class="header-right">
+                <text class="status-tag">黑名单</text>
+              </view>
             </view>
-            <view class="info-grid">
-              <view class="info-item">
-                <text class="label">省份</text>
-                <text class="value">{{ item.receiverProvince }}</text>
+
+            <view class="card-body">
+              <view class="code-row">
+                <text class="label">信用代码:</text>
+                <text class="value code-text">{{ item.socialCreditCode }}</text>
               </view>
-              <view class="info-item">
-                <text class="label">性质</text>
-                <text class="value">{{ item.customerNature }}</text>
+              <view class="info-grid">
+                <view class="info-item">
+                  <text class="label">省份</text>
+                  <text class="value">{{ item.province }}</text>
+                </view>
               </view>
             </view>
           </view>
         </view>
 
-        <view class="loading-more" v-if="loading">
-          <image
-            class="loading-icon"
-            src="../../../../static/images/loading.png"
-          />
+        <view v-if="loading" class="loading-more">
+          <image class="loading-icon" src="../../../../static/images/loading.png" />
           <text class="loading-text">加载中...</text>
         </view>
 
-        <view v-if="!loading && displayRows.length === 0" class="empty-data">
+        <view v-if="!loading && rows.length === 0" class="empty-data">
           <EmptyView text="暂无历史记录" />
         </view>
-
-        <view v-if="!hasMore && displayRows.length > 0" class="no-more">
-          <text>没有更多数据了</text>
-        </view>
       </view>
     </scroll-view>
+
+    <!-- 蒙层 -->
+    <view class="report-export-create-modal-mask" v-if="emailModalOpen" @click="closeEmailModal"
+      @touchmove.stop.prevent></view>
+
+    <!-- 邮箱导出弹窗 -->
+    <view class="report-export-create-modal report-export-create-email-modal"
+      :class="{ 'report-export-create-modal--open': emailModalOpen }" v-if="emailModalOpen">
+      <view class="report-export-create-modal-title">
+        <text>发送至邮箱</text>
+        <view class="report-export-create-modal-close" @click.stop="closeEmailModal">×</view>
+      </view>
+      <view class="report-export-create-modal-body">
+        <view class="export-date-tip">导出日期:{{ currentExportDate }}</view>
+        <up-input v-model="emailForm.email" border="none" :customStyle="{
+          backgroundColor: '#ebf3fb',
+          height: '80rpx',
+          paddingLeft: '20rpx',
+        }" placeholder="请填写邮箱" @input="emailError = false">
+          <template #suffix>
+            <view style="margin-right: 20rpx; color: #666">@999.com.cn</view>
+          </template>
+        </up-input>
+        <text v-if="emailError" class="report-export-error-text">请输入邮箱</text>
+      </view>
+      <view class="report-export-create-modal-footer">
+        <view class="report-export-create-modal-btn cancel-btn" @click.stop="closeEmailModal">取消</view>
+        <view class="report-export-create-modal-btn confirm-btn" @click.stop="handleSendEmail">发送</view>
+      </view>
+    </view>
   </view>
 </template>
 
 <script>
 import EmptyView from "../../../../wigets/empty.vue";
 import { formatDate } from "../../../../utils/utils.js";
-
-const PROVINCES = ["北京市", "上海市", "广东省", "浙江省", "江苏省"];
-const NATURES = ["协议客户", "非协议客户", "商业客户", "终端客户"];
+import request from "../../../../request/index.js";
 
 export default {
   components: {
@@ -96,9 +106,6 @@ export default {
       isRefreshing: false,
       loading: false,
       rows: [],
-      hasMore: true,
-      pageNum: 1,
-      pageSize: 20,
       currentFilter: "all",
       timeFilters: [
         { label: "全部", value: "all" },
@@ -106,15 +113,37 @@ export default {
         { label: "近15天", value: "15" },
         { label: "近30天", value: "30" },
       ],
+      emailModalOpen: false,
+      emailForm: {
+        email: "",
+      },
+      emailError: false,
+      currentExportDate: "",
     };
   },
   computed: {
-    displayRows() {
-      return this.rows;
+    groupedRows() {
+      const groups = {};
+      this.rows.forEach((item) => {
+        const date = item.billTime || "未知日期";
+        if (!groups[date]) {
+          groups[date] = [];
+        }
+        groups[date].push(item);
+      });
+      // 按日期倒序排列
+      return Object.keys(groups)
+        .sort((a, b) => new Date(b.replace(/-/g, "/")) - new Date(a.replace(/-/g, "/")))
+        .map((date) => ({
+          date,
+          list: groups[date],
+        }));
     },
   },
   created() {
     this.resetFetch();
+    const userEmail = uni.getStorageSync('traceCode_useremail')
+    this.emailForm.email = userEmail
   },
   methods: {
     formatDate,
@@ -125,78 +154,93 @@ export default {
       this.resetFetch();
     },
 
-    generateFakeData() {
-      const newRows = [];
-      const count = 15;
-      const today = new Date();
-      const codes = [
-        "91370104771001730R",
-        "91420105783155681H",
-        "91330110MA2CCJE32Y",
-        "91441581761581268X",
-        "91510106768621824L",
-      ];
-
-      for (let i = 0; i < count; i++) {
-        const date = new Date(today);
-        const daysBack = Math.floor(Math.random() * 30);
-
-        if (this.currentFilter !== "all") {
-          const limit = parseInt(this.currentFilter);
-          if (daysBack > limit) continue;
-        }
+    fetchList() {
+      if (this.loading) return;
+      this.loading = true;
 
-        date.setDate(date.getDate() - daysBack);
-
-        newRows.push({
-          id: Math.random().toString(36).substr(2, 9),
-          date: formatDate(date, "YYYY-MM-DD"),
-          receiverName: `黑名单企业${Math.floor(Math.random() * 100)}有限公司`,
-          companyCode: codes[Math.floor(Math.random() * codes.length)],
-          receiverProvince:
-            PROVINCES[Math.floor(Math.random() * PROVINCES.length)],
-          customerNature: NATURES[Math.floor(Math.random() * NATURES.length)],
-        });
+      const today = new Date();
+      let fromDateStr = "";
+      const toDateStr = formatDate(today, "YYYY-MM-DD");
+
+      if (this.currentFilter === "all") {
+        // 全部:最近2个月
+        const twoMonthsAgo = new Date();
+        twoMonthsAgo.setMonth(today.getMonth() - 2);
+        fromDateStr = formatDate(twoMonthsAgo, "YYYY-MM-DD");
+      } else {
+        const days = parseInt(this.currentFilter);
+        const pastDate = new Date();
+        pastDate.setDate(today.getDate() - days);
+        fromDateStr = formatDate(pastDate, "YYYY-MM-DD");
       }
 
-      newRows.sort((a, b) => new Date(b.date) - new Date(a.date));
-      return newRows;
+      request("/blacklist-report/get-export-company-data", {
+        fromDate: '2024-01-01',//fromDateStr,
+        toDate: '2026-01-01',//toDateStr,
+        path: "traceabilityReport/pages/blacklist/history/index.vue",
+      }).then((res) => {
+        if (res.code == 200) {
+          this.rows = res.data || [];
+        }
+        this.loading = false;
+        this.isRefreshing = false;
+      });
     },
 
     async onRefresh() {
       this.isRefreshing = true;
-      this.pageNum = 1;
-      this.hasMore = true;
-      setTimeout(() => {
-        this.rows = this.generateFakeData();
-        this.isRefreshing = false;
-      }, 1000);
-    },
-
-    onLoadMore() {
-      if (this.loading || !this.hasMore) return;
-      this.loading = true;
-      this.pageNum++;
-      setTimeout(() => {
-        const more = this.generateFakeData();
-        if (more.length > 0) {
-          this.rows = [...this.rows, ...more];
-        } else {
-          this.hasMore = false;
-        }
-        this.loading = false;
-      }, 800);
+      this.fetchList();
     },
 
     resetFetch() {
-      this.loading = true;
-      this.pageNum = 1;
-      this.hasMore = true;
       this.rows = [];
-      setTimeout(() => {
-        this.rows = this.generateFakeData();
-        this.loading = false;
-      }, 500);
+      this.fetchList();
+    },
+
+    toDetail(item) {
+      uni.navigateTo({
+        url: `/traceCodePackages/traceabilityReport/pages/blacklist/detail/index?id=${item.socialCreditCode}&dateStr=${item.billTime}&name=${encodeURIComponent(item.queryCustomer)}`,
+      });
+    },
+
+    handleExportByDate(data) {
+      this.currentExportDate = data.date;
+      this.emailModalOpen = true;
+      this.emailError = false;
+      this.taskId = data.list[0]?.taskId || ''
+    },
+
+    closeEmailModal() {
+      this.emailModalOpen = false;
+      this.taskId = ''
+    },
+
+    handleSendEmail() {
+      if (!this.emailForm.email) {
+        this.emailError = true;
+        return;
+      }
+      uni.showLoading({ title: "发送中..." });
+
+      request("/report/sendemail", {
+        taskId: this.taskId,
+        emailAddress: this.emailForm.email + "@999.com.cn",
+        path: "traceabilityReport/pages/blacklist/history/index.vue",
+      }).then((res) => {
+        uni.hideLoading();
+        if (res.code == 200) {
+          uni.showToast({
+            title: "发送成功",
+            icon: "success",
+          });
+          this.closeEmailModal();
+        } else {
+          uni.showToast({
+            title: res.msg || "发送失败",
+            icon: "none",
+          });
+        }
+      });
     },
   },
 };
@@ -210,6 +254,14 @@ export default {
   background: #f5f7fa;
 }
 
+.company-name {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #096dd9;
+  line-height: 1.4;
+  text-decoration: underline;
+}
+
 .header-section {
   background: linear-gradient(135deg, #2b32b2 0%, #1488cc 100%);
   padding: 30rpx 30rpx 60rpx;
@@ -262,9 +314,64 @@ export default {
 }
 
 .list-container {
+  padding-top: 65rpx;
   padding-bottom: 40rpx;
 }
 
+.date-group {
+  margin-bottom: 40rpx;
+}
+
+.date-group-card {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  background: #fff;
+  padding: 20rpx 30rpx;
+  border-radius: 16rpx;
+  margin-bottom: 20rpx;
+  box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.03);
+  border-left: 8rpx solid #1488cc;
+}
+
+.group-left {
+  display: flex;
+  align-items: center;
+}
+
+.calendar-icon {
+  font-size: 32rpx;
+  margin-right: 16rpx;
+}
+
+.date-label {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #333;
+}
+
+.group-export-btn {
+  margin: 0;
+  padding: 0 24rpx;
+  height: 52rpx;
+  line-height: 50rpx;
+  font-size: 24rpx;
+  color: #1488cc;
+  background: #fff;
+  border: 2rpx solid #1488cc;
+  border-radius: 26rpx;
+  font-weight: 500;
+}
+
+.group-export-btn::after {
+  border: none;
+}
+
+.group-export-btn:active {
+  opacity: 0.7;
+  transform: scale(0.95);
+}
+
 .card-item {
   background: #fff;
   border-radius: 20rpx;
@@ -361,8 +468,117 @@ export default {
   from {
     transform: rotate(0deg);
   }
+
   to {
     transform: rotate(360deg);
   }
 }
+
+/* 弹窗样式 */
+.report-export-create-modal-mask {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.6);
+  z-index: 999;
+  backdrop-filter: blur(2px);
+}
+
+.report-export-create-modal {
+  position: fixed;
+  z-index: 1000;
+  background: #fff;
+  transition: all 0.3s ease;
+}
+
+.report-export-create-email-modal {
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  width: 620rpx;
+  border-radius: 24rpx;
+  opacity: 0;
+  visibility: hidden;
+  overflow: hidden;
+}
+
+.report-export-create-email-modal.report-export-create-modal--open {
+  opacity: 1;
+  visibility: visible;
+}
+
+.report-export-create-modal-title {
+  padding: 36rpx 32rpx;
+  font-size: 36rpx;
+  color: #333;
+  text-align: center;
+  font-weight: 600;
+  border-bottom: 1rpx solid #f0f0f0;
+  position: relative;
+}
+
+.report-export-create-modal-close {
+  position: absolute;
+  right: 32rpx;
+  top: 50%;
+  transform: translateY(-50%);
+  font-size: 44rpx;
+  color: #999;
+  line-height: 1;
+  padding: 10rpx;
+}
+
+.report-export-create-modal-body {
+  padding: 40rpx 40rpx 32rpx;
+}
+
+.export-date-tip {
+  font-size: 26rpx;
+  color: #1488cc;
+  margin-bottom: 32rpx;
+  background: #ebf3fb;
+  padding: 20rpx 24rpx;
+  border-radius: 12rpx;
+  border-left: 6rpx solid #1488cc;
+  font-weight: 500;
+}
+
+.report-export-error-text {
+  font-size: 24rpx;
+  color: #ff4d4f;
+  margin-top: 12rpx;
+  display: block;
+}
+
+.report-export-create-modal-footer {
+  padding: 0 40rpx 48rpx;
+  display: flex;
+  justify-content: space-between;
+  gap: 24rpx;
+}
+
+.report-export-create-modal-btn {
+  height: 88rpx;
+  line-height: 88rpx;
+  border-radius: 44rpx;
+  text-align: center;
+  font-size: 30rpx;
+  font-weight: 600;
+  flex: 1;
+  margin: 0;
+}
+
+.cancel-btn {
+  background: #f5f7fa;
+  color: #666;
+  border: 1rpx solid #e4e7ed;
+}
+
+.confirm-btn {
+  background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
+  color: #fff;
+  box-shadow: 0 6rpx 16rpx rgba(24, 144, 255, 0.3);
+}
 </style>

+ 235 - 173
traceCodePackages/traceabilityReport/pages/blacklist/index.vue

@@ -4,37 +4,23 @@
     <view class="header-section">
       <view class="header-content">
         <view class="title-group">
-          <text class="main-title">黑名单企业</text>
+          <text class="main-title">黑名单企业监控</text>
           <text class="sub-title">实时监控异常经营企业</text>
         </view>
         <view class="stat-box">
-          <text class="stat-num">{{ totalCount }}</text>
+          <text class="stat-num">{{ rows?.length || 0 }}</text>
           <text class="stat-unit">家</text>
         </view>
       </view>
       <view class="header-toolbar">
         <view class="update-tip">
-          <text class="tip-icon">🕒</text>
-          <text
-            >更新:{{
-              formatDate(
-                new Date().setDate(new Date().getDate() - 1),
-                "YYYY-MM-DD",
-              ) || "--"
-            }}</text
-          >
-        </view>
-
-        <view class="filter-wrap">
-          <view class="selector" @click.stop="toggleProvinceDropdown">
-            <text class="selector-text">{{
-              selectedProvince || "全部省份"
-            }}</text>
-            <text
-              class="selector-arrow"
-              :class="{ open: dropdownProvinceOpen }"
-            ></text>
-          </view>
+          <!-- <text class="tip-icon">🕒</text> -->
+          <text>更新:{{
+            formatDate(
+              new Date().setDate(new Date().getDate() - 1),
+              "YYYY-MM-DD",
+            ) || "--"
+          }}</text>
         </view>
       </view>
       <!-- 背景装饰 -->
@@ -42,25 +28,14 @@
       <view class="header-circle circle-2"></view>
     </view>
 
-    <scroll-view
-      class="list-scroll"
-      scroll-y="true"
-      refresher-enabled
-      :refresher-triggered="isRefreshing"
-      @refresherrefresh="onRefresh"
-      @scrolltolower="onLoadMore"
-    >
+    <scroll-view class="list-scroll" scroll-y="true" refresher-enabled :refresher-triggered="isRefreshing"
+      @refresherrefresh="onRefresh">
       <view class="list-container">
-        <view
-          class="card-item"
-          v-for="(item, index) in rows"
-          :key="index"
-          @click="toDetail(item)"
-        >
+        <view class="card-item" v-for="(item, index) in rows" :key="index" @click="toDetail(item)">
           <view class="card-header">
             <view class="header-left">
               <text class="index-num">{{ index + 1 }}</text>
-              <text class="company-name">{{ item.receiverName }}</text>
+              <text class="company-name">{{ item.queryCustomer }}</text>
             </view>
             <view class="header-right">
               <text class="status-tag">黑名单</text>
@@ -70,36 +45,20 @@
           <view class="card-body">
             <view class="code-row">
               <text class="label">信用代码:</text>
-              <text class="value code-text">{{ item.companyCode }}</text>
+              <text class="value code-text">{{ item.socialCreditCode }}</text>
             </view>
             <view class="info-grid">
               <view class="info-item">
                 <text class="label">省份</text>
-                <text class="value">{{ item.receiverProvince }}</text>
-              </view>
-              <view class="info-item">
-                <text class="label">性质</text>
-                <text class="value">{{ item.customerNature }}</text>
+                <text class="value">{{ item.province }}</text>
               </view>
             </view>
           </view>
         </view>
 
-        <view class="loading-more" v-if="loading">
-          <image
-            class="loading-icon"
-            src="../../../static/images/loading.png"
-          />
-          <text class="loading-text">加载中...</text>
-        </view>
-
         <view v-if="!loading && rows.length === 0" class="empty-data">
           <EmptyView text="无相关数据" />
         </view>
-
-        <view v-if="!hasMore && rows.length > 0" class="no-more">
-          <text>没有更多数据了</text>
-        </view>
       </view>
     </scroll-view>
 
@@ -112,38 +71,54 @@
       </button>
     </view>
 
+    <!-- 蒙层 -->
+    <view class="report-export-create-modal-mask" v-if="emailModalOpen" @click="closeEmailModal"
+      @touchmove.stop.prevent></view>
+
+    <!-- 邮箱导出弹窗 -->
+    <view class="report-export-create-modal report-export-create-email-modal"
+      :class="{ 'report-export-create-modal--open': emailModalOpen }" v-if="emailModalOpen">
+      <view class="report-export-create-modal-title">
+        <text>发送至邮箱</text>
+        <view class="report-export-create-modal-close" @click.stop="closeEmailModal">×</view>
+      </view>
+      <view class="report-export-create-modal-body" :style="{ padding: '40rpx' }">
+        <up-input v-model="emailForm.email" border="none" :customStyle="{
+          backgroundColor: '#ebf3fb',
+          height: '80rpx',
+          paddingLeft: '20rpx',
+        }" placeholder="请填写邮箱" @input="emailError = false">
+          <template #suffix>
+            <view style="margin-right: 20rpx; color: #666">@999.com.cn</view>
+          </template>
+        </up-input>
+        <text v-if="emailError" class="report-export-error-text">请输入邮箱</text>
+      </view>
+      <view class="report-export-create-modal-footer" :style="{
+        padding: '20rpx 40rpx 40rpx',
+        display: 'flex',
+        'justify-content': 'space-between',
+      }">
+        <view class="report-export-create-modal-btn cancel-btn" @click.stop="closeEmailModal">取消</view>
+        <view class="report-export-create-modal-btn confirm-btn" @click.stop="handleSendEmail">发送</view>
+      </view>
+    </view>
+
     <!-- 独立的下拉菜单层 -->
     <view class="dropdown-layer" v-if="dropdownProvinceOpen" @click.stop>
       <view class="dropdown">
         <view class="dropdown-search-bar">
-          <input
-            class="dropdown-search-input"
-            v-model="provinceSearchText"
-            @input="onProvinceSearch"
-            placeholder="搜索省份..."
-            placeholder-style="color: #999"
-          />
+          <input class="dropdown-search-input" v-model="provinceSearchText" @input="onProvinceSearch"
+            placeholder="搜索省份..." placeholder-style="color: #999" />
         </view>
         <scroll-view scroll-y="true" class="dropdown-scroll-view">
-          <view
-            class="dropdown-item"
-            v-for="(p, i) in filteredProvinceList"
-            :key="p || i"
-            :class="{
-              active: p === selectedProvince || (!selectedProvince && i === 0),
-            }"
-            @click.stop="selectProvince(p)"
-          >
+          <view class="dropdown-item" v-for="(p, i) in filteredProvinceList" :key="p || i" :class="{
+            active: p === selectedProvince || (!selectedProvince && i === 0),
+          }" @click.stop="selectProvince(p)">
             {{ p || "全部省份" }}
-            <text
-              v-if="p === selectedProvince || (!selectedProvince && i === 0)"
-              class="check-mark"
-              >✓</text
-            >
+            <text v-if="p === selectedProvince || (!selectedProvince && i === 0)" class="check-mark">✓</text>
           </view>
-          <view v-if="filteredProvinceList.length === 0" class="dropdown-empty"
-            >暂无数据</view
-          >
+          <view v-if="filteredProvinceList.length === 0" class="dropdown-empty">暂无数据</view>
         </scroll-view>
       </view>
     </view>
@@ -155,7 +130,6 @@ import EmptyView from "../../../wigets/empty.vue";
 import request from "../../../request/index.js";
 import { formatDate } from "../../../utils/utils.js";
 
-const NATURES = ["协议客户", "非协议客户", "商业客户", "终端客户"];
 
 export default {
   components: {
@@ -166,19 +140,22 @@ export default {
       isRefreshing: false,
       loading: false,
       rows: [],
-      totalCount: 60, // Simulated total count
-      hasMore: true,
-      pageNum: 1,
-      pageSize: 20,
       dropdownProvinceOpen: false,
       provinceSearchText: "",
       provinceList: [""],
       selectedProvince: "",
+      emailModalOpen: false,
+      emailForm: {
+        email: "",
+      },
+      emailError: false,
     };
   },
   created() {
     this.resetFetch();
     this.getProviceList();
+    const userEmail = uni.getStorageSync('traceCode_useremail')
+    this.emailForm.email = userEmail
   },
   methods: {
     formatDate,
@@ -194,82 +171,41 @@ export default {
       });
     },
 
-    generateFakeData() {
-      const newRows = [];
-      const codes = [
-        "91370104771001730R",
-        "91420105783155681H",
-        "91330110MA2CCJE32Y",
-        "91441581761581268X",
-        "91510106768621824L",
-      ];
-      const provinces =
-        this.provinceList.length > 1
-          ? this.provinceList.slice(1)
-          : ["北京市", "上海市", "广东省", "浙江省", "江苏省"];
-
-      const startIdx = (this.pageNum - 1) * this.pageSize;
-      const endIdx = Math.min(startIdx + this.pageSize, this.totalCount);
-
-      if (startIdx >= this.totalCount) {
-        this.hasMore = false;
-        return [];
-      }
+    fetchList() {
+      if (this.loading) return;
+      this.loading = true;
 
-      for (let i = startIdx; i < endIdx; i++) {
-        newRows.push({
-          id: i,
-          receiverName: `测试收货企业${i + 1}有限公司`,
-          companyCode: codes[i % codes.length],
-          receiverProvince: provinces[i % provinces.length],
-          customerNature: NATURES[i % NATURES.length],
-        });
-      }
-      return newRows;
-    },
+      const yesterday = new Date();
+      yesterday.setDate(yesterday.getDate() - 1);
+      const dateStr = this.formatDate(yesterday, "YYYY-MM-DD");
 
-    async onRefresh() {
-      this.isRefreshing = true;
-      this.pageNum = 1;
-      this.hasMore = true;
-      // Simulate network request
-      setTimeout(() => {
-        this.rows = this.generateFakeData();
-        this.isRefreshing = false;
-      }, 1000);
-    },
-
-    onLoadMore() {
-      if (this.loading || !this.hasMore) return;
-      this.loading = true;
-      this.pageNum++;
 
-      // Simulate network request
-      setTimeout(() => {
-        const more = this.generateFakeData();
-        if (more.length > 0) {
-          this.rows = [...this.rows, ...more];
-        } else {
-          this.hasMore = false;
+      request("/blacklist-report/get-export-company-data", {
+        fromDate: dateStr,
+        toDate: dateStr,
+        path: "traceabilityReport/pages/blacklist/index.vue",
+      }).then((res) => {
+        if (res.code == 200) {
+          this.rows = res.data || [];
         }
         this.loading = false;
-      }, 800);
+        this.isRefreshing = false;
+      });
+    },
+
+    async onRefresh() {
+      this.isRefreshing = true;
+      this.fetchList();
     },
 
     resetFetch() {
-      this.loading = true;
-      this.pageNum = 1;
-      this.hasMore = true;
-      // Simulate initial load
-      setTimeout(() => {
-        this.rows = this.generateFakeData();
-        this.loading = false;
-      }, 500);
+      this.rows = [];
+      this.fetchList();
     },
 
     toDetail(item) {
       uni.navigateTo({
-        url: `/traceCodePackages/traceabilityReport/pages/blacklist/detail/index?id=${item.id}&name=${encodeURIComponent(item.receiverName)}`,
+        url: `/traceCodePackages/traceabilityReport/pages/blacklist/detail/index?id=${item.socialCreditCode}&dateStr=${item.billTime}&name=${encodeURIComponent(item.queryCustomer)}`,
       });
     },
 
@@ -280,25 +216,47 @@ export default {
     },
 
     handleExport() {
-      uni.showModal({
-        title: "导出至邮箱",
-        editable: true,
-        placeholderText: "请输入邮箱地址",
-        success: (res) => {
-          if (res.confirm) {
-            if (!res.content) {
-              uni.showToast({
-                title: "请输入邮箱",
-                icon: "none",
-              });
-              return;
-            }
-            uni.showToast({
-              title: "导出请求已发送",
-              icon: "success",
-            });
-          }
-        },
+      if (this.rows?.length == 0) {
+        uni.showToast({
+          title: '暂无数据...',
+          icon: 'none'
+        })
+        return
+      }
+      this.emailModalOpen = true;
+      this.emailError = false;
+    },
+
+    closeEmailModal() {
+      this.emailModalOpen = false;
+    },
+
+    handleSendEmail() {
+
+      if (!this.emailForm.email) {
+        this.emailError = true;
+        return;
+      }
+      uni.showLoading({ title: "发送中..." });
+
+      request("/report/sendemail", {
+        taskId: this.rows[0]?.taskId,
+        emailAddress: this.emailForm.email + "@999.com.cn",
+        path: "traceabilityReport/pages/blacklist/index.vue",
+      }).then((res) => {
+        uni.hideLoading();
+        if (res.code == 200) {
+          uni.showToast({
+            title: "发送成功",
+            icon: "success",
+          });
+          this.closeEmailModal();
+        } else {
+          uni.showToast({
+            title: res.msg || "发送失败",
+            icon: "none",
+          });
+        }
       });
     },
 
@@ -313,6 +271,7 @@ export default {
     selectProvince(province) {
       this.selectedProvince = province || "";
       this.dropdownProvinceOpen = false;
+      this.resetFetch();
     },
   },
   computed: {
@@ -340,7 +299,8 @@ export default {
 /* Header Section */
 .header-section {
   background: linear-gradient(135deg, #2b32b2 0%, #1488cc 100%);
-  padding: 40rpx 40rpx 80rpx; /* Extra padding at bottom for overlap effect */
+  padding: 40rpx 40rpx 80rpx;
+  /* Extra padding at bottom for overlap effect */
   position: relative;
   z-index: 20;
   color: #fff;
@@ -457,7 +417,8 @@ export default {
 /* Dropdown Layer (Absolute on top of everything) */
 .dropdown-layer {
   position: absolute;
-  top: 300rpx; /* Adjust based on header layout */
+  top: 300rpx;
+  /* Adjust based on header layout */
   right: 40rpx;
   z-index: 999;
 }
@@ -543,9 +504,11 @@ export default {
 .list-scroll {
   flex: 1;
   height: 0;
-  padding: 0 24rpx; /* Remove vertical padding from scroll container */
+  padding: 0 24rpx;
+  /* Remove vertical padding from scroll container */
   box-sizing: border-box;
-  margin-top: -50rpx; /* Negative margin to pull list up */
+  margin-top: -50rpx;
+  /* Negative margin to pull list up */
   position: relative;
   z-index: 21;
 }
@@ -592,8 +555,9 @@ export default {
 .company-name {
   font-size: 32rpx;
   font-weight: 600;
-  color: #333;
+  color: #096dd9;
   line-height: 1.4;
+  text-decoration: underline;
 }
 
 .header-right {
@@ -698,6 +662,7 @@ export default {
   from {
     transform: rotate(0deg);
   }
+
   to {
     transform: rotate(360deg);
   }
@@ -748,4 +713,101 @@ export default {
   color: #fff;
   box-shadow: 0 6rpx 16rpx rgba(24, 144, 255, 0.35);
 }
+
+/* 弹窗样式 */
+.report-export-create-modal-mask {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.6);
+  z-index: 999;
+  backdrop-filter: blur(2px);
+}
+
+.report-export-create-modal {
+  position: fixed;
+  z-index: 1000;
+  background: #fff;
+  transition: all 0.3s ease;
+}
+
+.report-export-create-email-modal {
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  width: 620rpx;
+  border-radius: 24rpx;
+  opacity: 0;
+  visibility: hidden;
+  overflow: hidden;
+}
+
+.report-export-create-email-modal.report-export-create-modal--open {
+  opacity: 1;
+  visibility: visible;
+}
+
+.report-export-create-modal-title {
+  padding: 36rpx 32rpx;
+  font-size: 36rpx;
+  color: #333;
+  text-align: center;
+  font-weight: 600;
+  border-bottom: 1rpx solid #f0f0f0;
+  position: relative;
+}
+
+.report-export-create-modal-close {
+  position: absolute;
+  right: 32rpx;
+  top: 50%;
+  transform: translateY(-50%);
+  font-size: 44rpx;
+  color: #999;
+  line-height: 1;
+  padding: 10rpx;
+}
+
+.report-export-create-modal-body {
+  padding: 48rpx 40rpx 32rpx;
+}
+
+.report-export-error-text {
+  font-size: 24rpx;
+  color: #ff4d4f;
+  margin-top: 12rpx;
+  display: block;
+}
+
+.report-export-create-modal-footer {
+  padding: 0 40rpx 48rpx;
+  display: flex;
+  justify-content: space-between;
+  gap: 24rpx;
+}
+
+.report-export-create-modal-btn {
+  height: 88rpx;
+  line-height: 88rpx;
+  border-radius: 44rpx;
+  text-align: center;
+  font-size: 30rpx;
+  font-weight: 600;
+  flex: 1;
+  margin: 0;
+}
+
+.cancel-btn {
+  background: #f5f7fa;
+  color: #666;
+  border: 1rpx solid #e4e7ed;
+}
+
+.confirm-btn {
+  background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
+  color: #fff;
+  box-shadow: 0 6rpx 16rpx rgba(24, 144, 255, 0.3);
+}
 </style>

+ 70 - 63
traceCodePackages/traceabilityReport/pages/ganmaoling/detail/index.vue

@@ -15,29 +15,27 @@
     >
       <view class="meta">
         <view class="dot"></view>
-        <text class="meta-text">所在省份:{{ regionName }}</text>
+        <text class="meta-text">所在省份:{{ customerProvinceName || "--" }}</text>
       </view>
       <view class="meta">
         <view class="dot"></view>
         <text class="meta-text"
-          >客户等级:{{
-            scanType == 1 ? "VIP" : scanType == 2 ? "二级" : "三级"
-          }}客户</text
+          >客户等级:{{ customerLevel || "--" }}</text
         >
       </view>
       <view class="meta">
         <view class="dot"></view>
-        <text class="meta-text">客户性质:{{ customerType || "--" }}</text>
+        <text class="meta-text">客户性质:{{ customerCategory || "--" }}</text>
       </view>
       <view class="meta">
         <view class="dot"></view>
-        <text class="meta-text">责任经理:{{ managerName || "--" }}</text>
+        <text class="meta-text">责任经理:{{ responsibleManager || "--" }}</text>
       </view>
       <view class="meta">
         <view class="dot"></view>
         <text class="meta-text"
           >累计预警次数:{{
-            warningCount || warningCount == 0 ? warningCount : "--"
+            totalWarningAmount || totalWarningAmount == 0 ? totalWarningAmount : "--"
           }}</text
         >
       </view>
@@ -55,7 +53,7 @@
             <view class="th col-batch">批号</view>
             <view class="th col-source">货源地</view>
             <view class="th col-qty">数量</view>
-            <view class="th col-chain">链路</view>
+            <!-- <view class="th col-chain">链路</view> -->
           </view>
           <view class="flow-body">
             <view
@@ -63,22 +61,15 @@
               v-for="(item, index) in flowList"
               :key="index"
             >
-              <view class="td col-time">{{ item.billTime }}</view>
+              <view class="td col-time">{{ formatBillTime(item.billTime) }}</view>
               <view class="td col-type">{{ item.billType }}</view>
               <view class="td col-product">{{ item.productName }}</view>
-              <view class="td-group">
-                <view
-                  class="sub-row"
-                  v-for="(sub, sIdx) in item.items"
-                  :key="sIdx"
-                >
-                  <view class="td col-batch">{{ sub.batchNo }}</view>
-                  <view class="td col-source">{{ sub.source }}</view>
-                  <view class="td col-qty">{{ sub.quantity }}</view>
-                </view>
-              </view>
-              <view class="td col-chain">{{ item.chain }}</view>
+              <view class="td col-batch">{{ item.productBatchNo }}</view>
+              <view class="td col-source">{{ item.regionName }}</view>
+              <view class="td col-qty">{{ item.fromQty }}</view>
+              <!-- <view class="td col-chain chain-text">{{ item.batchTraceLink }}</view> -->
             </view>
+            <view v-if="flowList.length === 0" class="empty-row">暂无数据</view>
           </view>
         </view>
       </scroll-view>
@@ -88,6 +79,9 @@
 
 <script>
 import Water from "@/components/water/water.vue";
+import request from "../../../../request/index.js";
+import { formatDate } from "../../../../utils/utils.js";
+
 export default {
   components: {
     Water,
@@ -95,60 +89,65 @@ export default {
   data() {
     return {
       title: "",
-      managerName: "夜神月",
-      warningCount: 0,
-      scanType: "",
+      customerProvinceName: "",
+      customerLevel: "",
+      customerCategory: "",
+      responsibleManager: "",
+      totalWarningAmount: 0,
       statusBarHeight: 20,
-      regionName: "广东",
-      customerType: "协议客户",
-      flowList: [
-        {
-          billTime: "2023-01-01 12:00",
-          billType: "销售出库",
-          productName: "感冒灵颗粒",
-          items: [
-            { batchNo: "A20230101", source: "广州工厂", quantity: "1000" },
-            { batchNo: "A20230102", source: "深圳工厂", quantity: "500" },
-          ],
-          chain: "经销商A -> 药店B",
-        },
-        {
-          billTime: "2023-01-02 14:30",
-          billType: "调拨入库",
-          productName: "三九胃泰",
-          items: [
-            { batchNo: "B20230105", source: "北京仓库", quantity: "200" },
-            { batchNo: "B20230106", source: "上海仓库", quantity: "300" },
-            { batchNo: "B20230107", source: "武汉仓库", quantity: "150" },
-          ],
-          chain: "总仓 -> 分仓",
-        },
-        {
-          billTime: "2023-01-02 14:30",
-          billType: "调拨入库",
-          productName: "三九胃泰",
-          items: [
-            { batchNo: "B20230105", source: "北京仓库", quantity: "200" },
-            { batchNo: "B20230106", source: "上海仓库", quantity: "300" },
-            { batchNo: "B20230107", source: "武汉仓库", quantity: "150" },
-          ],
-          chain: "总仓 -> 分仓",
-        },
-      ],
+      flowList: [],
+      loading: false,
     };
   },
   onLoad(options) {
     const info = uni.getSystemInfoSync();
     this.statusBarHeight = info.statusBarHeight || 20;
-    this.title =
-      options && options.name ? decodeURIComponent(options.name) : "报表详情";
+    const name = options.name ? decodeURIComponent(options.name) : "";
+    const updatedTime = options.updatedTime || "";
+    this.title = name || "报表详情";
+
+    if (name && updatedTime) {
+      this.fetchDetail(name, updatedTime);
+    }
   },
   methods: {
+    formatDate,
+    formatBillTime(time) {
+      if (!time) return "--";
+      try {
+        return formatDate(new Date(time), "YYYY-MM-DD HH:mm");
+      } catch (e) {
+        return time;
+      }
+    },
     onBack() {
       try {
         uni.navigateBack();
       } catch (e) {}
     },
+    fetchDetail(customerName, updatedTime) {
+      this.loading = true;
+      uni.showLoading({ title: "加载中..." });
+      request(
+        `/report/ganmaoling/list/customers/${customerName}/date/${updatedTime}`,
+        {
+          path: "traceabilityReport/pages/ganmaoling/detail/index.vue",
+        },
+        "get",
+      ).then((res) => {
+        uni.hideLoading();
+        if (res.code == 200 && res.data) {
+          const d = res.data;
+          this.customerProvinceName = d.customerProvinceName;
+          this.customerLevel = d.customerLevel;
+          this.customerCategory = d.customerCategory;
+          this.responsibleManager = d.responsibleManager;
+          this.totalWarningAmount = d.totalWarningAmount;
+          this.flowList = d.reportDTOList || [];
+        }
+        this.loading = false;
+      });
+    },
   },
 };
 </script>
@@ -251,7 +250,7 @@ export default {
 }
 
 .flow-table {
-  min-width: 1300rpx;
+  min-width: 1060rpx;
   border-top: 1rpx solid #ccc;
   border-left: 1rpx solid #ccc;
 }
@@ -323,6 +322,14 @@ export default {
   width: 240rpx;
 }
 
+.chain-text {
+  white-space: pre-wrap;
+  text-align: left !important;
+  font-size: 22rpx;
+  line-height: 1.4;
+  padding: 10rpx !important;
+}
+
 .td-group {
   display: flex;
   flex-direction: column;

+ 386 - 104
traceCodePackages/traceabilityReport/pages/ganmaoling/history/index.vue

@@ -21,54 +21,67 @@
       refresher-enabled
       :refresher-triggered="isRefreshing"
       @refresherrefresh="onRefresh"
-      @scrolltolower="onLoadMore"
     >
       <view class="list-container">
         <view
-          class="card-item"
-          v-for="(item, index) in displayRows"
-          :key="index"
+          class="date-group"
+          v-for="groupItem in groupedRows"
+          :key="groupItem.date"
         >
-          <view class="card-header">
-            <view class="header-left">
-              <text class="date-text">{{ item.date }}</text>
-            </view>
-            <view class="header-right">
-              <text class="alert-count" v-if="item.alertCount > 0"
-                >{{ item.alertCount }}次预警</text
-              >
+          <view class="date-group-card">
+            <view class="group-left">
+              <!-- <text class="calendar-icon">📅</text> -->
+              <text class="date-label">{{ groupItem.date }}</text>
             </view>
+            <button class="group-export-btn" @click.stop="handleExportByDate(groupItem.date)">
+              导出
+            </button>
           </view>
 
-          <view class="card-body">
-            <view class="row">
-              <text class="label">公司名称:</text>
-              <text class="value">{{ item.receiverName }}</text>
-              <text
-                class="level-tag"
-                :class="getLevelClass(item.customerLevel)"
-              >
-                {{ item.customerLevel }}
-              </text>
-            </view>
-            <view class="info-grid">
-              <view class="info-item">
-                <text class="label">省份</text>
-                <text class="value">{{ item.receiverProvince }}</text>
+          <view
+            class="card-item"
+            v-for="(item, index) in groupItem.list"
+            :key="index"
+            @click="toDetail(item)"
+          >
+            <view class="card-header">
+              <view class="header-left">
+                <text class="index-num">{{ index + 1 }}</text>
+                <text class="company-name">{{ item.customerName }}</text>
+                <text
+                  class="level-tag"
+                  :class="getLevelClass(item.customerLevel)"
+                >
+                  {{ item.customerLevel }}
+                </text>
               </view>
-              <view class="info-item">
-                <text class="label">性质</text>
-                <text class="value">{{ item.customerNature }}</text>
+              <view class="header-right">
+                <text class="alert-count" v-if="item.totalWarningAmount > 0"
+                  >{{ item.totalWarningAmount }}次预警</text
+                >
               </view>
-              <view class="info-item">
-                <text class="label">责任人</text>
-                <text class="value">{{ item.responderName }}</text>
+            </view>
+
+            <view class="card-body">
+              <view class="info-grid">
+                <view class="info-item">
+                  <text class="label">省份</text>
+                  <text class="value">{{ item.customerProvinceName }}</text>
+                </view>
+                <view class="info-item">
+                  <text class="label">性质</text>
+                  <text class="value">{{ item.responsibleManager }}</text>
+                </view>
+                <view class="info-item">
+                  <text class="label">责任人</text>
+                  <text class="value">{{ item.customerCategory }}</text>
+                </view>
               </view>
             </view>
           </view>
         </view>
 
-        <view class="loading-more" v-if="loading">
+        <view v-if="loading" class="loading-more">
           <image
             class="loading-icon"
             src="../../../../static/images/loading.png"
@@ -76,23 +89,79 @@
           <text class="loading-text">加载中...</text>
         </view>
 
-        <view v-if="!loading && displayRows.length === 0" class="empty-data">
+        <view v-if="!loading && rows.length === 0" class="empty-data">
           <EmptyView text="暂无历史记录" />
         </view>
-
-        <view v-if="!hasMore && displayRows.length > 0" class="no-more">
-          <text>没有更多数据了</text>
-        </view>
       </view>
     </scroll-view>
+
+    <!-- 蒙层 -->
+    <view
+      class="report-export-create-modal-mask"
+      v-if="emailModalOpen"
+      @click="closeEmailModal"
+      @touchmove.stop.prevent
+    ></view>
+
+    <!-- 邮箱导出弹窗 -->
+    <view
+      class="report-export-create-modal report-export-create-email-modal"
+      :class="{ 'report-export-create-modal--open': emailModalOpen }"
+      v-if="emailModalOpen"
+    >
+      <view class="report-export-create-modal-title">
+        <text>发送至邮箱</text>
+        <view
+          class="report-export-create-modal-close"
+          @click.stop="closeEmailModal"
+          >×</view
+        >
+      </view>
+      <view
+        class="report-export-create-modal-body"
+      >
+        <view class="export-date-tip">导出日期:{{ currentExportDate }}</view>
+        <up-input
+          v-model="emailForm.email"
+          border="none"
+          :customStyle="{
+            backgroundColor: '#ebf3fb',
+            height: '80rpx',
+            paddingLeft: '20rpx',
+          }"
+          placeholder="请填写邮箱"
+          @input="emailError = false"
+        >
+          <template #suffix>
+            <view style="margin-right: 20rpx; color: #666">@999.com.cn</view>
+          </template>
+        </up-input>
+        <text v-if="emailError" class="report-export-error-text"
+          >请输入邮箱</text
+        >
+      </view>
+      <view
+        class="report-export-create-modal-footer"
+      >
+        <view
+          class="report-export-create-modal-btn cancel-btn"
+          @click.stop="closeEmailModal"
+          >取消</view
+        >
+        <view
+          class="report-export-create-modal-btn confirm-btn"
+          @click.stop="handleSendEmail"
+          >发送</view
+        >
+      </view>
+    </view>
   </view>
 </template>
 
 <script>
 import EmptyView from "../../../../wigets/empty.vue";
 import { formatDate } from "../../../../utils/utils.js";
-
-const PROVINCES = ["北京市", "上海市", "广东省", "浙江省", "江苏省"];
+import request from "../../../../request/index.js";
 
 export default {
   components: {
@@ -103,9 +172,6 @@ export default {
       isRefreshing: false,
       loading: false,
       rows: [],
-      hasMore: true,
-      pageNum: 1,
-      pageSize: 20,
       currentFilter: "all",
       timeFilters: [
         { label: "全部", value: "all" },
@@ -113,11 +179,33 @@ export default {
         { label: "近15天", value: "15" },
         { label: "近30天", value: "30" },
       ],
+      emailModalOpen: false,
+      emailForm: {
+        email: "",
+      },
+      emailError: false,
+      currentExportDate: "",
     };
   },
   computed: {
-    displayRows() {
-      return this.rows;
+    groupedRows() {
+      const groups = {};
+      this.rows.forEach((item) => {
+        // 假设接口返回的时间字段为 updatedTime
+        const fullTime = item.updatedTime || "";
+        const date = fullTime.split(" ")[0] || fullTime.split("T")[0] || "未知日期";
+        if (!groups[date]) {
+          groups[date] = [];
+        }
+        groups[date].push(item);
+      });
+      // 按日期倒序排列
+      return Object.keys(groups)
+        .sort((a, b) => new Date(b.replace(/-/g, "/")) - new Date(a.replace(/-/g, "/")))
+        .map((date) => ({
+          date,
+          list: groups[date],
+        }));
     },
   },
   created() {
@@ -139,76 +227,84 @@ export default {
       this.resetFetch();
     },
 
-    generateFakeData() {
-      const newRows = [];
-      const count = 15;
-      const today = new Date();
-
-      for (let i = 0; i < count; i++) {
-        const date = new Date(today);
-        // Random date within last 30 days
-        const daysBack = Math.floor(Math.random() * 30);
+    fetchList() {
+      if (this.loading) return;
+      this.loading = true;
 
-        // Filter logic simulation
-        if (this.currentFilter !== "all") {
-          const limit = parseInt(this.currentFilter);
-          if (daysBack > limit) continue;
-        }
+      const params = {
+        path: "traceabilityReport/pages/ganmaoling/history/index.vue",
+      };
 
-        date.setDate(date.getDate() - daysBack);
-
-        newRows.push({
-          id: Math.random().toString(36).substr(2, 9),
-          date: formatDate(date, "YYYY-MM-DD"),
-          receiverName: `历史收货企业${Math.floor(Math.random() * 100)}有限公司`,
-          receiverProvince:
-            PROVINCES[Math.floor(Math.random() * PROVINCES.length)],
-          customerNature: Math.random() > 0.5 ? "协议客户" : "非协议客户",
-          alertCount: Math.floor(Math.random() * 5),
-          customerLevel: ["VIP", "二级", "三级"][Math.floor(Math.random() * 3)],
-        });
+      if (this.currentFilter !== "all") {
+        params.days = this.currentFilter;
       }
 
-      // Sort by date descending
-      newRows.sort((a, b) => new Date(b.date) - new Date(a.date));
-
-      return newRows;
+      request(`/report/ganmaoling/list`, params,"get").then((res) => {
+        if (res.code == 200) {
+          this.rows = res.data || [];
+        }
+        this.loading = false;
+        this.isRefreshing = false;
+      });
     },
 
     async onRefresh() {
       this.isRefreshing = true;
-      this.pageNum = 1;
-      this.hasMore = true;
-      setTimeout(() => {
-        this.rows = this.generateFakeData();
-        this.isRefreshing = false;
-      }, 1000);
-    },
-
-    onLoadMore() {
-      if (this.loading || !this.hasMore) return;
-      this.loading = true;
-      this.pageNum++;
-      setTimeout(() => {
-        const more = this.generateFakeData();
-        if (more.length > 0) {
-          this.rows = [...this.rows, ...more];
-        } else {
-          this.hasMore = false;
-        }
-        this.loading = false;
-      }, 800);
+      this.fetchList();
     },
 
     resetFetch() {
-      this.loading = true;
-      this.pageNum = 1;
-      this.hasMore = true;
       this.rows = [];
-      setTimeout(() => {
-        this.rows = this.generateFakeData();
-        this.loading = false;
-      }, 500);
+      this.fetchList();
+    },
+
+    toDetail(item) {
+      uni.navigateTo({
+        url: `/traceCodePackages/traceabilityReport/pages/ganmaoling/detail/index?name=${encodeURIComponent(item.customerName)}&updatedTime=${item.updatedTime || ""}`,
+      });
+    },
+    handleExportByDate(date) {
+      this.currentExportDate = date;
+      this.emailModalOpen = true;
+      this.emailForm.email = "";
+      this.emailError = false;
+    },
+
+    closeEmailModal() {
+      this.emailModalOpen = false;
+    },
+
+    handleSendEmail() {
+      if (!this.emailForm.email) {
+        this.emailError = true;
+        return;
+      }
+      uni.showLoading({ title: "发送中..." });
+
+      // 根据实际接口文档,如果支持按日期导出,则传日期
+      request(
+        `/report/ganmaoling/list`,
+        {
+          updatedTime: this.currentExportDate,
+          email: this.emailForm.email + "@999.com.cn",
+          path: "traceabilityReport/pages/ganmaoling/history/index.vue",
+        },
+        "get",
+      ).then((res) => {
+        uni.hideLoading();
+        if (res.code == 200) {
+          uni.showToast({
+            title: "发送成功",
+            icon: "success",
+          });
+          this.closeEmailModal();
+        } else {
+          uni.showToast({
+            title: res.msg || "发送失败",
+            icon: "none",
+          });
+        }
+      });
     },
   },
 };
@@ -231,6 +327,29 @@ export default {
   flex-shrink: 0;
 }
 
+.header-left {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  margin-right: 20rpx;
+  gap: 12rpx;
+}
+
+.index-num {
+  font-size: 24rpx;
+  color: #999;
+  font-family: monospace;
+}
+
+.company-name {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #096dd9;
+  line-height: 1.4;
+  text-decoration: underline;
+}
+
 .tag-vip {
   background: linear-gradient(135deg, #e6f7ff, #bae7ff);
   color: #096dd9;
@@ -304,9 +423,64 @@ export default {
 }
 
 .list-container {
+  padding-top: 50rpx;
   padding-bottom: 40rpx;
 }
 
+.date-group {
+  margin-bottom: 40rpx;
+}
+
+.date-group-card {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  background: #fff;
+  padding: 20rpx 30rpx;
+  border-radius: 16rpx;
+  margin-bottom: 20rpx;
+  box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.03);
+  border-left: 8rpx solid #1890ff;
+}
+
+.group-left {
+  display: flex;
+  align-items: center;
+}
+
+.calendar-icon {
+  font-size: 32rpx;
+  margin-right: 16rpx;
+}
+
+.date-label {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #333;
+}
+
+.group-export-btn {
+  margin: 0;
+  padding: 0 24rpx;
+  height: 52rpx;
+  line-height: 50rpx;
+  font-size: 24rpx;
+  color: #1890ff;
+  background: #fff;
+  border: 2rpx solid #1890ff;
+  border-radius: 26rpx;
+  font-weight: 500;
+}
+
+.group-export-btn::after {
+  border: none;
+}
+
+.group-export-btn:active {
+  opacity: 0.7;
+  transform: scale(0.95);
+}
+
 .card-item {
   background: #fff;
   border-radius: 20rpx;
@@ -394,4 +568,112 @@ export default {
     transform: rotate(360deg);
   }
 }
+
+/* 弹窗样式 */
+.report-export-create-modal-mask {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.6);
+  z-index: 999;
+  backdrop-filter: blur(2px);
+}
+
+.report-export-create-modal {
+  position: fixed;
+  z-index: 1000;
+  background: #fff;
+  transition: all 0.3s ease;
+}
+
+.report-export-create-email-modal {
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  width: 620rpx;
+  border-radius: 24rpx;
+  opacity: 0;
+  visibility: hidden;
+  overflow: hidden;
+}
+
+.report-export-create-email-modal.report-export-create-modal--open {
+  opacity: 1;
+  visibility: visible;
+}
+
+.report-export-create-modal-title {
+  padding: 36rpx 32rpx;
+  font-size: 36rpx;
+  color: #333;
+  text-align: center;
+  font-weight: 600;
+  border-bottom: 1rpx solid #f0f0f0;
+  position: relative;
+}
+
+.report-export-create-modal-close {
+  position: absolute;
+  right: 32rpx;
+  top: 50%;
+  transform: translateY(-50%);
+  font-size: 44rpx;
+  color: #999;
+  line-height: 1;
+  padding: 10rpx;
+}
+
+.report-export-create-modal-body {
+  padding: 40rpx 40rpx 32rpx;
+}
+
+.export-date-tip {
+  font-size: 26rpx;
+  color: #1890ff;
+  margin-bottom: 32rpx;
+  background: #e6f7ff;
+  padding: 20rpx 24rpx;
+  border-radius: 12rpx;
+  border-left: 6rpx solid #1890ff;
+  font-weight: 500;
+}
+
+.report-export-error-text {
+  font-size: 24rpx;
+  color: #ff4d4f;
+  margin-top: 12rpx;
+  display: block;
+}
+
+.report-export-create-modal-footer {
+  padding: 0 40rpx 48rpx;
+  display: flex;
+  justify-content: space-between;
+  gap: 24rpx;
+}
+
+.report-export-create-modal-btn {
+  height: 88rpx;
+  line-height: 88rpx;
+  border-radius: 44rpx;
+  text-align: center;
+  font-size: 30rpx;
+  font-weight: 600;
+  flex: 1;
+  margin: 0;
+}
+
+.cancel-btn {
+  background: #f5f7fa;
+  color: #666;
+  border: 1rpx solid #e4e7ed;
+}
+
+.confirm-btn {
+  background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
+  color: #fff;
+  box-shadow: 0 6rpx 16rpx rgba(24, 144, 255, 0.3);
+}
 </style>

+ 233 - 339
traceCodePackages/traceabilityReport/pages/ganmaoling/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <view class="detail-page" @click="closeProvinceDropdown">
+  <view class="detail-page">
     <!-- 背景层 -->
     <view class="header-bg"></view>
 
@@ -23,7 +23,7 @@
 
       <view class="header-toolbar">
         <view class="update-tip">
-          <text class="tip-icon">🕒</text>
+          <!-- <text class="tip-icon">🕒</text> -->
           <text
             >更新:{{
               formatDate(
@@ -33,18 +33,6 @@
             }}</text
           >
         </view>
-
-        <view class="filter-wrap">
-          <view class="selector" @click.stop="toggleProvinceDropdown">
-            <text class="selector-text">{{
-              selectedProvince || "全部省份"
-            }}</text>
-            <text
-              class="selector-arrow"
-              :class="{ open: dropdownProvinceOpen }"
-            ></text>
-          </view>
-        </view>
       </view>
     </view>
 
@@ -55,19 +43,18 @@
       refresher-enabled
       :refresher-triggered="isRefreshing"
       @refresherrefresh="onRefresh"
-      @scrolltolower="onLoadMore"
     >
       <view class="list-container">
         <view
           class="card-item"
-          v-for="(item, index) in displayRows"
+          v-for="(item, index) in rows"
           :key="index"
           @click="toDetail(item)"
         >
           <view class="card-header">
             <view class="header-left">
               <text class="index-num">{{ index + 1 }}</text>
-              <text class="company-name">{{ item.receiverName }}</text>
+              <text class="company-name">{{ item.customerName }}</text>
               <text
                 class="level-tag"
                 :class="getLevelClass(item.customerLevel)"
@@ -76,7 +63,7 @@
               </text>
             </view>
             <view class="header-right">
-              <text class="alert-count">{{ item.alertCount }}次预警</text>
+              <text class="alert-count">{{ item.totalWarningAmount }}次预警</text>
             </view>
           </view>
 
@@ -84,74 +71,26 @@
             <view class="info-grid">
               <view class="info-item">
                 <text class="label">省份</text>
-                <text class="value">{{ item.receiverProvince }}</text>
+                <text class="value">{{ item.customerProvinceName }}</text>
               </view>
               <view class="info-item">
                 <text class="label">责任人</text>
-                <text class="value">{{ item.manager }}</text>
+                <text class="value">{{ item.responsibleManager }}</text>
               </view>
               <view class="info-item">
                 <text class="label">性质</text>
-                <text class="value">{{ item.customerNature }}</text>
+                <text class="value">{{ item.customerCategory }}</text>
               </view>
             </view>
           </view>
         </view>
 
-        <view class="loading-more" v-if="loading">
-          <image
-            class="loading-icon"
-            src="../../../static/images/loading.png"
-          />
-          <text class="loading-text">加载中...</text>
-        </view>
-
-        <view v-if="!loading && displayRows.length === 0" class="empty-data">
+        <view v-if="!loading && rows.length === 0" class="empty-data">
           <EmptyView text="无相关数据" />
         </view>
-
-        <view v-if="!hasMore && displayRows.length > 0" class="no-more">
-          <text>没有更多数据了</text>
-        </view>
       </view>
     </scroll-view>
 
-    <!-- 独立的下拉菜单层 -->
-    <view class="dropdown-layer" v-if="dropdownProvinceOpen" @click.stop>
-      <view class="dropdown">
-        <view class="dropdown-search-bar">
-          <input
-            class="dropdown-search-input"
-            v-model="provinceSearchText"
-            @input="onProvinceSearch"
-            placeholder="搜索省份..."
-            placeholder-style="color: #999"
-          />
-        </view>
-        <scroll-view scroll-y="true" class="dropdown-scroll-view">
-          <view
-            class="dropdown-item"
-            v-for="(p, i) in filteredProvinceList"
-            :key="p || i"
-            :class="{
-              active: p === selectedProvince || (!selectedProvince && i === 0),
-            }"
-            @click.stop="selectProvince(p)"
-          >
-            {{ p || "全部省份" }}
-            <text
-              v-if="p === selectedProvince || (!selectedProvince && i === 0)"
-              class="check-mark"
-              >✓</text
-            >
-          </view>
-          <view v-if="filteredProvinceList.length === 0" class="dropdown-empty"
-            >暂无数据</view
-          >
-        </scroll-view>
-      </view>
-    </view>
-
     <!-- 底部按钮区 -->
     <view class="footer-btn-area">
       <button class="action-btn history-btn" @click="handleHistory">
@@ -161,6 +100,66 @@
         导出至邮箱
       </button>
     </view>
+
+    <!-- 蒙层 -->
+    <view
+      class="report-export-create-modal-mask"
+      v-if="emailModalOpen"
+      @click="closeEmailModal"
+      @touchmove.stop.prevent
+    ></view>
+
+    <!-- 邮箱导出弹窗 -->
+    <view
+      class="report-export-create-modal report-export-create-email-modal"
+      :class="{ 'report-export-create-modal--open': emailModalOpen }"
+      v-if="emailModalOpen"
+    >
+      <view class="report-export-create-modal-title">
+        <text>发送至邮箱</text>
+        <view
+          class="report-export-create-modal-close"
+          @click.stop="closeEmailModal"
+          >×</view
+        >
+      </view>
+      <view
+        class="report-export-create-modal-body"
+      >
+        <up-input
+          v-model="emailForm.email"
+          border="none"
+          :customStyle="{
+            backgroundColor: '#ebf3fb',
+            height: '80rpx',
+            paddingLeft: '20rpx',
+          }"
+          placeholder="请填写邮箱"
+          @input="emailError = false"
+        >
+          <template #suffix>
+            <view style="margin-right: 20rpx; color: #666">@999.com.cn</view>
+          </template>
+        </up-input>
+        <text v-if="emailError" class="report-export-error-text"
+          >请输入邮箱</text
+        >
+      </view>
+      <view
+        class="report-export-create-modal-footer"
+      >
+        <view
+          class="report-export-create-modal-btn cancel-btn"
+          @click.stop="closeEmailModal"
+          >取消</view
+        >
+        <view
+          class="report-export-create-modal-btn confirm-btn"
+          @click.stop="handleSendEmail"
+          >发送</view
+        >
+      </view>
+    </view>
   </view>
 </template>
 
@@ -178,49 +177,20 @@ export default {
       isRefreshing: false,
       loading: false,
       rows: [],
-      totalCount: 60, // Simulated total count
-      hasMore: true,
-      pageNum: 1,
-      pageSize: 20,
-      dropdownProvinceOpen: false,
-      provinceSearchText: "",
-      provinceList: [""],
-      selectedProvince: "",
+      totalCount: 0,
+      emailModalOpen: false,
+      emailForm: {
+        email: "",
+      },
+      emailError: false,
     };
   },
-  computed: {
-    displayRows() {
-      if (!this.selectedProvince) return this.rows;
-      return this.rows.filter(
-        (item) => item.receiverProvince === this.selectedProvince,
-      );
-    },
-    filteredProvinceList() {
-      const keyword = (this.provinceSearchText || "").trim();
-      if (!keyword) return this.provinceList;
-      return this.provinceList.filter((p) =>
-        (p || "全部省份").includes(keyword),
-      );
-    },
-  },
   created() {
     this.resetFetch();
-    this.getProviceList();
   },
   methods: {
     formatDate,
 
-    getProviceList() {
-      request("/common/getProviceList", {
-        path: "traceabilityReport/pages/ganmaoling/index.vue",
-      }).then((res) => {
-        if (res.code == 200) {
-          const _data = res.data || [];
-          this.provinceList = ["", ..._data.map((item) => item.regionName)];
-        }
-      });
-    },
-
     getLevelClass(level) {
       if (level === "VIP") return "tag-vip";
       if (level === "二级") return "tag-l2";
@@ -228,118 +198,82 @@ export default {
       return "tag-default";
     },
 
-    generateFakeData() {
-      const newRows = [];
-      const levels = ["VIP", "二级", "三级"];
-      const natures = ["协议客户", "非协议客户"];
-      const managers = ["张明华", "李建华", "王丽萍", "陈大文"];
-      const provinces =
-        this.provinceList.length > 1
-          ? this.provinceList.slice(1)
-          : ["北京市", "上海市", "广东省", "浙江省", "江苏省"];
-
-      const startIdx = (this.pageNum - 1) * this.pageSize;
-      const endIdx = Math.min(startIdx + this.pageSize, this.totalCount);
-
-      if (startIdx >= this.totalCount) {
-        this.hasMore = false;
-        return [];
-      }
-
-      for (let i = startIdx; i < endIdx; i++) {
-        newRows.push({
-          id: i,
-          receiverName: `测试收货企业${i + 1}有限公司`,
-          receiverProvince: provinces[i % provinces.length],
-          customerLevel: levels[i % levels.length],
-          customerNature: natures[i % natures.length],
-          manager: managers[i % managers.length],
-          alertCount: Math.floor(Math.random() * 10) + 1,
-        });
-      }
-      return newRows;
-    },
-
-    toggleProvinceDropdown() {
-      this.dropdownProvinceOpen = !this.dropdownProvinceOpen;
-    },
-
-    closeProvinceDropdown() {
-      this.dropdownProvinceOpen = false;
-    },
+    fetchList() {
+      if (this.loading) return;
+      this.loading = true;
 
-    selectProvince(province) {
-      this.selectedProvince = province || "";
-      this.dropdownProvinceOpen = false;
+      request(
+        `/report/ganmaoling/list`,
+        {
+          days: 1,
+          path: "traceabilityReport/pages/ganmaoling/index.vue",
+        },
+        "get",
+      ).then((res) => {
+        if (res.code == 200) {
+          this.rows = res.data || [];
+          this.totalCount = res.data?.length || 0;
+        }
+        this.loading = false;
+        this.isRefreshing = false;
+      });
     },
 
-    onProvinceSearch() {},
-
     async onRefresh() {
       this.isRefreshing = true;
-      this.pageNum = 1;
-      this.hasMore = true;
-      // Simulate network request
-      setTimeout(() => {
-        this.rows = this.generateFakeData();
-        this.isRefreshing = false;
-      }, 1000);
-    },
-
-    onLoadMore() {
-      if (this.loading || !this.hasMore) return;
-      this.loading = true;
-      this.pageNum++;
-
-      // Simulate network request
-      setTimeout(() => {
-        const more = this.generateFakeData();
-        if (more.length > 0) {
-          this.rows = [...this.rows, ...more];
-        } else {
-          this.hasMore = false;
-        }
-        this.loading = false;
-      }, 800);
+      this.fetchList();
     },
 
     resetFetch() {
-      this.loading = true;
-      this.pageNum = 1;
-      this.hasMore = true;
-      // Simulate initial load
-      setTimeout(() => {
-        this.rows = this.generateFakeData();
-        this.loading = false;
-      }, 500);
+      this.rows = [];
+      this.fetchList();
     },
 
     toDetail(item) {
       uni.navigateTo({
-        url: `/traceCodePackages/traceabilityReport/pages/ganmaoling/detail/index?id=${item.id}&name=${encodeURIComponent(item.receiverName)}`,
+        url: `/traceCodePackages/traceabilityReport/pages/ganmaoling/detail/index?name=${encodeURIComponent(item.customerName)}&updatedTime=${item.updatedTime || ""}`,
       });
     },
 
     handleExport() {
-      uni.showModal({
-        title: "导出至邮箱",
-        editable: true,
-        placeholderText: "请输入邮箱地址",
-        success: (res) => {
-          if (res.confirm) {
-            if (!res.content) {
-              uni.showToast({
-                title: "请输入邮箱",
-                icon: "none",
-              });
-              return;
-            }
-            uni.showToast({
-              title: "导出请求已发送",
-              icon: "success",
-            });
-          }
+      this.emailModalOpen = true;
+      this.emailForm.email = "";
+      this.emailError = false;
+    },
+
+    closeEmailModal() {
+      this.emailModalOpen = false;
+    },
+
+    handleSendEmail() {
+      if (!this.emailForm.email) {
+        this.emailError = true;
+        return;
+      }
+      uni.showLoading({ title: "发送中..." });
+
+      request(
+        `/report/ganmaoling/list`,
+        {
+          days: 1,
+          email: this.emailForm.email + "@999.com.cn",
+          path: "traceabilityReport/pages/ganmaoling/index.vue",
         },
+        "get",
+      ).then((res) => {
+        uni.hideLoading();
+        if (res.code == 200) {
+          uni.showToast({
+            title: "发送成功",
+            icon: "success",
+          });
+          this.closeEmailModal();
+        } else {
+          uni.showToast({
+            title: res.msg || "发送失败",
+            icon: "none",
+          });
+        }
       });
     },
 
@@ -456,108 +390,6 @@ export default {
   font-size: 20rpx;
 }
 
-/* Filter Styles */
-.filter-wrap {
-  position: relative;
-}
-
-.selector {
-  display: flex;
-  align-items: center;
-  color: #fff;
-  font-size: 26rpx;
-  font-weight: 500;
-  padding: 8rpx 20rpx;
-  background: rgba(255, 255, 255, 0.2);
-  border-radius: 30rpx;
-  border: 1rpx solid rgba(255, 255, 255, 0.3);
-}
-
-.selector-text {
-  max-width: 200rpx;
-  overflow: hidden;
-  white-space: nowrap;
-  text-overflow: ellipsis;
-}
-
-.selector-arrow {
-  margin-left: 10rpx;
-  width: 0;
-  height: 0;
-  border-left: 8rpx solid transparent;
-  border-right: 8rpx solid transparent;
-  border-top: 10rpx solid #fff;
-  transition: transform 0.3s;
-}
-
-.selector-arrow.open {
-  transform: rotate(180deg);
-}
-
-/* Dropdown Layer (Absolute on top of everything) */
-.dropdown-layer {
-  position: absolute;
-  top: 270rpx; /* Adjust based on header layout */
-  right: 40rpx;
-  z-index: 999;
-}
-
-.dropdown {
-  width: 360rpx;
-  background: #fff;
-  border-radius: 12rpx;
-  box-shadow: 0 8rpx 30rpx rgba(0, 0, 0, 0.15);
-  overflow: hidden;
-}
-
-.dropdown-search-bar {
-  padding: 16rpx;
-  border-bottom: 1rpx solid #f0f0f0;
-}
-
-.dropdown-search-input {
-  background: #f5f7fa;
-  height: 64rpx;
-  border-radius: 32rpx;
-  padding: 0 24rpx;
-  font-size: 26rpx;
-}
-
-.dropdown-scroll-view {
-  max-height: 400rpx;
-}
-
-.dropdown-item {
-  padding: 20rpx 30rpx;
-  font-size: 28rpx;
-  color: #333;
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  transition: background 0.2s;
-}
-
-.dropdown-item:active {
-  background: #f5f7fa;
-}
-
-.dropdown-item.active {
-  color: #1890ff;
-  font-weight: 500;
-  background: #e6f7ff;
-}
-
-.check-mark {
-  font-size: 24rpx;
-}
-
-.dropdown-empty {
-  padding: 40rpx;
-  text-align: center;
-  color: #999;
-  font-size: 26rpx;
-}
-
 /* List Section */
 .list-scroll {
   flex: 1;
@@ -612,8 +444,9 @@ export default {
 .company-name {
   font-size: 32rpx;
   font-weight: 600;
-  color: #333;
+  color: #096dd9;
   line-height: 1.4;
+  text-decoration: underline;
 }
 
 .level-tag {
@@ -687,46 +520,10 @@ export default {
   font-weight: 600;
 }
 
-/* Loading & Footer */
-.loading-more {
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  padding: 30rpx 0;
-}
-
-.loading-text {
-  font-size: 24rpx;
-  color: #999;
-}
-
-.loading-icon {
-  width: 32rpx;
-  height: 32rpx;
-  margin-right: 12rpx;
-  animation: spin 1s linear infinite;
-}
-
 .empty-data {
   padding-top: 120rpx;
 }
 
-.no-more {
-  text-align: center;
-  color: #ccc;
-  font-size: 24rpx;
-  padding: 30rpx 0;
-}
-
-@keyframes spin {
-  from {
-    transform: rotate(0deg);
-  }
-  to {
-    transform: rotate(360deg);
-  }
-}
-
 .footer-btn-area {
   padding: 24rpx 32rpx calc(24rpx + env(safe-area-inset-bottom));
   background: #fff;
@@ -772,4 +569,101 @@ export default {
   color: #fff;
   box-shadow: 0 6rpx 16rpx rgba(24, 144, 255, 0.35);
 }
+
+/* 弹窗样式 */
+.report-export-create-modal-mask {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.6);
+  z-index: 999;
+  backdrop-filter: blur(2px);
+}
+
+.report-export-create-modal {
+  position: fixed;
+  z-index: 1000;
+  background: #fff;
+  transition: all 0.3s ease;
+}
+
+.report-export-create-email-modal {
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  width: 620rpx;
+  border-radius: 24rpx;
+  opacity: 0;
+  visibility: hidden;
+  overflow: hidden;
+}
+
+.report-export-create-email-modal.report-export-create-modal--open {
+  opacity: 1;
+  visibility: visible;
+}
+
+.report-export-create-modal-title {
+  padding: 36rpx 32rpx;
+  font-size: 36rpx;
+  color: #333;
+  text-align: center;
+  font-weight: 600;
+  border-bottom: 1rpx solid #f0f0f0;
+  position: relative;
+}
+
+.report-export-create-modal-close {
+  position: absolute;
+  right: 32rpx;
+  top: 50%;
+  transform: translateY(-50%);
+  font-size: 44rpx;
+  color: #999;
+  line-height: 1;
+  padding: 10rpx;
+}
+
+.report-export-create-modal-body {
+  padding: 48rpx 40rpx 32rpx;
+}
+
+.report-export-error-text {
+  font-size: 24rpx;
+  color: #ff4d4f;
+  margin-top: 12rpx;
+  display: block;
+}
+
+.report-export-create-modal-footer {
+  padding: 0 40rpx 48rpx;
+  display: flex;
+  justify-content: space-between;
+  gap: 24rpx;
+}
+
+.report-export-create-modal-btn {
+  height: 88rpx;
+  line-height: 88rpx;
+  border-radius: 44rpx;
+  text-align: center;
+  font-size: 30rpx;
+  font-weight: 600;
+  flex: 1;
+  margin: 0;
+}
+
+.cancel-btn {
+  background: #f5f7fa;
+  color: #666;
+  border: 1rpx solid #e4e7ed;
+}
+
+.confirm-btn {
+  background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
+  color: #fff;
+  box-shadow: 0 6rpx 16rpx rgba(24, 144, 255, 0.3);
+}
 </style>

+ 25 - 44
traceCodePackages/traceabilityReport/pages/index.vue

@@ -3,59 +3,29 @@
   <view v-if="hasFunction" class="page" @click.stop="closeDropdowns">
     <view class="tabs-container">
       <view class="tabs">
-        <view
-          v-if="hasScanFunction"
-          class="tab"
-          :class="{ active: activeTab === 'scan' }"
-          @click="activeTab = 'scan'"
-        >
-          扫码率</view
-        >
-        <view
-          v-if="hasReportFunction"
-          class="tab"
-          :class="{ active: activeTab === 'export' }"
-          @click="activeTab = 'export'"
-          >报表导出</view
-        >
-        <view
-          v-if="hasBlacklistFunction"
-          class="tab"
-          :class="{ active: activeTab === 'blacklist' }"
-          @click="activeTab = 'blacklist'"
-          >黑名单汇总</view
-        >
-        <view
-          v-if="hasGanmaolingFunction"
-          class="tab"
-          :class="{ active: activeTab === 'ganmaoling' }"
-          @click="activeTab = 'ganmaoling'"
-          >感冒灵大批量</view
-        >
+        <view v-if="hasScanFunction" class="tab" :class="{ active: activeTab === 'scan' }" @click="activeTab = 'scan'">
+          扫码率</view>
+        <view v-if="hasReportFunction" class="tab" :class="{ active: activeTab === 'export' }"
+          @click="activeTab = 'export'">报表导出</view>
+        <view v-if="hasBlacklistFunction" class="tab" :class="{ active: activeTab === 'blacklist' }"
+          @click="activeTab = 'blacklist'">黑名单汇总</view>
+        <view v-if="hasGanmaolingFunction" class="tab" :class="{ active: activeTab === 'ganmaoling' }"
+          @click="activeTab = 'ganmaoling'">感冒灵大批量</view>
       </view>
     </view>
     <view class="content" v-if="activeTab === 'scan' && hasScanFunction">
       <ScanningRate ref="scanRate" />
     </view>
-    <view
-      class="content"
-      :style="{ padding: '0', marginTop: '85rpx' }"
-      v-if="activeTab === 'export' && hasReportFunction"
-    >
+    <view class="content" :style="{ padding: '0', marginTop: '85rpx' }"
+      v-if="activeTab === 'export' && hasReportFunction">
       <ReportExport />
     </view>
-    <view
-      class="content"
-      :style="{ padding: '0', marginTop: '85rpx' }"
-      v-if="activeTab === 'blacklist' && hasBlacklistFunction"
-    >
+    <view class="content" :style="{ padding: '0', marginTop: '85rpx' }"
+      v-if="activeTab === 'blacklist' && hasBlacklistFunction">
       <Blacklist />
     </view>
-    <view
-      class="content"
-      :style="{ padding: '0', marginTop: '85rpx' }"
-      v-if="activeTab === 'ganmaoling' && hasGanmaolingFunction"
-    >
+    <view class="content" :style="{ padding: '0', marginTop: '85rpx' }"
+      v-if="activeTab === 'ganmaoling' && hasGanmaolingFunction">
       <Ganmaoling />
     </view>
   </view>
@@ -73,6 +43,7 @@ import Ganmaoling from "./ganmaoling/index.vue";
 import Empty from "../../wigets/empty.vue";
 import Water from "@/components/water/water.vue";
 import { hasFunction } from "../../utils/utils.js";
+import request from "../../request/index.js";
 export default {
   components: {
     ScanningRate,
@@ -106,6 +77,7 @@ export default {
   },
   created() {
     this.getFunc();
+    this.getUserEmail()
   },
   computed: {
     hasFunction() {
@@ -118,6 +90,15 @@ export default {
     },
   },
   methods: {
+    async getUserEmail() {
+      const resp = await request("/report/getEmailAddress", {
+        accessToken: uni.getStorageSync("accessToken"),
+      });
+      if (resp.code == 200) {
+        const mail = resp.data.split("@")[0];
+        uni.setStorageSync('traceCode_useremail', mail)
+      }
+    },
     closeDropdowns() {
       if (!this.$refs.scanRate) return;
       this.$refs.scanRate.closeDropdown();

+ 2 - 10
traceCodePackages/traceabilityReport/pages/reportExport/index.vue

@@ -873,12 +873,13 @@ export default {
     const userInfo = uni.getStorageSync("userInfo");
     // const staffEmail = userInfo?.ldap || "";
     // this.emailForm.email = staffEmail.split("@")[0];
+    const userEmail = uni.getStorageSync('traceCode_useremail')
+    this.emailForm.email = userEmail
     console.log("reportExport\index.vue,storage,userInfo", userInfo);
     this.initList();
     this.getProviceList();
     this.getDrugInfoName();
     this.fetchList();
-    this.getUserEmail();
     try {
       uni.onKeyboardHeightChange(({ height }) => {
         this.dropdownUpward = height > 0;
@@ -932,15 +933,6 @@ export default {
     },
   },
   methods: {
-    async getUserEmail() {
-      const resp = await request("/report/getEmailAddress", {
-        accessToken: uni.getStorageSync("accessToken"),
-      });
-      if (resp.code == 200) {
-        const mail = resp.data.split("@")[0];
-        this.emailForm.email = mail || "";
-      }
-    },
     getCustomerName(data = {}) {
       const { customerName = "", otherCustomer = "" } = data;
       let name = customerName || "";