diff --git a/config/VERSION b/config/VERSION
index 532db09..3df742a 100644
--- a/config/VERSION
+++ b/config/VERSION
@@ -1 +1 @@
-VERSION=1.2.50
+VERSION=1.2.51
diff --git a/frontend/src/pages/Add.jsx.bak b/frontend/src/pages/Add.jsx.bak
new file mode 100644
index 0000000..f09e0cb
--- /dev/null
+++ b/frontend/src/pages/Add.jsx.bak
@@ -0,0 +1,673 @@
+// 添加藏品页面 - 支持 AI 识别/手工录入/批量录入
+import React, { useState, useRef } from 'react'
+import { APP_VERSION } from '../config/version'
+const numberCategoryOptions = [{value:'无2347',label:'无2347'},{value:'无347',label:'无347'},{value:'无247',label:'无247'},{value:'无47',label:'无47'},{value:'无4',label:'无4'},{value:'带4',label:'带4'},{value:'其他',label:'其他'}];
+
+// 字段转换函数
+const convertField = (obj) => {
+ const map = {
+ id: 'f99_90_id', userId: 'f99_91_user_id',
+ name: 'f01_01_name', code: 'f01_02_code', category: 'f01_03_category',
+ status: 'f01_04_status', remark: 'f01_05_remark',
+ prefixSerial: 'f02_10_prefix_serial', version: 'f02_11_version',
+ packaging: 'f02_12_packaging', rarity: 'f02_13_rarity',
+ isGraded: 'f03_20_is_graded', gradingCompany: 'f03_21_grading_company',
+ gradingScore: 'f03_22_grading_score', threeStar: 'f03_23_three_star',
+ specialMark: 'f04_30_special_mark', serialFeature: 'f04_31_serial_feature', numberCategory: 'f02_14_number_category',
+ issuer: 'f04_32_issuer', issueYear: 'f04_33_issue_year',
+ material: 'f04_34_material', denomination: 'f04_35_denomination',
+ issueQuantity: 'f04_36_issue_quantity',
+ costPrice: 'f05_40_cost_price', targetPrice: 'f05_41_target_price',
+ goalPrice: 'f05_42_goal_price', repairFee: 'f05_43_repair_fee',
+ gradingFee: 'f05_44_grading_fee', purpose: 'f06_50_purpose'
+ }
+ const result = {}
+ const numberFields = ['costPrice', 'targetPrice', 'goalPrice', 'repairFee', 'gradingFee']
+ for (const key in obj) {
+ let value = obj[key]
+ if (value === '' || value === null) value = null
+ else if (numberFields.includes(key)) {
+ value = parseFloat(value)
+ if (isNaN(value)) value = null
+ }
+ result[map[key] || key] = value
+ }
+ return result
+}
+
+// 通用 Input 组件
+const Input = ({ form, handleChange, label, field, type = 'text', options = null }) => {
+ const onChange = (e) => {
+ const value = e.target.value
+ if (type === 'number' && value !== '') {
+ const num = parseFloat(value)
+ handleChange(field, isNaN(num) ? '' : num)
+ } else handleChange(field, value)
+ }
+ return (
+
+
{label}
+ {options ? (
+
+ ) : (
+
+ )}
+
+ )
+}
+
+const getDefaultForm = () => ({
+ name: '藏品', code: '', category: '自持', rarity: '通货', prefixSerial: '',
+ version: '2024 龙', denomination: '', status: 'in_collection', packaging: '单张',
+ material: '塑料钞', issueQuantity: '1 亿', purpose: '收藏', isGraded: false,
+ gradingCompany: '', gradingScore: '', threeStar: false, specialMark: '',
+ serialFeature: '', numberCategory: '', issuer: '中国人民银行', issueYear: '2024',
+ costPrice: '', targetPrice: '', goalPrice: '', repairFee: '', gradingFee: '', remark: ''
+})
+
+export default function Add() {
+ // 根据 URL 参数确定默认标签
+ const params = new URLSearchParams(window.location.hash.split('?')[1] || '')
+ const modeParam = params.get('mode')
+ const defaultTab = modeParam === 'manual' ? 'manual' : 'ai'
+
+ const [activeTab, setActiveTab] = useState(defaultTab) // ai, manual, batch
+ const [form, setForm] = useState(getDefaultForm())
+ const [saving, setSaving] = useState(false)
+ const [error, setError] = useState('')
+ const [recognizing, setRecognizing] = useState(false)
+ const [selectedImage, setSelectedImage] = useState(null)
+ const [imagePreview, setImagePreview] = useState(null)
+ const [recognizedImage, setRecognizedImage] = useState(null)
+ const fileInputRef = useRef(null)
+ const imageFileInputRef = useRef(null)
+ const [uploadImages, setUploadImages] = useState([])
+ const [tempImage, setTempImage] = useState(null) // OCR识别后的临时图片
+ const maxImages = 1
+
+ const handleUploadImage = (e) => {
+ const files = Array.from(e.target.files)
+ if (files.length === 0) return
+ if (files.length + uploadImages.length > maxImages) {
+ alert(`最多只能上传${maxImages}张图片`)
+ return
+ }
+ const newImages = files.map(file => ({
+ file,
+ preview: URL.createObjectURL(file),
+ name: file.name,
+ size: file.size
+ }))
+ setUploadImages([...uploadImages, ...newImages])
+ }
+
+ const statusOptions = [
+ { value: 'in_collection', label: '收藏中' },
+ { value: 'selling', label: '出售中' },
+ { value: 'sold', label: '已售' },
+ { value: 'grading', label: '送评中' },
+ { value: 'repairing', label: '修复中' },
+ { value: 'transit', label: '在途中' },
+ { value: 'seeking', label: '寻号中' },
+ { value: 'other', label: '其他' }
+ ]
+
+ const categoryOptions = [
+ { value: '自持', label: '自持' },
+ { value: '寄存', label: '寄存' },
+ { value: '寄售', label: '寄售' },
+ { value: '共有', label: '共有' },
+ { value: '其他', label: '其他' }
+ ]
+
+ const rarityOptions = [
+ { value: '通货', label: '通货' },
+ { value: '特色', label: '特色' },
+ { value: '少见', label: '少见' },
+ { value: '稀有', label: '稀有' },
+ { value: '珍品', label: '珍品' },
+ { value: '孤品', label: '孤品' }
+ ]
+
+ const packagingOptions = [
+ { value: '标十', label: '标十' },
+ { value: '标百', label: '标百' },
+ { value: '单张', label: '单张' },
+ { value: '裸钞', label: '裸钞' }
+ ]
+
+ const handleChange = (key, value) => {
+ setForm({ ...form, [key]: value })
+ // 只有设置了出售价(goalPrice)才自动设置为"已售"
+ if (key === 'goalPrice' && value) {
+ setForm(prev => ({ ...prev, status: 'sold' }))
+ }
+ // 冠字号变化时自动分类
+ if (key === 'prefixSerial') {
+ const serial = value
+ let cat = ''
+ const match = serial.match(/J(\d{9})/)
+ const digits = match ? match[1] : serial.replace(/\D/g, '').slice(0, 9)
+ if (digits) {
+ if (!digits.includes('2') && !digits.includes('3') && !digits.includes('4') && !digits.includes('7')) cat = '无2347'
+ else if (!digits.includes('3') && !digits.includes('4') && !digits.includes('7')) cat = '无347'
+ else if (!digits.includes('2') && !digits.includes('4') && !digits.includes('7')) cat = '无247'
+ else if (!digits.includes('4') && !digits.includes('7') && digits.includes('2') && digits.includes('3')) cat = '无47'
+ else if (digits.includes('7') && !digits.includes('4')) cat = '无4'
+ else if (digits.includes('4')) cat = '带4'
+ else cat = '其他'
+ }
+ setForm(prev => ({ ...prev, numberCategory: cat }))
+ }
+ // 版别变化时同步发行年份
+ if (key === 'version') {
+ const yearMatch = value.match(/(20\d{2})/)
+ if (yearMatch) {
+ setForm(prev => ({ ...prev, issueYear: yearMatch[1] }))
+ }
+ }
+ }
+
+ const handleSelectImage = (e) => {
+ const file = e.target.files[0]
+ if (!file) return
+ setSelectedImage(file)
+ const reader = new FileReader()
+ reader.onload = (e) => setImagePreview(e.target.result)
+ reader.readAsDataURL(file)
+ }
+
+ const handleRecognize = async () => {
+ if (!selectedImage) { setError('请先选择图片'); return }
+ setRecognizing(true)
+ setError('')
+ const token = localStorage.getItem('token')
+ const formData = new FormData()
+ formData.append('image', selectedImage)
+
+ // 使用 XMLHttpRequest 替代 fetch
+ const data = await new Promise((resolve, reject) => {
+ const xhr = new XMLHttpRequest()
+ xhr.open('POST', '/api/ocr/recognize')
+ xhr.setRequestHeader('Authorization', 'Bearer ' + token)
+
+ xhr.onload = function() {
+ if (xhr.status >= 200 && xhr.status < 300) {
+ try {
+ const data = JSON.parse(xhr.responseText)
+ resolve(data)
+ } catch (e) {
+ reject(new Error('JSON解析失败: ' + xhr.responseText.substring(0, 100)))
+ }
+ } else {
+ try {
+ const data = JSON.parse(xhr.responseText)
+ reject(new Error(data.error?.message || data.detail || '识别失败'))
+ } catch (e) {
+ reject(new Error('请求失败: ' + xhr.status))
+ }
+ }
+ }
+
+ xhr.onerror = function() {
+ reject(new Error('网络错误'))
+ }
+
+ xhr.send(formData)
+ })
+ try {
+ if (data.fields) {
+ const recognizedForm = { ...getDefaultForm() }
+ // 直接对应 AI 返回的字段,不需要二次解析
+ if (data.fields.issuer) recognizedForm.issuer = data.fields.issuer
+ if (data.fields.version) recognizedForm.version = data.fields.version
+ if (data.fields.denomination) recognizedForm.denomination = data.fields.denomination
+ if (data.fields.prefix_serial) recognizedForm.prefixSerial = data.fields.prefix_serial
+ if (data.fields.packaging) recognizedForm.packaging = data.fields.packaging
+ if (data.fields.grading_company) recognizedForm.gradingCompany = data.fields.grading_company
+ if (data.fields.grading_score) recognizedForm.gradingScore = data.fields.grading_score
+ if (data.fields.special_mark && data.fields.special_mark !== '无') recognizedForm.specialMark = data.fields.special_mark
+ if (data.fields.serial_feature && data.fields.serial_feature !== '无') recognizedForm.serialFeature = data.fields.serial_feature
+ // 布尔字段
+ if (data.fields.is_graded !== undefined) recognizedForm.isGraded = data.fields.is_graded
+ if (data.fields.three_star !== undefined) recognizedForm.threeStar = data.fields.three_star
+
+ // 保存识别的图片信息(包含服务器临时路径)
+ const newImage = {
+ file: selectedImage,
+ preview: URL.createObjectURL(selectedImage),
+ name: selectedImage.name,
+ size: selectedImage.size,
+ temp_image: data.temp_image // 保存临时图片信息
+ }
+ setUploadImages([newImage])
+ setTempImage(data.temp_image) // 保存临时图片信息
+
+ setForm(recognizedForm)
+ setActiveTab('manual')
+ console.log('AI 识别结果:', recognizedForm)
+ console.log('识别图片:', newImage)
+ } else setError('识别结果为空')
+ } catch (e) { setError('识别失败:' + e.message) }
+ finally { setRecognizing(false) }
+ }
+
+ const handleSave = async () => {
+ if (!form.name || !form.version) { setError('名称和版别为必填项'); return }
+ setSaving(true)
+ setError('')
+ const token = localStorage.getItem('token')
+ // 保存前自动根据冠字号设置号码分类
+ const formWithCategory = { ...form }
+ if (formWithCategory.prefixSerial && !formWithCategory.numberCategory) {
+ const serial = formWithCategory.prefixSerial
+ const match = serial.match(/J(\d{9})/)
+ const digits = match ? match[1] : serial.replace(/\D/g, '').slice(0, 9)
+ if (digits) {
+ let cat = ''
+ if (!digits.includes('2') && !digits.includes('3') && !digits.includes('4') && !digits.includes('7')) cat = '无2347'
+ else if (!digits.includes('3') && !digits.includes('4') && !digits.includes('7')) cat = '无347'
+ else if (!digits.includes('2') && !digits.includes('4') && !digits.includes('7')) cat = '无247'
+ else if (!digits.includes('4') && !digits.includes('7') && digits.includes('2') && digits.includes('3')) cat = '无47'
+ else if (digits.includes('7') && !digits.includes('4')) cat = '无4'
+ else if (digits.includes('4')) cat = '带4'
+ else cat = '其他'
+ formWithCategory.numberCategory = cat
+ }
+ }
+
+ const formData = convertField(formWithCategory)
+ try {
+ // 1. 保存藏品信息(先检查是否重复)
+ const res = await fetch('/api/collections', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
+ body: JSON.stringify(formData)
+ })
+ if (!res.ok) {
+ const data = await res.json()
+ throw new Error(data.error?.message || data.detail || '保存失败')
+ }
+ const result = await res.json()
+ let collectionId = result.f99_90_id || result.id
+
+ // 检查是否有错误(重复编号等)
+ if (result.error) {
+ throw new Error(result.error.message || '保存失败')
+ }
+
+ // 检查是否有重复警告
+ if (result.warning && result.warning.code === 'DUPLICATE_SERIAL') {
+ const { warning } = result
+ const confirmed = window.confirm(
+ `⚠️ 发现重复冠字号!\n\n` +
+ `冠字号:${warning.existing_collection.prefix_serial}\n` +
+ `已存在于:${warning.existing_collection.name} (编号:${warning.existing_collection.code})\n\n` +
+ `是否继续保存?`
+ )
+
+ if (!confirmed) {
+ setSaving(false)
+ return
+ }
+
+ // 用户确认继续,再次调用 API(添加 force=true 参数)
+ const forceRes = await fetch('/api/collections?force=true', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
+ body: JSON.stringify(formData)
+ })
+
+ if (!forceRes.ok) {
+ const forceData = await forceRes.json()
+ throw new Error(forceData.error?.message || '保存失败')
+ }
+
+ const forceResult = await forceRes.json()
+ collectionId = forceResult.f99_90_id || forceResult.id
+ console.log('藏品保存成功(确认重复),ID:', collectionId)
+ } else if (collectionId) {
+ console.log('藏品保存成功,ID:', collectionId)
+ } else {
+ throw new Error('保存失败:未返回藏品 ID')
+ }
+
+ // 上传图片(无论是否重复都执行)
+ let totalUploadCount = 0
+
+ // 2. 处理识别的图片(如果有临时图片,直接认领;否则上传)
+ if (uploadImages.length > 0 && collectionId) {
+ console.log('处理图片,数量:', uploadImages.length)
+
+ for (const img of uploadImages) {
+ // 检查是否有临时图片(OCR识别后保存的)
+ if (img.temp_image && img.temp_image.id) {
+ // 认领临时图片
+ try {
+ const claimRes = await fetch(`/api/ocr/claim-temp-image?temp_id=${img.temp_image.id}&collection_id=${collectionId}`, {
+ method: 'POST',
+ headers: { 'Authorization': 'Bearer ' + token }
+ })
+ if (claimRes.ok) {
+ totalUploadCount++
+ console.log('✅ 临时图片认领成功')
+ } else {
+ console.error('临时图片认领失败:', await claimRes.text())
+ }
+ } catch (claimErr) {
+ console.error('临时图片认领异常:', claimErr)
+ }
+ } else {
+ // 普通上传
+ const imgFormData = new FormData()
+ imgFormData.append('file', img.file)
+ try {
+ const uploadRes = await fetch(`/api/collections/upload-image?collection_id=${collectionId}`, {
+ method: 'POST',
+ headers: { 'Authorization': 'Bearer ' + token },
+ body: imgFormData
+ })
+ if (uploadRes.ok) {
+ totalUploadCount++
+ console.log(`图片上传成功`)
+ } else {
+ console.error('图片上传失败:', await uploadRes.text())
+ }
+ } catch (uploadErr) {
+ console.error('图片上传异常:', uploadErr)
+ }
+ }
+ }
+
+ console.log(`✅ 共处理 ${totalUploadCount} 张图片`)
+ }
+
+ alert('保存成功!' + (totalUploadCount > 0 ? `已上传${totalUploadCount}张图片` : ''))
+ window.location.hash = '#/list'
+ window.refreshList?.()
+ window.refreshHome?.()
+ } catch (e) {
+ console.error('保存失败:', e)
+ alert('保存失败:' + e.message)
+ }
+ finally { setSaving(false) }
+ }
+
+ return (
+
+ {/* 顶部标签切换 */}
+
+
+
+
+
+
+
+
+ {error && (
+
⚠️ {error}
+ )}
+
+ {/* AI 识别模式 */}
+ {activeTab === 'ai' && (
+
+
+
+ {imagePreview ? (
+
+

+
+
+
+
+
+ ) : (
+
+
{ fileInputRef.current.setAttribute('capture', 'environment'); fileInputRef.current.click(); }}
+ style={{ padding: '24px', background: 'rgba(59, 130, 246, 0.1)', border: '2px dashed rgba(59, 130, 246, 0.5)', borderRadius: '12px', cursor: 'pointer', marginBottom: '16px', textAlign: 'center' }}>
+
📷
+
拍照识别
+
使用相机拍照并识别
+
+
{ fileInputRef.current.removeAttribute('capture'); fileInputRef.current.click(); }}
+ style={{ padding: '24px', background: 'rgba(34, 197, 94, 0.1)', border: '2px dashed rgba(34, 197, 94, 0.5)', borderRadius: '12px', cursor: 'pointer', textAlign: 'center' }}>
+
🖼️
+
从相册选择
+
从相册选择已有图片
+
+
+ )}
+
+
+
💡 识别说明
+
+ - 支持拍照或从相册选择图片
+ - 自动识别名称、版别、冠字序号等字段
+ - 识别结果可手动修改完善
+ - 建议拍摄清晰、光线充足的正面照片
+
+
+
+ )}
+
+ {/* 手工录入模式 - 完整表单 */}
+ {activeTab === 'manual' && (
+
+ {/* 图片上传区域 */}
+
+
+
藏品图片 ({uploadImages.length}/1)
+
+
+
+ {uploadImages.length > 0 ? (
+
+ {uploadImages.map((img, index) => (
+
+

+
{img.name}
+
+
{index + 1}/{uploadImages.length}
+
+ ))}
+ {uploadImages.length < 1 && (
+
imageFileInputRef.current?.click()} style={{
+ aspectRatio: '1.5',
+ background: 'rgba(255,255,255,0.05)',
+ border: '2px dashed rgba(255,255,255,0.3)',
+ borderRadius: '12px',
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ justifyContent: 'center',
+ cursor: 'pointer',
+ color: '#94a3b8',
+ fontSize: '13px'
+ }}>
+
📷
+
添加图片
+
最多可上传 1 张
+
+ )}
+
+ ) : (
+
imageFileInputRef.current?.click()} style={{
+ aspectRatio: '1.5',
+ background: 'rgba(255,255,255,0.05)',
+ border: '2px dashed #fbbf24',
+ borderRadius: '12px',
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ justifyContent: 'center',
+ cursor: 'pointer',
+ color: '#fbbf24',
+ fontSize: '13px'
+ }}>
+
📷
+
点击上传图片
+
最多可上传 1 张
+
+ )}
+
+
+ {/* 基本信息 */}
+
+
+ {/* 评级信息 */}
+
+
+ {/* 特殊信息 */}
+
+
+ {/* 价格信息 */}
+
+
+ {/* 备注 */}
+
+
+ {/* 保存按钮 */}
+
+
+
+
+ )}
+
+ {/* 批量录入模式 */}
+ {activeTab === 'batch' && (
+
+
🚧
+
批量录入开发中
+
敬请期待后续版本
+
+ )}
+
+ {/* 版本号 */}
+
v{APP_VERSION}
+
+ )
+}
diff --git a/frontend/src/pages/News.jsx b/frontend/src/pages/News.jsx
index b316cc6..66f01e9 100644
--- a/frontend/src/pages/News.jsx
+++ b/frontend/src/pages/News.jsx
@@ -1,5 +1,4 @@
import React, { useState, useEffect } from 'react'
-import YichensBoard from './YichensBoard'
// 本地获取用户手机号 - 添加异常处理
const getUserPhone = () => {
@@ -73,11 +72,18 @@ export default function News() {
try {
// 寻配号对所有人公开,无需登录即可查看
const token = localStorage.getItem('token')
- // 根据tab获取不同类型的数据
- const type = activeTab === 'seek' ? 'seek' : activeTab === 'deal' ? 'deal' : 'yichen'
- // 始终传递token,以便获取准确的matched_count
const headers = token ? { Authorization: `Bearer ${token}` } : {}
- const res = await fetch(`${API_BASE}/api/information/list?info_type=${type}&page=${currentPage}&page_size=50`, { headers })
+
+ let url = ''
+ if (activeTab === 'yichen') {
+ // 一尘看板 - 直接调用一尘数据API
+ url = '/api/yichens/posts?limit=390&offset=0'
+ } else {
+ const type = activeTab === 'seek' ? 'seek' : 'deal'
+ url = `${API_BASE}/api/information/list?info_type=${type}&page=${currentPage}&page_size=50`
+ }
+
+ const res = await fetch(url, { headers })
// 从响应头获取总页数
const total = res.headers.get('X-Total-Pages') || res.headers.get('x-total-pages')
if (total) setTotalPages(parseInt(total))
@@ -478,7 +484,7 @@ export default function News() {
) : (
- {activeTab === 'yichen' ?
: infoList.map(item => {
+ {activeTab === 'yichen' ?
: infoList.map(item => {
// 解析正文中的号码特征和联系方式
const content = item.content || ''
const featuresMatch = content.match(/号码特征[:\s]*(.+?)(?:\n|$)/)
@@ -489,14 +495,15 @@ export default function News() {
const cleanContent = content.replace(/价格[:\s]*.+?(\n|$)/g, '').replace(/号码特征[:\s]*.+?(\n|$)/g, '').replace(/联系方式[:\s]*.+?(\n|$)/g, '').trim()
return (
-
+
{/* 标题 */}
{item.title}
{/* 创建日期 + 用户名 */}
- 📅 {formatDate(item.created_at)} | 👤 {(item.content && item.content.match(/发布者: (.+)/)?.[1]) || item.user_name || '匿名用户'}
+ 📅 {item.post_time ? item.post_time.substring(0,16) : formatDate(item.created_at)} | 👤 {item.author_username || item.user_name || '匿名用户'}
+ {item.category && {item.category}}
{/* 号码特征 */}
{features && (
@@ -992,3 +999,193 @@ export default function News() {
)
}
+
+// ==================== 一尘看板完整功能组件 ====================
+function YichenBoardFull() {
+ const [todayStats, setTodayStats] = useState({ total: 0, deals: 0, wants: 0, others: 0, dragons: 0, horses: 0, snakes: 0, tianma: 0 })
+ const [todayCategory, setTodayCategory] = useState([])
+ const [posts, setPosts] = useState([])
+ const [loading, setLoading] = useState(false)
+ const [expandedPosts, setExpandedPosts] = useState({})
+ const [postTypeFilter, setPostTypeFilter] = useState('all')
+ const [categoryFilter, setCategoryFilter] = useState('')
+ const [page, setPage] = useState(1)
+ const [totalPosts, setTotalPosts] = useState(0)
+ const [searchKeyword, setSearchKeyword] = useState('')
+
+ const today = new Date()
+ const dateStr = today.getFullYear() + '/' + (today.getMonth() + 1) + '/' + today.getDate()
+
+ useEffect(() => { fetchTodayStats(); fetchPosts(1, '') }, [])
+ useEffect(() => { if (todayStats.total > 0) fetchTodayCategory() }, [todayStats])
+ useEffect(() => { setPage(1); fetchPosts(1, categoryFilter) }, [postTypeFilter, categoryFilter, searchKeyword])
+
+ const fetchTodayStats = async () => {
+ try {
+ const res = await fetch('/api/yichens/stats/today')
+ setTodayStats(await res.json())
+ } catch(e) { console.error(e) }
+ }
+
+ const fetchTodayCategory = async () => {
+ try {
+ const res = await fetch('/api/yichens/stats/today-category')
+ setTodayCategory(await res.json())
+ } catch(e) { console.error(e) }
+ }
+
+ const fetchPosts = async (p, cat) => {
+ setLoading(true)
+ const currentPage = p !== undefined ? p : page
+ const currentCat = cat !== undefined ? cat : categoryFilter
+
+ let url = '/api/yichens/posts?limit=390&offset=' + ((currentPage - 1) * 390)
+ if (postTypeFilter === 'deal') url += '&post_type=deal'
+ else if (postTypeFilter === 'want') url += '&post_type=want'
+ else if (postTypeFilter === 'other') url += '&post_type=normal'
+
+ try {
+ const res = await fetch(url)
+ let data = await res.json() || []
+
+ if (currentCat) {
+ if (currentCat === '龙') data = data.filter(p => p.category && (p.category.includes('龙') || p.category.includes('纪念钞')))
+ else if (currentCat === '蛇') data = data.filter(p => p.category && p.category.includes('蛇'))
+ else if (currentCat === '马') data = data.filter(p => p.category && p.category.includes('马'))
+ else if (currentCat === '其他') data = data.filter(p => p.category && !p.category.includes('龙') && !p.category.includes('纪念钞') && !p.category.includes('蛇') && !p.category.includes('马'))
+ }
+
+ if (searchKeyword) {
+ const kw = searchKeyword.trim()
+ if (kw) {
+ data = data.filter(p => (p.title && p.title.includes(kw)) || (p.content && p.content.includes(kw)) || (p.category && p.category.includes(kw)))
+ }
+ }
+
+ setPosts(data)
+ setTotalPosts(todayStats.total || 0)
+ } catch { setPosts([]) }
+ setLoading(false)
+ }
+
+ const toggleExpand = (postId) => setExpandedPosts(prev => ({...prev, [postId]: !prev[postId]}))
+
+ const StatCard = ({ label, value, color }) => (
+
+ )
+
+ return (
+
+ {/* 统计卡片 */}
+
+
+ 📈 连体钞/纪念钞 {dateStr}
+
+
+
+
+
+
+
+
+
+ {/* 分类统计 */}
+ {todayCategory.length > 0 && (
+
+
📊 今日分类统计
+
+ {todayCategory.map(cat => (
+
+ {cat.category} ({cat.count})
+
+ ))}
+
+
+ )}
+
+ {/* 搜索框 */}
+
+
+ setSearchKeyword(e.target.value)}
+ style={{ flex: 1, padding: '10px 14px', borderRadius: 8, border: '1px solid #374151', background: '#1f2937', color: '#fff', fontSize: 13, outline: 'none' }} />
+
+
+ {searchKeyword &&
搜索: "{searchKeyword}",找到 {posts.length} 条结果
}
+
+
+ {/* 类型筛选 */}
+
+ {[{ key: 'all', label: '全部' }, { key: 'deal', label: '出售' }, { key: 'want', label: '求购' }, { key: 'other', label: '其他' }].map(item => (
+
+ ))}
+
+
+ {/* 分类筛选 */}
+
+ {[{ key: '', label: '全部' }, { key: '龙', label: '龙钞' }, { key: '蛇', label: '蛇钞' }, { key: '马', label: '马钞' }, { key: '其他', label: '其他' }].map(item => (
+
+ ))}
+
+
+ {/* 帖子列表 */}
+ {loading ? (
+
加载中...
+ ) : posts.length === 0 ? (
+
暂无数据
+ ) : (
+
+ {posts.map(item => (
+
+
toggleExpand(item.post_id)} style={{ cursor: 'pointer' }}>
+
+
{item.title || '无标题'}
+
+
+ {item.post_type === 'deal' ? '出售' : item.post_type === 'want' ? '求购' : '其他'}
+
+
+ {item.category || '-'}
+
+
+
+
+ {item.author_username || '未知'}
+ {item.post_time?.substring(0, 16) || ''}
+
+
+ {expandedPosts[item.post_id] && item.content && (
+
+
{item.content}
+ {item.url && (
+
查看原帖 →
+ )}
+
+ )}
+
+ ))}
+
+ {/* 分页 */}
+
+
共 {totalPosts} 条帖子,{Math.ceil(totalPosts/390)} 页,当前第 {page} 页
+
+
+
+
+
+
+ )}
+
+ )
+}