淘宝按钮填充
This commit is contained in:
parent
b90005038f
commit
c84eef485b
|
|
@ -4,6 +4,10 @@ DATABASE_URL=postgresql://user:password@localhost:5432/database
|
|||
# Payload
|
||||
PAYLOAD_SECRET=YOUR_SECRET_HERE
|
||||
|
||||
# Onebound API(淘宝商品数据)https://open.onebound.cn
|
||||
ONEBOUND_API_KEY=your-onebound-key
|
||||
ONEBOUND_API_SECRET=your-onebound-secret
|
||||
|
||||
# Redis Configuration
|
||||
REDIS_URL=redis://localhost:6379
|
||||
|
||||
|
|
|
|||
|
|
@ -22,8 +22,11 @@ import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0
|
|||
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 { TaobaoSyncButtons as TaobaoSyncButtons_1287e89ff664e3153e7b1d531ac3c868 } from '../../../components/sync/TaobaoSyncButtons'
|
||||
import { TaobaoFetchButton as TaobaoFetchButton_6da2c7669760b5ece28f442df13318c7 } from '../../../components/fields/TaobaoFetchButton'
|
||||
import { TaobaoLinkPreview as TaobaoLinkPreview_44c9439e828c0463191af62d21ad4959 } from '../../../components/fields/TaobaoLinkPreview'
|
||||
import { UnifiedSyncButton as UnifiedSyncButton_fc99b3f144909da232f9fd4ff7269523 } from '../../../components/sync/UnifiedSyncButton'
|
||||
import { TaobaoSyncAllButton as TaobaoSyncAllButton_e831fa632dca24f7a1678e011885f4da } from '../../../components/sync/TaobaoSyncAllButton'
|
||||
import { default as default_c2e3814fe427263135b1f5931c37f6f2 } from '../../../components/list/ProductGridStyler'
|
||||
import { PreorderProgressCell as PreorderProgressCell_67df47753573233f0c83480de687f13b } from '../../../components/cells/PreorderProgressCell'
|
||||
import { RefreshOrderCountField as RefreshOrderCountField_ef327f0ad449eac595b5e301044c0996 } from '../../../components/fields/RefreshOrderCountField'
|
||||
|
|
@ -65,8 +68,11 @@ export const importMap = {
|
|||
"@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ParagraphFeatureClient": ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"/components/fields/RelatedProductsField#RelatedProductsField": RelatedProductsField_f3e26ca26ab1ef52a2ee0f6932180426,
|
||||
"/components/sync/TaobaoSyncButtons#TaobaoSyncButtons": TaobaoSyncButtons_1287e89ff664e3153e7b1d531ac3c868,
|
||||
"/components/fields/TaobaoFetchButton#TaobaoFetchButton": TaobaoFetchButton_6da2c7669760b5ece28f442df13318c7,
|
||||
"/components/fields/TaobaoLinkPreview#TaobaoLinkPreview": TaobaoLinkPreview_44c9439e828c0463191af62d21ad4959,
|
||||
"/components/sync/UnifiedSyncButton#UnifiedSyncButton": UnifiedSyncButton_fc99b3f144909da232f9fd4ff7269523,
|
||||
"/components/sync/TaobaoSyncAllButton#TaobaoSyncAllButton": TaobaoSyncAllButton_e831fa632dca24f7a1678e011885f4da,
|
||||
"/components/list/ProductGridStyler#default": default_c2e3814fe427263135b1f5931c37f6f2,
|
||||
"/components/cells/PreorderProgressCell#PreorderProgressCell": PreorderProgressCell_67df47753573233f0c83480de687f13b,
|
||||
"/components/fields/RefreshOrderCountField#RefreshOrderCountField": RefreshOrderCountField_ef327f0ad449eac595b5e301044c0996,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
import { getPayload } from 'payload'
|
||||
import config from '@payload-config'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { syncProductTaobaoLinks } from '@/lib/taobao'
|
||||
import { addCorsHeaders, handleCorsOptions } from '@/lib/cors'
|
||||
|
||||
export async function OPTIONS(request: Request) {
|
||||
return handleCorsOptions(request.headers.get('origin'))
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/admin/taobao/sync-all
|
||||
* 遍历所有产品,解析淘宝链接并回填 title / thumbnail / price
|
||||
*
|
||||
* Body: { force?: boolean }
|
||||
* 返回: { success, total, updated, skipped, errors[] }
|
||||
*/
|
||||
export async function POST(request: Request) {
|
||||
const origin = request.headers.get('origin')
|
||||
|
||||
try {
|
||||
const body = await request.json().catch(() => ({}))
|
||||
const force: boolean = body.force ?? false
|
||||
|
||||
const payload = await getPayload({ config })
|
||||
const collections = ['products', 'preorder-products'] as const
|
||||
|
||||
let total = 0
|
||||
let updated = 0
|
||||
let skipped = 0
|
||||
const errors: string[] = []
|
||||
|
||||
for (const collection of collections) {
|
||||
let page = 1
|
||||
let hasMore = true
|
||||
|
||||
while (hasMore) {
|
||||
const result = await payload.find({
|
||||
collection,
|
||||
limit: 20,
|
||||
page,
|
||||
pagination: true,
|
||||
})
|
||||
|
||||
for (const product of result.docs) {
|
||||
const links: any[] = (product as any).taobaoLinks || []
|
||||
if (links.length === 0) {
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
|
||||
total++
|
||||
try {
|
||||
const r = await syncProductTaobaoLinks(
|
||||
payload,
|
||||
product.id,
|
||||
collection,
|
||||
force,
|
||||
)
|
||||
if (r.updated) updated++
|
||||
else skipped++
|
||||
} catch (err: any) {
|
||||
errors.push(`${collection}/${product.id}: ${err?.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
hasMore = result.hasNextPage ?? false
|
||||
page++
|
||||
}
|
||||
}
|
||||
|
||||
const message = `共处理 ${total} 个产品,更新 ${updated} 个,跳过 ${skipped} 个${errors.length ? `,${errors.length} 个错误` : ''}`
|
||||
console.log(`[taobao/sync-all] ${message}`)
|
||||
|
||||
return addCorsHeaders(
|
||||
NextResponse.json({
|
||||
success: true,
|
||||
message,
|
||||
total,
|
||||
updated,
|
||||
skipped,
|
||||
errors: errors.length ? errors : undefined,
|
||||
}),
|
||||
origin,
|
||||
)
|
||||
} catch (err: any) {
|
||||
console.error('[taobao/sync-all]', err)
|
||||
return addCorsHeaders(
|
||||
NextResponse.json({ success: false, error: err?.message ?? 'Unknown error' }, { status: 500 }),
|
||||
origin,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import { getPayload } from 'payload'
|
||||
import config from '@payload-config'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { syncProductTaobaoLinks } from '@/lib/taobao'
|
||||
import { addCorsHeaders, handleCorsOptions } from '@/lib/cors'
|
||||
|
||||
export async function OPTIONS(request: Request) {
|
||||
return handleCorsOptions(request.headers.get('origin'))
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/admin/taobao/sync-product
|
||||
* 为指定产品解析淘宝链接并回填 title / thumbnail / price
|
||||
*
|
||||
* Body: {
|
||||
* productId: string
|
||||
* collection: 'products' | 'preorder-products'
|
||||
* force?: boolean // true = 覆盖已有字段;false (默认) = 只填充空字段
|
||||
* }
|
||||
*/
|
||||
export async function POST(request: Request) {
|
||||
const origin = request.headers.get('origin')
|
||||
|
||||
try {
|
||||
const { productId, collection, force = false } = await request.json()
|
||||
|
||||
if (!productId || !collection) {
|
||||
return addCorsHeaders(
|
||||
NextResponse.json({ success: false, error: 'productId 和 collection 必填' }, { status: 400 }),
|
||||
origin,
|
||||
)
|
||||
}
|
||||
|
||||
if (!['products', 'preorder-products'].includes(collection)) {
|
||||
return addCorsHeaders(
|
||||
NextResponse.json({ success: false, error: '无效的 collection' }, { status: 400 }),
|
||||
origin,
|
||||
)
|
||||
}
|
||||
|
||||
const payload = await getPayload({ config })
|
||||
const result = await syncProductTaobaoLinks(payload, productId, collection, force)
|
||||
|
||||
return addCorsHeaders(
|
||||
NextResponse.json({ success: true, ...result }),
|
||||
origin,
|
||||
)
|
||||
} catch (err: any) {
|
||||
console.error('[taobao/sync-product]', err)
|
||||
return addCorsHeaders(
|
||||
NextResponse.json({ success: false, error: err?.message ?? 'Unknown error' }, { status: 500 }),
|
||||
origin,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -85,6 +85,8 @@ export async function POST(request: Request) {
|
|||
// 转换数据
|
||||
const productData = transformMedusaProductToPayload(medusaProduct)
|
||||
const seedId = productData.seedId
|
||||
// 注意:taobaoLinks 在此仅写入原始 URL(不调用 Onebound API)
|
||||
// 标题/封面/价格 需要在 Payload 后台手动点击“更新淘宝信息”按鈕才会解析
|
||||
|
||||
// 查找现有产品(优先通过 seedId)
|
||||
let existingProduct: any = null
|
||||
|
|
@ -161,8 +163,13 @@ export async function POST(request: Request) {
|
|||
mergedData.title = productData.title
|
||||
mergedData.handle = productData.handle
|
||||
mergedData.status = productData.status
|
||||
// thumbnail 只在为空时同步(Payload 编辑优先)
|
||||
// thumbnail 只在为空时同步(Payload 编辑优先;可能来自 Medusa/S3 或淘宝链接首图)
|
||||
if (!existingProduct.thumbnail) mergedData.thumbnail = productData.thumbnail
|
||||
|
||||
// taobaoLinks 只在为空时同步(Payload 编辑优先)
|
||||
if ((!existingProduct.taobaoLinks || existingProduct.taobaoLinks.length === 0) && (productData as any).taobaoLinks) {
|
||||
mergedData.taobaoLinks = (productData as any).taobaoLinks
|
||||
}
|
||||
// description 始终从 Medusa 同步(纯文本,只读字段)
|
||||
mergedData.description = medusaProduct.description || null
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
import { NextResponse } from 'next/server'
|
||||
import { parseTaobaoMeta } from '@/lib/taobao'
|
||||
import { addCorsHeaders, handleCorsOptions } from '@/lib/cors'
|
||||
|
||||
export async function OPTIONS(request: Request) {
|
||||
return handleCorsOptions(request.headers.get('origin'))
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/taobao/parse
|
||||
* Body: { url: string }
|
||||
* Returns: { success: true, title, thumbnail, price }
|
||||
*
|
||||
* 用于前端"解析"按钮和同步流程调用
|
||||
*/
|
||||
export async function POST(request: Request) {
|
||||
const origin = request.headers.get('origin')
|
||||
|
||||
try {
|
||||
const { url } = await request.json()
|
||||
|
||||
if (!url || typeof url !== 'string') {
|
||||
return addCorsHeaders(
|
||||
NextResponse.json({ success: false, error: 'url is required' }, { status: 400 }),
|
||||
origin,
|
||||
)
|
||||
}
|
||||
|
||||
const meta = await parseTaobaoMeta(url)
|
||||
|
||||
return addCorsHeaders(
|
||||
NextResponse.json({ success: true, ...meta }),
|
||||
origin,
|
||||
)
|
||||
} catch (err: any) {
|
||||
return addCorsHeaders(
|
||||
NextResponse.json({ success: false, error: err?.message ?? 'Unknown error' }, { status: 500 }),
|
||||
origin,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -38,6 +38,7 @@ export const PreorderProducts: CollectionConfig = {
|
|||
beforeListTable: [
|
||||
'/components/sync/UnifiedSyncButton#UnifiedSyncButton',
|
||||
'/components/views/PreorderHealthCheckButton#PreorderHealthCheckButton',
|
||||
'/components/sync/TaobaoSyncAllButton#TaobaoSyncAllButton',
|
||||
'/components/list/PreorderProductGridStyler#PreorderProductGridStyler',
|
||||
],
|
||||
},
|
||||
|
|
@ -243,7 +244,18 @@ export const PreorderProducts: CollectionConfig = {
|
|||
MedusaAttributesTab,
|
||||
{
|
||||
label: '🛒 淘宝链接',
|
||||
fields: [TaobaoLinksField],
|
||||
fields: [
|
||||
{
|
||||
type: 'ui',
|
||||
name: 'taobaoSyncButtons',
|
||||
admin: {
|
||||
components: {
|
||||
Field: '/components/sync/TaobaoSyncButtons#TaobaoSyncButtons',
|
||||
},
|
||||
},
|
||||
},
|
||||
TaobaoLinksField,
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import type { CollectionConfig } from 'payload'
|
||||
import { logAfterChange, logAfterDelete } from '../hooks/logAction'
|
||||
import { cacheAfterChange, cacheAfterDelete } from '../hooks/cacheInvalidation'
|
||||
import { ProductBaseFields, RelatedProductsField, TaobaoLinksField, MedusaAttributesTab } from './base/ProductBase'
|
||||
import { ProductBaseFields, RelatedProductsField, MedusaAttributesTab } from './base/ProductBase'
|
||||
import { TaobaoLinksField } from './base/TaobaoLinksField'
|
||||
import {
|
||||
AlignFeature,
|
||||
BlocksFeature,
|
||||
|
|
@ -37,6 +38,7 @@ export const Products: CollectionConfig = {
|
|||
components: {
|
||||
beforeListTable: [
|
||||
'/components/sync/UnifiedSyncButton#UnifiedSyncButton',
|
||||
'/components/sync/TaobaoSyncAllButton#TaobaoSyncAllButton',
|
||||
'/components/list/ProductGridStyler',
|
||||
],
|
||||
},
|
||||
|
|
@ -119,7 +121,18 @@ export const Products: CollectionConfig = {
|
|||
MedusaAttributesTab,
|
||||
{
|
||||
label: '🛒 淘宝链接',
|
||||
fields: [TaobaoLinksField],
|
||||
fields: [
|
||||
{
|
||||
type: 'ui',
|
||||
name: 'taobaoSyncButtons',
|
||||
admin: {
|
||||
components: {
|
||||
Field: '/components/sync/TaobaoSyncButtons#TaobaoSyncButtons',
|
||||
},
|
||||
},
|
||||
},
|
||||
TaobaoLinksField,
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -284,66 +284,5 @@ export const RelatedProductsField: Field = {
|
|||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* 淘宝链接字段配置
|
||||
* 仅后台管理员可见,不通过 API 暴露
|
||||
*/
|
||||
export const TaobaoLinksField: Field = {
|
||||
name: 'taobaoLinks',
|
||||
type: 'array',
|
||||
label: '淘宝采购链接列表',
|
||||
admin: {
|
||||
description: '💡 管理淘宝采购链接(仅后台显示,不通过 API 暴露)',
|
||||
initCollapsed: false,
|
||||
},
|
||||
access: {
|
||||
read: ({ req: { user } }) => !!user,
|
||||
update: ({ req: { user } }) => !!user,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'url',
|
||||
type: 'text',
|
||||
label: '🔗 淘宝链接',
|
||||
required: true,
|
||||
admin: {
|
||||
placeholder: 'https://item.taobao.com/...',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
label: '📝 标题',
|
||||
admin: {
|
||||
placeholder: '链接标题或商品名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'thumbnail',
|
||||
type: 'text',
|
||||
label: '🖼️ 缩略图 URL',
|
||||
admin: {
|
||||
placeholder: 'https://...',
|
||||
description: '淘宝商品图片地址',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'note',
|
||||
type: 'textarea',
|
||||
label: '📄 备注',
|
||||
admin: {
|
||||
placeholder: '其他备注信息...',
|
||||
rows: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'ui',
|
||||
name: 'linkPreview',
|
||||
admin: {
|
||||
components: {
|
||||
Field: '/components/fields/TaobaoLinkPreview#TaobaoLinkPreview',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
// TaobaoLinksField 已移至独立文件,包含自动解析功能
|
||||
export { TaobaoLinksField } from './TaobaoLinksField'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
import type { Field } from 'payload'
|
||||
|
||||
/**
|
||||
* 淘宝采购链接字段
|
||||
*
|
||||
* 功能:
|
||||
* - 储存淘宝采购链接列表(仅后台可见,不通过 API 暴露)
|
||||
* - 每条链接支持自动解析:点击"🔍 自动解析"按钮调用 /api/taobao/parse
|
||||
* 自动填入标题、封面图(thumbnail,字符串 URL)、人民币价格
|
||||
* - 第一条链接的 thumbnail 在同步时可作为产品封面的备用来源
|
||||
*
|
||||
* 数据流向:
|
||||
* Medusa seed → metadata.taobao_links (JSON string[])
|
||||
* Payload sync → taobaoLinks[].url 写入;调用 /api/taobao/parse 解析并填入其余字段
|
||||
* Merging rule → 只在字段为空时同步(Payload 编辑优先)
|
||||
*/
|
||||
export const TaobaoLinksField: Field = {
|
||||
name: 'taobaoLinks',
|
||||
type: 'array',
|
||||
label: '淘宝采购链接列表',
|
||||
admin: {
|
||||
description: '💡 管理淘宝采购链接(仅后台显示,不通过 API 暴露)',
|
||||
initCollapsed: false,
|
||||
},
|
||||
access: {
|
||||
read: ({ req: { user } }) => !!user,
|
||||
update: ({ req: { user } }) => !!user,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'url',
|
||||
type: 'text',
|
||||
label: '🔗 淘宝链接',
|
||||
required: true,
|
||||
admin: {
|
||||
placeholder: 'https://item.taobao.com/item.htm?id=...',
|
||||
},
|
||||
},
|
||||
{
|
||||
// 自动解析按钮 —— 读取 url,填入 title / thumbnail / price
|
||||
type: 'ui',
|
||||
name: 'fetchButton',
|
||||
admin: {
|
||||
components: {
|
||||
Field: '/components/fields/TaobaoFetchButton#TaobaoFetchButton',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
label: '📝 标题',
|
||||
admin: {
|
||||
placeholder: '自动解析或手动填写',
|
||||
description: '淘宝商品标题(解析后自动填入)',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'thumbnail',
|
||||
type: 'text',
|
||||
label: '🖼️ 封面 URL',
|
||||
admin: {
|
||||
placeholder: 'https://...',
|
||||
description: '淘宝商品图片地址(字符串 URL;解析后自动填入;首条链接的封面可作为产品封面备用来源)',
|
||||
components: {
|
||||
Cell: '/components/cells/ThumbnailCell#ThumbnailCell',
|
||||
Field: '/components/fields/ThumbnailField#ThumbnailField',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'price',
|
||||
type: 'number',
|
||||
label: '💴 价格(CNY)',
|
||||
admin: {
|
||||
placeholder: '0.00',
|
||||
description: '淘宝商品人民币价格(解析后自动填入)',
|
||||
step: 0.01,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'note',
|
||||
type: 'textarea',
|
||||
label: '📄 备注',
|
||||
admin: {
|
||||
placeholder: '其他备注信息…',
|
||||
rows: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
// 已有的预览组件(展示缩略图 + 跳转按钮)
|
||||
type: 'ui',
|
||||
name: 'linkPreview',
|
||||
admin: {
|
||||
components: {
|
||||
Field: '/components/fields/TaobaoLinkPreview#TaobaoLinkPreview',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
'use client'
|
||||
|
||||
import { useField, useFormFields } from '@payloadcms/ui'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
/**
|
||||
* 淘宝自动解析按钮
|
||||
* 放置在 taobaoLinks 数组项内,读取当前 url 字段,
|
||||
* 调用 /api/taobao/parse 获取标题 / 封面 / 价格,
|
||||
* 然后自动填入同一数组项的对应字段。
|
||||
*
|
||||
* 使用方式(TaobaoLinksField.ts 的 UI 字段):
|
||||
* { type: 'ui', name: 'fetchButton', admin: { components: { Field: '/components/fields/TaobaoFetchButton#TaobaoFetchButton' } } }
|
||||
*/
|
||||
export const TaobaoFetchButton: React.FC<{ path?: string }> = ({ path = '' }) => {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [message, setMessage] = useState<string | null>(null)
|
||||
|
||||
// path 例如 "taobaoLinks.0.fetchButton",取前缀 "taobaoLinks.0."
|
||||
const prefix = path.replace(/[^.]+$/, '')
|
||||
|
||||
const { value: url } = useField<string>({ path: `${prefix}url` })
|
||||
const { setValue: setTitle } = useField<string>({ path: `${prefix}title` })
|
||||
const { setValue: setThumbnail } = useField<string>({ path: `${prefix}thumbnail` })
|
||||
const { setValue: setPrice } = useField<number>({ path: `${prefix}price` })
|
||||
|
||||
const handleFetch = async () => {
|
||||
if (!url) {
|
||||
setMessage('请先填写淘宝链接')
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
setMessage(null)
|
||||
try {
|
||||
const res = await fetch('/api/taobao/parse', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url }),
|
||||
})
|
||||
const data = await res.json()
|
||||
if (!data.success) throw new Error(data.error || '解析失败')
|
||||
|
||||
if (data.title) setTitle(data.title)
|
||||
if (data.thumbnail) setThumbnail(data.thumbnail)
|
||||
if (data.price != null) setPrice(data.price)
|
||||
|
||||
const filled = [data.title && '标题', data.thumbnail && '封面', data.price != null && '价格']
|
||||
.filter(Boolean)
|
||||
.join('、')
|
||||
setMessage(filled ? `✅ 已填入:${filled}` : '⚠️ 未能解析到内容')
|
||||
} catch (err: any) {
|
||||
setMessage(`❌ ${err?.message ?? '请求失败'}`)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ marginTop: '0.5rem', display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleFetch}
|
||||
disabled={loading || !url}
|
||||
style={{
|
||||
padding: '0.4rem 0.9rem',
|
||||
background: loading ? '#9ca3af' : '#f97316',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: loading || !url ? 'not-allowed' : 'pointer',
|
||||
fontSize: '0.8rem',
|
||||
fontWeight: 500,
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{loading ? '解析中…' : '🔍 自动解析'}
|
||||
</button>
|
||||
{message && (
|
||||
<span style={{ fontSize: '0.78rem', color: message.startsWith('✅') ? '#16a34a' : '#dc2626' }}>
|
||||
{message}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
'use client'
|
||||
|
||||
import React, { useState } from 'react'
|
||||
|
||||
/**
|
||||
* 列表页 — 淘宝全量同步按钮
|
||||
*
|
||||
* 加入 beforeListTable 后显示两个按钮:
|
||||
* 🔄 更新全部淘宝信息 → 仅填充空字段 (force=false)
|
||||
* ⚡ 强制更新全部淘宝信息 → 覆盖所有字段 (force=true,二次确认)
|
||||
*
|
||||
* 依赖 API:POST /api/admin/taobao/sync-all
|
||||
*/
|
||||
export const TaobaoSyncAllButton: React.FC = () => {
|
||||
const [loadingNormal, setLoadingNormal] = useState(false)
|
||||
const [loadingForce, setLoadingForce] = useState(false)
|
||||
const [confirmForce, setConfirmForce] = useState(false)
|
||||
const [message, setMessage] = useState<string | null>(null)
|
||||
|
||||
const run = async (force: boolean) => {
|
||||
const setLoading = force ? setLoadingForce : setLoadingNormal
|
||||
setLoading(true)
|
||||
setMessage(null)
|
||||
setConfirmForce(false)
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/admin/taobao/sync-all', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ force }),
|
||||
})
|
||||
const data = await res.json()
|
||||
|
||||
if (!data.success) throw new Error(data.error || '请求失败')
|
||||
setMessage(`✅ ${data.message}`)
|
||||
} catch (err: any) {
|
||||
setMessage(`❌ ${err?.message ?? '未知错误'}`)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const busy = loadingNormal || loadingForce
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.5rem',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
{/* 更新(非强制) */}
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={() => run(false)}
|
||||
style={{
|
||||
padding: '0.4rem 0.85rem',
|
||||
background: busy ? '#9ca3af' : '#10b981',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: busy ? 'not-allowed' : 'pointer',
|
||||
fontSize: '0.78rem',
|
||||
fontWeight: 500,
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{loadingNormal ? '更新中…' : '🔄 更新全部淘宝'}
|
||||
</button>
|
||||
|
||||
{/* 强制更新(二次确认) */}
|
||||
{!confirmForce ? (
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={() => setConfirmForce(true)}
|
||||
style={{
|
||||
padding: '0.4rem 0.85rem',
|
||||
background: busy ? '#9ca3af' : '#ef4444',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: busy ? 'not-allowed' : 'pointer',
|
||||
fontSize: '0.78rem',
|
||||
fontWeight: 500,
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
⚡ 强制更新全部淘宝
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<span style={{ fontSize: '0.78rem', color: '#dc2626', fontWeight: 600 }}>
|
||||
确认覆盖所有字段?
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={() => run(true)}
|
||||
style={{
|
||||
padding: '0.35rem 0.75rem',
|
||||
background: '#dc2626',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.78rem',
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
{loadingForce ? '更新中…' : '确认'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setConfirmForce(false)}
|
||||
style={{
|
||||
padding: '0.35rem 0.75rem',
|
||||
background: 'transparent',
|
||||
color: 'var(--theme-elevation-600)',
|
||||
border: '1px solid var(--theme-elevation-200)',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.78rem',
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{message && (
|
||||
<span
|
||||
style={{
|
||||
fontSize: '0.78rem',
|
||||
color: message.startsWith('✅') ? '#16a34a' : '#dc2626',
|
||||
}}
|
||||
>
|
||||
{message}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
'use client'
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useDocumentInfo } from '@payloadcms/ui'
|
||||
|
||||
/**
|
||||
* 产品编辑页 — 淘宝信息同步按钮
|
||||
*
|
||||
* 放置在淘宝链接 Tab 顶部(UI 字段),显示两个操作按钮:
|
||||
* 🔄 更新淘宝信息 → 仅填充空字段 (force=false)
|
||||
* ⚡ 强制更新淘宝信息 → 覆盖所有字段 (force=true)
|
||||
*
|
||||
* 依赖 API:POST /api/admin/taobao/sync-product
|
||||
*/
|
||||
export const TaobaoSyncButtons: React.FC = () => {
|
||||
const { id, collectionSlug } = useDocumentInfo()
|
||||
|
||||
const [loadingNormal, setLoadingNormal] = useState(false)
|
||||
const [loadingForce, setLoadingForce] = useState(false)
|
||||
const [message, setMessage] = useState<string | null>(null)
|
||||
|
||||
if (!id) return null // 新建文档时不显示
|
||||
|
||||
const isValidCollection =
|
||||
collectionSlug === 'products' || collectionSlug === 'preorder-products'
|
||||
if (!isValidCollection) return null
|
||||
|
||||
const run = async (force: boolean) => {
|
||||
const setLoading = force ? setLoadingForce : setLoadingNormal
|
||||
setLoading(true)
|
||||
setMessage(null)
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/admin/taobao/sync-product', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ productId: id, collection: collectionSlug, force }),
|
||||
})
|
||||
const data = await res.json()
|
||||
|
||||
if (!data.success) throw new Error(data.error || '请求失败')
|
||||
setMessage(`✅ ${data.message || '完成'}`)
|
||||
|
||||
// 刷新页面以显示更新后的字段值
|
||||
setTimeout(() => window.location.reload(), 1200)
|
||||
} catch (err: any) {
|
||||
setMessage(`❌ ${err?.message ?? '未知错误'}`)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const busy = loadingNormal || loadingForce
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '0.5rem',
|
||||
padding: '0.75rem 1rem',
|
||||
marginBottom: '1rem',
|
||||
background: 'var(--theme-elevation-50)',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid var(--theme-elevation-150)',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: '0.8rem', fontWeight: 600, color: 'var(--theme-elevation-600)' }}>
|
||||
淘宝自动解析
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
|
||||
{/* 更新(非强制) */}
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={() => run(false)}
|
||||
style={{
|
||||
padding: '0.4rem 0.9rem',
|
||||
background: busy ? '#9ca3af' : '#3b82f6',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: busy ? 'not-allowed' : 'pointer',
|
||||
fontSize: '0.8rem',
|
||||
fontWeight: 500,
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{loadingNormal ? '解析中…' : '🔄 更新淘宝信息'}
|
||||
</button>
|
||||
|
||||
{/* 强制全量更新 */}
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={() => run(true)}
|
||||
style={{
|
||||
padding: '0.4rem 0.9rem',
|
||||
background: busy ? '#9ca3af' : '#f97316',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: busy ? 'not-allowed' : 'pointer',
|
||||
fontSize: '0.8rem',
|
||||
fontWeight: 500,
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{loadingForce ? '解析中…' : '⚡ 强制更新淘宝信息'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 说明文字 */}
|
||||
<div style={{ fontSize: '0.73rem', color: 'var(--theme-elevation-450)', lineHeight: 1.5 }}>
|
||||
<strong>🔄 更新</strong>:仅填充空白字段(标题、封面、价格) 
|
||||
<strong>⚡ 强制更新</strong>:覆盖已有字段
|
||||
</div>
|
||||
|
||||
{message && (
|
||||
<div
|
||||
style={{
|
||||
padding: '0.4rem 0.75rem',
|
||||
borderRadius: '4px',
|
||||
background: message.startsWith('✅') ? 'var(--theme-success-50)' : 'var(--theme-error-50)',
|
||||
color: message.startsWith('✅') ? 'var(--theme-success-750)' : 'var(--theme-error-750)',
|
||||
fontSize: '0.8rem',
|
||||
}}
|
||||
>
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -277,6 +277,26 @@ export function transformMedusaProductToPayload(product: MedusaProduct) {
|
|||
thumbnailUrl = product.images[0].url
|
||||
}
|
||||
|
||||
// 解析 metadata.taobao_links(JSON string[] → { url }[])
|
||||
// 标题/封面/价格 在同步到 Payload 时由 route 自动调用 parseTaobaoMeta 填充
|
||||
let taobaoLinks: Array<{ url: string }> = []
|
||||
if (product.metadata?.taobao_links) {
|
||||
try {
|
||||
const parsed = typeof product.metadata.taobao_links === 'string'
|
||||
? JSON.parse(product.metadata.taobao_links)
|
||||
: product.metadata.taobao_links
|
||||
if (Array.isArray(parsed)) {
|
||||
taobaoLinks = (parsed as any[]).map(item =>
|
||||
typeof item === 'string' ? { url: item } : { url: item.url }
|
||||
).filter(item => !!item.url)
|
||||
}
|
||||
} catch {
|
||||
console.warn('[transformMedusaProductToPayload] 解析 taobao_links 失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 如果 Medusa/S3 没有封面,'thumbnail 由同步 route 自动解析淘宝首图后回填'
|
||||
|
||||
// 提取 tags(逗号分隔)
|
||||
const tags = product.tags?.map(tag => tag.value).join(', ') || null
|
||||
|
||||
|
|
@ -332,6 +352,9 @@ export function transformMedusaProductToPayload(product: MedusaProduct) {
|
|||
midCode: product.mid_code || product.metadata?.mid_code || product.metadata?.midCode || null,
|
||||
hsCode: product.hs_code || product.metadata?.hs_code || product.metadata?.hsCode || null,
|
||||
countryOfOrigin: product.origin_country || product.metadata?.country_of_origin || product.metadata?.countryOfOrigin || null,
|
||||
|
||||
// 淘宝链接(仅存 url,其余字段由同步 route 调用 parseTaobaoMeta 填充)
|
||||
taobaoLinks: taobaoLinks.length > 0 ? taobaoLinks : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,176 @@
|
|||
/**
|
||||
* 淘宝商品数据解析 —— 基于 Onebound API
|
||||
* https://open.onebound.cn/help/api/taobao.item_get.html
|
||||
*
|
||||
* 环境变量:
|
||||
* ONEBOUND_API_KEY API Key
|
||||
* ONEBOUND_API_SECRET API 密钥
|
||||
*/
|
||||
|
||||
const ONEBOUND_BASE = 'https://api.onebound.cn/taobao/api_sell/'
|
||||
|
||||
export interface TaobaoMeta {
|
||||
title: string | null
|
||||
thumbnail: string | null
|
||||
price: number | null
|
||||
}
|
||||
|
||||
/**
|
||||
* 从淘宝 URL 中提取商品 ID
|
||||
* 支持格式:
|
||||
* https://item.taobao.com/item.htm?id=123456
|
||||
* https://a.m.taobao.com/i123456.htm
|
||||
* https://detail.tmall.com/item.htm?id=123456
|
||||
*/
|
||||
export function extractTaobaoItemId(url: string): string | null {
|
||||
try {
|
||||
const parsed = new URL(url)
|
||||
const idParam = parsed.searchParams.get('id')
|
||||
if (idParam) return idParam
|
||||
const mobileMatch = parsed.pathname.match(/\/i(\d+)\.htm/)
|
||||
if (mobileMatch) return mobileMatch[1]
|
||||
} catch {
|
||||
const match = url.match(/[?&]id=(\d+)/i) || url.match(/\/i(\d+)\.htm/)
|
||||
if (match) return match[1]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 Onebound taobao.item_get 接口获取商品元信息
|
||||
* 返回 title、thumbnail(主图 URL 字符串)、price(CNY 浮点数)
|
||||
*/
|
||||
export async function parseTaobaoMeta(url: string): Promise<TaobaoMeta> {
|
||||
const empty: TaobaoMeta = { title: null, thumbnail: null, price: null }
|
||||
|
||||
const apiKey = process.env.ONEBOUND_API_KEY
|
||||
const apiSecret = process.env.ONEBOUND_API_SECRET
|
||||
|
||||
if (!apiKey || !apiSecret) {
|
||||
console.warn('[parseTaobaoMeta] ONEBOUND_API_KEY / ONEBOUND_API_SECRET 未配置')
|
||||
return empty
|
||||
}
|
||||
|
||||
const itemId = extractTaobaoItemId(url)
|
||||
if (!itemId) {
|
||||
console.warn(`[parseTaobaoMeta] 无法提取商品 ID:${url}`)
|
||||
return empty
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
key: apiKey,
|
||||
secret: apiSecret,
|
||||
api_name: 'taobao.item_get',
|
||||
num_iid: itemId,
|
||||
lang: 'zh-CN',
|
||||
})
|
||||
|
||||
try {
|
||||
const res = await fetch(`${ONEBOUND_BASE}?${params.toString()}`, {
|
||||
signal: AbortSignal.timeout(15_000),
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
console.warn(`[parseTaobaoMeta] HTTP ${res.status} for item ${itemId}`)
|
||||
return empty
|
||||
}
|
||||
|
||||
const data = await res.json()
|
||||
|
||||
// Onebound 错误响应
|
||||
if (data.error || data.error_response) {
|
||||
console.warn(`[parseTaobaoMeta] API 错误 (${itemId}):`, data.error || data.error_response)
|
||||
return empty
|
||||
}
|
||||
|
||||
const item = data.item
|
||||
if (!item) {
|
||||
console.warn(`[parseTaobaoMeta] 响应中无 item 字段 (${itemId})`)
|
||||
return empty
|
||||
}
|
||||
|
||||
// ── 标题 ──────────────────────────────────────────────────────────────────
|
||||
const title: string | null = item.title?.trim() || null
|
||||
|
||||
// ── 封面(字符串 URL) ─────────────────────────────────────────────────────
|
||||
// 优先级:pic_url → item_imgs[0].url → main_imgs[0]
|
||||
let thumbnail: string | null = null
|
||||
if (item.pic_url) {
|
||||
thumbnail = normalizeImageUrl(item.pic_url)
|
||||
} else if (Array.isArray(item.item_imgs) && item.item_imgs.length > 0) {
|
||||
thumbnail = normalizeImageUrl(item.item_imgs[0]?.url ?? item.item_imgs[0])
|
||||
} else if (Array.isArray(item.main_imgs) && item.main_imgs.length > 0) {
|
||||
thumbnail = normalizeImageUrl(item.main_imgs[0])
|
||||
}
|
||||
|
||||
// ── 价格(CNY) ────────────────────────────────────────────────────────────
|
||||
const rawPrice = item.price ?? item.original_price ?? null
|
||||
const price = rawPrice != null ? parseFloat(String(rawPrice)) : null
|
||||
|
||||
console.log(
|
||||
`[parseTaobaoMeta] ✅ (${itemId}) title=${title} thumbnail=${thumbnail ? '✓' : '✗'} price=${price}`,
|
||||
)
|
||||
return { title, thumbnail, price }
|
||||
} catch (err: any) {
|
||||
console.warn(`[parseTaobaoMeta] 请求失败 (${itemId}): ${err?.message}`)
|
||||
return empty
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对单个 Payload 产品的 taobaoLinks 进行解析并回写
|
||||
* @param payload Payload 实例
|
||||
* @param productId 产品 ID
|
||||
* @param collection 集合名('products' | 'preorder-products')
|
||||
* @param force true → 覆盖已有字段;false → 只填充空字段
|
||||
*/
|
||||
export async function syncProductTaobaoLinks(
|
||||
payload: any,
|
||||
productId: string,
|
||||
collection: 'products' | 'preorder-products',
|
||||
force: boolean,
|
||||
): Promise<{ updated: boolean; message: string; linksCount?: number }> {
|
||||
const product = await payload.findByID({ collection, id: productId })
|
||||
if (!product) return { updated: false, message: `产品 ${productId} 未找到` }
|
||||
|
||||
const taobaoLinks: any[] = product.taobaoLinks || []
|
||||
if (taobaoLinks.length === 0) return { updated: false, message: '没有淘宝链接' }
|
||||
|
||||
// 对每条链接调用 Onebound API
|
||||
const updatedLinks = await Promise.all(
|
||||
taobaoLinks.map(async (link: any) => {
|
||||
if (!link.url) return link
|
||||
// 非强制模式:所有字段已有值时跳过
|
||||
if (!force && link.title && link.thumbnail && link.price != null) return link
|
||||
|
||||
const meta = await parseTaobaoMeta(link.url)
|
||||
return {
|
||||
...link,
|
||||
title: force ? (meta.title ?? link.title) : (link.title || meta.title),
|
||||
thumbnail: force ? (meta.thumbnail ?? link.thumbnail) : (link.thumbnail || meta.thumbnail),
|
||||
price: force
|
||||
? (meta.price !== null ? meta.price : link.price)
|
||||
: (link.price != null ? link.price : meta.price),
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
const updateData: any = { taobaoLinks: updatedLinks }
|
||||
|
||||
// 回填产品封面(空时或强制覆盖)
|
||||
if (!product.thumbnail || force) {
|
||||
const firstThumb = updatedLinks.find((l) => l.thumbnail)?.thumbnail
|
||||
if (firstThumb) updateData.thumbnail = firstThumb
|
||||
}
|
||||
|
||||
await payload.update({ collection, id: productId, data: updateData })
|
||||
return { updated: true, message: `已更新 ${updatedLinks.length} 条链接`, linksCount: updatedLinks.length }
|
||||
}
|
||||
|
||||
/** 确保图片 URL 包含协议 */
|
||||
function normalizeImageUrl(url: string | null | undefined): string | null {
|
||||
if (!url) return null
|
||||
const s = String(url).trim()
|
||||
if (s.startsWith('//')) return `https:${s}`
|
||||
return s
|
||||
}
|
||||
|
|
@ -322,11 +322,18 @@ export interface Product {
|
|||
taobaoLinks?:
|
||||
| {
|
||||
url: string;
|
||||
/**
|
||||
* 淘宝商品标题(解析后自动填入)
|
||||
*/
|
||||
title?: string | null;
|
||||
/**
|
||||
* 淘宝商品图片地址
|
||||
* 淘宝商品图片地址(字符串 URL;解析后自动填入;首条链接的封面可作为产品封面备用来源)
|
||||
*/
|
||||
thumbnail?: string | null;
|
||||
/**
|
||||
* 淘宝商品人民币价格(解析后自动填入)
|
||||
*/
|
||||
price?: number | null;
|
||||
note?: string | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
|
|
@ -485,11 +492,18 @@ export interface PreorderProduct {
|
|||
taobaoLinks?:
|
||||
| {
|
||||
url: string;
|
||||
/**
|
||||
* 淘宝商品标题(解析后自动填入)
|
||||
*/
|
||||
title?: string | null;
|
||||
/**
|
||||
* 淘宝商品图片地址
|
||||
* 淘宝商品图片地址(字符串 URL;解析后自动填入;首条链接的封面可作为产品封面备用来源)
|
||||
*/
|
||||
thumbnail?: string | null;
|
||||
/**
|
||||
* 淘宝商品人民币价格(解析后自动填入)
|
||||
*/
|
||||
price?: number | null;
|
||||
note?: string | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
|
|
@ -1068,6 +1082,7 @@ export interface ProductsSelect<T extends boolean = true> {
|
|||
url?: T;
|
||||
title?: T;
|
||||
thumbnail?: T;
|
||||
price?: T;
|
||||
note?: T;
|
||||
id?: T;
|
||||
};
|
||||
|
|
@ -1113,6 +1128,7 @@ export interface PreorderProductsSelect<T extends boolean = true> {
|
|||
url?: T;
|
||||
title?: T;
|
||||
thumbnail?: T;
|
||||
price?: T;
|
||||
note?: T;
|
||||
id?: T;
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue