二手回收后台,师傅端,用户端
需求:
1、城市+站点+品类 旧机费只能有一个
2、扣费模版 去掉代收代费和信息费汇总
3个文件已修改
1260 ■■■■■ 已修改文件
management/src/pages/serviceManage/deductionTemplate/index.jsx 1004 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
management/src/pages/serviceManage/deductionTemplate/service.js 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
management/src/pages/serviceManage/deductionTemplate/style.less 231 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
management/src/pages/serviceManage/deductionTemplate/index.jsx
@@ -1,44 +1,33 @@
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,
@@ -49,42 +38,15 @@
  })),
}));
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 === '') {
@@ -94,100 +56,33 @@
  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);
@@ -196,341 +91,125 @@
    }
  };
  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>
@@ -552,20 +231,18 @@
        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 || '导入失败');
        }
@@ -577,65 +254,33 @@
  };
  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>
@@ -644,12 +289,10 @@
                <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);
                  }}
                >
                  查询
@@ -657,11 +300,10 @@
                <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, {});
                  }}
                >
                  重置
@@ -672,34 +314,33 @@
        </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);
              }, '确认删除所选扣费模板吗?');
            }}
          >
            批量删除
@@ -720,224 +361,92 @@
            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>
@@ -948,127 +457,6 @@
            >
              <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>
management/src/pages/serviceManage/deductionTemplate/service.js
@@ -42,22 +42,8 @@
  });
};
export const cityUpdate = async (data) => {
  return request('/deductionTemplate/city/update', {
    method: 'POST',
    data,
  });
};
export const cityDelete = async (params) => {
  return request('/deductionTemplate/city/delete', {
    method: 'GET',
    params,
  });
};
export const cityBatchDelete = async (params) => {
  return request('/deductionTemplate/city/batchDelete', {
export const remove = async (params) => {
  return request('/deductionTemplate/delete', {
    method: 'GET',
    params,
  });
@@ -69,3 +55,10 @@
    params,
  });
};
export const siteList = async (params) => {
  return request('/site/list', {
    method: 'GET',
    params,
  });
};
management/src/pages/serviceManage/deductionTemplate/style.less
@@ -1,240 +1,29 @@
.page {
  padding: 0 0 24px;
}
.platformBar {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 14px;
}
.platformTabs {
  display: inline-flex;
  align-items: stretch;
  min-height: 32px;
  overflow: hidden;
  border: 1px solid #6aa5ff;
  border-radius: 2px;
  background: #fff;
  flex-wrap: wrap;
}
.platformTab {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-width: 84px;
  height: 32px;
  padding: 0 14px 0 18px;
  font-size: 13px;
  color: #2f6fd6;
  background: #fff;
  border: 0;
  border-right: 1px solid #6aa5ff;
  cursor: pointer;
}
.platformTab:last-child {
  border-right: 0;
}
.platformTabActive {
  color: #fff;
  background: #2f7fff;
}
.platformEmpty {
  min-height: 32px;
  padding: 0 12px;
}
.platformEmpty :global(.ant-empty) {
  margin: 0;
}
.platformEmpty :global(.ant-empty-description) {
  color: #8c8c8c;
}
.platformClose {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 12px;
  height: 12px;
  font-size: 12px;
  line-height: 1;
  opacity: 0.8;
  padding-bottom: 24px;
}
.searchCard {
  margin-bottom: 12px;
}
.searchForm {
.searchForm,
.toolbar {
  display: flex;
  align-items: center;
  gap: 18px;
  flex-wrap: wrap;
}
.searchInput {
  width: 150px !important;
}
.searchSelect {
  width: 220px !important;
  gap: 12px;
  align-items: center;
}
.toolbar {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 10px;
}
.serviceCover,
.serviceCoverEmpty {
  width: 80px;
  height: 52px;
  border-radius: 2px;
.searchInput,
.searchSelect {
  width: 200px !important;
}
.serviceCover {
  object-fit: cover;
  display: block;
}
.serviceCoverEmpty {
  display: flex;
  align-items: center;
  justify-content: center;
  color: #999;
  font-size: 12px;
  background: #f0f0f0;
}
.cityConfigForm :global(.ant-input-number),
.cityConfigForm :global(.ant-cascader),
.cityConfigForm :global(.ant-select) {
.templateForm :global(.ant-input-number),
.templateForm :global(.ant-select) {
  width: 100%;
}
.modalBody {
  padding-top: 2px;
}
.section {
  margin-bottom: 8px;
}
.sectionTitle {
  margin-bottom: 10px;
  font-size: 14px;
  font-weight: 600;
  color: #333;
}
.priceRow {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 20px;
}
.cityList {
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.cityRow {
  display: grid;
  grid-template-columns: 1.1fr 1fr 1fr 34px;
  gap: 14px;
  align-items: center;
}
.cityActions {
  margin-top: 10px;
}
.cityActions :global(.ant-btn) {
  width: 84px;
}
.priceRow :global(.ant-form-item) {
  margin-bottom: 0;
}
.section :global(.ant-form-item) {
  margin-bottom: 14px;
}
.section :global(.ant-form-item-label) {
  padding-bottom: 4px;
}
.section :global(.ant-input),
.section :global(.ant-cascader),
.section :global(.ant-select),
.section :global(.ant-input-number) {
  width: 100%;
}
.section :global(.ant-radio-group) {
  display: flex;
  flex-wrap: wrap;
  gap: 12px 18px;
}
.platformForm :global(.ant-form-item-label) {
  padding-bottom: 6px;
}
.platformForm :global(.ant-input-number),
.platformForm :global(.ant-input),
.platformForm :global(.ant-cascader),
.platformForm :global(.ant-select) {
  width: 100%;
}
.platformHint {
  margin: -12px 0 18px;
  color: #8c8c8c;
  font-size: 12px;
  line-height: 20px;
}
.platformPriceRow {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 64px;
}
.platformPriceRow :global(.ant-form-item) {
  margin-bottom: 12px;
}
.platformAddBtn {
  width: 70px;
  margin-bottom: 12px;
}
.platformCityList {
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.platformCityRow {
  display: grid;
  grid-template-columns: 1fr 1.08fr 1.08fr 34px;
  gap: 18px;
  align-items: center;
}
.platformCityRow :global(.ant-form-item) {
  margin-bottom: 0;
}