v1.2.82 - 行情录入功能优化:添加号码分类、尾号、大小号、包装类型、出售者、购买者字段;提交时自动计算尾号和大小号

This commit is contained in:
甲辰生产 2026-04-11 01:47:55 +08:00
parent acac7922af
commit bd22c20108
16 changed files with 495 additions and 735 deletions

View File

@ -13,7 +13,7 @@ router = APIRouter(prefix="/api/auth", tags=["认证"])
def generate_user_code(db): def generate_user_code(db):
"""生成用户编码从201开始按自然数顺序递增""" """生成用户编码从201开始按自然数顺序递增,跳过已存在的"""
# 查找最大的user_code # 查找最大的user_code
max_code = db.query(User.user_code).filter(User.user_code != None).order_by(User.user_code.desc()).first() max_code = db.query(User.user_code).filter(User.user_code != None).order_by(User.user_code.desc()).first()
if max_code and max_code[0]: if max_code and max_code[0]:
@ -21,6 +21,9 @@ def generate_user_code(db):
num = int(max_code[0]) + 1 num = int(max_code[0]) + 1
if num < 201: if num < 201:
num = 201 num = 201
# 检查是否已存在,如果存在则继续递增
while db.query(User).filter(User.user_code == str(num)).first():
num += 1
return str(num) return str(num)
except: except:
pass pass
@ -100,7 +103,20 @@ def register(user_data: UserCreate, db: Session = Depends(get_db)):
db.commit() db.commit()
db.refresh(user) db.refresh(user)
return user # 返回用户信息避免Pydantic序列化问题
return {
"id": user.f99_90_id,
"username": user.f01_01_name,
"user_code": user.user_code,
"email": user.email,
"phone": user.phone,
"avatar": user.avatar,
"role": user.role,
"level": user.f99_94_level,
"aiCount": user.f99_95_ai_count or 0,
"searchCount": user.f99_96_search_count or 0,
"collectionCount": user.f99_97_collection_count or 0
}
@router.post("/login", response_model=Token) @router.post("/login", response_model=Token)

View File

@ -132,14 +132,17 @@ def get_collections(
# 如果指定 all_users=true则返回所有用户藏品 # 如果指定 all_users=true则返回所有用户藏品
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
# 管理员默认查看全库,普通用户只看自己 # 管理员默认查看全库,普通用户只看自己,未登录返回空列表
if current_user.role == "admin": if current_user is None or current_user.role != "admin":
# 联表查询获取用户名 # 非管理员或未登录用户只能查看自己的藏品(如果没有登录则返回空)
if current_user is None:
return {"data": [], "total": 0, "page": 1, "limit": 20}
query = db.query(Collection).filter(Collection.f99_91_user_id == current_user.f99_90_id)
else:
# 管理员查看所有藏品
query = db.query(Collection, User.f01_01_name.label('owner_name')).join( query = db.query(Collection, User.f01_01_name.label('owner_name')).join(
User, Collection.f99_91_user_id == User.f99_90_id, isouter=True User, Collection.f99_91_user_id == User.f99_90_id, isouter=True
) )
else:
query = db.query(Collection).filter(Collection.f99_91_user_id == current_user.f99_90_id)
# 如果指定了user_id参数则只返回该用户的藏品 # 如果指定了user_id参数则只返回该用户的藏品
if user_id: if user_id:
@ -202,7 +205,7 @@ def get_collections(
data_list = [] data_list = []
for item in data: for item in data:
# 处理联表查询结果 # 处理联表查询结果
if current_user.role == "admin": if current_user is not None and current_user.role == "admin":
collection_item, owner_name = item collection_item, owner_name = item
else: else:
collection_item = item collection_item = item
@ -278,12 +281,12 @@ def get_stats(
): ):
"""获取藏品统计""" """获取藏品统计"""
# 获取所有藏品 # 获取所有藏品
if current_user.role == "admin": if current_user is None or current_user.role != "admin":
all_collections = db.query(Collection).all()
else:
all_collections = db.query(Collection).filter( all_collections = db.query(Collection).filter(
Collection.f99_91_user_id == current_user.f99_90_id Collection.f99_91_user_id == current_user.f99_90_id
).all() ).all()
else:
all_collections = db.query(Collection).all()
# 总数 # 总数
total_count = len(all_collections) total_count = len(all_collections)
@ -382,7 +385,7 @@ def get_collection(
collection = dict(result._mapping) collection = dict(result._mapping)
# 非管理员只能查看自己的藏品 # 非管理员只能查看自己的藏品
if current_user.role != "admin" and collection.get('f99_91_user_id') != current_user.f99_90_id: if (current_user is None or current_user.role != "admin") and collection.get('f99_91_user_id') != current_user.f99_90_id:
raise HTTPException(status_code=403, detail="无权访问") raise HTTPException(status_code=403, detail="无权访问")
result_dict = { result_dict = {
@ -496,6 +499,11 @@ def create_collection(
} }
} }
# 自动分类:如果未提供号码分类,则根据冠字号自动分类
if not collection_data.f02_14_number_category and collection_data.f02_10_prefix_serial:
from app.utils.number_category import get_number_category
collection_data.f02_14_number_category = get_number_category(collection_data.f02_10_prefix_serial)
collection = Collection( collection = Collection(
f99_91_user_id=current_user.f99_90_id, f99_91_user_id=current_user.f99_90_id,
f01_01_name=collection_data.f01_01_name, f01_01_name=collection_data.f01_01_name,
@ -709,7 +717,7 @@ async def delete_image(
Collection.f99_90_id == image.collection_id Collection.f99_90_id == image.collection_id
).first() ).first()
if collection and current_user.role != "admin" and collection.f99_91_user_id != current_user.f99_90_id: if collection and (current_user is None or current_user.role != "admin") and collection.f99_91_user_id != current_user.f99_90_id:
raise HTTPException(status_code=403, detail="E00014: 无权删除此图片") raise HTTPException(status_code=403, detail="E00014: 无权删除此图片")
# 删除OSS文件如果path是OSS URL # 删除OSS文件如果path是OSS URL

View File

@ -11,7 +11,7 @@ router = APIRouter(prefix="/api", tags=["用户"])
# ============ 当前用户接口 ============ # ============ 当前用户接口 ============
@router.get("/users/me", response_model=UserResponse) @router.get("/users/me") # 无 response_model避免 Pydantic 序列化问题
def get_current_user_info( def get_current_user_info(
current_user: User = Depends(get_current_user) current_user: User = Depends(get_current_user)
): ):
@ -47,7 +47,7 @@ def get_current_user_info(
"f99_93_updated_at": current_user.f99_93_updated_at.isoformat() if current_user.f99_93_updated_at else None "f99_93_updated_at": current_user.f99_93_updated_at.isoformat() if current_user.f99_93_updated_at else None
} }
@router.put("/users/me", response_model=UserResponse) @router.put("/users/me") # 无 response_model避免 Pydantic 序列化问题
def update_current_user( def update_current_user(
user_update: UserUpdate, user_update: UserUpdate,
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),

View File

@ -0,0 +1,132 @@
# 号码分类工具
# 按MEMORY.md最新规则 (2026-04-02)
# 优先级:数字越小越有价值
CATEGORIES = [
# 1. 圆圆号:无.123457只能用0689
{"name": "圆圆号", "cannot_use": ".123457", "must_have": ""},
# 2. 倒置号无23457可用01689必须有1
{"name": "倒置号", "cannot_use": "23457", "must_have": "1"},
# 3. 金马王无12347可用05689必须有5
{"name": "金马王", "cannot_use": "12347", "must_have": "5"},
# 4. 金马号无2347可用015689必须有1和5
{"name": "金马号", "cannot_use": "2347", "must_have": "15"},
# 5. 金山王无12457可用03689必须有3
{"name": "金山王", "cannot_use": "12457", "must_have": "3"},
# 6. 天马王无1247可用035689必须有3和5
{"name": "天马王", "cannot_use": "1247", "must_have": "35"},
# 7. 金山号无2457可用013689必须有1和3
{"name": "金山号", "cannot_use": "2457", "must_have": "13"},
# 8. 天马号无247可用0135689必须有1、3和5
{"name": "天马号", "cannot_use": "247", "must_have": "135"},
# 9. 朦胧王无13457
{"name": "朦胧王", "cannot_use": "13457", "must_have": ""},
# 10. 朦胧号无3457
{"name": "朦胧号", "cannot_use": "3457", "must_have": ""},
# 11. 如意号无1347
{"name": "如意号", "cannot_use": "1347", "must_have": ""},
# 12. 钻石号无347
{"name": "钻石号", "cannot_use": "347", "must_have": ""},
# 13. 永恒号无47
{"name": "永恒号", "cannot_use": "47", "must_have": ""},
# 14. 无4号不包含4
{"name": "无4号", "cannot_use": "4", "must_have": ""},
# 15. 通货含4
{"name": "通货", "cannot_use": "", "must_have": "4"},
]
def extract_digits(serial: str) -> dict:
"""提取冠字号中的数字部分"""
if not serial:
return {"digits": "", "type": "single"}
nums = serial.replace("J", "").replace(",", "").replace(".", "").strip()
nums = "".join(c for c in nums if c.isdigit())
if nums.endswith("01"):
return {"digits": nums[:-2], "type": "hundred"}
elif nums.endswith("1"):
return {"digits": nums[:-1], "type": "ten"}
else:
return {"digits": nums, "type": "single"}
def matches_category(digits: str, category: dict) -> bool:
cannot_use = category.get("cannot_use", "")
must_have = category.get("must_have", "")
# 检查不能用的数字
for n in cannot_use:
if n in digits:
return False
# 检查必须有的数字
if must_have:
for n in must_have:
if n not in digits:
return False
return True
def get_number_category(serial: str) -> str:
"""号码分类函数"""
if not serial:
return ""
# 提取数字
info = extract_digits(serial)
digits = info["digits"]
if not digits:
return ""
# 根据类型取对应位数
digits_type = info["type"]
if digits_type == "hundred":
# 标百看后6位
check_digits = digits[-6:] if len(digits) >= 6 else digits
elif digits_type == "ten":
# 标十看后7位
check_digits = digits[-7:] if len(digits) >= 7 else digits
else:
# 散钞看全部
check_digits = digits
# 按优先级匹配分类
for category in CATEGORIES:
if matches_category(check_digits, category):
return category["name"]
return "通货"
def get_number_category_color(category: str) -> str:
"""获取分类颜色"""
colors = {
"圆圆号": "#ef4444", # 红
"倒置号": "#f97316", # 橙
"金马王": "#eab308", # 黄
"金马号": "#84cc16", # 绿
"金山王": "#22c55e", # 深绿
"天马王": "#14b8a6", # 青
"金山号": "#06b6d4", # 蓝
"天马号": "#0ea5e9", # 浅蓝
"朦胧王": "#6366f1", # 靛蓝
"朦胧号": "#8b5cf6", # 紫
"如意号": "#a855f7", # 深紫
"钻石号": "#d946ef", # 品红
"永恒号": "#ec4899", # 粉红
"无4号": "#64748b", # 灰
"通货": "#9ca3af", # 浅灰
}
return colors.get(category, "#9ca3af")
def get_category_priority(category: str) -> int:
"""获取分类优先级(数字越小越高级)"""
for i, cat in enumerate(CATEGORIES, 1):
if cat["name"] == category:
return i
return 999

View File

@ -1 +1 @@
VERSION=1.2.79 VERSION=1.2.81

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<title>甲辰收藏 v=1.2.78</title> <title>甲辰收藏 v=1.2.81</title>
<!-- Favicon --> <!-- Favicon -->
<link rel="icon" type="image/png" href="/static/icons/favicon.png" /> <link rel="icon" type="image/png" href="/static/icons/favicon.png" />

View File

@ -1,12 +1,12 @@
{ {
"name": "jiachenlong-frontend", "name": "jiachenlong-frontend",
"version": "1.2.72", "version": "1.2.81",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "jiachenlong-frontend", "name": "jiachenlong-frontend",
"version": "1.2.72", "version": "1.2.81",
"dependencies": { "dependencies": {
"axios": "^1.7.9", "axios": "^1.7.9",
"react": "^18.3.1", "react": "^18.3.1",
@ -15,7 +15,7 @@
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"vite": "^6.0.7" "vite": "^6.4.2"
} }
}, },
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
@ -2047,9 +2047,9 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "6.4.1", "version": "6.4.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "resolved": "https://registry.npmmirror.com/vite/-/vite-6.4.2.tgz",
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {

View File

@ -1,6 +1,6 @@
{ {
"name": "jiachenlong-frontend", "name": "jiachenlong-frontend",
"version": "1.2.77", "version": "1.2.81",
"private": true, "private": true,
"description": "甲辰藏品管理系统 - 移动端前端", "description": "甲辰藏品管理系统 - 移动端前端",
"scripts": { "scripts": {
@ -9,13 +9,13 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"axios": "^1.7.9",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-router-dom": "^7.1.0", "react-router-dom": "^7.1.0"
"axios": "^1.7.9"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"vite": "^6.0.7" "vite": "^6.4.2"
} }
} }

1
frontend/package.txt Normal file
View File

@ -0,0 +1 @@
v=1.2.80

View File

@ -5,7 +5,6 @@ import List from './pages/List'
import Add from './pages/Add' import Add from './pages/Add'
import Stats from './pages/Stats' import Stats from './pages/Stats'
import News from './pages/News' import News from './pages/News'
import Info from './pages/Info'
import Login from './pages/Login' import Login from './pages/Login'
import Detail from './pages/Detail' import Detail from './pages/Detail'
import Edit from './pages/Edit' import Edit from './pages/Edit'
@ -31,7 +30,6 @@ export default function App() {
const basePath = path.split('?')[0] const basePath = path.split('?')[0]
if (basePath === '/') return <Home /> if (basePath === '/') return <Home />
if (basePath === '/news') return <News /> if (basePath === '/news') return <News />
if (basePath === '/info') return <Info />
if (basePath === '/stats') return <Stats /> if (basePath === '/stats') return <Stats />
if (basePath === '/list') return <List /> if (basePath === '/list') return <List />
if (basePath === '/add') return <Add /> if (basePath === '/add') return <Add />
@ -53,7 +51,6 @@ export default function App() {
} }
const isAdmin = user && user.role === 'admin' const isAdmin = user && user.role === 'admin'
const isEditor = user && user.role === 'editor' const isEditor = user && user.role === 'editor'
const canAccessInfo = isAdmin || isEditor
// //
if (!token) { if (!token) {
@ -91,8 +88,7 @@ export default function App() {
{ path: '/news', icon: '📰', label: '资讯' }, { path: '/news', icon: '📰', label: '资讯' },
{ path: '/stats', icon: '📊', label: '统计' }, { path: '/stats', icon: '📊', label: '统计' },
{ path: '/list', icon: '📚', label: '藏品' }, { path: '/list', icon: '📚', label: '藏品' },
{ path: '/add', icon: '🎯', label: '添加' }, { path: '/add', icon: '🎯', label: '录入' },
...(canAccessInfo ? [{ path: '/info', icon: '📝', label: '信息' }] : []),
...(isAdmin ? [{ path: '/admin', icon: '⚙️', label: '管理' }] : []) ...(isAdmin ? [{ path: '/admin', icon: '⚙️', label: '管理' }] : [])
].map(tab => ( ].map(tab => (
<div <div
@ -116,3 +112,4 @@ export default function App() {
</div> </div>
) )
} }
// Fri Apr 10 03:44:24 PM CST 2026

View File

@ -3,7 +3,7 @@
// 修改版本号只需修改 ../../VERSION 文件中的 VERSION=xxx // 修改版本号只需修改 ../../VERSION 文件中的 VERSION=xxx
// 从环境变量读取vite.config.js 注入) // 从环境变量读取vite.config.js 注入)
export const APP_VERSION = import.meta.env.APP_VERSION || '1.0.0' export const APP_VERSION = import.meta.env.APP_VERSION || '1.2.82'
// 版本信息 // 版本信息
export const VERSION_INFO = { export const VERSION_INFO = {

View File

@ -1,6 +1,7 @@
// - AI // // - AI /
import React, { useState, useRef } from 'react' import React, { useState, useRef } from 'react'
import { APP_VERSION } from '../config/version' import { APP_VERSION } from '../config/version'
const API_BASE = localStorage.getItem('API_BASE') || ''
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 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:'其他'}];
// //
@ -71,12 +72,47 @@ const getDefaultForm = () => ({
}) })
export default function Add() { export default function Add() {
//
const [dealForm, setDealForm] = useState({
serial: '',
category: '',
packaging: '标十',
price: '',
platform: '淘宝',
seller: '',
buyer: '',
date: new Date().toISOString().split('T')[0]
})
const [savingDeal, setSavingDeal] = useState(false)
//
const autoCategory = (serial) => {
const digits = serial.replace('J', '').replace(/[^0-9]/g, '').slice(0, 9)
if (!digits) return ''
const d = digits
if (!d.includes('1') && !d.includes('2') && !d.includes('3') && !d.includes('4') && !d.includes('5') && !d.includes('7')) return '圆圆号'
if (!d.includes('2') && !d.includes('3') && !d.includes('4') && !d.includes('5') && !d.includes('7')) return '倒置号'
if (!d.includes('1') && !d.includes('2') && !d.includes('3') && !d.includes('4') && !d.includes('7')) return '金马王'
if (!d.includes('2') && !d.includes('3') && !d.includes('4') && !d.includes('7')) return '金马号'
if (!d.includes('1') && !d.includes('2') && !d.includes('4') && !d.includes('5') && !d.includes('7')) return '金山王'
if (!d.includes('1') && !d.includes('2') && !d.includes('4') && !d.includes('7')) return '天马王'
if (!d.includes('2') && !d.includes('4') && !d.includes('5') && !d.includes('7')) return '金山号'
if (!d.includes('2') && !d.includes('4') && !d.includes('7')) return '天马号'
if (!d.includes('1') && !d.includes('3') && !d.includes('4') && !d.includes('5') && !d.includes('7')) return '朦胧王'
if (!d.includes('3') && !d.includes('4') && !d.includes('5') && !d.includes('7')) return '朦胧号'
if (!d.includes('1') && !d.includes('3') && !d.includes('4') && !d.includes('7')) return '如意号'
if (!d.includes('3') && !d.includes('4') && !d.includes('7')) return '钻石'
if (!d.includes('4') && !d.includes('7')) return '永恒'
if (!d.includes('4')) return '无4'
return '通货'
}
// URL // URL
const params = new URLSearchParams(window.location.hash.split('?')[1] || '') const params = new URLSearchParams(window.location.hash.split('?')[1] || '')
const modeParam = params.get('mode') const modeParam = params.get('mode')
const defaultTab = modeParam === 'manual' ? 'manual' : 'ai' const defaultTab = modeParam === 'manual' ? 'manual' : 'ai'
const [activeTab, setActiveTab] = useState(defaultTab) // ai, manual, batch const [activeTab, setActiveTab] = useState(defaultTab) // ai, manual, deal
const [form, setForm] = useState(getDefaultForm()) const [form, setForm] = useState(getDefaultForm())
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
const [error, setError] = useState('') const [error, setError] = useState('')
@ -405,15 +441,15 @@ export default function Add() {
<div style={{ display: 'flex', gap: '8px' }}> <div style={{ display: 'flex', gap: '8px' }}>
<button onClick={() => setActiveTab('ai')} <button onClick={() => setActiveTab('ai')}
style={{ flex: 1, padding: '10px', background: activeTab === 'ai' ? '#fbbf24' : 'rgba(255,255,255,0.05)', color: activeTab === 'ai' ? '#1e293b' : '#fff', border: 'none', borderRadius: '8px', fontSize: '14px', fontWeight: '600', cursor: 'pointer' }}> style={{ flex: 1, padding: '10px', background: activeTab === 'ai' ? '#fbbf24' : 'rgba(255,255,255,0.05)', color: activeTab === 'ai' ? '#1e293b' : '#fff', border: 'none', borderRadius: '8px', fontSize: '14px', fontWeight: '600', cursor: 'pointer' }}>
🤖 AI 识别 AI 识别
</button> </button>
<button onClick={() => setActiveTab('manual')} <button onClick={() => setActiveTab('manual')}
style={{ flex: 1, padding: '10px', background: activeTab === 'manual' ? '#fbbf24' : 'rgba(255,255,255,0.05)', color: activeTab === 'manual' ? '#1e293b' : '#fff', border: 'none', borderRadius: '8px', fontSize: '14px', fontWeight: '600', cursor: 'pointer' }}> style={{ flex: 1, padding: '10px', background: activeTab === 'manual' ? '#fbbf24' : 'rgba(255,255,255,0.05)', color: activeTab === 'manual' ? '#1e293b' : '#fff', border: 'none', borderRadius: '8px', fontSize: '14px', fontWeight: '600', cursor: 'pointer' }}>
手工录入 手工录入
</button> </button>
<button onClick={() => setActiveTab('batch')} disabled <button onClick={() => setActiveTab('deal')}
style={{ flex: 1, padding: '10px', background: 'rgba(255,255,255,0.02)', color: '#64748b', border: '1px solid rgba(255,255,255,0.05)', borderRadius: '8px', fontSize: '14px', fontWeight: '600', cursor: 'not-allowed' }}> style={{ flex: 1, padding: '10px', background: activeTab === 'deal' ? '#fbbf24' : 'rgba(255,255,255,0.05)', color: activeTab === 'deal' ? '#1e293b' : '#fff', border: 'none', borderRadius: '8px', fontSize: '14px', fontWeight: '600', cursor: 'pointer' }}>
📦 批量录入 行情录入
</button> </button>
</div> </div>
</div> </div>
@ -435,7 +471,7 @@ export default function Add() {
style={{ padding: '12px 24px', background: 'rgba(239, 68, 68, 0.2)', color: '#ef4444', border: '1px solid #ef4444', borderRadius: '8px', fontSize: '14px', cursor: 'pointer' }}>🗑 重新选择</button> style={{ padding: '12px 24px', background: 'rgba(239, 68, 68, 0.2)', color: '#ef4444', border: '1px solid #ef4444', borderRadius: '8px', fontSize: '14px', cursor: 'pointer' }}>🗑 重新选择</button>
<button onClick={handleRecognize} disabled={recognizing} <button onClick={handleRecognize} disabled={recognizing}
style={{ padding: '12px 24px', background: recognizing ? '#64748b' : '#fbbf24', color: recognizing ? '#94a3b8' : '#1e293b', border: 'none', borderRadius: '8px', fontSize: '14px', fontWeight: '600', cursor: recognizing ? 'not-allowed' : 'pointer' }}> style={{ padding: '12px 24px', background: recognizing ? '#64748b' : '#fbbf24', color: recognizing ? '#94a3b8' : '#1e293b', border: 'none', borderRadius: '8px', fontSize: '14px', fontWeight: '600', cursor: recognizing ? 'not-allowed' : 'pointer' }}>
{recognizing ? '🔍 识别中...' : '🤖 开始识别'} {recognizing ? '🔍 识别中...' : '开始识别'}
</button> </button>
</div> </div>
</div> </div>
@ -657,16 +693,137 @@ export default function Add() {
</div> </div>
)} )}
{/* 批量录入模式 */}
{activeTab === 'batch' && (
<div style={{ padding: '48px 16px', textAlign: 'center', color: '#94a3b8' }}> {/* 版本号 */}
<div style={{ fontSize: '48px', marginBottom: '16px' }}>🚧</div>
<div style={{ fontSize: '16px', fontWeight: 'bold', marginBottom: '8px' }}>批量录入开发中</div> {/* 行情录入模式 */}
<div style={{ fontSize: '13px' }}>敬请期待后续版本</div> {activeTab === 'deal' && (
<div style={{ padding: '16px' }}>
<div style={{ background: 'rgba(255,255,255,0.03)', borderRadius: '12px', padding: '20px', marginBottom: '16px' }}>
<div style={{ color: '#fbbf24', fontSize: '16px', fontWeight: 'bold', marginBottom: '16px' }}>成交行情录入</div>
<div style={{ marginBottom: '12px' }}>
<div style={{ color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>冠字号 *</div>
<input value={dealForm.serial} onChange={(e) => setDealForm({...dealForm, serial: e.target.value.toUpperCase(), category: autoCategory(e.target.value)})}
placeholder="J0xxxxxxxx"
style={{ width: '100%', padding: '12px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff', fontSize: '15px' }} />
</div>
{dealForm.category && (
<div style={{ background: 'rgba(251,191,36,0.2)', padding: '10px', borderRadius: '8px', textAlign: 'center', marginBottom: '12px' }}>
<div style={{ fontSize: '12px', color: '#94a3b8' }}>号码分类</div>
<div style={{ fontSize: '14px', color: '#fbbf24', fontWeight: 'bold' }}>{dealForm.category}</div>
</div>
)}
<div style={{ marginBottom: '12px' }}>
<div style={{ color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>包装类型</div>
<div style={{ display: 'flex', gap: '8px' }}>
{['单张', '标十', '标百'].map(p => (
<button key={p} onClick={() => setDealForm({...dealForm, packaging: p})}
style={{ flex: 1, padding: '10px', background: dealForm.packaging === p ? '#fbbf24' : 'rgba(255,255,255,0.05)', color: dealForm.packaging === p ? '#1e293b' : '#fff', border: 'none', borderRadius: '8px', fontSize: '13px', fontWeight: '600', cursor: 'pointer' }}>
{p}
</button>
))}
</div>
</div>
<div style={{ marginBottom: '12px' }}>
<div style={{ color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>成交价格 *</div>
<input type="number" value={dealForm.price} onChange={(e) => setDealForm({...dealForm, price: e.target.value})}
placeholder="请输入成交价格"
style={{ width: '100%', padding: '12px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff', fontSize: '15px' }} />
</div>
<div style={{ marginBottom: '12px' }}>
<div style={{ color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>成交平台 *</div>
<select value={dealForm.platform} onChange={(e) => setDealForm({...dealForm, platform: e.target.value})}
style={{ width: '100%', padding: '12px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff', fontSize: '15px' }}>
<option value="淘宝">淘宝</option>
<option value="咸鱼">咸鱼</option>
<option value="抖音">抖音</option>
<option value="微拍堂">微拍堂</option>
<option value="拼多多">拼多多</option>
<option value="其他">其他</option>
</select>
</div>
<div style={{ display: 'flex', gap: '12px', marginBottom: '12px' }}>
<div style={{ flex: 1 }}>
<div style={{ color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>出售者</div>
<input value={dealForm.seller} onChange={(e) => setDealForm({...dealForm, seller: e.target.value})}
placeholder="请输入出售者"
style={{ width: '100%', padding: '12px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff', fontSize: '15px' }} />
</div>
<div style={{ flex: 1 }}>
<div style={{ color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>购买者</div>
<input value={dealForm.buyer} onChange={(e) => setDealForm({...dealForm, buyer: e.target.value})}
placeholder="请输入购买者"
style={{ width: '100%', padding: '12px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff', fontSize: '15px' }} />
</div>
</div>
<div style={{ marginBottom: '16px' }}>
<div style={{ color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>成交日期 *</div>
<input type="date" value={dealForm.date} onChange={(e) => setDealForm({...dealForm, date: e.target.value})}
style={{ width: '100%', padding: '12px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff', fontSize: '15px' }} />
</div>
<button onClick={async () => {
if (!dealForm.serial || !dealForm.price || !dealForm.platform || !dealForm.date) {
alert('请填写所有必填项')
return
}
setSavingDeal(true)
try {
const token = localStorage.getItem('token')
//
const digits = dealForm.serial.replace('J', '').replace(/[^0-9]/g, '').slice(0, 9)
let tailNumber = '', sizeType = ''
if (dealForm.packaging === '标十' && digits.length >= 2) {
tailNumber = digits.slice(-2)
sizeType = ['01','11','21','31','41','51'].includes(tailNumber) ? '小号' : '大号'
} else if (dealForm.packaging === '标百' && digits.length >= 3) {
tailNumber = digits.slice(-3)
sizeType = ['101','201','301','401','501'].includes(tailNumber) ? '小号' : '大号'
}
const response = await fetch(`${API_BASE}/api/information/`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
info_type: 'deal',
title: `J0${dealForm.serial.replace('J', '').slice(0, 8)} - ¥${dealForm.price}`,
content: `分类: ${dealForm.category || '未分类'}\n尾号: ${tailNumber || '-'}\n大小号: ${sizeType || '-'}\n包装: ${dealForm.packaging}\n平台: ${dealForm.platform}\n价格: ¥${dealForm.price}\n出售者: ${dealForm.seller || '-'}\n购买者: ${dealForm.buyer || '-'}\n日期: ${dealForm.date}`,
deal_price: parseFloat(dealForm.price),
deal_date: dealForm.date,
number_category: dealForm.category,
tail_number: tailNumber,
packaging: dealForm.packaging,
size_type: sizeType
})
})
if (response.ok) {
alert('行情录入成功!')
setDealForm({ serial: '', category: '', packaging: '标十', price: '', platform: '淘宝', seller: '', buyer: '', date: new Date().toISOString().split('T')[0] })
} else {
const data = await response.json()
alert('录入失败: ' + (data.detail || '未知错误'))
}
} catch (e) {
alert('提交失败: ' + e.message)
} finally {
setSavingDeal(false)
}
}}
disabled={savingDeal}
style={{ width: '100%', padding: '14px', background: savingDeal ? '#64748b' : '#fbbf24', color: savingDeal ? '#94a3b8' : '#1e293b', border: 'none', borderRadius: '10px', fontSize: '16px', fontWeight: '600', cursor: savingDeal ? 'not-allowed' : 'pointer' }}>
{savingDeal ? '提交中...' : '提交行情'}
</button>
</div>
</div> </div>
)} )}
{/* 版本号 */}
<div style={{ position: 'fixed', bottom: '16px', right: '16px', color: 'rgba(255,255,255,0.2)', fontSize: '11px', zIndex: 100 }}>v{APP_VERSION}</div> <div style={{ position: 'fixed', bottom: '16px', right: '16px', color: 'rgba(255,255,255,0.2)', fontSize: '11px', zIndex: 100 }}>v{APP_VERSION}</div>
</div> </div>
) )

View File

@ -367,13 +367,13 @@ export default function Home() {
{(isAdmin ? [ {(isAdmin ? [
{ icon: '🏠', label: '首页', hash: '#/', idx: 0 }, { icon: '🏠', label: '首页', hash: '#/', idx: 0 },
{ icon: '📚', label: '藏品', hash: '#/list', idx: 1 }, { icon: '📚', label: '藏品', hash: '#/list', idx: 1 },
{ icon: '', label: '添加', hash: '#/ocr', idx: 2 }, { icon: '', label: '录入', hash: '#/ocr', idx: 2 },
{ icon: '📊', label: '统计', hash: '#/stats', idx: 3 }, { icon: '📊', label: '统计', hash: '#/stats', idx: 3 },
{ icon: '⚙️', label: '管理', hash: '#/admin', idx: 4 } { icon: '⚙️', label: '管理', hash: '#/admin', idx: 4 }
] : [ ] : [
{ icon: '🏠', label: '首页', hash: '#/', idx: 0 }, { icon: '🏠', label: '首页', hash: '#/', idx: 0 },
{ icon: '📚', label: '藏品', hash: '#/list', idx: 1 }, { icon: '📚', label: '藏品', hash: '#/list', idx: 1 },
{ icon: '', label: '添加', hash: '#/ocr', idx: 2 }, { icon: '', label: '录入', hash: '#/ocr', idx: 2 },
{ icon: '📊', label: '统计', hash: '#/stats', idx: 3 } { icon: '📊', label: '统计', hash: '#/stats', idx: 3 }
]).map((item) => ( ]).map((item) => (
<div key={item.idx} onClick={() => window.location.hash = item.hash} style={{ <div key={item.idx} onClick={() => window.location.hash = item.hash} style={{

View File

@ -1,680 +0,0 @@
import React, { useState, useEffect } from 'react'
// - /
export default function Info() {
//
if (!localStorage.getItem('token')) {
window.location.hash = '#/login'
return null
}
// tab /
const [activeTab, setActiveTab] = useState('manage')
const [showPublish, setShowPublish] = useState(false)
const [publishType, setPublishType] = useState('deal') // 'seek' 'deal'
const [myList, setMyList] = useState([])
const [loading, setLoading] = useState(false)
const [editingItem, setEditingItem] = useState(null)
const [filterType, setFilterType] = useState('all') // all/seek/deal
const [expandedItems, setExpandedItems] = useState({}) //
const [formData, setFormData] = useState({
edition: '龙钞', type: '标百', isGraded: true, category: '成交', source: '直播', title: ''
})
//
const [seekForm, setSeekForm] = useState({
edition: '龙钞', price: '', features: '', contact: '', content: '', title: ''
})
const API_BASE = localStorage.getItem('API_BASE') || ''
// /
const toggleExpand = (itemId) => {
setExpandedItems(prev => ({
...prev,
[itemId]: !prev[itemId]
}))
}
useEffect(() => {
if (activeTab === 'manage') fetchMyList()
}, [activeTab])
useEffect(() => {
//
const now = new Date()
const date = `${now.getFullYear()}/${now.getMonth()+1}/${now.getDate()}`
const grade = formData.isGraded ? '(评级币)' : '(裸钞)'
const title = `${date} ${formData.edition} ${formData.type} ${formData.category} 行情信息${grade}`
setFormData(prev => ({ ...prev, title }))
}, [formData.edition, formData.type, formData.isGraded, formData.category])
useEffect(() => {
//
if (seekForm.edition || seekForm.features) {
const title = `「寻号 ${seekForm.edition} J0${seekForm.features || 'XXXXXXXX'}`
setSeekForm(prev => ({ ...prev, title }))
}
}, [seekForm.edition, seekForm.features])
const fetchMyList = async () => {
setLoading(true)
try {
const token = localStorage.getItem('token')
const res = await fetch(`${API_BASE}/api/information/list`, {
headers: { Authorization: `Bearer ${token}` }
})
if (res.ok) {
const data = await res.json()
setMyList(data || [])
}
} catch (e) {
console.error(e)
}
setLoading(false)
}
//
const handlePublishSeek = async () => {
if (!seekForm.contact) {
alert('请填写联系方式')
return
}
try {
const token = localStorage.getItem('token')
const res = await fetch(`${API_BASE}/api/information/`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: seekForm.title,
content: document.getElementById('seekContent')?.value || seekForm.content,
info_type: 'seek',
expect_category: seekForm.edition,
expect_number: seekForm.features ? `J0${seekForm.features}` : null
})
})
const data = await res.json()
if (data.id || data.code === 0) {
alert('发布成功!')
setShowPublish(false)
setSeekForm({ edition: '龙钞', price: '', features: '', contact: '', content: '', title: '' })
const contentEl = document.getElementById('seekContent')
if (contentEl) contentEl.value = ''
fetchMyList()
} else {
alert(data.message || '发布失败')
}
} catch (e) {
alert('发布失败: ' + e.message)
}
}
//
const handlePublishDeal = async () => {
const content = document.getElementById('publishContent')?.value || ''
if (!formData.title) {
alert('请填写标题')
return
}
try {
const token = localStorage.getItem('token')
const res = await fetch(`${API_BASE}/api/information/`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: formData.title,
content: content,
info_type: formData.category === '成交' ? 'deal' : 'seek'
})
})
const data = await res.json()
if (data.id || data.code === 0) {
alert('发布成功!')
setShowPublish(false)
setFormData({ edition: '龙钞', type: '标百', isGraded: true, category: '成交', source: '直播', title: '' })
const contentEl = document.getElementById('publishContent')
if (contentEl) contentEl.value = ''
fetchMyList()
} else {
alert(data.message || '发布失败')
}
} catch (e) {
alert('发布失败: ' + e.message)
}
}
//
const handleDelete = async (id) => {
if (!confirm('确定删除这条信息吗?')) return
try {
const token = localStorage.getItem('token')
const res = await fetch(`${API_BASE}/api/information/${id}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }
})
if (res.ok) {
alert('删除成功')
fetchMyList()
} else {
alert('删除失败')
}
} catch (e) {
alert('删除失败: ' + e.message)
}
}
//
const handleEdit = (item) => {
setEditingItem({ id: item.id, title: item.title, content: item.content })
}
const saveEdit = async () => {
if (!editingItem) return
try {
const token = localStorage.getItem('token')
const res = await fetch(`${API_BASE}/api/information/${editingItem.id}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ title: editingItem.title, content: editingItem.content })
})
if (res.ok) {
alert('保存成功')
setEditingItem(null)
fetchMyList()
} else {
alert('保存失败')
}
} catch (e) {
alert('保存失败: ' + e.message)
}
}
const getUserPhone = () => {
try {
const user = JSON.parse(localStorage.getItem('user') || '{}')
return user.phone || user.phoneNumber || user.mobile || user.tel || ''
} catch {
return ''
}
}
useEffect(() => {
setSeekForm(prev => ({ ...prev, contact: getUserPhone() }))
}, [])
//
const formatDate = (date) => {
if (!date) return '-'
return new Date(date).toLocaleString('zh-CN').slice(0, 16)
}
//
const filteredList = myList.filter(item => filterType === 'all' || item.info_type === filterType)
return (
<div style={{ minHeight: '100vh', background: 'linear-gradient(180deg, #0f172a 0%, #1e293b 100%)', paddingBottom: '80px' }}>
{/* 顶部 Tab 切换 */}
<div style={{ position: 'sticky', top: 0, background: 'rgba(15,23,42,0.95)', backdropFilter: 'blur(10px)', padding: '16px 20px', borderBottom: '1px solid #1e293b', zIndex: 100 }}>
<div style={{ display: 'flex', gap: '8px', background: '#1e293b', borderRadius: '12px', padding: '4px' }}>
{[
{ key: 'manage', label: '📋 发布管理' }
].map(tab => (
<div
key={tab.key}
onClick={() => setActiveTab(tab.key)}
style={{
flex: 1,
padding: '12px 16px',
borderRadius: '10px',
background: activeTab === tab.key ? 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)' : 'transparent',
color: activeTab === tab.key ? '#fff' : '#9ca3af',
cursor: 'pointer',
textAlign: 'center',
fontSize: '14px',
fontWeight: '600',
transition: 'all 0.3s'
}}
>
{tab.label}
</div>
))}
</div>
</div>
<div style={{ padding: '20px' }}>
{/* 寻配号发布页 - 已删除 */}
{false && (
<div style={{ background: 'linear-gradient(145deg, #1f2937 0%, #111827 100%)', borderRadius: '16px', padding: '20px', border: '1px solid #374151', boxShadow: '0 4px 20px rgba(0,0,0,0.3)' }}>
<h3 style={{ color: '#f9fafb', margin: '0 0 20px 0', fontSize: '18px', fontWeight: '600' }}>🔍 寻配号发布</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<div>
<label style={{ color: '#9ca3af', fontSize: '13px', display: 'block', marginBottom: '8px' }}>版别</label>
<div style={{ display: 'flex', gap: '8px' }}>
{['龙钞', '马钞', '蛇钞'].map(ed => {
const selected = seekForm.edition === ed
return (
<button
key={ed}
onClick={() => setSeekForm({ ...seekForm, edition: ed })}
style={{
flex: 1,
padding: '10px',
borderRadius: '8px',
border: selected ? '2px solid #10b981' : '1px solid #374151',
background: selected ? 'rgba(16,185,129,0.15)' : '#1f2937',
color: selected ? '#10b981' : '#d1d5db',
cursor: 'pointer',
fontSize: '14px',
fontWeight: '500'
}}
>
{ed}
</button>
)
})}
</div>
</div>
<div>
<label style={{ color: '#9ca3af', fontSize: '13px', display: 'block', marginBottom: '8px' }}>号码特征8</label>
<input
type="text"
value={seekForm.features}
onChange={e => setSeekForm({ ...seekForm, features: e.target.value.toUpperCase().slice(0, 8) })}
placeholder="输入号码特征12345678"
maxLength={8}
style={{ width: '100%', padding: '12px', background: '#1f2937', border: '1px solid #374151', borderRadius: '10px', color: '#fff', fontSize: '14px', boxSizing: 'border-box' }}
/>
<div style={{ color: '#6b7280', fontSize: '11px', marginTop: '4px' }}>X=任意数字 A=非4 B=非47 C=非347 D=非247</div>
</div>
<div>
<label style={{ color: '#9ca3af', fontSize: '13px', display: 'block', marginBottom: '8px' }}>标题自动生成</label>
<input
type="text"
value={seekForm.title}
readOnly
style={{ width: '100%', padding: '12px', background: '#1f2937', border: '1px solid #374151', borderRadius: '10px', color: '#10b981', fontSize: '14px', boxSizing: 'border-box' }}
/>
</div>
<div>
<label style={{ color: '#9ca3af', fontSize: '13px', display: 'block', marginBottom: '8px' }}>正文</label>
<textarea
id="seekContent"
placeholder="请输入详细信息..."
style={{ width: '100%', padding: '12px', background: '#1f2937', border: '1px solid #374151', borderRadius: '10px', color: '#fff', fontSize: '14px', boxSizing: 'border-box', resize: 'vertical', minHeight: '80px' }}
/>
</div>
<div>
<label style={{ color: '#ef4444', fontSize: '13px', display: 'block', marginBottom: '8px' }}>联系方式 *</label>
<input
type="text"
value={seekForm.contact}
onChange={e => setSeekForm({ ...seekForm, contact: e.target.value })}
placeholder="请输入手机号或微信"
style={{ width: '100%', padding: '12px', background: '#1f2937', border: '1px solid #374151', borderRadius: '10px', color: '#fff', fontSize: '14px', boxSizing: 'border-box' }}
/>
</div>
<button
onClick={handlePublishSeek}
style={{
width: '100%',
padding: '14px',
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
border: 'none',
borderRadius: '10px',
color: '#fff',
fontSize: '16px',
fontWeight: 'bold',
cursor: 'pointer'
}}
>
🚀 发布寻配号
</button>
</div>
</div>
)}
{/* 发布管理页 */}
{activeTab === 'manage' && (
<div>
{/* 新增发布区域 - 发布行情信息 */}
<div style={{ background: 'linear-gradient(145deg, #1f2937 0%, #111827 100%)', borderRadius: '16px', padding: '20px', marginBottom: '16px', border: '1px solid #374151', boxShadow: '0 4px 20px rgba(0,0,0,0.3)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
<h3 style={{ color: '#f9fafb', margin: 0, fontSize: '16px', fontWeight: '600' }}>📋 发布管理</h3>
<button
onClick={() => setShowPublish(!showPublish)}
style={{
padding: '10px 20px',
background: showPublish ? 'rgba(108,114,132,0.5)' : 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
border: 'none',
borderRadius: '10px',
color: '#fff',
cursor: 'pointer',
fontSize: '14px',
fontWeight: '600'
}}
>
{showPublish ? '✕ 收起' : '+ 💰 新增行情发布'}
</button>
</div>
{/* 行情发布表单 */}
{showPublish && (
<div style={{ background: '#0f172a', borderRadius: '12px', padding: '16px', marginBottom: '16px' }}>
<div style={{ color: '#f59e0b', fontSize: '14px', fontWeight: '600', marginBottom: '16px' }}>💰 发布行情信息</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<div style={{ display: 'flex', gap: '12px' }}>
<div style={{ flex: 2 }}>
<label style={{ color: '#9ca3af', fontSize: '12px', display: 'block', marginBottom: '6px' }}>版别</label>
<div style={{ display: 'flex', gap: '6px' }}>
{['龙钞', '马钞', '蛇钞', '其他'].map(ed => (
<button
key={ed}
onClick={() => setFormData({ ...formData, edition: ed })}
style={{
flex: 1,
padding: '8px',
borderRadius: '6px',
border: formData.edition === ed ? '2px solid #f59e0b' : '1px solid #374151',
background: formData.edition === ed ? 'rgba(245,158,11,0.15)' : '#1f2937',
color: formData.edition === ed ? '#f59e0b' : '#d1d5db',
cursor: 'pointer',
fontSize: '12px'
}}
>
{ed}
</button>
))}
</div>
</div>
</div>
<div style={{ display: 'flex', gap: '12px' }}>
<div style={{ flex: 2 }}>
<label style={{ color: '#9ca3af', fontSize: '12px', display: 'block', marginBottom: '6px' }}>类型</label>
<div style={{ display: 'flex', gap: '6px' }}>
{['标百', '标十', '单张'].map(t => (
<button
key={t}
onClick={() => setFormData({ ...formData, type: t })}
style={{
flex: 1,
padding: '8px',
borderRadius: '6px',
border: formData.type === t ? '2px solid #f59e0b' : '1px solid #374151',
background: formData.type === t ? 'rgba(245,158,11,0.15)' : '#1f2937',
color: formData.type === t ? '#f59e0b' : '#d1d5db',
cursor: 'pointer',
fontSize: '12px'
}}
>
{t}
</button>
))}
</div>
</div>
<div style={{ flex: 1 }}>
<label style={{ color: '#9ca3af', fontSize: '12px', display: 'block', marginBottom: '6px' }}>是否评级</label>
<button
onClick={() => setFormData({ ...formData, isGraded: !formData.isGraded })}
style={{
width: '100%',
padding: '8px',
borderRadius: '6px',
border: formData.isGraded ? '2px solid #f59e0b' : '1px solid #374151',
background: formData.isGraded ? 'rgba(245,158,11,0.15)' : '#1f2937',
color: formData.isGraded ? '#f59e0b' : '#d1d5db',
cursor: 'pointer',
fontSize: '12px'
}}
>
{formData.isGraded ? '✓ 评级币' : '○ 裸钞'}
</button>
</div>
</div>
<div>
<label style={{ color: '#9ca3af', fontSize: '12px', display: 'block', marginBottom: '6px' }}>分类</label>
<div style={{ display: 'flex', gap: '8px' }}>
{[{value:'成交',label:'💰 成交'},{value:'求购',label:'🔍 求购'},{value:'出售',label:'💵 出售'}].map(opt => (
<button
key={opt.value}
onClick={() => setFormData({ ...formData, category: opt.value })}
style={{
flex: 1,
padding: '10px',
borderRadius: '8px',
border: formData.category === opt.value ? '2px solid #f59e0b' : '1px solid #374151',
background: formData.category === opt.value ? 'rgba(245,158,11,0.15)' : '#1f2937',
color: formData.category === opt.value ? '#f59e0b' : '#d1d5db',
cursor: 'pointer',
fontSize: '13px',
fontWeight: '500'
}}
>
{opt.label}
</button>
))}
</div>
</div>
<div>
<label style={{ color: '#9ca3af', fontSize: '12px', display: 'block', marginBottom: '6px' }}>标题自动生成</label>
<input
type="text"
value={formData.title}
readOnly
style={{ width: '100%', padding: '12px', background: '#1f2937', border: '1px solid #374151', borderRadius: '10px', color: '#10b981', fontSize: '14px', boxSizing: 'border-box' }}
/>
</div>
<div>
<label style={{ color: '#9ca3af', fontSize: '12px', display: 'block', marginBottom: '6px' }}>正文</label>
<textarea
id="publishContent"
placeholder="请输入行情详细信息..."
style={{ width: '100%', padding: '12px', background: '#1f2937', border: '1px solid #374151', borderRadius: '10px', color: '#fff', fontSize: '14px', boxSizing: 'border-box', resize: 'vertical', minHeight: '80px' }}
/>
</div>
<button
onClick={handlePublishDeal}
style={{
width: '100%',
padding: '14px',
background: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
border: 'none',
borderRadius: '10px',
color: '#fff',
fontSize: '15px',
fontWeight: 'bold',
cursor: 'pointer'
}}
>
🚀 提交发布
</button>
</div>
</div>
)}
</div>
{/* 列表过滤:全部 / 寻号 / 行情 */}
<div style={{ display: 'flex', gap: '8px', marginBottom: '16px' }}>
<button
onClick={() => setFilterType('all')}
style={{
padding: '8px 16px',
background: filterType === 'all' ? '#3b82f6' : '#374151',
border: 'none',
borderRadius: '6px',
color: '#fff',
fontSize: '13px',
cursor: 'pointer'
}}
>
全部
</button>
<button
onClick={() => setFilterType('seek')}
style={{
padding: '8px 16px',
background: filterType === 'seek' ? '#10b981' : '#374151',
border: 'none',
borderRadius: '6px',
color: '#fff',
fontSize: '13px',
cursor: 'pointer'
}}
>
🔍 寻号
</button>
<button
onClick={() => setFilterType('deal')}
style={{
padding: '8px 16px',
background: filterType === 'deal' ? '#f59e0b' : '#374151',
border: 'none',
borderRadius: '6px',
color: '#fff',
fontSize: '13px',
cursor: 'pointer'
}}
>
💰 行情
</button>
</div>
{/* 发布列表 */}
{loading ? (
<div style={{ color: '#9ca3af', textAlign: 'center', padding: '30px' }}>加载中...</div>
) : filteredList.length === 0 ? (
<div style={{ color: '#9ca3af', textAlign: 'center', padding: '30px', fontSize: '14px' }}>暂无发布记录</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{filteredList.map(item => (
<div key={item.id} style={{ background: '#1e293b', borderRadius: '12px', padding: '16px', border: '1px solid #334155' }}>
{/* 标题行 */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '8px' }}>
<div style={{ color: '#fbbf24', fontSize: '15px', fontWeight: '600', flex: 1 }}>{item.title}</div>
<span style={{
background: item.info_type === 'deal' ? 'rgba(245,158,11,0.2)' : 'rgba(16,185,129,0.2)',
color: item.info_type === 'deal' ? '#f59e0b' : '#10b981',
padding: '4px 10px',
borderRadius: '20px',
fontSize: '12px'
}}>
{item.info_type === 'deal' ? '💰 行情' : '🔍 寻号'}
</span>
</div>
{/* 日期+用户名 */}
<div style={{ color: '#64748b', fontSize: '12px', marginBottom: '8px' }}>
📅 {formatDate(item.created_at)} &nbsp;|&nbsp; 👤 {item.user_name || '匿名用户'}
</div>
{/* 正文 - 默认收起,点击展开 */}
{item.content && (
<div style={{ marginBottom: '12px' }}>
<div
onClick={() => toggleExpand(item.id)}
style={{
color: '#10b981',
fontSize: '13px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: '6px',
padding: '8px 12px',
background: 'rgba(16,185,129,0.1)',
borderRadius: '6px',
border: '1px solid rgba(16,185,129,0.2)'
}}
>
<span style={{ fontSize: '16px' }}>{expandedItems[item.id] ? '▼' : '▶'}</span>
<span>{expandedItems[item.id] ? '收起详情' : '展开查看详情'}</span>
</div>
{expandedItems[item.id] && (
<div style={{
color: '#e2e8f0',
fontSize: '14px',
lineHeight: '1.8',
marginTop: '12px',
background: 'rgba(255,255,255,0.03)',
padding: '12px',
borderRadius: '8px',
borderLeft: '3px solid #10b981'
}}>
{item.content.split('\n').map((line, i) => (
<div key={i} style={{ marginBottom: i < item.content.split('\n').length - 1 ? '6px' : 0 }}>
{line || ' '}
</div>
))}
</div>
)}
</div>
)}
{/* 操作按钮 */}
<div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', marginTop: '8px' }}>
<div style={{ display: 'flex', gap: '8px' }}>
<button onClick={() => handleEdit(item)} style={{ padding: '6px 12px', borderRadius: '6px', border: '1px solid #3b82f6', background: 'transparent', color: '#3b82f6', cursor: 'pointer', fontSize: '12px' }}> 编辑</button>
<button onClick={() => handleDelete(item.id)} style={{ padding: '6px 12px', borderRadius: '6px', border: '1px solid #ef4444', background: 'transparent', color: '#ef4444', cursor: 'pointer', fontSize: '12px' }}>🗑 删除</button>
</div>
</div>
</div>
))}
</div>
)}
</div>
)}
</div>
{/* 编辑弹窗 */}
{editingItem && (
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.8)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000, padding: '20px' }}>
<div style={{ background: '#1f2937', borderRadius: '16px', padding: '24px', width: '100%', maxWidth: '420px', border: '1px solid #374151' }}>
<h3 style={{ color: '#f9fafb', marginBottom: '20px', fontSize: '18px', fontWeight: '600' }}> 编辑信息</h3>
<div style={{ marginBottom: '16px' }}>
<label style={{ color: '#9ca3af', fontSize: '13px', display: 'block', marginBottom: '8px' }}>标题</label>
<input
type="text"
value={editingItem.title}
onChange={e => setEditingItem({ ...editingItem, title: e.target.value })}
style={{ width: '100%', padding: '12px', background: '#0f172a', border: '1px solid #374151', borderRadius: '8px', color: '#fff', boxSizing: 'border-box', fontSize: '14px' }}
/>
</div>
<div style={{ marginBottom: '20px' }}>
<label style={{ color: '#9ca3af', fontSize: '13px', display: 'block', marginBottom: '8px' }}>内容</label>
<textarea
value={editingItem.content}
onChange={e => setEditingItem({ ...editingItem, content: e.target.value })}
rows={4}
style={{ width: '100%', padding: '12px', background: '#0f172a', border: '1px solid #374151', borderRadius: '8px', color: '#fff', boxSizing: 'border-box', fontSize: '14px', resize: 'vertical' }}
/>
</div>
<div style={{ display: 'flex', gap: '12px' }}>
<button onClick={saveEdit} style={{ flex: 1, padding: '12px', background: '#10b981', border: 'none', borderRadius: '8px', color: '#fff', cursor: 'pointer', fontSize: '14px', fontWeight: '600' }}>💾 保存</button>
<button onClick={() => setEditingItem(null)} style={{ flex: 1, padding: '12px', border: '1px solid #374151', borderRadius: '8px', background: 'transparent', color: '#9ca3af', cursor: 'pointer', fontSize: '14px' }}>取消</button>
</div>
</div>
</div>
)}
</div>
)
}

View File

@ -337,9 +337,9 @@ export default function News() {
// Tab // Tab
const tabs = [ const tabs = [
{ key: 'seek', label: '🔍 寻配号' }, { key: 'seek', label: '寻配号' },
{ key: 'deal', label: '💰 成交行情' }, { key: 'deal', label: '成交行情' },
{ key: 'yichen', label: '📊 一尘看板' } { key: 'yichen', label: '一尘看板' }
] ]
const formatDate = (dateStr) => { const formatDate = (dateStr) => {
@ -459,7 +459,7 @@ export default function News() {
</div> </div>
)} )}
<h3 style={{ color: '#fff', marginBottom: '16px' }}> <h3 style={{ color: '#fff', marginBottom: '16px' }}>
{activeTab === 'seek' ? '🔍 寻配号信息' : activeTab === 'deal' ? '💰 成交行情信息' : '📊 一尘看板'} {activeTab === 'seek' ? '寻配号信息' : activeTab === 'deal' ? '成交行情信息' : '一尘看板'}
{activeTab === 'seek' && ( {activeTab === 'seek' && (
<div style={{ display: 'flex', gap: '8px', marginLeft: 'auto' }}> <div style={{ display: 'flex', gap: '8px', marginLeft: 'auto' }}>
<button onClick={() => { setViewMode('all'); fetchInfoList(); }} style={{ padding: '6px 12px', background: viewMode==='all'?'#3b82f6':'#1e293b', border: '1px solid #334155', borderRadius: '6px', color: '#fff', fontSize: '12px' }}>全部</button> <button onClick={() => { setViewMode('all'); fetchInfoList(); }} style={{ padding: '6px 12px', background: viewMode==='all'?'#3b82f6':'#1e293b', border: '1px solid #334155', borderRadius: '6px', color: '#fff', fontSize: '12px' }}>全部</button>

View File

@ -0,0 +1,129 @@
/**
* 号码分类工具
* 根据冠字号自动分类为圆圆号倒置号金马王金马号金山王天马王金山号天马号朦胧号如意号钻石号永恒号带7号带4号
*/
// 分类定义(按优先级排序)
const CATEGORIES = [
{ name: '圆圆号', mustNot: ['1','2','3','4','5','7'], mustHave: [] },
{ name: '倒置号', mustNot: ['2','3','4','5','7'], mustHave: ['1'] },
{ name: '金马王', mustNot: ['1','2','3','4','7'], mustHave: ['5'] },
{ name: '金马号', mustNot: ['2','3','4','7'], mustHave: ['1','5'] },
{ name: '金山王', mustNot: ['1','2','4','5','7'], mustHave: ['3'] },
{ name: '天马王', mustNot: ['1','2','4','7'], mustHave: ['3','5'] },
{ name: '金山号', mustNot: ['2','4','5','7'], mustHave: ['1','3'] },
{ name: '天马号', mustNot: ['2','4','7'], mustHave: ['1','3','5'] },
{ name: '朦胧号', mustNot: ['3','4','5','7'], mustHave: [] },
{ name: '如意号', mustNot: ['1','3','4','7'], mustHave: [] },
{ name: '钻石号', mustNot: ['3','4','7'], mustHave: [] },
{ name: '永恒号', mustNot: ['4','7'], mustHave: [] },
{ name: '带7号', mustNot: ['4'], mustHave: ['7'] },
{ name: '带4号', mustNot: [], mustHave: ['4'] },
]
/**
* 提取冠字号中的数字部分
* @param {string} serial - 冠字号 J0123456789 J0123456781 J0123456701
* @returns {object} - { digits: 数字串, type: 'single'|'ten'|'hundred' }
*/
export function extractDigits(serial) {
if (!serial) return { digits: '', type: 'single' }
// 去掉J取数字部分
const nums = serial.replace(/J/g, '').replace(/\D/g, '')
// 判断类型
if (nums.endsWith('01')) {
// 标百去掉最后2位
return { digits: nums.slice(0, -2), type: 'hundred' }
} else if (nums.endsWith('1')) {
// 标十去掉最后1位
return { digits: nums.slice(0, -1), type: 'ten' }
} else {
// 单张:全部数字
return { digits: nums, type: 'single' }
}
}
/**
* 号码分类函数
* @param {string} serial - 冠字号
* @returns {string} - 分类名称
*/
export function getNumberCategory(serial) {
const { digits } = extractDigits(serial)
if (!digits || digits.length < 7) {
return '其他'
}
// 按优先级匹配
for (const cat of CATEGORIES) {
if (matchesCategory(digits, cat)) {
return cat.name
}
}
return '其他'
}
/**
* 检查数字是否匹配分类条件
* @param {string} digits - 数字串
* @param {object} category - 分类定义
* @returns {boolean}
*/
function matchesCategory(digits, category) {
const mustNot = category.mustNot
const mustHave = category.mustHave
// 1. 检查必须不含的数字
for (const n of mustNot) {
if (digits.includes(n)) {
return false
}
}
// 2. 检查必须含有的数字
for (const n of mustHave) {
if (!digits.includes(n)) {
return false
}
}
return true
}
/**
* 获取分类颜色
* @param {string} category - 分类名称
* @returns {string} - 颜色 hex
*/
export function getNumberCategoryColor(category) {
const colors = {
'圆圆号': '#8b5cf6', // 紫
'倒置号': '#ec4899', // 粉
'金马王': '#f59e0b', // 金
'金马号': '#ef4444', // 红
'金山王': '#14b8a6', // 青
'天马王': '#06b6d4', // 蓝
'金山号': '#0d9488', // 绿松石
'天马号': '#22c55e', // 绿
'朦胧号': '#6366f1', // 靛蓝
'如意号': '#a855f7', // 紫红
'钻石号': '#eab308', // 黄
'永恒号': '#3b82f6', // <20><><EFBFBD>
'带7号': '#f97316', // 橙
'带4号': '#64748b', // 灰
'其他': '#94a3b8',
}
return colors[category] || colors['其他']
}
/**
* 分类优先级用于排序
*/
export const NUMBER_CATEGORY_ORDER = CATEGORIES.map(c => c.name)
// 可选值列表(用于下拉框)
export const NUMBER_CATEGORY_OPTIONS = CATEGORIES.map(c => ({ value: c.name, label: c.name }))