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