diff --git a/assets/config/point_book_config.json b/assets/config/point_book_config.json new file mode 100644 index 0000000..5f9aeda --- /dev/null +++ b/assets/config/point_book_config.json @@ -0,0 +1,91 @@ +{ + "bowOption": [ + { + "id": 1, + "name": "反曲弓", + "icon": "https://static.shelingxingqiu.com/attachment/2025-08-04/dbt8n02c0mwlgpcihn.png" + }, + { + "id": 2, + "name": "复合弓", + "icon": "https://static.shelingxingqiu.com/attachment/2025-08-04/dbt8n02d17qt8548j4.png" + }, + { + "id": 3, + "name": "美洲猎弓", + "icon": "https://static.shelingxingqiu.com/attachment/2025-08-04/dbt8n02cv1b8ucjp4y.png" + }, + { + "id": 4, + "name": "传统弓", + "icon": "https://static.shelingxingqiu.com/attachment/2025-08-04/dbt8n02cohc2omofsb.png" + }, + { + "id": 5, + "name": "光弓", + "icon": "https://static.shelingxingqiu.com/attachment/2025-08-04/dbt8n01ru48k0bwwz8.png" + } + ], + "targetOption": [ + { + "id": 1, + "name": "40 全环靶", + "icon": "https://static.shelingxingqiu.com/target20251126/%E5%85%A8%E7%8E%AF%E9%9D%B6@3x.svg", + "iconPng": "https://static.shelingxingqiu.com/shootTarget/shoot@2x.png" + }, + { + "id": 2, + "name": "80 全环靶", + "icon": "https://static.shelingxingqiu.com/target20251126/%E5%85%A8%E7%8E%AF%E9%9D%B6@3x.svg", + "iconPng": "https://static.shelingxingqiu.com/shootTarget/shoot@2x.png" + }, + { + "id": 3, + "name": "122 全环靶", + "icon": "https://static.shelingxingqiu.com/target20251126/%E5%85%A8%E7%8E%AF%E9%9D%B6@3x.svg", + "iconPng": "https://static.shelingxingqiu.com/shootTarget/shoot@2x.png" + }, + { + "id": 4, + "name": "40 半环靶", + "icon": "https://static.shelingxingqiu.com/target20251126/%E5%8D%8A%E7%8E%AF%E9%9D%B6.svg", + "iconPng": "https://static.shelingxingqiu.com/shootTarget/shoot@2x(1).png" + }, + { + "id": 5, + "name": "60 半环靶", + "icon": "https://static.shelingxingqiu.com/target20251126/%E5%8D%8A%E7%8E%AF%E9%9D%B6.svg", + "iconPng": "https://static.shelingxingqiu.com/shootTarget/shoot@2x(1).png" + }, + { + "id": 6, + "name": "80 半环靶", + "icon": "https://static.shelingxingqiu.com/target20251126/%E5%8D%8A%E7%8E%AF%E9%9D%B6.svg", + "iconPng": "https://static.shelingxingqiu.com/shootTarget/shoot@2x(1).png" + }, + { + "id": 7, + "name": "三连靶", + "icon": "https://static.shelingxingqiu.com/20260310target/%E4%B8%89%E8%BF%9E.svg", + "iconPng": "https://static.shelingxingqiu.com/shootTarget/shoot@2x(2).png" + }, + { + "id": 8, + "name": "品字靶", + "icon": "https://static.shelingxingqiu.com/target20251126/%E5%93%81%E5%AD%97%E9%9D%B6.svg", + "iconPng": "https://static.shelingxingqiu.com/shootTarget/shoot@2x(3).png" + }, + { + "id": 9, + "name": "复合 三连靶", + "icon": "https://static.shelingxingqiu.com/20260310target/%E5%A4%8D%E5%90%88%E4%B8%89%E8%BF%9E.svg", + "iconPng": "https://static.shelingxingqiu.com/shootTarget/shoot@2x(5).png" + }, + { + "id": 10, + "name": "复合 品字靶", + "icon": "https://static.shelingxingqiu.com/target20251126/%E5%A4%8D%E5%90%88%E5%93%81%E5%AD%97%E9%9D%B6.svg", + "iconPng": "https://static.shelingxingqiu.com/shootTarget/shoot@2x(4).png" + } + ] +} \ No newline at end of file diff --git a/lib/data/local/models/point_record.dart b/lib/data/local/models/point_record.dart index 3b58f51..1928abb 100644 --- a/lib/data/local/models/point_record.dart +++ b/lib/data/local/models/point_record.dart @@ -7,25 +7,6 @@ enum PointRecordStatus { completed, } -enum BowType { - recurve, - compound, - barebow, - traditional, - longbow, - other, -} - -enum TargetFaceType { - cm40, - cm60, - cm80, - cm122, - vegas3Spot, - vertical3Spot, - custom, -} - @embedded class HitPoint { HitPoint({ @@ -67,11 +48,17 @@ class PointRecord { @enumerated PointRecordStatus status = PointRecordStatus.draft; - @enumerated - BowType bowType = BowType.recurve; + /// Matches [BowOption.id] from point book config. + @Index(type: IndexType.value) + int bowOptionId = 1; - @enumerated - TargetFaceType targetFaceType = TargetFaceType.cm40; + late String bowName; + + /// Matches [TargetOption.id] from point book config. + @Index(type: IndexType.value) + int targetOptionId = 1; + + late String targetName; late String title; int distanceMeters = 18; @@ -88,8 +75,10 @@ class PointRecord { scores.isEmpty ? null : totalScore / scores.length; factory PointRecord.createDraft({ - required BowType bowType, - required TargetFaceType targetFaceType, + required int bowOptionId, + required String bowName, + required int targetOptionId, + required String targetName, required int distanceMeters, required int endCount, required int arrowsPerEnd, @@ -101,8 +90,10 @@ class PointRecord { return PointRecord() ..title = title ?? '未完成记分' ..status = PointRecordStatus.draft - ..bowType = bowType - ..targetFaceType = targetFaceType + ..bowOptionId = bowOptionId + ..bowName = bowName + ..targetOptionId = targetOptionId + ..targetName = targetName ..distanceMeters = distanceMeters ..endCount = endCount ..arrowsPerEnd = arrowsPerEnd @@ -117,8 +108,10 @@ class PointRecord { ..id = id ..title = title ..status = PointRecordStatus.completed - ..bowType = bowType - ..targetFaceType = targetFaceType + ..bowOptionId = bowOptionId + ..bowName = bowName + ..targetOptionId = targetOptionId + ..targetName = targetName ..distanceMeters = distanceMeters ..endCount = endCount ..arrowsPerEnd = arrowsPerEnd @@ -136,8 +129,10 @@ class PointRecord { ..id = id ..title = title ..status = status - ..bowType = bowType - ..targetFaceType = targetFaceType + ..bowOptionId = bowOptionId + ..bowName = bowName + ..targetOptionId = targetOptionId + ..targetName = targetName ..distanceMeters = distanceMeters ..endCount = endCount ..arrowsPerEnd = arrowsPerEnd diff --git a/lib/data/local/models/point_record.g.dart b/lib/data/local/models/point_record.g.dart index aa66c58..c9538d1 100644 --- a/lib/data/local/models/point_record.g.dart +++ b/lib/data/local/models/point_record.g.dart @@ -27,82 +27,90 @@ const PointRecordSchema = CollectionSchema( name: r'averageScore', type: IsarType.double, ), - r'bowType': PropertySchema( + r'bowName': PropertySchema( id: 2, - name: r'bowType', - type: IsarType.byte, - enumMap: _PointRecordbowTypeEnumValueMap, + name: r'bowName', + type: IsarType.string, + ), + r'bowOptionId': PropertySchema( + id: 3, + name: r'bowOptionId', + type: IsarType.long, ), r'completedAt': PropertySchema( - id: 3, + id: 4, name: r'completedAt', type: IsarType.dateTime, ), r'createdAt': PropertySchema( - id: 4, + id: 5, name: r'createdAt', type: IsarType.dateTime, ), r'distanceMeters': PropertySchema( - id: 5, + id: 6, name: r'distanceMeters', type: IsarType.long, ), r'endCount': PropertySchema( - id: 6, + id: 7, name: r'endCount', type: IsarType.long, ), r'hitPoints': PropertySchema( - id: 7, + id: 8, name: r'hitPoints', type: IsarType.objectList, target: r'HitPoint', ), r'isDraft': PropertySchema( - id: 8, + id: 9, name: r'isDraft', type: IsarType.bool, ), r'note': PropertySchema( - id: 9, + id: 10, name: r'note', type: IsarType.string, ), r'scores': PropertySchema( - id: 10, + id: 11, name: r'scores', type: IsarType.longList, ), r'status': PropertySchema( - id: 11, + id: 12, name: r'status', type: IsarType.byte, enumMap: _PointRecordstatusEnumValueMap, ), - r'targetFaceType': PropertySchema( - id: 12, - name: r'targetFaceType', - type: IsarType.byte, - enumMap: _PointRecordtargetFaceTypeEnumValueMap, + r'targetName': PropertySchema( + id: 13, + name: r'targetName', + type: IsarType.string, + ), + r'targetOptionId': PropertySchema( + id: 14, + name: r'targetOptionId', + type: IsarType.long, ), r'title': PropertySchema( - id: 13, + id: 15, name: r'title', type: IsarType.string, ), r'totalArrows': PropertySchema( - id: 14, + id: 16, name: r'totalArrows', type: IsarType.long, ), r'totalScore': PropertySchema( - id: 15, + id: 17, name: r'totalScore', type: IsarType.long, ), r'updatedAt': PropertySchema( - id: 16, + id: 18, name: r'updatedAt', type: IsarType.dateTime, ) @@ -151,6 +159,32 @@ const PointRecordSchema = CollectionSchema( caseSensitive: false, ) ], + ), + r'bowOptionId': IndexSchema( + id: 5551937090384643164, + name: r'bowOptionId', + unique: false, + replace: false, + properties: [ + IndexPropertySchema( + name: r'bowOptionId', + type: IndexType.value, + caseSensitive: false, + ) + ], + ), + r'targetOptionId': IndexSchema( + id: -8007721351844109533, + name: r'targetOptionId', + unique: false, + replace: false, + properties: [ + IndexPropertySchema( + name: r'targetOptionId', + type: IndexType.value, + caseSensitive: false, + ) + ], ) }, links: {}, @@ -167,6 +201,7 @@ int _pointRecordEstimateSize( Map> allOffsets, ) { var bytesCount = offsets.last; + bytesCount += 3 + object.bowName.length * 3; bytesCount += 3 + object.hitPoints.length * 3; { final offsets = allOffsets[HitPoint]!; @@ -182,6 +217,7 @@ int _pointRecordEstimateSize( } } bytesCount += 3 + object.scores.length * 8; + bytesCount += 3 + object.targetName.length * 3; bytesCount += 3 + object.title.length * 3; return bytesCount; } @@ -194,26 +230,28 @@ void _pointRecordSerialize( ) { writer.writeLong(offsets[0], object.arrowsPerEnd); writer.writeDouble(offsets[1], object.averageScore); - writer.writeByte(offsets[2], object.bowType.index); - writer.writeDateTime(offsets[3], object.completedAt); - writer.writeDateTime(offsets[4], object.createdAt); - writer.writeLong(offsets[5], object.distanceMeters); - writer.writeLong(offsets[6], object.endCount); + writer.writeString(offsets[2], object.bowName); + writer.writeLong(offsets[3], object.bowOptionId); + writer.writeDateTime(offsets[4], object.completedAt); + writer.writeDateTime(offsets[5], object.createdAt); + writer.writeLong(offsets[6], object.distanceMeters); + writer.writeLong(offsets[7], object.endCount); writer.writeObjectList( - offsets[7], + offsets[8], allOffsets, HitPointSchema.serialize, object.hitPoints, ); - writer.writeBool(offsets[8], object.isDraft); - writer.writeString(offsets[9], object.note); - writer.writeLongList(offsets[10], object.scores); - writer.writeByte(offsets[11], object.status.index); - writer.writeByte(offsets[12], object.targetFaceType.index); - writer.writeString(offsets[13], object.title); - writer.writeLong(offsets[14], object.totalArrows); - writer.writeLong(offsets[15], object.totalScore); - writer.writeDateTime(offsets[16], object.updatedAt); + writer.writeBool(offsets[9], object.isDraft); + writer.writeString(offsets[10], object.note); + writer.writeLongList(offsets[11], object.scores); + writer.writeByte(offsets[12], object.status.index); + writer.writeString(offsets[13], object.targetName); + writer.writeLong(offsets[14], object.targetOptionId); + writer.writeString(offsets[15], object.title); + writer.writeLong(offsets[16], object.totalArrows); + writer.writeLong(offsets[17], object.totalScore); + writer.writeDateTime(offsets[18], object.updatedAt); } PointRecord _pointRecordDeserialize( @@ -224,31 +262,29 @@ PointRecord _pointRecordDeserialize( ) { final object = PointRecord(); object.arrowsPerEnd = reader.readLong(offsets[0]); - object.bowType = - _PointRecordbowTypeValueEnumMap[reader.readByteOrNull(offsets[2])] ?? - BowType.recurve; - object.completedAt = reader.readDateTimeOrNull(offsets[3]); - object.createdAt = reader.readDateTime(offsets[4]); - object.distanceMeters = reader.readLong(offsets[5]); - object.endCount = reader.readLong(offsets[6]); + object.bowName = reader.readString(offsets[2]); + object.bowOptionId = reader.readLong(offsets[3]); + object.completedAt = reader.readDateTimeOrNull(offsets[4]); + object.createdAt = reader.readDateTime(offsets[5]); + object.distanceMeters = reader.readLong(offsets[6]); + object.endCount = reader.readLong(offsets[7]); object.hitPoints = reader.readObjectList( - offsets[7], + offsets[8], HitPointSchema.deserialize, allOffsets, HitPoint(), ) ?? []; object.id = id; - object.note = reader.readStringOrNull(offsets[9]); - object.scores = reader.readLongList(offsets[10]) ?? []; + object.note = reader.readStringOrNull(offsets[10]); + object.scores = reader.readLongList(offsets[11]) ?? []; object.status = - _PointRecordstatusValueEnumMap[reader.readByteOrNull(offsets[11])] ?? + _PointRecordstatusValueEnumMap[reader.readByteOrNull(offsets[12])] ?? PointRecordStatus.draft; - object.targetFaceType = _PointRecordtargetFaceTypeValueEnumMap[ - reader.readByteOrNull(offsets[12])] ?? - TargetFaceType.cm40; - object.title = reader.readString(offsets[13]); - object.updatedAt = reader.readDateTime(offsets[16]); + object.targetName = reader.readString(offsets[13]); + object.targetOptionId = reader.readLong(offsets[14]); + object.title = reader.readString(offsets[15]); + object.updatedAt = reader.readDateTime(offsets[18]); return object; } @@ -264,17 +300,18 @@ P _pointRecordDeserializeProp

( case 1: return (reader.readDoubleOrNull(offset)) as P; case 2: - return (_PointRecordbowTypeValueEnumMap[reader.readByteOrNull(offset)] ?? - BowType.recurve) as P; + return (reader.readString(offset)) as P; case 3: - return (reader.readDateTimeOrNull(offset)) as P; - case 4: - return (reader.readDateTime(offset)) as P; - case 5: return (reader.readLong(offset)) as P; + case 4: + return (reader.readDateTimeOrNull(offset)) as P; + case 5: + return (reader.readDateTime(offset)) as P; case 6: return (reader.readLong(offset)) as P; case 7: + return (reader.readLong(offset)) as P; + case 8: return (reader.readObjectList( offset, HitPointSchema.deserialize, @@ -282,48 +319,32 @@ P _pointRecordDeserializeProp

( HitPoint(), ) ?? []) as P; - case 8: - return (reader.readBool(offset)) as P; case 9: - return (reader.readStringOrNull(offset)) as P; + return (reader.readBool(offset)) as P; case 10: - return (reader.readLongList(offset) ?? []) as P; + return (reader.readStringOrNull(offset)) as P; case 11: + return (reader.readLongList(offset) ?? []) as P; + case 12: return (_PointRecordstatusValueEnumMap[reader.readByteOrNull(offset)] ?? PointRecordStatus.draft) as P; - case 12: - return (_PointRecordtargetFaceTypeValueEnumMap[ - reader.readByteOrNull(offset)] ?? - TargetFaceType.cm40) as P; case 13: return (reader.readString(offset)) as P; case 14: return (reader.readLong(offset)) as P; case 15: - return (reader.readLong(offset)) as P; + return (reader.readString(offset)) as P; case 16: + return (reader.readLong(offset)) as P; + case 17: + return (reader.readLong(offset)) as P; + case 18: return (reader.readDateTime(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); } } -const _PointRecordbowTypeEnumValueMap = { - 'recurve': 0, - 'compound': 1, - 'barebow': 2, - 'traditional': 3, - 'longbow': 4, - 'other': 5, -}; -const _PointRecordbowTypeValueEnumMap = { - 0: BowType.recurve, - 1: BowType.compound, - 2: BowType.barebow, - 3: BowType.traditional, - 4: BowType.longbow, - 5: BowType.other, -}; const _PointRecordstatusEnumValueMap = { 'draft': 0, 'completed': 1, @@ -332,24 +353,6 @@ const _PointRecordstatusValueEnumMap = { 0: PointRecordStatus.draft, 1: PointRecordStatus.completed, }; -const _PointRecordtargetFaceTypeEnumValueMap = { - 'cm40': 0, - 'cm60': 1, - 'cm80': 2, - 'cm122': 3, - 'vegas3Spot': 4, - 'vertical3Spot': 5, - 'custom': 6, -}; -const _PointRecordtargetFaceTypeValueEnumMap = { - 0: TargetFaceType.cm40, - 1: TargetFaceType.cm60, - 2: TargetFaceType.cm80, - 3: TargetFaceType.cm122, - 4: TargetFaceType.vegas3Spot, - 5: TargetFaceType.vertical3Spot, - 6: TargetFaceType.custom, -}; Id _pointRecordGetId(PointRecord object) { return object.id; @@ -395,6 +398,22 @@ extension PointRecordQueryWhereSort ); }); } + + QueryBuilder anyBowOptionId() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + const IndexWhereClause.any(indexName: r'bowOptionId'), + ); + }); + } + + QueryBuilder anyTargetOptionId() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + const IndexWhereClause.any(indexName: r'targetOptionId'), + ); + }); + } } extension PointRecordQueryWhere @@ -759,6 +778,190 @@ extension PointRecordQueryWhere )); }); } + + QueryBuilder bowOptionIdEqualTo( + int bowOptionId) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.equalTo( + indexName: r'bowOptionId', + value: [bowOptionId], + )); + }); + } + + QueryBuilder + bowOptionIdNotEqualTo(int bowOptionId) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'bowOptionId', + lower: [], + upper: [bowOptionId], + includeUpper: false, + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'bowOptionId', + lower: [bowOptionId], + includeLower: false, + upper: [], + )); + } else { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'bowOptionId', + lower: [bowOptionId], + includeLower: false, + upper: [], + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'bowOptionId', + lower: [], + upper: [bowOptionId], + includeUpper: false, + )); + } + }); + } + + QueryBuilder + bowOptionIdGreaterThan( + int bowOptionId, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.between( + indexName: r'bowOptionId', + lower: [bowOptionId], + includeLower: include, + upper: [], + )); + }); + } + + QueryBuilder bowOptionIdLessThan( + int bowOptionId, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.between( + indexName: r'bowOptionId', + lower: [], + upper: [bowOptionId], + includeUpper: include, + )); + }); + } + + QueryBuilder bowOptionIdBetween( + int lowerBowOptionId, + int upperBowOptionId, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.between( + indexName: r'bowOptionId', + lower: [lowerBowOptionId], + includeLower: includeLower, + upper: [upperBowOptionId], + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + targetOptionIdEqualTo(int targetOptionId) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.equalTo( + indexName: r'targetOptionId', + value: [targetOptionId], + )); + }); + } + + QueryBuilder + targetOptionIdNotEqualTo(int targetOptionId) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'targetOptionId', + lower: [], + upper: [targetOptionId], + includeUpper: false, + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'targetOptionId', + lower: [targetOptionId], + includeLower: false, + upper: [], + )); + } else { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'targetOptionId', + lower: [targetOptionId], + includeLower: false, + upper: [], + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'targetOptionId', + lower: [], + upper: [targetOptionId], + includeUpper: false, + )); + } + }); + } + + QueryBuilder + targetOptionIdGreaterThan( + int targetOptionId, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.between( + indexName: r'targetOptionId', + lower: [targetOptionId], + includeLower: include, + upper: [], + )); + }); + } + + QueryBuilder + targetOptionIdLessThan( + int targetOptionId, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.between( + indexName: r'targetOptionId', + lower: [], + upper: [targetOptionId], + includeUpper: include, + )); + }); + } + + QueryBuilder + targetOptionIdBetween( + int lowerTargetOptionId, + int upperTargetOptionId, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.between( + indexName: r'targetOptionId', + lower: [lowerTargetOptionId], + includeLower: includeLower, + upper: [upperTargetOptionId], + includeUpper: includeUpper, + )); + }); + } } extension PointRecordQueryFilter @@ -903,52 +1106,188 @@ extension PointRecordQueryFilter }); } - QueryBuilder bowTypeEqualTo( - BowType value) { + QueryBuilder bowNameEqualTo( + String value, { + bool caseSensitive = true, + }) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.equalTo( - property: r'bowType', + property: r'bowName', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + bowNameGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'bowName', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder bowNameLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'bowName', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder bowNameBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'bowName', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + bowNameStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'bowName', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder bowNameEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'bowName', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder bowNameContains( + String value, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'bowName', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder bowNameMatches( + String pattern, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'bowName', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + bowNameIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'bowName', + value: '', + )); + }); + } + + QueryBuilder + bowNameIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'bowName', + value: '', + )); + }); + } + + QueryBuilder + bowOptionIdEqualTo(int value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'bowOptionId', value: value, )); }); } QueryBuilder - bowTypeGreaterThan( - BowType value, { + bowOptionIdGreaterThan( + int value, { bool include = false, }) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.greaterThan( include: include, - property: r'bowType', + property: r'bowOptionId', value: value, )); }); } - QueryBuilder bowTypeLessThan( - BowType value, { + QueryBuilder + bowOptionIdLessThan( + int value, { bool include = false, }) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.lessThan( include: include, - property: r'bowType', + property: r'bowOptionId', value: value, )); }); } - QueryBuilder bowTypeBetween( - BowType lower, - BowType upper, { + QueryBuilder + bowOptionIdBetween( + int lower, + int upper, { bool includeLower = true, bool includeUpper = true, }) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.between( - property: r'bowType', + property: r'bowOptionId', lower: lower, includeLower: includeLower, upper: upper, @@ -1698,53 +2037,189 @@ extension PointRecordQueryFilter } QueryBuilder - targetFaceTypeEqualTo(TargetFaceType value) { + targetNameEqualTo( + String value, { + bool caseSensitive = true, + }) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.equalTo( - property: r'targetFaceType', + property: r'targetName', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + targetNameGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'targetName', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + targetNameLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'targetName', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + targetNameBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'targetName', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + targetNameStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'targetName', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + targetNameEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'targetName', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + targetNameContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'targetName', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + targetNameMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'targetName', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + targetNameIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'targetName', + value: '', + )); + }); + } + + QueryBuilder + targetNameIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'targetName', + value: '', + )); + }); + } + + QueryBuilder + targetOptionIdEqualTo(int value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'targetOptionId', value: value, )); }); } QueryBuilder - targetFaceTypeGreaterThan( - TargetFaceType value, { + targetOptionIdGreaterThan( + int value, { bool include = false, }) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.greaterThan( include: include, - property: r'targetFaceType', + property: r'targetOptionId', value: value, )); }); } QueryBuilder - targetFaceTypeLessThan( - TargetFaceType value, { + targetOptionIdLessThan( + int value, { bool include = false, }) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.lessThan( include: include, - property: r'targetFaceType', + property: r'targetOptionId', value: value, )); }); } QueryBuilder - targetFaceTypeBetween( - TargetFaceType lower, - TargetFaceType upper, { + targetOptionIdBetween( + int lower, + int upper, { bool includeLower = true, bool includeUpper = true, }) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.between( - property: r'targetFaceType', + property: r'targetOptionId', lower: lower, includeLower: includeLower, upper: upper, @@ -2095,15 +2570,27 @@ extension PointRecordQuerySortBy }); } - QueryBuilder sortByBowType() { + QueryBuilder sortByBowName() { return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'bowType', Sort.asc); + return query.addSortBy(r'bowName', Sort.asc); }); } - QueryBuilder sortByBowTypeDesc() { + QueryBuilder sortByBowNameDesc() { return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'bowType', Sort.desc); + return query.addSortBy(r'bowName', Sort.desc); + }); + } + + QueryBuilder sortByBowOptionId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'bowOptionId', Sort.asc); + }); + } + + QueryBuilder sortByBowOptionIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'bowOptionId', Sort.desc); }); } @@ -2192,16 +2679,28 @@ extension PointRecordQuerySortBy }); } - QueryBuilder sortByTargetFaceType() { + QueryBuilder sortByTargetName() { return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'targetFaceType', Sort.asc); + return query.addSortBy(r'targetName', Sort.asc); + }); + } + + QueryBuilder sortByTargetNameDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'targetName', Sort.desc); + }); + } + + QueryBuilder sortByTargetOptionId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'targetOptionId', Sort.asc); }); } QueryBuilder - sortByTargetFaceTypeDesc() { + sortByTargetOptionIdDesc() { return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'targetFaceType', Sort.desc); + return query.addSortBy(r'targetOptionId', Sort.desc); }); } @@ -2282,15 +2781,27 @@ extension PointRecordQuerySortThenBy }); } - QueryBuilder thenByBowType() { + QueryBuilder thenByBowName() { return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'bowType', Sort.asc); + return query.addSortBy(r'bowName', Sort.asc); }); } - QueryBuilder thenByBowTypeDesc() { + QueryBuilder thenByBowNameDesc() { return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'bowType', Sort.desc); + return query.addSortBy(r'bowName', Sort.desc); + }); + } + + QueryBuilder thenByBowOptionId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'bowOptionId', Sort.asc); + }); + } + + QueryBuilder thenByBowOptionIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'bowOptionId', Sort.desc); }); } @@ -2391,16 +2902,28 @@ extension PointRecordQuerySortThenBy }); } - QueryBuilder thenByTargetFaceType() { + QueryBuilder thenByTargetName() { return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'targetFaceType', Sort.asc); + return query.addSortBy(r'targetName', Sort.asc); + }); + } + + QueryBuilder thenByTargetNameDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'targetName', Sort.desc); + }); + } + + QueryBuilder thenByTargetOptionId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'targetOptionId', Sort.asc); }); } QueryBuilder - thenByTargetFaceTypeDesc() { + thenByTargetOptionIdDesc() { return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'targetFaceType', Sort.desc); + return query.addSortBy(r'targetOptionId', Sort.desc); }); } @@ -2467,9 +2990,16 @@ extension PointRecordQueryWhereDistinct }); } - QueryBuilder distinctByBowType() { + QueryBuilder distinctByBowName( + {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'bowType'); + return query.addDistinctBy(r'bowName', caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctByBowOptionId() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'bowOptionId'); }); } @@ -2522,9 +3052,16 @@ extension PointRecordQueryWhereDistinct }); } - QueryBuilder distinctByTargetFaceType() { + QueryBuilder distinctByTargetName( + {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'targetFaceType'); + return query.addDistinctBy(r'targetName', caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctByTargetOptionId() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'targetOptionId'); }); } @@ -2574,9 +3111,15 @@ extension PointRecordQueryProperty }); } - QueryBuilder bowTypeProperty() { + QueryBuilder bowNameProperty() { return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'bowType'); + return query.addPropertyName(r'bowName'); + }); + } + + QueryBuilder bowOptionIdProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'bowOptionId'); }); } @@ -2636,10 +3179,15 @@ extension PointRecordQueryProperty }); } - QueryBuilder - targetFaceTypeProperty() { + QueryBuilder targetNameProperty() { return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'targetFaceType'); + return query.addPropertyName(r'targetName'); + }); + } + + QueryBuilder targetOptionIdProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'targetOptionId'); }); } diff --git a/lib/features/records/presentation/point_book_list_page.dart b/lib/features/records/presentation/point_book_list_page.dart index abd7b63..79e55a7 100644 --- a/lib/features/records/presentation/point_book_list_page.dart +++ b/lib/features/records/presentation/point_book_list_page.dart @@ -3,7 +3,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:arcx/app/app_theme.dart'; import 'package:arcx/data/local/models/models.dart'; +import 'package:arcx/features/scoring/application/point_book_config_provider.dart'; import 'package:arcx/features/scoring/application/scoring_service.dart'; +import 'package:arcx/features/scoring/domain/point_book_config.dart'; import 'package:arcx/features/scoring/presentation/point_book_edit_page.dart'; class PointBookListPage extends ConsumerStatefulWidget { @@ -14,22 +16,31 @@ class PointBookListPage extends ConsumerStatefulWidget { } class _PointBookListPageState extends ConsumerState { - BowType? _bowFilter; - TargetFaceType? _targetFilter; + int? _bowFilter; + int? _targetFilter; @override Widget build(BuildContext context) { final recordsAsync = ref.watch(pointRecordsProvider); + final configAsync = ref.watch(pointBookConfigProvider); return Scaffold( appBar: AppBar(title: const Text('计分记录')), body: Column( children: [ - _FilterBar( - bowFilter: _bowFilter, - targetFilter: _targetFilter, - onBowChanged: (v) => setState(() => _bowFilter = v), - onTargetChanged: (v) => setState(() => _targetFilter = v), + configAsync.when( + loading: () => const SizedBox(height: 64), + error: (e, _) => Padding( + padding: const EdgeInsets.all(16), + child: Text('配置加载失败:$e'), + ), + data: (config) => _FilterBar( + config: config, + bowFilter: _bowFilter, + targetFilter: _targetFilter, + onBowChanged: (v) => setState(() => _bowFilter = v), + onTargetChanged: (v) => setState(() => _targetFilter = v), + ), ), Expanded( child: recordsAsync.when( @@ -64,10 +75,10 @@ class _PointBookListPageState extends ConsumerState { List _applyFilters(List records) { var result = records; if (_bowFilter != null) { - result = result.where((r) => r.bowType == _bowFilter).toList(); + result = result.where((r) => r.bowOptionId == _bowFilter).toList(); } if (_targetFilter != null) { - result = result.where((r) => r.targetFaceType == _targetFilter).toList(); + result = result.where((r) => r.targetOptionId == _targetFilter).toList(); } return result; } @@ -107,16 +118,18 @@ class _PointBookListPageState extends ConsumerState { class _FilterBar extends StatelessWidget { const _FilterBar({ + required this.config, required this.bowFilter, required this.targetFilter, required this.onBowChanged, required this.onTargetChanged, }); - final BowType? bowFilter; - final TargetFaceType? targetFilter; - final ValueChanged onBowChanged; - final ValueChanged onTargetChanged; + final PointBookConfig config; + final int? bowFilter; + final int? targetFilter; + final ValueChanged onBowChanged; + final ValueChanged onTargetChanged; @override Widget build(BuildContext context) { @@ -125,21 +138,23 @@ class _FilterBar extends StatelessWidget { child: Row( children: [ Expanded( - child: _Dropdown( + child: _Dropdown( label: '弓型', value: bowFilter, - items: BowType.values, - labelOf: _bowLabel, + items: config.bowOption.map((e) => e.id).toList(), + labelOf: (id) => + config.bowById(id)?.name ?? id.toString(), onChanged: onBowChanged, ), ), const SizedBox(width: 12), Expanded( - child: _Dropdown( + child: _Dropdown( label: '靶纸', value: targetFilter, - items: TargetFaceType.values, - labelOf: _targetLabel, + items: config.targetOption.map((e) => e.id).toList(), + labelOf: (id) => + config.targetById(id)?.name ?? id.toString(), onChanged: onTargetChanged, ), ), @@ -177,10 +192,12 @@ class _Dropdown extends StatelessWidget { ), items: [ DropdownMenuItem(value: null, child: Text('全部')), - ...items.map((e) => DropdownMenuItem( - value: e, - child: Text(labelOf(e)), - )), + ...items.map( + (e) => DropdownMenuItem( + value: e, + child: Text(labelOf(e), overflow: TextOverflow.ellipsis), + ), + ), ], onChanged: onChanged, ); @@ -232,7 +249,7 @@ class _RecordTile extends StatelessWidget { ], ), subtitle: Text( - '${_bowLabel(record.bowType)} · ${record.distanceMeters}m · ${_targetLabel(record.targetFaceType)}\n' + '${record.bowName} · ${record.distanceMeters}m · ${record.targetName}\n' '${_formatDate(record.createdAt)} ${record.totalArrows}支 ${record.averageScore != null ? '均${record.averageScore!.toStringAsFixed(1)}环' : ''}', style: TextStyle(fontSize: 12, color: AppTheme.textSecondary), ), @@ -272,42 +289,6 @@ class _EmptyState extends StatelessWidget { } } -String _bowLabel(BowType t) { - switch (t) { - case BowType.recurve: - return '反曲弓'; - case BowType.compound: - return '复合弓'; - case BowType.barebow: - return '光弓'; - case BowType.traditional: - return '传统弓'; - case BowType.longbow: - return '长弓'; - case BowType.other: - return '其他'; - } -} - -String _targetLabel(TargetFaceType t) { - switch (t) { - case TargetFaceType.cm40: - return '40cm'; - case TargetFaceType.cm60: - return '60cm'; - case TargetFaceType.cm80: - return '80cm'; - case TargetFaceType.cm122: - return '122cm'; - case TargetFaceType.vegas3Spot: - return '维加斯三联'; - case TargetFaceType.vertical3Spot: - return '垂直三联'; - case TargetFaceType.custom: - return '自定义'; - } -} - String _formatDate(DateTime dt) { return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')} ' '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}'; diff --git a/lib/features/scoring/application/point_book_config_provider.dart b/lib/features/scoring/application/point_book_config_provider.dart new file mode 100644 index 0000000..621f707 --- /dev/null +++ b/lib/features/scoring/application/point_book_config_provider.dart @@ -0,0 +1,7 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../domain/point_book_config.dart'; + +final pointBookConfigProvider = FutureProvider((ref) { + return PointBookConfig.fromAsset(); +}); diff --git a/lib/features/scoring/application/scoring_service.dart b/lib/features/scoring/application/scoring_service.dart index 1745460..6d11573 100644 --- a/lib/features/scoring/application/scoring_service.dart +++ b/lib/features/scoring/application/scoring_service.dart @@ -40,8 +40,10 @@ class PointRecordsNotifier extends AsyncNotifier> { /// Creates a new draft from scoring parameters and returns it. Future createDraft({ - required BowType bowType, - required TargetFaceType targetFaceType, + required int bowOptionId, + required String bowName, + required int targetOptionId, + required String targetName, required int distanceMeters, required int endCount, required int arrowsPerEnd, @@ -49,8 +51,10 @@ class PointRecordsNotifier extends AsyncNotifier> { }) async { final isar = await _isar(); final draft = PointRecord.createDraft( - bowType: bowType, - targetFaceType: targetFaceType, + bowOptionId: bowOptionId, + bowName: bowName, + targetOptionId: targetOptionId, + targetName: targetName, distanceMeters: distanceMeters, endCount: endCount, arrowsPerEnd: arrowsPerEnd, diff --git a/lib/features/scoring/domain/point_book_config.dart b/lib/features/scoring/domain/point_book_config.dart new file mode 100644 index 0000000..6725ac4 --- /dev/null +++ b/lib/features/scoring/domain/point_book_config.dart @@ -0,0 +1,93 @@ +import 'dart:convert'; + +import 'package:flutter/services.dart'; + +class BowOption { + const BowOption({ + required this.id, + required this.name, + required this.icon, + }); + + final int id; + final String name; + final String icon; + + factory BowOption.fromJson(Map json) { + return BowOption( + id: json['id'] as int, + name: json['name'] as String, + icon: json['icon'] as String? ?? '', + ); + } +} + +class TargetOption { + const TargetOption({ + required this.id, + required this.name, + required this.icon, + required this.iconPng, + }); + + final int id; + final String name; + final String icon; + final String iconPng; + + factory TargetOption.fromJson(Map json) { + return TargetOption( + id: json['id'] as int, + name: json['name'] as String, + icon: json['icon'] as String? ?? '', + iconPng: json['iconPng'] as String? ?? '', + ); + } +} + +class PointBookConfig { + const PointBookConfig({ + required this.bowOption, + required this.targetOption, + }); + + static const assetPath = 'assets/config/point_book_config.json'; + + final List bowOption; + final List targetOption; + + factory PointBookConfig.fromJson(Map json) { + final bows = (json['bowOption'] as List? ?? const []) + .cast>() + .map(BowOption.fromJson) + .toList(); + final targets = (json['targetOption'] as List? ?? const []) + .cast>() + .map(TargetOption.fromJson) + .toList(); + return PointBookConfig(bowOption: bows, targetOption: targets); + } + + static Future fromAsset({ + String path = assetPath, + }) async { + final raw = await rootBundle.loadString(path); + return PointBookConfig.fromJson( + jsonDecode(raw) as Map, + ); + } + + BowOption? bowById(int id) { + for (final o in bowOption) { + if (o.id == id) return o; + } + return null; + } + + TargetOption? targetById(int id) { + for (final o in targetOption) { + if (o.id == id) return o; + } + return null; + } +} diff --git a/lib/features/scoring/presentation/point_book_create_page.dart b/lib/features/scoring/presentation/point_book_create_page.dart index 519183b..ca94bda 100644 --- a/lib/features/scoring/presentation/point_book_create_page.dart +++ b/lib/features/scoring/presentation/point_book_create_page.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:arcx/app/app_theme.dart'; -import 'package:arcx/data/local/models/models.dart'; +import 'package:arcx/features/scoring/application/point_book_config_provider.dart'; import 'package:arcx/features/scoring/application/scoring_service.dart'; +import 'package:arcx/features/scoring/domain/point_book_config.dart'; class PointBookCreatePage extends ConsumerStatefulWidget { const PointBookCreatePage({super.key}); @@ -14,95 +16,187 @@ class PointBookCreatePage extends ConsumerStatefulWidget { } class _PointBookCreatePageState extends ConsumerState { - BowType _bowType = BowType.recurve; - TargetFaceType _targetFace = TargetFaceType.cm40; + static const _distances = [10, 18, 25, 30, 40, 50, 60, 70, 90]; + static const _endOptions = [3, 4, 5, 6, 8, 10, 12]; + static const _arrowOptions = [3, 4, 5, 6, 12]; + + int? _bowOptionId; + int? _targetOptionId; int _distance = 18; int _endCount = 6; int _arrowsPerEnd = 6; bool _creating = false; - static const _distances = [10, 18, 25, 30, 40, 50, 60, 70, 90]; - static const _endOptions = [3, 4, 5, 6, 8, 10, 12]; - static const _arrowOptions = [3, 4, 5, 6, 12]; + final _distanceCustom = TextEditingController(); + final _endCustom = TextEditingController(); + final _arrowCustom = TextEditingController(); + + @override + void dispose() { + _distanceCustom.dispose(); + _endCustom.dispose(); + _arrowCustom.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('新建记分')), - body: ListView( - padding: const EdgeInsets.all(16), - children: [ - _SectionTitle('弓型'), - _ChipGroup( - options: BowType.values, - value: _bowType, - labelOf: _bowLabel, - onChanged: (v) => setState(() => _bowType = v), - ), - const SizedBox(height: 16), - _SectionTitle('距离'), - _ChipGroup( - options: _distances, - value: _distance, - labelOf: (v) => '${v}m', - onChanged: (v) => setState(() => _distance = v), - ), - const SizedBox(height: 16), - _SectionTitle('靶纸类型'), - _ChipGroup( - options: TargetFaceType.values, - value: _targetFace, - labelOf: _targetLabel, - onChanged: (v) => setState(() => _targetFace = v), - ), - const SizedBox(height: 16), - Row( - children: [ - Expanded( - child: _NumberPicker( - label: '组数', - value: _endCount, - options: _endOptions, - onChanged: (v) => setState(() => _endCount = v), - ), + final configAsync = ref.watch(pointBookConfigProvider); + + return GestureDetector( + onTap: _unfocus, + behavior: HitTestBehavior.translucent, + child: Scaffold( + appBar: AppBar(title: const Text('选择参数')), + body: configAsync.when( + loading: () => const Center(child: CircularProgressIndicator()), + error: (e, _) => Center(child: Text('配置加载失败:$e')), + data: (config) { + _ensureDefaults(config); + final bow = config.bowById(_bowOptionId!); + final target = config.targetById(_targetOptionId!); + if (bow == null || target == null) { + return const Center(child: Text('配置无效')); + } + return ListView( + padding: const EdgeInsets.all(16), + keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, + children: [ + const _SectionTitle('弓型'), + _ChipGroup( + options: config.bowOption.map((e) => e.id).toList(), + value: _bowOptionId!, + labelOf: (id) => config.bowById(id)?.name ?? '$id', + onChanged: (v) { + _unfocus(); + setState(() => _bowOptionId = v); + }, ), - const SizedBox(width: 12), - Expanded( - child: _NumberPicker( - label: '每组箭数', - value: _arrowsPerEnd, - options: _arrowOptions, - onChanged: (v) => setState(() => _arrowsPerEnd = v), - ), + const SizedBox(height: 16), + const _SectionTitle('距离'), + _PresetWithCustom( + options: _distances, + value: _distance, + labelOf: (v) => '${v}m', + customController: _distanceCustom, + customHint: '自定义(m)', + min: 1, + max: 300, + onPreset: (v) { + _unfocus(); + setState(() { + _distance = v; + _distanceCustom.clear(); + }); + }, + onCustom: (v) => setState(() => _distance = v), + onInvalid: () => _toast('请输入 1–300 的距离'), ), - ], - ), - const SizedBox(height: 32), - _Summary( - bowType: _bowType, - distance: _distance, - targetFace: _targetFace, - endCount: _endCount, - arrowsPerEnd: _arrowsPerEnd, - ), - const SizedBox(height: 16), - FilledButton.icon( - onPressed: _creating ? null : _start, - icon: const Icon(Icons.play_arrow), - label: const Text('开始记分'), - ), - ], + const SizedBox(height: 16), + const _SectionTitle('靶纸类型'), + _ChipGroup( + options: config.targetOption.map((e) => e.id).toList(), + value: _targetOptionId!, + labelOf: (id) => config.targetById(id)?.name ?? '$id', + onChanged: (v) { + _unfocus(); + setState(() => _targetOptionId = v); + }, + ), + const SizedBox(height: 16), + const _SectionTitle('组数'), + _PresetWithCustom( + options: _endOptions, + value: _endCount, + labelOf: (v) => '$v', + customController: _endCustom, + customHint: '自定义', + min: 1, + max: 36, + onPreset: (v) { + _unfocus(); + setState(() { + _endCount = v; + _endCustom.clear(); + }); + }, + onCustom: (v) => setState(() => _endCount = v), + onInvalid: () => _toast('请输入 1–36 的组数'), + ), + const SizedBox(height: 16), + const _SectionTitle('每组箭数'), + _PresetWithCustom( + options: _arrowOptions, + value: _arrowsPerEnd, + labelOf: (v) => '$v', + customController: _arrowCustom, + customHint: '自定义', + min: 1, + max: 36, + onPreset: (v) { + _unfocus(); + setState(() { + _arrowsPerEnd = v; + _arrowCustom.clear(); + }); + }, + onCustom: (v) => setState(() => _arrowsPerEnd = v), + onInvalid: () => _toast('请输入 1–36 的箭数'), + ), + const SizedBox(height: 32), + _Summary( + bowName: bow.name, + distance: _distance, + targetName: target.name, + endCount: _endCount, + arrowsPerEnd: _arrowsPerEnd, + ), + const SizedBox(height: 16), + FilledButton.icon( + onPressed: _creating + ? null + : () { + _unfocus(); + _start(bow: bow, target: target); + }, + icon: const Icon(Icons.play_arrow), + label: const Text('下一步'), + ), + ], + ); + }, + ), ), ); } - Future _start() async { + void _unfocus() { + FocusManager.instance.primaryFocus?.unfocus(); + } + + void _ensureDefaults(PointBookConfig config) { + _bowOptionId ??= config.bowOption.isNotEmpty + ? config.bowOption.first.id + : 1; + _targetOptionId ??= config.targetOption.isNotEmpty + ? config.targetOption.first.id + : 1; + } + + void _toast(String msg) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg))); + } + + Future _start({ + required BowOption bow, + required TargetOption target, + }) async { setState(() => _creating = true); - final draft = await ref - .read(pointRecordsProvider.notifier) - .createDraft( - bowType: _bowType, - targetFaceType: _targetFace, + final draft = await ref.read(pointRecordsProvider.notifier).createDraft( + bowOptionId: bow.id, + bowName: bow.name, + targetOptionId: target.id, + targetName: target.name, distanceMeters: _distance, endCount: _endCount, arrowsPerEnd: _arrowsPerEnd, @@ -164,84 +258,129 @@ class _ChipGroup extends StatelessWidget { } } -class _NumberPicker extends StatelessWidget { - const _NumberPicker({ - required this.label, - required this.value, +/// Preset chips plus a trailing custom number field. +class _PresetWithCustom extends StatelessWidget { + const _PresetWithCustom({ required this.options, - required this.onChanged, + required this.value, + required this.labelOf, + required this.customController, + required this.customHint, + required this.min, + required this.max, + required this.onPreset, + required this.onCustom, + required this.onInvalid, }); - final String label; - final int value; final List options; - final ValueChanged onChanged; + final int value; + final String Function(int) labelOf; + final TextEditingController customController; + final String customHint; + final int min; + final int max; + final ValueChanged onPreset; + final ValueChanged onCustom; + final VoidCallback onInvalid; + + bool get _isPreset => options.contains(value); + + void _applyCustom(String raw) { + final n = int.tryParse(raw.trim()); + if (n == null || n < min || n > max) { + onInvalid(); + return; + } + onCustom(n); + } @override Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.black12), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible(child: Text(label, style: const TextStyle(fontSize: 13))), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.remove_circle_outline), - iconSize: 20, - visualDensity: VisualDensity.compact, - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - onPressed: () { - final i = options.indexOf(value); - if (i > 0) onChanged(options[i - 1]); - }, + return Wrap( + spacing: 8, + runSpacing: 8, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + ...options.map((o) { + final selected = _isPreset && o == value; + return ChoiceChip( + label: Text(labelOf(o)), + selected: selected, + onSelected: (_) => onPreset(o), + selectedColor: AppTheme.primary, + labelStyle: TextStyle( + color: selected ? Colors.white : Colors.black87, + ), + ); + }), + SizedBox( + width: 96, + height: 40, + child: TextField( + controller: customController, + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 14), + decoration: InputDecoration( + isDense: true, + hintText: customHint, + contentPadding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 10, ), - Text( - '$value', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w700, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(20), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(20), + borderSide: BorderSide( + color: !_isPreset ? AppTheme.primary : Colors.black26, + width: !_isPreset ? 1.5 : 1, ), ), - IconButton( - icon: const Icon(Icons.add_circle_outline), - iconSize: 20, - visualDensity: VisualDensity.compact, - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - onPressed: () { - final i = options.indexOf(value); - if (i < options.length - 1) onChanged(options[i + 1]); - }, + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(20), + borderSide: const BorderSide( + color: AppTheme.primary, + width: 1.5, + ), ), - ], + ), + onTapOutside: (_) { + FocusManager.instance.primaryFocus?.unfocus(); + final raw = customController.text.trim(); + if (raw.isNotEmpty) _applyCustom(raw); + }, + onChanged: (raw) { + if (raw.trim().isEmpty) return; + final n = int.tryParse(raw.trim()); + if (n != null && n >= min && n <= max) { + onCustom(n); + } + }, + onSubmitted: _applyCustom, + onEditingComplete: () => _applyCustom(customController.text), ), - ], - ), + ), + ], ); } } class _Summary extends StatelessWidget { const _Summary({ - required this.bowType, + required this.bowName, required this.distance, - required this.targetFace, + required this.targetName, required this.endCount, required this.arrowsPerEnd, }); - final BowType bowType; + final String bowName; final int distance; - final TargetFaceType targetFace; + final String targetName; final int endCount; final int arrowsPerEnd; @@ -258,7 +397,7 @@ class _Summary extends StatelessWidget { children: [ Expanded( child: Text( - '${_bowLabel(bowType)} · ${distance}m · ${_targetLabel(targetFace)}', + '$bowName · ${distance}m · $targetName', style: const TextStyle(fontSize: 13), ), ), @@ -275,39 +414,3 @@ class _Summary extends StatelessWidget { ); } } - -String _bowLabel(BowType t) { - switch (t) { - case BowType.recurve: - return '反曲弓'; - case BowType.compound: - return '复合弓'; - case BowType.barebow: - return '光弓'; - case BowType.traditional: - return '传统弓'; - case BowType.longbow: - return '长弓'; - case BowType.other: - return '其他'; - } -} - -String _targetLabel(TargetFaceType t) { - switch (t) { - case TargetFaceType.cm40: - return '40cm'; - case TargetFaceType.cm60: - return '60cm'; - case TargetFaceType.cm80: - return '80cm'; - case TargetFaceType.cm122: - return '122cm'; - case TargetFaceType.vegas3Spot: - return '维加斯三联'; - case TargetFaceType.vertical3Spot: - return '垂直三联'; - case TargetFaceType.custom: - return '自定义'; - } -} diff --git a/pubspec.yaml b/pubspec.yaml index ad6ecea..5b85f44 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -49,3 +49,4 @@ flutter: assets: - assets/images/ - assets/branding/ + - assets/config/