| | |
| | | import { showDelConfirm } from '@/utils/antdUtils'; |
| | | import { allAreaList } from '@/utils/area'; |
| | | import { downLoad } from '@/utils/utils'; |
| | | import { DeleteOutlined, DownloadOutlined, PlusOutlined } from '@ant-design/icons'; |
| | | import { PageContainer, ProTable } from '@ant-design/pro-components'; |
| | | import { |
| | | Button, |
| | | Card, |
| | | Cascader, |
| | | Empty, |
| | | Form, |
| | | Input, |
| | | InputNumber, |
| | | message, |
| | | Modal, |
| | | Radio, |
| | | Select, |
| | | Space, |
| | | Upload, |
| | | message, |
| | | } from 'antd'; |
| | | import { CloseOutlined, DeleteOutlined, DownloadOutlined, PlusOutlined } from '@ant-design/icons'; |
| | | import { useEffect, useMemo, useState } from 'react'; |
| | | import { downLoad } from '@/utils/utils'; |
| | | import { allAreaList } from '@/utils/area'; |
| | | import { showDelConfirm } from '@/utils/antdUtils'; |
| | | import { useEffect, useState } from 'react'; |
| | | import { |
| | | batchDelete, |
| | | cityBatchDelete, |
| | | cityDelete, |
| | | cityUpdate, |
| | | classifySelect, |
| | | detail, |
| | | list, |
| | | pageList, |
| | | remove, |
| | | save, |
| | | siteList, |
| | | update, |
| | | } from './service'; |
| | | import styles from './style.less'; |
| | | |
| | | const cityOptions = (allAreaList || []).flatMap((provinceItem) => |
| | | (provinceItem.data || []).map((cityItem) => ({ |
| | | label: cityItem.city, |
| | | value: cityItem.value, |
| | | })), |
| | | ); |
| | | |
| | | const cityCascaderOptions = (allAreaList || []).map((provinceItem) => ({ |
| | | label: provinceItem.province, |
| | |
| | | })), |
| | | })); |
| | | |
| | | const cityDisplayMap = (allAreaList || []).reduce((map, provinceItem) => { |
| | | (provinceItem.data || []).forEach((cityItem) => { |
| | | map[cityItem.value] = `${provinceItem.province}>${cityItem.city}`; |
| | | }); |
| | | return map; |
| | | }, {}); |
| | | |
| | | const cityPathMap = (allAreaList || []).reduce((map, provinceItem) => { |
| | | (provinceItem.data || []).forEach((cityItem) => { |
| | | map[cityItem.city] = [provinceItem.value, cityItem.value]; |
| | | map[cityItem.value] = [provinceItem.value, cityItem.value]; |
| | | }); |
| | | return map; |
| | | }, {}); |
| | | |
| | | const emptyCityRow = () => ({ |
| | | key: `${Date.now()}-${Math.random().toString(16).slice(2)}`, |
| | | cityCode: undefined, |
| | | cityName: '', |
| | | costPrice: undefined, |
| | | infoFee: undefined, |
| | | }); |
| | | |
| | | const normalizeCityList = (list) => |
| | | (list || []).map((item, index) => ({ |
| | | key: item.id || `${item.cityCode || 'row'}-${index}`, |
| | | cityCode: cityPathMap[item.cityCode] || item.cityCode || undefined, |
| | | cityName: item.cityName || '', |
| | | costPrice: item.costPrice, |
| | | infoFee: item.infoFee, |
| | | })); |
| | | |
| | | const getCityCodeValue = (cityCode) => (Array.isArray(cityCode) ? cityCode[cityCode.length - 1] : cityCode); |
| | | |
| | | const isSuccess = (res) => String(res?.code) === '200' || res?.code === 200; |
| | | |
| | | const getTemplateId = (record = {}) => record.templateId || record.id; |
| | | |
| | | const formatMoney = (value) => { |
| | | if (value === undefined || value === null || value === '') { |
| | |
| | | return Number.isNaN(numberValue) ? value : `¥ ${numberValue.toFixed(2)}`; |
| | | }; |
| | | |
| | | const getTemplateOptionLabel = (record = {}) => |
| | | `${record.classifyName || '未命名服务'} / 默认成本 ${formatMoney(record.defaultCostPrice)} / 默认信息费 ${formatMoney(record.defaultInfoFee)}`; |
| | | |
| | | const getTemplateTabLabel = (record = {}) => |
| | | record.platformName || record.platformCode || `模板${record.id || ''}`; |
| | | |
| | | export default function DeductionTemplatePage() { |
| | | const [form] = Form.useForm(); |
| | | const [queryForm] = Form.useForm(); |
| | | const [platformForm] = Form.useForm(); |
| | | const [cityForm] = Form.useForm(); |
| | | const [modalVisible, setModalVisible] = useState(false); |
| | | const [modalMode, setModalMode] = useState('add'); |
| | | const [cityModalVisible, setCityModalVisible] = useState(false); |
| | | const [saving, setSaving] = useState(false); |
| | | const [citySaving, setCitySaving] = useState(false); |
| | | const [detailLoading, setDetailLoading] = useState(false); |
| | | const [importLoading, setImportLoading] = useState(false); |
| | | const [tableLoading, setTableLoading] = useState(false); |
| | | const [detailLoading, setDetailLoading] = useState(false); |
| | | const [selectedRowKeys, setSelectedRowKeys] = useState([]); |
| | | const [classifyOptions, setClassifyOptions] = useState([]); |
| | | const [classifyMap, setClassifyMap] = useState({}); |
| | | const [activePlatform, setActivePlatform] = useState(''); |
| | | const [activeTemplateId, setActiveTemplateId] = useState(); |
| | | const [platformTabs, setPlatformTabs] = useState([]); |
| | | const [platformModalVisible, setPlatformModalVisible] = useState(false); |
| | | const [siteOptions, setSiteOptions] = useState([]); |
| | | const [pageState, setPageState] = useState({ current: 1, pageSize: 10 }); |
| | | const [tableData, setTableData] = useState([]); |
| | | const [templateOptions, setTemplateOptions] = useState([]); |
| | | const [total, setTotal] = useState(0); |
| | | const [platformSaving, setPlatformSaving] = useState(false); |
| | | |
| | | const platformOptionList = useMemo( |
| | | () => { |
| | | const map = new Map(); |
| | | platformTabs.forEach((item) => { |
| | | if (item.platformCode && !map.has(item.platformCode)) { |
| | | map.set(item.platformCode, { |
| | | label: item.platformName || item.platformCode, |
| | | value: item.platformCode, |
| | | }); |
| | | } |
| | | }); |
| | | return [...map.values()]; |
| | | }, |
| | | [platformTabs], |
| | | ); |
| | | |
| | | const resolvePlatformLabel = (code) => |
| | | platformTabs.find((item) => item.platformCode === code)?.platformName || code || ''; |
| | | |
| | | const currentTitle = useMemo( |
| | | () => (modalMode === 'detail' ? '扣费模板详情' : modalMode === 'edit' ? '编辑扣费模板' : '新增扣费模板'), |
| | | [modalMode], |
| | | ); |
| | | |
| | | useEffect(() => { |
| | | classifySelect().then((res) => { |
| | | const list = Array.isArray(res?.data) ? res.data : []; |
| | | setClassifyOptions( |
| | | list.map((item) => ({ |
| | | label: item.subClassify || item.classifyName || item.name, |
| | | value: item.id, |
| | | })), |
| | | ); |
| | | const map = {}; |
| | | list.forEach((item) => { |
| | | map[item.id] = { |
| | | classifyName: item.subClassify || item.classifyName || item.name || '', |
| | | classifyIcon: item.classificationPicture || item.classifyIcon || '', |
| | | }; |
| | | }); |
| | | setClassifyMap(map); |
| | | }); |
| | | }, []); |
| | | |
| | | const fetchTable = async (nextPageState = pageState, nextFilters = {}) => { |
| | | const templateId = nextFilters.templateId || activeTemplateId; |
| | | if (!templateId) { |
| | | setTableData([]); |
| | | setTotal(0); |
| | | setTableLoading(false); |
| | | return; |
| | | } |
| | | const fetchTable = async (nextPageState = pageState, filters = queryForm.getFieldsValue()) => { |
| | | setTableLoading(true); |
| | | try { |
| | | const res = await pageList({ |
| | | templateId, |
| | | ...filters, |
| | | pageNum: nextPageState.current, |
| | | pageSize: nextPageState.pageSize, |
| | | cityCode: getCityCodeValue(nextFilters.cityCode), |
| | | }); |
| | | if (!isSuccess(res)) { |
| | | message.error(res?.msg || '加载失败'); |
| | | return; |
| | | } |
| | | setTableData(res?.data?.records || []); |
| | | setTotal(res?.data?.total || 0); |
| | |
| | | } |
| | | }; |
| | | |
| | | const fetchTemplateTabs = async (params = {}, preferredTemplateId, preferredPlatformCode) => { |
| | | const res = await list(params); |
| | | if (!isSuccess(res)) { |
| | | message.error(res?.msg || '加载模板失败'); |
| | | setPlatformTabs([]); |
| | | setActiveTemplateId(undefined); |
| | | setActivePlatform(''); |
| | | setTableData([]); |
| | | setTotal(0); |
| | | return undefined; |
| | | } |
| | | |
| | | const tabs = (res?.data || []).map((item) => ({ |
| | | label: getTemplateTabLabel(item), |
| | | value: item.id, |
| | | platformCode: item.platformCode, |
| | | platformName: item.platformName, |
| | | template: item, |
| | | closable: true, |
| | | })); |
| | | setPlatformTabs(tabs); |
| | | setTemplateOptions( |
| | | tabs.map((item) => ({ |
| | | label: getTemplateOptionLabel(item.template), |
| | | value: item.value, |
| | | template: item.template, |
| | | })), |
| | | ); |
| | | |
| | | const nextActive = |
| | | tabs.find((item) => item.value === preferredTemplateId) || |
| | | tabs.find((item) => item.platformCode === preferredPlatformCode) || |
| | | tabs[0]; |
| | | setActiveTemplateId(nextActive?.value); |
| | | setActivePlatform(nextActive?.platformCode || ''); |
| | | return nextActive; |
| | | }; |
| | | |
| | | useEffect(() => { |
| | | fetchTemplateTabs().then((nextActive) => { |
| | | if (nextActive?.value) { |
| | | fetchTable(pageState, { templateId: nextActive.value }); |
| | | } |
| | | classifySelect().then((res) => { |
| | | const data = Array.isArray(res?.data) ? res.data : []; |
| | | setClassifyOptions( |
| | | data |
| | | .map((item) => ({ |
| | | classifyName: item.subClassify || item.classifyName || item.name, |
| | | id: item.id, |
| | | })) |
| | | .filter((item) => item.classifyName && item.id !== undefined && item.id !== null) |
| | | .map((item) => ({ |
| | | label: item.classifyName, |
| | | value: item.id, |
| | | classifyName: item.classifyName, |
| | | })), |
| | | ); |
| | | }); |
| | | siteList().then((res) => { |
| | | const data = Array.isArray(res?.data) ? res.data : []; |
| | | setSiteOptions( |
| | | data |
| | | .map((item) => item.siteName) |
| | | .filter(Boolean) |
| | | .map((siteName) => ({ label: siteName, value: siteName })), |
| | | ); |
| | | }); |
| | | fetchTable({ current: 1, pageSize: 10 }, {}); |
| | | // eslint-disable-next-line react-hooks/exhaustive-deps |
| | | }, []); |
| | | |
| | | useEffect(() => { |
| | | fetchTable(pageState, queryForm.getFieldsValue()); |
| | | // eslint-disable-next-line react-hooks/exhaustive-deps |
| | | }, [activeTemplateId, pageState.current, pageState.pageSize]); |
| | | |
| | | const syncClassifyMeta = (classifyId, targetForm = form) => { |
| | | const meta = classifyMap[classifyId] || {}; |
| | | targetForm.setFieldsValue({ |
| | | classifyName: meta.classifyName || '', |
| | | classifyIcon: meta.classifyIcon || '', |
| | | }); |
| | | }; |
| | | |
| | | const buildTemplatePayload = (values) => ({ |
| | | ...values, |
| | | platformName: values.platformName || values.platformCode, |
| | | cityList: (values.cityList || []) |
| | | .filter((item) => item && item.cityCode) |
| | | .map((item) => ({ |
| | | cityCode: getCityCodeValue(item.cityCode), |
| | | cityName: item.cityName || cityOptions.find((city) => city.value === getCityCodeValue(item.cityCode))?.label || '', |
| | | costPrice: item.costPrice, |
| | | infoFee: item.infoFee, |
| | | })), |
| | | const buildPayload = (values) => ({ |
| | | ...(values.id ? { id: values.id } : {}), |
| | | cityCode: values.cityCode, |
| | | cityName: values.cityName?.trim(), |
| | | siteName: values.siteName?.trim(), |
| | | classifyId: values.classifyId, |
| | | classifyName: values.classifyName?.trim(), |
| | | costPrice: values.costPrice, |
| | | infoFee: values.infoFee, |
| | | }); |
| | | |
| | | const hasDuplicateCity = (cityList = []) => { |
| | | const cityCodes = cityList |
| | | .filter((item) => item?.cityCode) |
| | | .map((item) => getCityCodeValue(item.cityCode)); |
| | | return new Set(cityCodes).size !== cityCodes.length; |
| | | }; |
| | | |
| | | const buildCityPayload = (values) => { |
| | | const cityCode = getCityCodeValue(values.cityCode); |
| | | const restValues = { ...values }; |
| | | delete restValues.defaultCostPrice; |
| | | delete restValues.defaultInfoFee; |
| | | return { |
| | | ...restValues, |
| | | platformCode: activePlatform, |
| | | cityCode, |
| | | cityName: values.cityName || cityOptions.find((city) => city.value === cityCode)?.label || '', |
| | | }; |
| | | }; |
| | | |
| | | const openCityModal = (record = {}) => { |
| | | if (!activePlatform) { |
| | | message.info('请先选择模板'); |
| | | return; |
| | | } |
| | | cityForm.resetFields(); |
| | | if (record.templateId && !templateOptions.some((item) => item.value === record.templateId)) { |
| | | setTemplateOptions((prev) => [ |
| | | ...prev, |
| | | { |
| | | label: getTemplateOptionLabel(record), |
| | | value: record.templateId, |
| | | template: record, |
| | | }, |
| | | ]); |
| | | } |
| | | cityForm.setFieldsValue({ |
| | | id: record.id, |
| | | templateId: record.templateId, |
| | | cityCode: cityPathMap[record.cityCode] || record.cityCode || undefined, |
| | | cityName: record.cityName || '', |
| | | costPrice: record.costPrice, |
| | | infoFee: record.infoFee, |
| | | }); |
| | | |
| | | setCityModalVisible(true); |
| | | }; |
| | | |
| | | const openModal = async (mode, record = {}) => { |
| | | if (mode === 'add' && !activePlatform) { |
| | | message.info('请先添加平台模板'); |
| | | return; |
| | | } |
| | | setModalMode(mode); |
| | | setModalVisible(true); |
| | | form.resetFields(); |
| | | |
| | | if (mode === 'add') { |
| | | const values = { |
| | | platformCode: activePlatform, |
| | | platformName: resolvePlatformLabel(activePlatform), |
| | | cityList: [emptyCityRow()], |
| | | }; |
| | | setDetailLoading(true); |
| | | try { |
| | | if (activeTemplateId) { |
| | | const res = await detail({ id: activeTemplateId }); |
| | | const template = res?.data?.template || {}; |
| | | values.defaultCostPrice = template.defaultCostPrice; |
| | | values.defaultInfoFee = template.defaultInfoFee; |
| | | } |
| | | } finally { |
| | | setDetailLoading(false); |
| | | } |
| | | form.setFieldsValue({ |
| | | ...values, |
| | | }); |
| | | setModalVisible(true); |
| | | if (mode !== 'edit') { |
| | | return; |
| | | } |
| | | |
| | | setDetailLoading(true); |
| | | try { |
| | | const res = await detail({ id: getTemplateId(record) }); |
| | | const tpl = res?.data?.template || res?.data || {}; |
| | | const cityList = normalizeCityList(res?.data?.cityList || []); |
| | | const res = await detail({ id: record.id }); |
| | | if (!isSuccess(res)) { |
| | | message.error(res?.msg || '加载详情失败'); |
| | | return; |
| | | } |
| | | const data = res?.data?.template || res?.data || record; |
| | | form.setFieldsValue({ |
| | | ...tpl, |
| | | platformCode: tpl.platformCode || activePlatform, |
| | | platformName: tpl.platformName || resolvePlatformLabel(tpl.platformCode) || '', |
| | | cityList: cityList.length > 0 ? cityList : [emptyCityRow()], |
| | | id: data.id, |
| | | cityCode: data.cityCode, |
| | | cityName: data.cityName, |
| | | cityPath: cityPathMap[data.cityCode] || cityPathMap[data.cityName], |
| | | siteName: data.siteName, |
| | | classifyId: data.classifyId, |
| | | classifyName: data.classifyName, |
| | | costPrice: data.costPrice, |
| | | infoFee: data.infoFee, |
| | | }); |
| | | syncClassifyMeta(tpl.classifyId); |
| | | } finally { |
| | | setDetailLoading(false); |
| | | } |
| | | }; |
| | | |
| | | const handleAddPlatform = async () => { |
| | | const values = await platformForm.validateFields(); |
| | | if (hasDuplicateCity(values.cityList)) { |
| | | message.error('同一城市不能配置多个价格'); |
| | | return; |
| | | } |
| | | setPlatformSaving(true); |
| | | const handleSave = async () => { |
| | | const values = await form.validateFields(); |
| | | const payload = buildPayload(values); |
| | | setSaving(true); |
| | | try { |
| | | const payload = buildTemplatePayload(values); |
| | | const res = await save(payload); |
| | | const res = modalMode === 'edit' ? await update(payload) : await save(payload); |
| | | if (!isSuccess(res)) { |
| | | message.error(res?.msg || '保存失败'); |
| | | return; |
| | | } |
| | | setPlatformModalVisible(false); |
| | | platformForm.resetFields(); |
| | | setPageState({ current: 1, pageSize: 10 }); |
| | | const nextActive = await fetchTemplateTabs({}, undefined, values.platformCode); |
| | | if (nextActive?.value) { |
| | | fetchTable({ current: 1, pageSize: pageState.pageSize }, { templateId: nextActive.value }); |
| | | } |
| | | message.success('添加成功'); |
| | | } finally { |
| | | setPlatformSaving(false); |
| | | } |
| | | }; |
| | | |
| | | const clearSelection = () => { |
| | | setSelectedRowKeys([]); |
| | | }; |
| | | |
| | | const handlePlatformChange = (value) => { |
| | | const tab = platformTabs.find((item) => item.value === value); |
| | | setActiveTemplateId(value); |
| | | setActivePlatform(tab?.platformCode || ''); |
| | | clearSelection(); |
| | | setPageState((prev) => ({ ...prev, current: 1 })); |
| | | }; |
| | | |
| | | const removePlatformTab = (value) => { |
| | | const nextTabs = platformTabs.filter((tab) => tab.value !== value); |
| | | setPlatformTabs(nextTabs); |
| | | clearSelection(); |
| | | if (activeTemplateId === value) { |
| | | setTableData([]); |
| | | setTotal(0); |
| | | handlePlatformChange(nextTabs[0]?.value || ''); |
| | | } |
| | | }; |
| | | |
| | | const handleRemovePlatform = (value) => { |
| | | showDelConfirm(async () => { |
| | | const tab = platformTabs.find((item) => item.value === value); |
| | | if (!tab?.value || !tab?.platformCode) { |
| | | message.error('模板信息缺失,无法删除'); |
| | | return; |
| | | } |
| | | const deleteRes = await batchDelete({ ids: String(tab.value), platformCode: tab.platformCode }); |
| | | if (isSuccess(deleteRes)) { |
| | | removePlatformTab(value); |
| | | message.success('删除成功'); |
| | | } else { |
| | | message.error(deleteRes?.msg || '删除失败'); |
| | | } |
| | | }, '确认删除该平台信息吗?'); |
| | | }; |
| | | |
| | | const handleSave = async () => { |
| | | const values = await form.validateFields(); |
| | | if (hasDuplicateCity(values.cityList)) { |
| | | message.error('同一城市不能配置多个价格'); |
| | | return; |
| | | } |
| | | const payload = buildTemplatePayload(values); |
| | | setSaving(true); |
| | | try { |
| | | const res = modalMode === 'edit' ? await update(payload) : await save(payload); |
| | | if (isSuccess(res)) { |
| | | message.success('保存成功'); |
| | | setModalVisible(false); |
| | | fetchTemplateTabs({}, payload.id || activeTemplateId, payload.platformCode).then((nextActive) => { |
| | | fetchTable(pageState, { ...queryForm.getFieldsValue(), templateId: nextActive?.value || activeTemplateId }); |
| | | }); |
| | | } else { |
| | | message.error(res?.msg || '保存失败'); |
| | | } |
| | | message.success('保存成功'); |
| | | setModalVisible(false); |
| | | setSelectedRowKeys([]); |
| | | fetchTable(pageState); |
| | | } finally { |
| | | setSaving(false); |
| | | } |
| | | }; |
| | | |
| | | const handleCitySave = async () => { |
| | | const values = await cityForm.validateFields(); |
| | | const payload = buildCityPayload(values); |
| | | setCitySaving(true); |
| | | try { |
| | | const res = await cityUpdate(payload); |
| | | if (isSuccess(res)) { |
| | | message.success('保存成功'); |
| | | setCityModalVisible(false); |
| | | clearSelection(); |
| | | fetchTable(pageState, queryForm.getFieldsValue()); |
| | | } else { |
| | | message.error(res?.msg || '保存失败'); |
| | | const handleDelete = (record) => { |
| | | showDelConfirm(async () => { |
| | | const res = await remove({ id: record.id }); |
| | | if (!isSuccess(res)) { |
| | | message.error(res?.msg || '删除失败'); |
| | | return; |
| | | } |
| | | } finally { |
| | | setCitySaving(false); |
| | | } |
| | | message.success('删除成功'); |
| | | setSelectedRowKeys([]); |
| | | fetchTable(pageState); |
| | | }, '确认删除该扣费模板吗?'); |
| | | }; |
| | | |
| | | const columns = [ |
| | | { |
| | | title: '城市', |
| | | dataIndex: 'cityName', |
| | | render: (_, record) => cityDisplayMap[record.cityCode] || record.cityName || '--', |
| | | }, |
| | | { |
| | | title: '服务封面图', |
| | | dataIndex: 'classifyIcon', |
| | | render: (classifyIcon) => |
| | | classifyIcon ? ( |
| | | <img className={styles.serviceCover} src={classifyIcon} alt="服务封面图" /> |
| | | ) : ( |
| | | <div className={styles.serviceCoverEmpty}>暂无图片</div> |
| | | ), |
| | | }, |
| | | { |
| | | title: '服务名称', |
| | | dataIndex: 'classifyName', |
| | | render: (classifyName) => classifyName || '--', |
| | | }, |
| | | { |
| | | title: '旧机成本(回收价)', |
| | | dataIndex: 'costPrice', |
| | | render: (costPrice) => formatMoney(costPrice), |
| | | }, |
| | | { |
| | | title: '信息费', |
| | | dataIndex: 'infoFee', |
| | | render: (infoFee) => formatMoney(infoFee), |
| | | }, |
| | | { title: '城市', dataIndex: 'cityName', render: (value) => value || '--' }, |
| | | { title: '站点', dataIndex: 'siteName', render: (value) => value || '--' }, |
| | | { title: '品类', dataIndex: 'classifyName', render: (value) => value || '--' }, |
| | | { title: '旧机成本(回收价)', dataIndex: 'costPrice', render: formatMoney }, |
| | | { title: '信息费', dataIndex: 'infoFee', render: formatMoney }, |
| | | { |
| | | title: '操作', |
| | | hideInSearch: true, |
| | | render: (_, record) => ( |
| | | <Space> |
| | | <Button type="link" onClick={() => openCityModal(record)}> |
| | | <Button type="link" onClick={() => openModal('edit', record)}> |
| | | 编辑 |
| | | </Button> |
| | | <Button |
| | | type="link" |
| | | danger |
| | | onClick={() => { |
| | | showDelConfirm(async () => { |
| | | const res = await cityDelete({ id: record.id, platformCode: activePlatform }); |
| | | if (isSuccess(res)) { |
| | | message.success('删除成功'); |
| | | clearSelection(); |
| | | fetchTable(pageState, queryForm.getFieldsValue()); |
| | | } else { |
| | | message.error(res?.msg || '删除失败'); |
| | | } |
| | | }, '确认删除该城市配置吗?'); |
| | | }} |
| | | > |
| | | <Button type="link" danger onClick={() => handleDelete(record)}> |
| | | 删除 |
| | | </Button> |
| | | </Space> |
| | |
| | | message.error('请上传Excel文件'); |
| | | return Upload.LIST_IGNORE; |
| | | } |
| | | return isExcel; |
| | | return true; |
| | | }, |
| | | onChange: (info) => { |
| | | if (info.file.status === 'uploading') { |
| | | setImportLoading(true); |
| | | } |
| | | if (info.file.status === 'done') { |
| | | } else if (info.file.status === 'done') { |
| | | setImportLoading(false); |
| | | if (isSuccess(info.file.response)) { |
| | | message.success('导入成功'); |
| | | clearSelection(); |
| | | fetchTemplateTabs().then((nextActive) => { |
| | | fetchTable({ current: 1, pageSize: pageState.pageSize }, { ...queryForm.getFieldsValue(), templateId: nextActive?.value }); |
| | | }); |
| | | setSelectedRowKeys([]); |
| | | setPageState((current) => ({ ...current, current: 1 })); |
| | | fetchTable({ ...pageState, current: 1 }); |
| | | } else { |
| | | message.error(info.file.response?.msg || '导入失败'); |
| | | } |
| | |
| | | }; |
| | | |
| | | return ( |
| | | <PageContainer title="扣费模版设置"> |
| | | <PageContainer title="扣费模板"> |
| | | <div className={styles.page}> |
| | | <div className={styles.platformBar}> |
| | | <div className={styles.platformTabs}> |
| | | {platformTabs.length === 0 ? ( |
| | | <div className={styles.platformEmpty}> |
| | | <Empty description="暂无平台,请先添加" image={Empty.PRESENTED_IMAGE_SIMPLE} /> |
| | | </div> |
| | | ) : ( |
| | | platformTabs.map((item) => ( |
| | | <button |
| | | key={item.value} |
| | | type="button" |
| | | className={`${styles.platformTab} ${activeTemplateId === item.value ? styles.platformTabActive : ''}`} |
| | | onClick={() => handlePlatformChange(item.value)} |
| | | > |
| | | <span>{item.label}</span> |
| | | {item.closable !== false ? ( |
| | | <span |
| | | className={styles.platformClose} |
| | | onClick={(e) => { |
| | | e.stopPropagation(); |
| | | handleRemovePlatform(item.value); |
| | | }} |
| | | > |
| | | <CloseOutlined /> |
| | | </span> |
| | | ) : null} |
| | | </button> |
| | | )) |
| | | )} |
| | | </div> |
| | | <Button |
| | | type="primary" |
| | | ghost |
| | | icon={<PlusOutlined />} |
| | | onClick={() => { |
| | | platformForm.resetFields(); |
| | | platformForm.setFieldsValue({ |
| | | cityList: [emptyCityRow()], |
| | | }); |
| | | setPlatformModalVisible(true); |
| | | }} |
| | | > |
| | | 添加平台 |
| | | </Button> |
| | | </div> |
| | | |
| | | <Card className={styles.searchCard} bordered> |
| | | <Form form={queryForm} layout="inline" className={styles.searchForm}> |
| | | <Form.Item name="serviceName" label="服务名称"> |
| | | <Input placeholder="请输入" className={styles.searchInput} /> |
| | | <Form.Item name="cityName" label="城市"> |
| | | <Input className={styles.searchInput} placeholder="请输入城市" allowClear /> |
| | | </Form.Item> |
| | | <Form.Item name="cityCode" label="城市"> |
| | | <Cascader |
| | | placeholder="请选择省市" |
| | | <Form.Item name="siteName" label="站点"> |
| | | <Select |
| | | className={styles.searchSelect} |
| | | options={cityCascaderOptions} |
| | | options={siteOptions} |
| | | showSearch |
| | | optionFilterProp="label" |
| | | placeholder="请选择站点" |
| | | allowClear |
| | | /> |
| | | </Form.Item> |
| | | <Form.Item name="classifyName" label="品类"> |
| | | <Select |
| | | className={styles.searchSelect} |
| | | options={classifyOptions.map((item) => ({ |
| | | label: item.label, |
| | | value: item.classifyName, |
| | | }))} |
| | | showSearch |
| | | optionFilterProp="label" |
| | | placeholder="请选择品类" |
| | | allowClear |
| | | /> |
| | | </Form.Item> |
| | |
| | | <Button |
| | | type="primary" |
| | | onClick={() => { |
| | | clearSelection(); |
| | | setPageState((prev) => ({ ...prev, current: 1 })); |
| | | const values = queryForm.getFieldsValue(); |
| | | fetchTemplateTabs({ serviceName: values.serviceName }).then((nextActive) => { |
| | | fetchTable({ ...pageState, current: 1 }, { ...values, templateId: nextActive?.value }); |
| | | }); |
| | | const nextPageState = { ...pageState, current: 1 }; |
| | | setPageState(nextPageState); |
| | | setSelectedRowKeys([]); |
| | | fetchTable(nextPageState); |
| | | }} |
| | | > |
| | | 查询 |
| | |
| | | <Button |
| | | onClick={() => { |
| | | queryForm.resetFields(); |
| | | clearSelection(); |
| | | setPageState((prev) => ({ ...prev, current: 1 })); |
| | | fetchTemplateTabs().then((nextActive) => { |
| | | fetchTable({ ...pageState, current: 1 }, { templateId: nextActive?.value }); |
| | | }); |
| | | const nextPageState = { ...pageState, current: 1 }; |
| | | setPageState(nextPageState); |
| | | setSelectedRowKeys([]); |
| | | fetchTable(nextPageState, {}); |
| | | }} |
| | | > |
| | | 重置 |
| | |
| | | </Card> |
| | | |
| | | <div className={styles.toolbar}> |
| | | <Button type="primary" icon={<PlusOutlined />} disabled={!activePlatform} onClick={() => openModal('add')}> |
| | | 新增模板 |
| | | <Button type="primary" icon={<PlusOutlined />} onClick={() => openModal('add')}> |
| | | 新增扣费模板 |
| | | </Button> |
| | | <Upload {...uploadProps}> |
| | | <Button loading={importLoading}>Excel导入</Button> |
| | | </Upload> |
| | | <Button icon={<DownloadOutlined />} onClick={() => downLoad('/deductionTemplate/importTemplate', '扣费模板导入模板.xlsx')}> |
| | | <Button |
| | | icon={<DownloadOutlined />} |
| | | onClick={() => downLoad('/deductionTemplate/importTemplate', '扣费模板导入模板.xlsx')} |
| | | > |
| | | 下载导入模板 |
| | | </Button> |
| | | <Button |
| | | danger |
| | | icon={<DeleteOutlined />} |
| | | disabled={selectedRowKeys.length === 0} |
| | | onClick={() => { |
| | | showDelConfirm(async () => { |
| | | const ids = selectedRowKeys.filter(Boolean); |
| | | if (ids.length === 0) { |
| | | message.error('请选择要删除的城市配置'); |
| | | const res = await batchDelete({ ids: selectedRowKeys.join(',') }); |
| | | if (!isSuccess(res)) { |
| | | message.error(res?.msg || '删除失败'); |
| | | return; |
| | | } |
| | | const res = await cityBatchDelete({ ids: ids.join(','), platformCode: activePlatform }); |
| | | if (isSuccess(res)) { |
| | | message.success('删除成功'); |
| | | clearSelection(); |
| | | fetchTable(pageState, queryForm.getFieldsValue()); |
| | | } else { |
| | | message.error(res?.msg || '删除失败'); |
| | | } |
| | | }, '确认删除所选城市配置吗?'); |
| | | message.success('删除成功'); |
| | | setSelectedRowKeys([]); |
| | | fetchTable(pageState); |
| | | }, '确认删除所选扣费模板吗?'); |
| | | }} |
| | | > |
| | | 批量删除 |
| | |
| | | showQuickJumper: true, |
| | | }} |
| | | onChange={(pagination) => { |
| | | clearSelection(); |
| | | const next = { |
| | | const nextPageState = { |
| | | current: pagination.current || 1, |
| | | pageSize: pagination.pageSize || 10, |
| | | }; |
| | | setPageState(next); |
| | | setPageState(nextPageState); |
| | | setSelectedRowKeys([]); |
| | | fetchTable(nextPageState); |
| | | }} |
| | | rowSelection={{ |
| | | selectedRowKeys, |
| | | onChange: setSelectedRowKeys, |
| | | }} |
| | | rowSelection={{ selectedRowKeys, onChange: setSelectedRowKeys }} |
| | | /> |
| | | |
| | | <Modal |
| | | getContainer={false} |
| | | width={980} |
| | | width={560} |
| | | destroyOnClose |
| | | title={currentTitle} |
| | | title={modalMode === 'edit' ? '编辑扣费模板' : '新增扣费模板'} |
| | | open={modalVisible} |
| | | onCancel={() => setModalVisible(false)} |
| | | onOk={handleSave} |
| | | confirmLoading={saving || detailLoading} |
| | | footer={ |
| | | modalMode === 'detail' |
| | | ? [ |
| | | <Button key="close" onClick={() => setModalVisible(false)}> |
| | | 关闭 |
| | | </Button>, |
| | | ] |
| | | : [ |
| | | <Button key="cancel" onClick={() => setModalVisible(false)}> |
| | | 取消 |
| | | </Button>, |
| | | <Button key="save" type="primary" onClick={handleSave} loading={saving || detailLoading}> |
| | | 保存 |
| | | </Button>, |
| | | ] |
| | | } |
| | | > |
| | | <div className={styles.modalBody}> |
| | | <Form form={form} layout="vertical" disabled={modalMode === 'detail'}> |
| | | <div className={styles.section}> |
| | | <Form.Item |
| | | label="所属平台" |
| | | name="platformCode" |
| | | rules={[{ required: true, message: '请选择所属平台' }]} |
| | | > |
| | | <Radio.Group |
| | | options={platformOptionList} |
| | | onChange={(e) => { |
| | | const value = e.target.value; |
| | | form.setFieldsValue({ |
| | | platformName: resolvePlatformLabel(value), |
| | | }); |
| | | }} |
| | | disabled={platformOptionList.length === 0} |
| | | /> |
| | | </Form.Item> |
| | | |
| | | <Form.Item name="platformName" hidden> |
| | | <Input /> |
| | | </Form.Item> |
| | | |
| | | <Form.Item |
| | | label="所属分类" |
| | | name="classifyId" |
| | | rules={[{ required: true, message: '请选择所属分类' }]} |
| | | > |
| | | <Select |
| | | options={classifyOptions} |
| | | showSearch |
| | | optionFilterProp="label" |
| | | placeholder="请选择" |
| | | onChange={(value) => syncClassifyMeta(value)} |
| | | /> |
| | | </Form.Item> |
| | | |
| | | <Form.Item name="classifyName" hidden> |
| | | <Input /> |
| | | </Form.Item> |
| | | |
| | | <Form.Item name="classifyIcon" hidden> |
| | | <Input /> |
| | | </Form.Item> |
| | | |
| | | <div className={styles.sectionTitle}>回收价格</div> |
| | | <div className={styles.priceRow}> |
| | | <Form.Item |
| | | label="默认成本价" |
| | | name="defaultCostPrice" |
| | | rules={[{ required: true, message: '请输入默认成本价' }]} |
| | | > |
| | | <InputNumber min={0} precision={2} placeholder="请输入" /> |
| | | </Form.Item> |
| | | <Form.Item |
| | | label="默认信息费" |
| | | name="defaultInfoFee" |
| | | rules={[{ required: true, message: '请输入默认信息费' }]} |
| | | > |
| | | <InputNumber min={0} precision={2} placeholder="请输入" /> |
| | | </Form.Item> |
| | | </div> |
| | | </div> |
| | | |
| | | <div className={styles.section}> |
| | | <div className={styles.sectionTitle}>城市明细</div> |
| | | <Form.List name="cityList"> |
| | | {(fields, { add, remove }) => ( |
| | | <> |
| | | <div className={styles.cityList}> |
| | | {fields.map((field) => ( |
| | | <div className={styles.cityRow} key={field.key}> |
| | | <Form.Item |
| | | {...field} |
| | | name={[field.name, 'cityCode']} |
| | | rules={[{ required: true, message: '请选择城市' }]} |
| | | > |
| | | <Cascader |
| | | options={cityCascaderOptions} |
| | | placeholder="请选择省市" |
| | | showSearch |
| | | onChange={(value, selectedOptions = []) => { |
| | | const nextRows = form.getFieldValue('cityList') || []; |
| | | nextRows[field.name] = { |
| | | ...(nextRows[field.name] || {}), |
| | | cityCode: value, |
| | | cityName: selectedOptions[1]?.label || '', |
| | | }; |
| | | form.setFieldsValue({ cityList: nextRows }); |
| | | }} |
| | | /> |
| | | </Form.Item> |
| | | <Form.Item {...field} name={[field.name, 'cityName']} hidden> |
| | | <Input /> |
| | | </Form.Item> |
| | | <Form.Item |
| | | {...field} |
| | | name={[field.name, 'costPrice']} |
| | | rules={[{ required: true, message: '请输入旧机成本价' }]} |
| | | > |
| | | <InputNumber min={0} precision={2} placeholder="旧机成本价" /> |
| | | </Form.Item> |
| | | <Form.Item |
| | | {...field} |
| | | name={[field.name, 'infoFee']} |
| | | rules={[{ required: true, message: '请输入信息费' }]} |
| | | > |
| | | <InputNumber min={0} precision={2} placeholder="信息费" /> |
| | | </Form.Item> |
| | | <Button |
| | | danger |
| | | icon={<DeleteOutlined />} |
| | | disabled={modalMode === 'detail'} |
| | | onClick={() => remove(field.name)} |
| | | /> |
| | | </div> |
| | | ))} |
| | | </div> |
| | | {modalMode !== 'detail' ? ( |
| | | <div className={styles.cityActions}> |
| | | <Button icon={<PlusOutlined />} onClick={() => add(emptyCityRow())}> |
| | | 添加 |
| | | </Button> |
| | | </div> |
| | | ) : null} |
| | | </> |
| | | )} |
| | | </Form.List> |
| | | </div> |
| | | </Form> |
| | | </div> |
| | | </Modal> |
| | | |
| | | <Modal |
| | | getContainer={false} |
| | | width={560} |
| | | title="编辑城市配置" |
| | | open={cityModalVisible} |
| | | onCancel={() => setCityModalVisible(false)} |
| | | onOk={handleCitySave} |
| | | confirmLoading={citySaving} |
| | | destroyOnClose |
| | | > |
| | | <Form form={cityForm} layout="vertical" className={styles.cityConfigForm}> |
| | | <Form form={form} layout="vertical" className={styles.templateForm}> |
| | | <Form.Item name="id" hidden> |
| | | <Input /> |
| | | </Form.Item> |
| | | <Form.Item |
| | | name="templateId" |
| | | label="所属模板" |
| | | rules={[{ required: true, message: '请选择所属模板' }]} |
| | | > |
| | | <Select |
| | | options={templateOptions} |
| | | placeholder="请选择" |
| | | showSearch |
| | | optionFilterProp="label" |
| | | disabled |
| | | /> |
| | | </Form.Item> |
| | | <Form.Item name="cityName" hidden> |
| | | <Form.Item name="cityName" hidden rules={[{ required: true, message: '请选择城市' }]}> |
| | | <Input /> |
| | | </Form.Item> |
| | | <Form.Item name="cityCode" label="城市" rules={[{ required: true, message: '请选择城市' }]}> |
| | | <Form.Item name="cityCode" hidden rules={[{ required: true, message: '请选择城市' }]}> |
| | | <Input /> |
| | | </Form.Item> |
| | | <Form.Item |
| | | name="cityPath" |
| | | label="城市" |
| | | rules={[{ required: true, message: '请选择城市' }]} |
| | | > |
| | | <Cascader |
| | | options={cityCascaderOptions} |
| | | placeholder="请选择省市" |
| | | showSearch |
| | | onChange={(value, selectedOptions = []) => { |
| | | cityForm.setFieldsValue({ |
| | | cityName: selectedOptions[1]?.label || '', |
| | | form.setFieldsValue({ |
| | | cityName: selectedOptions[selectedOptions.length - 1]?.label || '', |
| | | cityCode: value?.[value.length - 1], |
| | | }); |
| | | }} |
| | | /> |
| | | </Form.Item> |
| | | <Form.Item |
| | | label="旧机成本价" |
| | | name="siteName" |
| | | label="站点" |
| | | rules={[{ required: true, message: '请选择站点' }]} |
| | | > |
| | | <Select |
| | | options={siteOptions} |
| | | showSearch |
| | | optionFilterProp="label" |
| | | placeholder="请选择站点" |
| | | /> |
| | | </Form.Item> |
| | | <Form.Item |
| | | name="classifyName" |
| | | hidden |
| | | rules={[{ required: true, message: '请选择品类' }]} |
| | | > |
| | | <Input /> |
| | | </Form.Item> |
| | | <Form.Item |
| | | name="classifyId" |
| | | label="品类" |
| | | rules={[{ required: true, message: '请选择品类' }]} |
| | | > |
| | | <Select |
| | | options={classifyOptions} |
| | | showSearch |
| | | optionFilterProp="label" |
| | | placeholder="请选择品类" |
| | | onChange={(_, option) => |
| | | form.setFieldsValue({ classifyName: option?.classifyName || '' }) |
| | | } |
| | | /> |
| | | </Form.Item> |
| | | <Form.Item |
| | | label="旧机成本(回收价)" |
| | | name="costPrice" |
| | | rules={[{ required: true, message: '请输入旧机成本价' }]} |
| | | rules={[{ required: true, message: '请输入旧机成本(回收价)' }]} |
| | | > |
| | | <InputNumber min={0} precision={2} placeholder="请输入" /> |
| | | </Form.Item> |
| | |
| | | > |
| | | <InputNumber min={0} precision={2} placeholder="请输入" /> |
| | | </Form.Item> |
| | | </Form> |
| | | </Modal> |
| | | |
| | | <Modal |
| | | getContainer={false} |
| | | width={780} |
| | | title="添加平台" |
| | | open={platformModalVisible} |
| | | onCancel={() => setPlatformModalVisible(false)} |
| | | onOk={handleAddPlatform} |
| | | confirmLoading={platformSaving} |
| | | destroyOnClose |
| | | > |
| | | <Form form={platformForm} layout="vertical" className={styles.platformForm}> |
| | | <Form.Item |
| | | name="platformCode" |
| | | label="所属平台编码" |
| | | rules={[{ required: true, message: '请输入所属平台编码' }]} |
| | | > |
| | | <Input placeholder="请输入平台编码" /> |
| | | </Form.Item> |
| | | <Form.Item |
| | | name="platformName" |
| | | label="所属平台名称" |
| | | rules={[{ required: true, message: '请输入所属平台名称' }]} |
| | | > |
| | | <Input placeholder="请输入平台名称" /> |
| | | </Form.Item> |
| | | <div className={styles.platformHint}>示例:京东/jd、淘宝天猫/tm、抖音/dy、拼多多/pdd</div> |
| | | <Form.Item name="classifyId" label="所属分类" rules={[{ required: true, message: '请选择所属分类' }]}> |
| | | <Select |
| | | options={classifyOptions} |
| | | showSearch |
| | | optionFilterProp="label" |
| | | placeholder="请选择" |
| | | onChange={(value) => syncClassifyMeta(value, platformForm)} |
| | | /> |
| | | </Form.Item> |
| | | <Form.Item name="classifyName" hidden> |
| | | <Input /> |
| | | </Form.Item> |
| | | <Form.Item name="classifyIcon" hidden> |
| | | <Input /> |
| | | </Form.Item> |
| | | |
| | | <div className={styles.sectionTitle}>回收价格</div> |
| | | <div className={styles.platformPriceRow}> |
| | | <Form.Item |
| | | label="默认价" |
| | | name="defaultCostPrice" |
| | | rules={[{ required: true, message: '请输入默认价' }]} |
| | | > |
| | | <InputNumber min={0} precision={2} placeholder="请输入" /> |
| | | </Form.Item> |
| | | <Form.Item |
| | | label="默认信息费" |
| | | name="defaultInfoFee" |
| | | rules={[{ required: true, message: '请输入默认信息费' }]} |
| | | > |
| | | <InputNumber min={0} precision={2} placeholder="请输入" /> |
| | | </Form.Item> |
| | | </div> |
| | | <Form.List name="cityList"> |
| | | {(fields, { add, remove }) => ( |
| | | <> |
| | | <Button type="primary" className={styles.platformAddBtn} onClick={() => add(emptyCityRow())}> |
| | | 添加 |
| | | </Button> |
| | | <div className={styles.platformCityList}> |
| | | {fields.map((field) => ( |
| | | <div className={styles.platformCityRow} key={field.key}> |
| | | <Form.Item |
| | | {...field} |
| | | name={[field.name, 'cityCode']} |
| | | rules={[{ required: true, message: '请选择城市' }]} |
| | | > |
| | | <Cascader |
| | | options={cityCascaderOptions} |
| | | placeholder="请选择省市" |
| | | showSearch |
| | | onChange={(value, selectedOptions = []) => { |
| | | const nextRows = platformForm.getFieldValue('cityList') || []; |
| | | nextRows[field.name] = { |
| | | ...(nextRows[field.name] || {}), |
| | | cityCode: value, |
| | | cityName: selectedOptions[1]?.label || '', |
| | | }; |
| | | platformForm.setFieldsValue({ cityList: nextRows }); |
| | | }} |
| | | /> |
| | | </Form.Item> |
| | | <Form.Item {...field} name={[field.name, 'cityName']} hidden> |
| | | <Input /> |
| | | </Form.Item> |
| | | <Form.Item |
| | | {...field} |
| | | name={[field.name, 'costPrice']} |
| | | rules={[{ required: true, message: '请输入该城市的回收价格' }]} |
| | | > |
| | | <InputNumber min={0} precision={2} placeholder="请输入在该城市 的回收价格" /> |
| | | </Form.Item> |
| | | <Form.Item |
| | | {...field} |
| | | name={[field.name, 'infoFee']} |
| | | rules={[{ required: true, message: '请输入该城市的信息费' }]} |
| | | > |
| | | <InputNumber min={0} precision={2} placeholder="请输入在该城市 的信息费" /> |
| | | </Form.Item> |
| | | <Button |
| | | type="text" |
| | | danger |
| | | icon={<CloseOutlined />} |
| | | disabled={fields.length === 1} |
| | | onClick={() => remove(field.name)} |
| | | /> |
| | | </div> |
| | | ))} |
| | | </div> |
| | | </> |
| | | )} |
| | | </Form.List> |
| | | </Form> |
| | | </Modal> |
| | | </div> |