feat(supply-chain): 限价筛选条件支持自定义
This commit is contained in:
parent
7ce4b93706
commit
89eb66c6f6
@ -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}
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user