skill-calorie 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/SKILL.md +54 -26
  2. package/dist/analysis/anomaly/common.d.ts.map +1 -1
  3. package/dist/analysis/anomaly/common.js +2 -1
  4. package/dist/analysis/anomaly/common.js.map +1 -1
  5. package/dist/analysis/cross.d.ts.map +1 -1
  6. package/dist/analysis/cross.js +3 -2
  7. package/dist/analysis/cross.js.map +1 -1
  8. package/dist/analysis/deficit.d.ts.map +1 -1
  9. package/dist/analysis/deficit.js +4 -0
  10. package/dist/analysis/deficit.js.map +1 -1
  11. package/dist/analysis/diet.js +2 -2
  12. package/dist/analysis/diet.js.map +1 -1
  13. package/dist/analysis/exercise.js +5 -5
  14. package/dist/analysis/exercise.js.map +1 -1
  15. package/dist/analysis/review.js +2 -2
  16. package/dist/analysis/review.js.map +1 -1
  17. package/dist/analysis/series.d.ts +7 -1
  18. package/dist/analysis/series.d.ts.map +1 -1
  19. package/dist/analysis/series.js +22 -6
  20. package/dist/analysis/series.js.map +1 -1
  21. package/dist/analysis/utils.d.ts +7 -0
  22. package/dist/analysis/utils.d.ts.map +1 -1
  23. package/dist/analysis/utils.js +7 -0
  24. package/dist/analysis/utils.js.map +1 -1
  25. package/dist/analysis/weightCompare3.js +2 -2
  26. package/dist/analysis/weightCompare3.js.map +1 -1
  27. package/dist/cli/cmd_read.d.ts +21 -3
  28. package/dist/cli/cmd_read.d.ts.map +1 -1
  29. package/dist/cli/cmd_read.js +460 -54
  30. package/dist/cli/cmd_read.js.map +1 -1
  31. package/dist/cli/keys.d.ts +89 -1
  32. package/dist/cli/keys.d.ts.map +1 -1
  33. package/dist/cli/keys.js +30 -0
  34. package/dist/cli/keys.js.map +1 -1
  35. package/dist/cli/write.d.ts +12 -9
  36. package/dist/cli/write.d.ts.map +1 -1
  37. package/dist/cli/write.js +133 -48
  38. package/dist/cli/write.js.map +1 -1
  39. package/dist/output.d.ts +55 -1
  40. package/dist/output.d.ts.map +1 -1
  41. package/dist/output.js +203 -5
  42. package/dist/output.js.map +1 -1
  43. package/dist/render/dietDocs.d.ts +45 -0
  44. package/dist/render/dietDocs.d.ts.map +1 -0
  45. package/dist/render/dietDocs.js +561 -0
  46. package/dist/render/dietDocs.js.map +1 -0
  47. package/dist/render/envelope.d.ts +32 -6
  48. package/dist/render/envelope.d.ts.map +1 -1
  49. package/dist/render/envelope.js +71 -0
  50. package/dist/render/envelope.js.map +1 -1
  51. package/dist/render/exercisePort.d.ts +161 -0
  52. package/dist/render/exercisePort.d.ts.map +1 -0
  53. package/dist/render/exercisePort.js +395 -0
  54. package/dist/render/exercisePort.js.map +1 -0
  55. package/dist/render/goal.d.ts.map +1 -1
  56. package/dist/render/goal.js +13 -2
  57. package/dist/render/goal.js.map +1 -1
  58. package/dist/render/goalPlate.d.ts +1 -1
  59. package/dist/render/goalPlate.d.ts.map +1 -1
  60. package/dist/render/goalPlate.js +5 -1
  61. package/dist/render/goalPlate.js.map +1 -1
  62. package/dist/render/helpCenter.d.ts +104 -0
  63. package/dist/render/helpCenter.d.ts.map +1 -0
  64. package/dist/render/helpCenter.js +386 -0
  65. package/dist/render/helpCenter.js.map +1 -0
  66. package/dist/render/html.d.ts.map +1 -1
  67. package/dist/render/html.js +2 -1
  68. package/dist/render/html.js.map +1 -1
  69. package/dist/render/index.d.ts +3 -0
  70. package/dist/render/index.d.ts.map +1 -1
  71. package/dist/render/index.js +2 -0
  72. package/dist/render/index.js.map +1 -1
  73. package/dist/render/nutritionPort.d.ts +89 -0
  74. package/dist/render/nutritionPort.d.ts.map +1 -0
  75. package/dist/render/nutritionPort.js +183 -0
  76. package/dist/render/nutritionPort.js.map +1 -0
  77. package/dist/render/nutritionPortDocs.d.ts +6 -0
  78. package/dist/render/nutritionPortDocs.d.ts.map +1 -0
  79. package/dist/render/nutritionPortDocs.js +317 -0
  80. package/dist/render/nutritionPortDocs.js.map +1 -0
  81. package/dist/render/photo.d.ts.map +1 -1
  82. package/dist/render/photo.js +5 -1
  83. package/dist/render/photo.js.map +1 -1
  84. package/dist/render/planPlate.d.ts +2 -1
  85. package/dist/render/planPlate.d.ts.map +1 -1
  86. package/dist/render/planPlate.js +6 -1
  87. package/dist/render/planPlate.js.map +1 -1
  88. package/dist/render/receipt.d.ts +47 -1
  89. package/dist/render/receipt.d.ts.map +1 -1
  90. package/dist/render/receipt.js +67 -3
  91. package/dist/render/receipt.js.map +1 -1
  92. package/dist/render/sportDocs.d.ts +14 -0
  93. package/dist/render/sportDocs.d.ts.map +1 -0
  94. package/dist/render/sportDocs.js +540 -0
  95. package/dist/render/sportDocs.js.map +1 -0
  96. package/dist/render/sportPortDocs.d.ts +8 -0
  97. package/dist/render/sportPortDocs.d.ts.map +1 -0
  98. package/dist/render/sportPortDocs.js +468 -0
  99. package/dist/render/sportPortDocs.js.map +1 -0
  100. package/dist/render/trendDocs.d.ts +11 -0
  101. package/dist/render/trendDocs.d.ts.map +1 -0
  102. package/dist/render/trendDocs.js +445 -0
  103. package/dist/render/trendDocs.js.map +1 -0
  104. package/dist/render/trendMiscPort.d.ts +127 -0
  105. package/dist/render/trendMiscPort.d.ts.map +1 -0
  106. package/dist/render/trendMiscPort.js +277 -0
  107. package/dist/render/trendMiscPort.js.map +1 -0
  108. package/dist/render/trendMiscPortDocs.d.ts +10 -0
  109. package/dist/render/trendMiscPortDocs.d.ts.map +1 -0
  110. package/dist/render/trendMiscPortDocs.js +397 -0
  111. package/dist/render/trendMiscPortDocs.js.map +1 -0
  112. package/dist/render/wizardPort.d.ts +125 -0
  113. package/dist/render/wizardPort.d.ts.map +1 -0
  114. package/dist/render/wizardPort.js +393 -0
  115. package/dist/render/wizardPort.js.map +1 -0
  116. package/dist/render/wizardPortDocs.d.ts +6 -0
  117. package/dist/render/wizardPortDocs.d.ts.map +1 -0
  118. package/dist/render/wizardPortDocs.js +274 -0
  119. package/dist/render/wizardPortDocs.js.map +1 -0
  120. package/dist/triggers/routing.d.ts +21 -21
  121. package/dist/triggers/routing.d.ts.map +1 -1
  122. package/dist/triggers/routing.js +60 -30
  123. package/dist/triggers/routing.js.map +1 -1
  124. package/package.json +2 -2
  125. package/templates/diet.html +2 -2
  126. package/templates/exercise.html +2 -2
  127. package/templates/goal.html +2 -2
  128. package/templates/help.html +2 -2
  129. package/templates/home.html +2 -2
  130. package/templates/photo-gallery.html +2 -2
