| | |
| | | </template> |
| | | </up-form-item> |
| | | <up-form-item label="项目名称" |
| | | prop="projectName" |
| | | prop="projectId" |
| | | required> |
| | | <up-input v-model="form.projectName" |
| | | placeholder="请输入项目名称" /> |
| | | <FormPicker v-model="form.projectId" |
| | | type="select" |
| | | title="选择项目" |
| | | :options="projectOptions" |
| | | placeholder="请选择项目" |
| | | @change="onProjectChange" /> |
| | | </up-form-item> |
| | | <up-form-item label="签订日期" |
| | | prop="executionDate" |
| | | required> |
| | | <up-input v-model="form.executionDate" |
| | | readonly |
| | | placeholder="点击选择时间" |
| | | @click="showDatePicker = true" /> |
| | | <template #right> |
| | | <up-icon name="arrow-right" |
| | | @click="showDatePicker = true"></up-icon> |
| | | </template> |
| | | <FormPicker v-model="form.executionDate" |
| | | type="date" |
| | | placeholder="请选择签订日期" |
| | | @change="onExecutionDateChange" /> |
| | | </up-form-item> |
| | | <up-form-item label="年份"> |
| | | <up-input :model-value="form.year === '' ? '自动取签订日期' : String(form.year)" |
| | | disabled /> |
| | | </up-form-item> |
| | | <up-form-item label="付款方式" |
| | | prop="paymentMethod"> |
| | | <up-input v-model="form.paymentMethod" |
| | | placeholder="请输入付款方式" /> |
| | | <FormPicker v-model="form.paymentMethod" |
| | | type="select" |
| | | title="选择付款方式" |
| | | :display-text="paymentDisplayText" |
| | | :options="paymentOptions" |
| | | placeholder="请选择付款方式" /> |
| | | </up-form-item> |
| | | <up-form-item label="交货日期" |
| | | prop="deliveryDate"> |
| | | <FormPicker v-model="form.deliveryDate" |
| | | type="date" |
| | | placeholder="请选择交货日期" /> |
| | | </up-form-item> |
| | | <up-form-item label="签单单位" |
| | | prop="signingUnitId"> |
| | | <FormPicker v-model="form.signingUnitId" |
| | | type="select" |
| | | title="选择签单单位" |
| | | :options="deptOptions" |
| | | placeholder="请选择签单单位" /> |
| | | </up-form-item> |
| | | <up-form-item label="办事处" |
| | | prop="officeId"> |
| | | <FormPicker v-model="form.officeId" |
| | | type="select" |
| | | title="选择办事处" |
| | | :options="deptOptions" |
| | | placeholder="请选择办事处" /> |
| | | </up-form-item> |
| | | <up-form-item label="组负责人" |
| | | prop="groupLeaderId"> |
| | | <FormPicker v-model="form.groupLeaderId" |
| | | type="select" |
| | | title="选择组负责人" |
| | | :options="userOptions" |
| | | placeholder="请选择组负责人" /> |
| | | </up-form-item> |
| | | <up-form-item label="是否开发票"> |
| | | <view class="switch-row"> |
| | | <up-switch v-model="form.isInvoice" |
| | | :activeValue="1" |
| | | :inactiveValue="0" /> |
| | | </view> |
| | | </up-form-item> |
| | | <up-form-item label="是否中标"> |
| | | <view class="switch-row"> |
| | | <up-switch v-model="form.isBid" |
| | | :activeValue="1" |
| | | :inactiveValue="0" /> |
| | | </view> |
| | | </up-form-item> |
| | | <up-form-item v-if="form.isBid === 1" |
| | | label="中标价"> |
| | | <up-input v-model="form.bidPrice" |
| | | type="number" |
| | | placeholder="请输入中标价" /> |
| | | </up-form-item> |
| | | <up-form-item v-else |
| | | label="未中标原因"> |
| | | <up-input v-model="form.unbidReason" |
| | | placeholder="请输入未中标原因" /> |
| | | </up-form-item> |
| | | <up-form-item label="合同原件约定回传日期"> |
| | | <FormPicker v-model="form.contractOriginalReturnDate" |
| | | type="date" |
| | | placeholder="请选择合同原件约定回传日期" /> |
| | | </up-form-item> |
| | | <up-form-item label="备注"> |
| | | <up-textarea v-model="form.remarks" |
| | | placeholder="请输入备注" |
| | | :maxlength="200" |
| | | count |
| | | auto-height /> |
| | | </up-form-item> |
| | | <up-form-item label="录入人" |
| | | prop="entryPersonName"> |
| | |
| | | title="选择业务员" |
| | | @select="onSalesmanSelect" |
| | | @close="showPicker = false" /> |
| | | <!-- 日期选择 --> |
| | | <up-popup :show="showDatePicker" |
| | | mode="bottom" |
| | | @close="showDatePicker = false"> |
| | | <up-datetime-picker :show="true" |
| | | v-model="pickerDateValue" |
| | | @confirm="onDateConfirm" |
| | | @cancel="showDatePicker = false" |
| | | mode="date" /> |
| | | </up-popup> |
| | | <!-- 客户选择 --> |
| | | <up-action-sheet :show="showCustomerPicker" |
| | | :actions="customerActionList" |
| | |
| | | </up-form-item> |
| | | </view> |
| | | </view> |
| | | <!-- 需求五.4:签约金额 = 含税单价 × 数量,即含税总价,按产品行汇总 --> |
| | | <view v-if="productData.length > 0" |
| | | class="product-summary"> |
| | | <view class="summary-row"> |
| | | <text class="summary-label">签约金额合计(元)</text> |
| | | <text class="summary-value highlight">{{ productTotals.taxInclusive }}</text> |
| | | </view> |
| | | <view class="summary-row"> |
| | | <text class="summary-label">不含税总价合计(元)</text> |
| | | <text class="summary-value">{{ productTotals.taxExclusive }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </up-form> |
| | | <!-- 使用公共底部按钮组件 --> |
| | |
| | | <script setup> |
| | | import { onMounted, ref, computed } from "vue"; |
| | | import { userListNoPage } from "@/api/system/user"; |
| | | import { formatDateToYMD } from "@/utils/ruoyi"; |
| | | import { userListNoPageByTenantId } from "@/api/system/user"; |
| | | import { getDept } from "@/api/collaborativeApproval/approvalProcess"; |
| | | import { |
| | | addOrUpdateSalesLedger, |
| | | customerList, |
| | |
| | | modelList, |
| | | productTreeList, |
| | | } from "@/api/salesManagement/salesLedger"; |
| | | import { getProject } from "@/api/projectManagement/project"; |
| | | import useUserStore from "@/store/modules/user"; |
| | | import { calculateTaxExclusiveTotalPrice } from "@/utils/summarizeTable"; |
| | | import { useDict } from "@/utils/dict"; |
| | | import { loadProjectOptions } from "@/utils/projectOptions"; |
| | | import { takePrefillProject } from "@/utils/prefill"; |
| | | import FormPicker from "@/components/FormPicker.vue"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import FooterButtons from "@/components/FooterButtons.vue"; |
| | | |
| | | const { checkout_payment } = useDict("checkout_payment"); |
| | | const paymentOptions = computed(() => |
| | | (checkout_payment.value || []).map(item => ({ |
| | | label: item.label, |
| | | value: item.value, |
| | | })) |
| | | ); |
| | | |
| | | // 改动前付款方式是自由文本,老单据的值可能不在字典里,此时回显原值而不是空白 |
| | | const paymentDisplayText = computed(() => { |
| | | const value = form.value.paymentMethod; |
| | | if (!value) return ""; |
| | | const matched = paymentOptions.value.find( |
| | | item => String(item.value) === String(value) |
| | | ); |
| | | return matched ? matched.label : String(value); |
| | | }); |
| | | |
| | | // 获取页面参数 |
| | | const operationType = ref(""); |
| | |
| | | const form = ref({ |
| | | id: "", |
| | | salesContractNo: "", |
| | | autoGenerateContractNo: true, |
| | | customerId: "", |
| | | customerName: "", |
| | | projectId: "", |
| | | projectName: "", |
| | | executionDate: "", |
| | | year: "", |
| | | paymentMethod: "", |
| | | deliveryDate: "", |
| | | signingUnitId: "", |
| | | officeId: "", |
| | | groupLeaderId: "", |
| | | isInvoice: 0, |
| | | isBid: 0, |
| | | bidPrice: "", |
| | | unbidReason: "", |
| | | contractOriginalReturnDate: "", |
| | | remarks: "", |
| | | entryPerson: "", |
| | | entryPersonName: "", |
| | | entryDate: "", |
| | | }); |
| | | const showPicker = ref(false); |
| | | const showDatePicker = ref(false); |
| | | const pickerDateValue = ref(Date.now()); |
| | | const showCustomerPicker = ref(false); |
| | | const userList = ref([]); |
| | | const customerOption = ref([]); |
| | | const projectOptions = ref([]); |
| | | const deptOptions = ref([]); |
| | | const userOptions = ref([]); |
| | | const userActionList = computed(() => { |
| | | return userList.value.map(user => ({ |
| | | name: user.text, |
| | | value: user.value, |
| | | })); |
| | | }); |
| | | const formatter = (type, value) => { |
| | | if (type === "year") { |
| | | return `${value}`; |
| | | } |
| | | if (type === "month") { |
| | | return `${value}`; |
| | | } |
| | | if (type === "day") { |
| | | return `${value}`; |
| | | } |
| | | return value; |
| | | }; |
| | | const customerActionList = computed(() => { |
| | | return customerOption.value.map(customer => ({ |
| | | name: customer.text, |
| | | value: customer.value, |
| | | })); |
| | | }); |
| | | |
| | | // 日期选择列表已移除,改用 up-datetime-picker |
| | | |
| | | // 产品大类选择列表 |
| | | const categoryActionList = computed(() => { |
| | |
| | | customerName: [ |
| | | { required: true, message: "请选择客户名称", trigger: "change" }, |
| | | ], |
| | | projectName: [{ required: true, message: "请输入项目名称", trigger: "blur" }], |
| | | projectId: [{ required: true, message: "请选择项目", trigger: "change" }], |
| | | executionDate: [ |
| | | { required: true, message: "请选择签订日期", trigger: "change" }, |
| | | ], |
| | |
| | | form.value.salesman = item.name; |
| | | }; |
| | | |
| | | // 日期确认事件 |
| | | const onDateConfirm = e => { |
| | | form.value.executionDate = formatDateToYMD(e.value); |
| | | // 保持pickerDateValue为时间戳格式,而不是格式化的字符串 |
| | | pickerDateValue.value = e.value; |
| | | showDatePicker.value = false; |
| | | // 签订日期 → 年份自动取年份(管理端同此逻辑,年份只读) |
| | | const onExecutionDateChange = value => { |
| | | form.value.year = value ? Number(String(value).substring(0, 4)) : ""; |
| | | }; |
| | | |
| | | // 客户选择事件 |
| | | const onCustomerSelect = item => { |
| | | form.value.customerName = item.name; |
| | | form.value.customerId = item.value; |
| | | }; |
| | | |
| | | // 需求五.4:签约金额 = 含税单价 × 数量,逐行汇总 |
| | | const productTotals = computed(() => { |
| | | const sumBy = key => |
| | | (productData.value || []) |
| | | .reduce((sum, row) => sum + (Number(row?.[key]) || 0), 0) |
| | | .toFixed(2); |
| | | return { |
| | | taxInclusive: sumBy("taxInclusiveTotalPrice"), |
| | | taxExclusive: sumBy("taxExclusiveTotalPrice"), |
| | | }; |
| | | }); |
| | | |
| | | // 需求五.1:选项目后带出客户与产品明细(管理端 fetchAndPopulateProject 同逻辑) |
| | | const applyProject = async id => { |
| | | const matched = projectOptions.value.find( |
| | | item => String(item.value) === String(id) |
| | | ); |
| | | form.value.projectName = matched?.name || ""; |
| | | |
| | | if (!id) { |
| | | productData.value = []; |
| | | return; |
| | | } |
| | | |
| | | try { |
| | | const res = await getProject(id); |
| | | const detail = res?.data?.data ?? res?.data ?? res; |
| | | const info = detail?.info || {}; |
| | | if (info.clientId) form.value.customerId = info.clientId; |
| | | if (info.clientName) form.value.customerName = info.clientName; |
| | | |
| | | const products = detail?.salesLedgerProductList; |
| | | productData.value = Array.isArray(products) |
| | | ? products.map(item => ({ |
| | | ...item, |
| | | id: undefined, |
| | | salesLedgerId: undefined, |
| | | })) |
| | | : []; |
| | | } catch (error) { |
| | | uni.showToast({ title: "获取项目信息失败", icon: "none" }); |
| | | } |
| | | }; |
| | | |
| | | // v-model 已先写入 projectId,取消时要回滚到上一个项目 |
| | | const previousProjectId = ref(""); |
| | | |
| | | const confirmProjectChange = id => { |
| | | previousProjectId.value = id; |
| | | applyProject(id); |
| | | }; |
| | | |
| | | const onProjectChange = id => { |
| | | if (productData.value && productData.value.length > 0) { |
| | | uni.showModal({ |
| | | title: "提示", |
| | | content: "当前已录入产品明细,切换项目将被覆盖,是否继续?", |
| | | success: res => { |
| | | if (res.confirm) { |
| | | confirmProjectChange(id); |
| | | } else { |
| | | form.value.projectId = previousProjectId.value; |
| | | } |
| | | }, |
| | | }); |
| | | return; |
| | | } |
| | | confirmProjectChange(id); |
| | | }; |
| | | |
| | | // 原有的确认方法已被新的action-sheet选择方法替代 |
| | |
| | | const month = String(today.getMonth() + 1).padStart(2, "0"); |
| | | const day = String(today.getDate()).padStart(2, "0"); |
| | | form.value.entryDate = `${year}-${month}-${day}`; |
| | | // 设置日期选择器默认值为今天 |
| | | pickerDateValue.value = `${year}-${month}-${day}`; |
| | | }; |
| | | // 填充表单数据(编辑模式) |
| | | const fillFormData = () => { |
| | | if (!editData.value) return; |
| | | getSalesLedgerWithProducts({ id: editData.value.id, type: 1 }).then(res => { |
| | | productData.value = res.productData; |
| | | }); |
| | | console.log(editData.value); |
| | | // 填充基本信息 |
| | | form.value.salesContractNo = editData.value.salesContractNo || ""; |
| | | form.value.customerName = editData.value.customerName || ""; |
| | | form.value.projectName = editData.value.projectName || ""; |
| | | form.value.executionDate = editData.value.executionDate || ""; |
| | | form.value.paymentMethod = editData.value.paymentMethod || ""; |
| | | form.value.salesman = editData.value.salesman || ""; |
| | | form.value.entryPerson = editData.value.entryPerson || ""; |
| | | form.value.entryPersonName = editData.value.entryPersonName || ""; |
| | | form.value.entryDate = editData.value.entryDate || ""; |
| | | form.value.id = editData.value.id || ""; |
| | | form.value.customerId = editData.value.customerId || ""; |
| | | const row = editData.value; |
| | | |
| | | // 设置日期选择器的值 |
| | | if (editData.value.executionDate) { |
| | | pickerDateValue.value = editData.value.executionDate; |
| | | } |
| | | // 先用列表行同步铺一遍,保证请求返回前表单不是空的 |
| | | form.value.salesContractNo = row.salesContractNo || ""; |
| | | form.value.customerName = row.customerName || ""; |
| | | form.value.customerId = row.customerId || ""; |
| | | form.value.projectId = row.projectId ?? ""; |
| | | form.value.projectName = row.projectName || ""; |
| | | form.value.executionDate = row.executionDate || ""; |
| | | form.value.paymentMethod = row.paymentMethod || ""; |
| | | form.value.deliveryDate = row.deliveryDate || ""; |
| | | form.value.signingUnitId = row.signingUnitId ?? ""; |
| | | form.value.officeId = row.officeId ?? ""; |
| | | form.value.groupLeaderId = row.groupLeaderId ?? ""; |
| | | form.value.isInvoice = row.isInvoice ?? 0; |
| | | form.value.isBid = row.isBid ?? 0; |
| | | form.value.bidPrice = row.bidPrice ?? ""; |
| | | form.value.unbidReason = row.unbidReason || ""; |
| | | form.value.contractOriginalReturnDate = |
| | | row.contractOriginalReturnDate || ""; |
| | | form.value.remarks = row.remarks || ""; |
| | | form.value.salesman = row.salesman || ""; |
| | | form.value.entryPerson = row.entryPerson || ""; |
| | | form.value.entryPersonName = row.entryPersonName || ""; |
| | | form.value.entryDate = row.entryDate || ""; |
| | | form.value.year = row.year || ""; |
| | | form.value.id = row.id || ""; |
| | | previousProjectId.value = form.value.projectId; |
| | | |
| | | // 详情接口字段比列表行全,再整体覆盖一次补齐新增字段 |
| | | getSalesLedgerWithProducts({ id: row.id, type: 1 }) |
| | | .then(res => { |
| | | const data = res?.data ?? res ?? {}; |
| | | productData.value = data.productData || []; |
| | | form.value = { ...form.value, ...data, id: row.id }; |
| | | previousProjectId.value = form.value.projectId ?? ""; |
| | | if (form.value.executionDate) { |
| | | form.value.year = Number( |
| | | String(form.value.executionDate).substring(0, 4) |
| | | ); |
| | | } |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "获取台账详情失败", icon: "none" }); |
| | | }); |
| | | }; |
| | | const getUserList = () => { |
| | | userListNoPage().then(res => { |
| | |
| | | }); |
| | | }; |
| | | |
| | | // 项目下拉(需求五.1 的联动入口) |
| | | const getProjectOptions = () => { |
| | | loadProjectOptions() |
| | | .then(options => { |
| | | projectOptions.value = options; |
| | | }) |
| | | .catch(() => { |
| | | projectOptions.value = []; |
| | | }); |
| | | }; |
| | | |
| | | // 签单单位 / 办事处共用部门下拉,组负责人用员工下拉 |
| | | const getOrgOptions = async () => { |
| | | const [depts, users] = await Promise.all([ |
| | | getDept().catch(() => ({})), |
| | | userListNoPageByTenantId().catch(() => ({})), |
| | | ]); |
| | | deptOptions.value = (depts?.data || []).map(item => ({ |
| | | name: item.deptName, |
| | | value: item.deptId, |
| | | })); |
| | | const rows = users?.data || []; |
| | | userOptions.value = (Array.isArray(rows) ? rows : []).map(item => ({ |
| | | name: item.nickName || item.userName || `用户${item.userId}`, |
| | | value: item.userId, |
| | | })); |
| | | }; |
| | | |
| | | // 需求三.6:招投标中标后带项目信息跳进来(utils/prefill.js 写入) |
| | | const applyPrefillProject = () => { |
| | | const prefill = takePrefillProject(); |
| | | if (!prefill) return; |
| | | form.value.projectId = prefill.projectId ?? ""; |
| | | form.value.projectName = prefill.projectName || ""; |
| | | if (prefill.customerId) form.value.customerId = prefill.customerId; |
| | | if (prefill.customerName) form.value.customerName = prefill.customerName; |
| | | previousProjectId.value = form.value.projectId; |
| | | if (form.value.projectId) applyProject(form.value.projectId); |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | // 获取页面参数 |
| | | operationType.value = uni.getStorageSync("operationType") || ""; |
| | |
| | | getCustomerList(); |
| | | // 获取产品大类tree数据 |
| | | getProductOptions(); |
| | | // 获取项目 / 部门 / 员工下拉 |
| | | getProjectOptions(); |
| | | getOrgOptions(); |
| | | // 赋值默认信息 |
| | | if (operationType.value === "add") { |
| | | setUserInfo(); |
| | | applyPrefillProject(); |
| | | } |
| | | |
| | | // 获取编辑数据并填充表单 |
| | |
| | | |
| | | <style lang="scss"> |
| | | @import "@/static/scss/form-common.scss"; |
| | | |
| | | .switch-row { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | width: 100%; |
| | | } |
| | | |
| | | .product-summary { |
| | | margin: 12px 0 0; |
| | | padding: 12px 14px; |
| | | background: #f8fbff; |
| | | border-radius: 10px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .summary-row { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | } |
| | | |
| | | .summary-label { |
| | | font-size: 14px; |
| | | color: #606266; |
| | | } |
| | | |
| | | .summary-value { |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | color: #22324d; |
| | | } |
| | | |
| | | .summary-value.highlight { |
| | | color: #2979ff; |
| | | } |
| | | </style> |