Release v1.2.6 - 号码分类功能稳定版

- 添加号码分类字段(无2347/无347/无247/无47/无4/带4/其他)
- 录入/编辑时自动根据冠字号分类
- 统计页面显示号码分类分布
- 列表页显示号码分类标签
- 编号自动生成(4位→5位)
- 用户协议页面修复
- 修复重复编号错误处理
- 藏品名称默认设置为藏品
This commit is contained in:
甲辰生产 2026-03-21 23:56:45 +08:00
parent 231c705c36
commit de036dfb06
10 changed files with 155 additions and 24 deletions

View File

@ -55,6 +55,7 @@ class Collection(Base):
f02_11_version = Column(String(100), nullable=True, index=True) f02_11_version = Column(String(100), nullable=True, index=True)
f02_12_packaging = 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_13_rarity = Column(String(50), nullable=True, index=True) # 珍惜度
f02_14_number_category = Column(String(20), nullable=True, index=True) # 号码分类
# f03 评级信息 # f03 评级信息
f03_20_is_graded = Column(Boolean, default=False, index=True) f03_20_is_graded = Column(Boolean, default=False, index=True)

View File

@ -38,6 +38,7 @@ def to_camel_case(data: dict) -> dict:
'f02_11_version': 'version', 'f02_11_version': 'version',
'f02_12_packaging': 'packaging', 'f02_12_packaging': 'packaging',
'f02_13_rarity': 'rarity', 'f02_13_rarity': 'rarity',
'f02_14_number_category': 'numberCategory',
'f03_20_is_graded': 'isGraded', 'f03_20_is_graded': 'isGraded',
'f03_21_grading_company': 'gradingCompany', 'f03_21_grading_company': 'gradingCompany',
'f03_22_grading_score': 'gradingScore', 'f03_22_grading_score': 'gradingScore',
@ -73,8 +74,8 @@ def generate_code(version: str, user_id: str, db: Session) -> str:
max_num = 0 max_num = 0
for (code,) in user_codes: for (code,) in user_codes:
# 只处理纯数字或 4 位数字编码(忽略 TEST001 等特殊编码 # 处理纯数字编码支持4位和5位
if re.match(r'^\d{4}$', code): if re.match(r'^\d{4,5}$', code):
try: try:
num = int(code) num = int(code)
if num > max_num: if num > max_num:
@ -84,7 +85,12 @@ def generate_code(version: str, user_id: str, db: Session) -> str:
# 当前用户最大号 +1 # 当前用户最大号 +1
next_num = max_num + 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") @router.get("/next-code")
@ -164,6 +170,7 @@ def get_collections(
'f02_11_version': collection_item.f02_11_version, 'f02_11_version': collection_item.f02_11_version,
'f02_12_packaging': collection_item.f02_12_packaging, 'f02_12_packaging': collection_item.f02_12_packaging,
'f02_13_rarity': collection_item.f02_13_rarity, '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_20_is_graded': collection_item.f03_20_is_graded,
'f03_21_grading_company': collection_item.f03_21_grading_company, 'f03_21_grading_company': collection_item.f03_21_grading_company,
'f03_22_grading_score': collection_item.f03_22_grading_score, '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_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_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_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 # 总成本: SUM(cost_price + repair_fee + grading_fee) for ALL collections
total_cost = sum( total_cost = sum(
@ -291,6 +299,7 @@ def get_stats(
"byGradingCompany": [{"company": c, "count": n} for c, n in by_grading_company], "byGradingCompany": [{"company": c, "count": n} for c, n in by_grading_company],
"byGradingScore": [{"score": s, "count": n} for s, n in by_grading_score], "byGradingScore": [{"score": s, "count": n} for s, n in by_grading_score],
"bySpecialMark": [{"mark": m, "count": n} for m, n in by_special_mark], "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": [ "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)}, {"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_11_version': collection.get('f02_11_version'),
'f02_12_packaging': collection.get('f02_12_packaging'), 'f02_12_packaging': collection.get('f02_12_packaging'),
'f02_13_rarity': collection.get('f02_13_rarity'), '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_20_is_graded': collection.get('f03_20_is_graded'),
'f03_21_grading_company': collection.get('f03_21_grading_company'), 'f03_21_grading_company': collection.get('f03_21_grading_company'),
'f03_22_grading_score': collection.get('f03_22_grading_score'), '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_11_version=collection_data.f02_11_version,
f02_12_packaging=collection_data.f02_12_packaging, f02_12_packaging=collection_data.f02_12_packaging,
f02_13_rarity=collection_data.f02_13_rarity, 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_20_is_graded=collection_data.f03_20_is_graded or False,
f03_21_grading_company=collection_data.f03_21_grading_company, f03_21_grading_company=collection_data.f03_21_grading_company,
f03_22_grading_score=collection_data.f03_22_grading_score, f03_22_grading_score=collection_data.f03_22_grading_score,

View File

@ -58,6 +58,7 @@ class CollectionBase(BaseModel):
f02_11_version: Optional[str] = Field(None, alias="version") f02_11_version: Optional[str] = Field(None, alias="version")
f02_12_packaging: Optional[str] = Field(None, alias="packaging") f02_12_packaging: Optional[str] = Field(None, alias="packaging")
f02_13_rarity: Optional[str] = Field(None, alias="rarity") f02_13_rarity: Optional[str] = Field(None, alias="rarity")
f02_14_number_category: Optional[str] = Field(None, alias="numberCategory")
# f03 评级信息 # f03 评级信息
f03_20_is_graded: Optional[bool] = Field(False, alias="isGraded") 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_11_version: Optional[str] = Field(None, alias="version")
f02_12_packaging: Optional[str] = Field(None, alias="packaging") f02_12_packaging: Optional[str] = Field(None, alias="packaging")
f02_13_rarity: Optional[str] = Field(None, alias="rarity") f02_13_rarity: Optional[str] = Field(None, alias="rarity")
f02_14_number_category: Optional[str] = Field(None, alias="numberCategory")
# f03 评级信息 # f03 评级信息
f03_20_is_graded: Optional[bool] = Field(None, alias="isGraded") f03_20_is_graded: Optional[bool] = Field(None, alias="isGraded")

View File

@ -1 +1 @@
VERSION=1.2.5 VERSION=1.2.6

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>甲辰收藏 v1.2.5</title> <title>甲辰收藏 v1.2.6</title>
<!-- Favicon --> <!-- Favicon -->
<link rel="icon" type="image/x-icon" href="/static/icons/favicon.ico" /> <link rel="icon" type="image/x-icon" href="/static/icons/favicon.ico" />

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 numberCategoryOptions = [{value:'无2347',label:'无2347'},{value:'无347',label:'无347'},{value:'无247',label:'无247'},{value:'无47',label:'无47'},{value:'无4',label:'无4'},{value:'带4',label:'带4'},{value:'其他',label:'其他'}];
// //
const convertField = (obj) => { const convertField = (obj) => {
@ -12,7 +13,7 @@ const convertField = (obj) => {
packaging: 'f02_12_packaging', rarity: 'f02_13_rarity', packaging: 'f02_12_packaging', rarity: 'f02_13_rarity',
isGraded: 'f03_20_is_graded', gradingCompany: 'f03_21_grading_company', isGraded: 'f03_20_is_graded', gradingCompany: 'f03_21_grading_company',
gradingScore: 'f03_22_grading_score', threeStar: 'f03_23_three_star', 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', issuer: 'f04_32_issuer', issueYear: 'f04_33_issue_year',
material: 'f04_34_material', denomination: 'f04_35_denomination', material: 'f04_34_material', denomination: 'f04_35_denomination',
issueQuantity: 'f04_36_issue_quantity', issueQuantity: 'f04_36_issue_quantity',
@ -61,11 +62,11 @@ const Input = ({ form, handleChange, label, field, type = 'text', options = null
} }
const getDefaultForm = () => ({ const getDefaultForm = () => ({
name: '龙钞', code: '', category: '自持', rarity: '通货', prefixSerial: '', name: '藏品', code: '', category: '自持', rarity: '通货', prefixSerial: '',
version: '2024 龙', denomination: '', status: 'in_collection', packaging: '单张', version: '2024 龙', denomination: '', status: 'in_collection', packaging: '单张',
material: '塑料钞', issueQuantity: '1 亿', purpose: '收藏', isGraded: false, material: '塑料钞', issueQuantity: '1 亿', purpose: '收藏', isGraded: false,
gradingCompany: '', gradingScore: '', threeStar: false, specialMark: '', gradingCompany: '', gradingScore: '', threeStar: false, specialMark: '',
serialFeature: '', issuer: '中国人民银行', issueYear: '2024', serialFeature: '', numberCategory: '', issuer: '中国人民银行', issueYear: '2024',
costPrice: '', targetPrice: '', goalPrice: '', repairFee: '', gradingFee: '', remark: '' costPrice: '', targetPrice: '', goalPrice: '', repairFee: '', gradingFee: '', remark: ''
}) })
@ -146,6 +147,23 @@ export default function Add() {
if (key === 'goalPrice' && value) { if (key === 'goalPrice' && value) {
setForm(prev => ({ ...prev, status: 'sold' })) 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') { if (key === 'version') {
const yearMatch = value.match(/(20\d{2})/) const yearMatch = value.match(/(20\d{2})/)
@ -244,7 +262,26 @@ export default function Add() {
setSaving(true) setSaving(true)
setError('') setError('')
const token = localStorage.getItem('token') 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 { try {
// 1. // 1.
const res = await fetch('/api/collections', { const res = await fetch('/api/collections', {
@ -259,6 +296,11 @@ export default function Add() {
const result = await res.json() const result = await res.json()
let collectionId = result.f99_90_id || result.id 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') { if (result.warning && result.warning.code === 'DUPLICATE_SERIAL') {
const { warning } = result const { warning } = result
@ -536,6 +578,7 @@ export default function Add() {
<div style={{ color: '#fbbf24', fontSize: '14px', fontWeight: 'bold', marginBottom: '12px' }}>基本信息</div> <div style={{ color: '#fbbf24', fontSize: '14px', fontWeight: 'bold', marginBottom: '12px' }}>基本信息</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '12px' }}> <div style={{ display: 'flex', flexWrap: 'wrap', gap: '12px' }}>
<Input form={form} handleChange={handleChange} label="名称 *" field="name" /> <Input form={form} handleChange={handleChange} label="名称 *" field="name" />
<Input form={form} handleChange={handleChange} label="编号(可选)" field="code" />
<Input form={form} handleChange={handleChange} label="持仓类型" field="category" options={categoryOptions} /> <Input form={form} handleChange={handleChange} label="持仓类型" field="category" options={categoryOptions} />
<Input form={form} handleChange={handleChange} label="珍惜度" field="rarity" options={rarityOptions} /> <Input form={form} handleChange={handleChange} label="珍惜度" field="rarity" options={rarityOptions} />
<Input form={form} handleChange={handleChange} label="冠字序号" field="prefixSerial" /> <Input form={form} handleChange={handleChange} label="冠字序号" field="prefixSerial" />
@ -573,6 +616,7 @@ export default function Add() {
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '12px' }}> <div style={{ display: 'flex', flexWrap: 'wrap', gap: '12px' }}>
<Input form={form} handleChange={handleChange} label="特殊标识" field="specialMark" /> <Input form={form} handleChange={handleChange} label="特殊标识" field="specialMark" />
<Input form={form} handleChange={handleChange} label="号码特征" field="serialFeature" /> <Input form={form} handleChange={handleChange} label="号码特征" field="serialFeature" />
<Input form={form} handleChange={handleChange} label="号码分类" field="numberCategory" options={numberCategoryOptions} />
<Input form={form} handleChange={handleChange} label="发行方" field="issuer" /> <Input form={form} handleChange={handleChange} label="发行方" field="issuer" />
<Input form={form} handleChange={handleChange} label="发行年份" field="issueYear" /> <Input form={form} handleChange={handleChange} label="发行年份" field="issueYear" />
<Input form={form} handleChange={handleChange} label="材质" field="material" /> <Input form={form} handleChange={handleChange} label="材质" field="material" />

View File

@ -230,6 +230,7 @@ export default function Detail() {
<Section title="特殊信息"> <Section title="特殊信息">
<Field label="特殊标识" value={collection.specialMark} /> <Field label="特殊标识" value={collection.specialMark} />
<Field label="号码特征" value={collection.serialFeature} /> <Field label="号码特征" value={collection.serialFeature} />
<Field label="号码分类" value={collection.numberCategory} />
<Field label="发行方" value={collection.issuer} /> <Field label="发行方" value={collection.issuer} />
<Field label="发行年份" value={collection.issueYear} /> <Field label="发行年份" value={collection.issueYear} />
<Field label="材质" value={collection.material} /> <Field label="材质" value={collection.material} />

View File

@ -46,6 +46,7 @@ const categoryOptions = [
{ value: '其他', label: '其他' } { 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 = [ const rarityOptions = [
{ value: '通货', label: '通货' }, { value: '通货', label: '通货' },
{ value: '特色', label: '特色' }, { value: '特色', label: '特色' },
@ -68,7 +69,7 @@ export default function Edit() {
const editId = params.get('id') const editId = params.get('id')
const [form, setForm] = useState({ const [form, setForm] = useState({
name: '', code: '', category: '自持', rarity: '通货', prefixSerial: '', name: '', code: '', category: '自持', rarity: '通货', numberCategory: '', prefixSerial: '',
version: '2024 龙', denomination: '', status: 'in_collection', packaging: '单张', version: '2024 龙', denomination: '', status: 'in_collection', packaging: '单张',
material: '塑料钞', issueQuantity: '1 亿', purpose: '收藏', isGraded: false, material: '塑料钞', issueQuantity: '1 亿', purpose: '收藏', isGraded: false,
gradingCompany: '', gradingScore: '', threeStar: false, specialMark: '', gradingCompany: '', gradingScore: '', threeStar: false, specialMark: '',
@ -96,6 +97,7 @@ export default function Edit() {
code: data.code || '', code: data.code || '',
category: data.category || '自持', category: data.category || '自持',
rarity: data.rarity || '通货', rarity: data.rarity || '通货',
numberCategory: data.numberCategory || '',
prefixSerial: data.prefixSerial || '', prefixSerial: data.prefixSerial || '',
version: data.version || '2024 龙', version: data.version || '2024 龙',
denomination: data.denomination || '', denomination: data.denomination || '',
@ -146,6 +148,23 @@ export default function Edit() {
const handleChange = (key, value) => { const handleChange = (key, value) => {
setForm({ ...form, [key]: value }) setForm({ ...form, [key]: value })
if (key === 'targetPrice' && value) setForm(prev => ({ ...prev, status: 'sold' })) 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') { if (key === 'version') {
const yearMatch = value.match(/(20\d{2})/) const yearMatch = value.match(/(20\d{2})/)
@ -238,6 +257,7 @@ export default function Edit() {
status: 'f01_04_status', remark: 'f01_05_remark', status: 'f01_04_status', remark: 'f01_05_remark',
prefixSerial: 'f02_10_prefix_serial', version: 'f02_11_version', prefixSerial: 'f02_10_prefix_serial', version: 'f02_11_version',
packaging: 'f02_12_packaging', rarity: 'f02_13_rarity', packaging: 'f02_12_packaging', rarity: 'f02_13_rarity',
numberCategory: 'f02_14_number_category',
isGraded: 'f03_20_is_graded', gradingCompany: 'f03_21_grading_company', isGraded: 'f03_20_is_graded', gradingCompany: 'f03_21_grading_company',
gradingScore: 'f03_22_grading_score', threeStar: 'f03_23_three_star', 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',
@ -398,6 +418,7 @@ export default function Edit() {
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '12px' }}> <div style={{ display: 'flex', flexWrap: 'wrap', gap: '12px' }}>
<Input form={form} handleChange={handleChange} label="特殊标识" field="specialMark" /> <Input form={form} handleChange={handleChange} label="特殊标识" field="specialMark" />
<Input form={form} handleChange={handleChange} label="号码特征" field="serialFeature" /> <Input form={form} handleChange={handleChange} label="号码特征" field="serialFeature" />
<Input form={form} handleChange={handleChange} label="号码分类" field="numberCategory" options={numberCategoryOptions} />
<Input form={form} handleChange={handleChange} label="发行方" field="issuer" /> <Input form={form} handleChange={handleChange} label="发行方" field="issuer" />
<Input form={form} handleChange={handleChange} label="发行年份" field="issueYear" /> <Input form={form} handleChange={handleChange} label="发行年份" field="issueYear" />
<Input form={form} handleChange={handleChange} label="材质" field="material" /> <Input form={form} handleChange={handleChange} label="材质" field="material" />

View File

@ -94,6 +94,7 @@ export default function List() {
category: 'category', category: 'category',
packaging: 'packaging', packaging: 'packaging',
rarity: 'rarity', rarity: 'rarity',
numberCategory: 'numberCategory',
version: 'version', version: 'version',
gradingCompany: 'gradingCompany', gradingCompany: 'gradingCompany',
gradingScore: 'gradingScore', gradingScore: 'gradingScore',
@ -278,6 +279,11 @@ export default function List() {
const rarityOrder = { '通货': 1, '特色': 2, '少见': 3, '稀有': 4, '珍品': 5, '孤品': 6 } const rarityOrder = { '通货': 1, '特色': 2, '少见': 3, '稀有': 4, '珍品': 5, '孤品': 6 }
aVal = rarityOrder[aVal] || 0 aVal = rarityOrder[aVal] || 0
bVal = rarityOrder[bVal] || 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 (aVal == null) return 1
if (bVal == null) return -1 if (bVal == null) return -1
@ -317,11 +323,25 @@ export default function List() {
return colors[category] || '#64748b' 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 getRarityColor = (rarity) => {
const colors = { '通货': '#22c55e', '特色': '#06b6d4', '少见': '#3b82f6', '稀有': '#ec4899', '珍品': '#ef4444', '孤品': '#8b5cf6' } const colors = { '通货': '#22c55e', '特色': '#06b6d4', '少见': '#3b82f6', '稀有': '#ec4899', '珍品': '#ef4444', '孤品': '#8b5cf6' }
return colors[rarity] || '#64748b' return colors[rarity] || '#64748b'
} }
const formatPrefixSerial = (serial) => {
if (!serial) return '-'
// J101J + 9
const match = serial.match(/J(\d{9})/)
if (match) return 'J' + match[1]
// J10
return serial.substring(0, 10)
}
const getPackagingColor = (packaging) => { const getPackagingColor = (packaging) => {
const colors = { '裸钞': '#22c55e', '单张': '#3b82f6', '标十': '#fbbf24', '标百': '#8b5cf6' } const colors = { '裸钞': '#22c55e', '单张': '#3b82f6', '标十': '#fbbf24', '标百': '#8b5cf6' }
return colors[packaging] || '#64748b' return colors[packaging] || '#64748b'
@ -365,13 +385,14 @@ export default function List() {
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '4px' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '4px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', flexWrap: 'wrap' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '6px', flexWrap: 'wrap' }}>
<span style={{ color: '#fff', fontSize: '12px', fontWeight: 'normal' }}>{item.code || '-'}</span> <span style={{ color: '#fff', fontSize: '12px', fontWeight: 'normal' }}>{item.code || '-'}</span>
<span style={{ color: '#fbbf24', fontSize: '14px', fontFamily: 'monospace', fontWeight: 'bold' }}>{item.prefixSerial || '-'}</span> <span style={{ color: '#fbbf24', fontSize: '14px', fontFamily: 'monospace', fontWeight: 'bold' }}>{formatPrefixSerial(item.prefixSerial)}</span>
{item.packaging && <span style={{ background: getPackagingColor(item.packaging) + '20', color: getPackagingColor(item.packaging), fontSize: '10px', padding: '1px 5px', borderRadius: '3px' }}>{item.packaging}</span>} {item.packaging && <span style={{ background: getPackagingColor(item.packaging) + '20', color: getPackagingColor(item.packaging), fontSize: '10px', padding: '1px 5px', borderRadius: '3px' }}>{item.packaging}</span>}
{item.numberCategory && <span style={{ color: getNumberCategoryColor(item.numberCategory), fontSize: '9px', padding: '1px 4px', background: getNumberCategoryColor(item.numberCategory) + '20', borderRadius: '2px', marginLeft: '4px' }}>{item.numberCategory}</span>}
</div> </div>
<div style={{ display: 'flex', gap: '3px' }}> <div style={{ display: 'flex', gap: '3px' }}>
{item.status && <span style={{ color: getStatusColor(item.status), fontSize: '9px', padding: '1px 4px', background: getStatusColor(item.status) + '25', borderRadius: '2px' }}>{getStatusText(item.status)}</span>} {item.status && <span style={{ color: getStatusColor(item.status), fontSize: '9px', padding: '1px 4px', background: getStatusColor(item.status) + '25', borderRadius: '2px' }}>{getStatusText(item.status)}</span>}
{item.category && <span style={{ color: getCategoryColor(item.category), fontSize: '9px', padding: '1px 4px', background: getCategoryColor(item.category) + '25', borderRadius: '2px' }}>{getCategoryText(item.category)}</span>} {item.category && <span style={{ color: getCategoryColor(item.category), fontSize: '9px', padding: '1px 4px', background: getCategoryColor(item.category) + '25', borderRadius: '2px' }}>{getCategoryText(item.category)}</span>}
{item.rarity && <span style={{ color: getRarityColor(item.rarity), fontSize: '9px', padding: '1px 4px', background: getRarityColor(item.rarity) + '20', borderRadius: '2px' }}>{item.rarity}</span>} {item.rarity && <span style={{ color: getRarityColor(item.rarity), fontSize: '9px', padding: '1px 4px', background: getRarityColor(item.rarity) + '20', borderRadius: '2px' }}>{item.rarity}</span>}
</div> </div>
</div> </div>
{/* 第2行版本(彩色) + 已评级 + 评级公司 + 评级分数 + 三星 + 特殊标识 | 备注 */} {/* 第2行版本(彩色) + 已评级 + 评级公司 + 评级分数 + 三星 + 特殊标识 | 备注 */}
@ -487,6 +508,7 @@ export default function List() {
{ key: 'goalPrice', label: '售价' }, { key: 'goalPrice', label: '售价' },
{ key: 'category', label: '持仓类型' }, { key: 'category', label: '持仓类型' },
{ key: 'rarity', label: '珍惜度' }, { key: 'rarity', label: '珍惜度' },
{ key: 'numberCategory', label: '号码分类' },
{ key: 'gradingScore', label: '评级分数' } { key: 'gradingScore', label: '评级分数' }
].map(item => ( ].map(item => (
<div key={item.key} onClick={() => { <div key={item.key} onClick={() => {
@ -523,7 +545,7 @@ export default function List() {
<div key={item.id} onClick={() => 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' }}> <div key={item.id} onClick={() => 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' }}>
<div style={{ height: '80px', background: 'rgba(255,255,255,0.03)', borderRadius: '8px', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '32px', marginBottom: '10px' }}>🐉</div> <div style={{ height: '80px', background: 'rgba(255,255,255,0.03)', borderRadius: '8px', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '32px', marginBottom: '10px' }}>🐉</div>
<div style={{ color: '#fff', fontSize: '12px', fontWeight: 'normal' }}>{item.code || '-'}</div> <div style={{ color: '#fff', fontSize: '12px', fontWeight: 'normal' }}>{item.code || '-'}</div>
<div style={{ color: '#fbbf24', fontSize: '12px', fontFamily: 'monospace', marginTop: '2px' }}>{item.prefixSerial || '-'}</div> <div style={{ color: '#fbbf24', fontSize: '12px', fontFamily: 'monospace', marginTop: '2px' }}>{formatPrefixSerial(item.prefixSerial)}</div>
<div style={{ display: 'flex', gap: '4px', marginTop: '6px', flexWrap: 'wrap' }}> <div style={{ display: 'flex', gap: '4px', marginTop: '6px', flexWrap: 'wrap' }}>
{item.gradingScore && <span style={{ color: '#fbbf24', fontSize: '12px', fontWeight: 'bold' }}>{item.gradingScore}</span>} {item.gradingScore && <span style={{ color: '#fbbf24', fontSize: '12px', fontWeight: 'bold' }}>{item.gradingScore}</span>}
{item.threeStar && <span style={{ color: '#fbbf24', fontSize: '10px' }}></span>} {item.threeStar && <span style={{ color: '#fbbf24', fontSize: '10px' }}></span>}

View File

@ -10,6 +10,7 @@ export default function Stats() {
byGrading: [], byGrading: [],
byPackaging: [], byPackaging: [],
byRarity: [], byRarity: [],
byNumberCategory: [],
byVersion: [], byVersion: [],
byGradingCompany: [], byGradingCompany: [],
byGradingScore: [], byGradingScore: [],
@ -49,6 +50,7 @@ export default function Stats() {
byGrading: data.byGrading || [], byGrading: data.byGrading || [],
byPackaging: data.byPackaging || [], byPackaging: data.byPackaging || [],
byRarity: data.byRarity || [], byRarity: data.byRarity || [],
byNumberCategory: data.byNumberCategory || [],
byVersion: data.byVersion || [], byVersion: data.byVersion || [],
byGradingCompany: data.byGradingCompany || [], byGradingCompany: data.byGradingCompany || [],
byGradingScore: data.byGradingScore || [], byGradingScore: data.byGradingScore || [],
@ -94,10 +96,15 @@ export default function Stats() {
// //
const colors = { const colors = {
packaging: { '标十': '#22c55e', '标百': '#3b82f6', '单张': '#f59e0b', '裸钞': '#64748b' }, 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' }, status: { 'in_collection': '#22c55e', 'selling': '#f59e0b', 'sold': '#ef4444', 'grading': '#8b5cf6', 'repairing': '#f97316', 'transit': '#06b6d4', 'seeking': '#ec4899' },
category: { '自持': '#22c55e', '寄存': '#3b82f6', '寄售': '#f59e0b', '共有': '#8b5cf6', '寻号': '#06b6d4', '生肖钞': '#22c55e', '其他': '#64748b' }, 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) => { 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) => { const getLabel = (type, value) => {
@ -130,11 +143,23 @@ export default function Stats() {
return Number(val).toLocaleString('zh-CN') 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 }) => ( const DistributionCard = ({ title, data, type, valueKey, labelKey }) => (
<div style={{ background: 'rgba(255,255,255,0.03)', borderRadius: '12px', padding: '16px', marginBottom: '12px' }}> <div style={{ background: 'rgba(255,255,255,0.03)', borderRadius: '12px', padding: '16px', marginBottom: '12px' }}>
<div style={{ color: '#fbbf24', fontSize: '14px', fontWeight: 'bold', marginBottom: '12px' }}>{title}</div> <div style={{ color: '#fbbf24', fontSize: '14px', fontWeight: 'bold', marginBottom: '12px' }}>{title}</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '8px' }}> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '8px' }}>
{data.slice(0, 6).map((item, index) => { {getSortedData(data, type).slice(0, 6).map((item, index) => {
const value = item[valueKey] const value = item[valueKey]
const label = getLabel(type, item[labelKey] || value) const label = getLabel(type, item[labelKey] || value)
const color = getColor(type, value) const color = getColor(type, value)
@ -159,13 +184,17 @@ export default function Stats() {
e.currentTarget.style.borderColor = 'rgba(255,255,255,0.05)' e.currentTarget.style.borderColor = 'rgba(255,255,255,0.05)'
}} }}
> >
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', marginBottom: '4px' }}> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div style={{ width: '8px', height: '8px', borderRadius: '2px', background: color }} /> <div style={{ display: 'flex', alignItems: 'center', gap: '6px', flex: 1, overflow: 'hidden' }}>
<div style={{ color: '#fff', fontSize: '13px', fontWeight: '500', flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}> <div style={{ width: '8px', height: '8px', borderRadius: '2px', background: color, flexShrink: 0 }} />
{label} <div style={{ color: color, fontSize: '12px', fontWeight: '500', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{label}
</div>
</div>
<div style={{ color: '#fff', fontSize: '14px', fontWeight: 'bold', marginLeft: '8px', flexShrink: 0 }}>
{item.count}
</div> </div>
</div> </div>
<div style={{ color: '#fff', fontSize: '16px', fontWeight: 'bold' }}>{item.count}</div>
</div> </div>
) )
})} })}
@ -246,9 +275,9 @@ export default function Stats() {
</div> </div>
</div> </div>
<DistributionCard title="📋 状态分布" data={stats.byStatus} type="status" valueKey="status" />
<DistributionCard title="💼 持仓类型分布" data={stats.byCategory} type="category" valueKey="category" />
<DistributionCard title="📦 包装分布" data={stats.byPackaging} type="packaging" valueKey="packaging" /> <DistributionCard title="📦 包装分布" data={stats.byPackaging} type="packaging" valueKey="packaging" />
<DistributionCard title="🔢 号码分类分布" data={stats.byNumberCategory} type="numberCategory" valueKey="numberCategory" />
<DistributionCard title="📋 状态分布" data={stats.byStatus} type="status" valueKey="status" />
<DistributionCard title="⭐ 珍惜度分布" data={stats.byRarity} type="rarity" valueKey="rarity" /> <DistributionCard title="⭐ 珍惜度分布" data={stats.byRarity} type="rarity" valueKey="rarity" />
<DistributionCard title="🏷️ 版别分布" data={stats.byVersion} type="version" valueKey="version" /> <DistributionCard title="🏷️ 版别分布" data={stats.byVersion} type="version" valueKey="version" />
<DistributionCard title="🏅 评级机构分布" data={stats.byGradingCompany} type="gradingCompany" valueKey="company" /> <DistributionCard title="🏅 评级机构分布" data={stats.byGradingCompany} type="gradingCompany" valueKey="company" />