分类 preoder 和 order

This commit is contained in:
龟男日记\www 2026-02-20 04:56:52 +08:00
parent efb3f2c727
commit 35928a6144
8 changed files with 504 additions and 12 deletions

View File

@ -28,9 +28,11 @@ import { RelatedProductsField as RelatedProductsField_f3e26ca26ab1ef52a2ee0f6932
import { TaobaoLinkPreview as TaobaoLinkPreview_44c9439e828c0463191af62d21ad4959 } from '../../../components/fields/TaobaoLinkPreview' import { TaobaoLinkPreview as TaobaoLinkPreview_44c9439e828c0463191af62d21ad4959 } from '../../../components/fields/TaobaoLinkPreview'
import { UnifiedSyncButton as UnifiedSyncButton_fc99b3f144909da232f9fd4ff7269523 } from '../../../components/sync/UnifiedSyncButton' import { UnifiedSyncButton as UnifiedSyncButton_fc99b3f144909da232f9fd4ff7269523 } from '../../../components/sync/UnifiedSyncButton'
import { default as default_c2e3814fe427263135b1f5931c37f6f2 } from '../../../components/list/ProductGridStyler' import { default as default_c2e3814fe427263135b1f5931c37f6f2 } from '../../../components/list/ProductGridStyler'
import { PreorderProgressCell as PreorderProgressCell_67df47753573233f0c83480de687f13b } from '../../../components/cells/PreorderProgressCell'
import { RefreshOrderCountField as RefreshOrderCountField_ef327f0ad449eac595b5e301044c0996 } from '../../../components/fields/RefreshOrderCountField'
import { BlocksFeatureClient as BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' import { BlocksFeatureClient as BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { PreorderOrdersField as PreorderOrdersField_a4aa1b8cbd6dec364a834b059228f43f } from '../../../components/fields/PreorderOrdersField' import { PreorderOrdersField as PreorderOrdersField_a4aa1b8cbd6dec364a834b059228f43f } from '../../../components/fields/PreorderOrdersField'
import { RefreshOrderCountButton as RefreshOrderCountButton_f6ce1bfc16a20083ee4c6ceb7022839e } from '../../../components/sync/RefreshOrderCountButton' import { PreorderProductGridStyler as PreorderProductGridStyler_e7f6f7c2233fc58ae87e992227bb80c5 } from '../../../components/list/PreorderProductGridStyler'
import { default as default_767734c8b7b095ea28d54c32abcf46e4 } from '../../../components/views/AdminPanel' import { default as default_767734c8b7b095ea28d54c32abcf46e4 } from '../../../components/views/AdminPanel'
import { default as default_a766ef013722c08f9bb937940272cb5f } from '../../../components/views/LogsManagerView' import { default as default_a766ef013722c08f9bb937940272cb5f } from '../../../components/views/LogsManagerView'
import { S3ClientUploadHandler as S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24 } from '@payloadcms/storage-s3/client' import { S3ClientUploadHandler as S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24 } from '@payloadcms/storage-s3/client'
@ -67,9 +69,11 @@ export const importMap = {
"/components/fields/TaobaoLinkPreview#TaobaoLinkPreview": TaobaoLinkPreview_44c9439e828c0463191af62d21ad4959, "/components/fields/TaobaoLinkPreview#TaobaoLinkPreview": TaobaoLinkPreview_44c9439e828c0463191af62d21ad4959,
"/components/sync/UnifiedSyncButton#UnifiedSyncButton": UnifiedSyncButton_fc99b3f144909da232f9fd4ff7269523, "/components/sync/UnifiedSyncButton#UnifiedSyncButton": UnifiedSyncButton_fc99b3f144909da232f9fd4ff7269523,
"/components/list/ProductGridStyler#default": default_c2e3814fe427263135b1f5931c37f6f2, "/components/list/ProductGridStyler#default": default_c2e3814fe427263135b1f5931c37f6f2,
"/components/cells/PreorderProgressCell#PreorderProgressCell": PreorderProgressCell_67df47753573233f0c83480de687f13b,
"/components/fields/RefreshOrderCountField#RefreshOrderCountField": RefreshOrderCountField_ef327f0ad449eac595b5e301044c0996,
"@payloadcms/richtext-lexical/client#BlocksFeatureClient": BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, "@payloadcms/richtext-lexical/client#BlocksFeatureClient": BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"/components/fields/PreorderOrdersField#PreorderOrdersField": PreorderOrdersField_a4aa1b8cbd6dec364a834b059228f43f, "/components/fields/PreorderOrdersField#PreorderOrdersField": PreorderOrdersField_a4aa1b8cbd6dec364a834b059228f43f,
"/components/sync/RefreshOrderCountButton#RefreshOrderCountButton": RefreshOrderCountButton_f6ce1bfc16a20083ee4c6ceb7022839e, "/components/list/PreorderProductGridStyler#PreorderProductGridStyler": PreorderProductGridStyler_e7f6f7c2233fc58ae87e992227bb80c5,
"/components/views/AdminPanel#default": default_767734c8b7b095ea28d54c32abcf46e4, "/components/views/AdminPanel#default": default_767734c8b7b095ea28d54c32abcf46e4,
"/components/views/LogsManagerView#default": default_a766ef013722c08f9bb937940272cb5f, "/components/views/LogsManagerView#default": default_a766ef013722c08f9bb937940272cb5f,
"@payloadcms/storage-s3/client#S3ClientUploadHandler": S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24, "@payloadcms/storage-s3/client#S3ClientUploadHandler": S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24,

View File

@ -27,7 +27,7 @@ export const PreorderProducts: CollectionConfig = {
slug: 'preorder-products', slug: 'preorder-products',
admin: { admin: {
useAsTitle: 'title', useAsTitle: 'title',
defaultColumns: ['thumbnail', 'title', 'medusaId', 'status', 'updatedAt'], defaultColumns: ['thumbnail', 'title', 'medusaId', 'progress', 'status', 'updatedAt'],
description: '管理预售商品的详细内容和描述', description: '管理预售商品的详细内容和描述',
listSearchableFields: ['title', 'medusaId'], listSearchableFields: ['title', 'medusaId'],
pagination: { pagination: {
@ -36,13 +36,15 @@ export const PreorderProducts: CollectionConfig = {
components: { components: {
beforeListTable: [ beforeListTable: [
'/components/sync/UnifiedSyncButton#UnifiedSyncButton', '/components/sync/UnifiedSyncButton#UnifiedSyncButton',
'/components/sync/RefreshOrderCountButton#RefreshOrderCountButton', '/components/list/PreorderProductGridStyler#PreorderProductGridStyler',
'/components/list/ProductGridStyler',
], ],
}, },
}, },
access: { access: {
read: () => true, read: () => true, // 公开可读
create: ({ req: { user } }) => !!user, // 登录用户可创建
update: ({ req: { user } }) => !!user, // 登录用户可更新
delete: ({ req: { user } }) => !!user, // 登录用户可删除
}, },
fields: [ fields: [
{ {
@ -123,6 +125,15 @@ export const PreorderProducts: CollectionConfig = {
}, },
}, },
}, },
{
name: 'progress',
type: 'ui',
admin: {
components: {
Cell: '/components/cells/PreorderProgressCell#PreorderProgressCell',
},
},
},
], ],
}, },
{ {
@ -168,6 +179,15 @@ export const PreorderProducts: CollectionConfig = {
}, },
}, },
}, },
{
type: 'ui',
name: 'refreshOrderCount',
admin: {
components: {
Field: '/components/fields/RefreshOrderCountField#RefreshOrderCountField',
},
},
},
{ {
type: 'row', type: 'row',
fields: [ fields: [
@ -268,6 +288,17 @@ export const PreorderProducts: CollectionConfig = {
Field: '/components/fields/RelatedProductsField#RelatedProductsField', Field: '/components/fields/RelatedProductsField#RelatedProductsField',
}, },
}, },
filterOptions: ({ relationTo, data }) => {
// 过滤掉当前商品本身,避免自引用
if (data?.id) {
return {
id: {
not_equals: data.id,
},
}
}
return true
},
}, },
], ],
}, },

View File

@ -196,6 +196,17 @@ export const Products: CollectionConfig = {
Field: '/components/fields/RelatedProductsField#RelatedProductsField', Field: '/components/fields/RelatedProductsField#RelatedProductsField',
}, },
}, },
filterOptions: ({ relationTo, data }) => {
// 过滤掉当前商品本身,避免自引用
if (data?.id) {
return {
id: {
not_equals: data.id,
},
}
}
return true
},
}, },
], ],
}, },

View File

@ -0,0 +1,48 @@
'use client'
import React from 'react'
/**
*
*
*/
export function PreorderProgressCell({ rowData }: any) {
const orderCount = parseInt(rowData?.orderCount || '0', 10) || 0
const fakeOrderCount = parseInt(rowData?.fakeOrderCount || '0', 10) || 0
const fundingGoal = parseInt(rowData?.fundingGoal || '0', 10) || 100
const totalCount = orderCount + fakeOrderCount
const percentage = fundingGoal > 0 ? Math.min(Math.round((totalCount / fundingGoal) * 100), 100) : 0
return (
<div className="preorder-progress-info">
<div className="progress-label">
<span></span>
<span className="progress-count">
{totalCount} / {fundingGoal}
</span>
</div>
<div className="progress-bar">
<div
className="progress-fill"
style={{ width: `${percentage}%` }}
/>
</div>
<div className="progress-stats">
<div className="stat-item">
<span className="stat-label"></span>
<span className="stat-value">{orderCount}</span>
</div>
<div className="stat-item">
<span className="stat-label">Fake</span>
<span className="stat-value">{fakeOrderCount}</span>
</div>
<div className="stat-item">
<span className="stat-label"></span>
<span className="stat-value">{percentage}%</span>
</div>
</div>
</div>
)
}

