From 8c82d181171f8ff06763998ea31fe616aaecbbcf Mon Sep 17 00:00:00 2001 From: zty Date: Fri, 10 Jul 2026 03:30:36 -0400 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E6=9D=90=E6=96=99=E5=BA=93?= =?UTF-8?q?=E6=8E=92=E5=BA=8F=E4=BE=9D=E6=8D=AE/=E5=88=86=E7=BB=84?= =?UTF-8?q?=E5=BC=80=E5=85=B3=20+=20=E5=81=A5=E5=BA=B7=E6=A1=A3=E6=A0=87?= =?UTF-8?q?=E7=AD=BE=E4=B8=8E=E5=88=86=E5=8C=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01VCShKevM5prWZhp1kk1iGh --- .../src/components/material-card/index.tsx | 698 ++++++++++++++++++ .../iapip-web/src/services/api/material.ts | 1 + 2 files changed, 699 insertions(+) create mode 100644 空气质量预测/源码/用户端/iapip-web/src/components/material-card/index.tsx diff --git a/空气质量预测/源码/用户端/iapip-web/src/components/material-card/index.tsx b/空气质量预测/源码/用户端/iapip-web/src/components/material-card/index.tsx new file mode 100644 index 0000000..6dfe4ec --- /dev/null +++ b/空气质量预测/源码/用户端/iapip-web/src/components/material-card/index.tsx @@ -0,0 +1,698 @@ +import { DATA_TYPE } from '@/common/constants' +import { ADMIN_TYPE, AnyObj, ECO_LEVEL, Material } from '@/common/types' +import { searchParams } from '@/common/utils' +import MaterialModalForm from '@/components/material-modal-form' +import PageCard from '@/components/page-card' +import api from '@/services/api' +import { + ActionType, + LightFilter, + ProFormCascader, + ProFormSelect, + ProFormText, + ProTable, + QueryFilter +} from '@ant-design/pro-components' +import { Button, Form, InputNumber, Modal, Popconfirm, Select, Switch, Tag, message } from 'antd' +import { useRef, useState } from 'react' +import { useModel } from '@umijs/max' +import ImportMaterialsModal from '../import-file-modal/materials' + +const MaterialCard: React.FC<{ + title?: string + modal?: boolean + modalProps?: { + open?: boolean + onCancel?: () => void + } + defaultSelected?: Material[] + onAdd?: (addedMtrls?: Material[]) => void +}> = ({ title, modal, modalProps, defaultSelected, onAdd }) => { + const [tab, setTab] = useState(DATA_TYPE.PUBLIC) + const tableRef = useRef() + const [filterForm] = Form.useForm() + const [filterConditions, setFilterConditions] = useState() + const [details, setDetails] = useState>() + const [selected, setSelected] = useState() + // 材料排序依据(默认综合 Ypt 升序)与是否按健康档分组显示 + const [sortKey, setSortKey] = useState('ypt') + const [groupByTier, setGroupByTier] = useState(false) + const { initialState } = useModel('@@initialState') + const cert = initialState?.cert + const isAdmin = !!cert?.is_admin + // 公共材料编辑权限:超管,或具备材料管理权限的管理员(与后端 authz 规则一致) + const canPubMtrlMgmt = + isAdmin && + (cert?.type === ADMIN_TYPE.SUPER_ADMIN || + (cert?.type === ADMIN_TYPE.ADMIN && cert?.authz_mtrl_mgmt === 1)) + const pageDataRef = useRef([]) + // 仅在「手动排序」视图下启用排序控件(避免在按其他字段排序时破坏 sort_order 语义) + const canSort = !modal && tab === DATA_TYPE.PUBLIC && isAdmin && (!filterConditions?.sort || filterConditions?.sort === 'sort_order') + // pageDataRef.current 为全部材料(按 sort_order 升序)。输入目标全局序号(1-based)直接定位: + // 把材料移到目标位置后,对全部材料按新顺序连续重编号(0,1,2,...),保证全局顺序一致、不跨页错乱 + const moveSortTo = async (material_id: string, targetPos: number) => { + const list = pageDataRef.current ?? [] + const fromIdx = list.findIndex((m) => m.material_id === material_id) + if (fromIdx < 0) return + let toIdx = Math.floor(targetPos) - 1 + if (Number.isNaN(toIdx)) return + if (toIdx < 0) toIdx = 0 + if (toIdx >= list.length) toIdx = list.length - 1 + if (toIdx === fromIdx) return + const arr = [...list] + const [moved] = arr.splice(fromIdx, 1) + arr.splice(toIdx, 0, moved) + const updates: { material_id: string; sort_order: number }[] = [] + arr.forEach((m, i) => { + if ((m.sort_order ?? 0) !== i) { + updates.push({ material_id: m.material_id, sort_order: i }) + } + }) + if (!updates.length) return + try { + await Promise.all(updates.map((u) => api.material.updatePubSort(u))) + message.success('排序已更新') + await tableRef?.current?.reload?.() + } catch (err) {} + } + // 上下箭头:复用 moveSortTo,按全局位置上移/下移一格 + const moveSort = async (material_id: string, dir: -1 | 1) => { + const list = pageDataRef.current ?? [] + const idx = list.findIndex((m) => m.material_id === material_id) + if (idx < 0) return + const ni = idx + dir + if (ni < 0 || ni >= list.length) return + await moveSortTo(material_id, ni + 1) + } + // 取材料在全部数据中的全局序号(0-based),用于排序输入框显示 + const globalIndexOf = (material_id: string) => (pageDataRef.current ?? []).findIndex((m) => m.material_id === material_id) + const content = ( + setTab(key as DATA_TYPE)} + extra={ + !modal && tab === DATA_TYPE.PRIVATE + ? [ + 导入材料} + onSuccess={async () => { + await tableRef?.current?.reload?.() + }} + />, + 新建材料} + onFinish={async (data) => { + try { + await api.material.createSelf(data) + message.success('新建成功') + await tableRef?.current?.reload?.() + return true + } catch (err) { + return false + } + }} + /> + ] + : [] + } + cardStyle={ + modal + ? { + borderBottomWidth: 0 + } + : undefined + } + > +
+
+ setFilterConditions(data)}> + + + {/* 公共库/自建库改由顶部 tab 切换,此处不再需要「类型」筛选 */} + { + const cats = await api.config.getMtrlCatList() + return cats.map((item) => ({ + value: item.first_category, + label: item.first_category, + children: item.second_categories.map((c) => ({ + value: c.second_category, + label: c.second_category + })) + })) + }} + transform={(val) => ({ category: (val as string[][])?.map((v: string[]) => v.join('/')) ?? undefined })} + /> + + + + + + {modal || tab === DATA_TYPE.PUBLIC ? ( + + ) : ( + <> + )} + {isAdmin && ( + + )} + +
+
+ +
+
+
+ + 排序依据{' '} +