/** * DimensionCard —— 32 维诊断单项卡片(F3)。 * * - ok 维度:name_cn + value(四种形态由 formatDimValue 统一处理:字符串直显 / * 数字转字符串 / 数组 join '、' / 空值显示 '—')+ 来源标签中文映射 + 置信度百分比 * - confidence < 0.7 加“仅供参考”小标(NFR-UX-05) * - skipped 维度:整体灰显,evidence 作为未覆盖原因文案 * - rule_hits 非空时折叠展示“依据规则 N 条” */ import { EMPTY_DIMENSION, DimensionWire, formatDimValue, sourceColor, sourceLabel } from './ReportTypes' import { COLOR_PRIMARY } from '../../core/Theme' const CARD_PRIMARY_COLOR: string = COLOR_PRIMARY @Component export struct DimensionCard { dim: DimensionWire = EMPTY_DIMENSION /** 已提交的反馈判定('' = 未反馈;'accurate' / 'inaccurate' 高亮已选项,宿主回显用)。 */ feedbackVerdict: string = '' /** 提交反馈(组件保持无网络):宿主调 POST /reports/{id}/feedback。 */ onFeedback: (dimKey: string, verdict: string, correction: string) => void = () => {} @State ruleExpanded: boolean = false @State correctionOpen: boolean = false // 「不准」展开的行内修正输入 @State correctionText: string = '' private isSkipped(): boolean { return this.dim.status !== 'ok' } private titleText(): string { const n = this.dim.name_cn if (n !== undefined && n !== '') { return n } const k = this.dim.key if (k !== undefined && k !== '') { return k } const num = this.dim.number return num !== undefined ? `维度 ${num}` : '未知维度' } private hasConfidence(): boolean { const c = this.dim.confidence return c !== undefined && c !== null } private confidencePercent(): string { const c = this.dim.confidence return `${Math.round((c ?? 0) * 100)}%` } private isLowConfidence(): boolean { const c = this.dim.confidence return c !== undefined && c !== null && c < 0.7 } private ruleHits(): string[] { const r = this.dim.rule_hits return r ?? [] } private reasonText(): string { const ev = this.dim.evidence if (ev !== undefined && ev !== null && ev !== '') { return ev } return '本期未采集到足够信息' } /** 反馈区可用条件(T-P3-21):仅 status=ok 且有 key 的维度卡。 */ private feedbackable(): boolean { const k = this.dim.key return !this.isSkipped() && k !== undefined && k !== '' } private dimKey(): string { return this.dim.key ?? '' } private submitFeedback(verdict: string): void { if (verdict === 'inaccurate' && !this.correctionOpen) { // 点「不准」先展开行内修正输入(可选,≤100 字),再点「提交」才上报 this.correctionOpen = true return } this.correctionOpen = false this.onFeedback(this.dimKey(), verdict, verdict === 'inaccurate' ? this.correctionText.trim() : '') } build() { Column({ space: 8 }) { Row({ space: 8 }) { Text(this.titleText()) .fontSize(15) .fontWeight(FontWeight.Medium) .fontColor(this.isSkipped() ? '#9AA0A6' : '#333333') .layoutWeight(1) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) if (!this.isSkipped()) { Text(sourceLabel(this.dim.source)) .fontSize(10) .fontColor(sourceColor(this.dim.source)) .padding({ left: 6, right: 6, top: 2, bottom: 2 }) .backgroundColor('#F5F5F7') .borderRadius(4) if (this.hasConfidence()) { Text(this.confidencePercent()) .fontSize(12) .fontColor('#666666') } if (this.isLowConfidence()) { Text('仅供参考') .fontSize(10) .fontColor('#D46A0B') .padding({ left: 6, right: 6, top: 2, bottom: 2 }) .backgroundColor('#FDF3E3') .borderRadius(4) } } } .width('100%') if (this.isSkipped()) { Text(`未覆盖:${this.reasonText()}`) .fontSize(13) .fontColor('#9AA0A6') .width('100%') } else { Text(formatDimValue(this.dim.value)) .fontSize(14) .fontColor('#333333') .width('100%') } if (this.ruleHits().length > 0) { Text(this.ruleExpanded ? `收起依据(规则 ${this.ruleHits().length} 条)` : `依据规则 ${this.ruleHits().length} 条`) .fontSize(12) .fontColor(CARD_PRIMARY_COLOR) .onClick(() => { this.ruleExpanded = !this.ruleExpanded }) if (this.ruleExpanded) { Column({ space: 4 }) { ForEach(this.ruleHits(), (r: string) => { Text(`· ${r}`) .fontSize(12) .fontColor('#888888') .width('100%') }, (r: string) => r) } .width('100%') .alignItems(HorizontalAlign.Start) } } // 结论反馈区(T-P3-21):仅 ok 维度;已选项高亮,再点另一项即改判 if (this.feedbackable()) { Row({ space: 10 }) { Text('这个结论准吗?') .fontSize(12) .fontColor('#8A9099') .layoutWeight(1) Text('准 👍') .fontSize(12) .fontColor(this.feedbackVerdict === 'accurate' ? '#FFFFFF' : CARD_PRIMARY_COLOR) .padding({ left: 12, right: 12, top: 4, bottom: 4 }) .borderRadius(14) .backgroundColor(this.feedbackVerdict === 'accurate' ? CARD_PRIMARY_COLOR : '#F7E3EA') .onClick(() => { this.submitFeedback('accurate') }) Text('不准 👎') .fontSize(12) .fontColor(this.feedbackVerdict === 'inaccurate' ? '#FFFFFF' : '#666666') .padding({ left: 12, right: 12, top: 4, bottom: 4 }) .borderRadius(14) .backgroundColor(this.feedbackVerdict === 'inaccurate' ? '#666666' : '#F5F5F7') .onClick(() => { this.submitFeedback('inaccurate') }) } .width('100%') .alignItems(VerticalAlign.Center) .margin({ top: 4 }) // 「不准」的行内修正输入(可选,≤100 字) if (this.correctionOpen) { TextArea({ text: this.correctionText, placeholder: '你觉得应该是?(可选)' }) .width('100%') .height(72) .fontSize(13) .maxLength(100) .backgroundColor('#F5F5F7') .borderRadius(8) .onChange((v: string) => { this.correctionText = v }) Row() { Blank() Text('提交') .fontSize(13) .fontColor(Color.White) .padding({ left: 20, right: 20, top: 6, bottom: 6 }) .borderRadius(16) .backgroundColor(CARD_PRIMARY_COLOR) .onClick(() => { this.onFeedback(this.dimKey(), 'inaccurate', this.correctionText.trim()) this.correctionOpen = false }) } .width('100%') } } } .width('100%') .padding(12) .borderRadius(12) .backgroundColor(this.isSkipped() ? '#F7F7F8' : '#FFFFFF') .opacity(this.isSkipped() ? 0.85 : 1) } }