View File

@ -1,19 +1,21 @@
'use client' 'use client'
import Link from 'next/link' import Link from 'next/link'
import { usePathname } from 'next/navigation'
export const ThumbnailCell = (props: any) => { export const ThumbnailCell = (props: any) => {
console.log('=== ThumbnailCell All Props ===', props) const pathname = usePathname()
console.log('Props keys:', Object.keys(props))
// 从 URL 路径中提取 collection slug
const collectionSlug = pathname?.includes('/preorder-products')
? 'preorder-products'
: 'products'
// 尝试从不同的 props 路径获取值 // 尝试从不同的 props 路径获取值
const value = props.value || props.cellData || props.data const value = props.value || props.cellData || props.data
const rowData = props.rowData || props.row const rowData = props.rowData || props.row
console.log('Extracted value:', value)
console.log('Extracted rowData:', rowData)
const isImage = typeof value === 'string' && value.match(/^https?:\/\/.+/) const isImage = typeof value === 'string' && value.match(/^https?:\/\/.+/)
const editUrl = `/admin/collections/products/${rowData?.id || ''}` const editUrl = `/admin/collections/${collectionSlug}/${rowData?.id || ''}`
return ( return (
<Link <Link

View File

@ -0,0 +1,130 @@
'use client'
import { useState } from 'react'
import { Button, useDocumentInfo } from '@payloadcms/ui'
import { useRouter } from 'next/navigation'
/**
*
*
*/
export function RefreshOrderCountField() {
const { id } = useDocumentInfo()
const [loading, setLoading] = useState(false)
const [message, setMessage] = useState('')
const router = useRouter()
const handleRefresh = async () => {
if (!id) {
setMessage('⚠️ 无法获取商品 ID')
return
}
setLoading(true)
setMessage('')
try {
const response = await fetch('/api/refresh-order-counts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
productIds: [id],
}),
})
const data = await response.json()
if (data.success) {
setMessage(`${data.message || '订单计数刷新成功!'}`)
// 刷新页面数据
setTimeout(() => {
router.refresh()
// 重新加载页面以更新显示
window.location.reload()
}, 1000)
} else {
setMessage(`❌ 刷新失败: ${data.error || '未知错误'}`)
}
} catch (error) {
setMessage('❌ 刷新出错: ' + (error instanceof Error ? error.message : '未知错误'))
} finally {
setLoading(false)
}
}
return (
<div
style={{
padding: '1rem',
border: '1px solid var(--theme-elevation-150)',
borderRadius: '4px',
backgroundColor: 'var(--theme-elevation-50)',
marginBottom: '1rem',
}}
>
<div style={{ marginBottom: '0.75rem' }}>
<h4 style={{ margin: '0 0 0.5rem 0', fontSize: '0.875rem', fontWeight: 600 }}>
📊
</h4>
<p style={{ margin: 0, fontSize: '0.8125rem', color: 'var(--theme-elevation-600)' }}>
Medusa
</p>
</div>
<Button
onClick={handleRefresh}
disabled={loading}
buttonStyle="primary"
size="small"
>
{loading ? '同步中...' : '🔄 刷新订单计数'}
</Button>
{message && (
<div
style={{
padding: '0.75rem',
marginTop: '0.75rem',
borderRadius: '4px',
backgroundColor: message.startsWith('✅')
? 'var(--theme-success-50)'
: message.startsWith('⚠️')
? 'var(--theme-warning-50)'
: 'var(--theme-error-50)',
color: message.startsWith('✅')
? 'var(--theme-success-900)'
: message.startsWith('⚠️')
? 'var(--theme-warning-900)'
: 'var(--theme-error-900)',
fontSize: '0.8125rem',
}}
>
{message}
</div>
)}
<div
style={{
marginTop: '0.75rem',
padding: '0.75rem',
backgroundColor: 'var(--theme-elevation-100)',
borderRadius: '4px',
fontSize: '0.8125rem',
color: 'var(--theme-elevation-600)',
}}
>
<p style={{ margin: '0.25rem 0', fontWeight: 600 }}>💡 </p>
<p style={{ margin: '0.25rem 0' }}>
<strong></strong> Medusa
</p>
<p style={{ margin: '0.25rem 0' }}>
<strong>Fake计数</strong>
</p>
<p style={{ margin: '0.25rem 0' }}>
<strong></strong> = + Fake计数
</p>
</div>
</div>
)
}

View File

@ -0,0 +1,26 @@
'use client'
import { useEffect } from 'react'
import './preorder-product-grid-styler.scss'
/**
*
*
*/
export function PreorderProductGridStyler() {
useEffect(() => {
// 组件加载时添加样式类
const table = document.querySelector('.collection-list--preorder-products')
if (table) {
table.classList.add('preorder-grid-view')
}
return () => {
const table = document.querySelector('.collection-list--preorder-products')
if (table) {
table.classList.remove('preorder-grid-view')
}
}
}, [])
return null // 这是一个纯样式组件,不渲染任何内容
}

View File

@ -0,0 +1,240 @@
// 预购商品网格视图样式
// 将表格转换为卡片网格显示预购进度
.collection-list.collection-list--preorder-products.preorder-grid-view {
// 隐藏表头
thead {
display: none;
}
// 主体使用 Grid 布局
tbody {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 1.5rem;
padding: 1rem 0;
}
// 每个 tr 变成卡片
tr {
display: flex;
flex-direction: column;
border: 1px solid var(--theme-elevation-150);
border-radius: 8px;
padding: 1rem;
background: var(--theme-elevation-0);
transition: all 0.2s ease;
cursor: pointer;
position: relative;
overflow: hidden;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-color: var(--theme-elevation-300);
transform: translateY(-2px);
}
// 选中状态
&.row-selected {
border-color: var(--theme-success-500);
background: var(--theme-success-50);
}
}
// 所有单元格
td {
border: none !important;
padding: 0.25rem 0 !important;
background: transparent !important;
width: 100% !important;
max-width: none !important;
// 隐藏不需要的列
&:not([class*='thumbnail']):not([class*='title']):not([class*='medusaId']):not([class*='status']):not([class*='updatedAt']) {
display: none;
}
}
// 缩略图
td[class*='thumbnail'] {
order: -1;
margin-bottom: 0.75rem;
img {
width: 100%;
height: 200px;
object-fit: cover;
border-radius: 6px;
background: var(--theme-elevation-100);
}
// 如果没有图片显示占位符
&:empty::before {
content: '📦';
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 200px;
font-size: 4rem;
background: var(--theme-elevation-100);
border-radius: 6px;
color: var(--theme-elevation-400);
}
}
// 标题
td[class*='title'] {
font-size: 1rem;
font-weight: 600;
color: var(--theme-elevation-1000);
margin-bottom: 0.5rem;
line-height: 1.4;
// 限制两行超出省略
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
// Medusa ID
td[class*='medusaId'] {
font-size: 0.75rem;
color: var(--theme-elevation-500);
font-family: monospace;
margin-bottom: 0.5rem;
&::before {
content: '🆔 ';
}
}
// 状态标签
td[class*='status'] {
margin: 0.5rem 0;
.pill {
display: inline-flex;
align-items: center;
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 500;
&.pill--published {
background: var(--theme-success-100);
color: var(--theme-success-900);
}
&.pill--draft {
background: var(--theme-warning-100);
color: var(--theme-warning-900);
}
}
}
// 更新时间
td[class*='updatedAt'] {
font-size: 0.75rem;
color: var(--theme-elevation-500);
margin-top: auto;
padding-top: 0.75rem !important;
border-top: 1px solid var(--theme-elevation-100);
&::before {
content: '🕐 ';
}
}
// 复选框单元格
td:first-child {
position: absolute;
top: 0.75rem;
right: 0.75rem;
width: auto !important;
z-index: 10;
input[type='checkbox'] {
width: 20px;
height: 20px;
cursor: pointer;
border: 2px solid var(--theme-elevation-400);
border-radius: 4px;
&:checked {
background: var(--theme-success-500);
border-color: var(--theme-success-500);
}
}
}
// 操作按钮单元格
td:last-child {
position: absolute;
bottom: 0.75rem;
right: 0.75rem;
width: auto !important;
}
}
// 预购进度信息需要通过自定义 Cell 组件添加
.preorder-progress-info {
margin: 0.75rem 0;
padding: 0.75rem;
background: var(--theme-elevation-50);
border-radius: 6px;
border: 1px solid var(--theme-elevation-150);
.progress-label {
display: flex;
justify-content: space-between;
font-size: 0.75rem;
color: var(--theme-elevation-600);
margin-bottom: 0.5rem;
.progress-count {
font-weight: 600;
color: var(--theme-elevation-900);
}
}
.progress-bar {
width: 100%;
height: 8px;
background: var(--theme-elevation-150);
border-radius: 4px;
overflow: hidden;
position: relative;
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--theme-success-500), var(--theme-success-600));
transition: width 0.3s ease;
border-radius: 4px;
}
}
.progress-stats {
display: flex;
gap: 1rem;
margin-top: 0.5rem;
font-size: 0.75rem;
.stat-item {
display: flex;
flex-direction: column;
.stat-label {
color: var(--theme-elevation-500);
margin-bottom: 0.125rem;
}
.stat-value {
font-weight: 600;
color: var(--theme-elevation-900);
}
}
}
}