| | |
| | | <!-- 使用通用页面头部组件 --> |
| | | <PageHeader title="销售台账" |
| | | @back="goBack" /> |
| | | <!-- 状态Tab页筛选 --> |
| | | <view class="sales-ledger-status-bar"> |
| | | <view v-for="tab in salesLedgerStatusTabs" |
| | | :key="tab.key" |
| | | class="sales-ledger-status-tab" |
| | | :class="{ 'is-active': activeStatusTab === tab.key }" |
| | | @click="handleStatusTabChange(tab.key)"> |
| | | {{ tab.label }} |
| | | </view> |
| | | </view> |
| | | <!-- 搜索和筛选区域 --> |
| | | <view class="search-section"> |
| | | <view class="search-bar"> |
| | | <view class="search-input"> |
| | | <up-input class="search-text" |
| | | placeholder="请输入销售合同号搜索" |
| | | v-model="salesContractNo" |
| | | @change="getList" |
| | | v-model="searchForm.salesContractNo" |
| | | @change="handleQuery" |
| | | clearable /> |
| | | </view> |
| | | <view class="filter-button" |
| | | @click="getList"> |
| | | @click="handleQuery"> |
| | | <up-icon name="search" |
| | | size="24" |
| | | color="#999"></up-icon> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <!-- 销售台账瀑布流 --> |
| | | <!-- 销售台账列表 --> |
| | | <view class="ledger-list" |
| | | v-if="ledgerList.length > 0"> |
| | | <view v-for="(item, index) in ledgerList" |
| | |
| | | </view> |
| | | <text class="item-id">{{ item.salesContractNo }}</text> |
| | | </view> |
| | | <!-- <view class="item-tag">--> |
| | | <!-- <text class="tag-text">{{ item.recorder }}</text>--> |
| | | <!-- </view>--> |
| | | <view class="item-tag" |
| | | v-if="item.reviewStatus === 0"> |
| | | <text class="tag-text warning">待审核</text> |
| | | </view> |
| | | <view class="item-tag" |
| | | v-else-if="item.reviewStatus === 1"> |
| | | <text class="tag-text success">已审核</text> |
| | | </view> |
| | | <view class="item-tag" |
| | | v-else> |
| | | <text class="tag-text">已反审</text> |
| | | </view> |
| | | </view> |
| | | <up-divider></up-divider> |
| | | <view class="item-details"> |
| | |
| | | <text class="detail-value">{{ item.customerName }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">业务员</text> |
| | | <text class="detail-value">{{ item.salesman }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">项目名称</text> |
| | | <text class="detail-value">{{ item.projectName }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">付款方式</text> |
| | | <text class="detail-value">{{ item.paymentMethod }}</text> |
| | | <text class="detail-label">业务员 / 项目</text> |
| | | <text class="detail-value">{{ item.salesman }} / {{ item.projectName }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">合同金额(元)</text> |
| | | <text class="detail-value highlight">{{ item.contractAmount }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">签订日期</text> |
| | | <text class="detail-value">{{ item.executionDate }}</text> |
| | | <text class="detail-label">面积 / 数量</text> |
| | | <text class="detail-value">{{ item.productTotalArea }}㎡ * {{ item.productTotalQuantity }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">发货状态</text> |
| | | <u-tag size="mini" |
| | | :type="getLedgerShippingTagType(item)">{{ |
| | | getLedgerShippingLabel(item) |
| | | }}</u-tag> |
| | | <text class="detail-label">状态</text> |
| | | <view class="status-tags"> |
| | | <u-tag size="mini" |
| | | :type="getShippingStatusType(item)">{{ getShippingStatusText(item) }}</u-tag> |
| | | <u-tag size="mini" |
| | | :type="getStockStatusTagType(item)">{{ getStockStatusLabel(item) }}</u-tag> |
| | | <u-tag size="mini" |
| | | :type="item.orderStatus === 1 ? 'success' : 'primary'">{{ item.orderStatus === 1 ? '已完成' : '进行中' }}</u-tag> |
| | | </view> |
| | | </view> |
| | | <up-divider></up-divider> |
| | | <view class="detail-info"> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">录入人</text> |
| | | <text class="detail-value">{{ item.entryPersonName }}</text> |
| | | <text class="detail-label">录入人 / 日期</text> |
| | | <text class="detail-value">{{ item.entryPersonName }} / {{ item.entryDate }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">录入日期</text> |
| | | <text class="detail-value">{{ item.entryDate }}</text> |
| | | </view> |
| | | </view> |
| | | <up-divider></up-divider> |
| | | <view class="detail-buttons"> |
| | | <u-button class="detail-button" |
| | | size="small" |
| | | type="primary" |
| | | plain |
| | | :disabled="!canLedgerShip(item)" |
| | | @click.stop="handleShip(item)"> |
| | | 发货 |
| | | </u-button> |
| | | <!-- <u-button class="detail-button" |
| | | size="small" |
| | | type="primary" |
| | | @click.stop="handleInfo('edit', item)"> |
| | | 编辑 |
| | | </u-button> |
| | | <u-button class="detail-button" |
| | | size="small" |
| | | type="primary" |
| | | plain |
| | | @click.stop="openOut(item)"> |
| | | 发货状态 |
| | | </u-button> --> |
| | | <!-- <u-button class="detail-button" |
| | | size="small" |
| | | type="error" |
| | | plain |
| | | @click.stop="handleDelete(item)"> |
| | | 删除 |
| | | </u-button> --> |
| | | </view> |
| | | </view> |
| | | </view> |
| | |
| | | class="no-data"> |
| | | <text>暂无销售台账数据</text> |
| | | </view> |
| | | <!-- 浮动操作按钮 --> |
| | | <!-- <view class="fab-button" |
| | | @click="handleInfo('add')"> |
| | | <up-icon name="plus" |
| | | size="24" |
| | | color="#ffffff"></up-icon> |
| | | </view> --> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref } from "vue"; |
| | | import { onShow } from "@dcloudio/uni-app"; |
| | | import { ledgerListPage, delLedger, productList } from "@/api/salesManagement/salesLedger"; |
| | | import { |
| | | hasShippedProducts, |
| | | getLedgerShippingLabel, |
| | | getLedgerShippingTagType, |
| | | canLedgerShip, |
| | | executeSalesLedgerShip, |
| | | } from "@/utils/salesLedgerShip"; |
| | | import useUserStore from "@/store/modules/user"; |
| | | import { ref, onMounted } from "vue"; |
| | | import { ledgerListPage } from "@/api/salesManagement/salesLedger"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | const userStore = useUserStore(); |
| | | |
| | | // 状态Tab页 |
| | | const activeStatusTab = ref("all"); |
| | | const salesLedgerStatusTabs = [ |
| | | { key: "all", label: "全部" }, |
| | | { key: "pendingReview", label: "未审核" }, |
| | | { key: "reviewed", label: "已审核" }, |
| | | { key: "reverseReviewed", label: "反审核" }, |
| | | { key: "stocked", label: "已入库" }, |
| | | { key: "delivered", label: "已发货" }, |
| | | { key: "completed", label: "已完成" }, |
| | | ]; |
| | | |
| | | // 搜索表单 |
| | | const searchForm = ref({ |
| | | salesContractNo: "", |
| | | reviewStatus: undefined, |
| | | stockStatus: undefined, |
| | | deliveryStatus: undefined, |
| | | orderStatus: undefined, |
| | | }); |
| | | |
| | | // 销售台账数据 |
| | | const ledgerList = ref([]); |
| | | |
| | | // 分页 |
| | | const page = ref({ |
| | | current: 1, |
| | | size: 10, |
| | | }); |
| | | |
| | | // 重置状态筛选 |
| | | const resetStatusFilters = () => { |
| | | searchForm.value.reviewStatus = undefined; |
| | | searchForm.value.stockStatus = undefined; |
| | | searchForm.value.deliveryStatus = undefined; |
| | | searchForm.value.orderStatus = undefined; |
| | | }; |
| | | |
| | | // Tab页切换 |
| | | const handleStatusTabChange = tabKey => { |
| | | activeStatusTab.value = tabKey; |
| | | resetStatusFilters(); |
| | | switch (tabKey) { |
| | | case "all": |
| | | break; |
| | | case "pendingReview": |
| | | searchForm.value.reviewStatus = 0; |
| | | break; |
| | | case "reviewed": |
| | | searchForm.value.reviewStatus = 1; |
| | | break; |
| | | case "reverseReviewed": |
| | | searchForm.value.reviewStatus = 2; |
| | | break; |
| | | case "stocked": |
| | | searchForm.value.stockStatus = 2; |
| | | break; |
| | | case "delivered": |
| | | searchForm.value.deliveryStatus = 5; |
| | | break; |
| | | case "completed": |
| | | searchForm.value.orderStatus = 1; |
| | | break; |
| | | default: |
| | | break; |
| | | } |
| | | handleQuery(); |
| | | }; |
| | | |
| | | // 显示加载提示 |
| | | const showLoadingToast = message => { |
| | | uni.showLoading({ |
| | | title: message, |
| | | mask: true, |
| | | }); |
| | | }; |
| | | |
| | | // 关闭提示 |
| | | const closeToast = () => { |
| | | uni.hideLoading(); |
| | | }; |
| | | |
| | | // 搜索关键词 |
| | | const salesContractNo = ref(""); |
| | | |
| | | // 销售台账数据 |
| | | const ledgerList = ref([]); |
| | | |
| | | const handleShip = item => executeSalesLedgerShip(item); |
| | | |
| | | // 返回上一页 |
| | | const goBack = () => { |
| | | uni.navigateBack(); |
| | | }; |
| | | |
| | | // 重置搜索 |
| | | const resetSearch = () => { |
| | | searchForm.value = { |
| | | salesContractNo: "", |
| | | reviewStatus: undefined, |
| | | stockStatus: undefined, |
| | | deliveryStatus: undefined, |
| | | orderStatus: undefined, |
| | | }; |
| | | activeStatusTab.value = "all"; |
| | | page.value.current = 1; |
| | | getList(); |
| | | }; |
| | | |
| | | // 查询列表 |
| | | const handleQuery = () => { |
| | | page.value.current = 1; |
| | | getList(); |
| | | }; |
| | | |
| | | // 获取列表 |
| | | const getList = () => { |
| | | showLoadingToast("加载中..."); |
| | | const page = { |
| | | current: -1, |
| | | size: -1, |
| | | |
| | | // 构建查询参数 |
| | | const params = { |
| | | ...page.value, |
| | | reviewStatusList: [0, 1, 2], |
| | | }; |
| | | ledgerListPage({ ...page, salesContractNo: salesContractNo.value }) |
| | | |
| | | // 添加搜索条件 |
| | | if (searchForm.value.salesContractNo) { |
| | | params.salesContractNo = searchForm.value.salesContractNo; |
| | | } |
| | | if (searchForm.value.reviewStatus !== undefined) { |
| | | params.reviewStatus = searchForm.value.reviewStatus; |
| | | } |
| | | if (searchForm.value.stockStatus !== undefined) { |
| | | params.stockStatus = searchForm.value.stockStatus; |
| | | } |
| | | if (searchForm.value.deliveryStatus !== undefined) { |
| | | params.deliveryStatus = searchForm.value.deliveryStatus; |
| | | } |
| | | if (searchForm.value.orderStatus !== undefined) { |
| | | params.orderStatus = searchForm.value.orderStatus; |
| | | } |
| | | |
| | | ledgerListPage(params) |
| | | .then(res => { |
| | | console.log("销售台账----", res); |
| | | ledgerList.value = res.records; |
| | | ledgerList.value = res.records || res.data || []; |
| | | closeToast(); |
| | | }) |
| | | .catch(() => { |
| | | closeToast(); |
| | | uni.showToast({ |
| | | title: "查询失败", |
| | | icon: "none", |
| | | }); |
| | | }); |
| | | }; |
| | | |
| | | // 打开详情页 |
| | | const openOut = item => { |
| | | uni.setStorageSync("outData", JSON.stringify(item)); |
| | | uni.navigateTo({ |
| | |
| | | }); |
| | | }; |
| | | |
| | | // 删除单条销售台账 |
| | | const handleDelete = async row => { |
| | | if (!row || !row.id) return; |
| | | |
| | | // 获取产品列表,用于判断是否已发货 |
| | | let products = row.children && row.children.length > 0 ? row.children : null; |
| | | if (!products) { |
| | | try { |
| | | const res = await productList({ salesLedgerId: row.id, type: 1 }); |
| | | products = res.data || res.records || []; |
| | | } catch (e) { |
| | | products = []; |
| | | } |
| | | // 获取发货状态标签类型 |
| | | const getShippingStatusType = item => { |
| | | if (item.shippingDate || item.shippingCarNumber) { |
| | | return "success"; |
| | | } |
| | | |
| | | if (hasShippedProducts(products)) { |
| | | uni.showToast({ |
| | | title: "已发货、部分发货或已有发货记录的销售订单不能删除", |
| | | icon: "none", |
| | | }); |
| | | return; |
| | | const status = item.shippingStatus; |
| | | if (status === null || status === undefined || status === "") { |
| | | return "info"; |
| | | } |
| | | |
| | | uni.showModal({ |
| | | title: "删除确认", |
| | | content: "选中的内容将被删除,是否确认删除?", |
| | | success: async res => { |
| | | if (res.confirm) { |
| | | try { |
| | | showLoadingToast("处理中..."); |
| | | await delLedger([row.id]); |
| | | closeToast(); |
| | | uni.showToast({ |
| | | title: "删除成功", |
| | | icon: "success", |
| | | }); |
| | | getList(); |
| | | } catch (e) { |
| | | closeToast(); |
| | | uni.showToast({ |
| | | title: "删除失败,请重试", |
| | | icon: "none", |
| | | }); |
| | | } |
| | | } |
| | | }, |
| | | }); |
| | | }; |
| | | // 处理台账信息操作(查看/编辑/新增) |
| | | const handleInfo = (type, row) => { |
| | | try { |
| | | // 设置操作类型 |
| | | uni.setStorageSync("operationType", type); |
| | | uni.removeStorageSync("editData"); |
| | | |
| | | // 如果是查看或编辑操作 |
| | | if (type !== "add") { |
| | | // 验证行数据是否存在 |
| | | if (!row) { |
| | | uni.showToast({ |
| | | title: "数据不存在", |
| | | icon: "error", |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | // 检查权限:只有录入人才能编辑 |
| | | if (row.entryPerson != userStore.id) { |
| | | // 非录入人跳转到只读详情页面 |
| | | uni.setStorageSync("editData", JSON.stringify(row)); |
| | | uni.navigateTo({ |
| | | url: "/pages/sales/salesAccount/view", |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | // 录入人编辑:存储数据并跳转到编辑页面 |
| | | uni.setStorageSync("editData", JSON.stringify(row)); |
| | | uni.navigateTo({ |
| | | url: "/pages/sales/salesAccount/detail", |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | // 新增操作:直接跳转到编辑页面 |
| | | uni.navigateTo({ |
| | | url: "/pages/sales/salesAccount/detail", |
| | | }); |
| | | } catch (error) { |
| | | console.error("处理台账信息操作失败:", error); |
| | | uni.showToast({ |
| | | title: "操作失败,请重试", |
| | | icon: "error", |
| | | }); |
| | | } |
| | | const statusStr = String(status).trim(); |
| | | const typeMap = { |
| | | 待发货: "info", |
| | | 待审核: "info", |
| | | 审核中: "warning", |
| | | 审核拒绝: "danger", |
| | | 审核通过: "success", |
| | | 已发货: "success", |
| | | }; |
| | | return typeMap[statusStr] || "info"; |
| | | }; |
| | | |
| | | onShow(() => { |
| | | // 页面显示时刷新列表 |
| | | // 获取发货状态文本 |
| | | const getShippingStatusText = item => { |
| | | if (item.shippingDate || item.shippingCarNumber) { |
| | | return "已发货"; |
| | | } |
| | | const status = item.shippingStatus; |
| | | if (status === null || status === undefined || status === "") { |
| | | return "待发货"; |
| | | } |
| | | const statusStr = String(status).trim(); |
| | | const textMap = { |
| | | 待发货: "待发货", |
| | | 待审核: "待审核", |
| | | 审核中: "审核中", |
| | | 审核拒绝: "审核拒绝", |
| | | 审核通过: "审核通过", |
| | | 已发货: "已发货", |
| | | }; |
| | | return textMap[statusStr] || "待发货"; |
| | | }; |
| | | |
| | | // 获取入库状态标签类型 |
| | | const getStockStatusTagType = item => { |
| | | if (item.stockStatus === 2) { |
| | | return "success"; |
| | | } else if (item.stockStatus === 1) { |
| | | return "warning"; |
| | | } |
| | | return "info"; |
| | | }; |
| | | |
| | | // 获取入库状态标签 |
| | | const getStockStatusLabel = item => { |
| | | if (item.stockStatus === 2) { |
| | | return "已入库"; |
| | | } else if (item.stockStatus === 1) { |
| | | return "部分入库"; |
| | | } |
| | | return "未入库"; |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | getList(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/styles/sales-common.scss"; |
| | | .detail-buttons { |
| | | |
| | | .sales-account { |
| | | min-height: 100vh; |
| | | background: #f8f9fa; |
| | | } |
| | | |
| | | .sales-ledger-status-bar { |
| | | display: flex; |
| | | gap: 10px; |
| | | background: #fff; |
| | | padding: 12px 16px; |
| | | overflow-x: auto; |
| | | white-space: nowrap; |
| | | gap: 12px; |
| | | border-bottom: 1px solid #eee; |
| | | } |
| | | |
| | | .sales-ledger-status-tab { |
| | | padding: 8px 16px; |
| | | border-radius: 20px; |
| | | font-size: 14px; |
| | | color: #666; |
| | | background: #f5f7fa; |
| | | transition: all 0.3s; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .sales-ledger-status-tab.is-active { |
| | | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| | | color: #fff; |
| | | } |
| | | |
| | | .search-section { |
| | | background: #fff; |
| | | margin: 16px; |
| | | padding: 12px 16px; |
| | | border-radius: 12px; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); |
| | | } |
| | | |
| | | .search-bar { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .search-input { |
| | | flex: 1; |
| | | } |
| | | |
| | | .search-text { |
| | | width: 100%; |
| | | } |
| | | |
| | | .filter-button { |
| | | width: 40px; |
| | | height: 40px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | cursor: pointer; |
| | | } |
| | | |
| | | .ledger-list { |
| | | padding: 0 16px 16px; |
| | | } |
| | | |
| | | .ledger-item { |
| | | background: #ffffff; |
| | | border-radius: 12px; |
| | | margin-bottom: 16px; |
| | | overflow: hidden; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); |
| | | padding: 0 16px; |
| | | } |
| | | |
| | | .item-header { |
| | | padding: 16px 0; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | } |
| | | |
| | | .item-left { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .document-icon { |
| | | width: 24px; |
| | | height: 24px; |
| | | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| | | border-radius: 4px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .item-id { |
| | | font-size: 15px; |
| | | color: #333; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .item-tag { |
| | | .tag-text { |
| | | font-size: 12px; |
| | | padding: 4px 8px; |
| | | border-radius: 4px; |
| | | background: #f0f2f5; |
| | | color: #666; |
| | | |
| | | &.warning { |
| | | background: #fff3cd; |
| | | color: #856404; |
| | | } |
| | | |
| | | &.success { |
| | | background: #d4edda; |
| | | color: #155724; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .item-details { |
| | | padding-bottom: 16px; |
| | | } |
| | | |
| | | .detail-row { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | margin-bottom: 12px; |
| | | |
| | | &:last-child { |
| | | margin-bottom: 0; |
| | | } |
| | | } |
| | | |
| | | .detail-label { |
| | | font-size: 13px; |
| | | color: #777777; |
| | | min-width: 100px; |
| | | } |
| | | |
| | | .detail-value { |
| | | font-size: 13px; |
| | | color: #000000; |
| | | text-align: right; |
| | | flex: 1; |
| | | margin-left: 16px; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .detail-value.highlight { |
| | | color: #667eea; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .status-tags { |
| | | display: flex; |
| | | gap: 6px; |
| | | flex-wrap: wrap; |
| | | justify-content: flex-end; |
| | | } |
| | | |
| | | .detail-info { |
| | | padding: 8px 0; |
| | | } |
| | | |
| | | .no-data { |
| | | padding: 60px 0; |
| | | text-align: center; |
| | | color: #999; |
| | | } |
| | | </style> |