已修改8个文件
已添加9个文件
3972 ■■■■■ 文件已修改
src/api/cooperativeOffice/collaborativeApproval.js 111 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/salesManagement/salesLedger.js 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/user.js 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/config.js 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main.js 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages.json 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/cooperativeOffice/clientVisit/index.vue 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/cooperativeOffice/collaborativeApproval/detail.vue 535 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/cooperativeOffice/collaborativeApproval/index.vue 390 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/index.vue 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/sales/invoicingRegistration/add.vue 537 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/sales/invoicingRegistration/index.vue 448 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/sales/invoicingRegistration/view.vue 325 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/sales/salesAccount/detail.vue 999 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/sales/salesAccount/index.vue 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/sales/salesAccount/view.vue 315 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/summarizeTable.js 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/cooperativeOffice/collaborativeApproval.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,111 @@
// é”€å”®å°è´¦é¡µé¢æŽ¥å£
import request from "@/utils/request";
// åˆ†é¡µæŸ¥è¯¢
export function ledgerList(query) {
  return request({
    url: "/sales/ledger/list",
    method: "get",
    params: query,
  });
}
// å­è¡¨æ ¼æŸ¥è¯¢
export function productList(query) {
  return request({
    url: "/sales/product/list",
    method: "get",
    params: query,
  });
}
// æŸ¥è¯¢å®¢æˆ·åç§°åˆ—表
export function customerList(query) {
  return request({
    url: "/basic/customer/customerList",
    method: "get",
    params: query,
  });
}
// æ–°å¢žã€ä¿®æ”¹é”€å”®å°è´¦
export function addOrUpdateSalesLedger(query) {
  return request({
    url: "/sales/ledger/addOrUpdateSalesLedger",
    method: "post",
    data: query,
  });
}
// åˆ é™¤é”€å”®å°è´¦
export function delLedger(query) {
  return request({
    url: "/sales/ledger/delLedger",
    method: "delete",
    data: query,
  });
}
// æŸ¥è¯¢é”€å”®å°è´¦è¯¦æƒ…
export function getSalesLedgerWithProducts(query) {
  return request({
    url: "/sales/ledger/getSalesLedgerWithProducts",
    method: "get",
    params: query,
  });
}
// å®žæ—¶ä¿®æ”¹äº§å“ä¿¡æ¯
export function addOrUpdateSalesLedgerProduct(query) {
  return request({
    url: "/sales/product/addOrUpdateSalesLedgerProduct",
    method: "post",
    data: query,
  });
}
// åˆ é™¤äº§å“
export function delProduct(query) {
  return request({
    url: "/sales/product/delProduct",
    method: "delete",
    data: query,
  });
}
// ä¸Šä¼ é™„ä»¶
export function upload(query) {
  return request({
    url: "/file/upload",
    method: "post",
    data: query,
    responseType: "blob",
  });
}
// ç¼–辑时删除附件
export function delLedgerFile(query) {
  return request({
    url: "/sales/ledger/delLedgerFile",
    method: "delete",
    data: query,
  });
}
// é”€å”®ä¸åˆ†é¡µæŸ¥è¯¢
export function ledgerListNoPage(query) {
  return request({
    url: "/sales/ledger/listNoPage",
    method: "get",
    params: query,
  });
}
// åˆ†é¡µæŸ¥è¯¢
export function ledgerListPage(query) {
  return request({
    url: "/sales/ledger/listPage",
    method: "get",
    params: query,
  });
}
// æ ¹æ®é”€å”®åˆåŒå·æŸ¥äº§å“ä¿¡æ¯
export function getProductInfoByContractNo(query) {
  return request({
    url: "/purchase/ledger/getProductBySalesNo",
    method: "get",
    params: query,
  });
}
src/api/salesManagement/salesLedger.js
@@ -109,3 +109,19 @@
    params: query,
  });
}
// äº§å“æ ‘查询
export function productTreeList(query) {
  return request({
    url: '/basic/product/list',
    method: 'get',
    params: query
  })
}
// è§„格型号查询
export function modelList(query) {
  return request({
    url: '/basic/product/modelList',
    method: 'get',
    params: query
  })
}
src/api/system/user.js
@@ -39,3 +39,10 @@
    filePath: data.filePath
  })
}
// æŸ¥è¯¢ç”¨æˆ·åˆ—表
export function userListNoPage() {
  return request({
    url: '/system/user/userListNoPage',
    method: 'get'
  })
}
src/config.js
@@ -1,8 +1,7 @@
// åº”用全局配置
const config = {
  //  baseUrl: 'https://vue.ruoyi.vip/prod-api',
  // baseUrl: 'http://localhost/prod-api',
  baseUrl: 'http://114.132.189.42:8089',
  baseUrl: 'http://114.132.189.42:8089', // æµ‹è¯•库
  // baseUrl: 'http://192.168.1.147:7003', // æœ¬åœ°è”è°ƒ
   //cloud后台网关地址
  //  baseUrl: 'http://192.168.10.3:8080',
   // åº”用信息
src/main.js
@@ -11,6 +11,9 @@
import { useDict } from '@/utils/dict'
import { parseTime, resetForm, addDateRange, handleTree, selectDictLabel, selectDictLabels } from '@/utils/ruoyi'
import {
  calculateTaxExclusiveTotalPrice,
} from "@/utils/summarizeTable.js";
@@ -34,6 +37,7 @@
  app.config.globalProperties.addDateRange = addDateRange
  app.config.globalProperties.selectDictLabel = selectDictLabel
  app.config.globalProperties.selectDictLabels = selectDictLabels
  app.config.globalProperties.calculateTaxExclusiveTotalPrice = calculateTaxExclusiveTotalPrice;
  return {
    app
src/pages.json
@@ -59,7 +59,35 @@
    {
      "path": "pages/sales/salesAccount/detail",
      "style": {
        "navigationBarTitleText": "修改台账",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/sales/salesAccount/view",
      "style": {
        "navigationBarTitleText": "台账详情",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/sales/invoicingRegistration/index",
      "style": {
        "navigationBarTitleText": "开票登记",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/sales/invoicingRegistration/add",
      "style": {
        "navigationBarTitleText": "新增开票登记",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/sales/invoicingRegistration/view",
      "style": {
        "navigationBarTitleText": "开票登记详情",
        "navigationStyle": "custom"
      }
    },
@@ -74,6 +102,27 @@
      "style": {
        "navigationBarTitleText": "浏览文本"
      }
    },
    {
      "path": "pages/cooperativeOffice/collaborativeApproval/index",
      "style": {
        "navigationBarTitleText": "审批管理",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/cooperativeOffice/collaborativeApproval/detail",
      "style": {
        "navigationBarTitleText": "审批流程",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/cooperativeOffice/clientVisit/index",
      "style": {
        "navigationBarTitleText": "客户拜访",
        "navigationStyle": "custom"
      }
    }
  ],
  "subPackages": [
src/pages/cooperativeOffice/clientVisit/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,51 @@
// å®¢æˆ·æ‹œè®¿
<template>
  <view>
    <view class="page-header">
      <view class="header-left">
        <up-icon name="arrow-left" size="20" color="#333" @click="goBack"></up-icon>
      </view>
      <view class="header-center">
        <text class="page-title">客户拜访</text>
      </view>
    </view>
  </view>
</template>
<script>
export default {
  data() {
    return {
      title: '客户拜访'
    }
  },
  methods: {
    goBack() {
      uni.navigateBack({
        delta: 1
      })
    }
  }
}
</script>
<style>
.page-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 20px;
  background-color: #f5f5f5;
}
.header-left {
  display: flex;
  align-items: center;
}
.header-center {
  flex: 1;
  text-align: center;
}
.page-title {
  font-size: 18px;
  font-weight: bold;
}
</style>
src/pages/cooperativeOffice/collaborativeApproval/detail.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,535 @@
<template>
  <view class="account-detail">
    <!-- é¡¶éƒ¨æ ‡é¢˜æ  -->
    <view class="header">
      <up-icon name="arrow-left" size="20" color="#333" @click="goBack" />
      <text class="title">审批流程</text>
    </view>
    <!-- è¡¨å•区域 -->
    <view class="form-section">
      <van-form ref="formRef" @submit="submitForm" :rules="rules" input-align="right">
        <van-cell-group inset style="height:auto">
          <van-field
            v-model="taxPrice"
            name="taxPrice"
            label="姓名"
            placeholder="请输入姓名"
            :rules="[{ required: true, message: '姓名不能为空' }]"
            required
            readonly
          />
          <van-field
            v-model="result"
            readonly
            name="picker"
            label="申请部门"
            placeholder="请选择申请部门"
            :rules="[{ required: true, message: '请选择申请部门' }]"
            @click="showPicker = true"
            required
          />
          <van-popup
            v-model:show="showPicker"
            destroy-on-close
            position="bottom"
          >
            <van-picker
              :columns="columns"
              :model-value="pickerValue"
              @confirm="onConfirm"
              @cancel="showPicker = false"
            />
          </van-popup>
          <van-field
            v-model="message"
            name="message"
            rows="1"
            autosize
            label="申请事由"
            type="textarea"
            placeholder="请输入申请事由"
            height="100"
            :rules="[{ required: true, message: '申请事由不能为空' }]"
            required
          />
        </van-cell-group>
      </van-form>
    </view>
    <!-- å®¡æ ¸æµç¨‹åŒºåŸŸ -->
    <view class="approval-process">
      <view class="approval-header">
        <text class="approval-title">审核流程</text>
        <text class="approval-desc">已由管理员预设不可修改</text>
      </view>
      <view class="approval-steps">
        <view v-for="(step, stepIndex) in approvalSteps" :key="stepIndex" class="approval-step">
          <view class="step-title">
            <text>审批人</text>
          </view>
          <view class="approvers-container">
            <view v-for="(approver, approverIndex) in step.approvers" :key="approverIndex" class="approver-item">
              <view class="approver-avatar"></view>
              <text class="approver-name">{{ approver.name }}</text>
              <view class="delete-approver-btn" @click="removeApprover(stepIndex, approverIndex)">×</view>
            </view>
            <view class="add-approver-btn" @click="addApprover(stepIndex)">+
            </view>
          </view>
          <view class="step-line" v-if="stepIndex < approvalSteps.length - 1"></view>
          <view class="delete-step-btn" @click="removeApprovalStep(stepIndex)">删除节点</view>
        </view>
      </view>
      <view class="add-step-btn" @click="addApprovalStep">
        <text>新增节点审核人</text>
      </view>
    </view>
    <!-- åº•部按钮 -->
    <view class="footer-btns">
      <van-button class="cancel-btn" @click="goBack">取消</van-button>
      <van-button class="save-btn" @click="submitForm">保存</van-button>
    </view>
  </view>
</template>
<script>
import { ref, onMounted } from "vue";
export default {
  setup() {
    const rules = ref({
  taxPrice: {
    rules: [{ required: true, errorMessage: '姓名不能为空' }]
  },
  result: {
    rules: [{ required: true, errorMessage: '请选择申请部门' }]
  },
  message: {
    rules: [{ required: true, errorMessage: '申请事由不能为空' }]
  },
});
    const result = ref("");
    const pickerValue = ref([]);
    const showPicker = ref(false);
    const columns = ref([]);
    onMounted(async () => {
      try {
        // æ›¿æ¢ä¸ºå®žé™…接口地址
        // const response = await axios.get('/api/getDepartments');
        columns.value = [
          {
            text: "杭州",
            value: "Hangzhou",
          },
          {
            text: "宁波",
            value: "Ningbo",
          },
          {
            text: "温州",
            value: "Wenzhou",
          },
          {
            text: "绍兴",
            value: "Shaoxing",
          },
          {
            text: "湖州",
            value: "Huzhou",
          },
        ];
      } catch (error) {
        console.error("获取部门数据失败:", error);
      }
    });
    const onConfirm = ({ selectedValues, selectedOptions }) => {
      result.value = selectedOptions[0]?.text;
      pickerValue.value = selectedValues;
      showPicker.value = false;
    };
    const taxPrice = ref("");
    const contractAmount = ref("");
    const approvalSteps = ref([
      { approvers: [{ name: '卢小敏' }, { name: '卢小敏' }] },
      { approvers: [{ name: '卢小敏' }] },
      { approvers: [{ name: '卢小敏' }] },
      { approvers: [{ name: '卢小敏' }] }
    ]);
    const goBack = () => {
        uni.navigateBack();
    };
    const formRef = ref(null);
    const submitForm = () => {
      formRef.value.validate().then(() => {
        // è¡¨å•校验通过,可以提交数据
        console.log("表单数据:", {
          taxPrice: taxPrice.value,
          department: result.value,
          message: message.value,
          approvalSteps: approvalSteps.value
        });
        uni.showToast({
          title: "保存成功",
          icon: "success",
        });
      }).catch((error) => {
        console.error("表单校验失败:", error);
        // æ˜¾ç¤ºå…·ä½“的错误信息
        if (error.length > 0) {
          const firstError = error[0];
          uni.showToast({
            title: firstError.message || '表单校验失败',
            icon: 'none'
          });
        } else {
          uni.showToast({
            title: '表单校验失败,请检查必填项',
            icon: 'none'
          });
        }
      });
    };
    const message = ref("");
    const addApprover = (stepIndex) => {
      // åœ¨æŒ‡å®šå®¡æ‰¹æ­¥éª¤æ·»åŠ æ–°çš„å®¡æ‰¹äºº
      approvalSteps.value[stepIndex].approvers.push({ name: '卢小敏' });
    };
    const addApprovalStep = () => {
      // æ·»åŠ æ–°çš„å®¡æ‰¹æ­¥éª¤
      approvalSteps.value.push({ approvers: [{ name: '卢小敏' }] });
    };
    const removeApprover = (stepIndex, approverIndex) => {
      // ç¡®ä¿æ¯ä¸ªæ­¥éª¤è‡³å°‘保留一个审批人
      if (approvalSteps.value[stepIndex].approvers.length > 1) {
        approvalSteps.value[stepIndex].approvers.splice(approverIndex, 1);
      } else {
        uni.showToast({
          title: '每个步骤至少需要一个审批人',
          icon: 'none'
        });
      }
    };
    const removeApprovalStep = (stepIndex) => {
      // ç¡®ä¿è‡³å°‘保留一个审批步骤
      if (approvalSteps.value.length > 1) {
        approvalSteps.value.splice(stepIndex, 1);
      } else {
        uni.showToast({
          title: '至少需要一个审批步骤',
          icon: 'none'
        });
      }
    };
    return {
      rules,
      removeApprovalStep,
    removeApprover,
      result,
      pickerValue,
      columns,
      onConfirm,
      showPicker,
      taxPrice,
      contractAmount,
      goBack,
      submitForm,
      approvalSteps,
      addApprover,
      addApprovalStep,
      formRef,
      message
    };
  },
};
</script>
<style scoped lang="scss">
.account-detail {
  min-height: 100vh;
  background: #f8f9fa;
  padding-bottom: 80px;
}
.header {
  display: flex;
  align-items: center;
  background: #fff;
  padding: 16px 20px;
  border-bottom: 1px solid #f0f0f0;
  position: sticky;
  top: 0;
  z-index: 100;
}
.title {
  flex: 1;
  text-align: center;
  font-size: 18px;
  font-weight: 600;
  color: #333;
}
.form-section {
  margin-top: 16px;
}
.van-field {
  height: 56px;
  line-height: 36px;
}
.product-section {
  background: #fff;
  margin: 16px;
  border-radius: 16px;
  padding: 20px 16px 8px 16px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.section-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 12px;
}
.section-title {
  font-size: 16px;
  font-weight: 600;
  color: #333;
}
.add-btn {
  background: #2979ff;
  color: #fff;
  border-radius: 8px;
  padding: 4px 16px;
  font-size: 14px;
}
.product-card {
  background: #f8f9fa;
  border-radius: 12px;
  padding: 12px;
  margin-bottom: 16px;
  box-shadow: 0 1px 4px rgba(41, 121, 255, 0.06);
  position: relative;
}
.product-row {
  display: flex;
  align-items: center;
  margin-bottom: 8px;
}
.product-label {
  min-width: 60px;
  color: #888;
  font-size: 13px;
}
.del-row {
  justify-content: flex-end;
}
.del-btn {
  background: #ff4d4f;
  color: #fff;
  border-radius: 8px;
  padding: 4px 16px;
  font-size: 13px;
  margin-top: 4px;
}
.approval-process {
  background: #fff;
  margin: 16px;
  border-radius: 16px;
  padding: 16px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.approval-header {
  margin-bottom: 16px;
}
.approval-title {
  font-size: 16px;
  font-weight: 600;
  color: #333;
  display: block;
  margin-bottom: 4px;
}
.approval-desc {
  font-size: 12px;
  color: #999;
}
.approval-steps {
  padding-left: 16px;
  position: relative;
}
.approval-step {
  position: relative;
  margin-bottom: 20px;
}
.step-title {
  margin-bottom: 12px;
}
.step-title text {
  font-size: 14px;
  color: #666;
  background: #f0f0f0;
  padding: 2px 8px;
  border-radius: 4px;
}
.approvers-container {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin-bottom: 8px;
}
.approver-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 60px;
}
.approver-avatar {
  width: 40px;
  height: 40px;
  background: #e6f7ff;
  border-radius: 50%;
  margin-bottom: 4px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.approver-avatar::after {
  content: '👤';
  font-size: 20px;
}
.approver-name {
  font-size: 12px;
  color: #333;
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin-bottom: 2px;
}
.delete-approver-btn {
  font-size: 12px;
  color: #ff4d4f;
  background: rgba(255, 77, 79, 0.1);
  width: 16px;
  height: 16px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-top: 2px;
}
.delete-step-btn {
  margin-top: 8px;
  color: #ff4d4f;
  font-size: 12px;
  background: rgba(255, 77, 79, 0.1);
  padding: 2px 8px;
  border-radius: 4px;
  display: inline-block;
  }
.add-approver-btn {
  width: 40px;
  height: 40px;
  border: 1px dashed #ccc;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 20px;
  color: #999;
  margin-top: 8px;
}
.step-line {
  position: absolute;
  left: 20px;
  top: 100%;
  width: 1px;
  height: 30px;
  background: #e0e0e0;
}
.add-step-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  margin-top: 16px;
  color: #006cfb;
  font-size: 14px;
  padding: 8px 0;
  border: 1px dashed #006cfb;
  border-radius: 8px;
}
.footer-btns {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  background: #fff;
  display: flex;
  justify-content: space-around;
  align-items: center;
  padding: 12px 0;
  box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);
  z-index: 1000;
}
.cancel-btn {
  font-weight: 400;
  font-size: 16px;
  color: #ffffff;
  width: 102px;
  background: #c7c9cc;
  box-shadow: 0px 4px 10px 0px rgba(3, 88, 185, 0.2);
  border-radius: 40px 40px 40px 40px;
}
.save-btn {
  font-weight: 400;
  font-size: 16px;
  color: #ffffff;
  width: 224px;
  background: linear-gradient(140deg, #00baff 0%, #006cfb 100%);
  box-shadow: 0px 4px 10px 0px rgba(3, 88, 185, 0.2);
  border-radius: 40px 40px 40px 40px;
}
</style>
src/pages/cooperativeOffice/collaborativeApproval/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,390 @@
// å®¡æ‰¹ç®¡ç†ä¸»é¡µé¢
<template>
    <view class="sales-account">
        <!-- é¡µé¢å¤´éƒ¨ -->
        <view class="page-header">
            <view class="header-left">
                <up-icon name="arrow-left" size="20" color="#333" @click="goBack"></up-icon>
            </view>
            <view class="header-center">
                <text class="page-title">审批管理</text>
            </view>
        </view>
        <!-- æœç´¢å’Œç­›é€‰åŒºåŸŸ -->
        <view class="search-filter-section">
            <view class="search-bar">
                <view class="search-input">
                    <u-input placeholder="请输入采购合同号" class="search-text" v-model="searchKeyword">
                        <template #suffix>
                            <up-icon name="search" size="24" color="#999" @click="getList"></up-icon>
                        </template>
                    </u-input>
                </view>
                <view class="filter-button" @click="showFilterOptions">
                    <van-icon name="filter-o" size="24" color="#999"></van-icon>
                </view>
            </view>
        </view>
        <!-- é”€å”®å°è´¦ç€‘布流 -->
        <view class="ledger-list" v-if="total > 0">
            <view v-for="(item, index) in ledgerList" :key="index">
                <view class="ledger-item" @click="handleItemClick(item)">
                    <view class="item-header">
                        <view class="item-left">
                            <view class="document-icon">
                                <up-icon name="file-text" size="16" color="#ffffff"></up-icon>
                            </view>
                            <text class="item-id">{{ item.salesContractNo }}</text>
                        </view>
                    </view>
                    <up-divider></up-divider>
                    <view class="item-details">
                        <view class="detail-info">
                            <view class="detail-row">
                                <text class="detail-label">申请人</text>
                                <text class="detail-value">{{ item.entryPersonName }}</text>
                            </view>
                            <view class="detail-row">
                                <text class="detail-label">申请日期</text>
                                <text class="detail-value highlightBlue">{{ item.entryDate }}</text>
                            </view>
                        </view>
                        <view class="detail-info">
                            <view class="detail-row">
                                <text class="detail-label">申请部门</text>
                                <text class="detail-value">{{ item.entryPersonName }}</text>
                            </view>
                            <view class="detail-row">
                                <text class="detail-label">审批状态</text>
                                <text class="detail-value highlightYellow">{{ item.entryDate }}</text>
                            </view>
                        </view>
                    </view>
                </view>
            </view>
        </view>
        <view v-else class="no-data">
            <text>暂无审批数据</text>
        </view>
        <!-- æµ®åŠ¨æ“ä½œæŒ‰é’® -->
        <view class="fab-button" @click="handleAdd">
            <up-icon name="plus" size="24" color="#ffffff"></up-icon>
        </view>
    </view>
</template>
<script setup>
    import {
        ref,
        reactive,
        onMounted
    } from "vue";
    import {
        ledgerListPage
    } from "@/api/cooperativeOffice/collaborativeApproval";
    // æœç´¢å…³é”®è¯
    const searchKeyword = ref("");
    // é”€å”®å°è´¦æ•°æ®
    const ledgerList = ref([]);
    const total = ref(0);
    // è¿”回上一页
    const goBack = () => {
        uni.navigateBack();
    };
    // æŸ¥è¯¢åˆ—表
    const getList = () => {
        const page = {
            current: -1,
            size: -1,
        };
        ledgerListPage({
                ...page
            })
            .then((res) => {
                ledgerList.value = res.records;
                total.value = res.total;
            })
            .catch(() => {
                // tableLoading.value = false;
            });
    };
    // æ˜¾ç¤ºç­›é€‰é€‰é¡¹
    const showFilterOptions = () => {
        uni.showActionSheet({
            itemList: ["按日期筛选", "按状态筛选", "按金额筛选"],
            success: (res) => {
                console.log("选择了筛选选项:", res.tapIndex);
            },
        });
    };
    // ç‚¹å‡»åˆ—表项
    const handleItemClick = (item) => {
        uni.showToast({
            title: `查看合同: ${item.contractId}`,
            icon: "none",
        });
    };
    // æ·»åŠ æ–°è®°å½•
    const handleAdd = () => {
        uni.navigateTo({
            url: "/pages/cooperativeOffice/collaborativeApproval/detail",
        });
    };
    onMounted(() => {
        // é¡µé¢åŠ è½½å®ŒæˆåŽçš„åˆå§‹åŒ–é€»è¾‘
        getList();
    });
</script>
<style scoped lang="scss">
    .u-divider {
        margin: 0 !important;
    }
    .sales-account {
        min-height: 100vh;
        background: #f8f9fa;
        position: relative;
    }
    .page-header {
        background: #ffffff;
        padding: 16px 20px;
        display: flex;
        align-items: center;
        justify-content: space-between;
        border-bottom: 1px solid #f0f0f0;
        position: sticky;
        /* å…¼å®¹ iOS åˆ˜æµ·/灵动岛安全区 */
        padding-top: env(safe-area-inset-top);
        top: 0;
        z-index: 100;
    }
    .header-left {
        display: flex;
        align-items: center;
        gap: 8px;
    }
    .nav-icon {
        width: 24px;
        height: 24px;
        background: #2979ff;
        border-radius: 4px;
        display: flex;
        align-items: center;
        justify-content: center;
    }
    .nav-text {
        font-size: 14px;
        color: #2979ff;
        font-weight: 500;
    }
    .header-center {
        flex: 1;
        text-align: center;
    }
    .page-title {
        font-size: 18px;
        font-weight: 600;
        color: #333;
    }
    .header-right {
        display: flex;
        align-items: center;
    }
    .status-bar {
        display: flex;
        align-items: center;
        gap: 4px;
    }
    .signal,
    .wifi,
    .battery {
        width: 16px;
        height: 8px;
        background: #333;
        border-radius: 2px;
    }
    .search-filter-section {
        padding: 10px 20px;
        background: #ffffff;
    }
    .search-bar {
        display: flex;
        align-items: center;
        gap: 12px;
    }
    .search-input {
        flex: 1;
        background: #f5f5f5;
        border-radius: 24px;
        padding: 4px 16px;
        display: flex;
        align-items: center;
        gap: 8px;
    }
    .search-text {
        flex: 1;
        font-size: 14px;
        color: #333;
        background: transparent;
        border: none;
        outline: none;
    }
    .search-text::placeholder {
        color: #999;
    }
    .filter-button {
        width: 40px;
        height: 40px;
        border-radius: 8px;
        display: flex;
        align-items: center;
        justify-content: center;
    }
    .ledger-list {
        padding: 20px;
    }
    .ledger-item {
        background: #ffffff;
        border-radius: 12px;
        margin-bottom: 16px;
        overflow: hidden;
        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
        padding: 0 16px;
    }
    .item-header {
        padding: 16px 0;
        display: flex;
        align-items: center;
        justify-content: space-between;
    }
    .item-left {
        display: flex;
        align-items: center;
        gap: 8px;
    }
    .document-icon {
        width: 24px;
        height: 24px;
        background: #ed8d05;
        border-radius: 4px;
        display: flex;
        align-items: center;
        justify-content: center;
    }
    .item-id {
        font-size: 14px;
        color: #333;
        font-weight: 500;
    }
    .item-tag {
        background: #4caf50;
        border-radius: 4px;
        padding: 2px 4px;
    }
    .tag-text {
        font-size: 11px;
        color: #ffffff;
        font-weight: 500;
    }
    .item-details {
        padding: 16px 0;
    }
    .detail-row {
        display: flex;
        align-items: flex-end;
        justify-content: space-between;
        margin-bottom: 8px;
        &:last-child {
            margin-bottom: 0;
        }
    }
    .detail-info {
        margin-top: 10px;
        display: flex;
        align-items: flex-start;
        justify-content: space-between;
    }
    .detail-label {
        font-size: 12px;
        color: #777777;
        min-width: 60px;
    }
    .detail-value {
        font-size: 12px;
        color: #000000;
        text-align: right;
        flex: 1;
        margin-left: 16px;
    }
    .detail-value.highlightBlue {
        color: #2979ff;
        font-weight: 500;
    }
    .detail-value.highlightYellow {
        color: #ed8d05;
        font-weight: 500;
    }
    .no-data {
        padding: 40px 0;
        text-align: center;
        color: #999;
    }
    .fab-button {
        position: fixed;
        bottom: 30px;
        right: 30px;
        width: 56px;
        height: 56px;
        background: #ed8d05;
        border-radius: 50%;
        display: flex;
        align-items: center;
        justify-content: center;
        box-shadow: 0 4px 16px rgba(41, 121, 255, 0.3);
        z-index: 1000;
    }
</style>
src/pages/index.vue
@@ -20,7 +20,7 @@
            <view class="notice">
                <view class="notice-content">
                    <view class="notice-left">
                        <text class="notice-status">📊 å®žæ—¶ç›‘控</text>
                        <text class="notice-status">通知</text>
                    </view>
                    <view class="notice-separator"></view>
                    <view class="notice-right">
@@ -291,6 +291,21 @@
        case '销售台账':
            uni.navigateTo({
                url: '/pages/sales/salesAccount/index'
            });
            break;
        case '开票登记':
            uni.navigateTo({
                url: '/pages/sales/invoicingRegistration/index'
            });
            break;
        case '协同审批':
            uni.navigateTo({
                url: '/pages/cooperativeOffice/collaborativeApproval/index'
            });
            break;
                    case '客户拜访':
            uni.navigateTo({
                url: '/pages/cooperativeOffice/clientVisit/index'
            });
            break;
        default:
@@ -588,7 +603,7 @@
}
.notice-label {
    color: #1976d2;
    color: #333;
    font-size: 14px;
    font-weight: 500;
    margin-right: 12px;
src/pages/sales/invoicingRegistration/add.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,537 @@
<template>
  <view class="account-detail">
    <!-- é¡µé¢å¤´éƒ¨ -->
    <van-nav-bar
      title="新增开票登记"
      left-text="返回"
      left-arrow
      @click-left="goBack"
      fixed
      placeholder
    />
    <!-- è¡¨å•内容 -->
    <van-form @submit="submitForm" ref="formRef" label-width="110px" input-align="right" error-message-align="right" scroll-to-error scroll-to-error-position="center">
      <!-- åŸºæœ¬ä¿¡æ¯ -->
      <van-cell-group title="基本信息" inset>
        <van-field
          v-model="form.salesContractNo"
          label="销售合同号"
          readonly
          placeholder="自动填充"
        />
        <van-field
          v-model="form.customerName"
          label="客户名称"
          readonly
          placeholder="自动填充"
        />
        <van-field
          v-model="form.salesman"
          label="业务员"
          readonly
          placeholder="自动填充"
        />
        <van-field
          v-model="form.projectName"
          label="项目名称"
          readonly
          placeholder="自动填充"
        />
        <van-field
          v-model="form.createUer"
          label="录入人"
                    readonly
          placeholder="请输入录入人"
        />
                <van-field
                    v-model="form.createTime"
                    label="录入日期"
                    readonly
                    placeholder="请选择录入日期"
                    @click="showCreateTimePicker = true"
                />
                <van-field
                    v-model="form.invoiceNo"
                    label="发票号码"
                    required
                    placeholder="请输入发票号码"
                    :rules="[{ required: true, message: '请输入发票号码' }]"
                />
        <van-field
          v-model="form.issueDate"
          label="开票日期"
          readonly
          placeholder="请选择开票日期"
                    required
          @click="showIssueDatePicker = true"
          :rules="[{ required: true, message: '请选择开票日期' }]"
        />
      </van-cell-group>
      <!-- äº§å“ä¿¡æ¯ -->
      <view class="product-section">
        <view class="section-header">
          <text class="section-title">产品信息</text>
        </view>
        <view v-if="productData.length === 0" class="empty-state">
          <van-empty description="暂无产品数据" />
        </view>
        <view v-else class="product-list">
          <view
            v-for="(item, index) in productData"
            :key="index"
            class="product-card"
          >
            <!-- äº§å“å¤´éƒ¨ -->
            <view class="product-header">
              <view class="product-title">
                <van-icon name="description" color="#2979ff" size="15" />
                <text class="product-productCategory">产品 {{ index + 1 }}</text>
              </view>
            </view>
            <!-- äº§å“ä¿¡æ¯è¡¨å• -->
            <view class="product-form">
              <van-field
                v-model="item.productCategory"
                label="产品大类"
                readonly
              />
              <van-field
                v-model="item.specificationModel"
                label="规格型号"
                readonly
              />
              <van-field
                v-model="item.unit"
                label="单位"
                readonly
              />
              <van-field
                v-model="item.quantity"
                label="数量"
                readonly
              />
              <van-field
                v-model="item.taxRate"
                                label="税率(%)"
                readonly
              />
              <van-field
                v-model="item.taxInclusiveUnitPrice"
                label="含税单价(元)"
                readonly
              />
              <van-field
                v-model="item.taxInclusiveTotalPrice"
                label="含税总价(元)"
                readonly
              />
              <van-field
                v-model="item.taxExclusiveTotalPrice"
                label="不含税总价(元)"
                readonly
              />
              <!-- æœ¬æ¬¡å¼€ç¥¨ä¿¡æ¯ -->
              <van-field
                v-model="item.currentInvoiceNum"
                label="本次开票数"
                type="number"
                placeholder="请输入开票数量"
                @blur="invoiceNumBlur(item)"
              />
              <van-field
                v-model="item.currentInvoiceAmount"
                label="本次开票金额(元)"
                type="number"
                placeholder="请输入开票金额"
                @blur="invoiceAmountBlur(item)"
              />
              <!-- æœªå¼€ç¥¨ä¿¡æ¯ -->
              <van-field
                v-model="item.noInvoiceNum"
                label="未开票数"
                readonly
              />
              <van-field
                v-model="item.noInvoiceAmount"
                label="未开票金额(元)"
                readonly
              />
            </view>
          </view>
        </view>
      </view>
      <!-- æäº¤æŒ‰é’® -->
            <view class="footer-btns">
                <van-button class="cancel-btn" @click="goBack">取消</van-button>
                <van-button class="save-btn" native-type="submit" form-type="submit">保存</van-button>
            </view>
    </van-form>
    <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
    <van-popup v-model:show="showIssueDatePicker" position="bottom">
      <van-date-picker
        v-model="currentIssueDate"
        title="选择开票日期"
        @confirm="onIssueDateConfirm"
        @cancel="showIssueDatePicker = false"
      />
    </van-popup>
    <van-popup v-model:show="showCreateTimePicker" position="bottom">
      <van-date-picker
        v-model="currentCreateTime"
        title="选择录入日期"
        @confirm="onCreateTimeConfirm"
        @cancel="showCreateTimePicker = false"
      />
    </van-popup>
  </view>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { showToast, showLoadingToast, closeToast } from 'vant'
import { invoiceRegistrationSave } from '@/api/salesManagement/invoiceRegistration'
import useUserStore from '@/store/modules/user'
import {getSalesLedgerWithProducts} from "@/api/salesManagement/salesLedger";
const userStore = useUserStore()
const editData = ref(null);
// è¡¨å•引用
const formRef = ref()
// è¡¨å•数据
let form = ref({
  salesContractNo: '',
  customerName: '',
  salesman: '',
  projectName: '',
  createUer: '',
  issueDate: '',
  createTime: '',
  invoiceNo: ''
})
// äº§å“æ•°æ®
const productData = ref([])
// æ—¥æœŸé€‰æ‹©å™¨çŠ¶æ€
const showIssueDatePicker = ref(false)
const showCreateTimePicker = ref(false)
const currentIssueDate = ref([new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()])
const currentCreateTime = ref([new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()])
// æäº¤çŠ¶æ€
const submitting = ref(false)
// è¿”回上一页
const goBack = () => {
    // æ¸…理本地存储的数据
    uni.removeStorageSync('editData');
  uni.navigateBack()
}
// æ ¼å¼åŒ–æ•°å­—
const formatNumber = (value, precision = 2) => {
  if (!value && value !== 0) return '0.00'
  return Number(value).toFixed(precision)
}
// å¼€ç¥¨æ•°é‡å˜åŒ–处理
const invoiceNumBlur = (row) => {
    if (!row.currentInvoiceNum) {
        row.currentInvoiceNum = 0;
    }
    if (row.currentInvoiceNum > row.tempNoInvoiceNum) {
        showToast('本次开票数不得大于未开票数')
        row.currentInvoiceNum = 0;
    }
    // è®¡ç®—本次开票金额
    row.currentInvoiceAmount = (
        row.currentInvoiceNum * row.taxInclusiveUnitPrice
    ).toFixed(2);
    // è®¡ç®—未开票数
    row.noInvoiceNum = (row.originalNoInvoiceNum - row.currentInvoiceNum).toFixed(
        2
    );
    // è®¡ç®—未开票金额
    row.noInvoiceAmount = (
        row.tempnoInvoiceAmount - row.currentInvoiceAmount
    ).toFixed(2);
}
// å¼€ç¥¨é‡‘额变化处理
const invoiceAmountBlur = (row) => {
    if (!row.currentInvoiceAmount) {
        row.currentInvoiceAmount = 0;
    }
    // è®¡ç®—是否超过开票总金额
    if (row.currentInvoiceAmount > row.tempnoInvoiceAmount) {
        showToast('本次开票金额不得大于未开票金额')
        row.currentInvoiceAmount = 0;
    }
    // è®¡ç®—本次开票数
    row.currentInvoiceNum = (
        row.currentInvoiceAmount / row.taxInclusiveUnitPrice
    ).toFixed(2);
    // è®¡ç®—未开票数
    row.noInvoiceNum = (row.originalNoInvoiceNum - row.currentInvoiceNum).toFixed(
        2
    );
    // è®¡ç®—未开票金额
    row.noInvoiceAmount = (
        row.tempnoInvoiceAmount - row.currentInvoiceAmount
    ).toFixed(2);
}
// æ›´æ–°æœªå¼€ç¥¨æ•°æ®
const updateNoInvoiceData = (row) => {
  const totalQuantity = parseFloat(row.quantity) || 0
  const currentInvoiceNum = parseFloat(row.currentInvoiceNum) || 0
  const totalAmount = parseFloat(row.taxInclusiveTotalPrice) || 0
  const currentInvoiceAmount = parseFloat(row.currentInvoiceAmount) || 0
  row.noInvoiceNum = Math.max(0, totalQuantity - currentInvoiceNum).toFixed(2)
  row.noInvoiceAmount = Math.max(0, totalAmount - currentInvoiceAmount).toFixed(2)
}
// å¼€ç¥¨æ—¥æœŸç¡®è®¤
const onIssueDateConfirm = ({ selectedValues }) => {
    console.log('selectedValues--', selectedValues)
    form.value.issueDate = selectedValues.join('-');
    currentIssueDate.value = selectedValues;
    showIssueDatePicker.value = false;
};
// å½•入日期确认
const onCreateTimeConfirm = (value) => {
  try {
    // å¤„理不同的值格式
    let year, month, day;
    if (Array.isArray(value)) {
      // æ•°ç»„格式 [year, month, day]
      [year, month, day] = value;
    } else if (value && typeof value === 'object') {
      // Date对象格式
      year = value.getFullYear();
      month = value.getMonth() + 1;
      day = value.getDate();
    } else {
      // å…¶ä»–格式,使用当前日期
      const now = new Date();
      year = now.getFullYear();
      month = now.getMonth() + 1;
      day = now.getDate();
    }
    form.value.createTime = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
    showCreateTimePicker.value = false;
  } catch (error) {
    console.error('日期处理错误:', error);
    showToast('日期选择失败,请重试');
  }
}
// æ ¼å¼åŒ–日期
const formatDate = (date) => {
  const year = date.getFullYear()
  const month = String(date.getMonth() + 1).padStart(2, '0')
  const day = String(date.getDate()).padStart(2, '0')
  return `${year}-${month}-${day}`
}
// èŽ·å–äº§å“åˆ—è¡¨
const getProductList = async () => {
  try {
    showLoadingToast('加载中...')
    const res = await getSalesLedgerWithProducts({ id: editData.value.id, type: 1 })
        productData.value = res.productData;
        form.value = { ...res };
        // è®¾ç½®é»˜è®¤å½•入人
        form.value.createUer = userStore.name || ''
        // è®¾ç½®é»˜è®¤æ—¥æœŸ
        const today = new Date()
        form.value.createTime = formatDate(today)
    closeToast()
  } catch (error) {
    closeToast()
    showToast('获取产品列表失败')
  }
}
// æäº¤è¡¨å•
const submitForm = async () => {
  try {
    submitting.value = true
    // éªŒè¯äº§å“æ•°æ®
    if (productData.value.length === 0) {
      showToast('请先添加产品信息')
      return
    }
    // éªŒè¯å¼€ç¥¨æ•°æ®
    const hasInvoiceData = productData.value.some(item => {
      const num = parseFloat(item.currentInvoiceNum) || 0
      const amount = parseFloat(item.currentInvoiceAmount) || 0
      return num > 0 || amount > 0
    })
    if (!hasInvoiceData) {
      showToast('请至少输入一个产品的开票信息')
      return
    }
    const submitData = {
      ...form.value,
      productList: productData.value
    }
    await invoiceRegistrationSave(submitData)
    showToast('提交成功')
    // è¿”回上一页
    setTimeout(() => {
      uni.navigateBack()
    }, 1500)
  } catch (error) {
    showToast('提交失败,请重试')
  } finally {
    submitting.value = false
  }
}
// é¡µé¢åŠ è½½æ—¶åˆå§‹åŒ–æ•°æ®
onMounted(() => {
  // ä»Žé¡µé¢å‚数或缓存中获取销售合同信息
  const contractInfo = uni.getStorageSync('editData')
  if (contractInfo) {
        editData.value = JSON.parse(contractInfo);
    const contract = JSON.parse(contractInfo)
    form.value.salesContractNo = contract.salesContractNo || ''
    form.value.customerName = contract.customerName || ''
    form.value.salesman = contract.salesman || ''
    form.value.projectName = contract.projectName || ''
    // èŽ·å–äº§å“åˆ—è¡¨
    getProductList()
  }
})
</script>
<style scoped lang="scss">
.account-detail {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 5rem;
}
.empty-state {
  padding: 40px 0;
}
.product-section {
  background: #fff;
  margin-top: 1rem;
  padding: 1rem;
  box-shadow: 0 0.125rem 0.5rem rgba(0,0,0,0.04);
}
.section-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 1rem;
}
.section-title {
  font-size: 1rem;
  font-weight: 600;
  color: #333;
}
.product-list {
  .product-card {
    background: #FFFFFF;
    box-shadow: 0 0 1.25rem 0 rgba(0,57,117,0.08);
    border-radius: 0.5rem 0.5rem 0.5rem 0.5rem;
    padding: 1rem 0.5rem 0 0.5rem;
    position: relative;
    margin-bottom: 1rem;
  }
}
.product-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 0.5rem 0.75rem 0.5rem;
  border-bottom: 0.0625rem solid #e8e8e8;
}
.product-title {
  display: flex;
  align-items: center;
}
.product-productCategory {
  margin-left: 0.5rem;
  font-size: 0.875rem;
  font-weight: 500;
  color: #333;
}
.product-form {
  margin-bottom: 1rem;
}
.footer-btns {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    background: #fff;
    display: flex;
    justify-content: space-around;
    align-items: center;
    padding: 0.75rem 0;
    box-shadow: 0 -0.125rem 0.5rem rgba(0,0,0,0.05);
    z-index: 1000;
}
.cancel-btn {
    font-weight: 400;
    font-size: 1rem;
    color: #FFFFFF;
    width: 6.375rem;
    background: #C7C9CC;
    box-shadow: 0 0.25rem 0.625rem 0 rgba(3,88,185,0.2);
    border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
}
.save-btn {
    font-weight: 400;
    font-size: 1rem;
    color: #FFFFFF;
    width: 14rem;
    background: linear-gradient( 140deg, #00BAFF 0%, #006CFB 100%);
    box-shadow: 0 0.25rem 0.625rem 0 rgba(3,88,185,0.2);
    border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
}
// å“åº”式调整
@media (max-width: 768px) {
  .submit-section {
    padding: 12px;
  }
}
</style>
src/pages/sales/invoicingRegistration/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,448 @@
<template>
    <view class="sales-account">
        <!-- é¡µé¢å¤´éƒ¨ -->
        <van-nav-bar
            title="开票登记"
            left-text="返回"
            left-arrow
            @click-left="goBack"
            fixed
            placeholder
        />
        <!-- æœç´¢å’Œç­›é€‰åŒºåŸŸ -->
        <view class="search-filter-section">
            <view class="search-bar">
                <view class="search-input">
                    <input
                        class="search-text"
                        placeholder="请输入销售合同号/客户名称"
                        v-model="searchKeyword"
                    />
                </view>
                <view class="filter-button" @click="getList">
                    <up-icon name="search" size="24" color="#999"></up-icon>
                </view>
            </view>
        </view>
        <!-- é”€å”®å°è´¦ç€‘布流 -->
        <view class="ledger-list" v-if="total > 0">
            <view v-for="(item, index) in ledgerList" :key="index">
                <view class="ledger-item">
                    <view class="item-header">
                        <view class="item-left">
                            <view class="document-icon">
                                <up-icon name="file-text" size="16" color="#ffffff"></up-icon>
                            </view>
                            <text class="item-id">{{ item.salesContractNo }}</text>
                        </view>
                        <!--                            <view class="item-tag">-->
                        <!--                                <text class="tag-text">{{ item.recorder }}</text>-->
                        <!--                            </view>-->
                    </view>
                    <up-divider></up-divider>
                    <view class="item-details">
                        <view class="detail-row">
                            <text class="detail-label">客户名称</text>
                            <text class="detail-value">{{ item.customerName }}</text>
                        </view>
                        <view class="detail-row">
                            <text class="detail-label">客户合同号</text>
                            <text class="detail-value">{{ item.customerContractNo }}</text>
                        </view>
                        <view class="detail-row">
                            <text class="detail-label">业务员</text>
                            <text class="detail-value">{{ item.salesman }}</text>
                        </view>
                        <view class="detail-row">
                            <text class="detail-label">项目名称</text>
                            <text class="detail-value">{{ item.projectName }}</text>
                        </view>
                        <view class="detail-row">
                            <text class="detail-label">合同金额(元)</text>
                            <text class="detail-value highlight">{{ item.contractAmount }}</text>
                        </view>
                        <view class="detail-row">
                            <text class="detail-label">已开票金额(元)</text>
                            <text class="detail-value highlight">{{ item.invoiceTotal }}</text>
                        </view>
                        <view class="detail-row">
                            <text class="detail-label">未开票金额(元)</text>
                            <text class="detail-value redlight">{{ item.noInvoiceAmountTotal }}</text>
                        </view>
                    </view>
                    <!-- æ“ä½œæŒ‰é’®åŒºåŸŸ -->
                    <view class="action-buttons">
                        <van-button
                            type="primary"
                            size="small"
                            @click="handleAddInvoice(item)"
                            class="action-btn"
                            :disabled="item.entryPerson != userStore.id || item.noInvoiceAmountTotal == 0"
                        >
                            æ–°å¢žå¼€ç¥¨
                        </van-button>
                        <van-button
                            type="default"
                            size="small"
                            @click="handleViewDetail(item)"
                            class="action-btn"
                        >
                            æŸ¥çœ‹è¯¦æƒ…
                        </van-button>
                    </view>
                </view>
            </view>
        </view>
        <view v-else class="no-data">
            <text>暂无销售台账数据</text>
        </view>
        <!-- æµ®åŠ¨æ“ä½œæŒ‰é’® -->
        <!-- <view class="fab-button" @click="handleInfo('add')">
            <up-icon name="plus" size="24" color="#ffffff"></up-icon>
        </view> -->
    </view>
</template>
<script setup>
import { ref } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import {ledgerListPage} from "@/api/salesManagement/salesLedger";
import useUserStore from "@/store/modules/user";
const userStore = useUserStore()
// æœç´¢å…³é”®è¯
const searchKeyword = ref('');
// é”€å”®å°è´¦æ•°æ®
const ledgerList = ref([]);
const total = ref(0);
// åˆåŒé€‰æ‹©å™¨ç›¸å…³
const contractList = ref([]);
const contractLoading = ref(false);
const contractFinished = ref(false);
// è¿”回上一页
const goBack = () => {
    uni.navigateBack();
};
// æŸ¥è¯¢åˆ—表
const getList = () => {
    const page = {
        current: -1,
        size: -1
    }
    ledgerListPage({...page}).then((res) => {
        ledgerList.value = res.records;
        total.value = res.total;
    }).catch(() => {
        // tableLoading.value = false;
    });
};
// å¤„理新增开票
const handleAddInvoice = (item) => {
    try {
        // æ£€æŸ¥æƒé™ï¼šåªæœ‰å½•入人才能新增开票
        if (item.entryPerson != userStore.id) {
            uni.showToast({
                title: '只有录入人才能新增开票',
                icon: 'none'
            });
            return;
        }
        // å­˜å‚¨é€‰ä¸­çš„合同信息
        uni.setStorageSync('editData', JSON.stringify(item));
        // è·³è½¬åˆ°æ–°å¢žå¼€ç¥¨é¡µé¢
        uni.navigateTo({
            url: '/pages/sales/invoicingRegistration/add'
        });
    } catch (error) {
        console.error('处理新增开票失败:', error);
        uni.showToast({
            title: '操作失败,请重试',
            icon: 'error'
        });
    }
};
// å¤„理查看详情
const handleViewDetail = (item) => {
    try {
        // å­˜å‚¨æ•°æ®
        uni.setStorageSync('editData', JSON.stringify(item));
        // è·³è½¬åˆ°è¯¦æƒ…页面
        uni.navigateTo({
            url: '/pages/sales/invoicingRegistration/view'
        });
    } catch (error) {
        console.error('处理查看详情失败:', error);
        uni.showToast({
            title: '操作失败,请重试',
            icon: 'error'
        });
    }
};
onShow(() => {
    // é¡µé¢æ˜¾ç¤ºæ—¶åˆ·æ–°åˆ—表
    getList();
});
</script>
<style scoped lang="scss">
.u-divider {
    margin: 0 !important;
}
.sales-account {
    min-height: 100vh;
    background: #f8f9fa;
    position: relative;
}
.page-header {
    background: #ffffff;
    padding: 16px 20px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    border-bottom: 1px solid #f0f0f0;
    position: sticky;
    /* å…¼å®¹ iOS åˆ˜æµ·/灵动岛安全区 */
    padding-top: env(safe-area-inset-top);
    top: 0;
    z-index: 100;
}
.header-left {
    display: flex;
    align-items: center;
    gap: 8px;
}
.nav-icon {
    width: 24px;
    height: 24px;
    background: #2979ff;
    border-radius: 4px;
    display: flex;
    align-items: center;
    justify-content: center;
}
.nav-text {
    font-size: 14px;
    color: #2979ff;
    font-weight: 500;
}
.header-center {
    flex: 1;
    text-align: center;
}
.page-title {
    font-size: 18px;
    font-weight: 600;
    color: #333;
}
.header-right {
    display: flex;
    align-items: center;
}
.status-bar {
    display: flex;
    align-items: center;
    gap: 4px;
}
.signal, .wifi, .battery {
    width: 16px;
    height: 8px;
    background: #333;
    border-radius: 2px;
}
.search-filter-section {
    padding: 10px 20px;
    background: #ffffff;
}
.search-bar {
    display: flex;
    align-items: center;
    gap: 12px;
}
.search-input {
    flex: 1;
    background: #f5f5f5;
    border-radius: 24px;
    padding: 10px 16px;
    display: flex;
    align-items: center;
    gap: 8px;
}
.search-text {
    flex: 1;
    font-size: 14px;
    color: #333;
    background: transparent;
    border: none;
    outline: none;
}
.search-text::placeholder {
    color: #999;
}
.filter-button {
    width: 40px;
    height: 40px;
    border-radius: 8px;
    display: flex;
    align-items: center;
    justify-content: center;
}
.ledger-list {
    padding: 20px;
}
.ledger-item {
    background: #ffffff;
    border-radius: 12px;
    margin-bottom: 16px;
    overflow: hidden;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
    padding: 0 16px;
}
.item-header {
    padding: 16px 0;
    display: flex;
    align-items: center;
    justify-content: space-between;
}
.item-left {
    display: flex;
    align-items: center;
    gap: 8px;
}
.document-icon {
    width: 24px;
    height: 24px;
    background: #2979ff;
    border-radius: 4px;
    display: flex;
    align-items: center;
    justify-content: center;
}
.item-id {
    font-size: 14px;
    color: #333;
    font-weight: 500;
}
.item-tag {
    background: #4caf50;
    border-radius: 4px;
    padding: 2px 4px;
}
.tag-text {
    font-size: 11px;
    color: #ffffff;
    font-weight: 500;
}
.item-details {
    padding: 16px 0;
}
.detail-row {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    margin-bottom: 8px;
    &:last-child {
        margin-bottom: 0;
    }
}
.detail-info {
    margin-top: 10px;
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
}
.detail-label {
    font-size: 12px;
    color: #777777;
    min-width: 60px;
}
.detail-value {
    font-size: 12px;
    color: #000000;
    text-align: right;
    flex: 1;
    margin-left: 16px;
}
.detail-value.highlight {
    color: #2979ff;
    font-weight: 500;
}
.detail-value.redlight {
    color: red;
    font-weight: 500;
}
.action-buttons {
    display: flex;
    gap: 12px;
    padding: 0 0 16px 0;
    justify-content: space-between;
}
.action-btn {
    flex: 1;
}
.no-data {
    padding: 40px 0;
    text-align: center;
    color: #999;
}
.fab-button {
    position: fixed;
    bottom: 30px;
    right: 30px;
    width: 56px;
    height: 56px;
    background: #2979ff;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: 0 4px 16px rgba(41, 121, 255, 0.3);
    z-index: 1000;
}
</style>
src/pages/sales/invoicingRegistration/view.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,325 @@
<template>
  <view class="account-view">
    <!-- é¡¶éƒ¨æ ‡é¢˜æ  -->
        <van-nav-bar
            title="开票登记详情"
            left-text="返回"
            left-arrow
            @click-left="goBack"
            fixed
            placeholder
        />
    <!-- åŸºæœ¬ä¿¡æ¯å±•示 -->
    <view class="info-section">
      <view class="section-title">基本信息</view>
      <view class="info-grid">
        <view class="info-item">
          <text class="info-label">销售合同号</text>
          <text class="info-value">{{ form.salesContractNo }}</text>
        </view>
        <view class="info-item">
          <text class="info-label">客户合同号</text>
          <text class="info-value">{{ form.customerContractNo }}</text>
        </view>
        <view class="info-item">
          <text class="info-label">客户名称</text>
          <text class="info-value">{{ form.customerName }}</text>
        </view>
        <view class="info-item">
          <text class="info-label">业务员</text>
          <text class="info-value">{{ form.salesman }}</text>
        </view>
        <view class="info-item">
          <text class="info-label">项目名称</text>
          <text class="info-value">{{ form.projectName }}</text>
        </view>
        <view class="info-item">
          <text class="info-label">合同金额(元)</text>
          <text class="info-value highlight">{{ form.contractAmount }}</text>
        </view>
        <view class="info-item">
          <text class="info-label">已开票金额(元)</text>
          <text class="info-value highlight">{{ form.invoiceTotal }}</text>
        </view>
        <view class="info-item">
          <text class="info-label">未开票金额(元)</text>
          <text class="info-value redlight">{{ form.noInvoiceAmountTotal }}</text>
        </view>
      </view>
    </view>
    <!-- äº§å“ä¿¡æ¯å±•示 -->
    <view class="product-section" v-if="productData && productData.length > 0">
      <view class="section-title">产品信息</view>
      <view class="product-card" v-for="(product, idx) in productData" :key="idx">
        <view class="product-header">
          <view class="product-title">
            <van-icon name="description" color="#2979ff" size="15" />
            <text class="product-productCategory">产品 {{ idx + 1 }}</text>
          </view>
        </view>
        <view class="product-info">
          <view class="info-grid">
            <view class="info-item">
              <text class="info-label">产品大类</text>
              <text class="info-value">{{ product.productCategory }}</text>
            </view>
            <view class="info-item">
              <text class="info-label">规格型号</text>
              <text class="info-value">{{ product.specificationModel }}</text>
            </view>
            <view class="info-item">
              <text class="info-label">单位</text>
              <text class="info-value">{{ product.unit }}</text>
            </view>
            <view class="info-item">
              <text class="info-label">税率(%)</text>
              <text class="info-value">{{ product.taxRate }}</text>
            </view>
            <view class="info-item">
              <text class="info-label">数量</text>
              <text class="info-value highlight">{{ product.quantity }}</text>
            </view>
                        <view class="info-item">
                            <text class="info-label">含税单价(元)</text>
                            <text class="info-value highlight">{{ product.taxInclusiveUnitPrice }}</text>
                        </view>
            <view class="info-item">
              <text class="info-label">含税总价(元)</text>
              <text class="info-value highlight">{{ product.taxInclusiveTotalPrice }}</text>
            </view>
            <view class="info-item">
              <text class="info-label">不含税总价(元)</text>
              <text class="info-value highlight">{{ product.taxExclusiveTotalPrice }}</text>
            </view>
            <view class="info-item">
              <text class="info-label">开票数</text>
              <text class="info-value highlight">{{ product.invoiceNum }}</text>
            </view>
            <view class="info-item">
              <text class="info-label">开票金额(元)</text>
              <text class="info-value highlight">{{ product.invoiceAmount }}</text>
            </view>
            <view class="info-item">
              <text class="info-label">未开票数</text>
              <text class="info-value highlight">{{ product.noInvoiceNum }}</text>
            </view>
            <view class="info-item">
              <text class="info-label">未开票金额(元)</text>
              <text class="info-value redlight">{{ product.noInvoiceAmount }}</text>
            </view>
          </view>
        </view>
      </view>
    </view>
    <!-- æ— äº§å“ä¿¡æ¯æç¤º -->
    <view class="no-product" v-else>
      <text>暂无产品信息</text>
    </view>
  </view>
</template>
<script setup>
import { onMounted, ref } from 'vue';
import { getSalesLedgerWithProducts } from "@/api/salesManagement/salesLedger";
// è¡¨å•数据
const form = ref({
  id: '',
  salesContractNo: '',
  customerContractNo: '',
  customerId: '',
  customerName: '',
  projectName: '',
  executionDate: '',
    contractAmount: '',
  invoiceTotal: '',
  noInvoiceAmountTotal: '',
  salesman: ''
});
// äº§å“æ•°æ®
const productData = ref([]);
// ç¼–辑数据
const editData = ref(null);
// è¿”回上一页
const goBack = () => {
  // æ¸…理本地存储的数据
  uni.removeStorageSync('editData');
  uni.navigateBack();
};
// å¡«å……表单数据
const fillFormData = () => {
  if (!editData.value) return;
  // èŽ·å–å®Œæ•´çš„äº§å“ä¿¡æ¯
  getSalesLedgerWithProducts({ id: editData.value.id, type: 1 }).then((res) => {
    productData.value = res.productData || [];
  });
  // å¡«å……基本信息
  form.value.salesContractNo = editData.value.salesContractNo || '';
  form.value.customerContractNo = editData.value.customerContractNo || '';
  form.value.customerName = editData.value.customerName || '';
  form.value.projectName = editData.value.projectName || '';
  form.value.executionDate = editData.value.executionDate || '';
  form.value.contractAmount = editData.value.contractAmount || '';
  form.value.salesman = editData.value.salesman || '';
  form.value.invoiceTotal = editData.value.invoiceTotal || 0;
  form.value.noInvoiceAmountTotal = editData.value.noInvoiceAmountTotal || 0;
  form.value.id = editData.value.id || '';
  form.value.customerId = editData.value.customerId || '';
};
onMounted(() => {
  // èŽ·å–ç¼–è¾‘æ•°æ®å¹¶å¡«å……è¡¨å•
  const editDataStr = uni.getStorageSync('editData');
  if (editDataStr) {
    try {
      editData.value = JSON.parse(editDataStr);
      // ä½¿ç”¨ nextTick ç¡®ä¿æ•°æ®åŠ è½½å®ŒæˆåŽå†å¡«å……
      setTimeout(() => {
        fillFormData();
      }, 100);
    } catch (error) {
      console.error('解析编辑数据失败:', error);
    }
  }
});
</script>
<style scoped lang="scss">
.account-view {
  min-height: 100vh;
  background: #f8f9fa;
  padding-bottom: 2rem;
}
.header {
  display: flex;
  align-items: center;
  background: #fff;
  padding: 1rem 1.25rem;
  border-bottom: 0.0625rem solid #f0f0f0;
  position: sticky;
  top: 0;
  z-index: 100;
  /* å…¼å®¹ iOS åˆ˜æµ·/灵动岛安全区 */
  padding-top: env(safe-area-inset-top);
}
.title {
  flex: 1;
  text-align: center;
  font-size: 1.125rem;
  font-weight: 600;
  color: #333;
}
.info-section {
  background: #fff;
  margin: 1rem;
  padding: 1rem;
  border-radius: 0.5rem;
  box-shadow: 0 0.125rem 0.5rem rgba(0,0,0,0.04);
}
.section-title {
  font-size: 1rem;
  font-weight: 600;
  color: #333;
  margin-bottom: 1rem;
  padding-bottom: 1rem;
  border-bottom: 0.0625rem solid #e8e8e8;
}
.info-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.75rem;
}
.info-item {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}
.info-label {
  font-size: 0.75rem;
  color: #666;
  font-weight: 400;
}
.info-value {
  font-size: 0.875rem;
  color: #333;
  font-weight: 500;
}
.info-value.highlight {
  color: #2979ff;
  font-weight: 600;
}
.info-value.redlight {
  color: red;
  font-weight: 600;
}
.product-section {
  background: #fff;
  margin: 1rem;
  padding: 1rem;
  border-radius: 0.5rem;
  box-shadow: 0 0.125rem 0.5rem rgba(0,0,0,0.04);
}
.product-card {
  background: #f8f9fa;
  border-radius: 0.5rem;
  padding: 1rem;
  margin-bottom: 1rem;
}
.product-card:last-child {
  margin-bottom: 0;
}
.product-header {
  display: flex;
  align-items: center;
  padding-bottom: 0.75rem;
  border-bottom: 0.0625rem solid #e8e8e8;
  margin-bottom: 1rem;
}
.product-title {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.product-productCategory {
  font-size: 0.875rem;
  font-weight: 500;
  color: #333;
}
.product-info .info-grid {
  grid-template-columns: 1fr 1fr;
  gap: 0.5rem;
}
.no-product {
  text-align: center;
  padding: 2rem;
  color: #999;
  font-size: 0.875rem;
}
</style>
src/pages/sales/salesAccount/detail.vue
@@ -1,249 +1,854 @@
<template>
  <view class="account-detail">
    <!-- é¡¶éƒ¨æ ‡é¢˜æ  -->
    <view class="header">
      <up-icon name="arrow-left" size="20" color="#333" @click="goBack" />
      <text class="title">台账详情</text>
    </view>
        <van-nav-bar
            title="台账详情"
            left-text="返回"
            left-arrow
            @click-left="goBack"
            fixed
            placeholder
        />
    <!-- è¡¨å•区域 -->
    <view class="form-section">
      <van-form ref="formRef" :modelValue="form" :rules="rules" label-width="100px" input-align="right">
        <van-field label="销售合同号" name="salesContractNo" borderBottom="true" v-model="form.salesContractNo" placeholder="请输入销售合同号">
        </van-field>
                <van-field label="销售合同号" name="salesContractNo" borderBottom="true" v-model="form.salesContractNo" placeholder="请输入销售合同号">
                </van-field>
                <van-field label="销售合同号" name="salesContractNo" borderBottom="true" v-model="form.salesContractNo" placeholder="请输入销售合同号">
                </van-field>
                <van-field label="销售合同号" name="salesContractNo" borderBottom="true" v-model="form.salesContractNo" placeholder="请输入销售合同号">
                </van-field>
                <van-field label="销售合同号" name="salesContractNo" borderBottom="true" v-model="form.salesContractNo" placeholder="请输入销售合同号">
                </van-field>
                <van-field label="销售合同号" name="salesContractNo" borderBottom="true" v-model="form.salesContractNo" placeholder="请输入销售合同号">
                </van-field>
                <van-field label="录入人" name="salesContractNo" borderBottom="true" v-model="form.salesContractNo" placeholder="请输入" readonly>
                </van-field>
                <van-field label="录入日期" name="salesContractNo" borderBottom="true" v-model="form.salesContractNo" placeholder="请输入" readonly>
                </van-field>
      </van-form>
    </view>
    <!-- äº§å“ä¿¡æ¯ -->
    <view class="product-section">
      <view class="section-header">
        <text class="section-title">产品信息</text>
        <button class="add-btn" @click="addProduct">新增</button>
      </view>
      <view class="product-card" v-for="(product, idx) in products" :key="idx">
        <view class="product-row">
          <text class="product-label">产品类</text>
          <uni-easyinput v-model="product.type" placeholder="请输入产品类" />
        </view>
        <view class="product-row">
          <text class="product-label">单位</text>
          <uni-easyinput v-model="product.unit" placeholder="请输入单位" />
          <text class="product-label">数量</text>
          <uni-easyinput v-model="product.amount" placeholder="请输入数量" type="number" />
        </view>
        <view class="product-row">
          <text class="product-label">税率</text>
          <uni-easyinput v-model="product.taxRate" placeholder="请输入税率" />
          <text class="product-label">含税单价</text>
          <uni-easyinput v-model="product.taxPrice" placeholder="请输入含税单价" />
        </view>
        <view class="product-row">
          <text class="product-label">含税总价</text>
          <uni-easyinput v-model="product.taxTotal" placeholder="请输入含税总价" />
          <text class="product-label">合同金额</text>
          <uni-easyinput v-model="product.contractAmount" placeholder="请输入合同金额" />
        </view>
        <view class="product-row">
          <text class="product-label">操作</text>
          <uni-easyinput v-model="product.operateDate" placeholder="请输入操作时间" />
        </view>
        <view class="product-row">
          <text class="product-label">备注</text>
          <uni-easyinput v-model="product.remark" placeholder="请输入备注" />
        </view>
        <view class="product-row del-row">
          <button class="del-btn" @click="removeProduct(idx)">删除</button>
        </view>
      </view>
    </view>
    <!-- åº•部按钮 -->
    <view class="footer-btns">
      <van-button class="cancel-btn" @click="goBack">取消</van-button>
      <van-button class="save-btn" @click="submitForm">保存</van-button>
    </view>
         <!-- è¡¨å•区域 -->
        <van-form @submit="onSubmit" label-width="110px" input-align="right" style="margin-top: 10px" error-message-align="right" scroll-to-error scroll-to-error-position="center">
            <van-field label="销售合同号" name="salesContractNo" borderBottom="true" v-model="form.salesContractNo" placeholder="自动生成" disabled>
            </van-field>
            <van-field
                v-model="form.salesman"
                is-link
                readonly
                name="salesman"
                label="业务员"
                required
                placeholder="点击选择业务员"
                :rules="[{ required: true, message: '请选择业务员' }]"
                @click="showPicker = true"
            />
            <van-field label="客户合同号" name="customerContractNo" borderBottom="true"
                                 v-model="form.customerContractNo" required
                                 placeholder="请输入客户合同号" :rules="[{ required: true, message: '客户合同号不能为空' }]">
            </van-field>
            <van-field
                v-model="form.customerName"
                is-link
                readonly
                required
                name="customerName"
                label="客户名称"
                placeholder="点击选择客户"
                :rules="[{ required: true, message: '请选择客户' }]"
                @click="showCustomerPicker = true"
            />
            <van-field label="项目名称" name="projectName" borderBottom="true" v-model="form.projectName" placeholder="请输入项目名称" :rules="[{ required: true, message: '项目名称不能为空' }]" required>
            </van-field>
            <van-field
                v-model="form.executionDate"
                is-link
                readonly
                required
                name="executionDate"
                label="签订日期"
                placeholder="点击选择时间"
                :rules="[{ required: true, message: '签订日期不能为空' }]"
                @click="showDatePicker = true"
            />
            <van-popup v-model:show="showDatePicker" destroy-on-close position="bottom">
                <van-date-picker
                    v-model="pickerDateValue"
                    @confirm="onDateConfirm"
                    @cancel="showDatePicker = false"
                />
            </van-popup>
            <van-field label="付款方式" name="paymentMethod" borderBottom="true" v-model="form.paymentMethod" placeholder="请输入付款方式">
            </van-field>
            <van-field label="录入人" name="entryPersonName" borderBottom="true" v-model="form.entryPersonName" placeholder="请输入" disabled>
            </van-field>
            <van-field label="录入日期" name="entryDate" borderBottom="true" v-model="form.entryDate" placeholder="请输入" disabled>
            </van-field>
            <van-popup v-model:show="showPicker" destroy-on-close position="bottom">
                <van-picker
                    :columns="userList"
                    v-model="pickerValue"
                    @confirm="onConfirm"
                    @cancel="showPicker = false"
                />
            </van-popup>
            <van-popup v-model:show="showCustomerPicker" destroy-on-close position="bottom">
                <van-picker
                    :columns="customerOption"
                    v-model="pickerCustomerValue"
                    @confirm="onCustomerConfirm"
                    @cancel="showCustomerPicker = false"
                />
            </van-popup>
            <!-- äº§å“å¤§ç±»é€‰æ‹©å™¨ -->
            <van-popup v-model:show="showCategoryPicker" destroy-on-close position="bottom">
                <!-- å¤´éƒ¨æŒ‰é’®åŒºåŸŸ -->
                <view class="popup-header">
                    <view @click="showCategoryPicker = false" class="cancelButton">取消</view>
                    <view @click="confirmCategorySelection" class="confirmButton">确定</view>
                </view>
                <up-tree
                    :data="productOptions"
                    :props="defaultProps"
                    show-checkbox
                    default-expand-all
                    check-strictly
                    @check-change="onCategoryConfirm"
                />
            </van-popup>
            <!-- è§„格型号选择器 -->
            <van-popup v-model:show="showSpecificationPicker" destroy-on-close position="bottom">
                <van-picker
                    :columns="modelOptions"
                    v-model="pickerSpecificationValue"
                    @confirm="onSpecificationConfirm"
                    @cancel="showSpecificationPicker = false"
                />
            </van-popup>
            <!-- ç¨ŽçŽ‡é€‰æ‹©å™¨ -->
            <van-popup v-model:show="showTaxRatePicker" destroy-on-close position="bottom">
                <van-picker
                    :columns="taxRateOptions"
                    v-model="pickerTaxRateValue"
                    @confirm="onTaxRateConfirm"
                    @cancel="showTaxRatePicker = false"
                />
            </van-popup>
            <!-- å‘票类型选择器 -->
            <van-popup v-model:show="showInvoiceTypePicker" destroy-on-close position="bottom">
                <van-picker
                    :columns="invoiceTypeOptions"
                    v-model="pickerInvoiceTypeValue"
                    @confirm="onInvoiceTypeConfirm"
                    @cancel="showInvoiceTypePicker = false"
                />
            </van-popup>
            <!-- äº§å“ä¿¡æ¯ -->
            <view class="product-section">
                <view class="section-header">
                    <text class="section-title">产品信息</text>
                    <van-button type="primary" size="small" @click="addProduct" class="add-btn" icon="plus"  v-if="operationType !== 'view'">新增</van-button>
                </view>
                <view class="product-card" v-for="(product, idx) in productData" :key="idx">
                    <!-- äº§å“ç±» -->
                    <view class="product-header">
                        <view class="product-title">
                            <van-icon name="description" color="#2979ff" size="15" />
                            <text class="product-productCategory">产品 {{ idx + 1 }}</text>
                        </view>
                        <!-- æ“ä½œæŒ‰é’® -->
                        <view class="product-actions"  v-if="operationType !== 'view'">
                            <van-button type="danger" size="mini" @click="removeProduct(idx)" class="del-btn" icon="delete">删除</van-button>
                        </view>
                    </view>
                    <!-- äº§å“ä¿¡æ¯è¡¨å• -->
                    <view class="product-form">
                        <!-- äº§å“å¤§ç±» -->
                        <van-field
                            v-model="product.productCategory"
                            is-link
                            readonly
                            name="productCategory"
                            label="产品大类"
                            required
                            placeholder="请选择"
                            :rules="[{ required: true, message: '请选择' }]"
                            @click="openCategoryPicker(idx)"
                        />
                        <!-- è§„格型号 -->
                        <van-field
                            v-model="product.specificationModel"
                            is-link
                            readonly
                            name="specificationModel"
                            label="规格型号"
                            required
                            :rules="[{ required: true, message: '请选择' }]"
                            placeholder="请选择"
                            @click="openSpecificationPicker(idx)"
                        />
                        <!-- å•位 -->
                        <van-field
                            v-model="product.unit"
                            name="unit"
                            label="单位"
                            required
                            :rules="[{ required: true, message: '请输入' }]"
                            placeholder="请输入"
                        />
                        <!-- ç¨Žçއ -->
                        <van-field
                            v-model="product.taxRate"
                            is-link
                            readonly
                            name="taxRate"
                            label="税率(%)"
                            required
                            :rules="[{ required: true, message: '请选择' }]"
                            placeholder="请选择"
                            @click="openTaxRatePicker(idx)"
                        />
                        <!-- å«ç¨Žå•ä»· -->
                        <van-field
                            v-model="product.taxInclusiveUnitPrice"
                            name="taxInclusiveUnitPrice"
                            label="含税单价(元)"
                            type="number"
                            required
                            :rules="[{ required: true, message: '请输入' }]"
                            placeholder="请输入"
                            @blur="formatTaxPrice(idx)"
                        />
                        <!-- æ•°é‡ -->
                        <van-field
                            v-model="product.quantity"
                            name="quantity"
                            label="数量"
                            type="number"
                            :rules="[{ required: true, message: '请输入' }]"
                            required
                            placeholder="请输入"
                            @blur="formatAmount(idx)"
                        />
                        <!-- å«ç¨Žæ€»ä»· -->
                        <van-field
                            v-model="product.taxInclusiveTotalPrice"
                            name="taxInclusiveTotalPrice"
                            label="含税总价(元)"
                            type="number"
                            :rules="[{ required: true, message: '请输入' }]"
                            required
                            placeholder="请输入"
                            @blur="formatTaxTotal(idx)"
                        />
                        <!-- ä¸å«ç¨Žæ€»ä»· -->
                        <van-field
                            v-model="product.taxExclusiveTotalPrice"
                            name="taxExclusiveTotalPrice"
                            label="不含税总价(元)"
                            type="number"
                            required
                            :rules="[{ required: true, message: '请输入' }]"
                            placeholder="请输入"
                            @blur="formatNoTaxTotal(idx)"
                        />
                        <!-- å‘票类型 -->
                        <van-field
                            v-model="product.invoiceType"
                            is-link
                            readonly
                            name="invoiceType"
                            label="发票类型"
                            :rules="[{ required: true, message: '请选择' }]"
                            required
                            placeholder="请选择"
                            @click="openInvoiceTypePicker(idx)"
                        />
                    </view>
                </view>
            </view>
            <view class="footer-btns" v-if="operationType !== 'view'">
                <van-button class="cancel-btn" @click="goBack">取消</van-button>
                <van-button class="save-btn" native-type="submit" form-type="submit">保存</van-button>
            </view>
        </van-form>
  </view>
</template>
<script setup>
import { ref } from 'vue';
const goBack = () => {
  uni.navigateBack();
};
const formRef = ref();
const paymentMethods = ['对公转账', '现金', '其他'];
import {onMounted, ref} from 'vue';
import {userListNoPage} from "@/api/system/user";
import {
    addOrUpdateSalesLedger,
    addOrUpdateSalesLedgerProduct,
    customerList,
    getSalesLedgerWithProducts,
    modelList,
    productTreeList
} from "@/api/salesManagement/salesLedger";
import useUserStore from "@/store/modules/user";
import {calculateTaxExclusiveTotalPrice} from "@/utils/summarizeTable";
// èŽ·å–é¡µé¢å‚æ•°
const operationType = ref('');
const editData = ref(null);
const userStore = useUserStore()
const form = ref({
    id: '',
  salesContractNo: '',
  customerContract: '',
  projectName: '',
  contractAmount: '',
  executionDate: '',
    customerContractNo: '',
    customerId: '',
    customerName: '',
    projectName: '',
    executionDate: '',
  paymentMethod: '',
    entryPerson: '',
    entryPersonName: '',
    entryDate: '',
});
const rules = {
  salesContractNo: {
    rules: [{ required: true, errorMessage: '销售合同号不能为空' }]
  },
  customerContract: {
    rules: [{ required: true, errorMessage: '客户合同不能为空' }]
  },
  projectName: {
    rules: [{ required: true, errorMessage: '项目名称不能为空' }]
  },
  contractAmount: {
    rules: [{ required: true, errorMessage: '合同金额不能为空' }]
  },
  executionDate: {
    rules: [{ required: true, errorMessage: '签订日期不能为空' }]
  },
  paymentMethod: {
    rules: [{ required: true, errorMessage: '请选择付款方式' }]
  }
};
const products = ref([
  {
    type: 'LS-29911',
    unit: '周庄镇',
    amount: '86590905972612',
    taxRate: '这里是项目名称',
    taxPrice: '这里是项目名称',
    taxTotal: '这里是项目名称',
    contractAmount: '32011元',
    operateDate: '2022-02-22 11:30:50',
    remark: '',
  },
  {
    type: 'LS-29911',
    unit: '周庄镇',
    amount: '86590905972612',
    taxRate: '这里是项目名称',
    taxPrice: '这里是项目名称',
    taxTotal: '这里是项目名称',
    contractAmount: '32011元',
    operateDate: '2022-02-22 11:30:50',
    remark: '',
  },
const pickerValue = ref(['']);
const pickerDateValue = ref([]);
const showPicker = ref(false);
const showDatePicker = ref(false);
const pickerCustomerValue = ref(['']);
const showCustomerPicker = ref(false);
const userList = ref([]);
const customerOption = ref([]);
const productData = ref([]);
// é€‰æ‹©å™¨ç›¸å…³å˜é‡
const showCategoryPicker = ref(false);
const showSpecificationPicker = ref(false);
const showTaxRatePicker = ref(false);
const showInvoiceTypePicker = ref(false);
const pickerCategoryValue = ref(['']);
const pickerSpecificationValue = ref(['']);
const pickerTaxRateValue = ref(['']);
const pickerInvoiceTypeValue = ref(['']);
const currentProductIndex = ref(0);
// é€‰é¡¹æ•°æ®
const productOptions = ref([]);
const selectedCategoryNode = ref(null);
const defaultProps = ref({
    children: 'children',
    label: 'label',
    nodeKey: 'id'
});
const modelOptions = ref([]);
// é˜²æ­¢å¾ªçŽ¯è®¡ç®—çš„æ ‡å¿—
// const isCalculating = ref(false);
const taxRateOptions = ref([
  { text: '1', value: '1' },
  { text: '6', value: '6' },
  { text: '13', value: '13' },
]);
const invoiceTypeOptions = ref([
  { text: '增普票', value: '增普票' },
  { text: '增专票', value: '增专票' },
]);
const addProduct = () => {
  products.value.push({
    type: '',
    if (productData.value === null) {
        productData.value = []
    }
    productData.value.push({
    productCategory: '',
    specificationModel: '',
        productModelId: '',
    unit: '',
    amount: '',
    taxRate: '',
    taxPrice: '',
    taxTotal: '',
    contractAmount: '',
    operateDate: '',
    remark: '',
    taxInclusiveUnitPrice: '',
    quantity: '',
    taxInclusiveTotalPrice: '',
    taxExclusiveTotalPrice: '',
    invoiceType: ''
  });
};
const onConfirm = ({ selectedValues, selectedOptions }) => {
    form.value.salesman = selectedOptions[0]?.text;
    pickerValue.value = [selectedValues[0]];
    showPicker.value = false;
};
const onCustomerConfirm = ({ selectedValues, selectedOptions }) => {
    form.value.customerName = selectedOptions[0]?.text;
    form.value.customerId = selectedOptions[0]?.value;
    pickerCustomerValue.value = [selectedValues[0]];
    showCustomerPicker.value = false;
};
const onDateConfirm = ({ selectedValues }) => {
    form.value.executionDate = selectedValues.join('-');
    pickerDateValue.value = selectedValues;
    showDatePicker.value = false;
};
const removeProduct = (idx) => {
  products.value.splice(idx, 1);
    productData.value.splice(idx, 1);
};
const submitForm = () => {
  formRef.value.validate().then(() => {
    uni.showToast({ title: '保存成功', icon: 'success' });
  });
// æ˜¾ç¤ºé€‰æ‹©å™¨
const openCategoryPicker = (idx) => {
  currentProductIndex.value = idx;
  showCategoryPicker.value = true;
};
const openSpecificationPicker = (idx) => {
  currentProductIndex.value = idx;
  showSpecificationPicker.value = true;
};
const openTaxRatePicker = (idx) => {
  currentProductIndex.value = idx;
  showTaxRatePicker.value = true;
};
const openInvoiceTypePicker = (idx) => {
  currentProductIndex.value = idx;
  showInvoiceTypePicker.value = true;
};
// é€‰æ‹©å™¨ç¡®è®¤äº‹ä»¶
const onCategoryConfirm = (node) => {
    // èŽ·å–é€‰ä¸­çš„èŠ‚ç‚¹ä¿¡æ¯
    console.log('selected node---', node);
    // å­˜å‚¨é€‰ä¸­çš„节点,用于确认时获取数据
    selectedCategoryNode.value = node;
};
// ç¡®è®¤äº§å“å¤§ç±»é€‰æ‹©
const confirmCategorySelection = () => {
    if (selectedCategoryNode.value) {
        // è®¾ç½®é€‰ä¸­çš„产品大类
        productData.value[currentProductIndex.value].productCategory = selectedCategoryNode.value.label;
        const id = selectedCategoryNode.value.id
        // é‡ç½®é€‰ä¸­çš„节点
        selectedCategoryNode.value = null;
        productData.value[currentProductIndex.value].specificationModel = ''
        productData.value[currentProductIndex.value].productModelId = ''
        productData.value[currentProductIndex.value].pickerSpecificationValue = ['']
        getModels(id)
    }
    showCategoryPicker.value = false;
};
// èŽ·å–è§„æ ¼åž‹å·
const getModels = (value) => {
    modelList({ id: value }).then((res) => {
        modelOptions.value = res.map(user => ({
            text: user.model,
            value: user.id
        }));
    });
};
// é€‰æ‹©è§„格型号
const onSpecificationConfirm = ({ selectedValues, selectedOptions }) => {
    productData.value[currentProductIndex.value].specificationModel = selectedOptions[0]?.text;
    productData.value[currentProductIndex.value].productModelId = selectedOptions[0]?.value;
    productData.value[currentProductIndex.value].unit = selectedOptions[0]?.unit;
  pickerSpecificationValue.value = [selectedValues[0]];
  showSpecificationPicker.value = false;
};
// é€‰æ‹©ç¨Žçއ
const onTaxRateConfirm = ({ selectedValues, selectedOptions }) => {
    productData.value[currentProductIndex.value].taxRate = selectedOptions[0]?.value;
  pickerTaxRateValue.value = [selectedValues[0]];
  showTaxRatePicker.value = false;
    // if (isCalculating.value) return;
    const inclusiveTotalPrice = parseFloat(productData.value[currentProductIndex.value].taxInclusiveTotalPrice);
    const taxRate = parseFloat(productData.value[currentProductIndex.value].taxRate);
    if (!inclusiveTotalPrice || !taxRate) {
        return;
    }
    // isCalculating.value = true;
    // è®¡ç®—不含税总价
    productData.value[currentProductIndex.value].taxExclusiveTotalPrice =
        calculateTaxExclusiveTotalPrice(
            inclusiveTotalPrice,
            taxRate
        );
    // isCalculating.value = false;
};
const onInvoiceTypeConfirm = ({ selectedValues, selectedOptions }) => {
    productData.value[currentProductIndex.value].invoiceType = selectedOptions[0]?.text;
  pickerInvoiceTypeValue.value = [selectedValues[0]];
  showInvoiceTypePicker.value = false;
};
// æ ¼å¼åŒ–函数 - å›ºå®šä¸¤ä½å°æ•°
const formatTaxPrice = (idx) => {
  if (productData.value[idx].taxInclusiveUnitPrice) {
    const value = parseFloat(productData.value[idx].taxInclusiveUnitPrice);
    if (!isNaN(value)) {
            productData.value[idx].taxInclusiveUnitPrice = value.toFixed(2);
    }
  }
    if (!productData.value[currentProductIndex.value].taxRate) {
        uni.showToast({
            title: '请先选择税率',
            icon: 'none'
        });
        return;
    }
    const quantity = parseFloat(productData.value[currentProductIndex.value].quantity);
    const unitPrice = parseFloat(productData.value[currentProductIndex.value].taxInclusiveUnitPrice);
    if (!quantity || quantity <= 0 || !unitPrice) {
        return;
    }
    // è®¡ç®—含税总价
    productData.value[currentProductIndex.value].taxInclusiveTotalPrice = (unitPrice * quantity).toFixed(2);
    // å¦‚果有税率,计算不含税总价
    if (productData.value[currentProductIndex.value].taxRate) {
        productData.value[currentProductIndex.value].taxExclusiveTotalPrice =
            calculateTaxExclusiveTotalPrice(
                productData.value[currentProductIndex.value].taxInclusiveTotalPrice,
                productData.value[currentProductIndex.value].taxRate
            );
    }
};
// æ•°é‡è¾“入框失焦
const formatAmount = (idx) => {
  if (productData.value[idx].quantity) {
    const value = parseFloat(productData.value[idx].quantity);
    if (!isNaN(value)) {
            productData.value[idx].quantity = value.toFixed(2);
    }
  }
    if (!productData.value[currentProductIndex.value].taxRate) {
        uni.showToast({
            title: '请先选择税率',
            icon: 'none'
        });
        return;
    }
    const quantity = parseFloat(productData.value[currentProductIndex.value].quantity);
    const unitPrice = parseFloat(productData.value[currentProductIndex.value].taxInclusiveUnitPrice);
    if (!quantity || quantity <= 0 || !unitPrice) {
        return;
    }
    // è®¡ç®—含税总价
    productData.value[currentProductIndex.value].taxInclusiveTotalPrice = (unitPrice * quantity).toFixed(2);
    // å¦‚果有税率,计算不含税总价
    if (productData.value[currentProductIndex.value].taxRate) {
        productData.value[currentProductIndex.value].taxExclusiveTotalPrice =
            calculateTaxExclusiveTotalPrice(
                productData.value[currentProductIndex.value].taxInclusiveTotalPrice,
                productData.value[currentProductIndex.value].taxRate
            );
    }
};
// å«ç¨Žæ€»ä»·å¤±ç„¦ï¼Œæ ¹æ®å«ç¨Žæ€»ä»·è®¡ç®—含税单价和数量
const formatTaxTotal = (idx) => {
  if (productData.value[idx].taxInclusiveTotalPrice) {
    const value = parseFloat(productData.value[idx].taxInclusiveTotalPrice);
    if (!isNaN(value)) {
            productData.value[idx].taxInclusiveTotalPrice = value.toFixed(2);
    }
  }
    const totalPrice = parseFloat(productData.value[currentProductIndex.value].taxInclusiveTotalPrice);
    const quantity = parseFloat(productData.value[currentProductIndex.value].quantity);
    if (!totalPrice || !quantity || quantity <= 0) {
        return;
    }
    // è®¡ç®—含税单价 = å«ç¨Žæ€»ä»· / æ•°é‡
    productData.value[currentProductIndex.value].taxInclusiveUnitPrice = (totalPrice / quantity).toFixed(2);
    // å¦‚果有税率,计算不含税总价
    if (productData.value[currentProductIndex.value].taxRate) {
        productData.value[currentProductIndex.value].taxExclusiveTotalPrice =
            calculateTaxExclusiveTotalPrice(
                totalPrice,
                productData.value[currentProductIndex.value].taxRate
            );
    }
};
// ä¸å«ç¨Žæ€»ä»·å¤±ç„¦, æ ¹æ®ä¸å«ç¨Žæ€»ä»·è®¡ç®—含税单价和数量
const formatNoTaxTotal = (idx) => {
  if (productData.value[idx].taxExclusiveTotalPrice) {
    const value = parseFloat(productData.value[idx].taxExclusiveTotalPrice);
    if (!isNaN(value)) {
            productData.value[idx].taxExclusiveTotalPrice = value.toFixed(2);
    }
  }
    if (!productData.value[currentProductIndex.value].taxRate) {
        uni.showToast({
            title: '请先选择税率',
            icon: 'none'
        });
        return;
    }
    const exclusiveTotalPrice = parseFloat(productData.value[currentProductIndex.value].taxExclusiveTotalPrice);
    const quantity = parseFloat(productData.value[currentProductIndex.value].quantity);
    const taxRate = parseFloat(productData.value[currentProductIndex.value].taxRate);
    if (!exclusiveTotalPrice || !quantity || quantity <= 0 || !taxRate) {
        return;
    }
    // å…ˆè®¡ç®—含税总价 = ä¸å«ç¨Žæ€»ä»· / (1 - ç¨Žçއ/100)
    const taxRateDecimal = taxRate / 100;
    const inclusiveTotalPrice = exclusiveTotalPrice / (1 - taxRateDecimal);
    productData.value[currentProductIndex.value].taxInclusiveTotalPrice = inclusiveTotalPrice.toFixed(2);
    // è®¡ç®—含税单价 = å«ç¨Žæ€»ä»· / æ•°é‡
    productData.value[currentProductIndex.value].taxInclusiveUnitPrice = (inclusiveTotalPrice / quantity).toFixed(2);
};
const goBack = () => {
    // æ¸…理本地存储的数据
    uni.removeStorageSync('operationType');
    uni.removeStorageSync('editData');
    uni.navigateBack();
};
const onSubmit = () => {
    if (productData.value !== null && productData.value.length > 0) {
        form.value.productData = JSON.parse(JSON.stringify(productData.value));
    } else {
        uni.showToast({
            title: '请添加产品信息',
            icon: 'none'
        });
        return
    }
    form.value.type = 1;
    addOrUpdateSalesLedger(form.value).then((res) => {
        uni.showToast({
            title: '提交成功',
            icon: 'success',
        });
        goBack();
    });
};
const setUserInfo = () => {
    form.value.entryPerson = userStore.id;
    form.value.entryPersonName = userStore.name;
    // è®¾ç½®å½“天日期
    const today = new Date()
    const year = today.getFullYear()
    const month = String(today.getMonth() + 1).padStart(2, '0')
    const day = String(today.getDate()).padStart(2, '0')
    form.value.entryDate = `${year}-${month}-${day}`
    pickerDateValue.value = [year.toString(), month.toString(), day.toString()]
}
// å¡«å……表单数据(编辑模式)
const fillFormData = () => {
  if (!editData.value) return;
    getSalesLedgerWithProducts({ id: editData.value.id, type: 1 }).then((res) => {
        productData.value = res.productData;
    });
    console.log(editData.value)
  // å¡«å……基本信息
  form.value.salesContractNo = editData.value.salesContractNo || '';
  form.value.customerContractNo = editData.value.customerContractNo || '';
  form.value.customerName = editData.value.customerName || '';
  form.value.projectName = editData.value.projectName || '';
  form.value.executionDate = editData.value.executionDate || '';
  form.value.paymentMethod = editData.value.paymentMethod || '';
  form.value.salesman = editData.value.salesman || '';
  form.value.entryPerson = editData.value.entryPerson || '';
  form.value.entryPersonName = editData.value.entryPersonName || '';
  form.value.entryDate = editData.value.entryDate || '';
  form.value.id = editData.value.id || '';
  form.value.customerId = editData.value.customerId || '';
  // è®¾ç½®ä¸šåŠ¡å‘˜é€‰æ‹©å™¨çš„å€¼
  if (editData.value.salesman) {
    const salesmanIndex = userList.value.findIndex(user => user.text === editData.value.salesman);
    if (salesmanIndex !== -1) {
      pickerValue.value = [userList.value[salesmanIndex].value];
    }
  }
  // è®¾ç½®å®¢æˆ·é€‰æ‹©å™¨çš„值
  if (editData.value.customerName) {
    const customerIndex = customerOption.value.findIndex(customer => customer.text === editData.value.customerName);
    if (customerIndex !== -1) {
      pickerCustomerValue.value = [customerOption.value[customerIndex].value]
    }
  }
  // è®¾ç½®æ—¥æœŸé€‰æ‹©å™¨çš„值
  if (editData.value.executionDate) {
        pickerDateValue.value = editData.value.executionDate.split('-').map(num => parseInt(num, 10))
        console.log(pickerDateValue.value)
    }
};
const getUserList = () => {
    userListNoPage().then((res) => {
        // å°†ç”¨æˆ·æ•°æ®ç»„装成 picker éœ€è¦çš„æ ¼å¼
        userList.value = res.data.map(user => ({
            text: user.nickName,
            value: user.nickName
        }));
    })
}
const getCustomerList = () => {
    customerList().then((res) => {
        // å°†ç”¨æˆ·æ•°æ®ç»„装成 picker éœ€è¦çš„æ ¼å¼
        customerOption.value = res.map(item => ({
            text: item.customerName,
            value: item.id
        }));
    })
}
const convertIdToValue = (data) => {
    // å¦‚果传入的不是数组,则返回空数组
    if (!Array.isArray(data)) {
        return [];
    }
    // é€’归映射函数
    return data.map(item => {
        // åˆ›å»ºæ–°å¯¹è±¡ï¼Œæ˜ å°„字段
        const mappedItem = {
            label: item.label, // å…³é”®ï¼šå°† label æ˜ å°„为 text
            id: item.id,       // ä¿ç•™ id
        };
        // å¦‚果存在 children æ•°ç»„,则递归处理
        if (item.children && Array.isArray(item.children) && item.children.length > 0) {
            mappedItem.children = convertIdToValue(item.children);
        }
        return mappedItem;
    });
};
// èŽ·å–äº§å“å¤§ç±»tree数据
const getProductOptions = () => {
    productTreeList().then((res) => {
        productOptions.value = convertIdToValue(res);
    });
};
onMounted(() => {
    // èŽ·å–é¡µé¢å‚æ•°
    operationType.value = uni.getStorageSync('operationType') || '';
    // èŽ·å–äººå‘˜åˆ—è¡¨
    getUserList()
    // èŽ·å–å®¢æˆ·åˆ—è¡¨
    getCustomerList()
    // èŽ·å–äº§å“å¤§ç±»tree数据
    getProductOptions()
    // èµ‹å€¼é»˜è®¤ä¿¡æ¯
    if (operationType.value === 'add') {
        setUserInfo()
    }
    // èŽ·å–ç¼–è¾‘æ•°æ®å¹¶å¡«å……è¡¨å•
    const editDataStr = uni.getStorageSync('editData');
    if (editDataStr) {
        try {
            editData.value = JSON.parse(editDataStr);
            // å¦‚果是编辑模式,等待数据加载完成后填充表单数据
            if (operationType.value !== 'add' && editData.value) {
                // ä½¿ç”¨ nextTick ç¡®ä¿æ•°æ®åŠ è½½å®ŒæˆåŽå†å¡«å……
                setTimeout(() => {
                    fillFormData();
                }, 100);
            }
        } catch (error) {
            console.error('解析编辑数据失败:', error);
        }
    }
});
</script>
<style scoped lang="scss">
.account-detail {
  min-height: 100vh;
  background: #f8f9fa;
  padding-bottom: 80px;
  padding-bottom: 5rem;
}
.header {
  display: flex;
  align-items: center;
  background: #fff;
  padding: 16px 20px;
  border-bottom: 1px solid #f0f0f0;
  padding: 1rem 1.25rem;
  border-bottom: 0.0625rem solid #f0f0f0;
  position: sticky;
  top: 0;
  z-index: 100;
    /* å…¼å®¹ iOS åˆ˜æµ·/灵动岛安全区 */
    padding-top: env(safe-area-inset-top);
}
.title {
  flex: 1;
  text-align: center;
  font-size: 18px;
  font-size: 1.125rem;
  font-weight: 600;
  color: #333;
}
.form-section {
    margin-top: 16px;
    margin-top: 1rem;
}
.van-field {
    height: 56px;
    line-height: 36px;
    height: 3.4rem;
}
.van-cell {
    align-items: center;
}
.product-section {
  background: #fff;
  margin: 16px;
  border-radius: 16px;
  padding: 20px 16px 8px 16px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.04);
    margin-top: 1rem;
  padding: 1rem;
  box-shadow: 0 0.125rem 0.5rem rgba(0,0,0,0.04);
}
.section-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 12px;
  margin-bottom: 1rem;
}
.section-title {
  font-size: 16px;
  font-size: 1rem;
  font-weight: 600;
  color: #333;
}
.add-btn {
  background: #2979ff;
  color: #fff;
  border-radius: 8px;
  padding: 4px 16px;
  font-size: 14px;
  border-radius: 0.25rem;
}
.product-card {
  background: #f8f9fa;
  border-radius: 12px;
  padding: 12px;
  margin-bottom: 16px;
  box-shadow: 0 1px 4px rgba(41,121,255,0.06);
    background: #FFFFFF;
    box-shadow: 0 0 1.25rem 0 rgba(0,57,117,0.08);
    border-radius: 0.5rem 0.5rem 0.5rem 0.5rem;
  padding: 1rem 0.5rem 0 0.5rem;
  position: relative;
}
.product-row {
.product-header {
  display: flex;
  align-items: center;
  margin-bottom: 8px;
    justify-content: space-between;
  padding: 0 0.5rem 0.75rem 0.5rem;
  border-bottom: 0.0625rem solid #e8e8e8;
}
.product-label {
  min-width: 60px;
  color: #888;
  font-size: 13px;
.product-productCategory {
  margin-left: 0.5rem;
  font-size: 0.875rem;
  font-weight: 500;
  color: #333;
}
.del-row {
  justify-content: flex-end;
.info-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.75rem;
  margin-bottom: 1rem;
}
.info-item {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}
.info-label {
  font-size: 0.75rem;
  color: #666;
  font-weight: 400;
}
.info-value {
  font-size: 0.875rem;
  color: #333;
  font-weight: 500;
}
.info-value.highlight {
  color: #2979ff;
  font-weight: 600;
}
.product-form {
  margin-bottom: 1rem;
}
.del-btn {
  background: #ff4d4f;
  color: #fff;
  border-radius: 8px;
  padding: 4px 16px;
  font-size: 13px;
  margin-top: 4px;
  border-radius: 0.25rem;
}
.footer-btns {
  position: fixed;
@@ -254,26 +859,46 @@
  display: flex;
  justify-content: space-around;
  align-items: center;
  padding: 12px 0;
  box-shadow: 0 -2px 8px rgba(0,0,0,0.05);
  padding: 0.75rem 0;
  box-shadow: 0 -0.125rem 0.5rem rgba(0,0,0,0.05);
  z-index: 1000;
}
.cancel-btn {
    font-weight: 400;
    font-size: 16px;
    font-size: 1rem;
    color: #FFFFFF;
    width: 102px;
    width: 6.375rem;
    background: #C7C9CC;
    box-shadow: 0px 4px 10px 0px rgba(3,88,185,0.2);
    border-radius: 40px 40px 40px 40px;
    box-shadow: 0 0.25rem 0.625rem 0 rgba(3,88,185,0.2);
    border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
}
.save-btn {
    font-weight: 400;
    font-size: 16px;
    font-size: 1rem;
    color: #FFFFFF;
    width: 224px;
    width: 14rem;
    background: linear-gradient( 140deg, #00BAFF 0%, #006CFB 100%);
    box-shadow: 0px 4px 10px 0px rgba(3,88,185,0.2);
    border-radius: 40px 40px 40px 40px;
    box-shadow: 0 0.25rem 0.625rem 0 rgba(3,88,185,0.2);
    border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
}
.popup-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 1rem;
    background: #fff;
    position: sticky;
    top: 0;
    z-index: 10;
}
.cancelButton {
    color: #969799
}
.confirmButton {
    color: #1989FA
}
.u-tree {
    height: 13rem;
}
</style>
src/pages/sales/salesAccount/index.vue
@@ -1,14 +1,14 @@
<template>
    <view class="sales-account">
        <!-- é¡µé¢å¤´éƒ¨ -->
        <view class="page-header">
            <view class="header-left">
                <up-icon name="arrow-left" size="20" color="#333" @click="goBack"></up-icon>
            </view>
            <view class="header-center">
                <text class="page-title">销售台账</text>
            </view>
        </view>
        <van-nav-bar
            title="销售台账"
            left-text="返回"
            left-arrow
            @click-left="goBack"
            fixed
            placeholder
        />
        
        <!-- æœç´¢å’Œç­›é€‰åŒºåŸŸ -->
        <view class="search-filter-section">
@@ -29,7 +29,7 @@
        <!-- é”€å”®å°è´¦ç€‘布流 -->
        <view class="ledger-list" v-if="total > 0">
            <view v-for="(item, index) in ledgerList" :key="index">
                <view class="ledger-item" @click="handleItemClick(item)">
                <view class="ledger-item" @click="handleInfo('edit', item)">
                    <view class="item-header">
                        <view class="item-left">
                            <view class="document-icon">
@@ -50,7 +50,7 @@
                        </view>
                        <view class="detail-row">
                            <text class="detail-label">客户合同号</text>
                            <text class="detail-value highlight">{{ item.customerContractNo }}</text>
                            <text class="detail-value">{{ item.customerContractNo }}</text>
                        </view>
                        <view class="detail-row">
                            <text class="detail-label">业务员</text>
@@ -92,15 +92,18 @@
        </view>
        
        <!-- æµ®åŠ¨æ“ä½œæŒ‰é’® -->
        <view class="fab-button" @click="handleAdd">
        <view class="fab-button" @click="handleInfo('add')">
            <up-icon name="plus" size="24" color="#ffffff"></up-icon>
        </view>
    </view>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { ref } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import {ledgerListPage} from "@/api/salesManagement/salesLedger";
import useUserStore from "@/store/modules/user";
const userStore = useUserStore()
// æœç´¢å…³é”®è¯
const searchKeyword = ref('');
@@ -126,34 +129,59 @@
            // tableLoading.value = false;
    });
};
// æ˜¾ç¤ºç­›é€‰é€‰é¡¹
const showFilterOptions = () => {
    uni.showActionSheet({
        itemList: ['按日期筛选', '按状态筛选', '按金额筛选'],
        success: (res) => {
            console.log('选择了筛选选项:', res.tapIndex);
        }
    });
// å¤„理台账信息操作(查看/编辑/新增)
const handleInfo = (type, row) => {
  try {
    // è®¾ç½®æ“ä½œç±»åž‹
    uni.setStorageSync('operationType', type);
    // å¦‚果是查看或编辑操作
    if (type !== 'add') {
      // éªŒè¯è¡Œæ•°æ®æ˜¯å¦å­˜åœ¨
      if (!row) {
        uni.showToast({
          title: '数据不存在',
          icon: 'error'
        });
        return;
      }
      // æ£€æŸ¥æƒé™ï¼šåªæœ‰å½•入人才能编辑
      if (row.entryPerson != userStore.id) {
        // éžå½•入人跳转到只读详情页面
        uni.setStorageSync('editData', JSON.stringify(row));
        uni.navigateTo({
          url: '/pages/sales/salesAccount/view'
        });
        return;
      }
      // å½•入人编辑:存储数据并跳转到编辑页面
      uni.setStorageSync('editData', JSON.stringify(row));
      uni.navigateTo({
        url: '/pages/sales/salesAccount/detail'
      });
      return;
    }
    // æ–°å¢žæ“ä½œï¼šç›´æŽ¥è·³è½¬åˆ°ç¼–辑页面
    uni.navigateTo({
      url: '/pages/sales/salesAccount/detail'
    });
  } catch (error) {
    console.error('处理台账信息操作失败:', error);
    uni.showToast({
      title: '操作失败,请重试',
      icon: 'error'
    });
  }
};
// ç‚¹å‡»åˆ—表项
const handleItemClick = (item) => {
    uni.showToast({
        title: `查看合同: ${item.contractId}`,
        icon: 'none'
    });
};
// æ·»åŠ æ–°è®°å½•
const handleAdd = () => {
  uni.navigateTo({
    url: '/pages/sales/salesAccount/detail'
  });
};
onMounted(() => {
    // é¡µé¢åŠ è½½å®ŒæˆåŽçš„åˆå§‹åŒ–é€»è¾‘
    getList()
onShow(() => {
    // é¡µé¢æ˜¾ç¤ºæ—¶åˆ·æ–°åˆ—表
    getList();
});
</script>
src/pages/sales/salesAccount/view.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,315 @@
<template>
  <view class="account-view">
    <!-- é¡¶éƒ¨æ ‡é¢˜æ  -->
        <van-nav-bar
            title="台账详情"
            left-text="返回"
            left-arrow
            @click-left="goBack"
            fixed
            placeholder
        />
    <!-- åŸºæœ¬ä¿¡æ¯å±•示 -->
    <view class="info-section">
      <view class="section-title">基本信息</view>
      <view class="info-grid">
        <view class="info-item">
          <text class="info-label">销售合同号</text>
          <text class="info-value">{{ form.salesContractNo }}</text>
        </view>
        <view class="info-item">
          <text class="info-label">客户合同号</text>
          <text class="info-value highlight">{{ form.customerContractNo }}</text>
        </view>
        <view class="info-item">
          <text class="info-label">客户名称</text>
          <text class="info-value">{{ form.customerName }}</text>
        </view>
        <view class="info-item">
          <text class="info-label">业务员</text>
          <text class="info-value">{{ form.salesman }}</text>
        </view>
        <view class="info-item">
          <text class="info-label">项目名称</text>
          <text class="info-value">{{ form.projectName }}</text>
        </view>
        <view class="info-item">
          <text class="info-label">签订日期</text>
          <text class="info-value">{{ form.executionDate }}</text>
        </view>
        <view class="info-item">
          <text class="info-label">付款方式</text>
          <text class="info-value">{{ form.paymentMethod }}</text>
        </view>
        <view class="info-item">
          <text class="info-label">录入人</text>
          <text class="info-value">{{ form.entryPersonName }}</text>
        </view>
        <view class="info-item">
          <text class="info-label">录入日期</text>
          <text class="info-value">{{ form.entryDate }}</text>
        </view>
      </view>
    </view>
    <!-- äº§å“ä¿¡æ¯å±•示 -->
    <view class="product-section" v-if="productData && productData.length > 0">
      <view class="section-title">产品信息</view>
      <view class="product-card" v-for="(product, idx) in productData" :key="idx">
        <view class="product-header">
          <view class="product-title">
            <van-icon name="description" color="#2979ff" size="15" />
            <text class="product-productCategory">产品 {{ idx + 1 }}</text>
          </view>
        </view>
        <view class="product-info">
          <view class="info-grid">
            <view class="info-item">
              <text class="info-label">产品大类</text>
              <text class="info-value">{{ product.productCategory }}</text>
            </view>
            <view class="info-item">
              <text class="info-label">规格型号</text>
              <text class="info-value">{{ product.specificationModel }}</text>
            </view>
            <view class="info-item">
              <text class="info-label">单位</text>
              <text class="info-value">{{ product.unit }}</text>
            </view>
            <view class="info-item">
              <text class="info-label">税率(%)</text>
              <text class="info-value">{{ product.taxRate }}</text>
            </view>
            <view class="info-item">
              <text class="info-label">含税单价(元)</text>
              <text class="info-value highlight">{{ product.taxInclusiveUnitPrice }}</text>
            </view>
            <view class="info-item">
              <text class="info-label">数量</text>
              <text class="info-value highlight">{{ product.quantity }}</text>
            </view>
            <view class="info-item">
              <text class="info-label">含税总价(元)</text>
              <text class="info-value highlight">{{ product.taxInclusiveTotalPrice }}</text>
            </view>
            <view class="info-item">
              <text class="info-label">不含税总价(元)</text>
              <text class="info-value highlight">{{ product.taxExclusiveTotalPrice }}</text>
            </view>
            <view class="info-item">
              <text class="info-label">发票类型</text>
              <text class="info-value">{{ product.invoiceType }}</text>
            </view>
          </view>
        </view>
      </view>
    </view>
    <!-- æ— äº§å“ä¿¡æ¯æç¤º -->
    <view class="no-product" v-else>
      <text>暂无产品信息</text>
    </view>
  </view>
</template>
<script setup>
import { onMounted, ref } from 'vue';
import { getSalesLedgerWithProducts } from "@/api/salesManagement/salesLedger";
// è¡¨å•数据
const form = ref({
  id: '',
  salesContractNo: '',
  customerContractNo: '',
  customerId: '',
  customerName: '',
  projectName: '',
  executionDate: '',
  paymentMethod: '',
  entryPerson: '',
  entryPersonName: '',
  entryDate: '',
  salesman: ''
});
// äº§å“æ•°æ®
const productData = ref([]);
// ç¼–辑数据
const editData = ref(null);
// è¿”回上一页
const goBack = () => {
  // æ¸…理本地存储的数据
  uni.removeStorageSync('editData');
  uni.navigateBack();
};
// å¡«å……表单数据
const fillFormData = () => {
  if (!editData.value) return;
  // èŽ·å–å®Œæ•´çš„äº§å“ä¿¡æ¯
  getSalesLedgerWithProducts({ id: editData.value.id, type: 1 }).then((res) => {
    productData.value = res.productData || [];
  });
  // å¡«å……基本信息
  form.value.salesContractNo = editData.value.salesContractNo || '';
  form.value.customerContractNo = editData.value.customerContractNo || '';
  form.value.customerName = editData.value.customerName || '';
  form.value.projectName = editData.value.projectName || '';
  form.value.executionDate = editData.value.executionDate || '';
  form.value.paymentMethod = editData.value.paymentMethod || '';
  form.value.salesman = editData.value.salesman || '';
  form.value.entryPerson = editData.value.entryPerson || '';
  form.value.entryPersonName = editData.value.entryPersonName || '';
  form.value.entryDate = editData.value.entryDate || '';
  form.value.id = editData.value.id || '';
  form.value.customerId = editData.value.customerId || '';
};
onMounted(() => {
  // èŽ·å–ç¼–è¾‘æ•°æ®å¹¶å¡«å……è¡¨å•
  const editDataStr = uni.getStorageSync('editData');
  if (editDataStr) {
    try {
      editData.value = JSON.parse(editDataStr);
      // ä½¿ç”¨ nextTick ç¡®ä¿æ•°æ®åŠ è½½å®ŒæˆåŽå†å¡«å……
      setTimeout(() => {
        fillFormData();
      }, 100);
    } catch (error) {
      console.error('解析编辑数据失败:', error);
    }
  }
});
</script>
<style scoped lang="scss">
.account-view {
  min-height: 100vh;
  background: #f8f9fa;
  padding-bottom: 2rem;
}
.header {
  display: flex;
  align-items: center;
  background: #fff;
  padding: 1rem 1.25rem;
  border-bottom: 0.0625rem solid #f0f0f0;
  position: sticky;
  top: 0;
  z-index: 100;
  /* å…¼å®¹ iOS åˆ˜æµ·/灵动岛安全区 */
  padding-top: env(safe-area-inset-top);
}
.title {
  flex: 1;
  text-align: center;
  font-size: 1.125rem;
  font-weight: 600;
  color: #333;
}
.info-section {
  background: #fff;
  margin: 1rem;
  padding: 1rem;
  border-radius: 0.5rem;
  box-shadow: 0 0.125rem 0.5rem rgba(0,0,0,0.04);
}
.section-title {
  font-size: 1rem;
  font-weight: 600;
  color: #333;
  margin-bottom: 1rem;
  padding-bottom: 1rem;
  border-bottom: 0.0625rem solid #e8e8e8;
}
.info-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.75rem;
}
.info-item {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}
.info-label {
  font-size: 0.75rem;
  color: #666;
  font-weight: 400;
}
.info-value {
  font-size: 0.875rem;
  color: #333;
  font-weight: 500;
}
.info-value.highlight {
  color: #2979ff;
  font-weight: 600;
}
.product-section {
  background: #fff;
  margin: 1rem;
  padding: 1rem;
  border-radius: 0.5rem;
  box-shadow: 0 0.125rem 0.5rem rgba(0,0,0,0.04);
}
.product-card {
  background: #f8f9fa;
  border-radius: 0.5rem;
  padding: 1rem;
  margin-bottom: 1rem;
}
.product-card:last-child {
  margin-bottom: 0;
}
.product-header {
  display: flex;
  align-items: center;
  padding-bottom: 0.75rem;
  border-bottom: 0.0625rem solid #e8e8e8;
  margin-bottom: 1rem;
}
.product-title {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.product-productCategory {
  font-size: 0.875rem;
  font-weight: 500;
  color: #333;
}
.product-info .info-grid {
  grid-template-columns: 1fr 1fr;
  gap: 0.5rem;
}
.no-product {
  text-align: center;
  padding: 2rem;
  color: #999;
  font-size: 0.875rem;
}
</style>
src/utils/summarizeTable.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,57 @@
/**
 * é€šç”¨çš„表格合计方法
 * @param {Object} param - åŒ…含表格列配置和数据源的对象
 * @param {Array<string>} summaryProps - éœ€è¦æ±‡æ€»çš„字段名数组
 * @param {Object} specialFormat - ç‰¹æ®Šæ ¼å¼åŒ–规则:字段名 -> æ ¼å¼åŒ–选项(如是否去掉小数)
 * @returns {Array} åˆè®¡è¡Œæ•°æ®
 */
const summarizeTable = (param, summaryProps, specialFormat = {}) => {
  const { columns, data } = param;
  const sums = [];
  columns.forEach((column, index) => {
    if (index === 0) {
      sums[index] = "合计";
      return;
    }
    const prop = column.property;
    if (summaryProps.includes(prop)) {
      const values = data.map((item) => Number(item[prop]));
      // åªå¯¹æœ‰æ•ˆæ•°å­—进行求和
      if (!values.every(isNaN)) {
        const sum = values.reduce(
          (acc, val) => (!isNaN(val) ? acc + val : acc),
          0
        );
        if (specialFormat[prop] && specialFormat[prop].noDecimal) {
          // å¦‚果指定了不需要保留小数,则直接转换为整数
          sums[index] = Math.round(sum).toString();
        } else {
          // é»˜è®¤ä¿ç•™ä¸¤ä½å°æ•°
          sums[index] = parseFloat(sum).toFixed(
            specialFormat[prop]?.decimalPlaces ?? 2
          );
        }
      } else {
        sums[index] = "";
      }
    } else {
      sums[index] = "";
    }
  });
  return sums;
};
// ä¸å«ç¨Žæ€»ä»·è®¡ç®—
const calculateTaxExclusiveTotalPrice = (taxInclusiveTotalPrice, taxRate) => {
  const taxRateDecimal = taxRate / 100;
  return (taxInclusiveTotalPrice / (1 + taxRateDecimal)).toFixed(2);
};
// å«ç¨Žæ€»ä»·è®¡ç®—
const calculateTaxIncludeTotalPrice = (taxInclusiveUnitPrice, quantity) => {
  return (taxInclusiveUnitPrice * quantity).toFixed(2);
};
// å¯¼å‡ºå‡½æ•°ä¾›å…¶ä»–文件使用
export {
  summarizeTable,
  calculateTaxExclusiveTotalPrice,
  calculateTaxIncludeTotalPrice,
};