@eeymoo/hum 0.1.15 → 0.1.17
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.
- package/bin/index.js +2 -0
- package/package.json +1 -1
- package/src/commands/diet.js +4 -0
- package/src/commands/exercise.js +4 -0
- package/src/commands/food.js +29 -0
- package/src/commands/sleep.js +19 -2
- package/src/commands/weight.js +4 -0
- package/src/lib/api.js +18 -3
- package/src/lib/foodSearch.js +174 -0
- package/src/lib/output.js +14 -1
package/bin/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import timeline from '../src/commands/timeline.js'
|
|
|
8
8
|
import weight from '../src/commands/weight.js'
|
|
9
9
|
import exercise from '../src/commands/exercise.js'
|
|
10
10
|
import diet from '../src/commands/diet.js'
|
|
11
|
+
import food from '../src/commands/food.js'
|
|
11
12
|
import sleep from '../src/commands/sleep.js'
|
|
12
13
|
import { checkVersion, getCliVersion } from '../src/lib/version-check.js'
|
|
13
14
|
|
|
@@ -32,6 +33,7 @@ program.addCommand(timeline)
|
|
|
32
33
|
program.addCommand(weight)
|
|
33
34
|
program.addCommand(exercise)
|
|
34
35
|
program.addCommand(diet)
|
|
36
|
+
program.addCommand(food)
|
|
35
37
|
program.addCommand(sleep)
|
|
36
38
|
|
|
37
39
|
program.parse()
|
package/package.json
CHANGED
package/src/commands/diet.js
CHANGED
|
@@ -16,6 +16,7 @@ diet
|
|
|
16
16
|
.option('--sodium <value>', 'Sodium (mg)')
|
|
17
17
|
.option('--foods <string>', 'Foods in format: "name:amount,name2:amount2"')
|
|
18
18
|
.option('--water <value>', 'Water (ml)')
|
|
19
|
+
.option('--extra-data <json>', 'Extra data (JSON string)')
|
|
19
20
|
.option('--note <note>', 'Note')
|
|
20
21
|
.option('--date <date>', 'Date (YYYY-MM-DD or ISO 8601 datetime)')
|
|
21
22
|
.option('--file <paths...>', 'File paths to attach')
|
|
@@ -31,6 +32,7 @@ diet
|
|
|
31
32
|
sodium: options.sodium,
|
|
32
33
|
foods: options.foods,
|
|
33
34
|
water: options.water,
|
|
35
|
+
extraData: options.extraData,
|
|
34
36
|
note: options.note,
|
|
35
37
|
date: appendTimezoneOffset(options.date)
|
|
36
38
|
}, options.file || [])
|
|
@@ -108,6 +110,7 @@ diet
|
|
|
108
110
|
.option('--sodium <value>', 'Updated sodium')
|
|
109
111
|
.option('--foods <string>', 'Updated foods')
|
|
110
112
|
.option('--water <value>', 'Updated water')
|
|
113
|
+
.option('--extra-data <json>', 'Updated extra data (JSON string)')
|
|
111
114
|
.option('--note <note>', 'Updated note')
|
|
112
115
|
.option('--date <date>', 'Updated date (YYYY-MM-DD or ISO 8601 datetime)')
|
|
113
116
|
.option('--file <paths...>', 'File paths to attach')
|
|
@@ -124,6 +127,7 @@ diet
|
|
|
124
127
|
sodium: options.sodium,
|
|
125
128
|
foods: options.foods,
|
|
126
129
|
water: options.water,
|
|
130
|
+
extraData: options.extraData,
|
|
127
131
|
note: options.note,
|
|
128
132
|
date: appendTimezoneOffset(options.date),
|
|
129
133
|
replaceAttachments: options.replaceAttachments ? 'true' : undefined
|
package/src/commands/exercise.js
CHANGED
|
@@ -14,6 +14,7 @@ exercise
|
|
|
14
14
|
.option('--heart-rate-avg <value>', 'Average heart rate')
|
|
15
15
|
.option('--heart-rate-max <value>', 'Max heart rate')
|
|
16
16
|
.option('--feeling <value>', 'Feeling 1-10')
|
|
17
|
+
.option('--extra-data <json>', 'Extra data (JSON string)')
|
|
17
18
|
.option('--location <location>', 'Location')
|
|
18
19
|
.option('--note <note>', 'Note')
|
|
19
20
|
.option('--date <date>', 'Date (YYYY-MM-DD or ISO 8601 datetime)')
|
|
@@ -28,6 +29,7 @@ exercise
|
|
|
28
29
|
heartRateAvg: options.heartRateAvg,
|
|
29
30
|
heartRateMax: options.heartRateMax,
|
|
30
31
|
feeling: options.feeling,
|
|
32
|
+
extraData: options.extraData,
|
|
31
33
|
location: options.location,
|
|
32
34
|
note: options.note,
|
|
33
35
|
date: appendTimezoneOffset(options.date)
|
|
@@ -104,6 +106,7 @@ exercise
|
|
|
104
106
|
.option('--heart-rate-avg <value>', 'Updated average heart rate')
|
|
105
107
|
.option('--heart-rate-max <value>', 'Updated max heart rate')
|
|
106
108
|
.option('--feeling <value>', 'Updated feeling')
|
|
109
|
+
.option('--extra-data <json>', 'Updated extra data (JSON string)')
|
|
107
110
|
.option('--location <location>', 'Updated location')
|
|
108
111
|
.option('--note <note>', 'Updated note')
|
|
109
112
|
.option('--date <date>', 'Updated date (YYYY-MM-DD or ISO 8601 datetime)')
|
|
@@ -119,6 +122,7 @@ exercise
|
|
|
119
122
|
heartRateAvg: options.heartRateAvg,
|
|
120
123
|
heartRateMax: options.heartRateMax,
|
|
121
124
|
feeling: options.feeling,
|
|
125
|
+
extraData: options.extraData,
|
|
122
126
|
location: options.location,
|
|
123
127
|
note: options.note,
|
|
124
128
|
date: appendTimezoneOffset(options.date),
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { searchFood } from '../lib/foodSearch.js'
|
|
3
|
+
import { outputData } from '../lib/output.js'
|
|
4
|
+
|
|
5
|
+
const food = new Command('food')
|
|
6
|
+
|
|
7
|
+
food
|
|
8
|
+
.description('查询食物营养信息')
|
|
9
|
+
.requiredOption('-n, --name <name>', '食物名称(支持模糊匹配)')
|
|
10
|
+
.option('-l, --limit <number>', '返回结果数量上限', '5')
|
|
11
|
+
.option('--no-cache', '跳过本地缓存,重新请求远端')
|
|
12
|
+
.option('--format <format>', '输出格式: json, table, toon', 'json')
|
|
13
|
+
.action(async (options) => {
|
|
14
|
+
try {
|
|
15
|
+
const limit = parseInt(options.limit, 10) || 5
|
|
16
|
+
const { items, degraded } = await searchFood(options.name, {
|
|
17
|
+
limit,
|
|
18
|
+
noCache: options.cache === false
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const cleaned = items.map(({ rawItem, ...rest }) => rest)
|
|
22
|
+
outputData({ foods: cleaned, totalPages: 1, total: cleaned.length }, { format: options.format, type: 'food-list' })
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error(error.message)
|
|
25
|
+
process.exitCode = 1
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
export default food
|
package/src/commands/sleep.js
CHANGED
|
@@ -7,7 +7,7 @@ const sleep = new Command('sleep')
|
|
|
7
7
|
|
|
8
8
|
sleep
|
|
9
9
|
.command('add')
|
|
10
|
-
.
|
|
10
|
+
.option('--duration <value>', 'Sleep duration in hours')
|
|
11
11
|
.requiredOption('--bedtime <time>', 'Bedtime (HH:mm)')
|
|
12
12
|
.requiredOption('--waketime <time>', 'Wake time (HH:mm)')
|
|
13
13
|
.requiredOption('--quality <value>', 'Sleep quality 1-10')
|
|
@@ -15,13 +15,27 @@ sleep
|
|
|
15
15
|
.option('--rem-sleep <value>', 'REM sleep duration in hours')
|
|
16
16
|
.option('--awakenings <value>', 'Number of awakenings')
|
|
17
17
|
.option('--feeling <value>', 'Feeling 1-10')
|
|
18
|
+
.option('--extra-data <json>', 'Extra data (JSON string)')
|
|
18
19
|
.option('--note <note>', 'Note')
|
|
19
20
|
.option('--date <date>', 'Date (YYYY-MM-DD or ISO 8601 datetime)')
|
|
20
21
|
.option('--file <paths...>', 'File paths to attach')
|
|
21
22
|
.action(async (options) => {
|
|
22
23
|
try {
|
|
24
|
+
let duration = options.duration
|
|
25
|
+
if (!duration && options.bedtime && options.waketime) {
|
|
26
|
+
const [bh, bm] = options.bedtime.split(':').map(Number)
|
|
27
|
+
const [wh, wm] = options.waketime.split(':').map(Number)
|
|
28
|
+
let diff = (wh * 60 + wm) - (bh * 60 + bm)
|
|
29
|
+
if (diff < 0) diff += 24 * 60
|
|
30
|
+
duration = (diff / 60).toFixed(1)
|
|
31
|
+
}
|
|
32
|
+
if (!duration) {
|
|
33
|
+
console.error('需要 --duration 或同时提供 --bedtime 和 --waketime')
|
|
34
|
+
process.exit(1)
|
|
35
|
+
}
|
|
36
|
+
|
|
23
37
|
const formData = createFormData({
|
|
24
|
-
duration
|
|
38
|
+
duration,
|
|
25
39
|
bedTime: options.bedtime,
|
|
26
40
|
wakeTime: options.waketime,
|
|
27
41
|
quality: options.quality,
|
|
@@ -29,6 +43,7 @@ sleep
|
|
|
29
43
|
remSleep: options.remSleep,
|
|
30
44
|
awakenings: options.awakenings,
|
|
31
45
|
feeling: options.feeling,
|
|
46
|
+
extraData: options.extraData,
|
|
32
47
|
note: options.note,
|
|
33
48
|
date: appendTimezoneOffset(options.date)
|
|
34
49
|
}, options.file || [])
|
|
@@ -104,6 +119,7 @@ sleep
|
|
|
104
119
|
.option('--rem-sleep <value>', 'Updated REM sleep')
|
|
105
120
|
.option('--awakenings <value>', 'Updated number of awakenings')
|
|
106
121
|
.option('--feeling <value>', 'Updated feeling')
|
|
122
|
+
.option('--extra-data <json>', 'Updated extra data (JSON string)')
|
|
107
123
|
.option('--note <note>', 'Updated note')
|
|
108
124
|
.option('--date <date>', 'Updated date (YYYY-MM-DD or ISO 8601 datetime)')
|
|
109
125
|
.option('--file <paths...>', 'File paths to attach')
|
|
@@ -119,6 +135,7 @@ sleep
|
|
|
119
135
|
remSleep: options.remSleep,
|
|
120
136
|
awakenings: options.awakenings,
|
|
121
137
|
feeling: options.feeling,
|
|
138
|
+
extraData: options.extraData,
|
|
122
139
|
note: options.note,
|
|
123
140
|
date: appendTimezoneOffset(options.date),
|
|
124
141
|
replaceAttachments: options.replaceAttachments ? 'true' : undefined
|
package/src/commands/weight.js
CHANGED
|
@@ -14,6 +14,7 @@ weight
|
|
|
14
14
|
.option('--water <value>', 'Water percentage')
|
|
15
15
|
.option('--bone-mass <value>', 'Bone mass (kg)')
|
|
16
16
|
.option('--visceral-fat <value>', 'Visceral fat level')
|
|
17
|
+
.option('--extra-data <json>', 'Extra data (JSON string)')
|
|
17
18
|
.option('--note <note>', 'Note for the record')
|
|
18
19
|
.option('--date <date>', 'Date (YYYY-MM-DD or ISO 8601 datetime)')
|
|
19
20
|
.option('--file <paths...>', 'File paths to attach')
|
|
@@ -27,6 +28,7 @@ weight
|
|
|
27
28
|
water: options.water,
|
|
28
29
|
boneMass: options.boneMass,
|
|
29
30
|
visceralFat: options.visceralFat,
|
|
31
|
+
extraData: options.extraData,
|
|
30
32
|
note: options.note,
|
|
31
33
|
date: appendTimezoneOffset(options.date)
|
|
32
34
|
}, options.file || [])
|
|
@@ -101,6 +103,7 @@ weight
|
|
|
101
103
|
.option('--water <value>', 'Updated water percentage')
|
|
102
104
|
.option('--bone-mass <value>', 'Updated bone mass (kg)')
|
|
103
105
|
.option('--visceral-fat <value>', 'Updated visceral fat level')
|
|
106
|
+
.option('--extra-data <json>', 'Updated extra data (JSON string)')
|
|
104
107
|
.option('--note <note>', 'Updated note')
|
|
105
108
|
.option('--date <date>', 'Updated date (YYYY-MM-DD or ISO 8601 datetime)')
|
|
106
109
|
.option('--file <paths...>', 'File paths to attach')
|
|
@@ -115,6 +118,7 @@ weight
|
|
|
115
118
|
water: options.water,
|
|
116
119
|
boneMass: options.boneMass,
|
|
117
120
|
visceralFat: options.visceralFat,
|
|
121
|
+
extraData: options.extraData,
|
|
118
122
|
note: options.note,
|
|
119
123
|
date: appendTimezoneOffset(options.date),
|
|
120
124
|
replaceAttachments: options.replaceAttachments ? 'true' : undefined
|
package/src/lib/api.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import fetch from 'node-fetch'
|
|
2
|
-
import { FormData, File } from 'node-fetch'
|
|
3
1
|
import { readFileSync } from 'fs'
|
|
4
2
|
import config from './config.js'
|
|
5
3
|
|
|
@@ -42,10 +40,27 @@ export function createFormData(fields, files = []) {
|
|
|
42
40
|
}
|
|
43
41
|
})
|
|
44
42
|
|
|
43
|
+
const mimeMap = {
|
|
44
|
+
'.png': 'image/png',
|
|
45
|
+
'.jpg': 'image/jpeg',
|
|
46
|
+
'.jpeg': 'image/jpeg',
|
|
47
|
+
'.gif': 'image/gif',
|
|
48
|
+
'.webp': 'image/webp',
|
|
49
|
+
'.bmp': 'image/bmp',
|
|
50
|
+
'.heic': 'image/heic',
|
|
51
|
+
'.heif': 'image/heif',
|
|
52
|
+
'.pdf': 'application/pdf',
|
|
53
|
+
'.txt': 'text/plain',
|
|
54
|
+
'.gpx': 'application/gpx+xml',
|
|
55
|
+
'.fit': 'application/fit'
|
|
56
|
+
}
|
|
57
|
+
|
|
45
58
|
files.forEach(filePath => {
|
|
46
59
|
const buffer = readFileSync(filePath)
|
|
47
60
|
const fileName = filePath.split('/').pop()
|
|
48
|
-
const
|
|
61
|
+
const ext = '.' + fileName.split('.').pop().toLowerCase()
|
|
62
|
+
const type = mimeMap[ext] || 'application/octet-stream'
|
|
63
|
+
const file = new File([buffer], fileName, { type })
|
|
49
64
|
formData.append('file', file)
|
|
50
65
|
})
|
|
51
66
|
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import config from './config.js'
|
|
2
|
+
|
|
3
|
+
const CACHE_KEY = 'foodCache'
|
|
4
|
+
|
|
5
|
+
function getCache() {
|
|
6
|
+
return config.get(CACHE_KEY) || []
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function setCache(cache) {
|
|
10
|
+
config.set(CACHE_KEY, cache)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function parseNumber(str) {
|
|
14
|
+
if (!str || str === '—' || str === '') return null
|
|
15
|
+
const num = parseFloat(String(str).replace(/[^\d.]/g, ''))
|
|
16
|
+
return isNaN(num) ? null : num
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseKjToKcal(str) {
|
|
20
|
+
const kj = parseNumber(str)
|
|
21
|
+
if (kj === null) return null
|
|
22
|
+
return Math.round(kj / 4.184 * 10) / 10
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parse a single food item from the API response array into a structured object.
|
|
27
|
+
* Field mapping (based on actual API response):
|
|
28
|
+
* [0] foodCode → sourceId
|
|
29
|
+
* [2] foodName → name
|
|
30
|
+
* [5] water %
|
|
31
|
+
* [6] edible portion
|
|
32
|
+
* [7] energy (kJ) → energyKcal (converted)
|
|
33
|
+
* [8] protein (g)
|
|
34
|
+
* [9] fat (g)
|
|
35
|
+
* [11] dietary fiber (g)
|
|
36
|
+
* [12] carbohydrate (g)
|
|
37
|
+
*/
|
|
38
|
+
function parseFoodItem(item) {
|
|
39
|
+
return {
|
|
40
|
+
source: 'chinanutri',
|
|
41
|
+
sourceId: String(item[0]),
|
|
42
|
+
name: item[2] || '',
|
|
43
|
+
energyKcal: parseKjToKcal(item[7]),
|
|
44
|
+
protein: parseNumber(item[8]),
|
|
45
|
+
fat: parseNumber(item[9]),
|
|
46
|
+
carbs: parseNumber(item[12]),
|
|
47
|
+
rawItem: item
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Search foods from the remote API.
|
|
53
|
+
* Returns an array of parsed food objects.
|
|
54
|
+
*/
|
|
55
|
+
export async function searchRemote(name, limit = 20) {
|
|
56
|
+
const body = new URLSearchParams({
|
|
57
|
+
categoryOne: '0',
|
|
58
|
+
categoryTwo: '0',
|
|
59
|
+
foodName: name,
|
|
60
|
+
pageNum: '1',
|
|
61
|
+
field: '0',
|
|
62
|
+
flag: '0'
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const controller = new AbortController()
|
|
66
|
+
const timeout = setTimeout(() => controller.abort(), 5000)
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const response = await fetch(
|
|
70
|
+
'https://nlc.chinanutri.cn/fq/FoodInfoQueryAction!queryFoodInfoList.do',
|
|
71
|
+
{
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: {
|
|
74
|
+
'Referer': 'https://nlc.chinanutri.cn/fq/foodlist.htm',
|
|
75
|
+
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
|
|
76
|
+
},
|
|
77
|
+
body: body.toString(),
|
|
78
|
+
signal: controller.signal
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
clearTimeout(timeout)
|
|
83
|
+
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
throw new Error(`HTTP ${response.status}`)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const data = await response.json()
|
|
89
|
+
const items = data.list || []
|
|
90
|
+
|
|
91
|
+
return items.slice(0, limit).map(parseFoodItem)
|
|
92
|
+
} catch (error) {
|
|
93
|
+
clearTimeout(timeout)
|
|
94
|
+
if (error.name === 'AbortError') {
|
|
95
|
+
throw new Error('TIMEOUT')
|
|
96
|
+
}
|
|
97
|
+
throw error
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Search cached foods by name (fuzzy match).
|
|
103
|
+
*/
|
|
104
|
+
export function searchCache(name, limit = 5) {
|
|
105
|
+
const cache = getCache()
|
|
106
|
+
const keyword = name.toLowerCase()
|
|
107
|
+
return cache
|
|
108
|
+
.filter(item => item.name.toLowerCase().includes(keyword))
|
|
109
|
+
.slice(0, limit)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Upsert food items into local cache.
|
|
114
|
+
*/
|
|
115
|
+
export function upsertCache(items) {
|
|
116
|
+
const cache = getCache()
|
|
117
|
+
for (const item of items) {
|
|
118
|
+
const idx = cache.findIndex(c => c.sourceId === item.sourceId)
|
|
119
|
+
const entry = {
|
|
120
|
+
source: item.source,
|
|
121
|
+
sourceId: item.sourceId,
|
|
122
|
+
name: item.name,
|
|
123
|
+
energyKcal: item.energyKcal,
|
|
124
|
+
protein: item.protein,
|
|
125
|
+
carbs: item.carbs,
|
|
126
|
+
fat: item.fat,
|
|
127
|
+
updatedAt: new Date().toISOString()
|
|
128
|
+
}
|
|
129
|
+
if (idx >= 0) {
|
|
130
|
+
cache[idx] = entry
|
|
131
|
+
} else {
|
|
132
|
+
cache.push(entry)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
setCache(cache)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Main search function: cache-first with remote fallback.
|
|
140
|
+
*/
|
|
141
|
+
export async function searchFood(name, { limit = 5, noCache = false } = {}) {
|
|
142
|
+
// 1. If cache is allowed, try cache first
|
|
143
|
+
if (!noCache) {
|
|
144
|
+
const cached = searchCache(name, limit)
|
|
145
|
+
if (cached.length > 0) {
|
|
146
|
+
return { items: cached, fromCache: true }
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 2. Query remote
|
|
151
|
+
try {
|
|
152
|
+
const remoteItems = await searchRemote(name, limit)
|
|
153
|
+
if (remoteItems.length > 0) {
|
|
154
|
+
upsertCache(remoteItems)
|
|
155
|
+
return { items: remoteItems.slice(0, limit), fromCache: false }
|
|
156
|
+
}
|
|
157
|
+
return { items: [], fromCache: false }
|
|
158
|
+
} catch (error) {
|
|
159
|
+
// 3. On remote failure, try cache as fallback
|
|
160
|
+
if (error.message === 'TIMEOUT') {
|
|
161
|
+
const cached = searchCache(name, limit)
|
|
162
|
+
if (cached.length > 0) {
|
|
163
|
+
return { items: cached, fromCache: true, degraded: true }
|
|
164
|
+
}
|
|
165
|
+
throw new Error('查询超时,请稍后重试')
|
|
166
|
+
}
|
|
167
|
+
// Other remote errors: degrade to cache
|
|
168
|
+
const cached = searchCache(name, limit)
|
|
169
|
+
if (cached.length > 0) {
|
|
170
|
+
return { items: cached, fromCache: true, degraded: true }
|
|
171
|
+
}
|
|
172
|
+
throw error
|
|
173
|
+
}
|
|
174
|
+
}
|
package/src/lib/output.js
CHANGED
|
@@ -39,6 +39,7 @@ function extractItems(data, type) {
|
|
|
39
39
|
'diet-list': 'diets',
|
|
40
40
|
'sleep-list': 'sleeps',
|
|
41
41
|
'record-list': 'records',
|
|
42
|
+
'food-list': 'foods',
|
|
42
43
|
'timeline': 'items'
|
|
43
44
|
}
|
|
44
45
|
const key = keyMap[type]
|
|
@@ -101,6 +102,15 @@ function buildRows(items, type) {
|
|
|
101
102
|
item.date ? formatDate(item.date, df) : '-',
|
|
102
103
|
item.note || '-'
|
|
103
104
|
])
|
|
105
|
+
case 'food-list':
|
|
106
|
+
return items.map((item, idx) => [
|
|
107
|
+
idx + 1,
|
|
108
|
+
item.name,
|
|
109
|
+
item.energyKcal ?? '-',
|
|
110
|
+
item.protein ?? '-',
|
|
111
|
+
item.carbs ?? '-',
|
|
112
|
+
item.fat ?? '-'
|
|
113
|
+
])
|
|
104
114
|
case 'timeline':
|
|
105
115
|
return items.map((item, idx) => {
|
|
106
116
|
let summary = ''
|
|
@@ -128,6 +138,7 @@ function getHeaders(type) {
|
|
|
128
138
|
case 'diet-list': return ['#', 'ID', '餐别', '热量', '蛋白质', '碳水', '脂肪', '日期']
|
|
129
139
|
case 'sleep-list': return ['#', 'ID', '时长(h)', '质量', '入睡', '醒来', '深睡(h)', '日期']
|
|
130
140
|
case 'record-list': return ['#', 'ID', '类型', '标签', '日期', '备注']
|
|
141
|
+
case 'food-list': return ['#', '名称', '热量(kcal)', '蛋白质(g)', '碳水(g)', '脂肪(g)']
|
|
131
142
|
case 'timeline': return ['#', '时间', '类型', '摘要']
|
|
132
143
|
default: return []
|
|
133
144
|
}
|
|
@@ -154,7 +165,9 @@ function outputStats(data, type, format) {
|
|
|
154
165
|
case 'exercise-stats':
|
|
155
166
|
if (data.count !== undefined) rows.push(['总次数', data.count])
|
|
156
167
|
if (data.totalDuration !== undefined) rows.push(['总时长', `${data.totalDuration} min`])
|
|
168
|
+
if (data.avgDuration !== null) rows.push(['平均时长', `${data.avgDuration.toFixed(1)} min`])
|
|
157
169
|
if (data.totalCalories !== undefined) rows.push(['总热量', `${data.totalCalories} kcal`])
|
|
170
|
+
if (data.avgCalories !== null) rows.push(['平均热量', `${data.avgCalories.toFixed(0)} kcal`])
|
|
158
171
|
if (data.frequencyByType) {
|
|
159
172
|
for (const [t, c] of Object.entries(data.frequencyByType)) {
|
|
160
173
|
rows.push([`频率 (${t})`, c])
|
|
@@ -162,7 +175,7 @@ function outputStats(data, type, format) {
|
|
|
162
175
|
}
|
|
163
176
|
break
|
|
164
177
|
case 'diet-stats':
|
|
165
|
-
if (data.
|
|
178
|
+
if (data.avgCalories !== undefined) rows.push(['平均热量', `${data.avgCalories?.toFixed?.(0) || data.avgCalories} kcal`])
|
|
166
179
|
if (data.avgProtein !== undefined) rows.push(['平均蛋白质', `${data.avgProtein?.toFixed?.(1) || data.avgProtein}g`])
|
|
167
180
|
if (data.avgCarbs !== undefined) rows.push(['平均碳水', `${data.avgCarbs?.toFixed?.(1) || data.avgCarbs}g`])
|
|
168
181
|
if (data.avgFat !== undefined) rows.push(['平均脂肪', `${data.avgFat?.toFixed?.(1) || data.avgFat}g`])
|