diff --git a/backend/app/models/models.py b/backend/app/models/models.py
index e857f99..5f9df82 100644
--- a/backend/app/models/models.py
+++ b/backend/app/models/models.py
@@ -55,6 +55,7 @@ class Collection(Base):
f02_11_version = Column(String(100), nullable=True, index=True)
f02_12_packaging = Column(String(100), nullable=True, index=True)
f02_13_rarity = Column(String(50), nullable=True, index=True) # 珍惜度
+ f02_14_number_category = Column(String(20), nullable=True, index=True) # 号码分类
# f03 评级信息
f03_20_is_graded = Column(Boolean, default=False, index=True)
diff --git a/backend/app/routers/collections.py b/backend/app/routers/collections.py
index 7e1b4e2..5d91468 100644
--- a/backend/app/routers/collections.py
+++ b/backend/app/routers/collections.py
@@ -38,6 +38,7 @@ def to_camel_case(data: dict) -> dict:
'f02_11_version': 'version',
'f02_12_packaging': 'packaging',
'f02_13_rarity': 'rarity',
+ 'f02_14_number_category': 'numberCategory',
'f03_20_is_graded': 'isGraded',
'f03_21_grading_company': 'gradingCompany',
'f03_22_grading_score': 'gradingScore',
@@ -73,8 +74,8 @@ def generate_code(version: str, user_id: str, db: Session) -> str:
max_num = 0
for (code,) in user_codes:
- # 只处理纯数字或 4 位数字编码(忽略 TEST001 等特殊编码)
- if re.match(r'^\d{4}$', code):
+ # 处理纯数字编码(支持4位和5位)
+ if re.match(r'^\d{4,5}$', code):
try:
num = int(code)
if num > max_num:
@@ -84,7 +85,12 @@ def generate_code(version: str, user_id: str, db: Session) -> str:
# 当前用户最大号 +1
next_num = max_num + 1
- return str(next_num).zfill(4)
+
+ # 如果超过9999,使用5位;否则使用4位
+ if next_num > 9999:
+ return str(next_num).zfill(5)
+ else:
+ return str(next_num).zfill(4)
@router.get("/next-code")
@@ -164,6 +170,7 @@ def get_collections(
'f02_11_version': collection_item.f02_11_version,
'f02_12_packaging': collection_item.f02_12_packaging,
'f02_13_rarity': collection_item.f02_13_rarity,
+ 'f02_14_number_category': collection_item.f02_14_number_category,
'f03_20_is_graded': collection_item.f03_20_is_graded,
'f03_21_grading_company': collection_item.f03_21_grading_company,
'f03_22_grading_score': collection_item.f03_22_grading_score,
@@ -247,6 +254,7 @@ def get_stats(
by_grading_company = Counter(c.f03_21_grading_company for c in all_collections if c.f03_20_is_graded and c.f03_21_grading_company).items()
by_grading_score = Counter(c.f03_22_grading_score for c in all_collections if c.f03_20_is_graded and c.f03_22_grading_score).items()
by_special_mark = Counter(c.f04_30_special_mark for c in all_collections if c.f04_30_special_mark).items()
+ by_number_category = Counter(c.f02_14_number_category for c in all_collections if c.f02_14_number_category).items()
# 总成本: SUM(cost_price + repair_fee + grading_fee) for ALL collections
total_cost = sum(
@@ -291,6 +299,7 @@ def get_stats(
"byGradingCompany": [{"company": c, "count": n} for c, n in by_grading_company],
"byGradingScore": [{"score": s, "count": n} for s, n in by_grading_score],
"bySpecialMark": [{"mark": m, "count": n} for m, n in by_special_mark],
+ "byNumberCategory": [{"numberCategory": n, "count": c} for n, c in by_number_category],
# 盈亏统计(只统计已售且有价格的藏品)
"byProfitLoss": [
{"type": "profit", "label": "盈利", "count": sum(1 for c in sold_collections if c.f05_42_goal_price and c.f05_40_cost_price and c.f05_42_goal_price > c.f05_40_cost_price)},
@@ -337,6 +346,7 @@ def get_collection(
'f02_11_version': collection.get('f02_11_version'),
'f02_12_packaging': collection.get('f02_12_packaging'),
'f02_13_rarity': collection.get('f02_13_rarity'),
+ 'f02_14_number_category': collection.get('f02_14_number_category'),
'f03_20_is_graded': collection.get('f03_20_is_graded'),
'f03_21_grading_company': collection.get('f03_21_grading_company'),
'f03_22_grading_score': collection.get('f03_22_grading_score'),
@@ -446,6 +456,7 @@ def create_collection(
f02_11_version=collection_data.f02_11_version,
f02_12_packaging=collection_data.f02_12_packaging,
f02_13_rarity=collection_data.f02_13_rarity,
+ f02_14_number_category=collection_data.f02_14_number_category,
f03_20_is_graded=collection_data.f03_20_is_graded or False,
f03_21_grading_company=collection_data.f03_21_grading_company,
f03_22_grading_score=collection_data.f03_22_grading_score,
diff --git a/backend/app/schemas/schemas.py b/backend/app/schemas/schemas.py
index b843b8e..0ba1bae 100644
--- a/backend/app/schemas/schemas.py
+++ b/backend/app/schemas/schemas.py
@@ -58,6 +58,7 @@ class CollectionBase(BaseModel):
f02_11_version: Optional[str] = Field(None, alias="version")
f02_12_packaging: Optional[str] = Field(None, alias="packaging")
f02_13_rarity: Optional[str] = Field(None, alias="rarity")
+ f02_14_number_category: Optional[str] = Field(None, alias="numberCategory")
# f03 评级信息
f03_20_is_graded: Optional[bool] = Field(False, alias="isGraded")
@@ -104,6 +105,7 @@ class CollectionUpdate(BaseModel):
f02_11_version: Optional[str] = Field(None, alias="version")
f02_12_packaging: Optional[str] = Field(None, alias="packaging")
f02_13_rarity: Optional[str] = Field(None, alias="rarity")
+ f02_14_number_category: Optional[str] = Field(None, alias="numberCategory")
# f03 评级信息
f03_20_is_graded: Optional[bool] = Field(None, alias="isGraded")
diff --git a/config/VERSION b/config/VERSION
index cfb0091..92448f4 100644
--- a/config/VERSION
+++ b/config/VERSION
@@ -1 +1 @@
-VERSION=1.2.5
+VERSION=1.2.6
diff --git a/frontend/index.html b/frontend/index.html
index 757b50b..0aeae80 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -4,7 +4,7 @@
-
甲辰收藏 v1.2.5
+ 甲辰收藏 v1.2.6
diff --git a/frontend/src/pages/Add.jsx b/frontend/src/pages/Add.jsx
index e9a9491..f09e0cb 100644
--- a/frontend/src/pages/Add.jsx
+++ b/frontend/src/pages/Add.jsx
@@ -1,6 +1,7 @@
// 添加藏品页面 - 支持 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) => {
@@ -12,7 +13,7 @@ const convertField = (obj) => {
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',
+ 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',
@@ -61,11 +62,11 @@ const Input = ({ form, handleChange, label, field, type = 'text', options = null
}
const getDefaultForm = () => ({
- name: '龙钞', code: '', category: '自持', rarity: '通货', prefixSerial: '',
+ name: '藏品', code: '', category: '自持', rarity: '通货', prefixSerial: '',
version: '2024 龙', denomination: '', status: 'in_collection', packaging: '单张',
material: '塑料钞', issueQuantity: '1 亿', purpose: '收藏', isGraded: false,
gradingCompany: '', gradingScore: '', threeStar: false, specialMark: '',
- serialFeature: '', issuer: '中国人民银行', issueYear: '2024',
+ serialFeature: '', numberCategory: '', issuer: '中国人民银行', issueYear: '2024',
costPrice: '', targetPrice: '', goalPrice: '', repairFee: '', gradingFee: '', remark: ''
})
@@ -146,6 +147,23 @@ export default function Add() {
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})/)
@@ -244,7 +262,26 @@ export default function Add() {
setSaving(true)
setError('')
const token = localStorage.getItem('token')
- const formData = convertField(form)
+ // 保存前自动根据冠字号设置号码分类
+ 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', {
@@ -259,6 +296,11 @@ export default function Add() {
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
@@ -536,6 +578,7 @@ export default function Add() {
基本信息
+
@@ -573,6 +616,7 @@ export default function Add() {
+
diff --git a/frontend/src/pages/Detail.jsx b/frontend/src/pages/Detail.jsx
index 31c1a8d..7df0297 100644
--- a/frontend/src/pages/Detail.jsx
+++ b/frontend/src/pages/Detail.jsx
@@ -230,6 +230,7 @@ export default function Detail() {
+
diff --git a/frontend/src/pages/Edit.jsx b/frontend/src/pages/Edit.jsx
index b9a8f08..802fc66 100644
--- a/frontend/src/pages/Edit.jsx
+++ b/frontend/src/pages/Edit.jsx
@@ -46,6 +46,7 @@ const categoryOptions = [
{ 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:'其他'}];
const rarityOptions = [
{ value: '通货', label: '通货' },
{ value: '特色', label: '特色' },
@@ -68,7 +69,7 @@ export default function Edit() {
const editId = params.get('id')
const [form, setForm] = useState({
- name: '', code: '', category: '自持', rarity: '通货', prefixSerial: '',
+ name: '', code: '', category: '自持', rarity: '通货', numberCategory: '', prefixSerial: '',
version: '2024 龙', denomination: '', status: 'in_collection', packaging: '单张',
material: '塑料钞', issueQuantity: '1 亿', purpose: '收藏', isGraded: false,
gradingCompany: '', gradingScore: '', threeStar: false, specialMark: '',
@@ -96,6 +97,7 @@ export default function Edit() {
code: data.code || '',
category: data.category || '自持',
rarity: data.rarity || '通货',
+ numberCategory: data.numberCategory || '',
prefixSerial: data.prefixSerial || '',
version: data.version || '2024 龙',
denomination: data.denomination || '',
@@ -146,6 +148,23 @@ export default function Edit() {
const handleChange = (key, value) => {
setForm({ ...form, [key]: value })
if (key === 'targetPrice' && 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('4')) cat = '带4'
+ else if (digits.includes('7')) cat = '无4'
+ else if (!digits.includes('4') && !digits.includes('7')) cat = '无47'
+ else if (!digits.includes('2') && !digits.includes('4') && !digits.includes('7')) cat = '无247'
+ else cat = '其他'
+ }
+ setForm(prev => ({ ...prev, numberCategory: cat }))
+ }
// 版别变化时同步发行年份
if (key === 'version') {
const yearMatch = value.match(/(20\d{2})/)
@@ -238,6 +257,7 @@ export default function Edit() {
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',
+ numberCategory: 'f02_14_number_category',
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',
@@ -398,6 +418,7 @@ export default function Edit() {
+
diff --git a/frontend/src/pages/List.jsx b/frontend/src/pages/List.jsx
index ccffac6..9af41fb 100644
--- a/frontend/src/pages/List.jsx
+++ b/frontend/src/pages/List.jsx
@@ -94,6 +94,7 @@ export default function List() {
category: 'category',
packaging: 'packaging',
rarity: 'rarity',
+ numberCategory: 'numberCategory',
version: 'version',
gradingCompany: 'gradingCompany',
gradingScore: 'gradingScore',
@@ -278,6 +279,11 @@ export default function List() {
const rarityOrder = { '通货': 1, '特色': 2, '少见': 3, '稀有': 4, '珍品': 5, '孤品': 6 }
aVal = rarityOrder[aVal] || 0
bVal = rarityOrder[bVal] || 0
+ } else if (sortField === 'numberCategory') {
+ // 号码分类按自定义顺序排序
+ const numberCategoryOrder = { '无2347': 1, '无347': 2, '无247': 3, '无47': 4, '无4': 5, '带4': 6, '其他': 7 }
+ aVal = numberCategoryOrder[aVal] || 99
+ bVal = numberCategoryOrder[bVal] || 99
}
if (aVal == null) return 1
if (bVal == null) return -1
@@ -317,11 +323,25 @@ export default function List() {
return colors[category] || '#64748b'
}
+ const getNumberCategoryColor = (cat) => {
+ const colors = { '无2347': '#8b5cf6', '无347': '#ef4444', '无247': '#fbbf24', '无47': '#3b82f6', '无4': '#06b6d4', '带4': '#22c55e', '其他': '#64748b' };
+ return colors[cat] || '#64748b';
+ };
+
const getRarityColor = (rarity) => {
const colors = { '通货': '#22c55e', '特色': '#06b6d4', '少见': '#3b82f6', '稀有': '#ec4899', '珍品': '#ef4444', '孤品': '#8b5cf6' }
return colors[rarity] || '#64748b'
}
+ const formatPrefixSerial = (serial) => {
+ if (!serial) return '-'
+ // 提取J开头的10位(1位J + 9位数字)
+ const match = serial.match(/J(\d{9})/)
+ if (match) return 'J' + match[1]
+ // 如果没有J开头,取前10位
+ return serial.substring(0, 10)
+ }
+
const getPackagingColor = (packaging) => {
const colors = { '裸钞': '#22c55e', '单张': '#3b82f6', '标十': '#fbbf24', '标百': '#8b5cf6' }
return colors[packaging] || '#64748b'
@@ -365,13 +385,14 @@ export default function List() {
{item.code || '-'}
- {item.prefixSerial || '-'}
+ {formatPrefixSerial(item.prefixSerial)}
{item.packaging && {item.packaging}}
+ {item.numberCategory && {item.numberCategory}}
{item.status && {getStatusText(item.status)}}
{item.category && {getCategoryText(item.category)}}
- {item.rarity && {item.rarity}}
+ {item.rarity && {item.rarity}}
{/* 第2行:版本(彩色) + 已评级 + 评级公司 + 评级分数 + 三星 + 特殊标识 | 备注 */}
@@ -487,6 +508,7 @@ export default function List() {
{ key: 'goalPrice', label: '售价' },
{ key: 'category', label: '持仓类型' },
{ key: 'rarity', label: '珍惜度' },
+ { key: 'numberCategory', label: '号码分类' },
{ key: 'gradingScore', label: '评级分数' }
].map(item => (
{
@@ -523,7 +545,7 @@ export default function List() {
goDetail(item.id)} style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.05)', borderRadius: '12px', padding: '12px', cursor: 'pointer' }}>
🐉
{item.code || '-'}
-
{item.prefixSerial || '-'}
+
{formatPrefixSerial(item.prefixSerial)}
{item.gradingScore &&
{item.gradingScore}}
{item.threeStar &&
⭐⭐⭐}
diff --git a/frontend/src/pages/Stats.jsx b/frontend/src/pages/Stats.jsx
index 59be7b9..c76bed7 100644
--- a/frontend/src/pages/Stats.jsx
+++ b/frontend/src/pages/Stats.jsx
@@ -10,6 +10,7 @@ export default function Stats() {
byGrading: [],
byPackaging: [],
byRarity: [],
+ byNumberCategory: [],
byVersion: [],
byGradingCompany: [],
byGradingScore: [],
@@ -49,6 +50,7 @@ export default function Stats() {
byGrading: data.byGrading || [],
byPackaging: data.byPackaging || [],
byRarity: data.byRarity || [],
+ byNumberCategory: data.byNumberCategory || [],
byVersion: data.byVersion || [],
byGradingCompany: data.byGradingCompany || [],
byGradingScore: data.byGradingScore || [],
@@ -94,10 +96,15 @@ export default function Stats() {
// 颜色配置
const colors = {
packaging: { '标十': '#22c55e', '标百': '#3b82f6', '单张': '#f59e0b', '裸钞': '#64748b' },
- rarity: { '通货': '#64748b', '特色': '#22c55e', '少见': '#3b82f6', '稀有': '#8b5cf6', '珍品': '#f59e0b', '孤品': '#ef4444' },
+ rarity: { '通货': '#64748b', '特色': '#22c55e', '少见': '#3b82f6', '稀有': '#8b5cf6', '珍品': '#ef4444', '孤品': '#fbbf24' },
status: { 'in_collection': '#22c55e', 'selling': '#f59e0b', 'sold': '#ef4444', 'grading': '#8b5cf6', 'repairing': '#f97316', 'transit': '#06b6d4', 'seeking': '#ec4899' },
category: { '自持': '#22c55e', '寄存': '#3b82f6', '寄售': '#f59e0b', '共有': '#8b5cf6', '寻号': '#06b6d4', '生肖钞': '#22c55e', '其他': '#64748b' },
- profitLoss: { 'profit': '#22c55e', 'loss': '#ef4444' }
+ profitLoss: { 'profit': '#22c55e', 'loss': '#ef4444' },
+ numberCategory: { '无2347': '#8b5cf6', '无347': '#ef4444', '无247': '#fbbf24', '无47': '#3b82f6', '无4': '#06b6d4', '带4': '#22c55e', '其他': '#64748b' },
+ version: {},
+ gradingCompany: {},
+ gradingScore: {},
+ specialMark: {}
}
// 标签映射
@@ -117,8 +124,14 @@ export default function Stats() {
}
}
+ const colorPalette = ['#22c55e', '#3b82f6', '#f59e0b', '#ef4444', '#8b5cf6', '#06b6d4', '#ec4899', '#f97316', '#14b8a6', '#a855f7']
const getColor = (type, value) => {
- return colors[type]?.[value] || '#64748b'
+ if (colors[type]?.[value]) return colors[type][value]
+ // 动态生成颜色
+ const key = String(value)
+ let hash = 0
+ for (let i = 0; i < key.length; i++) hash = key.charCodeAt(i) + ((hash << 5) - hash)
+ return colorPalette[Math.abs(hash) % colorPalette.length]
}
const getLabel = (type, value) => {
@@ -130,11 +143,23 @@ export default function Stats() {
return Number(val).toLocaleString('zh-CN')
}
+ const numberCategoryOrder = ['无2347', '无347', '无247', '无47', '无4', '带4', '其他']
+ const getSortedData = (data, type) => {
+ if (type === 'numberCategory') {
+ return [...data].sort((a, b) => {
+ const order = numberCategoryOrder.indexOf(a.numberCategory)
+ const order2 = numberCategoryOrder.indexOf(b.numberCategory)
+ return order - order2
+ })
+ }
+ return data
+ }
+
const DistributionCard = ({ title, data, type, valueKey, labelKey }) => (
{title}
- {data.slice(0, 6).map((item, index) => {
+ {getSortedData(data, type).slice(0, 6).map((item, index) => {
const value = item[valueKey]
const label = getLabel(type, item[labelKey] || value)
const color = getColor(type, value)
@@ -159,13 +184,17 @@ export default function Stats() {
e.currentTarget.style.borderColor = 'rgba(255,255,255,0.05)'
}}
>
-
-
-
- {label}
+
-
{item.count}
)
})}
@@ -246,9 +275,9 @@ export default function Stats() {
-
-
+
+