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

View File

@ -453,15 +453,46 @@ describe('限价管理', () => {
fireEvent.click(screen.getByRole('button', { name: '显示设置' })) fireEvent.click(screen.getByRole('button', { name: '显示设置' }))
fireEvent.click(screen.getByRole('menuitem', { name: '筛选设置' })) fireEvent.click(screen.getByRole('menuitem', { name: '筛选设置' }))
fireEvent.click(screen.getByRole('checkbox', { name: '品牌' })) fireEvent.click(screen.getByRole('checkbox', { name: '品牌' }))
fireEvent.click(screen.getByRole('checkbox', { name: '商品名称' }))
fireEvent.click(screen.getByRole('button', { name: '完成' })) fireEvent.click(screen.getByRole('button', { name: '完成' }))
await waitFor(() => { await waitFor(() => {
expect(filterTrigger('brand')).toBeNull() expect(filterTrigger('brand')).toBeNull()
expect(filterTrigger('productName')).not.toBeNull()
}) })
view.unmount() view.unmount()
render(<CustomTableView canEdit userId="user-b" />) render(<CustomTableView canEdit userId="user-b" />)
expect(filterTrigger('brand')).not.toBeNull() 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 () => { it('破价订单审核:点击订单在右侧查看破价数据与审核要点', async () => {

View File

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

View File

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

View File

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

View File

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