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 [clearMessage, setClearMessage] = useState('')
|
||||||
const [showClearConfirm, setShowClearConfirm] = useState(false)
|
const [showClearConfirm, setShowClearConfirm] = useState(false)
|
||||||
|
|
||||||
const [fixLoading, setFixLoading] = useState(false)
|
|
||||||
const [fixMessage, setFixMessage] = useState('')
|
|
||||||
|
|
||||||
const handleClearData = () => {
|
const handleClearData = () => {
|
||||||
setShowClearConfirm(true)
|
setShowClearConfirm(true)
|
||||||
setClearMessage('')
|
setClearMessage('')
|
||||||
|
|
@ -49,33 +46,6 @@ export default function AdminPanel() {
|
||||||
setClearMessage('')
|
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 (
|
return (
|
||||||
<div style={{ padding: '2rem', maxWidth: '1200px', margin: '0 auto' }}>
|
<div style={{ padding: '2rem', maxWidth: '1200px', margin: '0 auto' }}>
|
||||||
<h1 style={{ marginBottom: '2rem', fontSize: '2rem', fontWeight: 'bold' }}>
|
<h1 style={{ marginBottom: '2rem', fontSize: '2rem', fontWeight: 'bold' }}>
|
||||||
|
|
@ -179,56 +149,6 @@ export default function AdminPanel() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
{/* 系统信息区域 */}
|
{/* 系统信息区域 */}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
import * as migration_20260208_171142 from './20260208_171142'
|
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 = [
|
export const migrations = [
|
||||||
{
|
{
|
||||||
|
|
@ -8,14 +6,4 @@ export const migrations = [
|
||||||
down: migration_20260208_171142.down,
|
down: migration_20260208_171142.down,
|
||||||
name: '20260208_171142',
|
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