- 新增 core/Tracker.ets:埋点唯一入口,事件名正则校验,preferences 队列(≥20立即/ 30s节流/后台flush,每批50,上限200丢最旧),PII 键剔除,失败静默;首批10点位 - 新增 core/DailyLimiter.ets:试穿/试戴各3次/日本地软限制,自然日重置,失败不计数; setBonus 支持服务端 bonus_granted 加成,「做任务多换几次」真兑现 - 新增 components/f1/TaskBonusPanel.ets + core/TaskBonusState.ets:签到真实 claim 链路,40910/40310 处理,已领态面板与报告页互通;个人中心与对话耗尽态接入 - 报告页:维度「准/不准」反馈卡(DimensionCard 无网络走回调)+ 静默 claim report_feedback 闭环;全面报告预约留资卡(card_view/reserve_result 埋点) - 新增 components/f2/NetworkBanner.ets:首页/对话/个人中心三处网络错误统一横幅+重试; 对话耗尽横幅恢复 reset_at 时间显示 - 文案纪律:无价格/购买/会员/升级字样;hvigor assembleHap 构建 0 error - 子模块 meida_backend 指向 7bcbb8d(迭代1-3 后端)
225 lines
7.3 KiB
Plaintext
225 lines
7.3 KiB
Plaintext
/**
|
|
* 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)
|
|
}
|
|
}
|