From 2d157a517d45b34acfdc0a540078d57c105877e5 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期五, 03 四月 2026 13:03:11 +0800
Subject: [PATCH] 升级app 1.添加客户档案、销售报价

---
 src/api/salesManagement/salesQuotation.js   |  112 +++
 src/pages.json                              |   42 +
 src/pages/basicData/customerFile/edit.vue   |  342 ++++++++++
 src/pages/works.vue                         |   18 
 src/api/basicData/customerFile.js           |   41 +
 src/pages/basicData/customerFile/detail.vue |  205 ++++++
 src/pages/basicData/customerFile/index.vue  |  183 +++++
 src/pages/sales/salesQuotation/detail.vue   |  235 ++++++
 src/pages/sales/salesQuotation/edit.vue     |  613 ++++++++++++++++++
 src/pages/sales/salesQuotation/index.vue    |  226 ++++++
 10 files changed, 2,017 insertions(+), 0 deletions(-)

diff --git a/src/api/basicData/customerFile.js b/src/api/basicData/customerFile.js
index c52b76e..409161a 100644
--- a/src/api/basicData/customerFile.js
+++ b/src/api/basicData/customerFile.js
@@ -50,3 +50,44 @@
     })
 }
 
+
+// 鏂板瀹㈡埛璺熻繘
+export function addCustomerFollow(data) {
+    return request({
+        url: '/basic/customer-follow/add',
+        method: 'post',
+        data: data
+    })
+}
+
+// 淇敼瀹㈡埛璺熻繘
+export function updateCustomerFollow(data) {
+    return request({
+        url: '/basic/customer-follow/edit',
+        method: 'put',
+        data: data,
+    })
+}
+// 鍒犻櫎瀹㈡埛璺熻繘
+export function delCustomerFollow(id) {
+    return request({
+        url: '/basic/customer-follow/'+id,
+        method: 'delete',
+    })
+}
+
+// 鍥炶鎻愰啋-鏂板/鏇存柊
+export function addReturnVisit(data) {
+    return request({
+        url: '/basic/customer-follow/return-visit',
+        method: 'post',
+        data: data
+    })
+}
+// 鑾峰彇鍥炶鎻愰啋璇︽儏
+export function getReturnVisit(id) {
+    return request({
+        url: '/basic/customer-follow/return-visit/' + id,
+        method: 'get'
+    })
+}
\ No newline at end of file
diff --git a/src/api/salesManagement/salesQuotation.js b/src/api/salesManagement/salesQuotation.js
new file mode 100644
index 0000000..4329dd9
--- /dev/null
+++ b/src/api/salesManagement/salesQuotation.js
@@ -0,0 +1,112 @@
+// 閿�鍞姤浠烽〉闈㈡帴鍙�
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ鎶ヤ环鍗曞垪琛�
+export function getQuotationList(query) {
+  return request({
+    url: "/sales/quotation/list",
+    method: "get",
+    params: query,
+  });
+}
+
+// 鏌ヨ鎶ヤ环鍗曡鎯�
+export function getQuotationDetail(query) {
+  return request({
+    url: "/sales/quotation/detail",
+    method: "get",
+    params: query,
+  });
+}
+
+// 鏂板鎶ヤ环鍗�
+export function addQuotation(data) {
+  return request({
+    url: "/sales/quotation/add",
+    method: "post",
+    data: data,
+  });
+}
+
+// 淇敼鎶ヤ环鍗�
+export function updateQuotation(data) {
+  return request({
+    url: "/sales/quotation/update",
+    method: "post",
+    data: data,
+  });
+}
+
+// 鍒犻櫎鎶ヤ环鍗�
+export function deleteQuotation(query) {
+  return request({
+    url: "/sales/quotation/delete",
+    method: "delete",
+    data: query,
+  });
+}
+
+// 鍙戦�佹姤浠峰崟
+export function sendQuotation(data) {
+  return request({
+    url: "/sales/quotation/send",
+    method: "post",
+    data: data,
+  });
+}
+
+// 鎶ヤ环鍗曡浆璁㈠崟
+export function convertToOrder(data) {
+  return request({
+    url: "/sales/quotation/convertToOrder",
+    method: "post",
+    data: data,
+  });
+}
+
+// 鏌ヨ瀹㈡埛鍒楄〃
+export function getCustomerList(query) {
+  return request({
+    url: "/basic/customer/list",
+    method: "get",
+    params: query,
+  });
+}
+
+// 鏌ヨ浜у搧鍒楄〃
+export function getProductList(query) {
+  return request({
+    url: "/basic/product/list",
+    method: "get",
+    params: query,
+  });
+}
+
+// 鏌ヨ涓氬姟鍛樺垪琛�
+export function getSalespersonList(query) {
+  return request({
+    url: "/system/user/salespersonList",
+    method: "get",
+    params: query,
+  });
+}
+
+// 瀵煎嚭鎶ヤ环鍗�
+export function exportQuotation(query) {
+  return request({
+    url: "/sales/quotation/export",
+    method: "get",
+    params: query,
+    responseType: "blob",
+  });
+}
+
+// 鎵撳嵃鎶ヤ环鍗�
+export function printQuotation(query) {
+  return request({
+    url: "/sales/quotation/print",
+    method: "get",
+    params: query,
+    responseType: "blob",
+  });
+}
diff --git a/src/pages.json b/src/pages.json
index b6eeda7..e52db8a 100644
--- a/src/pages.json
+++ b/src/pages.json
@@ -73,6 +73,27 @@
       }
     },
     {
+      "path": "pages/basicData/customerFile/index",
+      "style": {
+        "navigationBarTitleText": "瀹㈡埛妗f",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/basicData/customerFile/edit",
+      "style": {
+        "navigationBarTitleText": "瀹㈡埛淇℃伅",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/basicData/customerFile/detail",
+      "style": {
+        "navigationBarTitleText": "瀹㈡埛璇︽儏",
+        "navigationStyle": "custom"
+      }
+    },
+    {
       "path": "pages/sales/salesAccount/index",
       "style": {
         "navigationBarTitleText": "閿�鍞彴璐�",
@@ -80,6 +101,27 @@
       }
     },
     {
+      "path": "pages/sales/salesQuotation/index",
+      "style": {
+        "navigationBarTitleText": "閿�鍞姤浠�",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/sales/salesQuotation/edit",
+      "style": {
+        "navigationBarTitleText": "閿�鍞姤浠�",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/sales/salesQuotation/detail",
+      "style": {
+        "navigationBarTitleText": "鎶ヤ环璇︽儏",
+        "navigationStyle": "custom"
+      }
+    },
+    {
       "path": "pages/sales/salesAccount/out",
       "style": {
         "navigationBarTitleText": "鍙戣揣鐘舵��",
diff --git a/src/pages/basicData/customerFile/detail.vue b/src/pages/basicData/customerFile/detail.vue
new file mode 100644
index 0000000..37fa67a
--- /dev/null
+++ b/src/pages/basicData/customerFile/detail.vue
@@ -0,0 +1,205 @@
+<template>
+  <view class="customer-detail-page">
+    <PageHeader title="瀹㈡埛璇︽儏" @back="goBack" />
+
+    <view class="detail-content">
+      <view class="section">
+        <view class="section-title">瀹㈡埛淇℃伅</view>
+        <view class="info-list">
+          <view class="info-item">
+            <text class="info-label">瀹㈡埛鍚嶇О</text>
+            <text class="info-value">{{ detailData.customerName || "-" }}</text>
+          </view>
+          <view class="info-item">
+            <text class="info-label">瀹㈡埛鍒嗙被</text>
+            <text class="info-value">{{ detailData.customerType || "-" }}</text>
+          </view>
+          <view class="info-item">
+            <text class="info-label">绾崇◣浜鸿瘑鍒彿</text>
+            <text class="info-value">{{ detailData.taxpayerIdentificationNumber || "-" }}</text>
+          </view>
+          <view class="info-item">
+            <text class="info-label">鍏徃鍦板潃</text>
+            <text class="info-value">{{ detailData.companyAddress || "-" }}</text>
+          </view>
+          <view class="info-item">
+            <text class="info-label">鍏徃鐢佃瘽</text>
+            <text class="info-value">{{ detailData.companyPhone || "-" }}</text>
+          </view>
+          <view class="info-item">
+            <text class="info-label">娉曚汉</text>
+            <text class="info-value">{{ detailData.corporation || "-" }}</text>
+          </view>
+          <view class="info-item">
+            <text class="info-label">浠g悊浜�</text>
+            <text class="info-value">{{ detailData.agent || "-" }}</text>
+          </view>
+          <view class="info-item">
+            <text class="info-label">浼犵湡</text>
+            <text class="info-value">{{ detailData.fax || "-" }}</text>
+          </view>
+        </view>
+      </view>
+
+      <view class="section">
+        <view class="section-title">鑱旂郴淇℃伅</view>
+        <view class="info-list">
+          <view class="info-item">
+            <text class="info-label">鑱旂郴浜�</text>
+            <text class="info-value">{{ detailData.contactPerson || "-" }}</text>
+          </view>
+          <view class="info-item">
+            <text class="info-label">鑱旂郴鐢佃瘽</text>
+            <text class="info-value">{{ detailData.contactPhone || "-" }}</text>
+          </view>
+        </view>
+      </view>
+
+      <view class="section">
+        <view class="section-title">閾惰淇℃伅</view>
+        <view class="info-list">
+          <view class="info-item">
+            <text class="info-label">閾惰鍩烘湰鎴�</text>
+            <text class="info-value">{{ detailData.basicBankAccount || "-" }}</text>
+          </view>
+          <view class="info-item">
+            <text class="info-label">閾惰璐﹀彿</text>
+            <text class="info-value">{{ detailData.bankAccount || "-" }}</text>
+          </view>
+          <view class="info-item">
+            <text class="info-label">寮�鎴烽摱琛�</text>
+            <text class="info-value">{{ detailData.bankName || "-" }}</text>
+          </view>
+          <view class="info-item">
+            <text class="info-label">寮�鎴疯鍙�</text>
+            <text class="info-value">{{ detailData.bankCode || "-" }}</text>
+          </view>
+        </view>
+      </view>
+
+      <view class="section">
+        <view class="section-title">缁存姢淇℃伅</view>
+        <view class="info-list">
+          <view class="info-item">
+            <text class="info-label">缁存姢浜�</text>
+            <text class="info-value">{{ detailData.maintainer || "-" }}</text>
+          </view>
+          <view class="info-item">
+            <text class="info-label">缁存姢鏃堕棿</text>
+            <text class="info-value">{{ detailData.maintenanceTime || "-" }}</text>
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <FooterButtons
+      cancelText="杩斿洖"
+      confirmText="缂栬緫"
+      @cancel="goBack"
+      @confirm="goEdit"
+    />
+  </view>
+</template>
+
+<script setup>
+  import { ref } from "vue";
+  import { onLoad, onShow } from "@dcloudio/uni-app";
+  import FooterButtons from "@/components/FooterButtons.vue";
+  import { getCustomer } from "@/api/basicData/customerFile";
+
+  const customerId = ref("");
+  const detailData = ref({});
+
+  const goBack = () => {
+    uni.navigateBack();
+  };
+
+  const goEdit = () => {
+    if (!customerId.value) return;
+    uni.navigateTo({ url: `/pages/basicData/customerFile/edit?id=${customerId.value}` });
+  };
+
+  const getDetail = () => {
+    if (!customerId.value) return;
+    uni.showLoading({ title: "鍔犺浇涓�...", mask: true });
+    getCustomer(customerId.value)
+      .then(res => {
+        detailData.value = res.data || {};
+      })
+      .catch(() => {
+        uni.showToast({ title: "鑾峰彇璇︽儏澶辫触", icon: "error" });
+      })
+      .finally(() => {
+        uni.hideLoading();
+      });
+  };
+
+  onLoad(options => {
+    if (options?.id) {
+      customerId.value = options.id;
+      getDetail();
+    }
+  });
+
+  onShow(() => {
+    if (customerId.value) {
+      getDetail();
+    }
+  });
+</script>
+
+<style scoped lang="scss">
+  .customer-detail-page {
+    min-height: 100vh;
+    background-color: #f5f5f5;
+    padding-bottom: 90px;
+  }
+
+  .detail-content {
+    padding: 16px;
+  }
+
+  .section {
+    background: #ffffff;
+    border-radius: 12px;
+    margin-bottom: 16px;
+    overflow: hidden;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+  }
+
+  .section-title {
+    padding: 16px;
+    font-size: 16px;
+    font-weight: 600;
+    color: #303133;
+    border-bottom: 1px solid #f0f0f0;
+  }
+
+  .info-list {
+    padding: 8px 0;
+  }
+
+  .info-item {
+    display: flex;
+    padding: 12px 16px;
+    border-bottom: 1px solid #f8f8f8;
+  }
+
+  .info-item:last-child {
+    border-bottom: none;
+  }
+
+  .info-label {
+    width: 120px;
+    font-size: 14px;
+    color: #606266;
+  }
+
+  .info-value {
+    flex: 1;
+    font-size: 14px;
+    color: #303133;
+    text-align: right;
+    word-break: break-all;
+  }
+</style>
diff --git a/src/pages/basicData/customerFile/edit.vue b/src/pages/basicData/customerFile/edit.vue
new file mode 100644
index 0000000..c10973e
--- /dev/null
+++ b/src/pages/basicData/customerFile/edit.vue
@@ -0,0 +1,342 @@
+<template>
+  <view class="account-detail">
+    <PageHeader :title="pageTitle" @back="goBack" />
+
+    <view class="form-container">
+      <up-form
+        ref="formRef"
+        :model="form"
+        :rules="rules"
+        label-width="110"
+        input-align="right"
+        error-message-align="right"
+      >
+        <u-cell-group title="瀹㈡埛淇℃伅" class="form-section">
+          <up-form-item label="瀹㈡埛鍚嶇О" prop="customerName" required>
+            <up-input
+              v-model="form.customerName"
+              placeholder="璇疯緭鍏ュ鎴峰悕绉�"
+              clearable
+            />
+          </up-form-item>
+          <up-form-item label="瀹㈡埛鍒嗙被" prop="customerType" required>
+            <up-input
+              v-model="customerTypeText"
+              placeholder="璇烽�夋嫨瀹㈡埛鍒嗙被"
+              readonly
+              @click="showCustomerTypeSheet = true"
+            />
+            <template #right>
+              <up-icon name="arrow-right" @click="showCustomerTypeSheet = true"></up-icon>
+            </template>
+          </up-form-item>
+          <up-form-item
+            label="绾崇◣浜鸿瘑鍒彿"
+            prop="taxpayerIdentificationNumber"
+          >
+            <up-input
+              v-model="form.taxpayerIdentificationNumber"
+              placeholder="璇疯緭鍏ョ撼绋庝汉璇嗗埆鍙�"
+              clearable
+            />
+          </up-form-item>
+          <up-form-item label="鍏徃鍦板潃" prop="companyAddress">
+            <up-input
+              v-model="form.companyAddress"
+              placeholder="璇疯緭鍏ュ叕鍙稿湴鍧�"
+              clearable
+            />
+          </up-form-item>
+          <up-form-item label="鍏徃鐢佃瘽" prop="companyPhone">
+            <up-input
+              v-model="form.companyPhone"
+              placeholder="璇疯緭鍏ュ叕鍙哥數璇�"
+              clearable
+            />
+          </up-form-item>
+          <up-form-item label="娉曚汉" prop="corporation">
+            <up-input
+              v-model="form.corporation"
+              placeholder="璇疯緭鍏ユ硶浜�"
+              clearable
+            />
+          </up-form-item>
+          <up-form-item label="浠g悊浜�" prop="agent">
+            <up-input
+              v-model="form.agent"
+              placeholder="璇疯緭鍏ヤ唬鐞嗕汉"
+              clearable
+            />
+          </up-form-item>
+          <up-form-item label="浼犵湡" prop="fax">
+            <up-input
+              v-model="form.fax"
+              placeholder="璇疯緭鍏ヤ紶鐪�"
+              clearable
+            />
+          </up-form-item>
+        </u-cell-group>
+
+        <u-cell-group title="鑱旂郴淇℃伅" class="form-section">
+          <up-form-item label="鑱旂郴浜�" prop="contactPerson">
+            <up-input
+              v-model="form.contactPerson"
+              placeholder="璇疯緭鍏ヨ仈绯讳汉"
+              clearable
+            />
+          </up-form-item>
+          <up-form-item label="鑱旂郴鐢佃瘽" prop="contactPhone">
+            <up-input
+              v-model="form.contactPhone"
+              placeholder="璇疯緭鍏ヨ仈绯荤數璇�"
+              clearable
+            />
+          </up-form-item>
+        </u-cell-group>
+
+        <u-cell-group title="閾惰淇℃伅" class="form-section">
+          <up-form-item label="閾惰鍩烘湰鎴�" prop="basicBankAccount">
+            <up-input
+              v-model="form.basicBankAccount"
+              placeholder="璇疯緭鍏ラ摱琛屽熀鏈埛"
+              clearable
+            />
+          </up-form-item>
+          <up-form-item label="閾惰璐﹀彿" prop="bankAccount">
+            <up-input
+              v-model="form.bankAccount"
+              placeholder="璇疯緭鍏ラ摱琛岃处鍙�"
+              clearable
+            />
+          </up-form-item>
+          <up-form-item label="寮�鎴烽摱琛�" prop="bankName">
+            <up-input
+              v-model="form.bankName"
+              placeholder="璇疯緭鍏ュ紑鎴烽摱琛�"
+              clearable
+            />
+          </up-form-item>
+          <up-form-item label="寮�鎴疯鍙�" prop="bankCode">
+            <up-input
+              v-model="form.bankCode"
+              placeholder="璇疯緭鍏ュ紑鎴疯鍙�"
+              clearable
+            />
+          </up-form-item>
+        </u-cell-group>
+
+        <u-cell-group title="缁存姢淇℃伅" class="form-section">
+          <up-form-item label="缁存姢浜�" prop="maintainer">
+            <up-input
+              v-model="form.maintainer"
+              disabled
+              placeholder="鑷姩濉厖"
+            />
+          </up-form-item>
+          <up-form-item label="缁存姢鏃堕棿" prop="maintenanceTime">
+            <up-input
+              v-model="form.maintenanceTime"
+              disabled
+              placeholder="鑷姩濉厖"
+            />
+          </up-form-item>
+        </u-cell-group>
+      </up-form>
+    </view>
+
+    <FooterButtons
+      :loading="loading"
+      confirmText="淇濆瓨"
+      @cancel="goBack"
+      @confirm="handleSubmit"
+    />
+
+    <up-action-sheet
+      :show="showCustomerTypeSheet"
+      title="閫夋嫨瀹㈡埛鍒嗙被"
+      :actions="customerTypeActions"
+      @select="onSelectCustomerType"
+      @close="showCustomerTypeSheet = false"
+    />
+  </view>
+</template>
+
+<script setup>
+  import { computed, onMounted, ref } from "vue";
+  import { onLoad } from "@dcloudio/uni-app";
+  import FooterButtons from "@/components/FooterButtons.vue";
+  import useUserStore from "@/store/modules/user";
+  import { formatDateToYMD } from "@/utils/ruoyi";
+  import { addCustomer, getCustomer, updateCustomer } from "@/api/basicData/customerFile";
+
+  const userStore = useUserStore();
+  const formRef = ref();
+  const loading = ref(false);
+  const customerId = ref("");
+  const showCustomerTypeSheet = ref(false);
+
+  const form = ref({
+    customerName: "",
+    customerType: "",
+    taxpayerIdentificationNumber: "",
+    companyAddress: "",
+    companyPhone: "",
+    corporation: "",
+    agent: "",
+    fax: "",
+    contactPerson: "",
+    contactPhone: "",
+    basicBankAccount: "",
+    bankAccount: "",
+    bankName: "",
+    bankCode: "",
+    maintainer: "",
+    maintenanceTime: "",
+  });
+
+  const rules = {
+    customerName: [{ required: true, message: "璇疯緭鍏ュ鎴峰悕绉�", trigger: "blur" }],
+    customerType: [{ required: true, message: "璇烽�夋嫨瀹㈡埛鍒嗙被", trigger: "change" }],
+  };
+
+  const customerTypeActions = [
+    { name: "闆跺敭瀹㈡埛", value: "闆跺敭瀹㈡埛" },
+    { name: "缁忛攢鍟嗗鎴�", value: "缁忛攢鍟嗗鎴�" },
+  ];
+
+  const pageTitle = computed(() =>
+    customerId.value ? "缂栬緫瀹㈡埛" : "鏂板瀹㈡埛"
+  );
+  const customerTypeText = computed(() => form.value.customerType || "");
+
+  const goBack = () => {
+    uni.navigateBack();
+  };
+
+  const initForAdd = () => {
+    form.value.maintainer = userStore.nickName || "";
+    form.value.maintenanceTime = formatDateToYMD(Date.now());
+  };
+
+  const loadDetail = () => {
+    if (!customerId.value) return;
+    uni.showLoading({ title: "鍔犺浇涓�...", mask: true });
+    getCustomer(customerId.value)
+      .then(res => {
+        form.value = { ...form.value, ...(res.data || {}) };
+      })
+      .catch(() => {
+        uni.showToast({ title: "鑾峰彇璇︽儏澶辫触", icon: "error" });
+      })
+      .finally(() => {
+        uni.hideLoading();
+      });
+  };
+
+  const onSelectCustomerType = action => {
+    form.value.customerType = action.value;
+    showCustomerTypeSheet.value = false;
+  };
+
+  const handleSubmit = async () => {
+    const valid = await formRef.value.validate().catch(() => false);
+    if (!valid) return;
+
+    loading.value = true;
+    const action = customerId.value ? updateCustomer : addCustomer;
+    action({ ...form.value, id: customerId.value || undefined })
+      .then(() => {
+        uni.showToast({ title: "淇濆瓨鎴愬姛", icon: "success" });
+        setTimeout(() => {
+          uni.navigateBack();
+        }, 300);
+      })
+      .catch(() => {
+        uni.showToast({ title: "淇濆瓨澶辫触", icon: "error" });
+      })
+      .finally(() => {
+        loading.value = false;
+      });
+  };
+
+  onMounted(async () => {
+    if (!userStore.nickName) {
+      await userStore.getInfo().catch(() => null);
+    }
+    if (!customerId.value) {
+      initForAdd();
+    }
+  });
+
+  onLoad(options => {
+    if (options?.id) {
+      customerId.value = options.id;
+      loadDetail();
+    }
+  });
+</script>
+
+<style scoped lang="scss">
+  @import "@/static/scss/form-common.scss";
+
+  .account-detail {
+    min-height: 100vh;
+    background: #f8f9fa;
+    padding-bottom: 100px;
+  }
+
+  .form-container {
+    padding: 12px 12px 0;
+  }
+
+  .hero-card {
+    margin-bottom: 12px;
+    padding: 18px 18px 16px;
+    border-radius: 16px;
+    background: linear-gradient(135deg, #f4f8ff 0%, #ffffff 100%);
+    box-shadow: 0 6px 18px rgba(41, 121, 255, 0.08);
+  }
+
+  .hero-title {
+    display: block;
+    font-size: 18px;
+    font-weight: 600;
+    color: #1f2d3d;
+    margin-bottom: 6px;
+  }
+
+  .hero-desc {
+    display: block;
+    font-size: 13px;
+    line-height: 1.6;
+    color: #7a8599;
+  }
+
+  .form-section {
+    margin-bottom: 12px;
+    border-radius: 12px;
+    overflow: hidden;
+    box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
+  }
+
+  :deep(.u-cell-group__title) {
+    padding: 14px 18px 10px !important;
+    font-size: 15px !important;
+    font-weight: 600 !important;
+    color: #22324d !important;
+    background: #f8fbff !important;
+  }
+
+  :deep(.u-form-item__content__slot) {
+    flex: 1;
+  }
+
+  :deep(.u-input__content) {
+    justify-content: flex-end;
+  }
+
+  :deep(.u-input__content__field-wrapper__field),
+  :deep(.u-input__input) {
+    text-align: right !important;
+  }
+</style>
diff --git a/src/pages/basicData/customerFile/index.vue b/src/pages/basicData/customerFile/index.vue
new file mode 100644
index 0000000..1d6fb9d
--- /dev/null
+++ b/src/pages/basicData/customerFile/index.vue
@@ -0,0 +1,183 @@
+<template>
+  <view class="sales-account">
+    <PageHeader title="瀹㈡埛妗f" @back="goBack" />
+
+    <view class="search-section">
+      <view class="search-bar">
+        <view class="search-input">
+          <up-input
+            class="search-text"
+            v-model="customerName"
+            placeholder="璇疯緭鍏ュ鎴峰悕绉�"
+            clearable
+            @change="getList"
+          />
+        </view>
+        <view class="filter-button" @click="getList">
+          <up-icon name="search" size="24" color="#999999"></up-icon>
+        </view>
+      </view>
+    </view>
+
+    <view class="tabs-section">
+      <up-tabs
+        v-model="tabValue"
+        :list="tabList"
+        itemStyle="width: 33.33%;height: 80rpx;"
+        @change="onTabChange"
+      />
+    </view>
+
+    <view v-if="list.length > 0" class="ledger-list">
+      <view v-for="item in list" :key="item.id" class="ledger-item">
+        <view class="item-header">
+          <view class="item-left">
+            <view class="document-icon">
+              <up-icon name="account-fill" size="16" color="#ffffff"></up-icon>
+            </view>
+            <text class="item-id">{{ item.customerName || "-" }}</text>
+          </view>
+          <text class="item-index">{{ item.customerType || "-" }}</text>
+        </view>
+
+        <up-divider></up-divider>
+
+        <view class="item-details">
+          <view class="detail-row">
+            <text class="detail-label">绾崇◣浜鸿瘑鍒彿</text>
+            <text class="detail-value">{{ item.taxpayerIdentificationNumber || "-" }}</text>
+          </view>
+          <view class="detail-row">
+            <text class="detail-label">鍏徃鐢佃瘽</text>
+            <text class="detail-value">{{ item.companyPhone || "-" }}</text>
+          </view>
+          <view class="detail-row">
+            <text class="detail-label">鍏徃鍦板潃</text>
+            <text class="detail-value">{{ item.companyAddress || "-" }}</text>
+          </view>
+          <view class="detail-row">
+            <text class="detail-label">娉曚汉</text>
+            <text class="detail-value">{{ item.corporation || "-" }}</text>
+          </view>
+          <view class="detail-row">
+            <text class="detail-label">浠g悊浜�</text>
+            <text class="detail-value">{{ item.agent || "-" }}</text>
+          </view>
+          <view class="detail-row">
+            <text class="detail-label">缁存姢浜�</text>
+            <text class="detail-value">{{ item.maintainer || "-" }}</text>
+          </view>
+          <view class="detail-row">
+            <text class="detail-label">缁存姢鏃堕棿</text>
+            <text class="detail-value">{{ item.maintenanceTime || "-" }}</text>
+          </view>
+        </view>
+
+        <view class="action-buttons">
+					<up-button class="action-btn" size="small" type="primary" @click="goEdit(item)"
+					>缂栬緫</up-button
+					>
+          <up-button class="action-btn" size="small" @click="goDetail(item)"
+            >璇︽儏</up-button
+          >
+        </view>
+      </view>
+    </view>
+
+    <view v-else class="no-data">
+      <text>鏆傛棤瀹㈡埛妗f鏁版嵁</text>
+    </view>
+
+    <view class="fab-button" @click="goAdd">
+      <up-icon name="plus" size="28" color="#ffffff"></up-icon>
+    </view>
+  </view>
+</template>
+
+<script setup>
+  import { reactive, ref } from "vue";
+  import { onShow } from "@dcloudio/uni-app";
+  import { listCustomer } from "@/api/basicData/customerFile";
+
+  const customerName = ref("");
+  const list = ref([]);
+
+  const tabList = reactive([
+    { name: "鍏ㄩ儴瀹㈡埛", value: "" },
+    { name: "闆跺敭瀹㈡埛", value: "闆跺敭瀹㈡埛" },
+    { name: "缁忛攢鍟嗗鎴�", value: "缁忛攢鍟嗗鎴�" },
+  ]);
+  const tabValue = ref(0);
+
+  const page = {
+    current: -1,
+    size: -1,
+  };
+
+  const goBack = () => {
+    uni.navigateBack();
+  };
+
+  const goAdd = () => {
+    uni.navigateTo({ url: "/pages/basicData/customerFile/edit" });
+  };
+
+  const goEdit = item => {
+    uni.navigateTo({ url: `/pages/basicData/customerFile/edit?id=${item.id}` });
+  };
+
+  const goDetail = item => {
+    uni.navigateTo({ url: `/pages/basicData/customerFile/detail?id=${item.id}` });
+  };
+
+  const onTabChange = val => {
+    tabValue.value = val.index;
+    getList();
+  };
+
+  const getCurrentCustomerType = () => {
+    const currentTab = tabList[tabValue.value];
+    return currentTab?.value || "";
+  };
+
+  const getList = () => {
+    uni.showLoading({ title: "鍔犺浇涓�...", mask: true });
+    listCustomer({
+      ...page,
+      customerName: customerName.value,
+      customerType: getCurrentCustomerType(),
+    })
+      .then(res => {
+        list.value = res?.records || res?.data?.records || [];
+      })
+      .catch(() => {
+        uni.showToast({ title: "鏌ヨ澶辫触", icon: "error" });
+      })
+      .finally(() => {
+        uni.hideLoading();
+      });
+  };
+
+  onShow(() => {
+    getList();
+  });
+</script>
+
+<style scoped lang="scss">
+  @import "@/styles/procurement-common.scss";
+
+  .tabs-section {
+    background: #ffffff;
+    padding: 0 12px 8px 12px;
+  }
+
+  .item-index {
+    max-width: 180rpx;
+    text-align: center;
+  }
+
+  .detail-value {
+    max-width: 70%;
+    word-break: break-all;
+  }
+</style>
diff --git a/src/pages/sales/salesQuotation/detail.vue b/src/pages/sales/salesQuotation/detail.vue
new file mode 100644
index 0000000..a273974
--- /dev/null
+++ b/src/pages/sales/salesQuotation/detail.vue
@@ -0,0 +1,235 @@
+<template>
+  <view class="customer-detail-page">
+    <PageHeader title="鎶ヤ环璇︽儏" @back="goBack" />
+
+    <view class="detail-content">
+      <view class="section">
+        <view class="section-title">鍩虹淇℃伅</view>
+        <view class="info-list">
+          <view class="info-item">
+            <text class="info-label">鎶ヤ环鍗曞彿</text>
+            <text class="info-value">{{ detailData.quotationNo || "-" }}</text>
+          </view>
+          <view class="info-item">
+            <text class="info-label">瀹㈡埛鍚嶇О</text>
+            <text class="info-value">{{ detailData.customer || "-" }}</text>
+          </view>
+          <view class="info-item">
+            <text class="info-label">涓氬姟鍛�</text>
+            <text class="info-value">{{ detailData.salesperson || "-" }}</text>
+          </view>
+          <view class="info-item">
+            <text class="info-label">鎶ヤ环鏃ユ湡</text>
+            <text class="info-value">{{ detailData.quotationDate || "-" }}</text>
+          </view>
+          <view class="info-item">
+            <text class="info-label">鏈夋晥鏈熻嚦</text>
+            <text class="info-value">{{ detailData.validDate || "-" }}</text>
+          </view>
+          <view class="info-item">
+            <text class="info-label">浠樻鏂瑰紡</text>
+            <text class="info-value">{{ detailData.paymentMethod || "-" }}</text>
+          </view>
+          <view class="info-item">
+            <text class="info-label">瀹℃壒鐘舵��</text>
+            <text class="info-value">{{ detailData.status || "-" }}</text>
+          </view>
+          <view class="info-item">
+            <text class="info-label">鎶ヤ环鎬婚</text>
+            <text class="info-value highlight">{{ formatAmount(detailData.totalAmount) }}</text>
+          </view>
+          <view class="info-item">
+            <text class="info-label">澶囨敞</text>
+            <text class="info-value">{{ detailData.remark || "-" }}</text>
+          </view>
+        </view>
+      </view>
+
+      <view class="section">
+        <view class="section-title">瀹℃壒鑺傜偣</view>
+        <view v-if="approverNames.length" class="info-list">
+          <view v-for="(name, index) in approverNames" :key="index" class="info-item">
+            <text class="info-label">瀹℃壒鑺傜偣 {{ index + 1 }}</text>
+            <text class="info-value">{{ name }}</text>
+          </view>
+        </view>
+        <view v-else class="empty-box">
+          <text>鏆傛棤瀹℃壒鑺傜偣</text>
+        </view>
+      </view>
+
+      <view class="section">
+        <view class="section-title">浜у搧鏄庣粏</view>
+        <view v-if="detailData.products && detailData.products.length > 0" class="product-list">
+          <view v-for="(item, index) in detailData.products" :key="index" class="product-card">
+            <view class="product-head">浜у搧 {{ index + 1 }}</view>
+            <view class="info-item">
+              <text class="info-label">浜у搧鍚嶇О</text>
+              <text class="info-value">{{ item.product || item.productName || "-" }}</text>
+            </view>
+            <view class="info-item">
+              <text class="info-label">瑙勬牸鍨嬪彿</text>
+              <text class="info-value">{{ item.specification || "-" }}</text>
+            </view>
+            <view class="info-item">
+              <text class="info-label">鍗曚綅</text>
+              <text class="info-value">{{ item.unit || "-" }}</text>
+            </view>
+            <view class="info-item">
+              <text class="info-label">鏁伴噺</text>
+              <text class="info-value">{{ item.quantity || "-" }}</text>
+            </view>
+            <view class="info-item">
+              <text class="info-label">鍗曚环</text>
+              <text class="info-value">{{ formatAmount(item.unitPrice) }}</text>
+            </view>
+            <view class="info-item">
+              <text class="info-label">閲戦</text>
+              <text class="info-value highlight">{{ formatAmount(item.amount) }}</text>
+            </view>
+          </view>
+        </view>
+        <view v-else class="empty-box">
+          <text>鏆傛棤浜у搧鏄庣粏</text>
+        </view>
+      </view>
+    </view>
+
+    <FooterButtons cancelText="杩斿洖" confirmText="缂栬緫" @cancel="goBack" @confirm="goEdit" />
+  </view>
+</template>
+
+<script setup>
+  import { computed, ref } from "vue";
+  import { onLoad, onShow } from "@dcloudio/uni-app";
+  import FooterButtons from "@/components/FooterButtons.vue";
+  import PageHeader from "@/components/PageHeader.vue";
+
+  const quotationId = ref("");
+  const detailData = ref({});
+
+  const approverNames = computed(() => {
+    const approverText = detailData.value.approveUserNames || detailData.value.approverNames || detailData.value.approveUserIds || "";
+    if (Array.isArray(approverText)) return approverText.filter(Boolean);
+    return String(approverText)
+      .split(",")
+      .map(item => item.trim())
+      .filter(Boolean);
+  });
+
+  const goBack = () => {
+    uni.navigateBack();
+  };
+
+  const goEdit = () => {
+    if (!quotationId.value) return;
+    uni.navigateTo({ url: `/pages/sales/salesQuotation/edit?id=${quotationId.value}` });
+  };
+
+  const formatAmount = amount => `楼${Number(amount || 0).toFixed(2)}`;
+
+  const loadDetailFromStorage = () => {
+    const cachedData = uni.getStorageSync("salesQuotationDetail");
+    detailData.value = cachedData || {};
+  };
+
+  onLoad(options => {
+    if (options?.id) {
+      quotationId.value = options.id;
+    }
+    loadDetailFromStorage();
+  });
+
+  onShow(() => {
+    loadDetailFromStorage();
+  });
+</script>
+
+<style scoped lang="scss">
+  .customer-detail-page {
+    min-height: 100vh;
+    background-color: #f5f5f5;
+    padding-bottom: 90px;
+  }
+
+  .detail-content {
+    padding: 16px;
+  }
+
+  .section {
+    background: #ffffff;
+    border-radius: 12px;
+    margin-bottom: 16px;
+    overflow: hidden;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+  }
+
+  .section-title {
+    padding: 16px;
+    font-size: 16px;
+    font-weight: 600;
+    color: #303133;
+    border-bottom: 1px solid #f0f0f0;
+  }
+
+  .info-list {
+    padding: 8px 0;
+  }
+
+  .info-item {
+    display: flex;
+    padding: 12px 16px;
+    border-bottom: 1px solid #f8f8f8;
+  }
+
+  .info-item:last-child {
+    border-bottom: none;
+  }
+
+  .info-label {
+    width: 120px;
+    font-size: 14px;
+    color: #606266;
+  }
+
+  .info-value {
+    flex: 1;
+    font-size: 14px;
+    color: #303133;
+    text-align: right;
+    word-break: break-all;
+  }
+
+  .highlight {
+    color: #2979ff;
+    font-weight: 600;
+  }
+
+  .empty-box {
+    padding: 20px 16px;
+    font-size: 14px;
+    color: #999;
+    text-align: center;
+  }
+
+  .product-list {
+    padding: 12px;
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+  }
+
+  .product-card {
+    background: #f9fafc;
+    border-radius: 10px;
+    overflow: hidden;
+  }
+
+  .product-head {
+    padding: 12px 16px;
+    font-size: 14px;
+    font-weight: 600;
+    color: #22324d;
+    border-bottom: 1px solid #eef2f7;
+  }
+</style>
diff --git a/src/pages/sales/salesQuotation/edit.vue b/src/pages/sales/salesQuotation/edit.vue
new file mode 100644
index 0000000..940a8d9
--- /dev/null
+++ b/src/pages/sales/salesQuotation/edit.vue
@@ -0,0 +1,613 @@
+<template>
+  <view class="account-detail">
+    <PageHeader :title="pageTitle" @back="goBack" />
+
+    <view class="form-container">
+      <up-form
+        ref="formRef"
+        :model="form"
+        :rules="rules"
+        label-width="110"
+        input-align="right"
+        error-message-align="right"
+      >
+        <u-cell-group title="鍩虹淇℃伅" class="form-section">
+          <up-form-item label="瀹㈡埛鍚嶇О" prop="customer" required>
+            <up-input v-model="form.customer" placeholder="璇烽�夋嫨瀹㈡埛" readonly @click="showCustomerSheet = true" />
+            <template #right>
+              <up-icon name="arrow-right" @click="showCustomerSheet = true"></up-icon>
+            </template>
+          </up-form-item>
+          <up-form-item label="涓氬姟鍛�" prop="salesperson" required>
+            <up-input
+              v-model="form.salesperson"
+              placeholder="璇烽�夋嫨涓氬姟鍛�"
+              readonly
+              @click="showSalespersonSheet = true"
+            />
+            <template #right>
+              <up-icon name="arrow-right" @click="showSalespersonSheet = true"></up-icon>
+            </template>
+          </up-form-item>
+          <up-form-item label="鎶ヤ环鏃ユ湡" prop="quotationDate" required>
+            <up-input
+              v-model="form.quotationDate"
+              placeholder="璇烽�夋嫨鎶ヤ环鏃ユ湡"
+              readonly
+              @click="showQuotationDatePicker = true"
+            />
+            <template #right>
+              <up-icon name="arrow-right" @click="showQuotationDatePicker = true"></up-icon>
+            </template>
+          </up-form-item>
+          <up-form-item label="鏈夋晥鏈熻嚦" prop="validDate" required>
+            <up-input
+              v-model="form.validDate"
+              placeholder="璇烽�夋嫨鏈夋晥鏈�"
+              readonly
+              @click="showValidDatePicker = true"
+            />
+            <template #right>
+              <up-icon name="arrow-right" @click="showValidDatePicker = true"></up-icon>
+            </template>
+          </up-form-item>
+          <up-form-item label="浠樻鏂瑰紡" prop="paymentMethod" required>
+            <up-input v-model="form.paymentMethod" placeholder="璇疯緭鍏ヤ粯娆炬柟寮�" clearable />
+          </up-form-item>
+          <up-form-item label="澶囨敞" prop="remark">
+            <up-textarea v-model="form.remark" placeholder="璇疯緭鍏ュ娉�" auto-height />
+          </up-form-item>
+        </u-cell-group>
+
+        <u-cell-group title="瀹℃壒鑺傜偣" class="form-section">
+          <view class="section-tools">
+            <up-button type="primary" size="small" text="鏂板鑺傜偣" @click="addApproverNode" />
+          </view>
+          <view v-if="salespersonList.length === 0" class="empty-text">
+            <text>鏆傛棤鍙�夊鎵逛汉锛岃妫�鏌ョ敤鎴锋暟鎹�</text>
+          </view>
+          <view class="node-list">
+            <view v-for="(node, index) in approverNodes" :key="node.id" class="node-card">
+              <view class="node-top">
+                <text class="node-title">瀹℃壒鑺傜偣 {{ index + 1 }}</text>
+                <up-icon
+                  v-if="approverNodes.length > 1"
+                  name="trash"
+                  color="#ee0a24"
+                  size="18"
+                  @click="removeApproverNode(index)"
+                ></up-icon>
+              </view>
+              <view class="picker-field" @click="openApproverPicker(index)">
+                <up-input :model-value="node.nickName || ''" placeholder="璇烽�夋嫨瀹℃壒浜�" readonly disabled />
+                <up-icon name="arrow-right" color="#909399" size="16"></up-icon>
+              </view>
+            </view>
+          </view>
+        </u-cell-group>
+
+        <u-cell-group title="浜у搧淇℃伅" class="form-section">
+          <view class="section-tools">
+            <up-button type="primary" size="small" text="鏂板浜у搧" @click="addProduct" />
+          </view>
+          <view v-if="form.products.length === 0" class="empty-text">
+            <text>鏆傛棤浜у搧锛岃鍏堟坊鍔犱骇鍝�</text>
+          </view>
+          <view v-else class="product-list">
+            <view v-for="(product, index) in form.products" :key="product.uid" class="product-card">
+              <view class="product-header">
+                <text class="product-title">浜у搧 {{ index + 1 }}</text>
+                <up-icon name="trash" color="#ee0a24" size="18" @click="removeProduct(index)"></up-icon>
+              </view>
+              <up-divider></up-divider>
+              <view class="product-body">
+                <up-form-item label="浜у搧鍚嶇О">
+                  <up-input
+                    v-model="product.product"
+                    placeholder="璇烽�夋嫨浜у搧"
+                    readonly
+                    @click="openProductPicker(index)"
+                  />
+                  <template #right>
+                    <up-icon name="arrow-right" @click="openProductPicker(index)"></up-icon>
+                  </template>
+                </up-form-item>
+                <up-form-item label="瑙勬牸鍨嬪彿">
+                  <up-input
+                    v-model="product.specification"
+                    placeholder="璇烽�夋嫨瑙勬牸鍨嬪彿"
+                    readonly
+                    @click="openModelPicker(index)"
+                  />
+                  <template #right>
+                    <up-icon name="arrow-right" @click="openModelPicker(index)"></up-icon>
+                  </template>
+                </up-form-item>
+                <up-form-item label="鍗曚綅">
+                  <up-input v-model="product.unit" placeholder="璇疯緭鍏ュ崟浣�" clearable />
+                </up-form-item>
+                <up-form-item label="鏁伴噺">
+                  <up-input
+                    v-model="product.quantity"
+                    type="number"
+                    placeholder="璇疯緭鍏ユ暟閲�"
+                    clearable
+                    @blur="calculateAmount(product)"
+                  />
+                </up-form-item>
+                <up-form-item label="鍗曚环">
+                  <up-input
+                    v-model="product.unitPrice"
+                    type="number"
+                    placeholder="璇疯緭鍏ュ崟浠�"
+                    clearable
+                    @blur="calculateAmount(product)"
+                  />
+                </up-form-item>
+                <up-form-item label="閲戦">
+                  <up-input :model-value="formatAmount(product.amount)" disabled placeholder="鑷姩璁$畻" />
+                </up-form-item>
+              </view>
+            </view>
+          </view>
+        </u-cell-group>
+
+        <u-cell-group title="姹囨�讳俊鎭�" class="form-section">
+          <up-form-item label="鎶ヤ环鎬婚">
+            <up-input :model-value="formatAmount(totalAmount)" disabled placeholder="鑷姩姹囨��" />
+          </up-form-item>
+        </u-cell-group>
+      </up-form>
+    </view>
+
+    <FooterButtons :loading="loading" confirmText="淇濆瓨" @cancel="goBack" @confirm="handleSubmit" />
+
+    <up-action-sheet :show="showCustomerSheet" title="閫夋嫨瀹㈡埛" :actions="customerActions" @select="onSelectCustomer" @close="showCustomerSheet = false" />
+    <up-action-sheet :show="showSalespersonSheet" title="閫夋嫨涓氬姟鍛�" :actions="salespersonActions" @select="onSelectSalesperson" @close="showSalespersonSheet = false" />
+    <up-action-sheet :show="showProductSheet" title="閫夋嫨浜у搧" :actions="productActions" @select="onSelectProduct" @close="showProductSheet = false" />
+    <up-action-sheet :show="showModelSheet" title="閫夋嫨瑙勬牸鍨嬪彿" :actions="modelActions" @select="onSelectModel" @close="showModelSheet = false" />
+    <up-datetime-picker :show="showQuotationDatePicker" v-model="quotationDateValue" mode="date" @confirm="onQuotationDateConfirm" @cancel="showQuotationDatePicker = false" />
+    <up-datetime-picker :show="showValidDatePicker" v-model="validDateValue" mode="date" @confirm="onValidDateConfirm" @cancel="showValidDatePicker = false" />
+  </view>
+</template>
+
+<script setup>
+  import { computed, onMounted, onUnmounted, ref } from "vue";
+  import { onLoad } from "@dcloudio/uni-app";
+  import FooterButtons from "@/components/FooterButtons.vue";
+  import PageHeader from "@/components/PageHeader.vue";
+  import { formatDateToYMD } from "@/utils/ruoyi";
+  import { modelList, productTreeList } from "@/api/basicData/product";
+  import { userListNoPageByTenantId } from "@/api/system/user";
+  import { addQuotation, getCustomerList, getQuotationDetail, updateQuotation } from "@/api/salesManagement/salesQuotation";
+
+  const formRef = ref();
+  const loading = ref(false);
+  const quotationId = ref("");
+  const showCustomerSheet = ref(false);
+  const showSalespersonSheet = ref(false);
+  const showProductSheet = ref(false);
+  const showModelSheet = ref(false);
+  const showQuotationDatePicker = ref(false);
+  const showValidDatePicker = ref(false);
+  const quotationDateValue = ref(Date.now());
+  const validDateValue = ref(Date.now());
+  const currentProductIndex = ref(-1);
+  const customerList = ref([]);
+  const salespersonList = ref([]);
+  const productList = ref([]);
+  const modelActions = ref([]);
+
+  let uidSeed = 1;
+  let nextApproverId = 2;
+
+  const form = ref({
+    id: undefined,
+    quotationNo: "",
+    customer: "",
+    salesperson: "",
+    quotationDate: "",
+    validDate: "",
+    paymentMethod: "",
+    status: "寰呭鎵�",
+    remark: "",
+    approveUserIds: "",
+    products: [],
+    totalAmount: 0,
+  });
+
+  const approverNodes = ref([{ id: 1, userId: "", nickName: "" }]);
+
+  const rules = {
+    customer: [{ required: true, message: "璇烽�夋嫨瀹㈡埛", trigger: "change" }],
+    salesperson: [{ required: true, message: "璇烽�夋嫨涓氬姟鍛�", trigger: "change" }],
+    quotationDate: [{ required: true, message: "璇烽�夋嫨鎶ヤ环鏃ユ湡", trigger: "change" }],
+    validDate: [{ required: true, message: "璇烽�夋嫨鏈夋晥鏈�", trigger: "change" }],
+    paymentMethod: [{ required: true, message: "璇疯緭鍏ヤ粯娆炬柟寮�", trigger: "blur" }],
+  };
+
+  const pageTitle = computed(() => (quotationId.value ? "缂栬緫鎶ヤ环" : "鏂板鎶ヤ环"));
+  const totalAmount = computed(() =>
+    Number((form.value.products || []).reduce((sum, item) => sum + Number(item.amount || 0), 0).toFixed(2))
+  );
+  const customerActions = computed(() => customerList.value.map(item => ({ name: item.customerName, value: item.customerName })));
+  const salespersonActions = computed(() => salespersonList.value.map(item => ({ name: item.nickName, value: item.nickName })));
+  const productActions = computed(() => productList.value.map(item => ({ name: item.label, value: item.value, label: item.label })));
+
+  const createEmptyProduct = () => ({
+    uid: `p_${uidSeed++}`,
+    productId: "",
+    product: "",
+    specificationId: "",
+    specification: "",
+    unit: "",
+    quantity: 1,
+    unitPrice: 0,
+    amount: 0,
+    modelOptions: [],
+  });
+
+  const flattenProductTree = nodes => {
+    const result = [];
+    const walk = list => {
+      (list || []).forEach(item => {
+        if (item.children && item.children.length) {
+          walk(item.children);
+        } else {
+          result.push({ label: item.label || item.productName || "", value: item.id || item.value });
+        }
+      });
+    };
+    walk(nodes);
+    return result;
+  };
+
+  const formatAmount = amount => `楼${Number(amount || 0).toFixed(2)}`;
+  const goBack = () => uni.navigateBack();
+
+  const calculateAmount = product => {
+    product.amount = Number((Number(product.quantity || 0) * Number(product.unitPrice || 0)).toFixed(2));
+    form.value.totalAmount = totalAmount.value;
+  };
+
+  const addApproverNode = () => approverNodes.value.push({ id: nextApproverId++, userId: "", nickName: "" });
+  const removeApproverNode = index => approverNodes.value.splice(index, 1);
+  const openApproverPicker = index => {
+    uni.setStorageSync("stepIndex", index);
+    uni.navigateTo({
+      url: "/pages/cooperativeOffice/collaborativeApproval/contactSelect",
+    });
+  };
+  const addProduct = () => form.value.products.push(createEmptyProduct());
+  const removeProduct = index => {
+    form.value.products.splice(index, 1);
+    form.value.totalAmount = totalAmount.value;
+  };
+
+  const fetchModelOptions = async (productId, product) => {
+    const rows = await modelList({ id: productId }).catch(() => []);
+    product.modelOptions = Array.isArray(rows) ? rows : [];
+  };
+
+  const openProductPicker = index => {
+    currentProductIndex.value = index;
+    showProductSheet.value = true;
+  };
+  const openModelPicker = index => {
+    currentProductIndex.value = index;
+    const current = form.value.products[index];
+    if (!current?.productId) {
+      uni.showToast({ title: "璇峰厛閫夋嫨浜у搧", icon: "none" });
+      return;
+    }
+    modelActions.value = (current.modelOptions || []).map(item => ({ name: item.model, value: item.id, unit: item.unit }));
+    if (!modelActions.value.length) {
+      uni.showToast({ title: "鏆傛棤瑙勬牸鍨嬪彿", icon: "none" });
+      return;
+    }
+    showModelSheet.value = true;
+  };
+
+  const onSelectCustomer = action => {
+    form.value.customer = action.value;
+    showCustomerSheet.value = false;
+  };
+  const onSelectSalesperson = action => {
+    form.value.salesperson = action.value;
+    showSalespersonSheet.value = false;
+  };
+  const onSelectApprover = data => {
+    const { stepIndex, contact } = data || {};
+    if (stepIndex === undefined || !contact) return;
+    if (!approverNodes.value[stepIndex]) return;
+    approverNodes.value[stepIndex].userId = contact.userId;
+    approverNodes.value[stepIndex].nickName = contact.nickName;
+  };
+  const onSelectProduct = action => {
+    const current = form.value.products[currentProductIndex.value];
+    if (!current) return;
+    current.productId = action.value;
+    current.product = action.label;
+    current.specificationId = "";
+    current.specification = "";
+    current.unit = "";
+    current.modelOptions = [];
+    showProductSheet.value = false;
+    fetchModelOptions(action.value, current);
+  };
+  const onSelectModel = action => {
+    const current = form.value.products[currentProductIndex.value];
+    if (!current) return;
+    current.specificationId = action.value;
+    current.specification = action.name;
+    current.unit = action.unit || current.unit;
+    showModelSheet.value = false;
+  };
+  const onQuotationDateConfirm = e => {
+    form.value.quotationDate = formatDateToYMD(e.value);
+    showQuotationDatePicker.value = false;
+  };
+  const onValidDateConfirm = e => {
+    form.value.validDate = formatDateToYMD(e.value);
+    showValidDatePicker.value = false;
+  };
+
+  const fetchBaseOptions = async () => {
+      const [customers, users, productTree] = await Promise.all([
+        getCustomerList({ current: -1, size: -1 }).catch(() => ({})),
+        userListNoPageByTenantId().catch(() => ({})),
+        productTreeList().catch(() => []),
+      ]);
+    customerList.value = customers?.data?.records || customers?.records || [];
+    const userRows = users?.data || [];
+    salespersonList.value = Array.isArray(userRows) ? userRows : [];
+    productList.value = flattenProductTree(Array.isArray(productTree) ? productTree : productTree?.data || []);
+  };
+
+  const normalizeProductRows = async rows => {
+    const normalized = await Promise.all((Array.isArray(rows) ? rows : []).map(async item => {
+      const row = {
+        uid: `p_${uidSeed++}`,
+        productId: item.productId || "",
+        product: item.product || item.productName || "",
+        specificationId: item.specificationId || "",
+        specification: item.specification || "",
+        unit: item.unit || "",
+        quantity: Number(item.quantity || 1),
+        unitPrice: Number(item.unitPrice || 0),
+        amount: Number(item.amount || 0),
+        modelOptions: [],
+      };
+      if (row.productId) await fetchModelOptions(row.productId, row);
+      return row;
+    }));
+    form.value.products = normalized;
+  };
+
+  const loadDetail = async () => {
+    if (!quotationId.value) return;
+    uni.showLoading({ title: "鍔犺浇涓�...", mask: true });
+    try {
+      const res = await getQuotationDetail({ id: quotationId.value });
+      const data = res?.data || {};
+      form.value = {
+        ...form.value,
+        id: data.id,
+        quotationNo: data.quotationNo || "",
+        customer: data.customer || "",
+        salesperson: data.salesperson || "",
+        quotationDate: data.quotationDate || "",
+        validDate: data.validDate || "",
+        paymentMethod: data.paymentMethod || "",
+        status: data.status || "寰呭鎵�",
+        remark: data.remark || "",
+      };
+      await normalizeProductRows(data.products || []);
+      if (data.approveUserIds) {
+        const ids = String(data.approveUserIds).split(",").map(item => item.trim()).filter(Boolean);
+        approverNodes.value = ids.map((userId, index) => ({
+          id: index + 1,
+          userId,
+          nickName: salespersonList.value.find(item => String(item.userId) === String(userId))?.nickName || "",
+        }));
+        nextApproverId = approverNodes.value.length + 1;
+      }
+      form.value.totalAmount = totalAmount.value;
+    } catch {
+      uni.showToast({ title: "鑾峰彇璇︽儏澶辫触", icon: "error" });
+    } finally {
+      uni.hideLoading();
+    }
+  };
+
+  const validateProducts = () => {
+    if (!form.value.products.length) {
+      uni.showToast({ title: "璇疯嚦灏戞坊鍔犱竴涓骇鍝�", icon: "none" });
+      return false;
+    }
+    const invalid = form.value.products.some(item => !item.productId || !item.specificationId || !item.unit || !Number(item.quantity) || !Number(item.unitPrice));
+    if (invalid) {
+      uni.showToast({ title: "璇峰畬鍠勪骇鍝佷俊鎭�", icon: "none" });
+      return false;
+    }
+    return true;
+  };
+  const validateApprovers = () => {
+    if (approverNodes.value.some(item => !item.userId)) {
+      uni.showToast({ title: "璇烽�夋嫨瀹℃壒浜�", icon: "none" });
+      return false;
+    }
+    return true;
+  };
+
+  const handleSubmit = async () => {
+    const valid = await formRef.value.validate().catch(() => false);
+    if (!valid || !validateApprovers() || !validateProducts()) return;
+    loading.value = true;
+    const payload = {
+      ...form.value,
+      approveUserIds: approverNodes.value.map(item => item.userId).join(","),
+      totalAmount: totalAmount.value,
+      products: form.value.products.map(item => ({
+        productId: item.productId,
+        product: item.product,
+        specificationId: item.specificationId,
+        specification: item.specification,
+        quantity: Number(item.quantity || 0),
+        unit: item.unit,
+        unitPrice: Number(item.unitPrice || 0),
+        amount: Number(item.amount || 0),
+      })),
+    };
+    const action = quotationId.value ? updateQuotation : addQuotation;
+    action(payload)
+      .then(() => {
+        uni.showToast({ title: "淇濆瓨鎴愬姛", icon: "success" });
+        setTimeout(() => uni.navigateBack(), 300);
+      })
+      .catch(() => {
+        uni.showToast({ title: "淇濆瓨澶辫触", icon: "error" });
+      })
+      .finally(() => {
+        loading.value = false;
+      });
+  };
+
+  onLoad(options => {
+    if (options?.id) {
+      quotationId.value = options.id;
+      form.value.id = options.id;
+    } else {
+      const today = formatDateToYMD(Date.now());
+      form.value.quotationDate = today;
+      form.value.validDate = today;
+    }
+  });
+
+  onMounted(async () => {
+    await fetchBaseOptions();
+    uni.$on("selectContact", onSelectApprover);
+    if (quotationId.value) {
+      await loadDetail();
+    }
+  });
+
+  onUnmounted(() => {
+    uni.$off("selectContact", onSelectApprover);
+    uni.removeStorageSync("stepIndex");
+  });
+</script>
+
+<style scoped lang="scss">
+  @import "@/static/scss/form-common.scss";
+
+  .account-detail {
+    min-height: 100vh;
+    background: #f8f9fa;
+    padding-bottom: 100px;
+  }
+
+  .form-container {
+    padding: 12px 12px 0;
+  }
+
+  .hero-card {
+    margin-bottom: 12px;
+    padding: 18px 18px 16px;
+    border-radius: 16px;
+    background: linear-gradient(135deg, #eef6ff 0%, #ffffff 100%);
+    box-shadow: 0 6px 18px rgba(41, 121, 255, 0.08);
+  }
+
+  .hero-title {
+    display: block;
+    font-size: 18px;
+    font-weight: 600;
+    color: #1f2d3d;
+    margin-bottom: 6px;
+  }
+
+  .hero-desc {
+    display: block;
+    font-size: 13px;
+    line-height: 1.6;
+    color: #7a8599;
+  }
+
+  .form-section {
+    margin-bottom: 12px;
+    border-radius: 12px;
+    overflow: hidden;
+    box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
+  }
+
+  .section-tools {
+    display: flex;
+    justify-content: flex-end;
+    padding: 12px 12px 0;
+  }
+
+  .node-list,
+  .product-list {
+    padding: 12px;
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+  }
+
+  .node-card {
+    background: #f8fbff;
+    border-radius: 12px;
+    padding: 12px;
+    border: 1px solid #e6eef8;
+  }
+
+  .picker-field {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+  }
+
+  .picker-field :deep(.u-input) {
+    flex: 1;
+  }
+
+  .node-top,
+  .product-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+  }
+
+  .node-title,
+  .product-title {
+    font-size: 14px;
+    font-weight: 600;
+    color: #22324d;
+  }
+
+  .product-card {
+    background: #fff;
+    border-radius: 12px;
+    padding: 0 12px 12px;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+  }
+
+  .product-header {
+    padding: 12px 0;
+  }
+
+  .empty-text {
+    padding: 16px 12px;
+    color: #999;
+    font-size: 14px;
+  }
+
+  :deep(.u-cell-group__title) {
+    padding: 14px 18px 10px !important;
+    font-size: 15px !important;
+    font-weight: 600 !important;
+    color: #22324d !important;
+    background: #f8fbff !important;
+  }
+</style>
diff --git a/src/pages/sales/salesQuotation/index.vue b/src/pages/sales/salesQuotation/index.vue
new file mode 100644
index 0000000..a6a5103
--- /dev/null
+++ b/src/pages/sales/salesQuotation/index.vue
@@ -0,0 +1,226 @@
+<template>
+  <view class="sales-account">
+    <PageHeader title="閿�鍞姤浠�" @back="goBack" />
+
+    <view class="search-section">
+      <view class="search-bar">
+        <view class="search-input">
+          <up-input
+            class="search-text"
+            v-model="quotationNo"
+            placeholder="璇疯緭鍏ユ姤浠峰崟鍙锋悳绱�"
+            clearable
+            @change="getList"
+          />
+        </view>
+        <view class="filter-button" @click="getList">
+          <up-icon name="search" size="24" color="#999"></up-icon>
+        </view>
+      </view>
+    </view>
+
+    <view class="tabs-section">
+      <up-tabs
+        v-model="tabValue"
+        :list="tabList"
+        itemStyle="width: 20%;height: 80rpx;"
+        @change="onTabChange"
+      />
+    </view>
+
+    <view v-if="quotationList.length > 0" class="ledger-list">
+      <view v-for="item in quotationList" :key="item.id" 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.quotationNo || "-" }}</text>
+          </view>
+          <text class="item-index">{{ item.status || "-" }}</text>
+        </view>
+
+        <up-divider></up-divider>
+
+        <view class="item-details">
+          <view class="detail-row">
+            <text class="detail-label">瀹㈡埛鍚嶇О</text>
+            <text class="detail-value">{{ item.customer || "-" }}</text>
+          </view>
+          <view class="detail-row">
+            <text class="detail-label">涓氬姟鍛�</text>
+            <text class="detail-value">{{ item.salesperson || "-" }}</text>
+          </view>
+          <view class="detail-row">
+            <text class="detail-label">鎶ヤ环鏃ユ湡</text>
+            <text class="detail-value">{{ item.quotationDate || "-" }}</text>
+          </view>
+          <view class="detail-row">
+            <text class="detail-label">鏈夋晥鏈熻嚦</text>
+            <text class="detail-value">{{ item.validDate || "-" }}</text>
+          </view>
+          <view class="detail-row">
+            <text class="detail-label">浠樻鏂瑰紡</text>
+            <text class="detail-value">{{ item.paymentMethod || "-" }}</text>
+          </view>
+          <view class="detail-row">
+            <text class="detail-label">鎶ヤ环閲戦</text>
+            <text class="detail-value highlight">{{ formatAmount(item.totalAmount) }}</text>
+          </view>
+          <view class="detail-row">
+            <text class="detail-label">澶囨敞</text>
+            <text class="detail-value">{{ item.remark || "-" }}</text>
+          </view>
+        </view>
+
+        <view class="action-buttons">
+					<up-button
+						class="action-btn"
+		        size="small"
+		        type="primary"
+		        :disabled="!canEdit(item)"
+		        @click="goEdit(item)"
+					>
+						缂栬緫
+					</up-button>
+          <up-button class="action-btn" size="small" @click="goDetail(item)">璇︽儏</up-button>
+          <up-button class="action-btn" size="small" type="error" plain @click="handleDelete(item)">
+            鍒犻櫎
+          </up-button>
+        </view>
+      </view>
+    </view>
+
+    <view v-else class="no-data">
+      <text>鏆傛棤閿�鍞姤浠锋暟鎹�</text>
+    </view>
+
+    <view class="fab-button" @click="goAdd">
+      <up-icon name="plus" size="28" color="#ffffff"></up-icon>
+    </view>
+  </view>
+</template>
+
+<script setup>
+  import { reactive, ref } from "vue";
+  import { onShow } from "@dcloudio/uni-app";
+  import PageHeader from "@/components/PageHeader.vue";
+  import { deleteQuotation, getQuotationList } from "@/api/salesManagement/salesQuotation";
+
+  const quotationNo = ref("");
+  const quotationList = ref([]);
+
+  const tabList = reactive([
+    { name: "鍏ㄩ儴", value: "" },
+    { name: "寰呭鎵�", value: "寰呭鎵�" },
+    { name: "瀹℃牳涓�", value: "瀹℃牳涓�" },
+    { name: "閫氳繃", value: "閫氳繃" },
+    { name: "鎷掔粷", value: "鎷掔粷" },
+  ]);
+  const tabValue = ref(0);
+
+  const page = {
+    current: -1,
+    size: -1,
+  };
+
+  const goBack = () => {
+    uni.navigateBack();
+  };
+
+  const goAdd = () => {
+    uni.navigateTo({ url: "/pages/sales/salesQuotation/edit" });
+  };
+
+  const goEdit = item => {
+    if (!canEdit(item)) return;
+    uni.navigateTo({ url: `/pages/sales/salesQuotation/edit?id=${item.id}` });
+  };
+
+  const goDetail = item => {
+    uni.setStorageSync("salesQuotationDetail", item || {});
+    uni.navigateTo({ url: `/pages/sales/salesQuotation/detail?id=${item.id}` });
+  };
+
+  const canEdit = item => ["寰呭鎵�", "鎷掔粷"].includes(item?.status);
+
+  const onTabChange = val => {
+    tabValue.value = val.index;
+    getList();
+  };
+
+  const getCurrentStatus = () => {
+    const currentTab = tabList[tabValue.value];
+    return currentTab?.value || "";
+  };
+
+  const formatAmount = amount => {
+    const num = Number(amount || 0);
+    return `楼${num.toFixed(2)}`;
+  };
+
+  const getList = () => {
+    uni.showLoading({ title: "鍔犺浇涓�...", mask: true });
+    getQuotationList({
+      ...page,
+      quotationNo: quotationNo.value,
+      status: getCurrentStatus(),
+    })
+      .then(res => {
+        const records = res?.data?.records || res?.records || [];
+        quotationList.value = Array.isArray(records) ? records : [];
+      })
+      .catch(() => {
+        uni.showToast({ title: "鏌ヨ澶辫触", icon: "error" });
+      })
+      .finally(() => {
+        uni.hideLoading();
+      });
+  };
+
+  const handleDelete = item => {
+    if (!item?.id) return;
+    uni.showModal({
+      title: "鍒犻櫎纭",
+      content: "纭鍒犻櫎璇ユ姤浠峰崟鍚楋紵",
+      success: res => {
+        if (!res.confirm) return;
+        uni.showLoading({ title: "澶勭悊涓�...", mask: true });
+        deleteQuotation(item.id)
+          .then(() => {
+            uni.showToast({ title: "鍒犻櫎鎴愬姛", icon: "success" });
+            getList();
+          })
+          .catch(() => {
+            uni.showToast({ title: "鍒犻櫎澶辫触", icon: "error" });
+          })
+          .finally(() => {
+            uni.hideLoading();
+          });
+      },
+    });
+  };
+
+  onShow(() => {
+    getList();
+  });
+</script>
+
+<style scoped lang="scss">
+  @import "@/styles/sales-common.scss";
+
+  .tabs-section {
+    background: #ffffff;
+    padding: 0 12px 8px 12px;
+  }
+
+  .item-index {
+    max-width: 180rpx;
+    text-align: center;
+  }
+
+  .detail-value {
+    max-width: 70%;
+    word-break: break-all;
+  }
+</style>
diff --git a/src/pages/works.vue b/src/pages/works.vue
index 89c8d26..acdc37b 100644
--- a/src/pages/works.vue
+++ b/src/pages/works.vue
@@ -301,6 +301,14 @@
   const marketingItems = reactive([
     {
       icon: "/static/images/icon/xiaoshoutaizhang.svg",
+      label: "瀹㈡埛妗f",
+    },
+    {
+      icon: "/static/images/icon/xiaoshoutaizhang.svg",
+      label: "閿�鍞姤浠�",
+    },
+    {
+      icon: "/static/images/icon/xiaoshoutaizhang.svg",
       label: "閿�鍞彴璐�",
     },
     {
@@ -551,11 +559,21 @@
   const handleCommonItemClick = item => {
     // 鏍规嵁涓嶅悓鐨勫姛鑳介」杩涜璺宠浆
     switch (item.label) {
+      case "瀹㈡埛妗f":
+        uni.navigateTo({
+          url: "/pages/basicData/customerFile/index",
+        });
+        break;
       case "閿�鍞彴璐�":
         uni.navigateTo({
           url: "/pages/sales/salesAccount/index",
         });
         break;
+      case "閿�鍞姤浠�":
+        uni.navigateTo({
+          url: "/pages/sales/salesQuotation/index",
+        });
+        break;
       case "寮�绁ㄧ櫥璁�":
         uni.navigateTo({
           url: "/pages/sales/invoicingRegistration/index",

--
Gitblit v1.9.3