fix asset-center material library sync and workflow verification
This commit is contained in:
parent
29036152d1
commit
f0bf6c09a2
@ -13,51 +13,11 @@
|
||||
|
||||
## 当前未完成项
|
||||
|
||||
### 待前端修正 (前端 UI Agent 负责)
|
||||
|
||||
- [ ] **资产中心弹窗主按钮禁用态过浅**
|
||||
归属:`前端负责`
|
||||
说明:资产中心内一批浅色弹窗底部的主按钮,在不可提交或 loading 前后状态下,存在“底色发灰、文字发白、和浅色 footer 混在一起”的问题,主次层级不清晰。请统一修正 `filled` 主按钮在浅色主题下的默认态 / disabled 态 / loading 态对比度,不要只改单个弹窗。
|
||||
涉及位置:
|
||||
`资产族弹窗`:`创建` / `保存修改`
|
||||
`规格款弹窗`:`创建规格款` / `保存修改`
|
||||
`CAD 导入弹窗`:`开始导入` / `继续导入`
|
||||
`新增/编辑连接角色弹窗`:`新增` / `保存`
|
||||
`基础资料库` 页面弹窗:`创建基材` / `保存修改` / `创建分类` / `创建饰面`
|
||||
|
||||
- [ ] **资产中心弹窗次级文字按钮在浅底上过淡**
|
||||
归属:`前端负责`
|
||||
说明:浅色弹窗 footer 内的 `plain` 次级按钮,现在默认文字和背景层级太接近,小字号时尤其不清楚。请统一检查并修正 `plain` 按钮在浅色弹窗中的默认态、hover 态、禁用态,让“取消 / 完成 / 新增连接角色”这类按钮肉眼一看就能分清。
|
||||
涉及位置:
|
||||
`资产族弹窗`:`取消`
|
||||
`规格款弹窗`:`取消`
|
||||
`CAD 导入弹窗`:`取消` / `完成`
|
||||
`连接角色区域`:`新增连接角色`
|
||||
`新增/编辑连接角色弹窗`:`取消`
|
||||
`基础资料库` 页:`新建分类`
|
||||
`基础资料库` 页面弹窗:`取消`
|
||||
|
||||
- [ ] **资产中心列表内图标按钮默认态辨识度不足**
|
||||
归属:`前端负责`
|
||||
说明:列表和卡片内的编辑 / 删除图标按钮,默认态颜色过浅,未 hover 时像“没激活”或“不可点”,尤其在浅色卡片和浅色分组背景里不够清楚。请统一调整这类圆形图标按钮的默认态和 hover 态,不要只修某一个图标。
|
||||
涉及位置:
|
||||
`资产族 / 规格款列表`:铅笔编辑按钮
|
||||
`连接角色列表`:编辑 / 删除图标按钮
|
||||
`基础资料库` 页:基材 / 分类 / 饰面列表的编辑 / 删除图标按钮
|
||||
|
||||
- [ ] **同一弹窗内主次按钮层级需要统一规范**
|
||||
归属:`前端负责`
|
||||
说明:当前资产中心多个弹窗里,主按钮、次按钮、危险按钮的视觉层级不完全一致;有的主按钮太淡,有的删除按钮太抢,有的次按钮像禁用态。请按统一视觉标准整理这三类按钮在浅色主题弹窗 footer 内的层级关系,保证“主操作最清楚、次操作清楚但不抢、危险操作醒目但不刺眼”。这一项是规则统一,不是逐页补丁。
|
||||
|
||||
- [ ] **基础资料库页面需要做正式视觉收口**
|
||||
归属:`前端负责`
|
||||
说明:`基础资料库` 已经作为独立子页面接入主流程,但目前还是功能先行版。请按全局统一视觉,把这个页面的左侧说明区、右侧列表区、分类筛选 chips、列表空态和“返回资产中心 / 新建分类 / 新建饰面 / 新建基材”几组操作做成和主线一致的正式界面,不要回退为老式后台页。
|
||||
|
||||
### 待联调确认 (全栈 / 后端 Agent 负责)
|
||||
|
||||
- [ ] **资产中心资产族新流程人工验收**
|
||||
归属:`联调确认`
|
||||
说明:确认资产中心现在已改成“先建资产族,再在资产族内新建规格款 / 发起 CAD 导入;缺资料时跳转基础资料库补录”的结构,用户理解是否自然,弹窗开关、列表刷新、导入完成后的落点是否符合预期。
|
||||
说明:确认资产中心现在已改成"先建资产族,再在资产族内新建规格款 / 发起 CAD 导入;缺资料时跳转基础资料库补录"的结构,用户理解是否自然,弹窗开关、列表刷新、导入完成后的落点是否符合预期。
|
||||
|
||||
- [ ] **资产族品类系列 + 中文连接角色维护人工验收**
|
||||
归属:`联调确认`
|
||||
|
||||
@ -1350,7 +1350,7 @@ export default function AssetCenterClient({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSearchText('')}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 rounded-full p-0.5 text-apple-label-tertiary hover:bg-apple-bg-secondary hover:text-apple-label-primary"
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 rounded-full p-0.5 text-apple-label-secondary hover:bg-apple-bg-secondary hover:text-apple-label-primary"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
@ -1448,7 +1448,7 @@ export default function AssetCenterClient({
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => { e.stopPropagation(); openEditFamilyModal(family) }}
|
||||
className="shrink-0 rounded-full p-1.5 text-apple-label-tertiary hover:bg-apple-bg-secondary hover:text-apple-label-primary transition"
|
||||
className="shrink-0 rounded-full p-1.5 text-apple-label-secondary hover:bg-apple-bg-secondary hover:text-apple-label-primary transition"
|
||||
title="编辑资产族"
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
@ -1499,7 +1499,7 @@ export default function AssetCenterClient({
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => { e.stopPropagation(); openEditVariantModal(variant) }}
|
||||
className="shrink-0 rounded-full p-1 text-apple-label-tertiary hover:bg-apple-bg-primary hover:text-apple-label-primary transition"
|
||||
className="shrink-0 rounded-full p-1 text-apple-label-secondary hover:bg-apple-bg-primary hover:text-apple-label-primary transition"
|
||||
title="编辑规格款"
|
||||
>
|
||||
<Pencil className="h-3 w-3" />
|
||||
@ -1514,24 +1514,16 @@ export default function AssetCenterClient({
|
||||
</div>
|
||||
)}
|
||||
{/* 新建规格款按钮 */}
|
||||
<div className="flex items-center justify-between gap-2 border-t border-apple-separator px-4 py-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openCreateVariantModal(family.id)}
|
||||
className="flex items-center gap-1.5 text-xs text-apple-blue hover:text-apple-blue/80 transition"
|
||||
>
|
||||
<div className="flex items-center gap-2 border-t border-apple-separator px-4 py-2">
|
||||
<Button variant="tinted" size="small" onClick={() => openCreateVariantModal(family.id)}>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
新建规格款
|
||||
</button>
|
||||
</Button>
|
||||
{isImportableAssetType(family.type) ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openCadImportModal(family)}
|
||||
className="flex items-center gap-1.5 text-xs text-apple-label-secondary hover:text-apple-blue transition"
|
||||
>
|
||||
<Button variant="plain" size="small" onClick={() => openCadImportModal(family)}>
|
||||
<FileUp className="h-3.5 w-3.5" />
|
||||
CAD 导入到该资产族
|
||||
</button>
|
||||
CAD 导入
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
@ -1553,19 +1545,14 @@ export default function AssetCenterClient({
|
||||
<div>
|
||||
<p className="text-sm font-apple-medium text-apple-label-primary">基础资料库</p>
|
||||
<p className="text-xs text-apple-label-secondary">
|
||||
统一维护基材、材质分类、饰面花色;补录完成后再回当前资产族继续操作。
|
||||
{substrates.length} 个基材 · {finishes.length} 个饰面花色
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-xs text-apple-label-tertiary">
|
||||
{substrates.length} 个基材 · {finishes.length} 个饰面
|
||||
</p>
|
||||
<div className="mt-1 flex items-center justify-end gap-1 text-sm text-apple-blue">
|
||||
打开资料页
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</div>
|
||||
</div>
|
||||
<span className="flex items-center gap-1 text-xs text-apple-blue">
|
||||
去基础资料库维护
|
||||
<ArrowRight className="h-3.5 w-3.5" />
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{/* 下一步引导 */}
|
||||
@ -1832,6 +1819,7 @@ export default function AssetCenterClient({
|
||||
</div>
|
||||
|
||||
{cadImportForm.assetType === 'BOARD' ? (
|
||||
<>
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<Select
|
||||
label="基材"
|
||||
@ -1857,6 +1845,10 @@ export default function AssetCenterClient({
|
||||
errorText={cadImportSubmitAttempted ? cadImportFieldErrors.finishId : undefined}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-apple-label-tertiary">
|
||||
找不到需要的基材或饰面?<Link href="/asset-center/material-library" className="text-apple-blue hover:underline">去基础资料库补录</Link>后再回来。
|
||||
</p>
|
||||
</>
|
||||
) : cadImportForm.assetType === 'COMPONENT' ? (
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<Select
|
||||
@ -2154,7 +2146,7 @@ export default function AssetCenterClient({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openEditRoleEditor(definition)}
|
||||
className="rounded-full p-1.5 text-apple-label-tertiary hover:bg-apple-bg-secondary hover:text-apple-label-primary transition"
|
||||
className="rounded-full p-1.5 text-apple-label-secondary hover:bg-apple-bg-secondary hover:text-apple-label-primary transition"
|
||||
title={`编辑 ${definition.label}`}
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
@ -2162,7 +2154,7 @@ export default function AssetCenterClient({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleDeleteRoleDefinition(definition.value)}
|
||||
className="rounded-full p-1.5 text-apple-label-tertiary hover:bg-apple-bg-secondary hover:text-apple-red transition"
|
||||
className="rounded-full p-1.5 text-apple-label-secondary hover:bg-apple-bg-secondary hover:text-apple-red transition"
|
||||
title={`删除 ${definition.label}`}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
|
||||
@ -3,6 +3,7 @@ import userEvent from '@testing-library/user-event'
|
||||
import type { ReactNode } from 'react'
|
||||
import AssetCenterClient from './AssetCenterClient'
|
||||
import { createAssetFamily, importAssetCad } from '@/lib/open-product/platform-api'
|
||||
import { parseZipFile } from '@/lib/zip-parser'
|
||||
import type { AssetFamilyDTO, AssetVariantDTO, MaterialDTO, SeriesTemplateDTO, SubstrateDTO } from '@/server/services'
|
||||
|
||||
jest.mock('next/link', () => ({
|
||||
@ -116,6 +117,11 @@ jest.mock('@/lib/open-product/platform-api', () => ({
|
||||
uploadModelFile: jest.fn(),
|
||||
}))
|
||||
|
||||
jest.mock('@/lib/zip-parser', () => ({
|
||||
__esModule: true,
|
||||
parseZipFile: jest.fn(),
|
||||
}))
|
||||
|
||||
function renderClient() {
|
||||
return render(
|
||||
<AssetCenterClient
|
||||
@ -168,6 +174,10 @@ function renderClient() {
|
||||
describe('AssetCenterClient workflow', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
jest.mocked(parseZipFile).mockResolvedValue({
|
||||
success: false,
|
||||
error: 'ZIP 文件解析失败',
|
||||
} as never)
|
||||
global.fetch = jest.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
@ -311,10 +321,10 @@ describe('AssetCenterClient workflow', () => {
|
||||
)
|
||||
|
||||
await actor.click(screen.getByText('桌面板资产族'))
|
||||
await actor.click(screen.getByRole('button', { name: 'CAD 导入到该资产族' }))
|
||||
await actor.click(screen.getByRole('button', { name: 'CAD 导入' }))
|
||||
expect(screen.getByText('导入 CAD 规格款 · 板材')).toBeInTheDocument()
|
||||
expect(screen.getByText('基础资料库')).toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /基础资料库/ })).toHaveAttribute(
|
||||
expect(screen.getByRole('link', { name: '去基础资料库补录' })).toHaveAttribute(
|
||||
'href',
|
||||
'/asset-center/material-library',
|
||||
)
|
||||
@ -462,7 +472,7 @@ describe('AssetCenterClient workflow', () => {
|
||||
)
|
||||
|
||||
await actor.click(screen.getByText('140台面资产族'))
|
||||
await actor.click(screen.getByRole('button', { name: 'CAD 导入到该资产族' }))
|
||||
await actor.click(screen.getByRole('button', { name: 'CAD 导入' }))
|
||||
await actor.type(screen.getByPlaceholderText('BOARD-VARIANT-001'), 'TM-001')
|
||||
await actor.type(screen.getByPlaceholderText('例如 桌面板 1200 / 方管桌腿 / 十字连接件'), '140台面-A')
|
||||
|
||||
@ -473,6 +483,9 @@ describe('AssetCenterClient workflow', () => {
|
||||
|
||||
const fileInput = document.querySelector('input[type="file"][accept=".zip"]') as HTMLInputElement
|
||||
const zipFile = new File(['zip-content'], '140台面.zip', { type: 'application/zip' })
|
||||
Object.defineProperty(zipFile, 'arrayBuffer', {
|
||||
value: async () => new TextEncoder().encode('zip-content').buffer,
|
||||
})
|
||||
await actor.upload(fileInput, zipFile)
|
||||
|
||||
await actor.click(screen.getByRole('button', { name: '开始导入' }))
|
||||
|
||||
@ -0,0 +1,227 @@
|
||||
import { render, screen, waitFor, within } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import type { ReactNode } from 'react'
|
||||
import MaterialLibraryClient from './MaterialLibraryClient'
|
||||
import { createSubstrate, updateMaterialCategory } from '@/lib/open-product/platform-api'
|
||||
import type { MaterialCategoryDTO, MaterialDTO, SubstrateDTO } from '@/server/services'
|
||||
|
||||
jest.mock('next/link', () => ({
|
||||
__esModule: true,
|
||||
default: ({
|
||||
href,
|
||||
children,
|
||||
...props
|
||||
}: {
|
||||
href: string
|
||||
children: ReactNode
|
||||
}) => (
|
||||
<a href={href} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}))
|
||||
|
||||
jest.mock('@/components/ui/Modal', () => ({
|
||||
__esModule: true,
|
||||
default: ({
|
||||
isOpen,
|
||||
title,
|
||||
children,
|
||||
footer,
|
||||
}: {
|
||||
isOpen: boolean
|
||||
title?: ReactNode
|
||||
children: ReactNode
|
||||
footer?: ReactNode
|
||||
}) =>
|
||||
isOpen ? (
|
||||
<div role="dialog" aria-label={typeof title === 'string' ? title : undefined}>
|
||||
<div>{title}</div>
|
||||
<div>{children}</div>
|
||||
<div>{footer}</div>
|
||||
</div>
|
||||
) : null,
|
||||
}))
|
||||
|
||||
jest.mock('@/components/ui/DeleteConfirmDialog', () => ({
|
||||
__esModule: true,
|
||||
DeleteConfirmDialog: () => null,
|
||||
}))
|
||||
|
||||
jest.mock('@/components/ui/Toast', () => ({
|
||||
__esModule: true,
|
||||
useToast: () => ({
|
||||
toasts: [],
|
||||
success: jest.fn(),
|
||||
error: jest.fn(),
|
||||
warning: jest.fn(),
|
||||
info: jest.fn(),
|
||||
removeToast: jest.fn(),
|
||||
}),
|
||||
ToastContainer: () => null,
|
||||
}))
|
||||
|
||||
jest.mock('@/components/platform/PlatformSplitLayout', () => ({
|
||||
__esModule: true,
|
||||
PlatformSplitLayout: ({
|
||||
leftContent,
|
||||
rightContent,
|
||||
}: {
|
||||
leftContent: ReactNode
|
||||
rightContent: ReactNode
|
||||
}) => (
|
||||
<div>
|
||||
<div>{leftContent}</div>
|
||||
<div>{rightContent}</div>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
jest.mock('@/lib/open-product/platform-api', () => ({
|
||||
__esModule: true,
|
||||
createMaterial: jest.fn(),
|
||||
createMaterialCategory: jest.fn(),
|
||||
createSubstrate: jest.fn(),
|
||||
deleteMaterial: jest.fn(),
|
||||
deleteMaterialCategory: jest.fn(),
|
||||
deleteSubstrate: jest.fn(),
|
||||
updateMaterial: jest.fn(),
|
||||
updateMaterialCategory: jest.fn(),
|
||||
updateSubstrate: jest.fn(),
|
||||
}))
|
||||
|
||||
const mockedCreateSubstrate = jest.mocked(createSubstrate)
|
||||
const mockedUpdateMaterialCategory = jest.mocked(updateMaterialCategory)
|
||||
|
||||
function renderClient() {
|
||||
return render(
|
||||
<MaterialLibraryClient
|
||||
initialSubstrates={[{ id: 'substrate-1', code: 'PB18', name: '18mm 颗粒板' }] as SubstrateDTO[]}
|
||||
initialFinishes={[
|
||||
{
|
||||
id: 'finish-1',
|
||||
name: '白橡木纹',
|
||||
categoryId: 'cat-1',
|
||||
categoryName: '木纹',
|
||||
thumbnailUrl: null,
|
||||
textureUrl: null,
|
||||
color: '#CCCCCC',
|
||||
metalness: 0,
|
||||
roughness: 0.5,
|
||||
opacity: 1,
|
||||
textureRepeat: 1,
|
||||
textureRotation: 0,
|
||||
textureSizeMm: 600,
|
||||
isPreset: false,
|
||||
isActive: true,
|
||||
createdAt: new Date('2026-04-08T00:00:00.000Z'),
|
||||
updatedAt: new Date('2026-04-08T00:00:00.000Z'),
|
||||
},
|
||||
] as unknown as MaterialDTO[]}
|
||||
initialMaterialCategories={[
|
||||
{ id: 'cat-1', name: '木纹', icon: 'Texture', sortOrder: 1, materialCount: 1 },
|
||||
] as MaterialCategoryDTO[]}
|
||||
metrics={[]}
|
||||
/>,
|
||||
)
|
||||
}
|
||||
|
||||
describe('MaterialLibraryClient', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('新建基材后使用后端返回的完整字段刷新列表', async () => {
|
||||
const actor = userEvent.setup()
|
||||
mockedCreateSubstrate.mockResolvedValue({
|
||||
substrate: {
|
||||
id: 'substrate-2',
|
||||
code: 'PB25',
|
||||
name: '25mm 颗粒板',
|
||||
description: '后端标准说明',
|
||||
densityKgM3: 720,
|
||||
metadata: { source: 'server' },
|
||||
isActive: true,
|
||||
createdAt: new Date('2026-04-08T10:00:00.000Z'),
|
||||
updatedAt: new Date('2026-04-08T10:00:00.000Z'),
|
||||
},
|
||||
})
|
||||
|
||||
renderClient()
|
||||
|
||||
await actor.click(screen.getByRole('button', { name: '新建基材' }))
|
||||
|
||||
const dialog = screen.getByRole('dialog', { name: '新建基材' })
|
||||
const inputs = within(dialog).getAllByRole('textbox')
|
||||
await actor.type(inputs[0], 'PB25')
|
||||
await actor.type(inputs[1], '25mm 颗粒板')
|
||||
await actor.type(inputs[2], '前端输入的旧说明')
|
||||
await actor.type(within(dialog).getByPlaceholderText('例如 680'), '680')
|
||||
await actor.click(within(dialog).getByRole('button', { name: '创建基材' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedCreateSubstrate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
code: 'PB25',
|
||||
name: '25mm 颗粒板',
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('25mm 颗粒板')).toBeInTheDocument()
|
||||
expect(screen.getByText('PB25')).toBeInTheDocument()
|
||||
expect(screen.getByText('720 kg/m³')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('编辑材质分类后,饰面搜索会立即使用新分类名', async () => {
|
||||
const actor = userEvent.setup()
|
||||
mockedUpdateMaterialCategory.mockResolvedValue({
|
||||
category: {
|
||||
id: 'cat-1',
|
||||
name: '深木纹',
|
||||
icon: 'Texture',
|
||||
sortOrder: 1,
|
||||
materialCount: 1,
|
||||
},
|
||||
})
|
||||
|
||||
renderClient()
|
||||
|
||||
await actor.click(screen.getByRole('button', { name: '饰面花色' }))
|
||||
|
||||
const categoryTitle = screen.getAllByText('木纹').find((element) => element.tagName === 'P')
|
||||
if (!categoryTitle) {
|
||||
throw new Error('未找到分类标题')
|
||||
}
|
||||
|
||||
const categoryBlock = categoryTitle.closest('div')?.parentElement
|
||||
if (!categoryBlock) {
|
||||
throw new Error('未找到分类区块')
|
||||
}
|
||||
|
||||
const categoryButtons = within(categoryBlock).getAllByRole('button')
|
||||
await actor.click(categoryButtons[0])
|
||||
|
||||
const dialog = screen.getByRole('dialog', { name: '编辑材质分类' })
|
||||
const categoryNameInput = within(dialog).getByPlaceholderText('例如 木纹、石纹、纯色')
|
||||
await actor.clear(categoryNameInput)
|
||||
await actor.type(categoryNameInput, '深木纹')
|
||||
await actor.click(within(dialog).getByRole('button', { name: '保存修改' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedUpdateMaterialCategory).toHaveBeenCalledWith(
|
||||
'cat-1',
|
||||
expect.objectContaining({ name: '深木纹' }),
|
||||
)
|
||||
})
|
||||
|
||||
const searchInput = screen.getByPlaceholderText('搜索分类或饰面名称...')
|
||||
await actor.clear(searchInput)
|
||||
await actor.type(searchInput, '深木纹')
|
||||
|
||||
expect(screen.getByText('白橡木纹')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -4,9 +4,7 @@ import Link from 'next/link'
|
||||
import { startTransition, useMemo, useState } from 'react'
|
||||
import {
|
||||
ArrowLeft,
|
||||
Layers3,
|
||||
Package2,
|
||||
Palette,
|
||||
Pencil,
|
||||
Plus,
|
||||
Search,
|
||||
@ -14,7 +12,6 @@ import {
|
||||
Trash2,
|
||||
X,
|
||||
} from 'lucide-react'
|
||||
import { Badge } from '@/components/ui/Badge'
|
||||
import Button from '@/components/ui/Button'
|
||||
import { DeleteConfirmDialog } from '@/components/ui/DeleteConfirmDialog'
|
||||
import Input from '@/components/ui/Input'
|
||||
@ -272,22 +269,7 @@ export default function MaterialLibraryClient({
|
||||
})
|
||||
|
||||
startTransition(() => {
|
||||
setSubstrates((current) =>
|
||||
sortByName([
|
||||
...current,
|
||||
{
|
||||
id: result.substrate.id,
|
||||
code: result.substrate.code,
|
||||
name: result.substrate.name,
|
||||
description: substrateForm.description.trim() || null,
|
||||
densityKgM3: toOptionalNumber(substrateForm.densityKgM3),
|
||||
metadata: null,
|
||||
isActive: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
]),
|
||||
)
|
||||
setSubstrates((current) => sortByName([...current, result.substrate]))
|
||||
setSubstrateModalOpen(false)
|
||||
toast.success('基材已创建')
|
||||
})
|
||||
@ -353,6 +335,18 @@ export default function MaterialLibraryClient({
|
||||
current.map((item) => (item.id === editingCategoryId ? result.category : item)),
|
||||
),
|
||||
)
|
||||
setFinishes((current) =>
|
||||
sortByName(
|
||||
current.map((item) =>
|
||||
item.categoryId === editingCategoryId
|
||||
? {
|
||||
...item,
|
||||
categoryName: result.category.name,
|
||||
}
|
||||
: item,
|
||||
),
|
||||
),
|
||||
)
|
||||
setCategoryModalOpen(false)
|
||||
toast.success('材质分类已更新')
|
||||
})
|
||||
@ -431,6 +425,18 @@ export default function MaterialLibraryClient({
|
||||
await deleteMaterialCategory(deleteTarget.id)
|
||||
startTransition(() => {
|
||||
setMaterialCategories((current) => current.filter((item) => item.id !== deleteTarget.id))
|
||||
setFinishes((current) =>
|
||||
sortByName(
|
||||
current.map((item) =>
|
||||
item.categoryId === deleteTarget.id
|
||||
? {
|
||||
...item,
|
||||
categoryName: undefined,
|
||||
}
|
||||
: item,
|
||||
),
|
||||
),
|
||||
)
|
||||
if (selectedCategoryId === deleteTarget.id) {
|
||||
setSelectedCategoryId('all')
|
||||
}
|
||||
@ -461,17 +467,14 @@ export default function MaterialLibraryClient({
|
||||
metrics={metrics}
|
||||
rightPanelClassName="w-[580px] max-w-[52vw]"
|
||||
leftContent={
|
||||
<div className="flex h-full flex-col gap-4 rounded-apple-lg bg-[var(--color-surface-container-low)] p-5">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex h-full flex-col gap-5 rounded-apple-lg bg-apple-bg-secondary p-5">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-[0.24em] text-apple-label-tertiary">
|
||||
Library Scope
|
||||
</p>
|
||||
<h2 className="mt-2 text-2xl font-apple-semibold text-apple-label-primary">
|
||||
基础资料统一维护
|
||||
<h2 className="text-xl font-apple-semibold text-apple-label-primary">
|
||||
基础资料库
|
||||
</h2>
|
||||
<p className="mt-2 max-w-2xl text-sm text-apple-label-secondary">
|
||||
资产中心主页面只保留“资产族 + 规格款”主流程。缺基材或饰面时,先来这里补录,保存后再回 CAD 导入或规格款弹窗继续操作。
|
||||
<p className="mt-1.5 text-sm text-apple-label-secondary">
|
||||
维护基材、分类和饰面花色,补录后返回资产中心继续操作。
|
||||
</p>
|
||||
</div>
|
||||
<Link href="/asset-center">
|
||||
@ -482,62 +485,17 @@ export default function MaterialLibraryClient({
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<div className="rounded-apple-lg border border-apple-separator bg-apple-bg-primary p-4">
|
||||
<div className="flex items-center gap-2 text-apple-label-primary">
|
||||
<Package2 className="h-4 w-4 text-apple-label-tertiary" />
|
||||
<span className="text-sm font-apple-medium">基材</span>
|
||||
</div>
|
||||
<p className="mt-3 text-3xl font-apple-semibold text-apple-label-primary">
|
||||
{substrates.length}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-apple-label-secondary">
|
||||
服务板材规格款、CAD 导入和 3D 右键快捷更改。
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-apple-lg border border-apple-separator bg-apple-bg-primary p-4">
|
||||
<div className="flex items-center gap-2 text-apple-label-primary">
|
||||
<Layers3 className="h-4 w-4 text-apple-label-tertiary" />
|
||||
<span className="text-sm font-apple-medium">材质分类</span>
|
||||
</div>
|
||||
<p className="mt-3 text-3xl font-apple-semibold text-apple-label-primary">
|
||||
{materialCategories.length}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-apple-label-secondary">
|
||||
继续保持“先分类,再具体饰面”的结构,不再写第二套口径。
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-apple-lg border border-apple-separator bg-apple-bg-primary p-4">
|
||||
<div className="flex items-center gap-2 text-apple-label-primary">
|
||||
<Palette className="h-4 w-4 text-apple-label-tertiary" />
|
||||
<span className="text-sm font-apple-medium">饰面 / 花色</span>
|
||||
</div>
|
||||
<p className="mt-3 text-3xl font-apple-semibold text-apple-label-primary">
|
||||
{finishes.length}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-apple-label-secondary">
|
||||
删除后会自动让已引用规格款回落为空,不额外制造异常状态。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-apple-label-secondary">
|
||||
<span className="font-apple-medium text-apple-label-primary">{substrates.length}</span> 基材
|
||||
<span className="mx-1.5 text-apple-separator">·</span>
|
||||
<span className="font-apple-medium text-apple-label-primary">{materialCategories.length}</span> 分类
|
||||
<span className="mx-1.5 text-apple-separator">·</span>
|
||||
<span className="font-apple-medium text-apple-label-primary">{finishes.length}</span> 饰面
|
||||
</p>
|
||||
|
||||
<div className="rounded-apple-lg border border-apple-separator bg-apple-bg-primary p-5">
|
||||
<h3 className="text-base font-apple-semibold text-apple-label-primary">使用口径</h3>
|
||||
<div className="mt-4 grid gap-3 md:grid-cols-2">
|
||||
<div className="rounded-apple-md bg-apple-bg-secondary/60 p-4">
|
||||
<p className="text-sm font-apple-medium text-apple-label-primary">什么时候来这里</p>
|
||||
<p className="mt-2 text-sm text-apple-label-secondary">
|
||||
需要新增或调整基材、材质分类、饰面花色时,统一来这个页面处理。
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-apple-md bg-apple-bg-secondary/60 p-4">
|
||||
<p className="text-sm font-apple-medium text-apple-label-primary">什么时候回资产中心</p>
|
||||
<p className="mt-2 text-sm text-apple-label-secondary">
|
||||
资料补录完成后,回资产中心继续 CAD 导入、规格款新建编辑和 3D 右键快捷更改。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-apple-label-tertiary leading-relaxed">
|
||||
缺基材或饰面时先来这里补录,保存后回 CAD 导入或规格款弹窗继续操作。
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
rightContent={
|
||||
@ -585,7 +543,7 @@ export default function MaterialLibraryClient({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSearchText('')}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 rounded-full p-0.5 text-apple-label-tertiary hover:bg-apple-bg-secondary hover:text-apple-label-primary"
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 rounded-full p-0.5 text-apple-label-secondary hover:bg-apple-bg-secondary hover:text-apple-label-primary"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
@ -611,188 +569,150 @@ export default function MaterialLibraryClient({
|
||||
</div>
|
||||
|
||||
{activeTab === 'substrate' ? (
|
||||
<div className="rounded-apple-lg border border-apple-separator bg-apple-bg-primary">
|
||||
<div className="border-b border-apple-separator px-4 py-3">
|
||||
<p className="text-sm font-apple-medium text-apple-label-primary">基材列表</p>
|
||||
<p className="text-xs text-apple-label-secondary">
|
||||
当前共 {filteredSubstrates.length} 条,删除后规格款会自动回落为未设置。
|
||||
</p>
|
||||
filteredSubstrates.length === 0 ? (
|
||||
<div className="py-12 text-center">
|
||||
<Package2 className="mx-auto h-8 w-8 text-apple-label-tertiary" />
|
||||
<p className="mt-3 text-sm text-apple-label-secondary">暂无基材</p>
|
||||
<p className="mt-1 text-xs text-apple-label-tertiary">点击上方「新建基材」添加第一条记录</p>
|
||||
</div>
|
||||
<div className="divide-y divide-apple-separator">
|
||||
{filteredSubstrates.length === 0 ? (
|
||||
<div className="px-4 py-8 text-center text-sm text-apple-label-tertiary">
|
||||
暂无匹配的基材
|
||||
</div>
|
||||
) : (
|
||||
filteredSubstrates.map((substrate) => (
|
||||
<div key={substrate.id} className="flex items-center gap-3 px-4 py-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="truncate text-sm font-apple-medium text-apple-label-primary">
|
||||
{substrate.name}
|
||||
</p>
|
||||
<Badge type="info" variant="subtle" size="small">
|
||||
{substrate.code}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-apple-label-secondary">
|
||||
{substrate.description?.trim() || '未填写说明'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right text-xs text-apple-label-tertiary">
|
||||
{substrate.densityKgM3 ? `${substrate.densityKgM3} kg/m³` : '未填密度'}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openEditSubstrateModal(substrate)}
|
||||
className="rounded-full p-1.5 text-apple-label-tertiary hover:bg-apple-bg-secondary hover:text-apple-label-primary transition"
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDeleteTarget({ type: 'substrate', id: substrate.id, name: substrate.name })}
|
||||
className="rounded-full p-1.5 text-apple-label-tertiary hover:bg-apple-bg-secondary hover:text-apple-red transition"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
) : (
|
||||
<div className="divide-y divide-apple-separator rounded-apple-lg border border-apple-separator bg-apple-bg-primary">
|
||||
{filteredSubstrates.map((substrate) => (
|
||||
<div key={substrate.id} className="flex items-center gap-3 px-4 py-3">
|
||||
<p className="min-w-0 truncate text-sm font-apple-medium text-apple-label-primary">
|
||||
{substrate.name}
|
||||
</p>
|
||||
<div className="ml-auto flex items-center gap-3 text-xs text-apple-label-tertiary">
|
||||
<span>{substrate.code}</span>
|
||||
{substrate.densityKgM3 ? <span>{substrate.densityKgM3} kg/m³</span> : null}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openEditSubstrateModal(substrate)}
|
||||
className="rounded-full p-1.5 text-apple-label-secondary hover:bg-apple-bg-secondary hover:text-apple-label-primary transition"
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDeleteTarget({ type: 'substrate', id: substrate.id, name: substrate.name })}
|
||||
className="rounded-full p-1.5 text-apple-label-secondary hover:bg-apple-bg-secondary hover:text-apple-red transition"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<div className="rounded-apple-lg border border-apple-separator bg-apple-bg-primary p-4">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div>
|
||||
<p className="text-sm font-apple-medium text-apple-label-primary">材质分类筛选</p>
|
||||
<p className="text-xs text-apple-label-secondary">
|
||||
先选分类再维护具体饰面;也可以查看全部。
|
||||
</p>
|
||||
</div>
|
||||
<Select
|
||||
size="small"
|
||||
value={selectedCategoryId}
|
||||
onChange={(event) => setSelectedCategoryId(event.target.value)}
|
||||
options={[
|
||||
{ value: 'all', label: '全部分类' },
|
||||
...materialCategories.map((category) => ({
|
||||
value: category.id,
|
||||
label: `${category.name} (${category.materialCount ?? 0})`,
|
||||
})),
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{materialCategories.map((category) => (
|
||||
<button
|
||||
key={category.id}
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setSelectedCategoryId((current) => (current === category.id ? 'all' : category.id))
|
||||
}
|
||||
className={cn(
|
||||
'rounded-full border px-3 py-1.5 text-xs transition',
|
||||
selectedCategoryId === category.id
|
||||
? 'border-apple-blue/30 bg-apple-blue/8 text-apple-blue'
|
||||
: 'border-apple-separator bg-apple-bg-secondary text-apple-label-secondary hover:text-apple-label-primary',
|
||||
)}
|
||||
>
|
||||
{category.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSelectedCategoryId('all')}
|
||||
className={cn(
|
||||
'rounded-full border px-3 py-1.5 text-xs transition',
|
||||
selectedCategoryId === 'all'
|
||||
? 'border-apple-blue/30 bg-apple-blue/10 text-apple-blue'
|
||||
: 'border-apple-separator bg-transparent text-apple-label-secondary hover:text-apple-label-primary',
|
||||
)}
|
||||
>
|
||||
全部
|
||||
</button>
|
||||
{materialCategories.map((category) => (
|
||||
<button
|
||||
key={category.id}
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setSelectedCategoryId((current) => (current === category.id ? 'all' : category.id))
|
||||
}
|
||||
className={cn(
|
||||
'rounded-full border px-3 py-1.5 text-xs transition',
|
||||
selectedCategoryId === category.id
|
||||
? 'border-apple-blue/30 bg-apple-blue/10 text-apple-blue'
|
||||
: 'border-apple-separator bg-transparent text-apple-label-secondary hover:text-apple-label-primary',
|
||||
)}
|
||||
>
|
||||
{category.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="rounded-apple-lg border border-apple-separator bg-apple-bg-primary">
|
||||
<div className="border-b border-apple-separator px-4 py-3">
|
||||
<p className="text-sm font-apple-medium text-apple-label-primary">饰面 / 花色资料</p>
|
||||
<p className="text-xs text-apple-label-secondary">
|
||||
分类与饰面在同一页面维护,当前共 {filteredFinishes.length} 条结果。
|
||||
</p>
|
||||
{groupedFinishes.length === 0 ? (
|
||||
<div className="py-12 text-center">
|
||||
<SwatchBook className="mx-auto h-8 w-8 text-apple-label-tertiary" />
|
||||
<p className="mt-3 text-sm text-apple-label-secondary">暂无饰面花色</p>
|
||||
<p className="mt-1 text-xs text-apple-label-tertiary">点击上方「新建饰面」添加第一条记录</p>
|
||||
</div>
|
||||
<div className="space-y-4 px-4 py-4">
|
||||
{groupedFinishes.length === 0 ? (
|
||||
<div className="rounded-apple-md bg-apple-bg-secondary/60 px-4 py-8 text-center text-sm text-apple-label-tertiary">
|
||||
当前筛选下暂无饰面 / 花色
|
||||
</div>
|
||||
) : (
|
||||
groupedFinishes.map((group, groupIndex) => (
|
||||
<div
|
||||
key={group.category.id}
|
||||
className={cn(groupIndex > 0 && 'border-t border-apple-separator pt-4')}
|
||||
>
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="truncate text-sm font-apple-medium text-apple-label-primary">
|
||||
{group.category.name}
|
||||
</p>
|
||||
<span className="text-[11px] text-apple-label-tertiary">
|
||||
{group.finishes.length} 种
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{group.category.id !== 'uncategorized' ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openEditCategoryModal(group.category)}
|
||||
className="rounded-full p-1.5 text-apple-label-tertiary hover:bg-apple-bg-secondary hover:text-apple-label-primary transition"
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDeleteTarget({ type: 'category', id: group.category.id, name: group.category.name })}
|
||||
className="rounded-full p-1.5 text-apple-label-tertiary hover:bg-apple-bg-secondary hover:text-apple-red transition"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{groupedFinishes.map((group, groupIndex) => (
|
||||
<div
|
||||
key={group.category.id}
|
||||
className={cn(groupIndex > 0 && 'border-t border-apple-separator pt-4')}
|
||||
>
|
||||
<div className="mb-2 flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-sm font-apple-medium text-apple-label-primary">
|
||||
{group.category.name}
|
||||
</p>
|
||||
<span className="text-[11px] text-apple-label-tertiary">
|
||||
{group.finishes.length}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
{group.finishes.map((finish) => (
|
||||
<div
|
||||
key={finish.id}
|
||||
className="flex items-center gap-3 rounded-apple-md bg-apple-bg-secondary/40 px-3 py-2"
|
||||
{group.category.id !== 'uncategorized' ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openEditCategoryModal(group.category)}
|
||||
className="rounded-full p-1.5 text-apple-label-secondary hover:bg-apple-bg-secondary hover:text-apple-label-primary transition"
|
||||
>
|
||||
<div
|
||||
className="h-8 w-8 shrink-0 rounded-apple-md border border-apple-separator"
|
||||
style={{ backgroundColor: finish.color }}
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-apple-medium text-apple-label-primary">
|
||||
{finish.name}
|
||||
</p>
|
||||
<p className="text-[11px] text-apple-label-secondary">
|
||||
{finish.categoryName ?? group.category.name}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openEditFinishModal(finish)}
|
||||
className="rounded-full p-1.5 text-apple-label-tertiary hover:bg-apple-bg-primary hover:text-apple-label-primary transition"
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDeleteTarget({ type: 'finish', id: finish.id, name: finish.name })}
|
||||
className="rounded-full p-1.5 text-apple-label-tertiary hover:bg-apple-bg-primary hover:text-apple-red transition"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDeleteTarget({ type: 'category', id: group.category.id, name: group.category.name })}
|
||||
className="rounded-full p-1.5 text-apple-label-secondary hover:bg-apple-bg-secondary hover:text-apple-red transition"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
<div className="grid gap-1.5">
|
||||
{group.finishes.map((finish) => (
|
||||
<div
|
||||
key={finish.id}
|
||||
className="flex items-center gap-3 rounded-apple-md px-3 py-2"
|
||||
>
|
||||
<div
|
||||
className="h-6 w-6 shrink-0 rounded border border-apple-separator"
|
||||
style={{ backgroundColor: finish.color }}
|
||||
/>
|
||||
<p className="min-w-0 flex-1 truncate text-sm text-apple-label-primary">
|
||||
{finish.name}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openEditFinishModal(finish)}
|
||||
className="rounded-full p-1.5 text-apple-label-secondary hover:bg-apple-bg-secondary hover:text-apple-label-primary transition"
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDeleteTarget({ type: 'finish', id: finish.id, name: finish.name })}
|
||||
className="rounded-full p-1.5 text-apple-label-secondary hover:bg-apple-bg-secondary hover:text-apple-red transition"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
@ -170,7 +170,7 @@ export async function importAssetCad(input: ImportAssetCadPayload): Promise<Asse
|
||||
}
|
||||
|
||||
export async function createSubstrate(input: CreateSubstrateInput): Promise<{
|
||||
substrate: { id: string; code: string; name: string }
|
||||
substrate: SubstrateDTO
|
||||
}> {
|
||||
return requestPlatform('/api/asset-center/substrates', {
|
||||
method: 'POST',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user