This commit is contained in:
parent
249423d73d
commit
07d1c2274b
|
|
@ -1,46 +0,0 @@
|
|||
import { getPayload } from 'payload'
|
||||
import config from '@payload-config'
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
/**
|
||||
* 修复数据库字段类型
|
||||
* GET /api/fix-database
|
||||
*/
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const payload = await getPayload({ config })
|
||||
const db = payload.db
|
||||
|
||||
console.log('🔧 开始修复数据库字段类型...')
|
||||
|
||||
// 修复 products 表的 thumbnail 字段
|
||||
await db.execute({
|
||||
raw: `
|
||||
ALTER TABLE "products"
|
||||
ALTER COLUMN "thumbnail" TYPE varchar
|
||||
USING CASE
|
||||
WHEN "thumbnail" IS NULL THEN NULL
|
||||
ELSE "thumbnail"::varchar
|
||||
END;
|
||||
`,
|
||||
})
|
||||
|
||||
console.log('✅ Products.thumbnail 字段已修复为 varchar 类型')
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '数据库字段类型修复成功!',
|
||||
fixes: ['products.thumbnail: integer → varchar'],
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('❌ 修复数据库出错:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
message: '数据库修复失败,请查看控制台日志',
|
||||
},
|
||||
{ status: 500 },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
'use client'
|
||||
import type { SelectFieldClientComponent } from 'payload'
|
||||
|
||||
// 隐藏字段(因为在ThumbnailAndStatusField中已经显示和编辑)
|
||||
export const HiddenField: SelectFieldClientComponent = () => {
|
||||
return null
|
||||
}
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
'use client'
|
||||
import { useFormFields, useField } from '@payloadcms/ui'
|
||||
import type { TextFieldClientComponent } from 'payload'
|
||||
|
||||
// 并排显示缩略图和状态(带状态选择器)
|
||||
export const ThumbnailAndStatusField: TextFieldClientComponent = ({ path }) => {
|
||||
// 获取thumbnail值
|
||||
const fields = useFormFields(([fields]) => fields)
|
||||
const thumbnail = fields.thumbnail?.value
|
||||
|
||||
// 获取status字段的值和setter
|
||||
const { value: status, setValue: setStatus } = useField({ path: 'status' })
|
||||
|
||||
const isImage = typeof thumbnail === 'string' && thumbnail.match(/^https?:\/\/.+/)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>商品缩略图和状态</label>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 16 }}>
|
||||
<div
|
||||
style={{
|
||||
width: 100,
|
||||
height: 100,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 8,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{isImage ? (
|
||||
<img
|
||||
src={thumbnail}
|
||||
alt="商品缩略图"
|
||||
style={{ maxWidth: '100%', maxHeight: '100%', borderRadius: 6 }}
|
||||
/>
|
||||
) : (
|
||||
<span style={{ color: '#bbb' }}>无图片</span>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<label style={{ display: 'block', marginBottom: 4, fontSize: 14 }}>状态</label>
|
||||
<select
|
||||
value={(status as string) || 'draft'}
|
||||
onChange={(e) => setStatus(e.target.value)}
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
borderRadius: 4,
|
||||
fontSize: 14,
|
||||
minWidth: 120,
|
||||
}}
|
||||
>
|
||||
<option value="draft">草稿</option>
|
||||
<option value="published">已发布</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
|
||||
/**
|
||||
* 管理员面板导航链接
|
||||
*/
|
||||
export function AdminPanelNavLink() {
|
||||
const pathname = usePathname()
|
||||
const isActive = pathname === '/admin/admin-panel'
|
||||
|
||||
return (
|
||||
<Link
|
||||
href="/admin/admin-panel"
|
||||
style={{
|
||||
display: 'block',
|
||||
padding: '0.75rem 1rem',
|
||||
textDecoration: 'none',
|
||||
color: isActive ? 'var(--theme-text)' : 'var(--theme-elevation-800)',
|
||||
backgroundColor: isActive ? 'var(--theme-elevation-100)' : 'transparent',
|
||||
borderRadius: '4px',
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isActive) {
|
||||
e.currentTarget.style.backgroundColor = 'var(--theme-elevation-50)'
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!isActive) {
|
||||
e.currentTarget.style.backgroundColor = 'transparent'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||
<span style={{ fontSize: '1.25rem' }}>🛠️</span>
|
||||
<span style={{ fontSize: '0.875rem', fontWeight: isActive ? '600' : '400' }}>
|
||||
管理员面板
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
'use client'
|
||||
import { useState } from 'react'
|
||||
import { Button } from '@payloadcms/ui'
|
||||
|
||||
/**
|
||||
* 清理数据库按钮
|
||||
* 用于清除 Products, Announcements, Articles 的所有数据
|
||||
*/
|
||||
export function ClearDataButton() {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [message, setMessage] = useState('')
|
||||
const [showConfirm, setShowConfirm] = useState(false)
|
||||
|
||||
const handleClearData = () => {
|
||||
setShowConfirm(true)
|
||||
setMessage('')
|
||||
}
|
||||
|
||||
const handleConfirm = async () => {
|
||||
setLoading(true)
|
||||
setMessage('')
|
||||
setShowConfirm(false)
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/clear-data?confirm=true', {
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
setMessage(data.message || '数据清理成功!')
|
||||
setTimeout(() => window.location.reload(), 2000)
|
||||
} else {
|
||||
setMessage(`清理失败: ${data.error}`)
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage(`清理出错: ${error instanceof Error ? error.message : '未知错误'}`)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
setShowConfirm(false)
|
||||
setMessage('')
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: '1rem', borderBottom: '1px solid var(--theme-elevation-100)' }}>
|
||||
<h3 style={{ marginBottom: '0.5rem', fontSize: '1rem' }}>🗑️ 清理数据库</h3>
|
||||
<p style={{ marginBottom: '1rem', fontSize: '0.875rem', color: 'var(--theme-elevation-500)' }}>
|
||||
清除所有商品、公告、文章数据(保留用户和媒体文件)
|
||||
</p>
|
||||
|
||||
{showConfirm ? (
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: '1rem',
|
||||
padding: '0.75rem',
|
||||
backgroundColor: 'var(--theme-error-50)',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid var(--theme-error-500)',
|
||||
}}
|
||||
>
|
||||
<p style={{ margin: '0', fontWeight: 'bold', color: 'var(--theme-error-700)' }}>
|
||||
⚠️ 确认清理所有数据?
|
||||
</p>
|
||||
<p style={{ margin: '0.5rem 0 0 0', fontSize: '0.875rem' }}>
|
||||
此操作不可撤销!将删除所有商品、公告和文章。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||
<Button onClick={handleConfirm} disabled={loading} buttonStyle="error">
|
||||
{loading ? '清理中...' : '确认清理'}
|
||||
</Button>
|
||||
<Button onClick={handleCancel} disabled={loading} buttonStyle="secondary">
|
||||
取消
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Button onClick={handleClearData} disabled={loading} buttonStyle="error">
|
||||
清理数据
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{message && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: '0.75rem',
|
||||
padding: '0.75rem',
|
||||
backgroundColor: message.includes('失败') || message.includes('出错')
|
||||
? 'var(--theme-error-50)'
|
||||
: 'var(--theme-success-50)',
|
||||
borderRadius: '4px',
|
||||
fontSize: '0.875rem',
|
||||
}}
|
||||
>
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -12,9 +12,6 @@ export default function AdminPanel() {
|
|||
const [clearMessage, setClearMessage] = useState('')
|
||||
const [showClearConfirm, setShowClearConfirm] = useState(false)
|
||||
|
||||
const [fixLoading, setFixLoading] = useState(false)
|
||||
const [fixMessage, setFixMessage] = useState('')
|
||||
|
||||
const handleClearData = () => {
|
||||
setShowClearConfirm(true)
|
||||
setClearMessage('')
|
||||
|
|
@ -49,33 +46,6 @@ export default function AdminPanel() {
|
|||
setClearMessage('')
|
||||
}
|
||||
|
||||
const handleFixDatabase = async () => {
|
||||
if (!confirm('确定要修复数据库字段类型吗?这会修改 products 表的 thumbnail 字段。')) {
|
||||
return
|
||||
}
|
||||
|
||||
setFixLoading(true)
|
||||
setFixMessage('')
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/fix-database', {
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
setFixMessage(data.message || '数据库修复成功!')
|
||||
} else {
|
||||
setFixMessage(`修复失败: ${data.error}`)
|
||||
}
|
||||
} catch (error) {
|
||||
setFixMessage(`修复出错: ${error instanceof Error ? error.message : '未知错误'}`)
|
||||
} finally {
|
||||
setFixLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: '2rem', maxWidth: '1200px', margin: '0 auto' }}>
|
||||
<h1 style={{ marginBottom: '2rem', fontSize: '2rem', fontWeight: 'bold' }}>
|
||||
|
|
@ -179,56 +149,6 @@ export default function AdminPanel() {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 修复数据库区域 */}
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'var(--theme-elevation-0)',
|
||||
borderRadius: '6px',
|
||||
padding: '1.5rem',
|
||||
marginTop: '1rem',
|
||||
border: '1px solid var(--theme-elevation-150)',
|
||||
}}
|
||||
>
|
||||
<h3 style={{ marginBottom: '0.5rem', fontSize: '1rem', fontWeight: '600' }}>
|
||||
🔧 修复数据库
|
||||
</h3>
|
||||
<p
|
||||
style={{
|
||||
marginBottom: '1rem',
|
||||
fontSize: '0.875rem',
|
||||
color: 'var(--theme-elevation-600)',
|
||||
}}
|
||||
>
|
||||
修复数据库字段类型(将 products.thumbnail 从 integer 改为 varchar)
|
||||
</p>
|
||||
|
||||
<Button onClick={handleFixDatabase} disabled={fixLoading} buttonStyle="primary">
|
||||
{fixLoading ? '修复中...' : '修复字段类型'}
|
||||
</Button>
|
||||
|
||||
{fixMessage && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: '1rem',
|
||||
padding: '1rem',
|
||||
backgroundColor:
|
||||
fixMessage.includes('失败') || fixMessage.includes('出错')
|
||||
? 'var(--theme-error-50)'
|
||||
: 'var(--theme-success-50)',
|
||||
borderRadius: '6px',
|
||||
fontSize: '0.875rem',
|
||||
border: `1px solid ${
|
||||
fixMessage.includes('失败') || fixMessage.includes('出错')
|
||||
? 'var(--theme-error-500)'
|
||||
: 'var(--theme-success-500)'
|
||||
}`,
|
||||
}}
|
||||
>
|
||||
{fixMessage}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 系统信息区域 */}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
import * as migration_20260208_171142 from './20260208_171142'
|
||||
import * as migration_20260211_180407 from './20260211_180407'
|
||||
import * as migration_20260211_180500_fix_thumbnail from './20260211_180500_fix_thumbnail'
|
||||
|
||||
export const migrations = [
|
||||
{
|
||||
|
|
@ -8,14 +6,4 @@ export const migrations = [
|
|||
down: migration_20260208_171142.down,
|
||||
name: '20260208_171142',
|
||||
},
|
||||
{
|
||||
up: migration_20260211_180407.up,
|
||||
down: migration_20260211_180407.down,
|
||||
name: '20260211_180407',
|
||||
},
|
||||
{
|
||||
up: migration_20260211_180500_fix_thumbnail.up,
|
||||
down: migration_20260211_180500_fix_thumbnail.down,
|
||||
name: '20260211_180500_fix_thumbnail',
|
||||
},
|
||||
]
|
||||
|
|
|
|||
Loading…
Reference in New Issue