@@ -0,0 +1,45 @@
1
+ /** 明细行最小形(fetch MealRow 的子集;调用方传全行亦可)。 */
2
+ export interface DietMealRow {
3
+ readonly date: string;
4
+ readonly time: string | null;
5
+ readonly food_name: string;
6
+ readonly grams: number;
7
+ readonly calories: number;
8
+ readonly protein: number;
9
+ readonly carbs: number;
10
+ readonly fat: number;
11
+ readonly note?: string | null;
12
+ }
13
+ import type { DaySeries } from '../analysis/series.js';
14
+ import type { FoodRanking, MacroRatio } from '../analysis/diet.js';
15
+ import type { DietOverview, MealDistribution } from './diet.js';
16
+ import type { DietReview } from './analysisPlate.js';
17
+ import type { AllRankings } from './ranking.js';
18
+ import type { HealthPlate } from './health.js';
19
+ import type { DedupeView } from './insightPlate.js';
20
+ import type { ProductLibrary, ProductSearch } from './library.js';
21
+ export interface TodayDietDocInput {
22
+ overview: DietOverview;
23
+ dist: MealDistribution;
24
+ meals: DietMealRow[];
25
+ macro: MacroRatio | null;
26
+ }
27
+ export declare function buildTodayDietDoc(input: TodayDietDocInput): string;
28
+ export interface ViewDietDocInput {
29
+ overview: DietOverview;
30
+ dist: MealDistribution;
31
+ distDate: string;
32
+ days: DaySeries[];
33
+ meals: DietMealRow[];
34
+ mealTotal: number;
35
+ mealsTruncated: boolean;
36
+ }
37
+ export declare function buildViewDietDoc(input: ViewDietDocInput): string;
38
+ export declare function buildDietReviewDoc(r: DietReview, top5: FoodRanking | null): string;
39
+ export declare function buildRankingDoc(r: FoodRanking): string;
40
+ export declare function buildAllRankingsDoc(a: AllRankings): string;
41
+ export declare function buildSearchDoc(s: ProductSearch): string;
42
+ export declare function buildLibraryDoc(lib: ProductLibrary, statsTotal: number): string;
43
+ export declare function buildHealthDoc(h: HealthPlate): string;
44
+ export declare function buildDedupeDoc(v: DedupeView): string;
45
+ //# sourceMappingURL=dietDocs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dietDocs.d.ts","sourceRoot":"","sources":["../../src/render/dietDocs.ts"],"names":[],"mappings":"AAgCA,0CAA0C;AAC1C,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AACD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACnE,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAChE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAuDlE,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,YAAY,CAAC;IACvB,IAAI,EAAE,gBAAgB,CAAC;IACvB,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;CAC1B;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,iBAAiB,GAAG,MAAM,CAiElE;AAID,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,YAAY,CAAC;IACvB,IAAI,EAAE,gBAAgB,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,SAAS,EAAE,CAAC;IAClB,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,OAAO,CAAC;CACzB;AAcD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,gBAAgB,GAAG,MAAM,CA+EhE;AAID,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,UAAU,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,GAAG,MAAM,CAiFlF;AAmBD,wBAAgB,eAAe,CAAC,CAAC,EAAE,WAAW,GAAG,MAAM,CAsCtD;AAED,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,WAAW,GAAG,MAAM,CAuC1D;AAeD,wBAAgB,cAAc,CAAC,CAAC,EAAE,aAAa,GAAG,MAAM,CAyBvD;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CA2B/E;AAWD,wBAAgB,cAAc,CAAC,CAAC,EAAE,WAAW,GAAG,MAAM,CA6DrD;AAID,wBAAgB,cAAc,CAAC,CAAC,EAAE,UAAU,GAAG,MAAM,CAwCpD"}
@@ -0,0 +1,561 @@
1
+ /** #108 · 营养/饮食域全文档装配(数据→区块→填充器)。
2
+ *
3
+ * 范围(47 页同质之饮食域,t71 口径「新版已有」):`calorie.today`/`calorie.view.diet`
4
+ * (today_diet/diet_overview/meal_distribution/today_meals 子集)/`calorie.view.diet-review`
5
+ * (diet_review)/`calorie.view.ranking`(food_ranking)/`calorie.view.search`(food_search)/
6
+ * `calorie.view.library`(food_library)/`calorie.view.health`(health_dashboard)/
7
+ * `calorie.view.dedupe`(dedupe_report)。
8
+ * 不碰:nutrition_ratio/nutrition_detail/source_stats/today_water(→ #112,需移植),
9
+ * 缺口/组合/异常/禁忌(→ #110),运动/体重/身体(→ #109)。
10
+ *
11
+ * 做法(#104 §4 用法):内容 = base-paint/blocks 12 区块(B-01 壳/B-02 KPI/B-03 表/
12
+ * B-04 图/B-05 列表/B-08 折叠/B-09 参数表单/B-11 复制区),文档 = fillTemplate 包裹
13
+ * (资产裸文本+填充器包裹;sharedCss = buildStyleSheet().css + blocksCss(),不走 extraCss;
14
+ * 图表页另加 CHARTS-HELPERS+buildChartsHelpersJs,图表 CSS 由其运行时注入)。
15
+ * 复制文本一律 buildDataText(#77 契约,技能侧不自产第二套序列化):指标页走 stat 投影,
16
+ * 行级页(榜单/食品/明细/去重)走 list 投影(JSON 行,旧「复制榜单/复制回 AI」的数据等价物)。
17
+ * 本层不做取数(数据由调用方 dispatch 备齐),不返空(缺失由数据层抛 missing-data)。
18
+ */
19
+ import { blocksCss, renderChartBlock, renderCopyBlock, renderDataTable, renderDisclosure, renderKpiGrid, renderListRows, renderPageShell, renderParamForm, } from 'base-paint/blocks';
20
+ import { buildChartsHelpersJs, buildDataText, buildSharedHelpersJs, buildStyleSheet, fillTemplate } from 'base-paint';
21
+ import { inferMealType } from '../fetch/diet.js';
22
+ /** envelope 头(值冻结对齐 cli/keys.ts ENVELOPE_VERSION/CALORIE_SKILL;测试钉死一致)。 */
23
+ const DOC_VERSION = '0.1.0';
24
+ const DOC_SKILL = 'calorie';
25
+ /** 内容页壳(裸标记+CONTENT 槽;包裹约定:资产裸文本+填充器包裹,标记不得预包裹)。
26
+ * wrap 带 ilife-page 兼容既有 --html 断言。 */
27
+ const DOC_SHELL = '<!doctype html>\n<html lang="zh-CN">\n<head>\n<meta charset="utf-8">\n' +
28
+ '<meta name="viewport" content="width=device-width,initial-scale=1">\n' +
29
+ '<title>卡路里·饮食</title>\n<!--SHARED-CSS-->\n</head>\n<body>\n' +
30
+ '<div class="wrap ilife-page">\n<!--CONTENT-->\n</div>\n<!--SHARED-HELPERS-->\n</body>\n</html>';
31
+ /** 图表页壳(多一个 CHARTS-HELPERS 标记,图表 CSS 由 charts helpers 运行时注入)。 */
32
+ const DOC_SHELL_CHARTS = '<!doctype html>\n<html lang="zh-CN">\n<head>\n<meta charset="utf-8">\n' +
33
+ '<meta name="viewport" content="width=device-width,initial-scale=1">\n' +
34
+ '<title>卡路里·饮食</title>\n<!--SHARED-CSS-->\n</head>\n<body>\n' +
35
+ '<div class="wrap ilife-page">\n<!--CONTENT-->\n</div>\n<!--SHARED-HELPERS-->\n<!--CHARTS-HELPERS-->\n</body>\n</html>';
36
+ function assemble(title, eyebrow, subtitle, content, charts) {
37
+ const assets = {
38
+ sharedCssText: buildStyleSheet().css + '\n' + blocksCss(),
39
+ sharedHelpersJs: buildSharedHelpersJs(),
40
+ };
41
+ if (charts)
42
+ assets.chartsHelpersJs = buildChartsHelpersJs();
43
+ const body = renderPageShell({
44
+ title,
45
+ ...(eyebrow ? { eyebrow } : {}),
46
+ ...(subtitle ? { subtitle } : {}),
47
+ content,
48
+ });
49
+ return fillTemplate({ template: charts ? DOC_SHELL_CHARTS : DOC_SHELL, assets, content: body }).html;
50
+ }
51
+ function copyBlock(title, input) {
52
+ return renderCopyBlock({ title, dataText: buildDataText(input) });
53
+ }
54
+ function fmt(n) {
55
+ if (n === null || n === undefined)
56
+ return '—';
57
+ return String(n);
58
+ }
59
+ function r1(n) {
60
+ return Math.round(n * 10) / 10;
61
+ }
62
+ const MEAL_NOTE = '窗口跟 MEAL_WINDOWS · 加餐=下午茶+夜宵';
63
+ export function buildTodayDietDoc(input) {
64
+ const { overview: o, dist, meals, macro } = input;
65
+ const names = new Set(meals.map((m) => m.food_name));
66
+ const kpis = renderKpiGrid([
67
+ { label: '当日摄入', value: String(o.totalCalories), unit: '卡', detail: '目标 ' + o.calorieGoal + ' 卡' },
68
+ { label: '餐数', value: String(meals.length), unit: '条', detail: names.size + ' 个品种' },
69
+ { label: '热量目标', value: String(o.calorieGoal), unit: '卡', detail: '剩余 ' + (o.calorieGoal - o.totalCalories) + ' 卡' },
70
+ { label: '餐别覆盖', value: dist.slices.filter((s) => s.count > 0).length + '/' + dist.slices.length, detail: MEAL_NOTE },
71
+ ]);
72
+ const parts = [kpis];
73
+ let charts = false;
74
+ if (dist.totalCalories > 0) {
75
+ parts.push(renderChartBlock({
76
+ kind: 'bar',
77
+ title: '餐别热量占比',
78
+ input: { items: dist.slices.map((s) => ({ label: s.meal, value: s.calories })) },
79
+ }));
80
+ charts = true;
81
+ }
82
+ if (macro && (macro.protein || macro.carb || macro.fat)) {
83
+ parts.push(renderChartBlock({
84
+ kind: 'donut',
85
+ title: '营养配比',
86
+ input: {
87
+ items: [
88
+ { label: '蛋白', value: macro.protein ? macro.protein.pct : 0 },
89
+ { label: '碳水', value: macro.carb ? macro.carb.pct : 0 },
90
+ { label: '脂肪', value: macro.fat ? macro.fat.pct : 0 },
91
+ ],
92
+ options: { showPercent: true },
93
+ },
94
+ }));
95
+ charts = true;
96
+ }
97
+ parts.push(renderDataTable({
98
+ columns: [
99
+ { key: 'time', label: '时间' },
100
+ { key: 'meal', label: '餐别' },
101
+ { key: 'food', label: '食物' },
102
+ { key: 'grams', label: '克数', align: 'right' },
103
+ { key: 'cal', label: '热量', align: 'right' },
104
+ { key: 'pro', label: '蛋白', align: 'right' },
105
+ { key: 'carbs', label: '碳水', align: 'right' },
106
+ { key: 'fat', label: '脂肪', align: 'right' },
107
+ ],
108
+ rows: meals.map((m) => ({
109
+ time: m.time ?? '', meal: inferMealType(String(m.time ?? '')), food: m.food_name,
110
+ grams: m.grams, cal: m.calories, pro: m.protein, carbs: m.carbs, fat: m.fat,
111
+ })),
112
+ caption: '今日明细(' + o.start + ',共 ' + meals.length + ' 条)',
113
+ emptyText: '本日无明细',
114
+ }));
115
+ parts.push(copyBlock('复制数据', {
116
+ envelope: {
117
+ version: DOC_VERSION, skill: DOC_SKILL, shape: 'list', key: 'calorie.today',
118
+ data: {
119
+ items: meals.map((m) => ({
120
+ date: m.date, time: m.time, food_name: m.food_name, grams: m.grams,
121
+ calories: m.calories, protein: m.protein, carbs: m.carbs, fat: m.fat,
122
+ })),
123
+ total: meals.length,
124
+ },
125
+ },
126
+ }));
127
+ return assemble('今日饮食 ' + o.start, 'calorie.today · 饮食域', MEAL_NOTE, parts.join(''), charts);
128
+ }
129
+ const MEAL_COLUMNS = [
130
+ { key: 'date', label: '日期' },
131
+ { key: 'time', label: '时间' },
132
+ { key: 'meal', label: '餐别' },
133
+ { key: 'food', label: '食物' },
134
+ { key: 'grams', label: '克数', align: 'right' },
135
+ { key: 'cal', label: '热量', align: 'right' },
136
+ { key: 'pro', label: '蛋白', align: 'right' },
137
+ { key: 'carbs', label: '碳水', align: 'right' },
138
+ { key: 'fat', label: '脂肪', align: 'right' },
139
+ ];
140
+ export function buildViewDietDoc(input) {
141
+ const { overview: o, dist, distDate, days, meals, mealTotal, mealsTruncated } = input;
142
+ const kpis = renderKpiGrid([
143
+ { label: '累计', value: String(o.totalCalories), unit: '卡', detail: o.loggedDays + '/' + o.days + '天有记录' },
144
+ { label: '日均', value: fmt(o.avgCalories), unit: '卡' },
145
+ { label: '目标', value: String(o.calorieGoal), unit: '卡' },
146
+ { label: '趋势', value: o.trend.summary.trend, detail: '均值 ' + o.trend.summary.avg + ' 卡' },
147
+ ]);
148
+ const parts = [kpis];
149
+ let charts = false;
150
+ const loggedDays = days.filter((d) => d.calories !== null);
151
+ if (loggedDays.length > 0) {
152
+ parts.push(renderChartBlock({
153
+ kind: 'line',
154
+ title: '每日摄入',
155
+ input: {
156
+ items: days.map((d) => ({ label: d.date.slice(5), value: d.calories })),
157
+ options: { markLine: { value: o.avgCalories ?? undefined, label: '日均' } },
158
+ },
159
+ }));
160
+ charts = true;
161
+ }
162
+ if (dist.totalCalories > 0) {
163
+ parts.push(renderChartBlock({
164
+ kind: 'bar',
165
+ title: '餐别分布 ' + distDate,
166
+ input: { items: dist.slices.map((s) => ({ label: s.meal + ' ' + s.count + '餐', value: s.calories })) },
167
+ }));
168
+ charts = true;
169
+ }
170
+ else {
171
+ parts.push(renderKpiGrid(dist.slices.map((s) => ({
172
+ label: '餐别分布 ' + s.meal, value: '0', unit: '卡', detail: distDate + ' 无记录(窗内有数,仅尾日回零)',
173
+ }))));
174
+ }
175
+ parts.push(renderDataTable({
176
+ columns: [
177
+ { key: 'date', label: '日期' },
178
+ { key: 'cal', label: '摄入', align: 'right' },
179
+ { key: 'pro', label: '蛋白', align: 'right' },
180
+ { key: 'carbs', label: '碳水', align: 'right' },
181
+ { key: 'fat', label: '脂肪', align: 'right' },
182
+ { key: 'goal', label: '目标', align: 'right' },
183
+ ],
184
+ rows: days.map((d) => ({
185
+ date: d.date, cal: d.calories, pro: d.protein, carbs: d.carbs, fat: d.fat, goal: d.calorieGoal,
186
+ })),
187
+ caption: '按日汇总(' + o.start + ' ~ ' + o.end + ',无记录日留空,不断 0)',
188
+ emptyText: '本窗无按日汇总',
189
+ }));
190
+ parts.push(renderDisclosure({
191
+ title: '窗口明细(共 ' + mealTotal + ' 条' + (mealsTruncated ? ',仅列前 ' + meals.length + ' 条' : '') + ')',
192
+ contentHtml: renderDataTable({
193
+ columns: [...MEAL_COLUMNS],
194
+ rows: meals.map((m) => ({
195
+ date: m.date, time: m.time ?? '', meal: inferMealType(String(m.time ?? '')), food: m.food_name,
196
+ grams: m.grams, cal: m.calories, pro: m.protein, carbs: m.carbs, fat: m.fat,
197
+ })),
198
+ caption: '窗口明细' + (mealsTruncated ? '(截断前 ' + meals.length + ' 条)' : ''),
199
+ emptyText: '本窗无明细',
200
+ }),
201
+ }));
202
+ parts.push(copyBlock('复制数据', {
203
+ envelope: {
204
+ version: DOC_VERSION, skill: DOC_SKILL, shape: 'stat', key: 'calorie.view.diet',
205
+ data: {
206
+ metrics: {
207
+ totalCalories: o.totalCalories, loggedDays: o.loggedDays, days: o.days,
208
+ calorieGoal: o.calorieGoal, distTotal: dist.totalCalories,
209
+ },
210
+ },
211
+ },
212
+ }));
213
+ return assemble('饮食总览 ' + o.start + ' ~ ' + o.end, 'calorie.view.diet · 饮食域', '餐别分布 ' + distDate + '(' + MEAL_NOTE + ')', parts.join(''), charts);
214
+ }
215
+ /* ── 饮食复盘(diet_review.html 对照:每日热量趋势+配比+高频 TOP+按餐汇总) ── */
216
+ export function buildDietReviewDoc(r, top5) {
217
+ const t = r.trend.status === 'ok' && r.trend.data ? r.trend.data : null;
218
+ const m = r.macro.status === 'ok' && r.macro.data ? r.macro.data : null;
219
+ const macroText = m && m.protein
220
+ ? '蛋白/碳水/脂肪 ' + m.protein.pct + '/' + (m.carb ? m.carb.pct : '—') + '/' + (m.fat ? m.fat.pct : '—')
221
+ : '—';
222
+ const parts = [renderKpiGrid([
223
+ {
224
+ label: '热量',
225
+ value: t ? String(t.totalCal) + ' 卡' : '—',
226
+ detail: t ? '日均 ' + t.avgCal + ' 卡 · ' + r.loggedDays + ' 天有记录' : r.loggedDays + ' 天有记录',
227
+ },
228
+ { label: '配比', value: macroText },
229
+ {
230
+ label: '达标天', value: t ? String(t.complianceDays) + '/' + t.daysCount : '—',
231
+ detail: t && t.calGoal ? '目标 ' + t.calGoal + ' 卡(±10%)' : '未设热量目标',
232
+ },
233
+ {
234
+ label: '周末/工作日', value: t ? t.weekendAvg + '/' + t.weekdayAvg : '—',
235
+ detail: '均值(卡)',
236
+ },
237
+ ])];
238
+ let charts = false;
239
+ if (t && t.daily.length > 0) {
240
+ parts.push(renderChartBlock({
241
+ kind: 'line',
242
+ title: '每日热量趋势',
243
+ input: {
244
+ items: t.daily.map((d) => ({ label: d.date.slice(5), value: d.totalCal })),
245
+ options: { avgLine: t.avgCal },
246
+ },
247
+ }));
248
+ charts = true;
249
+ }
250
+ if (m && (m.protein || m.carb || m.fat)) {
251
+ parts.push(renderChartBlock({
252
+ kind: 'donut',
253
+ title: '营养配比',
254
+ input: {
255
+ items: [
256
+ { label: '蛋白', value: m.protein ? m.protein.pct : 0 },
257
+ { label: '碳水', value: m.carb ? m.carb.pct : 0 },
258
+ { label: '脂肪', value: m.fat ? m.fat.pct : 0 },
259
+ ],
260
+ options: { showPercent: true },
261
+ },
262
+ }));
263
+ charts = true;
264
+ }
265
+ const top = top5 ? top5.items : [];
266
+ parts.push(renderDataTable({
267
+ columns: [
268
+ { key: 'rank', label: '排名', align: 'right' },
269
+ { key: 'food', label: '食物' },
270
+ { key: 'cal', label: '总热量', align: 'right' },
271
+ { key: 'cnt', label: '次数', align: 'right' },
272
+ { key: 'avg', label: '餐均', align: 'right' },
273
+ ],
274
+ rows: top.slice(0, 5).map((it) => ({
275
+ rank: it.rank, food: it.foodName, cal: it.totalCal, cnt: it.cnt, avg: it.avgCalPerMeal,
276
+ })),
277
+ caption: '高频食物 TOP5(' + r.start + ' ~ ' + r.end + ')',
278
+ emptyText: '本窗无高频食物',
279
+ }));
280
+ parts.push(renderDataTable({
281
+ columns: [
282
+ { key: 'meal', label: '餐别' },
283
+ { key: 'days', label: '天数', align: 'right' },
284
+ { key: 'cal', label: '累计热量', align: 'right' },
285
+ ],
286
+ rows: r.byMeal.map((s) => ({ meal: s.meal, days: s.days, cal: s.totalCalories })),
287
+ caption: '按餐汇总(' + MEAL_NOTE + ')',
288
+ emptyText: '本窗无按餐汇总',
289
+ }));
290
+ parts.push(copyBlock('复制数据', {
291
+ envelope: {
292
+ version: DOC_VERSION, skill: DOC_SKILL, shape: 'stat', key: 'calorie.view.diet-review',
293
+ data: { metrics: { loggedDays: r.loggedDays } },
294
+ },
295
+ }));
296
+ return assemble('饮食复盘 ' + r.start + ' ~ ' + r.end, 'calorie.view.diet-review · 饮食域', null, parts.join(''), charts);
297
+ }
298
+ /* ── 食品排行(food_ranking.html 对照:榜单表+复制榜单;tab 交互归宿主,静态页逐榜/全榜直出) ── */
299
+ const RANK_ZH = {
300
+ high_calorie: '高热量', low_calorie: '低热量', frequent: '常吃', high_carb: '高碳水', high_protein: '高蛋白',
301
+ };
302
+ const RANK_COLUMNS = [
303
+ { key: 'rank', label: '排名', align: 'right' },
304
+ { key: 'food', label: '食物' },
305
+ { key: 'cal', label: '总热量', align: 'right' },
306
+ { key: 'cnt', label: '次数', align: 'right' },
307
+ { key: 'avg', label: '餐均', align: 'right' },
308
+ { key: 'pro', label: '蛋白', align: 'right' },
309
+ { key: 'carbs', label: '碳水', align: 'right' },
310
+ { key: 'fat', label: '脂肪', align: 'right' },
311
+ ];
312
+ export function buildRankingDoc(r) {
313
+ const top = r.items[0];
314
+ const parts = [renderKpiGrid([
315
+ { label: '榜单', value: RANK_ZH[r.category] ?? r.category, detail: r.start + ' ~ ' + r.end },
316
+ { label: '上榜', value: String(r.items.length), unit: '种', detail: 'TOP ' + r.topN },
317
+ {
318
+ label: '头名', value: top ? top.foodName : '—',
319
+ detail: top ? top.totalCal + ' 卡 · ' + top.cnt + ' 次 · 均 ' + top.avgCalPerMeal + ' 卡/餐' : '',
320
+ },
321
+ ])];
322
+ parts.push(renderDataTable({
323
+ columns: [...RANK_COLUMNS],
324
+ rows: r.items.map((it) => ({
325
+ rank: it.rank, food: it.foodName, cal: it.totalCal, cnt: it.cnt,
326
+ avg: it.avgCalPerMeal, pro: it.totalProtein, carbs: it.totalCarbs, fat: it.totalFat,
327
+ })),
328
+ caption: r.title,
329
+ emptyText: '本窗无排行数据',
330
+ }));
331
+ parts.push(copyBlock('复制榜单', {
332
+ envelope: {
333
+ version: DOC_VERSION, skill: DOC_SKILL, shape: 'list', key: 'calorie.view.ranking',
334
+ data: {
335
+ items: r.items.map((it) => ({
336
+ rank: it.rank, foodName: it.foodName, totalCal: it.totalCal,
337
+ cnt: it.cnt, avgCalPerMeal: it.avgCalPerMeal,
338
+ })),
339
+ total: r.items.length,
340
+ },
341
+ },
342
+ }));
343
+ return assemble('排行 ' + (RANK_ZH[r.category] ?? r.category) + ' ' + r.start + ' ~ ' + r.end, 'calorie.view.ranking · 饮食域', 'tab 切换归宿主:单榜直出(' + r.title + ')', parts.join(''), false);
344
+ }
345
+ export function buildAllRankingsDoc(a) {
346
+ const cats = Object.keys(a.boards);
347
+ const parts = [renderKpiGrid(cats.map((c) => {
348
+ const b = a.boards[c];
349
+ if (!b || !b.items[0])
350
+ return { label: RANK_ZH[c] ?? c, value: '—', detail: '本窗无数据' };
351
+ const top = b.items[0];
352
+ return {
353
+ label: RANK_ZH[c] ?? c, value: top.foodName + ' ' + top.totalCal + ' 卡', detail: 'TOP ' + b.topN,
354
+ };
355
+ }))];
356
+ for (const c of cats) {
357
+ const b = a.boards[c];
358
+ parts.push(renderDisclosure({
359
+ title: (RANK_ZH[c] ?? c) + '榜' + (b ? '(TOP ' + b.topN + ')' : '(本窗无数据)'),
360
+ open: b !== null && cats.indexOf(c) === cats.findIndex((k) => a.boards[k] !== null),
361
+ contentHtml: renderDataTable({
362
+ columns: [...RANK_COLUMNS],
363
+ rows: (b ? b.items : []).map((it) => ({
364
+ rank: it.rank, food: it.foodName, cal: it.totalCal, cnt: it.cnt,
365
+ avg: it.avgCalPerMeal, pro: it.totalProtein, carbs: it.totalCarbs, fat: it.totalFat,
366
+ })),
367
+ caption: b ? b.title : '本窗无数据',
368
+ emptyText: '本窗无数据',
369
+ }),
370
+ }));
371
+ }
372
+ parts.push(copyBlock('复制数据', {
373
+ envelope: {
374
+ version: DOC_VERSION, skill: DOC_SKILL, shape: 'stat', key: 'calorie.view.ranking',
375
+ data: { metrics: { okCount: a.okCount, topN: a.topN } },
376
+ },
377
+ }));
378
+ return assemble('全部排行 ' + a.start + ' ~ ' + a.end, 'calorie.view.ranking · 饮食域', a.start + ' ~ ' + a.end + ' · ' + a.okCount + '/5 榜有数据', parts.join(''), false);
379
+ }
380
+ /* ── 查食品/食品库(food_search/food_library 对照:参数表单+结果表+复制) ── */
381
+ const PRODUCT_COLUMNS = [
382
+ { key: 'name', label: '食品名' },
383
+ { key: 'brand', label: '品牌' },
384
+ { key: 'cal', label: '热量', align: 'right' },
385
+ { key: 'pro', label: '蛋白', align: 'right' },
386
+ { key: 'fat', label: '脂肪', align: 'right' },
387
+ { key: 'carbs', label: '碳水', align: 'right' },
388
+ { key: 'sodium', label: '钠', align: 'right' },
389
+ { key: 'source', label: '来源' },
390
+ ];
391
+ export function buildSearchDoc(s) {
392
+ const rows = s.items.map((p) => ({
393
+ name: p.product_name, brand: p.brand ?? '', cal: p.calories, pro: p.protein,
394
+ fat: p.fat, carbs: p.carbohydrates, sodium: p.sodium, source: p.source ?? '',
395
+ }));
396
+ const parts = [
397
+ renderParamForm({
398
+ fields: [{ name: 'keyword', label: '关键词', value: s.keyword }],
399
+ description: '食品库按名称/品牌模糊查找(营养值为每 100g;实时搜索归宿主)',
400
+ }),
401
+ renderKpiGrid([{ label: '关键词', value: s.keyword, detail: '命中 ' + s.total + ' 条' }]),
402
+ renderDataTable({
403
+ columns: [...PRODUCT_COLUMNS],
404
+ rows,
405
+ caption: '查食品 ' + s.keyword + '(共 ' + s.total + ' 条)',
406
+ emptyText: '无命中',
407
+ }),
408
+ copyBlock('复制数据', {
409
+ envelope: {
410
+ version: DOC_VERSION, skill: DOC_SKILL, shape: 'list', key: 'calorie.view.search',
411
+ data: { items: rows, total: s.total },
412
+ },
413
+ }),
414
+ ];
415
+ return assemble('查食品 ' + s.keyword, 'calorie.view.search · 饮食域', null, parts.join(''), false);
416
+ }
417
+ export function buildLibraryDoc(lib, statsTotal) {
418
+ const rows = lib.items.map((p) => ({
419
+ name: p.product_name, brand: p.brand ?? '', cal: p.calories, pro: p.protein,
420
+ fat: p.fat, carbs: p.carbohydrates, sodium: p.sodium, source: p.source ?? '',
421
+ }));
422
+ const parts = [
423
+ renderParamForm({
424
+ fields: [{ name: 'category', label: '分类', value: lib.category ?? '' }],
425
+ description: '按分类浏览食品库(空=全量;分页与实时筛选归宿主)',
426
+ }),
427
+ renderKpiGrid([
428
+ { label: '食品库', value: lib.category ?? '全量', detail: '本页 ' + lib.total + ' 条 / 库 ' + statsTotal + ' 条' },
429
+ ]),
430
+ renderDataTable({
431
+ columns: [...PRODUCT_COLUMNS],
432
+ rows,
433
+ caption: '食品库' + (lib.category ? '(' + lib.category + ')' : '(全量)') + '(共 ' + lib.total + ' 条)',
434
+ emptyText: '该分类空库',
435
+ }),
436
+ copyBlock('复制数据', {
437
+ envelope: {
438
+ version: DOC_VERSION, skill: DOC_SKILL, shape: 'list', key: 'calorie.view.library',
439
+ data: { items: rows, total: lib.total },
440
+ },
441
+ }),
442
+ ];
443
+ return assemble('食品库' + (lib.category ? '(' + lib.category + ')' : ''), 'calorie.view.library · 饮食域', null, parts.join(''), false);
444
+ }
445
+ /* ── 健康盘(health_dashboard.html 对照:四维+今日该做什么+复制回 AI) ── */
446
+ const HEALTH_DIMS = [
447
+ { key: 'calorie', name: '摄入' },
448
+ { key: 'exercise', name: '运动' },
449
+ { key: 'weight', name: '体重' },
450
+ { key: 'deficit', name: '缺口' },
451
+ ];
452
+ export function buildHealthDoc(h) {
453
+ const dims = (h.dashboard.data ?? {});
454
+ const missing = HEALTH_DIMS.filter((d) => dims[d.key] === null || dims[d.key] === undefined).map((d) => d.name);
455
+ const parts = [renderKpiGrid([
456
+ { label: '区间', value: h.start + ' ~ ' + h.end, detail: '有记录 ' + h.loggedDays + ' 天' },
457
+ { label: '日均摄入', value: fmt(h.avgIntake), unit: '卡' },
458
+ { label: '日均缺口', value: fmt(h.avgDeficit), unit: '卡' },
459
+ {
460
+ label: '四维', value: h.dashboard.status === 'ok' ? '齐' : '部分缺',
461
+ detail: missing.length === 0 ? '体重维有' : '缺:' + missing.join('、'),
462
+ status: missing.length === 0 ? 'ok' : 'warn',
463
+ },
464
+ ])];
465
+ parts.push(renderListRows({
466
+ items: HEALTH_DIMS.map((d) => {
467
+ const v = dims[d.key];
468
+ const has = v !== null && v !== undefined;
469
+ return {
470
+ left: d.name,
471
+ main: has ? '有数据' : '缺数据(先补记录)',
472
+ right: has ? '✓' : '缺',
473
+ };
474
+ }),
475
+ }));
476
+ const logged = h.series.filter((d) => d.calories !== null);
477
+ const goalAvg = logged.length > 0
478
+ ? logged.reduce((a, d) => a + d.calorieGoal, 0) / logged.length
479
+ : null;
480
+ const actions = [];
481
+ if (h.avgIntake !== null && goalAvg) {
482
+ const ratio = h.avgIntake / goalAvg;
483
+ if (ratio > 1.1) {
484
+ actions.push({ left: '!', main: '摄入超标', right: '日均 ' + h.avgIntake + ' 卡 vs 目标 ' + r1(goalAvg) + ' 卡' });
485
+ }
486
+ else if (ratio < 0.7) {
487
+ actions.push({ left: '!', main: '摄入不足', right: '日均仅 ' + h.avgIntake + ' 卡,长期可能影响代谢' });
488
+ }
489
+ }
490
+ if (h.avgDeficit !== null) {
491
+ if (h.avgDeficit > 700)
492
+ actions.push({ left: '🔥', main: '缺口过大', right: '日均缺口 ' + h.avgDeficit + ' 卡,超过 700 不健康' });
493
+ else if (h.avgDeficit < 0)
494
+ actions.push({ left: '🍔', main: '热量盈余', right: '日均 ' + Math.abs(h.avgDeficit) + ' 卡盈余,可能在增重' });
495
+ }
496
+ if (missing.length > 0)
497
+ actions.push({ left: '补', main: '部分维度缺数据', right: '缺:' + missing.join('、') });
498
+ if (actions.length === 0)
499
+ actions.push({ left: '✓', main: '一切正常', right: '指标均在合理范围内' });
500
+ parts.push(renderDisclosure({
501
+ title: '今日该做什么(共 ' + actions.length + ' 条)',
502
+ open: true,
503
+ contentHtml: renderListRows({ items: actions }),
504
+ }));
505
+ parts.push(copyBlock('复制回 AI', {
506
+ envelope: {
507
+ version: DOC_VERSION, skill: DOC_SKILL, shape: 'stat', key: 'calorie.view.health',
508
+ data: {
509
+ metrics: {
510
+ loggedDays: h.loggedDays,
511
+ ...(h.avgIntake !== null ? { avgIntake: h.avgIntake } : {}),
512
+ ...(h.avgDeficit !== null ? { avgDeficit: h.avgDeficit } : {}),
513
+ },
514
+ },
515
+ },
516
+ }));
517
+ return assemble('健康盘 ' + h.start + ' ~ ' + h.end, 'calorie.view.health · 饮食域', h.dashboard.message, parts.join(''), false);
518
+ }
519
+ /* ── 去重报告(dedupe_report.html 对照:KPI+重复组表+处理建议) ── */
520
+ export function buildDedupeDoc(v) {
521
+ const parts = [renderKpiGrid([
522
+ { label: '重复组', value: String(v.groupCount), unit: '组' },
523
+ { label: '冗余条', value: String(v.rowCount), unit: '条' },
524
+ { label: '库内食品', value: String(v.totalProducts), unit: '条' },
525
+ ])];
526
+ parts.push(renderDataTable({
527
+ columns: [
528
+ { key: 'name', label: '食品名' },
529
+ { key: 'brand', label: '品牌' },
530
+ { key: 'count', label: '条数', align: 'right' },
531
+ { key: 'ids', label: 'ID' },
532
+ ],
533
+ rows: v.groups.slice(0, 50).map((g) => ({
534
+ name: g.productName, brand: g.brand ?? '', count: g.ids.length, ids: g.ids.join(','),
535
+ })),
536
+ caption: '重复组列表(共 ' + v.groupCount + ' 组' + (v.groups.length > 50 ? ',仅列前 50 组' : '') + ')',
537
+ emptyText: '无重复组(库内 ' + v.totalProducts + ' 条均唯一)',
538
+ }));
539
+ parts.push(renderDisclosure({
540
+ title: '处理建议',
541
+ contentHtml: renderListRows({
542
+ items: v.groupCount === 0
543
+ ? [{ main: '库内无重复,无需处理' }]
544
+ : [
545
+ { main: '每组合并保留一条(取数据最全者)', right: '共 ' + v.groupCount + ' 组' },
546
+ { main: '其余条目下架(deprecate),查询/搜索/导入去重不再出现' },
547
+ ],
548
+ }),
549
+ }));
550
+ parts.push(copyBlock('复制数据', {
551
+ envelope: {
552
+ version: DOC_VERSION, skill: DOC_SKILL, shape: 'list', key: 'calorie.view.dedupe',
553
+ data: {
554
+ items: v.groups.map((g) => ({ productName: g.productName, brand: g.brand, ids: g.ids })),
555
+ total: v.groupCount,
556
+ },
557
+ },
558
+ }));
559
+ return assemble('去重报告', 'calorie.view.dedupe · 饮食域', '数据来源:nutrition_products', parts.join(''), false);
560
+ }
561
+ //# sourceMappingURL=dietDocs.js.map