同步优化
This commit is contained in:
parent
4def0e2c0b
commit
14a2aaced0
|
|
@ -3,36 +3,37 @@ import { ThumbnailField as ThumbnailField_0d2fbe11370060d58b3925e5dbbb79d6 } fro
|
||||||
import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||||
import { RscEntryLexicalField as RscEntryLexicalField_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'
|
import { LexicalDiffComponent as LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||||
import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
import { RelationshipFeatureClient as RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { UploadFeatureClient as UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { BlocksFeatureClient as BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
import { FixedToolbarFeatureClient as FixedToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
import { FixedToolbarFeatureClient as FixedToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
import { UploadFeatureClient as UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
|
||||||
import { BlockquoteFeatureClient as BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
|
||||||
import { RelationshipFeatureClient as RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
|
||||||
import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
|
||||||
import { ChecklistFeatureClient as ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
import { ChecklistFeatureClient as ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { IndentFeatureClient as IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { InlineCodeFeatureClient as InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { BlockquoteFeatureClient as BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { AlignFeatureClient as AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
import { OrderedListFeatureClient as OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
import { OrderedListFeatureClient as OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
import { UnorderedListFeatureClient as UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
import { UnorderedListFeatureClient as UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
import { IndentFeatureClient as IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
|
||||||
import { AlignFeatureClient as AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
|
||||||
import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
|
||||||
import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
|
||||||
import { InlineCodeFeatureClient as InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
|
||||||
import { SuperscriptFeatureClient as SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
|
||||||
import { SubscriptFeatureClient as SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
|
||||||
import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
|
||||||
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
|
||||||
import { BoldFeatureClient as BoldFeatureClient_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 { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
import { RelatedProductsField as RelatedProductsField_f3e26ca26ab1ef52a2ee0f6932180426 } from '../../../components/fields/RelatedProductsField'
|
import { RelatedProductsField as RelatedProductsField_f3e26ca26ab1ef52a2ee0f6932180426 } from '../../../components/fields/RelatedProductsField'
|
||||||
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 { PreorderProgressCell as PreorderProgressCell_67df47753573233f0c83480de687f13b } from '../../../components/cells/PreorderProgressCell'
|
||||||
import { RefreshOrderCountField as RefreshOrderCountField_ef327f0ad449eac595b5e301044c0996 } from '../../../components/fields/RefreshOrderCountField'
|
import { RefreshOrderCountField as RefreshOrderCountField_ef327f0ad449eac595b5e301044c0996 } from '../../../components/fields/RefreshOrderCountField'
|
||||||
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 { PreorderHealthCheckButton as PreorderHealthCheckButton_5c0756e0fa67593931ce171329b92892 } from '../../../components/views/PreorderHealthCheckButton'
|
||||||
import { PreorderProductGridStyler as PreorderProductGridStyler_e7f6f7c2233fc58ae87e992227bb80c5 } from '../../../components/list/PreorderProductGridStyler'
|
import { PreorderProductGridStyler as PreorderProductGridStyler_e7f6f7c2233fc58ae87e992227bb80c5 } from '../../../components/list/PreorderProductGridStyler'
|
||||||
|
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { SubscriptFeatureClient as SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { SuperscriptFeatureClient as SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
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 { RestoreRecommendationsSeedButton as RestoreRecommendationsSeedButton_ebef550e255346daa9e9f2a11698b0da } from '../../../components/seed/RestoreRecommendationsSeedButton'
|
import { RestoreRecommendationsSeedButton as RestoreRecommendationsSeedButton_ebef550e255346daa9e9f2a11698b0da } from '../../../components/seed/RestoreRecommendationsSeedButton'
|
||||||
|
|
@ -45,36 +46,37 @@ export const importMap = {
|
||||||
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell": RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
|
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell": RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||||
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalField": RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e,
|
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalField": RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||||
"@payloadcms/richtext-lexical/rsc#LexicalDiffComponent": LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e,
|
"@payloadcms/richtext-lexical/rsc#LexicalDiffComponent": LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||||
"@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient": HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
"@payloadcms/richtext-lexical/client#RelationshipFeatureClient": RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#UploadFeatureClient": UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#BlocksFeatureClient": BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
"@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient": InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
"@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient": InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
"@payloadcms/richtext-lexical/client#FixedToolbarFeatureClient": FixedToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
"@payloadcms/richtext-lexical/client#FixedToolbarFeatureClient": FixedToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
"@payloadcms/richtext-lexical/client#UploadFeatureClient": UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
|
||||||
"@payloadcms/richtext-lexical/client#BlockquoteFeatureClient": BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
|
||||||
"@payloadcms/richtext-lexical/client#RelationshipFeatureClient": RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
|
||||||
"@payloadcms/richtext-lexical/client#LinkFeatureClient": LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
|
||||||
"@payloadcms/richtext-lexical/client#ChecklistFeatureClient": ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
"@payloadcms/richtext-lexical/client#ChecklistFeatureClient": ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#IndentFeatureClient": IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#InlineCodeFeatureClient": InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient": HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#BlockquoteFeatureClient": BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#AlignFeatureClient": AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#LinkFeatureClient": LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
"@payloadcms/richtext-lexical/client#OrderedListFeatureClient": OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
"@payloadcms/richtext-lexical/client#OrderedListFeatureClient": OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
"@payloadcms/richtext-lexical/client#UnorderedListFeatureClient": UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
"@payloadcms/richtext-lexical/client#UnorderedListFeatureClient": UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
"@payloadcms/richtext-lexical/client#IndentFeatureClient": IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
|
||||||
"@payloadcms/richtext-lexical/client#AlignFeatureClient": AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
|
||||||
"@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
|
||||||
"@payloadcms/richtext-lexical/client#ParagraphFeatureClient": ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
|
||||||
"@payloadcms/richtext-lexical/client#InlineCodeFeatureClient": InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
|
||||||
"@payloadcms/richtext-lexical/client#SuperscriptFeatureClient": SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
|
||||||
"@payloadcms/richtext-lexical/client#SubscriptFeatureClient": SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
|
||||||
"@payloadcms/richtext-lexical/client#StrikethroughFeatureClient": StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
|
||||||
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
|
||||||
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#ParagraphFeatureClient": ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
"/components/fields/RelatedProductsField#RelatedProductsField": RelatedProductsField_f3e26ca26ab1ef52a2ee0f6932180426,
|
"/components/fields/RelatedProductsField#RelatedProductsField": RelatedProductsField_f3e26ca26ab1ef52a2ee0f6932180426,
|
||||||
"/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/cells/PreorderProgressCell#PreorderProgressCell": PreorderProgressCell_67df47753573233f0c83480de687f13b,
|
||||||
"/components/fields/RefreshOrderCountField#RefreshOrderCountField": RefreshOrderCountField_ef327f0ad449eac595b5e301044c0996,
|
"/components/fields/RefreshOrderCountField#RefreshOrderCountField": RefreshOrderCountField_ef327f0ad449eac595b5e301044c0996,
|
||||||
"@payloadcms/richtext-lexical/client#BlocksFeatureClient": BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
|
||||||
"/components/fields/PreorderOrdersField#PreorderOrdersField": PreorderOrdersField_a4aa1b8cbd6dec364a834b059228f43f,
|
"/components/fields/PreorderOrdersField#PreorderOrdersField": PreorderOrdersField_a4aa1b8cbd6dec364a834b059228f43f,
|
||||||
|
"/components/views/PreorderHealthCheckButton#PreorderHealthCheckButton": PreorderHealthCheckButton_5c0756e0fa67593931ce171329b92892,
|
||||||
"/components/list/PreorderProductGridStyler#PreorderProductGridStyler": PreorderProductGridStyler_e7f6f7c2233fc58ae87e992227bb80c5,
|
"/components/list/PreorderProductGridStyler#PreorderProductGridStyler": PreorderProductGridStyler_e7f6f7c2233fc58ae87e992227bb80c5,
|
||||||
|
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#StrikethroughFeatureClient": StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#SubscriptFeatureClient": SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#SuperscriptFeatureClient": SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
"/components/views/AdminPanel#default": default_767734c8b7b095ea28d54c32abcf46e4,
|
"/components/views/AdminPanel#default": default_767734c8b7b095ea28d54c32abcf46e4,
|
||||||
"/components/views/LogsManagerView#default": default_a766ef013722c08f9bb937940272cb5f,
|
"/components/views/LogsManagerView#default": default_a766ef013722c08f9bb937940272cb5f,
|
||||||
"/components/seed/RestoreRecommendationsSeedButton#RestoreRecommendationsSeedButton": RestoreRecommendationsSeedButton_ebef550e255346daa9e9f2a11698b0da,
|
"/components/seed/RestoreRecommendationsSeedButton#RestoreRecommendationsSeedButton": RestoreRecommendationsSeedButton_ebef550e255346daa9e9f2a11698b0da,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
# 数据重置功能说明
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
通过 Admin Settings 中的"数据重置"按钮,一键完成完整的数据重置流程。
|
||||||
|
|
||||||
|
## 功能
|
||||||
|
|
||||||
|
**一键重置所有数据**,包括:
|
||||||
|
1. 清理 Payload CMS 数据(保留用户)
|
||||||
|
2. 清理 Medusa 数据
|
||||||
|
3. 重新导入 Medusa seed 数据
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
1. 登录 Payload CMS: `http://localhost:1145/admin`
|
||||||
|
2. 进入 **系统 → Admin Settings**
|
||||||
|
3. 在"数据管理"区域找到"🔄 数据重置(Payload + Medusa)"
|
||||||
|
4. 点击"🗑️ 重置所有数据"按钮
|
||||||
|
5. 确认操作后等待完成
|
||||||
|
|
||||||
|
## 技术实现
|
||||||
|
|
||||||
|
### Payload CMS 端
|
||||||
|
|
||||||
|
**API 端点:**
|
||||||
|
- `POST /api/admin/reset-data` - 数据重置主控端点
|
||||||
|
|
||||||
|
**UI 组件:**
|
||||||
|
- `ResetDataButton` - 重置按钮组件
|
||||||
|
- `AdminPanel` - 管理面板(包含重置按钮)
|
||||||
|
|
||||||
|
### Medusa 端
|
||||||
|
|
||||||
|
**API 端点:**
|
||||||
|
- `POST /admin/custom/clean` - 清理 Medusa 数据
|
||||||
|
- `POST /admin/custom/seed-pro` - 导入 seed 数据
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
```
|
||||||
|
用户点击按钮
|
||||||
|
↓
|
||||||
|
调用 /api/admin/reset-data
|
||||||
|
↓
|
||||||
|
步骤 1: 清理 Payload 数据(Products, PreorderProducts, Media, Announcements, Articles, Logs)
|
||||||
|
↓
|
||||||
|
步骤 2: 调用 Medusa /admin/custom/clean
|
||||||
|
↓
|
||||||
|
步骤 3: 调用 Medusa /admin/custom/seed-pro
|
||||||
|
↓
|
||||||
|
返回结果和详细信息
|
||||||
|
```
|
||||||
|
|
||||||
|
## 后续操作
|
||||||
|
|
||||||
|
数据重置完成后需要:
|
||||||
|
1. 同步 Medusa 商品到 Payload CMS
|
||||||
|
2. 设置 ProductRecommendations 商品推荐
|
||||||
|
3. 配置 PreorderProducts 的预购设置(fundingGoal, preorderEndDate 等)
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
⚠️ **危险操作** - 此操作不可撤销!
|
||||||
|
- 会删除所有商品、媒体、公告、文章和日志数据
|
||||||
|
- 保留用户账户和系统配置
|
||||||
|
- 整个过程可能需要 2-3 分钟
|
||||||
|
|
||||||
|
## 已移除的文件
|
||||||
|
|
||||||
|
精简脚本后移除了:
|
||||||
|
- `reset-data.bat` - 批处理脚本
|
||||||
|
- `gb-payload/src/scripts/clean-payload.ts` - 清理脚本
|
||||||
|
- `gb-payload/package.json` 中的 `clean` 命令
|
||||||
|
|
||||||
|
现在所有操作通过 Web UI 完成,无需命令行。
|
||||||
|
|
@ -0,0 +1,177 @@
|
||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { getPayload } from 'payload'
|
||||||
|
import config from '@payload-config'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API Route: Reset All Data
|
||||||
|
* POST /api/admin/reset-data
|
||||||
|
*
|
||||||
|
* 执行完整的数据重置流程:
|
||||||
|
* 1. 清理 Payload CMS 数据
|
||||||
|
* 2. 清理 Medusa 数据
|
||||||
|
* 3. 导入 Medusa seed 数据
|
||||||
|
*/
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const MEDUSA_BACKEND_URL = process.env.MEDUSA_BACKEND_URL || 'http://localhost:9000'
|
||||||
|
const results: any = {
|
||||||
|
steps: [],
|
||||||
|
success: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 步骤 1: 清理 Payload 数据 ====================
|
||||||
|
console.log('🧹 [1/3] 开始清理 Payload CMS 数据...')
|
||||||
|
const payloadResult = await cleanPayloadData()
|
||||||
|
results.steps.push({
|
||||||
|
step: 1,
|
||||||
|
name: 'Clean Payload',
|
||||||
|
success: payloadResult.success,
|
||||||
|
deleted: payloadResult.totalDeleted,
|
||||||
|
details: payloadResult.details,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!payloadResult.success) {
|
||||||
|
results.success = false
|
||||||
|
return NextResponse.json(results, { status: 500 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 步骤 2: 清理 Medusa 数据 ====================
|
||||||
|
console.log('🧹 [2/3] 开始清理 Medusa 数据...')
|
||||||
|
try {
|
||||||
|
const cleanResponse = await fetch(`${MEDUSA_BACKEND_URL}/admin/custom/clean`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!cleanResponse.ok) {
|
||||||
|
throw new Error(`Medusa clean failed: ${cleanResponse.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanData = await cleanResponse.json()
|
||||||
|
results.steps.push({
|
||||||
|
step: 2,
|
||||||
|
name: 'Clean Medusa',
|
||||||
|
success: true,
|
||||||
|
details: cleanData,
|
||||||
|
})
|
||||||
|
console.log('✅ Medusa 数据清理完成')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Medusa 清理失败:', error)
|
||||||
|
results.steps.push({
|
||||||
|
step: 2,
|
||||||
|
name: 'Clean Medusa',
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
})
|
||||||
|
results.success = false
|
||||||
|
return NextResponse.json(results, { status: 500 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 步骤 3: Seed Medusa 数据 ====================
|
||||||
|
console.log('🌱 [3/3] 开始导入 Medusa 数据...')
|
||||||
|
try {
|
||||||
|
const seedResponse = await fetch(`${MEDUSA_BACKEND_URL}/admin/custom/seed-pro`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!seedResponse.ok) {
|
||||||
|
throw new Error(`Medusa seed failed: ${seedResponse.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const seedData = await seedResponse.json()
|
||||||
|
results.steps.push({
|
||||||
|
step: 3,
|
||||||
|
name: 'Seed Medusa',
|
||||||
|
success: true,
|
||||||
|
details: seedData,
|
||||||
|
})
|
||||||
|
console.log('✅ Medusa 数据导入完成')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Medusa seed 失败:', error)
|
||||||
|
results.steps.push({
|
||||||
|
step: 3,
|
||||||
|
name: 'Seed Medusa',
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
})
|
||||||
|
results.success = false
|
||||||
|
return NextResponse.json(results, { status: 500 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 完成 ====================
|
||||||
|
console.log('✨ 数据重置完成!')
|
||||||
|
results.message = '数据重置完成!现在可以同步 Medusa 商品到 Payload CMS。'
|
||||||
|
|
||||||
|
return NextResponse.json(results, { status: 200 })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 数据重置失败:', error)
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理 Payload CMS 数据
|
||||||
|
*/
|
||||||
|
async function cleanPayloadData() {
|
||||||
|
const payload = await getPayload({ config })
|
||||||
|
|
||||||
|
const collections = [
|
||||||
|
'media',
|
||||||
|
'products',
|
||||||
|
'preorder-products',
|
||||||
|
'announcements',
|
||||||
|
'articles',
|
||||||
|
'logs'
|
||||||
|
]
|
||||||
|
|
||||||
|
const details: any = {}
|
||||||
|
let totalDeleted = 0
|
||||||
|
|
||||||
|
for (const collection of collections) {
|
||||||
|
try {
|
||||||
|
const result = await payload.find({
|
||||||
|
collection: collection as any,
|
||||||
|
limit: 1000,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.totalDocs > 0) {
|
||||||
|
for (const doc of result.docs) {
|
||||||
|
await payload.delete({
|
||||||
|
collection: collection as any,
|
||||||
|
id: doc.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
details[collection] = result.totalDocs
|
||||||
|
totalDeleted += result.totalDocs
|
||||||
|
console.log(` ✅ ${collection}: 已删除 ${result.totalDocs} 条记录`)
|
||||||
|
} else {
|
||||||
|
details[collection] = 0
|
||||||
|
console.log(` ℹ️ ${collection}: 集合为空`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(` ❌ ${collection}: 清理失败`, error)
|
||||||
|
details[collection] = { error: error instanceof Error ? error.message : 'Unknown error' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ Payload 数据清理完成,共删除 ${totalDeleted} 条记录`)
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
totalDeleted,
|
||||||
|
details,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { getPayload } from 'payload'
|
||||||
|
import config from '@payload-config'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/debug/preorder-products
|
||||||
|
* Debug endpoint to check PreorderProducts data
|
||||||
|
*/
|
||||||
|
export async function GET(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const payload = await getPayload({ config })
|
||||||
|
|
||||||
|
// 直接查询 PreorderProducts 集合
|
||||||
|
const preorderProducts = await payload.find({
|
||||||
|
collection: 'preorder-products',
|
||||||
|
limit: 5,
|
||||||
|
depth: 0, // 不查询关联数据
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('=== PreorderProducts Debug ===')
|
||||||
|
console.log('Total docs:', preorderProducts.totalDocs)
|
||||||
|
console.log('First doc:', JSON.stringify(preorderProducts.docs[0], null, 2))
|
||||||
|
console.log('===============================')
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
total: preorderProducts.totalDocs,
|
||||||
|
products: preorderProducts.docs.map(doc => ({
|
||||||
|
id: doc.id,
|
||||||
|
title: doc.title,
|
||||||
|
medusaId: doc.medusaId,
|
||||||
|
thumbnail: doc.thumbnail,
|
||||||
|
description: doc.description,
|
||||||
|
preorderType: doc.preorderType,
|
||||||
|
fundingGoal: doc.fundingGoal,
|
||||||
|
orderCount: doc.orderCount,
|
||||||
|
preorderStartDate: doc.preorderStartDate,
|
||||||
|
preorderEndDate: doc.preorderEndDate,
|
||||||
|
allFields: Object.keys(doc),
|
||||||
|
})),
|
||||||
|
}, { status: 200 })
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error fetching preorder products:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: 'Failed to fetch preorder products',
|
||||||
|
message: error.message,
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,7 @@ import config from '@payload-config'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/home
|
* GET /api/home
|
||||||
* 获取首页所有数据:公告 + Hero Slider + 产品推荐
|
* 获取首页所有数据:公告 + Hero Slider + 产品推荐(含完整产品信息)
|
||||||
*/
|
*/
|
||||||
export async function GET(req: NextRequest) {
|
export async function GET(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -36,11 +36,14 @@ export async function GET(req: NextRequest) {
|
||||||
slug: 'hero-slider',
|
slug: 'hero-slider',
|
||||||
})
|
})
|
||||||
|
|
||||||
// 获取产品推荐
|
// 获取产品推荐(包含深度查询的产品信息)
|
||||||
const productRecommendations = await payload.findGlobal({
|
const productRecommendations = await payload.findGlobal({
|
||||||
slug: 'product-recommendations',
|
slug: 'product-recommendations',
|
||||||
|
depth: 3, // 增加深度以确保完全获取嵌套数据
|
||||||
})
|
})
|
||||||
|
|
||||||
|
console.log('Raw productRecommendations:', JSON.stringify(productRecommendations, null, 2))
|
||||||
|
|
||||||
// 构建响应数据
|
// 构建响应数据
|
||||||
const response = {
|
const response = {
|
||||||
announcements: announcements.docs.map((announcement) => ({
|
announcements: announcements.docs.map((announcement) => ({
|
||||||
|
|
@ -56,7 +59,74 @@ export async function GET(req: NextRequest) {
|
||||||
},
|
},
|
||||||
productRecommendations: {
|
productRecommendations: {
|
||||||
enabled: productRecommendations.enabled || false,
|
enabled: productRecommendations.enabled || false,
|
||||||
lists: productRecommendations.lists || [],
|
lists: (productRecommendations.lists || []).map((list: any) => ({
|
||||||
|
title: list.title,
|
||||||
|
subtitle: list.subtitle,
|
||||||
|
preorder: list.preorder || false,
|
||||||
|
products: (list.products || []).map((productRef: any) => {
|
||||||
|
const product = productRef.value
|
||||||
|
|
||||||
|
// 调试日志:查看实际接收到的产品数据
|
||||||
|
console.log('=== Product Debug Info ===')
|
||||||
|
console.log('relationTo:', productRef.relationTo)
|
||||||
|
console.log('product object:', product)
|
||||||
|
console.log('Available fields:', Object.keys(product || {}))
|
||||||
|
console.log('=========================')
|
||||||
|
|
||||||
|
// 处理 description 字段
|
||||||
|
// Products 使用 textarea (字符串),PreorderProducts 使用 richText (对象)
|
||||||
|
let description = ''
|
||||||
|
if (typeof product.description === 'string') {
|
||||||
|
description = product.description
|
||||||
|
} else if (product.description && typeof product.description === 'object') {
|
||||||
|
// richText 字段,提取纯文本(简单处理)
|
||||||
|
description = JSON.stringify(product.description)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基础产品信息
|
||||||
|
const baseInfo = {
|
||||||
|
id: product.id,
|
||||||
|
medusaId: product.medusaId,
|
||||||
|
seedId: product.seedId,
|
||||||
|
title: product.title,
|
||||||
|
thumbnail: product.thumbnail,
|
||||||
|
status: product.status,
|
||||||
|
description,
|
||||||
|
minPrice: product.minPrice,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是预购产品,添加预购特有字段
|
||||||
|
if (productRef.relationTo === 'preorder-products') {
|
||||||
|
return {
|
||||||
|
...baseInfo,
|
||||||
|
relationTo: 'preorder-products',
|
||||||
|
preorder: {
|
||||||
|
type: product.preorderType || 'standard',
|
||||||
|
fundingGoal: product.fundingGoal || 0,
|
||||||
|
orderCount: product.orderCount || 0,
|
||||||
|
startDate: product.preorderStartDate,
|
||||||
|
endDate: product.preorderEndDate,
|
||||||
|
// 计算进度百分比
|
||||||
|
progress: product.fundingGoal > 0
|
||||||
|
? Math.min(Math.round((product.orderCount / product.fundingGoal) * 100), 100)
|
||||||
|
: 0,
|
||||||
|
// 计算剩余天数
|
||||||
|
daysLeft: product.preorderEndDate
|
||||||
|
? Math.max(0, Math.ceil((new Date(product.preorderEndDate).getTime() - Date.now()) / (1000 * 60 * 60 * 24)))
|
||||||
|
: null,
|
||||||
|
// 支持者数量(使用 orderCount)
|
||||||
|
backers: product.orderCount || 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 普通产品
|
||||||
|
return {
|
||||||
|
...baseInfo,
|
||||||
|
relationTo: 'products',
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
})),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,188 @@
|
||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { getPayload } from 'payload'
|
||||||
|
import config from '@payload-config'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/preorders/health-check
|
||||||
|
*
|
||||||
|
* 检查所有预购产品的健康状况
|
||||||
|
* 返回详细的预购产品信息和潜在问题
|
||||||
|
*/
|
||||||
|
export async function GET(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const payload = await getPayload({ config })
|
||||||
|
|
||||||
|
// 获取所有预购产品
|
||||||
|
const { docs: products } = await payload.find({
|
||||||
|
collection: 'preorder-products',
|
||||||
|
limit: 1000,
|
||||||
|
depth: 2,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!products || products.length === 0) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
summary: {
|
||||||
|
total: 0,
|
||||||
|
healthy: 0,
|
||||||
|
warnings: 0,
|
||||||
|
errors: 0,
|
||||||
|
},
|
||||||
|
products: [],
|
||||||
|
issues: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查每个产品
|
||||||
|
const productChecks: any[] = []
|
||||||
|
const allIssues: string[] = []
|
||||||
|
|
||||||
|
let healthyCount = 0
|
||||||
|
let warningCount = 0
|
||||||
|
let errorCount = 0
|
||||||
|
|
||||||
|
for (const product of products) {
|
||||||
|
const issues: string[] = []
|
||||||
|
let severity: 'healthy' | 'warning' | 'error' = 'healthy'
|
||||||
|
|
||||||
|
// 检查必要字段
|
||||||
|
if (!product.medusaId) {
|
||||||
|
issues.push('缺少 Medusa ID')
|
||||||
|
severity = 'error'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!product.title) {
|
||||||
|
issues.push('缺少产品标题')
|
||||||
|
severity = 'error'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查预购设置
|
||||||
|
if (product.fundingGoal === undefined || product.fundingGoal === null) {
|
||||||
|
issues.push('未设置众筹目标')
|
||||||
|
severity = severity === 'error' ? 'error' : 'warning'
|
||||||
|
} else if (product.fundingGoal === 0) {
|
||||||
|
issues.push('众筹目标为 0(将使用变体总和)')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查日期
|
||||||
|
if (!product.preorderStartDate) {
|
||||||
|
issues.push('未设置预购开始日期')
|
||||||
|
severity = severity === 'error' ? 'error' : 'warning'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!product.preorderEndDate) {
|
||||||
|
issues.push('未设置预购结束日期')
|
||||||
|
severity = severity === 'error' ? 'error' : 'warning'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查日期逻辑
|
||||||
|
if (product.preorderStartDate && product.preorderEndDate) {
|
||||||
|
const startDate = new Date(product.preorderStartDate)
|
||||||
|
const endDate = new Date(product.preorderEndDate)
|
||||||
|
|
||||||
|
if (startDate >= endDate) {
|
||||||
|
issues.push('预购开始日期晚于或等于结束日期')
|
||||||
|
severity = 'error'
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date()
|
||||||
|
if (endDate < now) {
|
||||||
|
issues.push('预购已结束')
|
||||||
|
} else if (startDate > now) {
|
||||||
|
issues.push('预购尚未开始')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查订单计数
|
||||||
|
const orderCount = parseInt(String(product.orderCount || 0), 10)
|
||||||
|
const fakeOrderCount = parseInt(String(product.fakeOrderCount || 0), 10)
|
||||||
|
const totalDisplayCount = orderCount + fakeOrderCount
|
||||||
|
const fundingGoal = parseInt(String(product.fundingGoal || 0), 10)
|
||||||
|
|
||||||
|
if (fundingGoal > 0) {
|
||||||
|
const completionPercentage = Math.round((totalDisplayCount / fundingGoal) * 100)
|
||||||
|
|
||||||
|
if (completionPercentage >= 100) {
|
||||||
|
issues.push(`已达成目标 (${completionPercentage}%)`)
|
||||||
|
} else if (completionPercentage < 10) {
|
||||||
|
issues.push(`完成度较低 (${completionPercentage}%)`)
|
||||||
|
severity = severity === 'error' ? 'error' : 'warning'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查状态
|
||||||
|
if (product.status !== 'published') {
|
||||||
|
issues.push(`产品状态为: ${product.status}`)
|
||||||
|
severity = severity === 'error' ? 'error' : 'warning'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新统计
|
||||||
|
if (severity === 'error') {
|
||||||
|
errorCount++
|
||||||
|
} else if (severity === 'warning' || issues.length > 0) {
|
||||||
|
warningCount++
|
||||||
|
} else {
|
||||||
|
healthyCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录产品检查结果
|
||||||
|
productChecks.push({
|
||||||
|
id: product.id,
|
||||||
|
title: product.title,
|
||||||
|
medusaId: product.medusaId,
|
||||||
|
seedId: product.seedId,
|
||||||
|
status: product.status,
|
||||||
|
severity,
|
||||||
|
issues,
|
||||||
|
stats: {
|
||||||
|
orderCount,
|
||||||
|
fakeOrderCount,
|
||||||
|
totalDisplayCount,
|
||||||
|
fundingGoal,
|
||||||
|
completionPercentage: fundingGoal > 0
|
||||||
|
? Math.round((totalDisplayCount / fundingGoal) * 100)
|
||||||
|
: 0,
|
||||||
|
},
|
||||||
|
dates: {
|
||||||
|
preorderStartDate: product.preorderStartDate,
|
||||||
|
preorderEndDate: product.preorderEndDate,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 添加到全局问题列表
|
||||||
|
if (issues.length > 0) {
|
||||||
|
allIssues.push(`${product.title}: ${issues.join(', ')}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按严重程度排序
|
||||||
|
productChecks.sort((a, b) => {
|
||||||
|
const severityOrder: { [key: string]: number } = { error: 0, warning: 1, healthy: 2 }
|
||||||
|
return severityOrder[a.severity] - severityOrder[b.severity]
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
summary: {
|
||||||
|
total: products.length,
|
||||||
|
healthy: healthyCount,
|
||||||
|
warnings: warningCount,
|
||||||
|
errors: errorCount,
|
||||||
|
},
|
||||||
|
products: productChecks,
|
||||||
|
issues: allIssues,
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[Health Check API] Error:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to check preorder products health',
|
||||||
|
message: error.message
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,123 +0,0 @@
|
||||||
import { getPayload } from 'payload'
|
|
||||||
import config from '@payload-config'
|
|
||||||
import { NextResponse } from 'next/server'
|
|
||||||
import { getAllMedusaProducts } from '@/lib/medusa'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Batch Sync Selected Products
|
|
||||||
* POST /api/sync/batch-medusa
|
|
||||||
* Body: { ids: string[], collection: 'products' | 'preorder-products', forceUpdate?: boolean }
|
|
||||||
*/
|
|
||||||
export async function POST(request: Request) {
|
|
||||||
try {
|
|
||||||
const body = await request.json()
|
|
||||||
const { ids, collection, forceUpdate = false } = body
|
|
||||||
|
|
||||||
if (!ids || !Array.isArray(ids) || ids.length === 0) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'No product IDs provided' },
|
|
||||||
{ status: 400 },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!collection || !['products', 'preorder-products'].includes(collection)) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'Invalid collection' },
|
|
||||||
{ status: 400 },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = await getPayload({ config })
|
|
||||||
|
|
||||||
// Get all Medusa products once
|
|
||||||
const medusaProducts = await getAllMedusaProducts()
|
|
||||||
const medusaProductMap = new Map(medusaProducts.map(p => [p.id, p]))
|
|
||||||
|
|
||||||
const results = {
|
|
||||||
total: ids.length,
|
|
||||||
success: 0,
|
|
||||||
failed: 0,
|
|
||||||
skipped: 0,
|
|
||||||
details: [] as any[],
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sync each selected product
|
|
||||||
for (const id of ids) {
|
|
||||||
try {
|
|
||||||
const product = await payload.findByID({
|
|
||||||
collection: collection as 'products' | 'preorder-products',
|
|
||||||
id,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!product || !product.medusaId) {
|
|
||||||
results.skipped++
|
|
||||||
results.details.push({
|
|
||||||
id,
|
|
||||||
title: product?.title || 'Unknown',
|
|
||||||
status: 'skipped',
|
|
||||||
reason: 'No Medusa ID',
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const medusaProduct = medusaProductMap.get(product.medusaId)
|
|
||||||
|
|
||||||
if (!medusaProduct) {
|
|
||||||
results.failed++
|
|
||||||
results.details.push({
|
|
||||||
id,
|
|
||||||
medusaId: product.medusaId,
|
|
||||||
title: product.title,
|
|
||||||
status: 'failed',
|
|
||||||
error: 'Product not found in Medusa',
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update basic fields from Medusa
|
|
||||||
const updateData: any = {
|
|
||||||
lastSyncedAt: new Date().toISOString(),
|
|
||||||
}
|
|
||||||
|
|
||||||
if (forceUpdate || !product.title) updateData.title = medusaProduct.title
|
|
||||||
if (forceUpdate || !product.thumbnail) updateData.thumbnail = medusaProduct.thumbnail
|
|
||||||
|
|
||||||
await payload.update({
|
|
||||||
collection: collection as 'products' | 'preorder-products',
|
|
||||||
id,
|
|
||||||
data: updateData,
|
|
||||||
})
|
|
||||||
|
|
||||||
results.success++
|
|
||||||
results.details.push({
|
|
||||||
id,
|
|
||||||
medusaId: product.medusaId,
|
|
||||||
title: product.title,
|
|
||||||
status: 'success',
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
results.failed++
|
|
||||||
results.details.push({
|
|
||||||
id,
|
|
||||||
status: 'failed',
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
message: `Batch sync completed: ${results.success} success, ${results.failed} failed, ${results.skipped} skipped`,
|
|
||||||
results,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[batch-sync-medusa] Error:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error',
|
|
||||||
},
|
|
||||||
{ status: 500 },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,526 +0,0 @@
|
||||||
import { getPayload } from 'payload'
|
|
||||||
import config from '@payload-config'
|
|
||||||
import { NextResponse } from 'next/server'
|
|
||||||
import {
|
|
||||||
getAllMedusaProducts,
|
|
||||||
transformMedusaProductToPayload,
|
|
||||||
getMedusaProductsPaginated,
|
|
||||||
getProductCollection,
|
|
||||||
} from '@/lib/medusa'
|
|
||||||
import { addCorsHeaders, handleCorsOptions } from '@/lib/cors'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理 CORS 预检请求
|
|
||||||
*/
|
|
||||||
export async function OPTIONS(request: Request) {
|
|
||||||
const origin = request.headers.get('origin')
|
|
||||||
return handleCorsOptions(origin)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 通过 seedId 或 medusaId 查找产品(优先使用 seedId)
|
|
||||||
* 会在两个 collection 中查找
|
|
||||||
* @returns { product, collection } 或 null
|
|
||||||
*/
|
|
||||||
async function findProductBySeedIdOrMedusaId(
|
|
||||||
payload: any,
|
|
||||||
seedId: string | null,
|
|
||||||
medusaId: string,
|
|
||||||
): Promise<{ product: any; collection: 'products' | 'preorder-products' } | null> {
|
|
||||||
const collections: Array<'products' | 'preorder-products'> = ['products', 'preorder-products']
|
|
||||||
|
|
||||||
// 优先通过 seedId 查找
|
|
||||||
if (seedId) {
|
|
||||||
for (const collection of collections) {
|
|
||||||
const result = await payload.find({
|
|
||||||
collection,
|
|
||||||
where: {
|
|
||||||
seedId: { equals: seedId },
|
|
||||||
},
|
|
||||||
limit: 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (result.docs[0]) {
|
|
||||||
return { product: result.docs[0], collection }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果通过 seedId 没找到,使用 medusaId 查找
|
|
||||||
for (const collection of collections) {
|
|
||||||
const result = await payload.find({
|
|
||||||
collection,
|
|
||||||
where: {
|
|
||||||
medusaId: { equals: medusaId },
|
|
||||||
},
|
|
||||||
limit: 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (result.docs[0]) {
|
|
||||||
return { product: result.docs[0], collection }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 合并产品数据 - 只更新 Payload 中为空的字段
|
|
||||||
* @param existingProduct Payload 中现有的产品数据
|
|
||||||
* @param newData 从 Medusa 转换来的新数据
|
|
||||||
* @param forceUpdate 是否强制更新所有字段
|
|
||||||
* @returns 合并后的数据
|
|
||||||
*/
|
|
||||||
function mergeProductData(existingProduct: any, newData: any, forceUpdate: boolean): any {
|
|
||||||
if (forceUpdate) {
|
|
||||||
// 强制更新模式:使用所有新数据
|
|
||||||
return { ...newData }
|
|
||||||
}
|
|
||||||
|
|
||||||
// 只填充空值模式:只更新空字段
|
|
||||||
const mergedData: any = {}
|
|
||||||
|
|
||||||
// 总是更新这些字段
|
|
||||||
mergedData.lastSyncedAt = newData.lastSyncedAt
|
|
||||||
mergedData.medusaId = newData.medusaId
|
|
||||||
|
|
||||||
// 如果 seedId 为空,更新它
|
|
||||||
if (!existingProduct.seedId && newData.seedId) {
|
|
||||||
mergedData.seedId = newData.seedId
|
|
||||||
}
|
|
||||||
|
|
||||||
// 只在字段为空时更新基础字段
|
|
||||||
if (!existingProduct.title) {
|
|
||||||
mergedData.title = newData.title
|
|
||||||
}
|
|
||||||
if (!existingProduct.handle) {
|
|
||||||
mergedData.handle = newData.handle
|
|
||||||
}
|
|
||||||
if (!existingProduct.thumbnail) {
|
|
||||||
mergedData.thumbnail = newData.thumbnail
|
|
||||||
}
|
|
||||||
if (!existingProduct.status) {
|
|
||||||
mergedData.status = newData.status
|
|
||||||
}
|
|
||||||
|
|
||||||
// Medusa 属性字段:总是更新(以 Medusa 为准)
|
|
||||||
mergedData.tags = newData.tags
|
|
||||||
mergedData.type = newData.type
|
|
||||||
mergedData.collection = newData.collection
|
|
||||||
mergedData.category = newData.category
|
|
||||||
|
|
||||||
// 物理属性:总是更新
|
|
||||||
mergedData.height = newData.height
|
|
||||||
mergedData.width = newData.width
|
|
||||||
mergedData.length = newData.length
|
|
||||||
mergedData.weight = newData.weight
|
|
||||||
|
|
||||||
// 海关与物流:总是更新
|
|
||||||
mergedData.midCode = newData.midCode
|
|
||||||
mergedData.hsCode = newData.hsCode
|
|
||||||
mergedData.countryOfOrigin = newData.countryOfOrigin
|
|
||||||
|
|
||||||
return mergedData
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 同步 Medusa 商品到 Payload CMS
|
|
||||||
* GET /api/sync/medusa - 同步所有商品
|
|
||||||
* GET /api/sync/medusa?medusaId=prod_xxx - 同步单个商品
|
|
||||||
* GET /api/sync/medusa?medusaId=prod_xxx&collection=preorder-products - 指定目标 collection
|
|
||||||
* GET /api/sync/medusa?forceUpdate=true - 强制更新所有字段
|
|
||||||
*/
|
|
||||||
export async function GET(request: Request) {
|
|
||||||
const origin = request.headers.get('origin')
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 可选的 API Key 验证
|
|
||||||
const authHeader = request.headers.get('authorization')
|
|
||||||
const payloadApiKey = process.env.PAYLOAD_API_KEY
|
|
||||||
|
|
||||||
// 如果配置了 PAYLOAD_API_KEY,则验证请求
|
|
||||||
if (payloadApiKey && authHeader) {
|
|
||||||
const token = authHeader.replace('Bearer ', '')
|
|
||||||
if (token !== payloadApiKey) {
|
|
||||||
const response = NextResponse.json(
|
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
error: 'Invalid API key',
|
|
||||||
},
|
|
||||||
{ status: 401 },
|
|
||||||
)
|
|
||||||
return addCorsHeaders(response, origin)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { searchParams } = new URL(request.url)
|
|
||||||
const medusaId = searchParams.get('medusaId')
|
|
||||||
const collection = searchParams.get('collection') as 'products' | 'preorder-products' | null
|
|
||||||
const forceUpdate = searchParams.get('forceUpdate') === 'true'
|
|
||||||
|
|
||||||
const payload = await getPayload({ config })
|
|
||||||
|
|
||||||
// 同步单个商品
|
|
||||||
if (medusaId) {
|
|
||||||
const result = await syncSingleProductByMedusaId(payload, medusaId, forceUpdate, collection || undefined)
|
|
||||||
const response = NextResponse.json(result)
|
|
||||||
return addCorsHeaders(response, origin)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 同步所有商品
|
|
||||||
const result = await syncAllProducts(payload, forceUpdate)
|
|
||||||
const response = NextResponse.json(result)
|
|
||||||
return addCorsHeaders(response, origin)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Sync API] ❌ 请求处理失败:', error)
|
|
||||||
const response = NextResponse.json(
|
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error',
|
|
||||||
},
|
|
||||||
{ status: 500 },
|
|
||||||
)
|
|
||||||
return addCorsHeaders(response, origin)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 通过 Medusa ID 同步单个商品
|
|
||||||
*/
|
|
||||||
async function syncSingleProductByMedusaId(
|
|
||||||
payload: any,
|
|
||||||
medusaId: string,
|
|
||||||
forceUpdate: boolean,
|
|
||||||
preferredCollection?: 'products' | 'preorder-products'
|
|
||||||
) {
|
|
||||||
console.log(`[Sync API] 🔄 开始同步产品: ${medusaId}`)
|
|
||||||
console.log(`[Sync API] ⚙️ forceUpdate: ${forceUpdate}, preferredCollection: ${preferredCollection || 'auto'}`)
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 从 Medusa 获取商品数据
|
|
||||||
const medusaProducts = await getAllMedusaProducts()
|
|
||||||
const medusaProduct = medusaProducts.find((p) => p.id === medusaId)
|
|
||||||
|
|
||||||
if (!medusaProduct) {
|
|
||||||
console.error(`[Sync API] ❌ Medusa 中未找到商品: ${medusaId}`)
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
action: 'not_found',
|
|
||||||
message: `Medusa 中未找到商品 ${medusaId}`,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[Sync API] ✅ 找到 Medusa 产品: ${medusaProduct.title}`)
|
|
||||||
|
|
||||||
// 确定应该同步到哪个 collection(优先使用传入的 collection,否则自动判断)
|
|
||||||
const targetCollection = preferredCollection || getProductCollection(medusaProduct)
|
|
||||||
console.log(`[Sync API] 🎯 目标 collection: ${targetCollection}${preferredCollection ? ' (指定)' : ' (自动判断)'}`)
|
|
||||||
|
|
||||||
const otherCollection =
|
|
||||||
targetCollection === 'preorder-products' ? 'products' : 'preorder-products'
|
|
||||||
|
|
||||||
// 转换数据
|
|
||||||
const productData = transformMedusaProductToPayload(medusaProduct)
|
|
||||||
const seedId = productData.seedId
|
|
||||||
console.log(`[Sync API] 📦 产品数据: title=${productData.title}, seedId=${seedId}`)
|
|
||||||
|
|
||||||
// 使用新的查找函数(优先 seedId)
|
|
||||||
const found = await findProductBySeedIdOrMedusaId(payload, seedId, medusaId)
|
|
||||||
|
|
||||||
// 如果在另一个 collection 中找到,需要移动
|
|
||||||
if (found && found.collection !== targetCollection) {
|
|
||||||
console.log(`[Sync API] 🚚 需要移动: ${found.collection} -> ${targetCollection}`)
|
|
||||||
|
|
||||||
await payload.delete({
|
|
||||||
collection: found.collection,
|
|
||||||
id: found.product.id,
|
|
||||||
})
|
|
||||||
|
|
||||||
const created = await payload.create({
|
|
||||||
collection: targetCollection,
|
|
||||||
data: productData,
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log(`[Sync API] ✅ 移动成功, 新 ID: ${created.id}`)
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
action: 'moved',
|
|
||||||
message: `商品 ${medusaId} 已从 ${found.collection} 移动到 ${targetCollection}`,
|
|
||||||
productId: created.id,
|
|
||||||
collection: targetCollection,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果在目标 collection 中找到
|
|
||||||
if (found) {
|
|
||||||
console.log(`[Sync API] 🔎 在 ${found.collection} 中找到现有产品, ID: ${found.product.id}`)
|
|
||||||
const existingProduct = found.product
|
|
||||||
|
|
||||||
// 如果存在且不强制更新,只更新空字段
|
|
||||||
if (!forceUpdate) {
|
|
||||||
console.log(`[Sync API] 🔄 模式: 只填充空字段,但 Medusa 属性总是更新`)
|
|
||||||
// 合并数据(只更新空字段,但 Medusa 属性总是更新)
|
|
||||||
const mergedData = mergeProductData(existingProduct, productData, false)
|
|
||||||
|
|
||||||
console.log(`[Sync API] 📝 更新字段(含 Medusa 属性): ${Object.keys(mergedData).join(', ')}`)
|
|
||||||
// 更新(只更新空字段 + Medusa 属性)
|
|
||||||
const updated = await payload.update({
|
|
||||||
collection: targetCollection,
|
|
||||||
id: existingProduct.id,
|
|
||||||
data: mergedData,
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log(`[Sync API] ✅ 部分更新成功`)
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
action: 'updated_partial',
|
|
||||||
message: `商品 ${medusaId} 已部分更新(仅空字段)于 ${targetCollection}`,
|
|
||||||
productId: updated.id,
|
|
||||||
collection: targetCollection,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[Sync API] ⚡ 模式: 强制更新所有字段`)
|
|
||||||
// 强制更新所有字段
|
|
||||||
const updated = await payload.update({
|
|
||||||
collection: targetCollection,
|
|
||||||
id: existingProduct.id,
|
|
||||||
data: productData,
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log(`[Sync API] ✅ 强制更新成功`)
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
action: 'updated',
|
|
||||||
message: `商品 ${medusaId} 已更新于 ${targetCollection}`,
|
|
||||||
productId: updated.id,
|
|
||||||
collection: targetCollection,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[Sync API] ✨ 创建新产品`)
|
|
||||||
// 不存在,创建新商品
|
|
||||||
const created = await payload.create({
|
|
||||||
collection: targetCollection,
|
|
||||||
data: productData,
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log(`[Sync API] ✅ 创建成功, ID: ${created.id}`)
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
action: 'created',
|
|
||||||
message: `商品 ${medusaId} 已创建于 ${targetCollection}`,
|
|
||||||
productId: created.id,
|
|
||||||
collection: targetCollection,
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[Sync API] ❌ 同步失败:`, error)
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
action: 'error',
|
|
||||||
message: `同步商品 ${medusaId} 失败`,
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 同步所有商品
|
|
||||||
*/
|
|
||||||
async function syncAllProducts(payload: any, forceUpdate: boolean) {
|
|
||||||
try {
|
|
||||||
let offset = 0
|
|
||||||
const limit = 100
|
|
||||||
let hasMore = true
|
|
||||||
const results = {
|
|
||||||
total: 0,
|
|
||||||
created: 0,
|
|
||||||
updated: 0,
|
|
||||||
updated_partial: 0,
|
|
||||||
moved: 0,
|
|
||||||
skipped: 0,
|
|
||||||
errors: 0,
|
|
||||||
details: [] as any[],
|
|
||||||
}
|
|
||||||
|
|
||||||
while (hasMore) {
|
|
||||||
// 分页获取 Medusa 商品
|
|
||||||
const { products: medusaProducts, count } = await getMedusaProductsPaginated(offset, limit)
|
|
||||||
|
|
||||||
if (medusaProducts.length === 0) {
|
|
||||||
hasMore = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
results.total += medusaProducts.length
|
|
||||||
|
|
||||||
// 处理每个商品
|
|
||||||
for (const medusaProduct of medusaProducts) {
|
|
||||||
try {
|
|
||||||
// 确定应该同步到哪个 collection
|
|
||||||
const targetCollection = getProductCollection(medusaProduct)
|
|
||||||
|
|
||||||
// 转换数据
|
|
||||||
const productData = transformMedusaProductToPayload(medusaProduct)
|
|
||||||
const seedId = productData.seedId
|
|
||||||
|
|
||||||
// 使用新的查找函数(优先 seedId)
|
|
||||||
const found = await findProductBySeedIdOrMedusaId(
|
|
||||||
payload,
|
|
||||||
seedId,
|
|
||||||
medusaProduct.id,
|
|
||||||
)
|
|
||||||
|
|
||||||
// 如果在错误的 collection 中,移动它
|
|
||||||
if (found && found.collection !== targetCollection) {
|
|
||||||
await payload.delete({
|
|
||||||
collection: found.collection,
|
|
||||||
id: found.product.id,
|
|
||||||
})
|
|
||||||
|
|
||||||
await payload.create({
|
|
||||||
collection: targetCollection,
|
|
||||||
data: productData,
|
|
||||||
})
|
|
||||||
|
|
||||||
results.moved++
|
|
||||||
results.details.push({
|
|
||||||
medusaId: medusaProduct.id,
|
|
||||||
seedId: seedId,
|
|
||||||
title: medusaProduct.title,
|
|
||||||
action: 'moved',
|
|
||||||
from: found.collection,
|
|
||||||
to: targetCollection,
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果在目标 collection 中找到
|
|
||||||
if (found) {
|
|
||||||
const existingProduct = found.product
|
|
||||||
|
|
||||||
// 如果不强制更新,只更新空字段,但 Medusa 属性总是更新
|
|
||||||
if (!forceUpdate) {
|
|
||||||
const mergedData = mergeProductData(existingProduct, productData, false)
|
|
||||||
|
|
||||||
// 更新(只更新空字段 + Medusa 属性)
|
|
||||||
await payload.update({
|
|
||||||
collection: targetCollection,
|
|
||||||
id: existingProduct.id,
|
|
||||||
data: mergedData,
|
|
||||||
})
|
|
||||||
|
|
||||||
results.updated_partial++
|
|
||||||
results.details.push({
|
|
||||||
medusaId: medusaProduct.id,
|
|
||||||
seedId: seedId,
|
|
||||||
title: medusaProduct.title,
|
|
||||||
action: 'updated_partial',
|
|
||||||
collection: targetCollection,
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 强制更新
|
|
||||||
await payload.update({
|
|
||||||
collection: targetCollection,
|
|
||||||
id: existingProduct.id,
|
|
||||||
data: productData,
|
|
||||||
})
|
|
||||||
results.updated++
|
|
||||||
results.details.push({
|
|
||||||
medusaId: medusaProduct.id,
|
|
||||||
seedId: seedId,
|
|
||||||
title: medusaProduct.title,
|
|
||||||
action: 'updated',
|
|
||||||
collection: targetCollection,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// 创建新商品
|
|
||||||
await payload.create({
|
|
||||||
collection: targetCollection,
|
|
||||||
data: productData,
|
|
||||||
})
|
|
||||||
results.created++
|
|
||||||
results.details.push({
|
|
||||||
medusaId: medusaProduct.id,
|
|
||||||
seedId: seedId,
|
|
||||||
title: medusaProduct.title,
|
|
||||||
action: 'created',
|
|
||||||
collection: targetCollection,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error processing product ${medusaProduct.id}:`, error)
|
|
||||||
results.errors++
|
|
||||||
results.details.push({
|
|
||||||
medusaId: medusaProduct.id,
|
|
||||||
title: medusaProduct.title,
|
|
||||||
action: 'error',
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新偏移量
|
|
||||||
offset += limit
|
|
||||||
if (offset >= count) {
|
|
||||||
hasMore = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: `同步完成: ${results.created} 个创建, ${results.updated} 个更新, ${results.updated_partial} 个部分更新, ${results.moved} 个移动, ${results.skipped} 个跳过, ${results.errors} 个错误`,
|
|
||||||
results,
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error syncing all products:', error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /api/sync/medusa
|
|
||||||
* 手动触发同步(与 GET 参数相同)
|
|
||||||
* Body: { medusaId?, collection?, forceUpdate? }
|
|
||||||
*/
|
|
||||||
export async function POST(request: Request) {
|
|
||||||
const origin = request.headers.get('origin')
|
|
||||||
|
|
||||||
try {
|
|
||||||
const payload = await getPayload({ config })
|
|
||||||
|
|
||||||
// 可以在这里添加认证检查
|
|
||||||
// const { user } = await payload.auth({ headers: request.headers })
|
|
||||||
// if (!user) {
|
|
||||||
// const response = NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
||||||
// return addCorsHeaders(response, origin)
|
|
||||||
// }
|
|
||||||
|
|
||||||
const body = await request.json()
|
|
||||||
const { medusaId, collection, forceUpdate = true } = body
|
|
||||||
|
|
||||||
// 同步单个商品
|
|
||||||
if (medusaId) {
|
|
||||||
const result = await syncSingleProductByMedusaId(payload, medusaId, forceUpdate, collection)
|
|
||||||
const response = NextResponse.json(result)
|
|
||||||
return addCorsHeaders(response, origin)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 同步所有商品
|
|
||||||
const result = await syncAllProducts(payload, forceUpdate)
|
|
||||||
const response = NextResponse.json(result)
|
|
||||||
return addCorsHeaders(response, origin)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Sync API] ❌ POST 请求处理失败:', error)
|
|
||||||
const response = NextResponse.json(
|
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error',
|
|
||||||
},
|
|
||||||
{ status: 500 },
|
|
||||||
)
|
|
||||||
return addCorsHeaders(response, origin)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -5,6 +5,7 @@ import {
|
||||||
getAllMedusaProducts,
|
getAllMedusaProducts,
|
||||||
transformMedusaProductToPayload,
|
transformMedusaProductToPayload,
|
||||||
getProductCollection,
|
getProductCollection,
|
||||||
|
convertTextToLexical,
|
||||||
} from '@/lib/medusa'
|
} from '@/lib/medusa'
|
||||||
import { addCorsHeaders, handleCorsOptions } from '@/lib/cors'
|
import { addCorsHeaders, handleCorsOptions } from '@/lib/cors'
|
||||||
|
|
||||||
|
|
@ -30,7 +31,15 @@ export async function POST(request: Request) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const { medusaId, collection: preferredCollection, forceUpdate = false } = body
|
const {
|
||||||
|
medusaId,
|
||||||
|
collection: preferredCollection,
|
||||||
|
forceUpdate = false,
|
||||||
|
// 预购相关字段(从 Medusa subscriber 传递)
|
||||||
|
fundingGoal,
|
||||||
|
preorderStartDate,
|
||||||
|
preorderEndDate,
|
||||||
|
} = body
|
||||||
|
|
||||||
if (!medusaId) {
|
if (!medusaId) {
|
||||||
const response = NextResponse.json(
|
const response = NextResponse.json(
|
||||||
|
|
@ -43,7 +52,12 @@ export async function POST(request: Request) {
|
||||||
return addCorsHeaders(response, origin)
|
return addCorsHeaders(response, origin)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[Sync Product API] 🎯 参数:', { medusaId, preferredCollection, forceUpdate })
|
console.log('[Sync Product API] 🎯 参数:', {
|
||||||
|
medusaId,
|
||||||
|
preferredCollection,
|
||||||
|
forceUpdate,
|
||||||
|
preorderData: { fundingGoal, preorderStartDate, preorderEndDate }
|
||||||
|
})
|
||||||
|
|
||||||
const payload = await getPayload({ config })
|
const payload = await getPayload({ config })
|
||||||
|
|
||||||
|
|
@ -123,9 +137,15 @@ export async function POST(request: Request) {
|
||||||
id: existingProduct.id,
|
id: existingProduct.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 准备移动数据,包括描述转换
|
||||||
|
const moveData: any = { ...productData }
|
||||||
|
if (medusaProduct.description) {
|
||||||
|
moveData.description = convertTextToLexical(medusaProduct.description)
|
||||||
|
}
|
||||||
|
|
||||||
finalProduct = await payload.create({
|
finalProduct = await payload.create({
|
||||||
collection: targetCollection,
|
collection: targetCollection,
|
||||||
data: productData,
|
data: moveData,
|
||||||
})
|
})
|
||||||
|
|
||||||
action = 'moved'
|
action = 'moved'
|
||||||
|
|
@ -148,6 +168,19 @@ export async function POST(request: Request) {
|
||||||
if (!existingProduct.handle) mergedData.handle = productData.handle
|
if (!existingProduct.handle) mergedData.handle = productData.handle
|
||||||
if (!existingProduct.thumbnail) mergedData.thumbnail = productData.thumbnail
|
if (!existingProduct.thumbnail) mergedData.thumbnail = productData.thumbnail
|
||||||
if (!existingProduct.status) mergedData.status = productData.status
|
if (!existingProduct.status) mergedData.status = productData.status
|
||||||
|
// 描述为空时也从 Medusa 导入(转换为富文本格式)
|
||||||
|
if (!existingProduct.description && medusaProduct.description) {
|
||||||
|
(mergedData as any).description = convertTextToLexical(medusaProduct.description)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最低价格和 seedId:总是更新
|
||||||
|
mergedData.seedId = productData.seedId
|
||||||
|
mergedData.startPrice = productData.startPrice
|
||||||
|
|
||||||
|
// 如果是预购产品,fundingGoal 也总是更新
|
||||||
|
if (targetCollection === 'preorder-products' && fundingGoal !== undefined) {
|
||||||
|
(mergedData as any).fundingGoal = fundingGoal
|
||||||
|
}
|
||||||
|
|
||||||
// Medusa 属性字段:总是更新(以 Medusa 为准)
|
// Medusa 属性字段:总是更新(以 Medusa 为准)
|
||||||
mergedData.tags = productData.tags
|
mergedData.tags = productData.tags
|
||||||
|
|
@ -166,6 +199,18 @@ export async function POST(request: Request) {
|
||||||
mergedData.hsCode = productData.hsCode
|
mergedData.hsCode = productData.hsCode
|
||||||
mergedData.countryOfOrigin = productData.countryOfOrigin
|
mergedData.countryOfOrigin = productData.countryOfOrigin
|
||||||
|
|
||||||
|
// 如果是预购产品,添加预购日期字段(只在为空时更新)
|
||||||
|
if (targetCollection === 'preorder-products') {
|
||||||
|
// Preorder Start Date - 只在为空时更新
|
||||||
|
if (!existingProduct.preorderStartDate && preorderStartDate) {
|
||||||
|
(mergedData as any).preorderStartDate = preorderStartDate
|
||||||
|
}
|
||||||
|
// Preorder End Date - 只在为空时更新
|
||||||
|
if (!existingProduct.preorderEndDate && preorderEndDate) {
|
||||||
|
(mergedData as any).preorderEndDate = preorderEndDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`[Sync Product API] 📝 更新字段(含 Medusa 属性): ${Object.keys(mergedData).join(', ')}`)
|
console.log(`[Sync Product API] 📝 更新字段(含 Medusa 属性): ${Object.keys(mergedData).join(', ')}`)
|
||||||
finalProduct = await payload.update({
|
finalProduct = await payload.update({
|
||||||
collection: targetCollection,
|
collection: targetCollection,
|
||||||
|
|
@ -174,12 +219,16 @@ export async function POST(request: Request) {
|
||||||
})
|
})
|
||||||
action = 'updated_partial'
|
action = 'updated_partial'
|
||||||
} else {
|
} else {
|
||||||
// 强制更新所有字段
|
// 强制更新所有字段(包括描述转换为富文本)
|
||||||
console.log(`[Sync Product API] ⚡ 强制更新所有字段`)
|
console.log(`[Sync Product API] ⚡ 强制更新所有字段`)
|
||||||
|
const forceUpdateData: any = { ...productData }
|
||||||
|
if (medusaProduct.description) {
|
||||||
|
forceUpdateData.description = convertTextToLexical(medusaProduct.description)
|
||||||
|
}
|
||||||
finalProduct = await payload.update({
|
finalProduct = await payload.update({
|
||||||
collection: targetCollection,
|
collection: targetCollection,
|
||||||
id: existingProduct.id,
|
id: existingProduct.id,
|
||||||
data: productData,
|
data: forceUpdateData,
|
||||||
})
|
})
|
||||||
action = 'updated'
|
action = 'updated'
|
||||||
}
|
}
|
||||||
|
|
@ -187,9 +236,35 @@ export async function POST(request: Request) {
|
||||||
// 不存在,创建新产品
|
// 不存在,创建新产品
|
||||||
else {
|
else {
|
||||||
console.log(`[Sync Product API] ✨ 创建新产品`)
|
console.log(`[Sync Product API] ✨ 创建新产品`)
|
||||||
|
|
||||||
|
// 如果是预购产品,添加预购相关字段
|
||||||
|
const createData: any = { ...productData }
|
||||||
|
|
||||||
|
// 添加描述(转换为富文本格式)
|
||||||
|
if (medusaProduct.description) {
|
||||||
|
createData.description = convertTextToLexical(medusaProduct.description)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetCollection === 'preorder-products') {
|
||||||
|
if (fundingGoal !== undefined) {
|
||||||
|
createData.fundingGoal = fundingGoal
|
||||||
|
}
|
||||||
|
if (preorderStartDate) {
|
||||||
|
createData.preorderStartDate = preorderStartDate
|
||||||
|
}
|
||||||
|
if (preorderEndDate) {
|
||||||
|
createData.preorderEndDate = preorderEndDate
|
||||||
|
}
|
||||||
|
console.log(`[Sync Product API] 📋 添加预购字段:`, {
|
||||||
|
fundingGoal: createData.fundingGoal,
|
||||||
|
preorderStartDate: createData.preorderStartDate,
|
||||||
|
preorderEndDate: createData.preorderEndDate,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
finalProduct = await payload.create({
|
finalProduct = await payload.create({
|
||||||
collection: targetCollection,
|
collection: targetCollection,
|
||||||
data: productData,
|
data: createData,
|
||||||
})
|
})
|
||||||
action = 'created'
|
action = 'created'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ export const PreorderProducts: CollectionConfig = {
|
||||||
components: {
|
components: {
|
||||||
beforeListTable: [
|
beforeListTable: [
|
||||||
'/components/sync/UnifiedSyncButton#UnifiedSyncButton',
|
'/components/sync/UnifiedSyncButton#UnifiedSyncButton',
|
||||||
|
'/components/views/PreorderHealthCheckButton#PreorderHealthCheckButton',
|
||||||
'/components/list/PreorderProductGridStyler#PreorderProductGridStyler',
|
'/components/list/PreorderProductGridStyler#PreorderProductGridStyler',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,25 @@ import { logAfterChange, logAfterDelete } from '../hooks/logAction'
|
||||||
import { cacheAfterChange, cacheAfterDelete } from '../hooks/cacheInvalidation'
|
import { cacheAfterChange, cacheAfterDelete } from '../hooks/cacheInvalidation'
|
||||||
import { ProductBaseFields, RelatedProductsField, TaobaoLinksField, MedusaAttributesTab } from './base/ProductBase'
|
import { ProductBaseFields, RelatedProductsField, TaobaoLinksField, MedusaAttributesTab } from './base/ProductBase'
|
||||||
import {
|
import {
|
||||||
|
AlignFeature,
|
||||||
|
BlocksFeature,
|
||||||
|
BoldFeature,
|
||||||
|
ChecklistFeature,
|
||||||
HeadingFeature,
|
HeadingFeature,
|
||||||
|
IndentFeature,
|
||||||
|
InlineCodeFeature,
|
||||||
|
ItalicFeature,
|
||||||
lexicalEditor,
|
lexicalEditor,
|
||||||
LinkFeature,
|
LinkFeature,
|
||||||
|
OrderedListFeature,
|
||||||
|
ParagraphFeature,
|
||||||
|
RelationshipFeature,
|
||||||
|
UnorderedListFeature,
|
||||||
UploadFeature,
|
UploadFeature,
|
||||||
FixedToolbarFeature,
|
FixedToolbarFeature,
|
||||||
InlineToolbarFeature,
|
InlineToolbarFeature,
|
||||||
HorizontalRuleFeature,
|
HorizontalRuleFeature,
|
||||||
|
BlockquoteFeature,
|
||||||
} from '@payloadcms/richtext-lexical'
|
} from '@payloadcms/richtext-lexical'
|
||||||
|
|
||||||
export const Products: CollectionConfig = {
|
export const Products: CollectionConfig = {
|
||||||
|
|
@ -44,29 +56,37 @@ export const Products: CollectionConfig = {
|
||||||
label: '📄 商品详情',
|
label: '📄 商品详情',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'content',
|
name: 'description',
|
||||||
type: 'richText',
|
type: 'richText',
|
||||||
admin: {
|
|
||||||
description: '商品详细内容(支持图文混排)',
|
|
||||||
},
|
|
||||||
editor: lexicalEditor({
|
editor: lexicalEditor({
|
||||||
features: ({ defaultFeatures }) => [
|
features: [
|
||||||
...defaultFeatures,
|
ParagraphFeature(),
|
||||||
HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }),
|
HeadingFeature({ enabledHeadingSizes: ['h2', 'h3', 'h4'] }),
|
||||||
LinkFeature({
|
BoldFeature(),
|
||||||
enabledCollections: ['products'],
|
ItalicFeature(),
|
||||||
fields: ({ defaultFields }) => [
|
UnorderedListFeature(),
|
||||||
...defaultFields,
|
OrderedListFeature(),
|
||||||
|
LinkFeature(),
|
||||||
|
AlignFeature(),
|
||||||
|
BlockquoteFeature(),
|
||||||
|
HorizontalRuleFeature(),
|
||||||
|
InlineCodeFeature(),
|
||||||
|
IndentFeature(),
|
||||||
|
ChecklistFeature(),
|
||||||
|
FixedToolbarFeature(),
|
||||||
|
InlineToolbarFeature(),
|
||||||
|
BlocksFeature({
|
||||||
|
blocks: [
|
||||||
{
|
{
|
||||||
name: 'rel',
|
slug: 'image',
|
||||||
label: 'Rel Attribute',
|
imageURL: '/api/media',
|
||||||
type: 'select',
|
fields: [
|
||||||
hasMany: true,
|
{
|
||||||
options: ['noopener', 'noreferrer', 'nofollow'],
|
name: 'caption',
|
||||||
admin: {
|
type: 'text',
|
||||||
description:
|
label: '图片说明',
|
||||||
'The rel attribute defines the relationship between a linked resource and the current document. This is a custom link field.',
|
|
||||||
},
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
|
@ -76,19 +96,19 @@ export const Products: CollectionConfig = {
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'caption',
|
name: 'caption',
|
||||||
type: 'richText',
|
type: 'text',
|
||||||
label: '图片说明',
|
label: '图片说明',
|
||||||
editor: lexicalEditor(),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
FixedToolbarFeature(),
|
RelationshipFeature(),
|
||||||
InlineToolbarFeature(),
|
|
||||||
HorizontalRuleFeature(),
|
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
admin: {
|
||||||
|
description: '商品详细描述(支持富文本编辑)',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,14 @@ export const ProductBaseFields: Field[] = [
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'startPrice',
|
||||||
|
type: 'number',
|
||||||
|
admin: {
|
||||||
|
description: '起始价格(从 Medusa 同步,单位:美分)',
|
||||||
|
readOnly: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'lastSyncedAt',
|
name: 'lastSyncedAt',
|
||||||
type: 'date',
|
type: 'date',
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,10 @@ export const ThumbnailCell = (props: any) => {
|
||||||
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
|
||||||
|
|
||||||
|
// 获取起始价格(已经是美元)
|
||||||
|
const startPrice = rowData?.startPrice
|
||||||
|
const formattedPrice = startPrice ? `$${startPrice.toFixed(2)}` : ''
|
||||||
|
|
||||||
// 优先从 props 中获取 collection 信息(Payload Cell API)
|
// 优先从 props 中获取 collection 信息(Payload Cell API)
|
||||||
let collectionSlug = props.collectionConfig?.slug || props.field?.relationTo || props.collection
|
let collectionSlug = props.collectionConfig?.slug || props.field?.relationTo || props.collection
|
||||||
|
|
||||||
|
|
@ -22,13 +26,30 @@ export const ThumbnailCell = (props: any) => {
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
href={editUrl}
|
href={editUrl}
|
||||||
style={{ display: 'block', width: '100%', height: '200px', textDecoration: 'none' }}
|
style={{ display: 'block', width: '100%', textDecoration: 'none', position: 'relative' }}
|
||||||
>
|
>
|
||||||
|
<div style={{ position: 'relative', width: '100%', height: '200px' }}>
|
||||||
{isImage ? (
|
{isImage ? (
|
||||||
<img src={value} alt="商品缩略图" className="thumbnail-img" />
|
<img src={value} alt="商品缩略图" className="thumbnail-img" />
|
||||||
) : (
|
) : (
|
||||||
<div className="no-image">{value || '无图片'}</div>
|
<div className="no-image">{value || '无图片'}</div>
|
||||||
)}
|
)}
|
||||||
|
{formattedPrice && (
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '8px',
|
||||||
|
right: '8px',
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.75)',
|
||||||
|
color: 'white',
|
||||||
|
padding: '4px 8px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
}}>
|
||||||
|
{formattedPrice}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
'use client'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { Button } from '@payloadcms/ui'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset Data Button
|
||||||
|
* 一键重置所有数据:清理 Payload + 清理 Medusa + Seed Medusa
|
||||||
|
*/
|
||||||
|
export function ResetDataButton({ className }: Props) {
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [message, setMessage] = useState('')
|
||||||
|
const [details, setDetails] = useState<any>(null)
|
||||||
|
|
||||||
|
const handleResetData = async () => {
|
||||||
|
if (!confirm(
|
||||||
|
'⚠️ 危险操作:重置所有数据\n\n' +
|
||||||
|
'此操作将:\n' +
|
||||||
|
'1. 清理所有 Payload CMS 数据(保留用户)\n' +
|
||||||
|
'2. 清理所有 Medusa 数据\n' +
|
||||||
|
'3. 重新导入 Medusa seed 数据\n\n' +
|
||||||
|
'⚠️ 此操作不可撤销!\n\n' +
|
||||||
|
'确认要继续吗?'
|
||||||
|
)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
setMessage('🔄 开始数据重置流程...')
|
||||||
|
setDetails(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/admin/reset-data', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await response.json()
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error || 'Reset failed')
|
||||||
|
}
|
||||||
|
|
||||||
|
setDetails(result)
|
||||||
|
setMessage(
|
||||||
|
'✅ 数据重置完成!\n\n' +
|
||||||
|
'下一步:\n' +
|
||||||
|
'1. 同步 Medusa 商品到 Payload CMS\n' +
|
||||||
|
'2. 设置 ProductRecommendations\n' +
|
||||||
|
'3. 配置 PreorderProducts 的预购设置'
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('数据重置失败:', error)
|
||||||
|
setMessage('❌ 重置失败: ' + (error instanceof Error ? error.message : 'Unknown error'))
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<div style={{ marginBottom: '1rem' }}>
|
||||||
|
<Button
|
||||||
|
onClick={handleResetData}
|
||||||
|
buttonStyle="error"
|
||||||
|
disabled={loading}
|
||||||
|
size="medium"
|
||||||
|
>
|
||||||
|
{loading ? '🔄 重置中...' : '🗑️ 重置所有数据'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{message && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: '1rem',
|
||||||
|
backgroundColor: message.includes('✅') ? '#d4edda' : message.includes('❌') ? '#f8d7da' : '#d1ecf1',
|
||||||
|
border: `1px solid ${message.includes('✅') ? '#c3e6cb' : message.includes('❌') ? '#f5c6cb' : '#bee5eb'}`,
|
||||||
|
borderRadius: '4px',
|
||||||
|
marginTop: '1rem',
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
fontSize: '0.9rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{details && details.steps && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: '1rem',
|
||||||
|
padding: '1rem',
|
||||||
|
backgroundColor: '#f8f9fa',
|
||||||
|
border: '1px solid #dee2e6',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '0.85rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h4 style={{ marginTop: 0, marginBottom: '0.5rem' }}>详细信息:</h4>
|
||||||
|
{details.steps.map((step: any, index: number) => (
|
||||||
|
<div key={index} style={{ marginBottom: '0.5rem' }}>
|
||||||
|
<strong>
|
||||||
|
[{step.step}/3] {step.name}:{' '}
|
||||||
|
</strong>
|
||||||
|
<span style={{ color: step.success ? 'green' : 'red' }}>
|
||||||
|
{step.success ? '✅ 成功' : '❌ 失败'}
|
||||||
|
</span>
|
||||||
|
{step.deleted !== undefined && (
|
||||||
|
<span style={{ marginLeft: '0.5rem' }}>
|
||||||
|
(删除 {step.deleted} 条记录)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { Button } from '@payloadcms/ui'
|
import { Button } from '@payloadcms/ui'
|
||||||
|
import { ResetDataButton } from '../sync/ResetDataButton'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 管理员控制面板
|
* 管理员控制面板
|
||||||
|
|
@ -144,6 +145,33 @@ export default function AdminPanel() {
|
||||||
📦 数据管理
|
📦 数据管理
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
|
{/* 数据重置 */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--theme-elevation-0)',
|
||||||
|
borderRadius: '6px',
|
||||||
|
padding: '1.5rem',
|
||||||
|
marginBottom: '1rem',
|
||||||
|
border: '1px solid var(--theme-elevation-150)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h3 style={{ marginBottom: '0.5rem', fontSize: '1rem', fontWeight: '600' }}>
|
||||||
|
🔄 数据重置(Payload + Medusa)
|
||||||
|
</h3>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
marginBottom: '1rem',
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
color: 'var(--theme-elevation-600)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
一键重置所有数据:清理 Payload CMS → 清理 Medusa → 重新导入 Medusa seed 数据
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ResetDataButton />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 清理数据库 */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'var(--theme-elevation-0)',
|
backgroundColor: 'var(--theme-elevation-0)',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,248 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { Button } from '@payloadcms/ui'
|
||||||
|
|
||||||
|
interface HealthCheckResult {
|
||||||
|
success: boolean
|
||||||
|
timestamp: string
|
||||||
|
summary: {
|
||||||
|
total: number
|
||||||
|
healthy: number
|
||||||
|
warnings: number
|
||||||
|
errors: number
|
||||||
|
}
|
||||||
|
products: Array<{
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
medusaId: string
|
||||||
|
seedId: string
|
||||||
|
status: string
|
||||||
|
severity: 'healthy' | 'warning' | 'error'
|
||||||
|
issues: string[]
|
||||||
|
stats: {
|
||||||
|
orderCount: number
|
||||||
|
fakeOrderCount: number
|
||||||
|
totalDisplayCount: number
|
||||||
|
fundingGoal: number
|
||||||
|
completionPercentage: number
|
||||||
|
}
|
||||||
|
dates: {
|
||||||
|
preorderStartDate: string | null
|
||||||
|
preorderEndDate: string | null
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
issues: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PreorderHealthCheck: React.FC = () => {
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [result, setResult] = useState<HealthCheckResult | null>(null)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const runHealthCheck = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/preorders/health-check')
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`)
|
||||||
|
}
|
||||||
|
const data = await response.json()
|
||||||
|
setResult(data)
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.message || 'Failed to run health check')
|
||||||
|
console.error('Health check error:', err)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 自动运行一次健康检查
|
||||||
|
runHealthCheck()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const getSeverityColor = (severity: string) => {
|
||||||
|
switch (severity) {
|
||||||
|
case 'error':
|
||||||
|
return 'text-red-600 bg-red-50'
|
||||||
|
case 'warning':
|
||||||
|
return 'text-yellow-600 bg-yellow-50'
|
||||||
|
case 'healthy':
|
||||||
|
return 'text-green-600 bg-green-50'
|
||||||
|
default:
|
||||||
|
return 'text-gray-600 bg-gray-50'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSeverityIcon = (severity: string) => {
|
||||||
|
switch (severity) {
|
||||||
|
case 'error':
|
||||||
|
return '❌'
|
||||||
|
case 'warning':
|
||||||
|
return '⚠️'
|
||||||
|
case 'healthy':
|
||||||
|
return '✅'
|
||||||
|
default:
|
||||||
|
return 'ℹ️'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDate = (dateString: string | null) => {
|
||||||
|
if (!dateString) return 'N/A'
|
||||||
|
try {
|
||||||
|
return new Date(dateString).toLocaleDateString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
return dateString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold mb-2">预购产品健康检查</h1>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
检查所有预购产品的配置状态和潜在问题
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={runHealthCheck}
|
||||||
|
disabled={loading}
|
||||||
|
buttonStyle="primary"
|
||||||
|
>
|
||||||
|
{loading ? '检查中...' : '刷新检查'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||||
|
<p className="text-red-800">
|
||||||
|
<strong>错误:</strong> {error}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{result && (
|
||||||
|
<>
|
||||||
|
{/* 概览统计 */}
|
||||||
|
<div className="grid grid-cols-4 gap-4 mb-6">
|
||||||
|
<div className="bg-blue-50 p-4 rounded-lg border border-blue-200">
|
||||||
|
<p className="text-sm text-blue-600 font-medium mb-1">总数</p>
|
||||||
|
<p className="text-3xl font-bold text-blue-700">{result.summary.total}</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-green-50 p-4 rounded-lg border border-green-200">
|
||||||
|
<p className="text-sm text-green-600 font-medium mb-1">健康</p>
|
||||||
|
<p className="text-3xl font-bold text-green-700">{result.summary.healthy}</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-yellow-50 p-4 rounded-lg border border-yellow-200">
|
||||||
|
<p className="text-sm text-yellow-600 font-medium mb-1">警告</p>
|
||||||
|
<p className="text-3xl font-bold text-yellow-700">{result.summary.warnings}</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-red-50 p-4 rounded-lg border border-red-200">
|
||||||
|
<p className="text-sm text-red-600 font-medium mb-1">错误</p>
|
||||||
|
<p className="text-3xl font-bold text-red-700">{result.summary.errors}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 检查时间 */}
|
||||||
|
<p className="text-sm text-gray-500 mb-6">
|
||||||
|
最后检查时间: {new Date(result.timestamp).toLocaleString('zh-CN')}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* 产品列表 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{result.products.map((product) => (
|
||||||
|
<div
|
||||||
|
key={product.id}
|
||||||
|
className={`border rounded-lg p-4 ${
|
||||||
|
product.severity === 'error'
|
||||||
|
? 'border-red-300 bg-red-50'
|
||||||
|
: product.severity === 'warning'
|
||||||
|
? 'border-yellow-300 bg-yellow-50'
|
||||||
|
: 'border-green-300 bg-green-50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between mb-3">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<span className="text-2xl">{getSeverityIcon(product.severity)}</span>
|
||||||
|
<h3 className="text-lg font-semibold">{product.title}</h3>
|
||||||
|
<span
|
||||||
|
className={`px-2 py-1 text-xs rounded-full ${
|
||||||
|
product.status === 'published'
|
||||||
|
? 'bg-green-100 text-green-700'
|
||||||
|
: 'bg-gray-100 text-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{product.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-600 space-y-1">
|
||||||
|
<p>Medusa ID: {product.medusaId}</p>
|
||||||
|
{product.seedId && <p>Seed ID: {product.seedId}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="text-sm text-gray-600">
|
||||||
|
<p>进度: {product.stats.completionPercentage}%</p>
|
||||||
|
<p>
|
||||||
|
{product.stats.totalDisplayCount} / {product.stats.fundingGoal}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 日期信息 */}
|
||||||
|
<div className="flex gap-4 text-sm text-gray-600 mb-3">
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">开始:</span>{' '}
|
||||||
|
{formatDate(product.dates.preorderStartDate)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">结束:</span>{' '}
|
||||||
|
{formatDate(product.dates.preorderEndDate)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 问题列表 */}
|
||||||
|
{product.issues.length > 0 && (
|
||||||
|
<div className="mt-3 pt-3 border-t border-gray-300">
|
||||||
|
<p className="text-sm font-medium mb-2">问题:</p>
|
||||||
|
<ul className="text-sm space-y-1">
|
||||||
|
{product.issues.map((issue, idx) => (
|
||||||
|
<li key={idx} className="flex items-start">
|
||||||
|
<span className="mr-2">•</span>
|
||||||
|
<span>{issue}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{result.products.length === 0 && (
|
||||||
|
<div className="text-center py-12 text-gray-500">
|
||||||
|
<p className="text-lg">没有找到预购产品</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!result && !error && loading && (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
||||||
|
<p className="mt-4 text-gray-600">正在检查预购产品...</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,260 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { Button, Modal } from '@payloadcms/ui'
|
||||||
|
|
||||||
|
interface HealthCheckResult {
|
||||||
|
success: boolean
|
||||||
|
timestamp: string
|
||||||
|
summary: {
|
||||||
|
total: number
|
||||||
|
healthy: number
|
||||||
|
warnings: number
|
||||||
|
errors: number
|
||||||
|
}
|
||||||
|
products: Array<{
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
medusaId: string
|
||||||
|
seedId: string
|
||||||
|
status: string
|
||||||
|
severity: 'healthy' | 'warning' | 'error'
|
||||||
|
issues: string[]
|
||||||
|
stats: {
|
||||||
|
orderCount: number
|
||||||
|
fakeOrderCount: number
|
||||||
|
totalDisplayCount: number
|
||||||
|
fundingGoal: number
|
||||||
|
completionPercentage: number
|
||||||
|
}
|
||||||
|
dates: {
|
||||||
|
preorderStartDate: string | null
|
||||||
|
preorderEndDate: string | null
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
issues: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PreorderHealthCheckButton: React.FC = () => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [result, setResult] = useState<HealthCheckResult | null>(null)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const runHealthCheck = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/preorders/health-check')
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`)
|
||||||
|
}
|
||||||
|
const data = await response.json()
|
||||||
|
setResult(data)
|
||||||
|
setIsOpen(true)
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.message || 'Failed to run health check')
|
||||||
|
console.error('Health check error:', err)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSeverityIcon = (severity: string) => {
|
||||||
|
switch (severity) {
|
||||||
|
case 'error':
|
||||||
|
return '❌'
|
||||||
|
case 'warning':
|
||||||
|
return '⚠️'
|
||||||
|
case 'healthy':
|
||||||
|
return '✅'
|
||||||
|
default:
|
||||||
|
return 'ℹ️'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDate = (dateString: string | null) => {
|
||||||
|
if (!dateString) return 'N/A'
|
||||||
|
try {
|
||||||
|
return new Date(dateString).toLocaleDateString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
return dateString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div style={{ marginBottom: '1rem', display: 'flex', gap: '0.5rem' }}>
|
||||||
|
<Button
|
||||||
|
onClick={runHealthCheck}
|
||||||
|
disabled={loading}
|
||||||
|
buttonStyle="secondary"
|
||||||
|
icon="health"
|
||||||
|
>
|
||||||
|
{loading ? '检查中...' : '🏥 健康检查'}
|
||||||
|
</Button>
|
||||||
|
{error && (
|
||||||
|
<span style={{ color: 'red', marginLeft: '1rem', alignSelf: 'center' }}>
|
||||||
|
错误: {error}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isOpen && result && (
|
||||||
|
<Modal slug="health-check-modal" onClose={() => setIsOpen(false)}>
|
||||||
|
<div style={{ padding: '2rem', maxWidth: '900px' }}>
|
||||||
|
<h2 style={{ marginBottom: '1.5rem', fontSize: '1.5rem', fontWeight: 'bold' }}>
|
||||||
|
预购产品健康检查
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{/* 概览统计 */}
|
||||||
|
<div style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(4, 1fr)',
|
||||||
|
gap: '1rem',
|
||||||
|
marginBottom: '1.5rem'
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
padding: '1rem',
|
||||||
|
backgroundColor: '#EFF6FF',
|
||||||
|
borderRadius: '0.5rem',
|
||||||
|
border: '1px solid #BFDBFE'
|
||||||
|
}}>
|
||||||
|
<p style={{ fontSize: '0.875rem', color: '#2563EB', fontWeight: '500', marginBottom: '0.25rem' }}>总数</p>
|
||||||
|
<p style={{ fontSize: '2rem', fontWeight: 'bold', color: '#1E40AF' }}>{result.summary.total}</p>
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
padding: '1rem',
|
||||||
|
backgroundColor: '#F0FDF4',
|
||||||
|
borderRadius: '0.5rem',
|
||||||
|
border: '1px solid #BBF7D0'
|
||||||
|
}}>
|
||||||
|
<p style={{ fontSize: '0.875rem', color: '#16A34A', fontWeight: '500', marginBottom: '0.25rem' }}>健康</p>
|
||||||
|
<p style={{ fontSize: '2rem', fontWeight: 'bold', color: '#15803D' }}>{result.summary.healthy}</p>
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
padding: '1rem',
|
||||||
|
backgroundColor: '#FEFCE8',
|
||||||
|
borderRadius: '0.5rem',
|
||||||
|
border: '1px solid #FDE047'
|
||||||
|
}}>
|
||||||
|
<p style={{ fontSize: '0.875rem', color: '#CA8A04', fontWeight: '500', marginBottom: '0.25rem' }}>警告</p>
|
||||||
|
<p style={{ fontSize: '2rem', fontWeight: 'bold', color: '#A16207' }}>{result.summary.warnings}</p>
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
padding: '1rem',
|
||||||
|
backgroundColor: '#FEF2F2',
|
||||||
|
borderRadius: '0.5rem',
|
||||||
|
border: '1px solid #FECACA'
|
||||||
|
}}>
|
||||||
|
<p style={{ fontSize: '0.875rem', color: '#DC2626', fontWeight: '500', marginBottom: '0.25rem' }}>错误</p>
|
||||||
|
<p style={{ fontSize: '2rem', fontWeight: 'bold', color: '#B91C1C' }}>{result.summary.errors}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style={{ fontSize: '0.875rem', color: '#6B7280', marginBottom: '1.5rem' }}>
|
||||||
|
检查时间: {new Date(result.timestamp).toLocaleString('zh-CN')}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* 产品列表 */}
|
||||||
|
<div style={{ maxHeight: '500px', overflowY: 'auto' }}>
|
||||||
|
{result.products.map((product) => (
|
||||||
|
<div
|
||||||
|
key={product.id}
|
||||||
|
style={{
|
||||||
|
border: `1px solid ${
|
||||||
|
product.severity === 'error'
|
||||||
|
? '#FCA5A5'
|
||||||
|
: product.severity === 'warning'
|
||||||
|
? '#FCD34D'
|
||||||
|
: '#86EFAC'
|
||||||
|
}`,
|
||||||
|
backgroundColor: product.severity === 'error'
|
||||||
|
? '#FEF2F2'
|
||||||
|
: product.severity === 'warning'
|
||||||
|
? '#FEFCE8'
|
||||||
|
: '#F0FDF4',
|
||||||
|
borderRadius: '0.5rem',
|
||||||
|
padding: '1rem',
|
||||||
|
marginBottom: '1rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.75rem' }}>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.5rem' }}>
|
||||||
|
<span style={{ fontSize: '1.5rem' }}>{getSeverityIcon(product.severity)}</span>
|
||||||
|
<h3 style={{ fontSize: '1.125rem', fontWeight: '600' }}>{product.title}</h3>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
padding: '0.25rem 0.5rem',
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
borderRadius: '9999px',
|
||||||
|
backgroundColor: product.status === 'published' ? '#D1FAE5' : '#F3F4F6',
|
||||||
|
color: product.status === 'published' ? '#065F46' : '#374151',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{product.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '0.875rem', color: '#4B5563' }}>
|
||||||
|
<p>Medusa ID: {product.medusaId}</p>
|
||||||
|
{product.seedId && <p>Seed ID: {product.seedId}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'right', fontSize: '0.875rem', color: '#4B5563' }}>
|
||||||
|
<p>进度: {product.stats.completionPercentage}%</p>
|
||||||
|
<p>
|
||||||
|
{product.stats.totalDisplayCount} / {product.stats.fundingGoal}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: '1rem', fontSize: '0.875rem', color: '#4B5563', marginBottom: '0.75rem' }}>
|
||||||
|
<div>
|
||||||
|
<span style={{ fontWeight: '500' }}>开始:</span>{' '}
|
||||||
|
{formatDate(product.dates.preorderStartDate)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span style={{ fontWeight: '500' }}>结束:</span>{' '}
|
||||||
|
{formatDate(product.dates.preorderEndDate)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{product.issues.length > 0 && (
|
||||||
|
<div style={{ marginTop: '0.75rem', paddingTop: '0.75rem', borderTop: '1px solid #D1D5DB' }}>
|
||||||
|
<p style={{ fontSize: '0.875rem', fontWeight: '500', marginBottom: '0.5rem' }}>问题:</p>
|
||||||
|
<ul style={{ fontSize: '0.875rem', paddingLeft: '1rem' }}>
|
||||||
|
{product.issues.map((issue, idx) => (
|
||||||
|
<li key={idx} style={{ marginBottom: '0.25rem' }}>
|
||||||
|
{issue}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{result.products.length === 0 && (
|
||||||
|
<p style={{ textAlign: 'center', padding: '3rem', color: '#6B7280' }}>
|
||||||
|
没有找到预购产品
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div style={{ marginTop: '1.5rem', textAlign: 'right' }}>
|
||||||
|
<Button onClick={() => setIsOpen(false)} buttonStyle="primary">
|
||||||
|
关闭
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -217,6 +217,47 @@ export function isPreorderProduct(product: MedusaProduct): boolean {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将纯文本转换为 Lexical 富文本格式
|
||||||
|
* 自动处理换行符,将其转换为段落
|
||||||
|
*/
|
||||||
|
export function convertTextToLexical(text: string | null | undefined): any {
|
||||||
|
if (!text) return null
|
||||||
|
|
||||||
|
// 按换行符分割文本,创建多个段落
|
||||||
|
const paragraphs = text.split(/\r?\n/).filter(line => line.trim().length > 0)
|
||||||
|
|
||||||
|
if (paragraphs.length === 0) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
root: {
|
||||||
|
type: 'root',
|
||||||
|
format: '',
|
||||||
|
indent: 0,
|
||||||
|
version: 1,
|
||||||
|
children: paragraphs.map(paragraph => ({
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
detail: 0,
|
||||||
|
format: 0,
|
||||||
|
mode: 'normal',
|
||||||
|
style: '',
|
||||||
|
text: paragraph.trim(),
|
||||||
|
type: 'text',
|
||||||
|
version: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
direction: 'ltr',
|
||||||
|
format: '',
|
||||||
|
indent: 0,
|
||||||
|
type: 'paragraph',
|
||||||
|
version: 1,
|
||||||
|
})),
|
||||||
|
direction: 'ltr',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取产品应该同步到的 collection
|
* 获取产品应该同步到的 collection
|
||||||
*/
|
*/
|
||||||
|
|
@ -248,6 +289,20 @@ export function transformMedusaProductToPayload(product: MedusaProduct) {
|
||||||
// 提取 category(从 categories 数组或 metadata)
|
// 提取 category(从 categories 数组或 metadata)
|
||||||
const category = product.categories?.[0]?.name || product.metadata?.category || null
|
const category = product.categories?.[0]?.name || product.metadata?.category || null
|
||||||
|
|
||||||
|
// 计算起始价格(从所有 variants 的 prices 中找最低价格,并转换为美元)
|
||||||
|
let startPrice: number | null = null
|
||||||
|
if (product.variants && product.variants.length > 0) {
|
||||||
|
const allPrices = product.variants.flatMap(variant =>
|
||||||
|
variant.prices?.map(price => price.amount) || []
|
||||||
|
).filter(price => typeof price === 'number' && price > 0)
|
||||||
|
|
||||||
|
if (allPrices.length > 0) {
|
||||||
|
// 将美分转换为美元(保留两位小数)
|
||||||
|
const minPriceInCents = Math.min(...allPrices)
|
||||||
|
startPrice = Math.round(minPriceInCents) / 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
medusaId: product.id,
|
medusaId: product.id,
|
||||||
seedId: product.metadata?.seed_id || null,
|
seedId: product.metadata?.seed_id || null,
|
||||||
|
|
@ -256,6 +311,7 @@ export function transformMedusaProductToPayload(product: MedusaProduct) {
|
||||||
thumbnail: thumbnailUrl || null,
|
thumbnail: thumbnailUrl || null,
|
||||||
status: 'draft',
|
status: 'draft',
|
||||||
lastSyncedAt: new Date().toISOString(),
|
lastSyncedAt: new Date().toISOString(),
|
||||||
|
startPrice: startPrice,
|
||||||
|
|
||||||
// Medusa 默认属性
|
// Medusa 默认属性
|
||||||
tags: tags,
|
tags: tags,
|
||||||
|
|
|
||||||
|
|
@ -217,14 +217,18 @@ export interface Product {
|
||||||
* 商品封面 URL(支持上传或输入 URL)
|
* 商品封面 URL(支持上传或输入 URL)
|
||||||
*/
|
*/
|
||||||
thumbnail?: string | null;
|
thumbnail?: string | null;
|
||||||
|
/**
|
||||||
|
* 起始价格(从 Medusa 同步,单位:美分)
|
||||||
|
*/
|
||||||
|
startPrice?: number | null;
|
||||||
/**
|
/**
|
||||||
* 最后同步时间
|
* 最后同步时间
|
||||||
*/
|
*/
|
||||||
lastSyncedAt?: string | null;
|
lastSyncedAt?: string | null;
|
||||||
/**
|
/**
|
||||||
* 商品详细内容(支持图文混排)
|
* 商品详细描述(支持富文本编辑)
|
||||||
*/
|
*/
|
||||||
content?: {
|
description?: {
|
||||||
root: {
|
root: {
|
||||||
type: string;
|
type: string;
|
||||||
children: {
|
children: {
|
||||||
|
|
@ -344,6 +348,10 @@ export interface PreorderProduct {
|
||||||
* 商品封面 URL(支持上传或输入 URL)
|
* 商品封面 URL(支持上传或输入 URL)
|
||||||
*/
|
*/
|
||||||
thumbnail?: string | null;
|
thumbnail?: string | null;
|
||||||
|
/**
|
||||||
|
* 起始价格(从 Medusa 同步,单位:美分)
|
||||||
|
*/
|
||||||
|
startPrice?: number | null;
|
||||||
/**
|
/**
|
||||||
* 最后同步时间
|
* 最后同步时间
|
||||||
*/
|
*/
|
||||||
|
|
@ -910,8 +918,9 @@ export interface ProductsSelect<T extends boolean = true> {
|
||||||
seedId?: T;
|
seedId?: T;
|
||||||
title?: T;
|
title?: T;
|
||||||
thumbnail?: T;
|
thumbnail?: T;
|
||||||
|
startPrice?: T;
|
||||||
lastSyncedAt?: T;
|
lastSyncedAt?: T;
|
||||||
content?: T;
|
description?: T;
|
||||||
relatedProducts?: T;
|
relatedProducts?: T;
|
||||||
tags?: T;
|
tags?: T;
|
||||||
type?: T;
|
type?: T;
|
||||||
|
|
@ -946,6 +955,7 @@ export interface PreorderProductsSelect<T extends boolean = true> {
|
||||||
seedId?: T;
|
seedId?: T;
|
||||||
title?: T;
|
title?: T;
|
||||||
thumbnail?: T;
|
thumbnail?: T;
|
||||||
|
startPrice?: T;
|
||||||
lastSyncedAt?: T;
|
lastSyncedAt?: T;
|
||||||
preorderType?: T;
|
preorderType?: T;
|
||||||
fundingGoal?: T;
|
fundingGoal?: T;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue