+
{(['day', 'month', 'year'] as const).map((grain) => (
)
diff --git a/furniture-3d-system/src/app/supply-chain/database/product-archive/archiveCodeQuery.ts b/furniture-3d-system/src/app/supply-chain/database/product-archive/archiveCodeQuery.ts
new file mode 100644
index 00000000..c6fb379a
--- /dev/null
+++ b/furniture-3d-system/src/app/supply-chain/database/product-archive/archiveCodeQuery.ts
@@ -0,0 +1,19 @@
+import { splitArchiveFilterValues } from './productArchiveToolbarFields'
+
+export const ARCHIVE_CODE_FIELDS = ['spu', 'sku', 'barcode'] as const
+export type ArchiveCodeField = (typeof ARCHIVE_CODE_FIELDS)[number]
+export const ARCHIVE_CODE_BATCH_LIMIT = 5000
+
+export function isArchiveCodeField(key: string): key is ArchiveCodeField {
+ return (ARCHIVE_CODE_FIELDS as readonly string[]).includes(key)
+}
+
+export function parseArchiveCodeQuery(text: string) {
+ const values = splitArchiveFilterValues(text)
+ const error = values.length > ARCHIVE_CODE_BATCH_LIMIT
+ ? `一次最多查询 ${ARCHIVE_CODE_BATCH_LIMIT} 个编码,当前 ${values.length} 个,请减少后重试。`
+ : values.some(value => value.length > 200)
+ ? '单个编码不能超过 200 个字符,请检查粘贴内容。'
+ : ''
+ return { values, error }
+}
diff --git a/furniture-3d-system/src/app/supply-chain/database/product-archive/productArchiveToolbarFields.test.ts b/furniture-3d-system/src/app/supply-chain/database/product-archive/productArchiveToolbarFields.test.ts
index 605d7a26..54e3f084 100644
--- a/furniture-3d-system/src/app/supply-chain/database/product-archive/productArchiveToolbarFields.test.ts
+++ b/furniture-3d-system/src/app/supply-chain/database/product-archive/productArchiveToolbarFields.test.ts
@@ -7,10 +7,10 @@ import {
} from './productArchiveToolbarFields'
describe('product archive toolbar fields', () => {
- it('puts SPU/barcode/supplier first in the default visible fields', () => {
- expect(DEFAULT_ARCHIVE_PRECISE_FIELDS.slice(0, 3)).toEqual(['spu', 'barcode', 'supplier'])
+ it('puts code inputs before the existing supplier and owner filters', () => {
+ expect(DEFAULT_ARCHIVE_PRECISE_FIELDS.slice(0, 4)).toEqual(['spu', 'sku', 'barcode', 'supplier'])
expect(DEFAULT_ARCHIVE_PRECISE_FIELDS).toEqual([
- 'spu', 'barcode', 'supplier',
+ 'spu', 'sku', 'barcode', 'supplier',
'designer', 'opsHead', 'opsDeptHead', 'opsDirector', 'purchaser', 'warehouseHead',
'categoryL1', 'brand', 'productStatus', 'ownerEmploymentStatus',
])
@@ -23,16 +23,15 @@ describe('product archive toolbar fields', () => {
expect(designer?.staticOptions).toBeUndefined()
})
- it('covers all quick-field keys and keeps sku/productName out of the precise pool', () => {
+ it('keeps code inputs configurable without duplicating quick-field keys', () => {
const preciseKeys = ARCHIVE_PRECISE_FIELDS.map((item) => item.key)
- for (const key of ['sku', 'productName']) {
- expect(preciseKeys).not.toContain(key)
- }
+ expect(preciseKeys).not.toContain('productName')
// SPU/条码/供应商既在点选框池也在「请选择」池
- for (const key of ['spu', 'barcode', 'supplier']) {
+ for (const key of ['spu', 'sku', 'barcode', 'supplier']) {
expect(preciseKeys).toContain(key)
expect(ARCHIVE_QUICK_FIELD_OPTIONS.some((item) => item.key === key)).toBe(true)
}
+ expect(new Set(ARCHIVE_QUICK_FIELD_OPTIONS.map(item => item.key)).size).toBe(ARCHIVE_QUICK_FIELD_OPTIONS.length)
})
it('normalizes precise fields by dedupe and pool membership', () => {
diff --git a/furniture-3d-system/src/app/supply-chain/database/product-archive/productArchiveToolbarFields.ts b/furniture-3d-system/src/app/supply-chain/database/product-archive/productArchiveToolbarFields.ts
index f88ebb02..470e33e7 100644
--- a/furniture-3d-system/src/app/supply-chain/database/product-archive/productArchiveToolbarFields.ts
+++ b/furniture-3d-system/src/app/supply-chain/database/product-archive/productArchiveToolbarFields.ts
@@ -16,11 +16,12 @@ function field(key: string, label: string, staticOptions?: readonly string[]): A
}
/**
- * 顶部常显字段池(设置里可配显隐)。统一形态:点击弹「可搜索 + 可勾选」小弹窗,
- * 字段名内置在框占位里。模糊搜索框已覆盖 SPU/SKU/条码/商品名称/供应商,不再放外框。
+ * 顶部常显字段池(设置里可配显隐)。编码字段直接输入、回车批量查询;
+ * 其余字段点击弹可搜索的勾选面板,字段名内置在框占位里。
*/
export const ARCHIVE_PRECISE_FIELDS: ArchiveToolbarField[] = [
field('spu', 'SPU'),
+ field('sku', 'SKU'),
field('barcode', '条码'),
field('supplier', '供应商'),
field('designer', '设计师'),
@@ -48,16 +49,15 @@ export const ARCHIVE_PRECISE_FIELDS: ArchiveToolbarField[] = [
field('installDocId', '安装图纸/视频ID'),
]
-/** 默认常显框(SPU/条码/供应商置顶 + 运营链字段 + 高频点选框)。 */
+/** 默认常显框(三个编码输入框置顶 + 供应商与运营链点选框)。 */
export const DEFAULT_ARCHIVE_PRECISE_FIELDS: string[] = [
- 'spu', 'barcode', 'supplier',
+ 'spu', 'sku', 'barcode', 'supplier',
'designer', 'opsHead', 'opsDeptHead', 'opsDirector', 'purchaser', 'warehouseHead',
'categoryL1', 'brand', 'productStatus', 'ownerEmploymentStatus',
]
/** 「请选择+请输入」的字段池:全部可精准匹配的字段。 */
export const ARCHIVE_QUICK_FIELD_OPTIONS: ArchiveToolbarField[] = [
- field('sku', 'SKU'),
field('productName', '商品名称'),
...ARCHIVE_PRECISE_FIELDS,
]
diff --git a/furniture-3d-system/src/app/supply-chain/database/product-archive/useProductArchiveToolbarPreferences.test.ts b/furniture-3d-system/src/app/supply-chain/database/product-archive/useProductArchiveToolbarPreferences.test.ts
index cc509b20..78d927fe 100644
--- a/furniture-3d-system/src/app/supply-chain/database/product-archive/useProductArchiveToolbarPreferences.test.ts
+++ b/furniture-3d-system/src/app/supply-chain/database/product-archive/useProductArchiveToolbarPreferences.test.ts
@@ -23,12 +23,20 @@ describe('product archive toolbar preferences', () => {
)
// spu 在字段框池(置顶点选框)被保留;nope 被清掉;quickSelects 已废弃被忽略
- expect(preferences.preciseFields).toEqual(['designer', 'spu', 'region'])
+ expect(preferences.preciseFields).toEqual(['designer', 'spu', 'sku', 'region'])
expect(preferences.collapsed).toBe(false)
expect(preferences.savedQueries).toEqual([])
expect('quickSelects' in preferences).toBe(false)
})
+ it('only adds the SKU input once and preserves an intentionally hidden input', () => {
+ const old = parseProductArchiveToolbarPreferences(JSON.stringify({ preciseFields: ['spu', 'barcode', 'brand'] }))
+ expect(old.preciseFields).toEqual(['spu', 'sku', 'barcode', 'brand'])
+ const hidden = parseProductArchiveToolbarPreferences(JSON.stringify({ ...old, preciseFields: ['spu', 'brand'] }))
+ expect(hidden.preciseFields).toEqual(['spu', 'brand'])
+ expect(parseProductArchiveToolbarPreferences(JSON.stringify({ preciseFields: [] })).preciseFields).toEqual([])
+ })
+
it('drops saved queries without usable filters and caps the list', () => {
const queries = Array.from({ length: 25 }, (_, index) => ({
id: `q-${index}`,
diff --git a/furniture-3d-system/src/app/supply-chain/database/product-archive/useProductArchiveToolbarPreferences.ts b/furniture-3d-system/src/app/supply-chain/database/product-archive/useProductArchiveToolbarPreferences.ts
index 8dffcc83..deaffc23 100644
--- a/furniture-3d-system/src/app/supply-chain/database/product-archive/useProductArchiveToolbarPreferences.ts
+++ b/furniture-3d-system/src/app/supply-chain/database/product-archive/useProductArchiveToolbarPreferences.ts
@@ -19,6 +19,7 @@ export type ArchiveSavedQuery = {
}
export type ProductArchiveToolbarPreferences = {
+ codeInputVersion: 1
/** 顶部常显的字段框(设置里可配,默认运营链字段+四个高频点选框)。 */
preciseFields: string[]
/** 顶部筛选行是否收起。 */
@@ -28,6 +29,7 @@ export type ProductArchiveToolbarPreferences = {
}
export const DEFAULT_PRODUCT_ARCHIVE_TOOLBAR_PREFERENCES: ProductArchiveToolbarPreferences = {
+ codeInputVersion: 1,
preciseFields: [...DEFAULT_ARCHIVE_PRECISE_FIELDS],
collapsed: false,
savedQueries: [],
@@ -63,9 +65,15 @@ export function parseProductArchiveToolbarPreferences(raw: string | null): Produ
if (!raw) return DEFAULT_PRODUCT_ARCHIVE_TOOLBAR_PREFERENCES
try {
const parsed = JSON.parse(raw) as Record
+ const preciseFields = normalizeArchivePreciseFields(parsed.preciseFields)
+ // 旧设置仅升级一次,补上独立 SKU 输入框;之后用户隐藏 SKU 的设置照常保留。
+ if (parsed.codeInputVersion !== 1 && preciseFields.length && !preciseFields.includes('sku')) {
+ preciseFields.splice(Math.max(0, preciseFields.indexOf('spu') + 1), 0, 'sku')
+ }
return {
+ codeInputVersion: 1,
// 全部取消勾选是合法状态(顶部一个字段框都不摆),空数组保持空。
- preciseFields: normalizeArchivePreciseFields(parsed.preciseFields),
+ preciseFields,
collapsed: parsed.collapsed === true,
savedQueries: sanitizeSavedQueries(parsed.savedQueries),
}
diff --git a/furniture-3d-system/src/components/ui/Modal.tsx b/furniture-3d-system/src/components/ui/Modal.tsx
index 002e4da7..69c24250 100644
--- a/furniture-3d-system/src/components/ui/Modal.tsx
+++ b/furniture-3d-system/src/components/ui/Modal.tsx
@@ -17,6 +17,8 @@ interface ModalProps {
title?: string
children: React.ReactNode
size?: 'small' | 'medium' | 'large' | 'xlarge'
+ /** 紧凑查询等短操作使用轻遮罩和较小留白;默认弹窗样式保持原有口径。 */
+ variant?: 'default' | 'compact'
className?: string
bodyClassName?: string
showCloseButton?: boolean
@@ -36,6 +38,7 @@ export default function Modal({
title,
children,
size = 'medium',
+ variant = 'default',
className,
bodyClassName,
showCloseButton = true,
@@ -136,8 +139,9 @@ export default function Modal({
if (!shouldRender) return null
+ const compact = variant === 'compact'
const sizeClasses = {
- small: 'max-w-md',
+ small: compact ? 'max-w-[400px]' : 'max-w-md',
medium: 'max-w-2xl',
large: 'max-w-4xl',
xlarge: 'max-w-6xl',
@@ -152,9 +156,9 @@ export default function Modal({
data-testid="obsidian-modal-overlay"
className="fixed inset-0 z-50 flex items-center justify-center p-3 sm:p-6"
style={{
- backgroundColor: isAnimating ? 'rgba(0, 0, 0, 0.45)' : 'rgba(0, 0, 0, 0)',
- backdropFilter: isAnimating ? 'blur(32px)' : 'blur(0px)',
- WebkitBackdropFilter: isAnimating ? 'blur(32px)' : 'blur(0px)',
+ backgroundColor: isAnimating ? (compact ? 'rgba(24, 24, 27, 0.16)' : 'rgba(0, 0, 0, 0.45)') : 'rgba(0, 0, 0, 0)',
+ backdropFilter: compact ? 'none' : isAnimating ? 'blur(32px)' : 'blur(0px)',
+ WebkitBackdropFilter: compact ? 'none' : isAnimating ? 'blur(32px)' : 'blur(0px)',
transition: `all ${enterDuration} ${appleAnimationTokens.components.modal.enter.easing}`,
}}
onClick={closeOnOverlayClick ? onClose : undefined}
@@ -171,7 +175,7 @@ export default function Modal({
maxHeight: 'min(92vh, 92dvh)',
display: 'flex',
flexDirection: 'column',
- boxShadow: isAnimating ? '0 40px 80px rgba(0, 0, 0, 0.30)' : 'none',
+ boxShadow: isAnimating ? (compact ? '0 12px 32px rgba(24, 24, 27, 0.14)' : '0 40px 80px rgba(0, 0, 0, 0.30)') : 'none',
transform: isAnimating ? 'scale(1) translateY(0)' : 'scale(0.98) translateY(10px)',
opacity: isAnimating ? 1 : 0,
transition: `all ${enterDuration} cubic-bezier(0.16, 1, 0.3, 1)`,
@@ -179,12 +183,12 @@ export default function Modal({
onClick={(e) => e.stopPropagation()}
>
{!hideHeader && (title || showCloseButton) && (
-
+
{title && (
{title}
@@ -203,7 +207,7 @@ export default function Modal({
)}