2025-09-09 18:48:53 +08:00

621 lines
29 KiB
TypeScript

/*
* @Author: zzy 411037547@qq.com
* @Date: 2022-05-24 16:03:36
* @LastEditors: cclu 1106109051@qq.com
* @LastEditTime: 2024-12-16 11:06:29
* @FilePath: \cloud-platform\src\pages\reports\Contract\index.tsx
* @Description: 合同应收预警模块
*/
import moment from 'moment';
import numeral from 'numeral';
import ReactToPrint from 'react-to-print';
import ProTable from '@ant-design/pro-table';
import ProCard from '@ant-design/pro-card';
import useRequest from "@ahooksjs/use-request";
import SubMenu from 'antd/lib/menu/SubMenu';
import { PageContainer } from '@ant-design/pro-layout';
import { MenuFoldOutlined } from '@ant-design/icons';
import { fmoney, getServerpartTree } from '@/services/options';
import { contractType } from '@/pages/contract/emun';
import { connect, history } from 'umi';
import { useEffect, useRef, useState } from 'react';
import { Avatar, Button, Descriptions, Drawer, Menu, message, Space, Statistic, Typography } from 'antd';
import type { ColumnsState } from '@ant-design/pro-table';
import type { ConnectState } from '@/models/connect';
import type { ProFormInstance } from '@ant-design/pro-form';
import type { ActionType, ProColumns } from '@ant-design/pro-table';
import type { CurrentUser } from '@/models/user';
import type { RevenueConfirmModel, RevenueConfirmParams } from './data';
import type { BusinessProjectModel } from '@/pages/BussinessProject/data';
import { getRevenueConfirmList } from './service';
import { printContract, handlePrint } from "@/utils/utils";
import { getProjectDetail } from '@/pages/BussinessProject/service';
import { exportExcel, printOutBody } from '@/utils/utils';
import RevenueList from '@/pages/BussinessProject/components/RevenueList';
import '@/pages/merchantManagement/style.less';
import session from "@/utils/session";
import { getDetail } from "@/pages/basicManage/Serverpart/service";
import PageTitleBox from '@/components/PageTitleBox';
const { Text } = Typography;
// 标题金额拆分小数点前后显示大小
const amountDom = (value: any) => {
const stringValue = `${value}`
const [intValue, floatValue] = stringValue.split(".")
return floatValue ?
<><Text type="warning" style={{
fontSize: 36, lineHeight: 1,
fontFamily: "Bahnschrift Regular"
}}>{numeral(intValue || 0).format("0,0")}</Text><Text type="warning" style={{
fontSize: 18,
fontFamily: "Bahnschrift Regular"
}}>.{floatValue}</Text>
<Text type="warning" style={{ fontSize: 14 }}></Text></>
:
<><Text type="warning" style={{
fontSize: 36, lineHeight: 1,
fontFamily: "Bahnschrift Regular"
}}>{intValue}</Text><Text type="warning" style={{ fontSize: 14 }}></Text></>
}
/**
* @description: 合同应收预警报表页面
* @param {currentUser} 当前登录用户信息
* @return { React.FC } 页面主体
*/
const ShareBenefit: React.FC<{ currentUser?: CurrentUser }> = (props) => {
const { currentUser } = props
const actionRef = useRef<ActionType>() // 表格对应的对象
const [showDetail, setShowDetail] = useState<boolean>(false) // 是否显示详情
const [currentRow, setCurrentRow] = useState<RevenueConfirmModel | any>(undefined) // 选中的当前行
const [selectShops, setSelectShops] = useState<any[]>() // 选中的数量
const [curProject, setProject] = useState<BusinessProjectModel | any>(undefined) // 选中的当前行
const [printOut, setPrintOut] = useState<any>(); // 打印数据的内容
const [currentServiceName, setServiceName] = useState<any>();// 拿到服务区的名字还为截取
// 树相关的属性和方法
const [currenMenu, setCurrenMenu] = useState<any>(); // 当前选中的左侧菜单
const [selectedId, setSelectedId] = useState<string[]>([])
const [collapsible, setCollapsible] = useState<boolean>(false)
// 选中的服务区名称
const [serviceName, setSelectServiceName] = useState<string>()
// 加载服务区树
const { loading: treeLoading, data: treeView = [] } = useRequest(() => { return getServerpartTree(currentUser?.ProvinceCode) })
// 表格数据
const [tableData, setTableData] = useState<any>()// 表格数据
// 根据左侧选中的菜单加载右侧数据
const loadSelectedId = async (item?: any) => {
setSelectedId(item.selectedKeys || []) // 选中的子菜单key
const [type, value] = item.key.split('-')
if (type === '1' && item.keyPath) {
const newCurrentMenu = item.keyPath[0].split('-').length > 1 ? item.keyPath[0].split('-')[1] : item.keyPath[0]
setCurrenMenu(newCurrentMenu)
const service = await getDetail(newCurrentMenu);
setServiceName(service.SERVERPART_NAME)
}
else {
const newCurrentMenu = value
// 父级菜单新老相等 并且触发点击为 点击父级菜单
if (newCurrentMenu === currenMenu && type === '0') {
return
}
setCurrenMenu(newCurrentMenu)
}
actionRef?.current?.reload()
}
// 生成左侧菜单
const getMenuDom = (data: any[], callback: (item: any) => void) => {
return (data.map((element: any) => {
if (element.children && element.children.length > 0) {
return (
<SubMenu title={element.label}
icon={element.ico ? <Avatar src={element.ico} size={16} style={{ marginRight: 4 }} shape="square" /> : null}
key={`${element.key || element.value}`}
onTitleClick={(item) => {
// 选中一级菜单
if (!currenMenu || item.key !== `${currenMenu?.key}`) {
callback.call(callback, item)
}
item.domEvent.stopPropagation();
}}
>
{element.children && element.children.length > 0 && getMenuDom(element.children, callback)}
</SubMenu>
)
}
return (<Menu.Item icon={element.ico ? <Avatar src={element.ico} size={16} shape="square" /> : null}
key={`${element.key || element.value}`}>{element.label}</Menu.Item>)
}))
}
const revenuenColumns: ProColumns<RevenueConfirmModel>[] = [
{
title: '合作单位',
dataIndex: 'MERCHANTS_NAME',
align: 'center',
hideInSearch: true,
},
{
title: '统计时间',
dataIndex: 'search_date',
valueType: 'dateRange',
hideInTable: true,
colSize: 2,
initialValue: [moment().add(-1, 'day').format('YYYY-01-01'), moment().add(-1, 'day').format('YYYY-MM-DD')],
search: {
transform: (value) => {
return {
StartDate: value[0],
EndDate: value[1],
};
},
},
},
{
title: '开始日期',
dataIndex: 'BUSINESS_STARTDATE',
valueType: "date",
width: 110,
align: 'center',
hideInSearch: true,
},
{
title: '结束日期',
dataIndex: 'BUSINESS_ENDDATE',
valueType: "date",
width: 110,
align: 'center',
hideInSearch: true,
},
{
title: '经营模式',
dataIndex: 'BUSINESS_TYPE',
valueType: 'select',
valueEnum: contractType,
width: 110,
align: "center",
hideInTable: true,
initialValue: '1000'
},
{
title: '营业额',
dataIndex: 'ACTUAL_REVENUE',
valueType: "money",
width: 140,
align: "center",
hideInSearch: true,
render: (_, record) => {
return <a onClick={async () => {
// 查询经营项目信息详情
// const project = await getProjectDetail(record.BUSINESSPROJECT_ID);
getProjectDetail(record.BUSINESSPROJECT_ID).then((project) => {
setProject({ ...project, BUSINESSPROJECT_ID: project.BUSINESSPROJECT_ID })
})
// 点击实际营业额时 打开抽屉 展示每日营收数据
setCurrentRow(record)
setShowDetail(true)
}}>{record.ACTUAL_REVENUE ? fmoney(record.ACTUAL_REVENUE, 2) : ''}</a>
},
},
{
title: '提成',
dataIndex: 'GUARANTEERATIO',
valueType: 'digit',
align: "center",
width: 110,
hideInSearch: true,
render: (_, record) => {
return `${record.GUARANTEERATIO}%`
},
},
{
title: '提成额',
dataIndex: 'PARTYA_SHAREPROFIT',
valueType: "money",
align: "center",
width: 140,
hideInSearch: true
},
{
title: '保证租金',
dataIndex: 'GUARANTEE_AMOUNT',
valueType: "money",
width: 140,
align: "center",
hideInSearch: true
},
{
title: '备注说明',
dataIndex: 'REVENUECONFIRM_DESC',
hideInSearch: true,
render: () => {
return ''
},
}
]
return (
<div ref={(el) => {
// 打印报表
setPrintOut(el);
}}>
<PageContainer header={{
title: '',
breadcrumb: {},
}}
style={{
width: '100%',
height: '100%'
}}>
<ProCard split="vertical" style={{ backgroundColor: '#fff', width: 'calc(100vw - 260px)', height: 'calc(100vh - 130px)' }}>
<ProCard
style={{ height: '100%' }}
className="pageTable-leftnav"
bodyStyle={{ padding: 0, paddingTop: 20, paddingLeft: 20 }}
extra={<MenuFoldOutlined onClick={() => { setCollapsible(!collapsible) }} />}
colSpan={!collapsible ? "300px" : "60px"}
title={!collapsible ? "请选择服务区" : ""}
headerBordered
collapsed={collapsible}
>
{!treeLoading && <Menu
mode="inline"
style={{ height: 'calc(100% - 100px)', overflowY: 'auto', overflowX: 'hidden' }}
selectedKeys={selectedId}
onSelect={(item) => {
// 拿到选择的服务区名称给导出的文件赋值
if (treeView && treeView.length > 0) {
treeView.forEach((i: any) => {
if (i.children && i.children.length > 0) {
i.children.forEach((subItem: any) => {
if (subItem.key === item.key) {
setSelectServiceName(subItem.label)
}
})
}
})
}
loadSelectedId(item)
}}
>
{getMenuDom(treeView, loadSelectedId)}
</Menu>}
</ProCard>
<ProCard bodyStyle={{ paddingTop: 0, paddingBottom: 0, paddingRight: 0 }}>
<ProTable<RevenueConfirmModel>
cardProps={{ // 去除表格内边距
bodyStyle: { padding: 24 }
}}
bordered={true}
rowKey="REVENUECONFIRM_ID"
headerTitle={<PageTitleBox props={props} />}
actionRef={actionRef}
search={{ span: 6, labelWidth: 'auto' }}
// manualRequest={true} // 是否需要手动触发首次请求, 配置为 true 时不可隐藏搜索表单
request={async (pramas, sorter) => {
const sortstr = Object.keys(sorter).map(n => {
const value = sorter[n]
return value ? `${n} ${value.replace('end', '')}` : ''
})
const data = await getRevenueConfirmList({
ServerpartId: selectedId && selectedId.length > 0 ? selectedId[0].split('-')[1] : currenMenu || '0',
StartDate: pramas.StartDate || '',
EndDate: pramas.EndDate || '',
sortstr: sortstr.toString()
} as RevenueConfirmParams
)
setTableData(data.data)
return {
...data, data: data.data.map((n: RevenueConfirmModel) => {
return { ...n, shopids: n.SERVERPARTSHOP_ID ? n.SERVERPARTSHOP_ID.split(',') : [] }
})
}
}}
options={false}
columns={revenuenColumns}
pagination={false}
rowSelection={{
onChange: (selectedRowKeys, selectedRows) => {
setSelectShops(selectedRows)
}
}}
toolbar={{
actions: [
// react打印插件
<ReactToPrint
// 打印内容的页面留白
pageStyle="@page { 15mm 15mm}"
// 显示的东西吧 button 在里面被点击触发content里面的事件
trigger={() => (
<Button key="printout" type="default"
>
</Button>
)}
// 点击触发的事件
content={() => {
// printOut是整个页面的dom 一般情况都是有的
if (printOut) {
// 标题 先找到标题的dom元素
const title = document.createElement('p')
// 设置标题dom元素的内容
title.innerHTML = `${currentServiceName}合作单位保底提成结算表`
// 给标题dom设置样式
title.setAttribute('style', 'font-size:20px;font-weight:600;display:flex;width:100%;justify-content: center;')
// 日期时间 timeUnit为父节点 time和date为子节点 显示打印内容标题下面 表格上面的那一块区域
const timeUnit = document.createElement('div')
const time = document.createElement('div')
const date = document.createElement('div')
// 时间显示的内容
time.innerHTML = `日期: ${moment().format('YYYY年MM月DD日')}`
date.innerHTML = `单位: 元`
// 加入到父节点中去
timeUnit.appendChild(time)
timeUnit.appendChild(date)
// 样式
timeUnit.setAttribute('style', 'width:100%;display:flex;justify-content: space-between;margin-bottom:15px;')
// 数据内容 打印的表格dom
// 表格的dom元素
const dataList = document.getElementsByClassName('ant-table-content')
// 克隆出来不影响原页面
const domNoChange = dataList[0].cloneNode(true)
// 拿到是不是有已经被选中的行
const length = selectShops ? selectShops.length : false
if (length) {
// 拿到表头的dom
const tableHeader = domNoChange.getElementsByClassName('ant-table-thead')
// 表头增加样式
tableHeader[0].setAttribute('style', 'width:100%;background: #fafafa;')
// 拿到每一列的标题
const th = tableHeader[0].getElementsByTagName("th");
// 由于页面的表头和效果图要的不一样 所以就自定义一个效果图上要的表头数组
// 由于这个页面莫名其妙在每行的后面都有一个空节点,所以最后给他赋值个空的
const titleText = ['合作单位', '结算起止时间', '营业额', '提成标准', '提成额', '保证租金', '备注说明', '']
const headList: { innerHTML: string; }[] = []
// th的第一个节点删除是因为要把选择框的节点去掉 打印不能有选择框出现
th.forEach((item: { innerHTML: string; }, index: string | number) => {
if (index === 0) {
item.remove()
} else if (index <= 7 && index !== 0) {
// 页面需要的节点 给他赋值上需要的标题 然后添加到一个数组里面
item.innerHTML = titleText[index]
headList.push(item)
}
})
// 不让标题内容换行 minWidth无法适配 只能不换行来满足需求
th.forEach((item: { innerHTML: string; }, index: string | number) => {
// 因为最后一个节点还是空节点所以不让最后一个不换行 其实问题也不大
if (index !== th.length - 1) {
item.style.whiteSpace = 'nowrap'
}
})
// 表单打印的数据内容
// 表单的数据节点
const tableBody = domNoChange.getElementsByClassName('ant-table-tbody')
// 每一行数据
const tr = tableBody[0].getElementsByTagName('tr')
// 哪几行是没被选中的索引列表
const numList: any[] = []
// 遍历每一行数据
tr.forEach((item: { getElementsByClassName: (arg0: string) => any; }, index: any) => {
// 拿到打钩的那个节点 如果这个节点下的checked节点为true那就是被选中了 false就是没选中
const isChecked = item.getElementsByClassName('ant-checkbox-input')
// 没被选中的行数的索引被添加到数组中
if (!isChecked[0].checked) {
numList.push(index)
}
})
// 倒序的for循环 正序的话当一个节点删除时 下一个节点会到刚刚删除的节点位置 无法参与判断
// 如果是没选中的节点就直接移除掉 留下来的节点都是选中的节点
for (let i = tr.length - 1; i >= 0; i -= 1) {
numList.forEach(item => {
if (i === item) {
tr[i].remove()
}
})
}
// 选中的节点继续遍历
tr.forEach((item: { getElementsByClassName: (arg0: string) => any; }, index: any) => {
// 因为页面的dom元素和要求图的dom元素不一样 所以用list先存页面中的数据
const list: any[] = []
// 获得每一行的 每一列对应的节点
const td = item.getElementsByTagName('td')
// 遍历一行的数据
td.forEach((subItem: { innerHTML: any; }, subIndex: number) => {
// 背景都变成白色
subItem.style.backgroundColor = 'white'
// 因为td的第一项是选择框 所以不放进数据列表中
if (subIndex >= 1) {
list.push(subItem.innerHTML)
}
})
// 把选择框的dom节点移除
td[0].remove()
// 拼接数据
td.forEach((subItem: { innerHTML: string; }, subIndex: number) => {
subItem.style.backgroundColor = 'white'
if (subIndex === 0) {
subItem.innerHTML = list[0]
subItem.style.whiteSpace = 'nowrap'
} else if (subIndex === 1) {
// 时间数据的拼接
subItem.innerHTML = `${moment(list[1]).format('YYYY年MM月DD日')} - ${moment(list[2]).format('YYYY年MM月DD日')}`
subItem.style.whiteSpace = 'nowrap'
} else if (subIndex > 1 && subIndex <= 6) {
// 因为上面拼接了一个 所以索引加1 小于6 是因为 要求的数据只有七行 但是会有一个空节点
subItem.innerHTML = list[subIndex + 1]
if (subIndex !== 6) {
// 备注就让它可以换行
subItem.style.whiteSpace = 'nowrap'
}
} else {
subItem.style.backgroundColor = 'white'
}
})
})
} else {
// 没有选中任何一行 就默认打印全部
// 表头dom节点 逻辑和上面差不多 就是没有了是否选中的判断
const tableHeader = domNoChange.getElementsByClassName('ant-table-thead')
const th = tableHeader[0].getElementsByTagName("th");
const titleText = ['合作单位', '结算起止时间', '营业额', '提成标准', '提成额', '保证租金', '备注说明', '']
const headList: { innerHTML: string; }[] = []
th[0].remove()
th.forEach((item: { innerHTML: string; }, index: string | number) => {
if (index !== th.length - 1) {
item.style.whiteSpace = 'nowrap'
}
if (index <= 7 && index >= 0) {
item.innerHTML = titleText[index]
headList.push(item)
} else {
item.style.maxWidth = '0px'
}
})
// 数据内容
const tableBody = domNoChange.getElementsByClassName('ant-table-tbody')
const tr = tableBody[0].getElementsByTagName('tr')
// 先遍历每一行的数据 拿到每一行中的数据
tr.forEach((item: { getElementsByTagName: (arg0: string) => any; }, index: any) => {
const list: any[] = []
const td = item.getElementsByTagName('td')
// 再对每一行的数据进行遍历 把原本的值先存一份
td.forEach((subItem: { innerHTML: any; }, subIndex: number) => {
if (subIndex >= 1) {
list.push(subItem.innerHTML)
}
})
td[0].remove()
td.forEach((subItem: { innerHTML: string; }, subIndex: number) => {
if (subIndex === 0) {
subItem.innerHTML = list[0]
subItem.style.whiteSpace = 'nowrap'
} else if (subIndex === 1) {
subItem.innerHTML = `${moment(list[1]).format('YYYY年MM月DD日')} - ${moment(list[2]).format('YYYY年MM月DD日')}`
subItem.style.whiteSpace = 'nowrap'
} else if (subIndex > 1 && subIndex <= 6) {
subItem.innerHTML = list[subIndex + 1]
if (subIndex !== 6) {
subItem.style.whiteSpace = 'nowrap'
}
} else {
item.setAttribute('style', 'width: 0%')
}
})
})
}
// 表单底部签字盖章数据
// 创建一个父节点 下面三个子节点 然后flex布局变掉 加一个当前用户的名称就可以
const currentUser = session.get('currentUser');
const signAndSeal = document.createElement('div')
signAndSeal.setAttribute('style', 'width:100%;display:flex;align-item:center;justify-content: space-between;margin-top:20px')
const serviceSeal = document.createElement('div')
const companySeal = document.createElement('div')
const makeExcel = document.createElement('div')
serviceSeal.innerHTML = '服务区 (签字盖章) :'
companySeal.innerHTML = '合作单位 (签字盖章) :'
makeExcel.innerHTML = `制表人 : ${currentUser.Name}`
makeExcel.setAttribute('style', 'text-align:right')
signAndSeal.appendChild(serviceSeal)
signAndSeal.appendChild(companySeal)
signAndSeal.appendChild(makeExcel)
// printContract方法也就是把第一个形参数组里面的每一项拼到一个父节点的里面 然后return出来
const ele = printContract([title || '', timeUnit || '', domNoChange || '', signAndSeal || ''], '');
// content方法中return出来的值 就会自动调起ReactToPrint依赖 然后根据ele来打印
return ele
}
// 拿不到整个页面dom就输出空 但是一般不会拿不到
return ''
}}
/>,
<Button
type="primary"
onClick={async () => {
const nowColumns = revenuenColumns.filter(n => !n.hideInTable)
const fat = nowColumns.map((n: any, index: number) => {
if (n?.children) {
return n?.children.map((m: { title: any; }) => {
const title = index === 0 ? m.title : n.title + m.title
return { ...m, title }
})
}
return n
})
console.log('tableData', tableData)
const dataList: any = JSON.parse(JSON.stringify(tableData))
dataList.forEach((item: any) => {
item.BUSINESS_ENDDATE = moment(item.BUSINESS_ENDDATE).format('YYYY-MM-DD')
item.BUSINESS_STARTDATE = moment(item.BUSINESS_STARTDATE).format('YYYY-MM-DD')
})
const success = await exportExcel(
// flat 就是数组维度降一层
fat.flat(),
dataList || [],
`${serviceName}合作单位保底提成结算表${moment().format('YYYY/MM/DD')}`,
);
if (success.message !== 'ok') {
message.info({ content: success.message });
}
}}
>
excel
</Button>
// <ReactToPrint
// pageStyle="@page { 15mm 15mm}"
// trigger={() => (
// <Button key="printout" type="default"
// style={{marginRight: '30px'}}
// >
// 导出
// </Button>
// )}
// content={() => {
// // printOut是整个页面的dom 一般情况都是有的
// if (printOut){
// const ele = handlePrint(printOut)
// return ele
// }
// return ''
// }}
// />
]
}}
/>
</ProCard>
</ProCard>
{/* 查看项目详情 右侧弹出的抽屉 */}
<Drawer
width="80%"
className="project-drawer"
visible={showDetail} // 抽屉弹框是否显示状态
onClose={() => { // 关闭抽屉 设置抽屉状态为关闭
setShowDetail(false);
setProject(undefined);
setCurrentRow(undefined);
}}
bodyStyle={{ backgroundColor: "#f9f9f9", padding: 0 }}
closable={false}
>
{/* 抽屉打开时 加载项目详情组件 */}
{showDetail && <RevenueList propsBP={curProject} propsRC={currentRow}
ShopRoyaltyId={currentRow?.SHOPROYALTY_ID ? currentRow?.SHOPROYALTY_ID : 0}
ShopIds={currentRow?.SERVERPARTSHOP_ID ? currentRow?.SERVERPARTSHOP_ID : ''}
StartDate={moment(currentRow?.BUSINESS_STARTDATE).format('YYYY-MM-DD')}
EndDate={moment(currentRow?.BUSINESS_ENDDATE).format('YYYY-MM-DD')} BusinessProjectId={currentRow?.BUSINESSPROJECT_ID}></RevenueList>}
</Drawer>
</PageContainer>
</div>
);
}
export default ShareBenefit;