重新布局
This commit is contained in:
parent
b4991fcefd
commit
84f94b53d9
|
|
@ -1,5 +1,5 @@
|
|||
import { ThumbnailCell as ThumbnailCell_a0b2acb813359aec894b6644d7c3bfd2 } from '../../../components/products/ThumbnailCell'
|
||||
import { ThumbnailField as ThumbnailField_ba44ab32cac4d742a03a48fb6960602e } from '../../../components/products/ThumbnailField'
|
||||
import { ThumbnailCell as ThumbnailCell_c4ec43b3e74df5c75a3fb90c93e06b1d } from '../../../components/cells/ThumbnailCell'
|
||||
import { ThumbnailField as ThumbnailField_0d2fbe11370060d58b3925e5dbbb79d6 } from '../../../components/fields/ThumbnailField'
|
||||
import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||
import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||
import { LexicalDiffComponent as LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||
|
|
@ -24,15 +24,16 @@ import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93
|
|||
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { SyncMedusaButton as SyncMedusaButton_8c90663551920f0510ea531726668adc } from '../../../components/products/SyncMedusaButton'
|
||||
import { default as default_3fd1353246fc8a459244c8dc11f58470 } from '../../../components/products/ProductGridStyler'
|
||||
import { ForceSyncButton as ForceSyncButton_86f9d5df4f20495427521354d06db618 } from '../../../components/products/ForceSyncButton'
|
||||
import { RelatedProductsField as RelatedProductsField_f3e26ca26ab1ef52a2ee0f6932180426 } from '../../../components/fields/RelatedProductsField'
|
||||
import { SyncMedusaButton as SyncMedusaButton_31e6578e170fdd0bad7013c8202d6e08 } from '../../../components/sync/SyncMedusaButton'
|
||||
import { default as default_c2e3814fe427263135b1f5931c37f6f2 } from '../../../components/list/ProductGridStyler'
|
||||
import { ForceSyncButton as ForceSyncButton_28396efe36d6238add95cf44109e281c } from '../../../components/sync/ForceSyncButton'
|
||||
import { S3ClientUploadHandler as S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24 } from '@payloadcms/storage-s3/client'
|
||||
import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc'
|
||||
|
||||
export const importMap = {
|
||||
"/components/products/ThumbnailCell#ThumbnailCell": ThumbnailCell_a0b2acb813359aec894b6644d7c3bfd2,
|
||||
"/components/products/ThumbnailField#ThumbnailField": ThumbnailField_ba44ab32cac4d742a03a48fb6960602e,
|
||||
"/components/cells/ThumbnailCell#ThumbnailCell": ThumbnailCell_c4ec43b3e74df5c75a3fb90c93e06b1d,
|
||||
"/components/fields/ThumbnailField#ThumbnailField": ThumbnailField_0d2fbe11370060d58b3925e5dbbb79d6,
|
||||
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell": RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalField": RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
"@payloadcms/richtext-lexical/rsc#LexicalDiffComponent": LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
|
|
@ -57,9 +58,10 @@ export const importMap = {
|
|||
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"/components/products/SyncMedusaButton#SyncMedusaButton": SyncMedusaButton_8c90663551920f0510ea531726668adc,
|
||||
"/components/products/ProductGridStyler#default": default_3fd1353246fc8a459244c8dc11f58470,
|
||||
"/components/products/ForceSyncButton#ForceSyncButton": ForceSyncButton_86f9d5df4f20495427521354d06db618,
|
||||
"/components/fields/RelatedProductsField#RelatedProductsField": RelatedProductsField_f3e26ca26ab1ef52a2ee0f6932180426,
|
||||
"/components/sync/SyncMedusaButton#SyncMedusaButton": SyncMedusaButton_31e6578e170fdd0bad7013c8202d6e08,
|
||||
"/components/list/ProductGridStyler#default": default_c2e3814fe427263135b1f5931c37f6f2,
|
||||
"/components/sync/ForceSyncButton#ForceSyncButton": ForceSyncButton_28396efe36d6238add95cf44109e281c,
|
||||
"@payloadcms/storage-s3/client#S3ClientUploadHandler": S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24,
|
||||
"@payloadcms/next/rsc#CollectionCards": CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,11 +33,11 @@ export const Products: CollectionConfig = {
|
|||
},
|
||||
components: {
|
||||
edit: {
|
||||
PreviewButton: '/components/products/ForceSyncButton#ForceSyncButton',
|
||||
PreviewButton: '/components/sync/ForceSyncButton#ForceSyncButton',
|
||||
},
|
||||
beforeListTable: [
|
||||
'/components/products/SyncMedusaButton#SyncMedusaButton',
|
||||
'/components/products/ProductGridStyler',
|
||||
'/components/sync/SyncMedusaButton#SyncMedusaButton',
|
||||
'/components/list/ProductGridStyler',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
@ -103,8 +103,8 @@ export const Products: CollectionConfig = {
|
|||
readOnly: true,
|
||||
width: '20%',
|
||||
components: {
|
||||
Cell: '/components/products/ThumbnailCell#ThumbnailCell',
|
||||
Field: '/components/products/ThumbnailField#ThumbnailField',
|
||||
Cell: '/components/cells/ThumbnailCell#ThumbnailCell',
|
||||
Field: '/components/fields/ThumbnailField#ThumbnailField',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -131,7 +131,8 @@ export const Products: CollectionConfig = {
|
|||
hasMany: true,
|
||||
options: ['noopener', 'noreferrer', 'nofollow'],
|
||||
admin: {
|
||||
description: 'The rel attribute defines the relationship between a linked resource and the current document. This is a custom link field.',
|
||||
description:
|
||||
'The rel attribute defines the relationship between a linked resource and the current document. This is a custom link field.',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
@ -170,7 +171,10 @@ export const Products: CollectionConfig = {
|
|||
relationTo: 'products',
|
||||
hasMany: true,
|
||||
admin: {
|
||||
description: '相关商品',
|
||||
description: '相关商品,支持搜索联想',
|
||||
components: {
|
||||
Field: '/components/fields/RelatedProductsField#RelatedProductsField',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,377 @@
|
|||
'use client'
|
||||
import { useField, useConfig, FieldLabel } from '@payloadcms/ui'
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import type { RelationshipFieldClientComponent } from 'payload'
|
||||
|
||||
/**
|
||||
* 相关商品字段组件 - 保留原始格子搜索布局,使用 Payload 默认样式
|
||||
* 横向滚动显示搜索结果,支持实时搜索联想
|
||||
*/
|
||||
export const RelatedProductsField: RelationshipFieldClientComponent = ({ path, field }) => {
|
||||
const { value, setValue } = useField<string[]>({ path })
|
||||
const { config } = useConfig()
|
||||
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const [searchResults, setSearchResults] = useState<any[]>([])
|
||||
const [selectedDetails, setSelectedDetails] = useState<any[]>([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
// Fetch details for selected items
|
||||
useEffect(() => {
|
||||
const fetchSelectedDetails = async () => {
|
||||
if (!value || value.length === 0) {
|
||||
setSelectedDetails([])
|
||||
return
|
||||
}
|
||||
|
||||
const ids = Array.isArray(value) ? value : [value as unknown as string]
|
||||
|
||||
try {
|
||||
const searchParams = new URLSearchParams()
|
||||
ids.forEach((id, index) => {
|
||||
const idStr = typeof id === 'object' ? (id as any).id : id
|
||||
searchParams.append(`where[id][in][${index}]`, idStr)
|
||||
})
|
||||
|
||||
const res = await fetch(
|
||||
`${config.routes.api}/products?${searchParams.toString()}&limit=${ids.length}`,
|
||||
)
|
||||
const data = await res.json()
|
||||
|
||||
if (data.docs) {
|
||||
const docsMap = new Map(data.docs.map((d: any) => [d.id, d]))
|
||||
const ordered = ids
|
||||
.map((id) => {
|
||||
const idStr = typeof id === 'object' ? (id as any).id : id
|
||||
return docsMap.get(idStr)
|
||||
})
|
||||
.filter(Boolean)
|
||||
setSelectedDetails(ordered)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error fetching selected products:', e)
|
||||
}
|
||||
}
|
||||
|
||||
fetchSelectedDetails()
|
||||
}, [value, config.routes.api])
|
||||
|
||||
// Search function with debounce
|
||||
const searchProducts = useCallback(
|
||||
async (term: string) => {
|
||||
if (!term) {
|
||||
setSearchResults([])
|
||||
return
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${config.routes.api}/products?where[title][like]=${encodeURIComponent(term)}&limit=10`,
|
||||
)
|
||||
const data = await res.json()
|
||||
setSearchResults(data.docs || [])
|
||||
} catch (e) {
|
||||
console.error('Search error:', e)
|
||||
setSearchResults([])
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
},
|
||||
[config.routes.api],
|
||||
)
|
||||
|
||||
// Debounced search
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
searchProducts(inputValue)
|
||||
}, 300)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}, [inputValue, searchProducts])
|
||||
|
||||
const handleAdd = (product: any) => {
|
||||
const currentIds = Array.isArray(value) ? value : []
|
||||
const exists = currentIds.some((id: any) => {
|
||||
const idStr = typeof id === 'object' ? id.id : id
|
||||
return idStr === product.id
|
||||
})
|
||||
|
||||
if (!exists) {
|
||||
setValue([...currentIds, product.id])
|
||||
setSelectedDetails((prev) => [...prev, product])
|
||||
}
|
||||
setInputValue('')
|
||||
}
|
||||
|
||||
const handleRemove = (idToRemove: string) => {
|
||||
const currentIds = Array.isArray(value) ? value : []
|
||||
const newValue = currentIds.filter((id: any) => {
|
||||
const idStr = typeof id === 'object' ? id.id : id
|
||||
return idStr !== idToRemove
|
||||
})
|
||||
setValue(newValue)
|
||||
setSelectedDetails((prev) => prev.filter((p) => p.id !== idToRemove))
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ marginBottom: 'var(--base)' }}>
|
||||
<FieldLabel field={field} />
|
||||
|
||||
{/* Selected Items Grid - 网格显示已选商品 */}
|
||||
{selectedDetails.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))',
|
||||
gap: 'var(--base)',
|
||||
marginBottom: 'var(--base)',
|
||||
}}
|
||||
>
|
||||
{selectedDetails.map((product) => (
|
||||
<div
|
||||
key={product.id}
|
||||
style={{
|
||||
border: '1px solid var(--theme-elevation-150)',
|
||||
borderRadius: 'var(--border-radius-m)',
|
||||
overflow: 'hidden',
|
||||
background: 'var(--theme-elevation-50)',
|
||||
position: 'relative',
|
||||
transition: 'all 0.2s',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.borderColor = 'var(--theme-elevation-400)'
|
||||
e.currentTarget.style.background = 'var(--theme-elevation-100)'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.borderColor = 'var(--theme-elevation-150)'
|
||||
e.currentTarget.style.background = 'var(--theme-elevation-50)'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: '120px',
|
||||
overflow: 'hidden',
|
||||
background: 'var(--theme-elevation-100)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{product.thumbnail ? (
|
||||
<img
|
||||
src={product.thumbnail}
|
||||
alt={product.title}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<span style={{ fontSize: '12px', color: 'var(--theme-elevation-500)' }}>
|
||||
无图片
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ padding: 'calc(var(--base) / 2)' }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
color: 'var(--theme-text)',
|
||||
}}
|
||||
>
|
||||
{product.title}
|
||||
</div>
|
||||
<div style={{ fontSize: '11px', color: 'var(--theme-elevation-500)' }}>
|
||||
{product.status}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemove(product.id)}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '4px',
|
||||
right: '4px',
|
||||
background: 'var(--theme-elevation-0)',
|
||||
border: '1px solid var(--theme-elevation-400)',
|
||||
borderRadius: '50%',
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: 'var(--theme-text)',
|
||||
opacity: 0.8,
|
||||
fontSize: '16px',
|
||||
lineHeight: '1',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.opacity = '1'
|
||||
e.currentTarget.style.background = 'var(--theme-error-100)'
|
||||
e.currentTarget.style.color = 'var(--theme-error-600)'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.opacity = '0.8'
|
||||
e.currentTarget.style.background = 'var(--theme-elevation-0)'
|
||||
e.currentTarget.style.color = 'var(--theme-text)'
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Search Input - 搜索输入框 */}
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
placeholder="搜索商品..."
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: 'calc(var(--base) / 2) var(--base)',
|
||||
borderRadius: 'var(--border-radius-m)',
|
||||
border: '1px solid var(--theme-elevation-400)',
|
||||
background: 'var(--theme-input-bg)',
|
||||
color: 'var(--theme-text)',
|
||||
fontFamily: 'inherit',
|
||||
fontSize: '1rem',
|
||||
}}
|
||||
onFocus={() => inputValue && searchProducts(inputValue)}
|
||||
/>
|
||||
|
||||
{/* Horizontal Scroll Results - 横向滚动搜索结果(保留原始格子布局) */}
|
||||
{inputValue && searchResults.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: 'calc(var(--base) / 2)',
|
||||
display: 'flex',
|
||||
overflowX: 'auto',
|
||||
gap: 'var(--base)',
|
||||
padding: 'calc(var(--base) / 2) 0',
|
||||
}}
|
||||
>
|
||||
{searchResults.map((product) => (
|
||||
<div
|
||||
key={product.id}
|
||||
onClick={() => handleAdd(product)}
|
||||
style={{
|
||||
flex: '0 0 160px',
|
||||
cursor: 'pointer',
|
||||
border: '1px solid var(--theme-elevation-150)',
|
||||
borderRadius: 'var(--border-radius-m)',
|
||||
background: 'var(--theme-elevation-50)',
|
||||
overflow: 'hidden',
|
||||
transition: 'all 0.2s',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.borderColor = 'var(--theme-primary-500)'
|
||||
e.currentTarget.style.background = 'var(--theme-elevation-100)'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.borderColor = 'var(--theme-elevation-150)'
|
||||
e.currentTarget.style.background = 'var(--theme-elevation-50)'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: '120px',
|
||||
overflow: 'hidden',
|
||||
background: 'var(--theme-elevation-100)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{product.thumbnail ? (
|
||||
<img
|
||||
src={product.thumbnail}
|
||||
alt=""
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<span style={{ fontSize: '10px', color: 'var(--theme-elevation-500)' }}>
|
||||
无图
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ padding: 'calc(var(--base) / 2)' }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
color: 'var(--theme-text)',
|
||||
}}
|
||||
>
|
||||
{product.title}
|
||||
</div>
|
||||
<div style={{ fontSize: '11px', color: 'var(--theme-elevation-500)' }}>
|
||||
ID: {product.medusaId || product.id}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* No results or loading - 加载状态和空结果提示 */}
|
||||
{inputValue && !isLoading && searchResults.length === 0 && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: 'calc(var(--base) / 2)',
|
||||
padding: 'var(--base)',
|
||||
textAlign: 'center',
|
||||
color: 'var(--theme-elevation-500)',
|
||||
fontSize: '14px',
|
||||
}}
|
||||
>
|
||||
未找到匹配的商品
|
||||
</div>
|
||||
)}
|
||||
{isLoading && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: 'calc(var(--base) / 2)',
|
||||
padding: 'var(--base)',
|
||||
textAlign: 'center',
|
||||
color: 'var(--theme-elevation-500)',
|
||||
fontSize: '14px',
|
||||
}}
|
||||
>
|
||||
搜索中...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Helper text */}
|
||||
{field.admin?.description && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: 'calc(var(--base) / 4)',
|
||||
fontSize: '0.875rem',
|
||||
color: 'var(--theme-elevation-500)',
|
||||
}}
|
||||
>
|
||||
{field.admin.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import './product-grid-styler.scss'
|
||||
|
||||
// 这个组件本身不渲染任何内容,只负责在 Products 列表页注入 CSS
|
||||
|
|
@ -207,7 +207,7 @@ export interface Product {
|
|||
*/
|
||||
handle?: string | null;
|
||||
/**
|
||||
* 相关商品
|
||||
* 相关商品,支持搜索联想
|
||||
*/
|
||||
relatedProducts?: (number | Product)[] | null;
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in New Issue