feat(supply-chain): 限价筛选条件支持自定义

This commit is contained in:
居居 2026-10-20 14:37:41 +08:00 committed by WuYongJie
parent 7ce4b93706
commit 89eb66c6f6
6 changed files with 150 additions and 55 deletions

View File

@ -73,6 +73,7 @@ export function ComboLimitSection({
loading,
error,
filters,
topFilterFieldKeys,
sort,
onPageChange,
onPageSizeChange,
@ -88,6 +89,7 @@ export function ComboLimitSection({
loading: boolean
error: string
filters: ComboLimitFilters
topFilterFieldKeys: string[]
sort: ComboLimitSort
onPageChange: (page: number) => void
onPageSizeChange: (pageSize: 50 | 100) => void
@ -121,6 +123,10 @@ export function ComboLimitSection({
() => filters as Record<string, string[]>,
[filters],
)
const topFilterFields = useMemo(
() => COMBO_FILTER_FIELDS.filter((field) => topFilterFieldKeys.includes(field.key)),
[topFilterFieldKeys],
)
const activeFilterChips = useMemo(() => COMBO_LIMIT_FIELDS.flatMap((field) => {
const values = filters[field.key] ?? []
@ -194,7 +200,10 @@ export function ComboLimitSection({
<>
<div className={styles.filterBar}>
<span className={styles.filterHint}>筛选</span>
{COMBO_FILTER_FIELDS.map((field) => (
{topFilterFields.length === 0 && (
<span className={styles.filterHint}>未配置筛选项,可在右上角「显示设置 → 筛选设置」中开启</span>
)}
{topFilterFields.map((field) => (
<LimitFilterSelect
key={field.key}
field={field}

View File

@ -453,15 +453,46 @@ describe('限价管理', () => {
fireEvent.click(screen.getByRole('button', { name: '显示设置' }))
fireEvent.click(screen.getByRole('menuitem', { name: '筛选设置' }))
fireEvent.click(screen.getByRole('checkbox', { name: '品牌' }))
fireEvent.click(screen.getByRole('checkbox', { name: '商品名称' }))
fireEvent.click(screen.getByRole('button', { name: '完成' }))
await waitFor(() => {
expect(filterTrigger('brand')).toBeNull()
expect(filterTrigger('productName')).not.toBeNull()
})
view.unmount()
render(<CustomTableView canEdit userId="user-b" />)
expect(filterTrigger('brand')).not.toBeNull()
expect(filterTrigger('productName')).toBeNull()
})
it('组合限价页支持独立的筛选设置并复用显示设置入口', async () => {
const view = render(<CustomTableView canEdit userId="user-a" />)
await screen.findByText('测试岩板餐桌')
fireEvent.click(screen.getByRole('button', { name: '组合限价' }))
await screen.findByText('COMBO-AB')
fireEvent.click(screen.getByRole('button', { name: '显示设置' }))
expect(screen.queryByRole('menuitem', { name: '列设置' })).not.toBeInTheDocument()
expect(screen.queryByRole('menuitemcheckbox', { name: /紧凑展示/ })).not.toBeInTheDocument()
fireEvent.click(screen.getByRole('menuitem', { name: '筛选设置' }))
expect(screen.getByRole('checkbox', { name: '组合编码' })).toBeChecked()
expect(screen.getByRole('checkbox', { name: '运营负责人' })).toBeChecked()
fireEvent.click(screen.getByRole('checkbox', { name: '运营负责人' }))
fireEvent.click(screen.getByRole('button', { name: '完成' }))
await waitFor(() => {
const keys = Array.from(document.querySelectorAll('[data-limit-filter]')).map((element) => element.getAttribute('data-limit-filter'))
expect(keys).toEqual(['comboCode'])
})
view.unmount()
render(<CustomTableView canEdit userId="user-b" />)
await screen.findByText('测试岩板餐桌')
fireEvent.click(screen.getByRole('button', { name: '组合限价' }))
await screen.findByText('COMBO-AB')
expect(document.querySelectorAll('[data-limit-filter]')).toHaveLength(2)
})
it('破价订单审核:点击订单在右侧查看破价数据与审核要点', async () => {

View File

@ -1,11 +1,12 @@
'use client'
import { RotateCcw, X } from 'lucide-react'
import { LIMIT_TABLE_FILTER_FIELDS } from './useLimitTableSettings'
import type { LimitTableFilterField } from './useLimitTableSettings'
import styles from './limit-price.module.css'
type Props = {
open: boolean
fields: LimitTableFilterField[]
filterFields: string[]
onToggleFilterField: (key: string) => void
onReset: () => void
@ -14,6 +15,7 @@ type Props = {
export function LimitFilterSettingsDrawer({
open,
fields,
filterFields,
onToggleFilterField,
onReset,
@ -34,7 +36,7 @@ export function LimitFilterSettingsDrawer({
<div className={styles.drawerBody}>
<p className={styles.drawerHint}>未勾选的筛选项不会显示,你已选择的条件也不会影响其他人。</p>
{LIMIT_TABLE_FILTER_FIELDS.map((field) => (
{fields.map((field) => (
<label key={field.key} className={styles.filterFieldRow}>
<input
type="checkbox"

View File

@ -22,6 +22,7 @@ import { LimitTableHeaderCell, type LimitTableSort } from './LimitTableHeaderCel
import { LimitTableSettingsMenu } from './LimitTableSettingsMenu'
import { requestDownloadCenterRefresh } from '@/components/layout/download-center-events'
import {
COMBO_LIMIT_FILTER_FIELDS,
LIMIT_TABLE_FILTER_FIELDS,
LIMIT_TABLE_FILTER_FIELD_MAP,
useLimitTableSettings,
@ -154,6 +155,8 @@ export function LimitPriceTab({ userId, canExport = false }: { userId: string; c
resetColumns,
toggleFilterField,
resetFilterFields,
toggleComboFilterField,
resetComboFilterFields,
setCompact,
setPageSize,
setColumnWidths,
@ -540,20 +543,20 @@ export function LimitPriceTab({ userId, canExport = false }: { userId: string; c
)}
{exportMessage ? <span className={styles.exportHint}>{exportMessage}</span> : null}
<span className={styles.toolbarSpacer} />
{productKind !== 'combo' && (
<LimitTableSettingsMenu
compact={settings.compact}
onCompactChange={setCompact}
columns={settings.columns}
filterFields={settings.filterFields}
onToggleColumn={toggleColumn}
onSetColumnsVisible={setColumnVisible}
onMoveColumn={moveColumn}
onResetColumns={resetColumns}
onToggleFilterField={toggleFilterField}
onResetFilterFields={resetFilterFields}
/>
)}
<LimitTableSettingsMenu
compact={settings.compact}
onCompactChange={setCompact}
columns={settings.columns}
filterFields={productKind === 'combo' ? settings.comboFilterFields : settings.filterFields}
filterFieldDefinitions={productKind === 'combo' ? COMBO_LIMIT_FILTER_FIELDS : LIMIT_TABLE_FILTER_FIELDS}
showProductSettings={productKind !== 'combo'}
onToggleColumn={toggleColumn}
onSetColumnsVisible={setColumnVisible}
onMoveColumn={moveColumn}
onResetColumns={resetColumns}
onToggleFilterField={productKind === 'combo' ? toggleComboFilterField : toggleFilterField}
onResetFilterFields={productKind === 'combo' ? resetComboFilterFields : resetFilterFields}
/>
</div>
{productKind === 'combo' ? (
@ -567,6 +570,7 @@ export function LimitPriceTab({ userId, canExport = false }: { userId: string; c
loading={comboData.loading}
error={comboData.error}
filters={comboFilters}
topFilterFieldKeys={settings.comboFilterFields}
sort={comboSort}
onPageChange={setPage}
onPageSizeChange={(size) => {

View File

@ -4,7 +4,7 @@ import { useEffect, useRef, useState } from 'react'
import { ListFilter, Rows3, SlidersHorizontal } from 'lucide-react'
import { LimitColumnSettingsDrawer } from './LimitColumnSettingsDrawer'
import { LimitFilterSettingsDrawer } from './LimitFilterSettingsDrawer'
import type { LimitTableColumnSetting } from './useLimitTableSettings'
import type { LimitTableColumnSetting, LimitTableFilterField } from './useLimitTableSettings'
import styles from './limit-price.module.css'
type Props = {
@ -12,6 +12,8 @@ type Props = {
onCompactChange: (compact: boolean) => void
columns: LimitTableColumnSetting[]
filterFields: string[]
filterFieldDefinitions: LimitTableFilterField[]
showProductSettings?: boolean
onToggleColumn: (key: string) => void
onSetColumnsVisible: (keys: string[], visible: boolean) => void
onMoveColumn: (fromKey: string, toKey: string) => void
@ -25,6 +27,8 @@ export function LimitTableSettingsMenu({
onCompactChange,
columns,
filterFields,
filterFieldDefinitions,
showProductSettings = true,
onToggleColumn,
onSetColumnsVisible,
onMoveColumn,
@ -61,29 +65,33 @@ export function LimitTableSettingsMenu({
{menuOpen && (
<div className={styles.settingsMenu} role="menu" aria-label="显示设置菜单">
<button
type="button"
role="menuitemcheckbox"
aria-checked={compact}
className={styles.settingsMenuRow}
onClick={() => onCompactChange(!compact)}
>
<Rows3 size={15} />
紧凑展示
<span className={`${styles.switch} ${compact ? styles.switchOn : ''}`} aria-hidden="true">
<span className={styles.switchKnob} />
</span>
</button>
<div className={styles.settingsMenuDivider} />
<button
type="button"
role="menuitem"
className={styles.settingsMenuRow}
onClick={() => { setMenuOpen(false); setColumnDrawerOpen(true) }}
>
<ListFilter size={15} />
列设置
</button>
{showProductSettings && (
<>
<button
type="button"
role="menuitemcheckbox"
aria-checked={compact}
className={styles.settingsMenuRow}
onClick={() => onCompactChange(!compact)}
>
<Rows3 size={15} />
紧凑展示
<span className={`${styles.switch} ${compact ? styles.switchOn : ''}`} aria-hidden="true">
<span className={styles.switchKnob} />
</span>
</button>
<div className={styles.settingsMenuDivider} />
<button
type="button"
role="menuitem"
className={styles.settingsMenuRow}
onClick={() => { setMenuOpen(false); setColumnDrawerOpen(true) }}
>
<ListFilter size={15} />
列设置
</button>
</>
)}
<button
type="button"
role="menuitem"
@ -107,6 +115,7 @@ export function LimitTableSettingsMenu({
/>
<LimitFilterSettingsDrawer
open={filterDrawerOpen}
fields={filterFieldDefinitions}
filterFields={filterFields}
onToggleFilterField={onToggleFilterField}
onReset={onResetFilterFields}

View File

@ -4,7 +4,9 @@ import { useCallback, useEffect, useState } from 'react'
import {
defaultLimitTableColumnKeys,
LIMIT_TABLE_COLUMN_MAP,
LIMIT_TABLE_COLUMNS,
} from './limit-table-columns'
import { COMBO_LIMIT_FIELDS } from '@/lib/supply-chain-combo-limit-price'
// 限价界面个人设置:列显示/顺序、筛选条件、紧凑展示、每页条数。
// 按用户 ID 分开存储,同一台电脑上多人使用互不影响。
@ -19,6 +21,7 @@ export type LimitTablePageSize = 50 | 100 | 200
export type LimitTableSettings = {
columns: LimitTableColumnSetting[]
filterFields: string[]
comboFilterFields: string[]
compact: boolean
pageSize: LimitTablePageSize
/** 人工调整的列宽(列 key → 像素宽);未设置的列用默认宽度 */
@ -33,24 +36,29 @@ export type LimitTableFilterField = {
optionsEndpoint?: string
}
export const LIMIT_TABLE_FILTER_FIELDS: LimitTableFilterField[] = [
{ key: 'productStatus', label: '商品状态', fixedOptions: ['销售', '停产', '退市'] },
{ key: 'brand', label: '品牌' },
{ key: 'categoryL2', label: '二级类目' },
{ key: 'opsHead', label: '运营负责人' },
{ key: 'supplier', label: '供应商' },
{ key: 'purchaser', label: '采购' },
{ key: 'opsDeptHead', label: '运营部门负责人' },
{ key: 'opsDirector', label: '运营总负责人' },
{ key: 'designer', label: '设计师' },
{ key: 'material', label: '材质' },
{ key: 'inOutWarehouse', label: '出入库仓库(采购类型)' },
{ key: 'storageType', label: '入库/代发' },
]
const FIXED_FRONT_FILTER_KEYS = new Set(['spu', 'sku', 'barcode'])
const PRODUCT_STATUS_OPTIONS = ['销售', '停产', '退市']
/** 商品页可配置筛选条件:覆盖所有支持筛选的明细表头,SPU/SKU/商品编码固定置顶不在此列。 */
export const LIMIT_TABLE_FILTER_FIELDS: LimitTableFilterField[] = [...new Map(
LIMIT_TABLE_COLUMNS.flatMap((column) => {
if (!column.filterKey || FIXED_FRONT_FILTER_KEYS.has(column.filterKey)) return []
return [{
key: column.filterKey,
label: column.label,
fixedOptions: column.filterKey === 'productStatus' ? PRODUCT_STATUS_OPTIONS : undefined,
}]
}).map((field) => [field.key, field] as const),
).values()]
export const COMBO_LIMIT_FILTER_FIELDS: LimitTableFilterField[] = COMBO_LIMIT_FIELDS
.filter((field) => field.key === 'comboCode' || field.key === 'opsHead')
.map((field) => ({ key: field.key, label: field.label }))
export const LIMIT_TABLE_FILTER_FIELD_MAP = new Map(
LIMIT_TABLE_FILTER_FIELDS.map((field) => [field.key, field]),
)
const COMBO_LIMIT_FILTER_FIELD_KEY_SET = new Set(COMBO_LIMIT_FILTER_FIELDS.map((field) => field.key))
const DEFAULT_VISIBLE_FILTER_FIELDS = [
'productStatus',
@ -67,6 +75,7 @@ export function buildDefaultLimitTableSettings(): LimitTableSettings {
return {
columns: defaultLimitTableColumnKeys().map((key) => ({ key, visible: true })),
filterFields: [...DEFAULT_VISIBLE_FILTER_FIELDS],
comboFilterFields: COMBO_LIMIT_FILTER_FIELDS.map((field) => field.key),
compact: false,
pageSize: 50,
widths: {},
@ -120,9 +129,18 @@ export function normalizeLimitTableSettings(value: unknown): LimitTableSettings
}
}
const comboFilterFields = Array.isArray(source.comboFilterFields)
? Array.from(new Set(
source.comboFilterFields.filter(
(key): key is string => typeof key === 'string' && COMBO_LIMIT_FILTER_FIELD_KEY_SET.has(key),
),
))
: defaults.comboFilterFields
return {
columns,
filterFields,
comboFilterFields,
compact: source.compact === true,
pageSize,
widths,
@ -216,6 +234,26 @@ export function useLimitTableSettings(userId: string) {
update((current) => ({ ...current, filterFields: [...DEFAULT_VISIBLE_FILTER_FIELDS] }))
}, [update])
const toggleComboFilterField = useCallback((key: string) => {
if (!COMBO_LIMIT_FILTER_FIELD_KEY_SET.has(key)) return
update((current) => {
const visible = current.comboFilterFields.includes(key)
const comboFilterFields = visible
? current.comboFilterFields.filter((item) => item !== key)
: COMBO_LIMIT_FILTER_FIELDS
.map((field) => field.key)
.filter((item) => current.comboFilterFields.includes(item) || item === key)
return { ...current, comboFilterFields }
})
}, [update])
const resetComboFilterFields = useCallback(() => {
update((current) => ({
...current,
comboFilterFields: COMBO_LIMIT_FILTER_FIELDS.map((field) => field.key),
}))
}, [update])
const setCompact = useCallback((compact: boolean) => {
update((current) => ({ ...current, compact }))
}, [update])
@ -238,6 +276,8 @@ export function useLimitTableSettings(userId: string) {
resetColumns,
toggleFilterField,
resetFilterFields,
toggleComboFilterField,
resetComboFilterFields,
setCompact,
setPageSize,
setColumnWidths,