feat(ms): 管理端材料库排序依据/分组 + 健康档标签
- Material 类型新增 ypt/health_tier 字段 - material.ts search() 查询参数补 group_by_tier - material-card 新增排序依据 Select(手动排序/综合 Ypt/五种污染物释放量)与按健康档分组 Switch, 健康档 Tag 列,分组时按档打分区标题 - 顶部排序依据 Select 为唯一排序权威来源:移除旧筛选区排序下拉对 searchParams 第三参数的驱动, 避免其静默覆盖新排序请求(对齐用户端 iapip-web 同一修复) - 经核查 ms 端不存在手动拖动/位置输入重排 sort_order 的既有功能,故无需像 web 端那样额外加 sortKey==='sort_order' && !groupByTier 门控 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VCShKevM5prWZhp1kk1iGh
This commit is contained in:
parent
c7134f4332
commit
fb033ada28
|
|
@ -0,0 +1,298 @@
|
||||||
|
export type AnyObj = {
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Arrayable<T> = T | T[]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server Types
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type Admin = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
password: string
|
||||||
|
username: string
|
||||||
|
status: number
|
||||||
|
type: ADMIN_TYPE
|
||||||
|
authz_hist_mgmt: number
|
||||||
|
authz_tmpl_mgmt: number
|
||||||
|
authz_mtrl_mgmt: number
|
||||||
|
authz_audit_mgmt: number
|
||||||
|
authz_acct_mgmt: number
|
||||||
|
authz_sys_cfg: number
|
||||||
|
creator_id: string
|
||||||
|
creator_name: string
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ADMIN_TYPE {
|
||||||
|
SUPER_ADMIN = 1,
|
||||||
|
ADMIN = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
export type User = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
password: string
|
||||||
|
username: string
|
||||||
|
status: number
|
||||||
|
creator_id: string
|
||||||
|
creator_name: string
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AdminCert = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
username: string
|
||||||
|
status: number
|
||||||
|
type: ADMIN_TYPE
|
||||||
|
authz_hist_mgmt: number
|
||||||
|
authz_tmpl_mgmt: number
|
||||||
|
authz_mtrl_mgmt: number
|
||||||
|
authz_audit_mgmt: number
|
||||||
|
authz_acct_mgmt: number
|
||||||
|
authz_sys_cfg: number
|
||||||
|
ip: string
|
||||||
|
login_at: Date
|
||||||
|
expire_at: Date
|
||||||
|
iat?: number
|
||||||
|
exp?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Creator = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
username: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Project = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
category?: string
|
||||||
|
province?: string
|
||||||
|
city?: string
|
||||||
|
climatic_zone?: string
|
||||||
|
constr_area?: number
|
||||||
|
usage_area?: number
|
||||||
|
spaces?: Space[]
|
||||||
|
status?: number
|
||||||
|
display?: number
|
||||||
|
predicted_at?: string
|
||||||
|
score?: number
|
||||||
|
grade?: number
|
||||||
|
collectors?: { user_id: string; project_id: string }[]
|
||||||
|
from?: FROM_TYPE
|
||||||
|
creator_id?: string
|
||||||
|
creator_name: string
|
||||||
|
creator?: Creator
|
||||||
|
related_user?: Creator
|
||||||
|
related_admin?: Creator
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Space = {
|
||||||
|
id: string
|
||||||
|
project_id: string
|
||||||
|
name: string
|
||||||
|
category?: string
|
||||||
|
layout: number
|
||||||
|
height?: number
|
||||||
|
area?: number
|
||||||
|
volume?: number
|
||||||
|
env_temp?: number
|
||||||
|
env_hum?: number
|
||||||
|
env_vent_rate?: number
|
||||||
|
apc_lmt_std_id?: string
|
||||||
|
apc_lmt_std?: ApcLimitStandard
|
||||||
|
materials?: Material[]
|
||||||
|
methanal_c?: number
|
||||||
|
tvoc_c?: number
|
||||||
|
benzene_c?: number
|
||||||
|
toluene_c?: number
|
||||||
|
p_xylene_c?: number
|
||||||
|
methanal_c_exceed?: number
|
||||||
|
tvoc_c_exceed?: number
|
||||||
|
benzene_c_exceed?: number
|
||||||
|
toluene_c_exceed?: number
|
||||||
|
p_xylene_c_exceed?: number
|
||||||
|
creator_id: string
|
||||||
|
creator_name: string
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Material = {
|
||||||
|
id?: number
|
||||||
|
material_id: string
|
||||||
|
project_id?: string
|
||||||
|
space_id?: string
|
||||||
|
name?: string
|
||||||
|
category?: string
|
||||||
|
brand?: string
|
||||||
|
factory?: string
|
||||||
|
spec?: string
|
||||||
|
eco_level?: ECO_LEVEL
|
||||||
|
unit?: MTRL_UNIT
|
||||||
|
qty?: number
|
||||||
|
display?: number
|
||||||
|
ypt?: number | null
|
||||||
|
health_tier?: 'A' | 'B' | 'C' | null
|
||||||
|
collectors?: { user_id: string; project_id: string }[]
|
||||||
|
methanal?: number
|
||||||
|
methanal_min_be?: number
|
||||||
|
methanal_be_area?: number
|
||||||
|
methanal_be_roc?: number
|
||||||
|
tvoc?: number
|
||||||
|
tvoc_min_be?: number
|
||||||
|
tvoc_be_area?: number
|
||||||
|
tvoc_be_roc?: number
|
||||||
|
benzene?: number
|
||||||
|
benzene_min_be?: number
|
||||||
|
benzene_be_area?: number
|
||||||
|
benzene_be_roc?: number
|
||||||
|
toluene?: number
|
||||||
|
toluene_min_be?: number
|
||||||
|
toluene_be_area?: number
|
||||||
|
toluene_be_roc?: number
|
||||||
|
p_xylene?: number
|
||||||
|
p_xylene_min_be?: number
|
||||||
|
p_xylene_be_area?: number
|
||||||
|
p_xylene_be_roc?: number
|
||||||
|
methanal_cr?: number
|
||||||
|
tvoc_cr?: number
|
||||||
|
benzene_cr?: number
|
||||||
|
toluene_cr?: number
|
||||||
|
p_xylene_cr?: number
|
||||||
|
methanal_cr_exceed?: number
|
||||||
|
tvoc_cr_exceed?: number
|
||||||
|
benzene_cr_exceed?: number
|
||||||
|
toluene_cr_exceed?: number
|
||||||
|
p_xylene_cr_exceed?: number
|
||||||
|
is_methanal_ps?: number
|
||||||
|
is_tvoc_ps?: number
|
||||||
|
is_benzene_ps?: number
|
||||||
|
is_toluene_ps?: number
|
||||||
|
is_p_xylene_ps?: number
|
||||||
|
from?: FROM_TYPE
|
||||||
|
user_id: string
|
||||||
|
user_name: string
|
||||||
|
creator_id?: string
|
||||||
|
creator_name: string
|
||||||
|
creator?: Creator
|
||||||
|
related_user?: Creator
|
||||||
|
related_admin?: Creator
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AuditRequest = {
|
||||||
|
id: string
|
||||||
|
status: AUDIT_REQ_STATUS
|
||||||
|
source_id?: string
|
||||||
|
source_creator_id?: string
|
||||||
|
source: AUDIT_REQ_SRC
|
||||||
|
type: AUDIT_REQ_TYPE
|
||||||
|
desc?: string
|
||||||
|
remark?: string
|
||||||
|
details: AuditRequestDetails[]
|
||||||
|
auditor_id?: string
|
||||||
|
auditor_name?: string
|
||||||
|
auditor?: Creator
|
||||||
|
audited_at?: string
|
||||||
|
creator_id?: string
|
||||||
|
creator_name: string
|
||||||
|
creator?: Creator
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AuditRequestDetails = {
|
||||||
|
id: number
|
||||||
|
audit_request_id: number
|
||||||
|
content: AnyObj
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ECO_LEVEL {
|
||||||
|
E0 = 'E0',
|
||||||
|
E1 = 'E1',
|
||||||
|
E2 = 'E2'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MTRL_UNIT {
|
||||||
|
AREA = 'AREA',
|
||||||
|
LEN = 'LEN'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum FROM_TYPE {
|
||||||
|
CREATE = 'CREATE',
|
||||||
|
IMPORT = 'IMPORT',
|
||||||
|
COLLECT = 'COLLECT'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AUDIT_REQ_SRC {
|
||||||
|
PUB_TEMP = 'PUB_TEMP',
|
||||||
|
SELF_TEMP = 'SELF_TEMP',
|
||||||
|
PUB_MTRL = 'PUB_MTRL',
|
||||||
|
SELF_MTRL = 'SELF_MTRL',
|
||||||
|
HIS_PROJ = 'HIS_PROJ'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AUDIT_REQ_TYPE {
|
||||||
|
CREATE = 'CREATE',
|
||||||
|
IMPORT = 'IMPORT',
|
||||||
|
UPDATE = 'UPDATE',
|
||||||
|
DELETE = 'DELETE',
|
||||||
|
COLLECT = 'COLLECT',
|
||||||
|
EXPORT = 'EXPORT'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AUDIT_REQ_STATUS {
|
||||||
|
REJECT = -1,
|
||||||
|
HOLD = 0,
|
||||||
|
PASS = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProjectSpaceCategory = {
|
||||||
|
pry_no_1: number
|
||||||
|
project_category: string
|
||||||
|
space_categories: {
|
||||||
|
pry_no_2: number
|
||||||
|
space_category: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MaterialCategory = {
|
||||||
|
pry_no_1: number
|
||||||
|
first_category: string
|
||||||
|
second_categories: {
|
||||||
|
pry_no_2: number
|
||||||
|
second_category: string
|
||||||
|
unit?: MTRL_UNIT
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CityInformation = {
|
||||||
|
pry_no_1: number
|
||||||
|
province: string
|
||||||
|
cities: {
|
||||||
|
pry_no_2: number
|
||||||
|
city: string
|
||||||
|
climatic_zone?: string
|
||||||
|
env_temp: number
|
||||||
|
env_hum: number
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ApcLimitStandard = {
|
||||||
|
id: string
|
||||||
|
methanal_c_lmt: number
|
||||||
|
tvoc_c_lmt: number
|
||||||
|
benzene_c_lmt: number
|
||||||
|
toluene_c_lmt: number
|
||||||
|
p_xylene_c_lmt: number
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,546 @@
|
||||||
|
import { DATA_TYPE } from '@/common/constants'
|
||||||
|
import { AnyObj, ECO_LEVEL, Material } from '@/common/types'
|
||||||
|
import { searchParams } from '@/common/utils'
|
||||||
|
import api from '@/services/api'
|
||||||
|
import {
|
||||||
|
ActionType,
|
||||||
|
LightFilter,
|
||||||
|
ProFormCascader,
|
||||||
|
ProFormSelect,
|
||||||
|
ProFormText,
|
||||||
|
ProTable
|
||||||
|
} from '@ant-design/pro-components'
|
||||||
|
import { Badge, Button, Form, Modal, Select, Switch, Tag } from 'antd'
|
||||||
|
import { useRef, useState } from 'react'
|
||||||
|
import OperateMaterial from '../apply-request-modal/operate-material'
|
||||||
|
import ImportMaterialsModal from '../import-file-modal/materials'
|
||||||
|
import MaterialModalForm from '../material-modal-form'
|
||||||
|
import PageCard from '../page-card'
|
||||||
|
|
||||||
|
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<ActionType>()
|
||||||
|
const [filterForm] = Form.useForm()
|
||||||
|
const [filterConditions, setFilterConditions] = useState<AnyObj>()
|
||||||
|
const [openCreateModal, setOpenCreateModal] = useState(false)
|
||||||
|
const [details, setDetails] = useState<Material>()
|
||||||
|
const [selected, setSelected] = useState<Material[] | undefined>()
|
||||||
|
const [content, setContent] = useState<AnyObj>()
|
||||||
|
// 材料排序依据(默认综合 Ypt 升序)与是否按健康档分组显示
|
||||||
|
const [sortKey, setSortKey] = useState<string>('ypt')
|
||||||
|
const [groupByTier, setGroupByTier] = useState<boolean>(false)
|
||||||
|
const page = (
|
||||||
|
<PageCard
|
||||||
|
title={title}
|
||||||
|
defActTabKey={!modal ? DATA_TYPE.PUBLIC : undefined}
|
||||||
|
tabItems={
|
||||||
|
!modal
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
key: DATA_TYPE.PUBLIC,
|
||||||
|
label: '公共库'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: DATA_TYPE.PRIVATE,
|
||||||
|
label: '自建库'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onTabChange={!modal ? (key) => setTab(key as DATA_TYPE) : undefined}
|
||||||
|
extra={
|
||||||
|
!modal && tab === DATA_TYPE.PUBLIC
|
||||||
|
? [
|
||||||
|
<ImportMaterialsModal
|
||||||
|
key="import-modal"
|
||||||
|
title="导入公共材料"
|
||||||
|
trigger={<Button>导入材料</Button>}
|
||||||
|
/>,
|
||||||
|
<>
|
||||||
|
<Button type="primary" onClick={() => setOpenCreateModal(true)}>
|
||||||
|
新建材料
|
||||||
|
</Button>
|
||||||
|
<MaterialModalForm
|
||||||
|
key="create-modal-form"
|
||||||
|
title="新建公共材料"
|
||||||
|
displayStatus
|
||||||
|
displayDetails
|
||||||
|
open={openCreateModal}
|
||||||
|
onCancel={() => setOpenCreateModal(false)}
|
||||||
|
onFinish={async (data) => setContent(data)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
cardStyle={
|
||||||
|
modal
|
||||||
|
? {
|
||||||
|
borderBottomWidth: 0
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="flex mb-4">
|
||||||
|
<div style={{ width: 'calc(100% - 80px)' }}>
|
||||||
|
<LightFilter form={filterForm} onFinish={async (data) => setFilterConditions(data)}>
|
||||||
|
<ProFormText name="material_id" label="材料ID" />
|
||||||
|
<ProFormText name="name" label="材料名称" />
|
||||||
|
<ProFormCascader
|
||||||
|
name="category"
|
||||||
|
label="材料类别"
|
||||||
|
fieldProps={{
|
||||||
|
placeholder: '',
|
||||||
|
multiple: true,
|
||||||
|
showSearch: true
|
||||||
|
}}
|
||||||
|
request={async () => {
|
||||||
|
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.map((v: string[]) => v.join('/')) })}
|
||||||
|
/>
|
||||||
|
<ProFormText name="brand" label="材料品牌" />
|
||||||
|
<ProFormText name="factory" label="材料厂家" />
|
||||||
|
<ProFormText name="spec" label="材料规格" />
|
||||||
|
<ProFormSelect
|
||||||
|
name="eco_level"
|
||||||
|
label="环保级别"
|
||||||
|
options={[{ value: ECO_LEVEL.E0 }, { value: ECO_LEVEL.E1 }, { value: ECO_LEVEL.E2 }]}
|
||||||
|
mode="multiple"
|
||||||
|
/>
|
||||||
|
{tab === DATA_TYPE.PRIVATE ? (
|
||||||
|
<ProFormText name="creator_name" label="创建用户" />
|
||||||
|
) : (
|
||||||
|
<ProFormSelect
|
||||||
|
name="display"
|
||||||
|
label="状态"
|
||||||
|
options={[
|
||||||
|
{ label: '展示', value: 1 },
|
||||||
|
{ label: '隐藏', value: 0 }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<ProFormSelect
|
||||||
|
name="sort"
|
||||||
|
label="排序"
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
label: '材料类别A-Z',
|
||||||
|
value: 'category'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '最近更新时间由近到远',
|
||||||
|
value: 'updated_at'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '甲醛释放量由低到高',
|
||||||
|
value: 'methanal_be_area'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'TVOC释放量由低到高',
|
||||||
|
value: 'tvoc_be_area'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '苯释放量由低到高',
|
||||||
|
value: 'benzene_be_area'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '甲苯释放量由低到高',
|
||||||
|
value: 'toluene_be_area'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '二甲苯释放量由低到高',
|
||||||
|
value: 'p_xylene_be_area'
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</LightFilter>
|
||||||
|
</div>
|
||||||
|
<div style={{ width: 80 }} className="flex justify-center">
|
||||||
|
<Button
|
||||||
|
key="reset"
|
||||||
|
onClick={() => {
|
||||||
|
setFilterConditions(undefined)
|
||||||
|
filterForm.resetFields()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
重置
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4 mb-4">
|
||||||
|
<span>
|
||||||
|
排序依据{' '}
|
||||||
|
<Select
|
||||||
|
size="small"
|
||||||
|
style={{ width: 160 }}
|
||||||
|
value={sortKey}
|
||||||
|
onChange={(v) => {
|
||||||
|
setSortKey(v)
|
||||||
|
tableRef.current?.reload()
|
||||||
|
}}
|
||||||
|
options={[
|
||||||
|
{ value: 'sort_order', label: '手动排序' },
|
||||||
|
{ value: 'ypt', label: '综合 Ypt' },
|
||||||
|
{ value: 'methanal_be_area', label: '甲醛 Yp' },
|
||||||
|
{ value: 'tvoc_be_area', label: 'TVOC Yp' },
|
||||||
|
{ value: 'benzene_be_area', label: '苯 Yp' },
|
||||||
|
{ value: 'toluene_be_area', label: '甲苯 Yp' },
|
||||||
|
{ value: 'p_xylene_be_area', label: '二甲苯 Yp' }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
按健康等级分组{' '}
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
checked={groupByTier}
|
||||||
|
onChange={(c) => {
|
||||||
|
setGroupByTier(c)
|
||||||
|
tableRef.current?.reload()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ProTable<Material>
|
||||||
|
cardProps={{
|
||||||
|
bodyStyle: {
|
||||||
|
padding: 0
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
size="small"
|
||||||
|
search={false}
|
||||||
|
options={false}
|
||||||
|
actionRef={tableRef}
|
||||||
|
scroll={modal ? { y: 'calc(100vh - 400px)' } : { y: 'calc(100vh - 360px)' }}
|
||||||
|
rowSelection={
|
||||||
|
modal
|
||||||
|
? {
|
||||||
|
type: 'checkbox',
|
||||||
|
selectedRowKeys: selected?.map((m) => m.material_id),
|
||||||
|
// onSelect: (mtrl, slct) => {
|
||||||
|
// setSelected((pre) => {
|
||||||
|
// if (slct) {
|
||||||
|
// const nxt = pre ? [...pre] : []
|
||||||
|
// nxt.push(mtrl)
|
||||||
|
// return nxt
|
||||||
|
// } else {
|
||||||
|
// return pre?.filter((m) => m.material_id !== mtrl.material_id)
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// },
|
||||||
|
onChange: (_, mtrls) => setSelected(mtrls),
|
||||||
|
getCheckboxProps: (mtrl) => ({
|
||||||
|
disabled:
|
||||||
|
defaultSelected &&
|
||||||
|
defaultSelected.findIndex((m) => m.material_id === mtrl.material_id) !== -1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
: false
|
||||||
|
}
|
||||||
|
onRow={(mtrl) => ({
|
||||||
|
onClick: () => {
|
||||||
|
if (!defaultSelected?.find((m) => m.material_id === mtrl.material_id)) {
|
||||||
|
setSelected((pre) => {
|
||||||
|
if (pre) {
|
||||||
|
if (pre.find((m) => m.material_id === mtrl.material_id)) {
|
||||||
|
return pre.filter((m) => m.material_id !== mtrl.material_id)
|
||||||
|
} else {
|
||||||
|
const nxt = [...pre]
|
||||||
|
nxt.push(mtrl)
|
||||||
|
return nxt
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return [mtrl]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
rowKey={(data) => data.material_id}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
title: '材料ID',
|
||||||
|
dataIndex: 'material_id',
|
||||||
|
width: 120,
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '材料名称',
|
||||||
|
dataIndex: 'name',
|
||||||
|
align: 'center',
|
||||||
|
render: (_, m) => {
|
||||||
|
const tierHead = (m as Material & { __tierHead?: string }).__tierHead
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{tierHead && (
|
||||||
|
<div style={{ fontWeight: 600, color: '#888', margin: '4px 0' }}>
|
||||||
|
{'档位 ' + tierHead}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<span>{m.name}</span>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '材料类别',
|
||||||
|
dataIndex: 'category',
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '材料品牌',
|
||||||
|
dataIndex: 'brand',
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '材料厂家',
|
||||||
|
dataIndex: 'factory',
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '材料规格',
|
||||||
|
dataIndex: 'spec',
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '环保等级',
|
||||||
|
dataIndex: 'eco_level',
|
||||||
|
align: 'center',
|
||||||
|
width: 90,
|
||||||
|
render: (_, { eco_level }) => (eco_level ? <Tag>{eco_level}</Tag> : '-')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '健康档',
|
||||||
|
dataIndex: 'health_tier',
|
||||||
|
align: 'center',
|
||||||
|
width: 70,
|
||||||
|
render: (_, m) => {
|
||||||
|
const color =
|
||||||
|
m.health_tier === 'A'
|
||||||
|
? 'green'
|
||||||
|
: m.health_tier === 'B'
|
||||||
|
? 'gold'
|
||||||
|
: m.health_tier === 'C'
|
||||||
|
? 'red'
|
||||||
|
: 'default'
|
||||||
|
return <Tag color={color}>{m.health_tier ?? '—'}</Tag>
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
dataIndex: 'display',
|
||||||
|
align: 'center',
|
||||||
|
width: 80,
|
||||||
|
hideInTable: modal || tab === DATA_TYPE.PRIVATE,
|
||||||
|
render: (_, { display }) =>
|
||||||
|
display === 1 ? (
|
||||||
|
<Badge status="success" text="展示" />
|
||||||
|
) : (
|
||||||
|
<Badge status="error" text="隐藏" />
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创建用户',
|
||||||
|
dataIndex: 'creator_name',
|
||||||
|
align: 'center',
|
||||||
|
hideInTable: modal || tab === DATA_TYPE.PUBLIC,
|
||||||
|
renderText: (_, { creator }) => creator?.username
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '最近更新时间',
|
||||||
|
dataIndex: 'updated_at',
|
||||||
|
align: 'center',
|
||||||
|
width: 160,
|
||||||
|
renderText: (dt) => new Date(dt).toLocaleString('zh-CN')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
valueType: 'option',
|
||||||
|
align: 'center',
|
||||||
|
width: 100,
|
||||||
|
render: (_, item) => (
|
||||||
|
<div className="flex justify-center space-x-2">
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
style={{ padding: 0 }}
|
||||||
|
onClick={(e) => {
|
||||||
|
setDetails(item)
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
详情
|
||||||
|
</Button>
|
||||||
|
{!modal && tab === DATA_TYPE.PUBLIC ? (
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
style={{ padding: 0 }}
|
||||||
|
onClick={(e) => {
|
||||||
|
setContent({ delete_id: item.material_id })
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
) : !modal && tab === DATA_TYPE.PRIVATE ? (
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
style={{ padding: 0 }}
|
||||||
|
onClick={(e) => {
|
||||||
|
setContent({ collect_id: item.material_id, creator_id: item.creator_id })
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
收录
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
params={{ type: tab, conditions: filterConditions, sortKey, groupByTier }}
|
||||||
|
request={async ({ type, conditions, sortKey: skParam, groupByTier: gbtParam, ...other }) => {
|
||||||
|
try {
|
||||||
|
// 旧筛选区「排序」下拉已被顶部「排序依据」Select 取代:
|
||||||
|
// 从 conditions 里剔除 sort,不让它随 otherConditions 传给后端,
|
||||||
|
// 也不再把它转成 searchParams() 的第三个参数(那会覆盖 query.sort_key/sort_val,
|
||||||
|
// 导致新 Select 选的排序字段被静默忽略)。'category' 仍保留为纯前端排序。
|
||||||
|
const { sort, ...otherConditions } = conditions ?? {}
|
||||||
|
const sortByCategory = sort === 'category'
|
||||||
|
const data = await api.material.search(
|
||||||
|
type,
|
||||||
|
searchParams(
|
||||||
|
{
|
||||||
|
...otherConditions,
|
||||||
|
...other,
|
||||||
|
display: modal ? 1 : undefined,
|
||||||
|
with_cltrs: 1,
|
||||||
|
// 顶部「排序依据」Select 始终是排序字段的唯一来源,不会被旧筛选区排序覆盖
|
||||||
|
sort_key: skParam,
|
||||||
|
sort_val: 'asc',
|
||||||
|
group_by_tier: gbtParam ? 1 : 0
|
||||||
|
},
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (sortByCategory) {
|
||||||
|
data.sort((a, b) => (a.category ?? '').localeCompare(b.category ?? ''))
|
||||||
|
}
|
||||||
|
if (gbtParam) {
|
||||||
|
// 分组开时后端已按档排序;为每档第一条打标,供「材料名称」列渲染分区小标题
|
||||||
|
let last: string | null | undefined = undefined
|
||||||
|
data.forEach((m) => {
|
||||||
|
if (m.health_tier !== last) {
|
||||||
|
;(m as Material & { __tierHead?: string }).__tierHead = m.health_tier ?? '数据不全'
|
||||||
|
last = m.health_tier
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
success: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
pagination={
|
||||||
|
!modal
|
||||||
|
? {
|
||||||
|
position: ['bottomRight'],
|
||||||
|
defaultPageSize: 10,
|
||||||
|
showSizeChanger: true
|
||||||
|
}
|
||||||
|
: false
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<MaterialModalForm
|
||||||
|
title={`${tab === DATA_TYPE.PUBLIC ? '公共' : '自建'}材料详情`}
|
||||||
|
initialValues={details}
|
||||||
|
displayId
|
||||||
|
displayCreator={!modal && tab === DATA_TYPE.PRIVATE}
|
||||||
|
displayStatus={!modal && tab === DATA_TYPE.PUBLIC}
|
||||||
|
displayDetails
|
||||||
|
readonly={modal || tab === DATA_TYPE.PRIVATE}
|
||||||
|
open={details !== undefined}
|
||||||
|
submitText="保存"
|
||||||
|
onFinish={tab === DATA_TYPE.PUBLIC ? async (data) => setContent(data) : undefined}
|
||||||
|
onCancel={() => setDetails(undefined)}
|
||||||
|
/>
|
||||||
|
</PageCard>
|
||||||
|
)
|
||||||
|
return modal ? (
|
||||||
|
<Modal
|
||||||
|
style={{
|
||||||
|
top: 65
|
||||||
|
}}
|
||||||
|
styles={{
|
||||||
|
content: {
|
||||||
|
padding: 0
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
margin: 0
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
width="calc(100% - 100px)"
|
||||||
|
footer={
|
||||||
|
<div className="space-x-4 pb-4 pr-[20px]">
|
||||||
|
<Button style={{ width: 64 }} onClick={modalProps?.onCancel}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button type="primary" style={{ width: 64 }} onClick={() => onAdd?.(selected)}>
|
||||||
|
添加
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
maskClosable={false}
|
||||||
|
destroyOnClose
|
||||||
|
open={modalProps?.open}
|
||||||
|
afterClose={() => {
|
||||||
|
setFilterConditions(undefined)
|
||||||
|
filterForm.resetFields()
|
||||||
|
setSelected(undefined)
|
||||||
|
}}
|
||||||
|
onCancel={modalProps?.onCancel}
|
||||||
|
>
|
||||||
|
{page}
|
||||||
|
</Modal>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{page}
|
||||||
|
<OperateMaterial
|
||||||
|
type={tab}
|
||||||
|
content={content}
|
||||||
|
open={content !== undefined}
|
||||||
|
onCancel={() => setContent(undefined)}
|
||||||
|
onFinish={() => {
|
||||||
|
setOpenCreateModal(false)
|
||||||
|
setDetails(undefined)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MaterialCard
|
||||||
|
|
@ -0,0 +1,156 @@
|
||||||
|
import { DATA_TYPE, HTTP_HEADER } from '@/common/constants'
|
||||||
|
import { AnyObj, Arrayable, ECO_LEVEL, FROM_TYPE, MTRL_UNIT, Material } from '@/common/types'
|
||||||
|
import { getLocalToken } from '@/common/utils'
|
||||||
|
import { request } from '@umijs/max'
|
||||||
|
|
||||||
|
export async function createPub(
|
||||||
|
body: {
|
||||||
|
name?: string
|
||||||
|
category?: string
|
||||||
|
brand?: string
|
||||||
|
factory?: string
|
||||||
|
spec?: string
|
||||||
|
eco_level?: ECO_LEVEL
|
||||||
|
display?: number
|
||||||
|
methanal_min_be?: number
|
||||||
|
methanal_be_area?: number
|
||||||
|
methanal_be_roc?: number
|
||||||
|
tvoc_min_be?: number
|
||||||
|
tvoc_be_area?: number
|
||||||
|
tvoc_be_roc?: number
|
||||||
|
benzene_min_be?: number
|
||||||
|
benzene_be_area?: number
|
||||||
|
benzene_be_roc?: number
|
||||||
|
toluene_min_be?: number
|
||||||
|
toluene_be_area?: number
|
||||||
|
toluene_be_roc?: number
|
||||||
|
p_xylene_min_be?: number
|
||||||
|
p_xylene_be_area?: number
|
||||||
|
p_xylene_be_roc?: number
|
||||||
|
from?: FROM_TYPE
|
||||||
|
},
|
||||||
|
options?: AnyObj
|
||||||
|
) {
|
||||||
|
return request<Material>(`/api/mtrl/pub`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
[HTTP_HEADER.AUTHORIZATION]: getLocalToken()
|
||||||
|
},
|
||||||
|
data: body,
|
||||||
|
...(options || {})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function collectSelf(
|
||||||
|
body: {
|
||||||
|
material_id: string
|
||||||
|
creator_id: string
|
||||||
|
display?: number
|
||||||
|
},
|
||||||
|
options?: AnyObj
|
||||||
|
) {
|
||||||
|
return request<Material>(`/api/mtrl/pub/self`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
[HTTP_HEADER.AUTHORIZATION]: getLocalToken()
|
||||||
|
},
|
||||||
|
data: body,
|
||||||
|
...(options || {})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function patchPub(
|
||||||
|
query: {
|
||||||
|
material_id: string
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
name?: string
|
||||||
|
category?: string
|
||||||
|
brand?: string
|
||||||
|
factory?: string
|
||||||
|
spec?: string
|
||||||
|
eco_level?: ECO_LEVEL
|
||||||
|
display?: number
|
||||||
|
methanal_min_be?: number
|
||||||
|
methanal_be_area?: number
|
||||||
|
methanal_be_roc?: number
|
||||||
|
tvoc_min_be?: number
|
||||||
|
tvoc_be_area?: number
|
||||||
|
tvoc_be_roc?: number
|
||||||
|
benzene_min_be?: number
|
||||||
|
benzene_be_area?: number
|
||||||
|
benzene_be_roc?: number
|
||||||
|
toluene_min_be?: number
|
||||||
|
toluene_be_area?: number
|
||||||
|
toluene_be_roc?: number
|
||||||
|
p_xylene_min_be?: number
|
||||||
|
p_xylene_be_area?: number
|
||||||
|
p_xylene_be_roc?: number
|
||||||
|
},
|
||||||
|
options?: AnyObj
|
||||||
|
) {
|
||||||
|
return request<Material>(`/api/mtrl/pub`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
[HTTP_HEADER.AUTHORIZATION]: getLocalToken()
|
||||||
|
},
|
||||||
|
params: query,
|
||||||
|
data: body,
|
||||||
|
...(options || {})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removePub(
|
||||||
|
query: {
|
||||||
|
material_id: Arrayable<string>
|
||||||
|
},
|
||||||
|
options?: AnyObj
|
||||||
|
) {
|
||||||
|
return request<{ count: number }>('/api/mtrl/pub', {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
[HTTP_HEADER.AUTHORIZATION]: getLocalToken()
|
||||||
|
},
|
||||||
|
params: query,
|
||||||
|
...(options || {})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function search(
|
||||||
|
type: DATA_TYPE,
|
||||||
|
query?: {
|
||||||
|
material_id?: Arrayable<string>
|
||||||
|
name?: Arrayable<string>
|
||||||
|
category?: Arrayable<string>
|
||||||
|
brand?: Arrayable<string>
|
||||||
|
factory?: Arrayable<string>
|
||||||
|
spec?: Arrayable<string>
|
||||||
|
eco_level?: Arrayable<ECO_LEVEL>
|
||||||
|
unit?: Arrayable<MTRL_UNIT>
|
||||||
|
display?: number
|
||||||
|
collected?: number
|
||||||
|
creator_id?: Arrayable<string>
|
||||||
|
creator_name?: Arrayable<string>
|
||||||
|
with_cltrs?: number
|
||||||
|
sort_key?: Arrayable<string>
|
||||||
|
sort_val?: Arrayable<string>
|
||||||
|
group_by_tier?: 0 | 1
|
||||||
|
page?: number
|
||||||
|
size?: number
|
||||||
|
},
|
||||||
|
options?: AnyObj
|
||||||
|
) {
|
||||||
|
if (type === DATA_TYPE.PRIVATE) {
|
||||||
|
delete query?.display
|
||||||
|
delete query?.collected
|
||||||
|
delete query?.with_cltrs
|
||||||
|
}
|
||||||
|
return request<Material[]>(`/api/mtrl/${type}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
[HTTP_HEADER.AUTHORIZATION]: getLocalToken()
|
||||||
|
},
|
||||||
|
params: query,
|
||||||
|
...(options || {})
|
||||||
|
})
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue