diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py
index 0f70470..0c6fe73 100644
--- a/backend/app/routers/auth.py
+++ b/backend/app/routers/auth.py
@@ -13,7 +13,7 @@ router = APIRouter(prefix="/api/auth", tags=["认证"])
def generate_user_code(db):
- """生成用户编码,从201开始,按自然数顺序递增"""
+ """生成用户编码,从201开始,按自然数顺序递增,跳过已存在的"""
# 查找最大的user_code
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]:
@@ -21,6 +21,9 @@ def generate_user_code(db):
num = int(max_code[0]) + 1
if num < 201:
num = 201
+ # 检查是否已存在,如果存在则继续递增
+ while db.query(User).filter(User.user_code == str(num)).first():
+ num += 1
return str(num)
except:
pass
@@ -100,7 +103,20 @@ def register(user_data: UserCreate, db: Session = Depends(get_db)):
db.commit()
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)
diff --git a/backend/app/routers/collections.py b/backend/app/routers/collections.py
index 7fbf85a..d04bb8f 100644
--- a/backend/app/routers/collections.py
+++ b/backend/app/routers/collections.py
@@ -132,14 +132,17 @@ def get_collections(
# 如果指定 all_users=true,则返回所有用户藏品
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(
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参数,则只返回该用户的藏品
if user_id:
@@ -202,7 +205,7 @@ def get_collections(
data_list = []
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
else:
collection_item = item
@@ -278,12 +281,12 @@ def get_stats(
):
"""获取藏品统计"""
# 获取所有藏品
- if current_user.role == "admin":
- all_collections = db.query(Collection).all()
- else:
+ if current_user is None or current_user.role != "admin":
all_collections = db.query(Collection).filter(
Collection.f99_91_user_id == current_user.f99_90_id
).all()
+ else:
+ all_collections = db.query(Collection).all()
# 总数
total_count = len(all_collections)
@@ -382,7 +385,7 @@ def get_collection(
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="无权访问")
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(
f99_91_user_id=current_user.f99_90_id,
f01_01_name=collection_data.f01_01_name,
@@ -709,7 +717,7 @@ async def delete_image(
Collection.f99_90_id == image.collection_id
).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: 无权删除此图片")
# 删除OSS文件(如果path是OSS URL)
diff --git a/backend/app/routers/users.py b/backend/app/routers/users.py
index 079be6e..c8cf2da 100644
--- a/backend/app/routers/users.py
+++ b/backend/app/routers/users.py
@@ -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(
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
}
-@router.put("/users/me", response_model=UserResponse)
+@router.put("/users/me") # 无 response_model,避免 Pydantic 序列化问题
def update_current_user(
user_update: UserUpdate,
current_user: User = Depends(get_current_user),
diff --git a/backend/app/utils/number_category.py b/backend/app/utils/number_category.py
new file mode 100644
index 0000000..340e9f2
--- /dev/null
+++ b/backend/app/utils/number_category.py
@@ -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
\ No newline at end of file
diff --git a/config/VERSION b/config/VERSION
index 8a27477..f73f4c0 100644
--- a/config/VERSION
+++ b/config/VERSION
@@ -1 +1 @@
-VERSION=1.2.79
+VERSION=1.2.81
diff --git a/frontend/index.html b/frontend/index.html
index 803dee4..e3a4d95 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -4,7 +4,7 @@
-
甲辰收藏 v=1.2.78
+ 甲辰收藏 v=1.2.81
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 95c053e..79e65c8 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "jiachenlong-frontend",
- "version": "1.2.72",
+ "version": "1.2.81",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "jiachenlong-frontend",
- "version": "1.2.72",
+ "version": "1.2.81",
"dependencies": {
"axios": "^1.7.9",
"react": "^18.3.1",
@@ -15,7 +15,7 @@
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.4",
- "vite": "^6.0.7"
+ "vite": "^6.4.2"
}
},
"node_modules/@babel/code-frame": {
@@ -2047,9 +2047,9 @@
}
},
"node_modules/vite": {
- "version": "6.4.1",
- "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
- "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
+ "version": "6.4.2",
+ "resolved": "https://registry.npmmirror.com/vite/-/vite-6.4.2.tgz",
+ "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
"dev": true,
"license": "MIT",
"dependencies": {
diff --git a/frontend/package.json b/frontend/package.json
index d1973e5..c37ff78 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,6 +1,6 @@
{
"name": "jiachenlong-frontend",
- "version": "1.2.77",
+ "version": "1.2.81",
"private": true,
"description": "甲辰藏品管理系统 - 移动端前端",
"scripts": {
@@ -9,13 +9,13 @@
"preview": "vite preview"
},
"dependencies": {
+ "axios": "^1.7.9",
"react": "^18.3.1",
"react-dom": "^18.3.1",
- "react-router-dom": "^7.1.0",
- "axios": "^1.7.9"
+ "react-router-dom": "^7.1.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.4",
- "vite": "^6.0.7"
+ "vite": "^6.4.2"
}
}
diff --git a/frontend/package.txt b/frontend/package.txt
new file mode 100644
index 0000000..1d01bc1
--- /dev/null
+++ b/frontend/package.txt
@@ -0,0 +1 @@
+v=1.2.80
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index ae35683..a123fa5 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -5,7 +5,6 @@ import List from './pages/List'
import Add from './pages/Add'
import Stats from './pages/Stats'
import News from './pages/News'
-import Info from './pages/Info'
import Login from './pages/Login'
import Detail from './pages/Detail'
import Edit from './pages/Edit'
@@ -31,7 +30,6 @@ export default function App() {
const basePath = path.split('?')[0]
if (basePath === '/') return
if (basePath === '/news') return
- if (basePath === '/info') return
if (basePath === '/stats') return
if (basePath === '/list') return
if (basePath === '/add') return
@@ -53,7 +51,6 @@ export default function App() {
}
const isAdmin = user && user.role === 'admin'
const isEditor = user && user.role === 'editor'
- const canAccessInfo = isAdmin || isEditor
// 登录页面独立渲染,不显示底部导航
if (!token) {
@@ -91,8 +88,7 @@ export default function App() {
{ path: '/news', icon: '📰', label: '资讯' },
{ path: '/stats', icon: '📊', label: '统计' },
{ path: '/list', icon: '📚', label: '藏品' },
- { path: '/add', icon: '🎯', label: '添加' },
- ...(canAccessInfo ? [{ path: '/info', icon: '📝', label: '信息' }] : []),
+ { path: '/add', icon: '🎯', label: '录入' },
...(isAdmin ? [{ path: '/admin', icon: '⚙️', label: '管理' }] : [])
].map(tab => (
)
}
+// Fri Apr 10 03:44:24 PM CST 2026
diff --git a/frontend/src/config/version.js b/frontend/src/config/version.js
index cf4abcc..e808cde 100644
--- a/frontend/src/config/version.js
+++ b/frontend/src/config/version.js
@@ -3,7 +3,7 @@
// 修改版本号只需修改 ../../VERSION 文件中的 VERSION=xxx
// 从环境变量读取(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 = {
diff --git a/frontend/src/pages/Add.jsx b/frontend/src/pages/Add.jsx
index f09e0cb..a4bc066 100644
--- a/frontend/src/pages/Add.jsx
+++ b/frontend/src/pages/Add.jsx
@@ -1,6 +1,7 @@
-// 添加藏品页面 - 支持 AI 识别/手工录入/批量录入
+// 添加藏品页面 - 支持 AI 识别/手工录入
import React, { useState, useRef } from 'react'
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:'其他'}];
// 字段转换函数
@@ -71,12 +72,47 @@ const getDefaultForm = () => ({
})
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 参数确定默认标签
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 [activeTab, setActiveTab] = useState(defaultTab) // ai, manual, deal
const [form, setForm] = useState(getDefaultForm())
const [saving, setSaving] = useState(false)
const [error, setError] = useState('')
@@ -405,15 +441,15 @@ export default function Add() {
-
@@ -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' }}>🗑️ 重新选择
- {recognizing ? '🔍 识别中...' : '🤖 开始识别'}
+ {recognizing ? '🔍 识别中...' : '开始识别'}
@@ -657,16 +693,137 @@ export default function Add() {
)}
- {/* 批量录入模式 */}
- {activeTab === 'batch' && (
-
-
🚧
-
批量录入开发中
-
敬请期待后续版本
-
- )}
+
{/* 版本号 */}
+
+ {/* 行情录入模式 */}
+ {activeTab === 'deal' && (
+
+
+
成交行情录入
+
+
+
冠字号 *
+
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' }} />
+
+
+ {dealForm.category && (
+
+
号码分类
+
{dealForm.category}
+
+ )}
+
+
+
包装类型
+
+ {['单张', '标十', '标百'].map(p => (
+ 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}
+
+ ))}
+
+
+
+
+
成交价格 *
+
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' }} />
+
+
+
+
成交平台 *
+
+
+
+
+
+
出售者
+
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' }} />
+
+
+
购买者
+
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' }} />
+
+
+
+
+
成交日期 *
+
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' }} />
+
+
+
{
+ 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 ? '提交中...' : '提交行情'}
+
+
+
+ )}
+
v{APP_VERSION}
)
diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx
index 6c38011..1476144 100644
--- a/frontend/src/pages/Home.jsx
+++ b/frontend/src/pages/Home.jsx
@@ -367,13 +367,13 @@ export default function Home() {
{(isAdmin ? [
{ icon: '🏠', label: '首页', hash: '#/', idx: 0 },
{ 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: '#/admin', idx: 4 }
] : [
{ icon: '🏠', label: '首页', hash: '#/', idx: 0 },
{ icon: '📚', label: '藏品', hash: '#/list', idx: 1 },
- { icon: '➕', label: '添加', hash: '#/ocr', idx: 2 },
+ { icon: '➕', label: '录入', hash: '#/ocr', idx: 2 },
{ icon: '📊', label: '统计', hash: '#/stats', idx: 3 }
]).map((item) => (
window.location.hash = item.hash} style={{
diff --git a/frontend/src/pages/Info.jsx b/frontend/src/pages/Info.jsx
deleted file mode 100644
index 17c7baf..0000000
--- a/frontend/src/pages/Info.jsx
+++ /dev/null
@@ -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 (
-
- {/* 顶部 Tab 切换 */}
-
-
- {[
- { key: 'manage', label: '📋 发布管理' }
- ].map(tab => (
-
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}
-
- ))}
-
-
-
-
- {/* 寻配号发布页 - 已删除 */}
- {false && (
-
-
🔍 寻配号发布
-
-
-
-
-
- {['龙钞', '马钞', '蛇钞'].map(ed => {
- const selected = seekForm.edition === ed
- return (
- 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}
-
- )
- })}
-
-
-
-
-
-
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' }}
- />
-
X=任意数字 A=非4 B=非47 C=非347 D=非247
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 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' }}
- />
-
-
-
- 🚀 发布寻配号
-
-
-
- )}
-
- {/* 发布管理页 */}
- {activeTab === 'manage' && (
-
- {/* 新增发布区域 - 发布行情信息 */}
-
-
-
📋 发布管理
- 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 ? '✕ 收起' : '+ 💰 新增行情发布'}
-
-
-
- {/* 行情发布表单 */}
- {showPublish && (
-
-
💰 发布行情信息
-
-
-
-
-
-
- {['龙钞', '马钞', '蛇钞', '其他'].map(ed => (
- 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}
-
- ))}
-
-
-
-
-
-
-
-
- {['标百', '标十', '单张'].map(t => (
- 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}
-
- ))}
-
-
-
-
- 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 ? '✓ 评级币' : '○ 裸钞'}
-
-
-
-
-
-
-
- {[{value:'成交',label:'💰 成交'},{value:'求购',label:'🔍 求购'},{value:'出售',label:'💵 出售'}].map(opt => (
- 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}
-
- ))}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 🚀 提交发布
-
-
-
- )}
-
-
- {/* 列表过滤:全部 / 寻号 / 行情 */}
-
- setFilterType('all')}
- style={{
- padding: '8px 16px',
- background: filterType === 'all' ? '#3b82f6' : '#374151',
- border: 'none',
- borderRadius: '6px',
- color: '#fff',
- fontSize: '13px',
- cursor: 'pointer'
- }}
- >
- 全部
-
- setFilterType('seek')}
- style={{
- padding: '8px 16px',
- background: filterType === 'seek' ? '#10b981' : '#374151',
- border: 'none',
- borderRadius: '6px',
- color: '#fff',
- fontSize: '13px',
- cursor: 'pointer'
- }}
- >
- 🔍 寻号
-
- setFilterType('deal')}
- style={{
- padding: '8px 16px',
- background: filterType === 'deal' ? '#f59e0b' : '#374151',
- border: 'none',
- borderRadius: '6px',
- color: '#fff',
- fontSize: '13px',
- cursor: 'pointer'
- }}
- >
- 💰 行情
-
-
-
- {/* 发布列表 */}
- {loading ? (
-
加载中...
- ) : filteredList.length === 0 ? (
-
暂无发布记录
- ) : (
-
- {filteredList.map(item => (
-
- {/* 标题行 */}
-
-
{item.title}
-
- {item.info_type === 'deal' ? '💰 行情' : '🔍 寻号'}
-
-
- {/* 日期+用户名 */}
-
- 📅 {formatDate(item.created_at)} | 👤 {item.user_name || '匿名用户'}
-
- {/* 正文 - 默认收起,点击展开 */}
- {item.content && (
-
-
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)'
- }}
- >
- {expandedItems[item.id] ? '▼' : '▶'}
- {expandedItems[item.id] ? '收起详情' : '展开查看详情'}
-
- {expandedItems[item.id] && (
-
- {item.content.split('\n').map((line, i) => (
-
- {line || ' '}
-
- ))}
-
- )}
-
- )}
- {/* 操作按钮 */}
-
-
- handleEdit(item)} style={{ padding: '6px 12px', borderRadius: '6px', border: '1px solid #3b82f6', background: 'transparent', color: '#3b82f6', cursor: 'pointer', fontSize: '12px' }}>✏️ 编辑
- handleDelete(item.id)} style={{ padding: '6px 12px', borderRadius: '6px', border: '1px solid #ef4444', background: 'transparent', color: '#ef4444', cursor: 'pointer', fontSize: '12px' }}>🗑️ 删除
-
-
-
- ))}
-
- )}
-
- )}
-
-
- {/* 编辑弹窗 */}
- {editingItem && (
-
-
-
✏️ 编辑信息
-
-
- 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' }}
- />
-
-
-
-
-
- 💾 保存
- setEditingItem(null)} style={{ flex: 1, padding: '12px', border: '1px solid #374151', borderRadius: '8px', background: 'transparent', color: '#9ca3af', cursor: 'pointer', fontSize: '14px' }}>取消
-
-
-
- )}
-
- )
-}
-
-
-
diff --git a/frontend/src/pages/News.jsx b/frontend/src/pages/News.jsx
index 0b6d882..7d2472b 100644
--- a/frontend/src/pages/News.jsx
+++ b/frontend/src/pages/News.jsx
@@ -337,9 +337,9 @@ export default function News() {
// Tab切换
const tabs = [
- { key: 'seek', label: '🔍 寻配号' },
- { key: 'deal', label: '💰 成交行情' },
- { key: 'yichen', label: '📊 一尘看板' }
+ { key: 'seek', label: '寻配号' },
+ { key: 'deal', label: '成交行情' },
+ { key: 'yichen', label: '一尘看板' }
]
const formatDate = (dateStr) => {
@@ -459,7 +459,7 @@ export default function News() {
)}
- {activeTab === 'seek' ? '🔍 寻配号信息' : activeTab === 'deal' ? '💰 成交行情信息' : '📊 一尘看板'}
+ {activeTab === 'seek' ? '寻配号信息' : activeTab === 'deal' ? '成交行情信息' : '一尘看板'}
{activeTab === 'seek' && (
{ setViewMode('all'); fetchInfoList(); }} style={{ padding: '6px 12px', background: viewMode==='all'?'#3b82f6':'#1e293b', border: '1px solid #334155', borderRadius: '6px', color: '#fff', fontSize: '12px' }}>全部
diff --git a/frontend/src/utils/numberCategory.js b/frontend/src/utils/numberCategory.js
new file mode 100644
index 0000000..abe12df
--- /dev/null
+++ b/frontend/src/utils/numberCategory.js
@@ -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', // ���
+ '带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 }))