@yorha2b-lab/autodev 3.4.0 → 3.4.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.
- package/bin/autodev.js +4 -1
- package/package.json +1 -1
- package/src/commands/watch.js +2 -7
- package/src/core/context.js +9 -7
- package/src/core/react-compiler.js +1 -2
- package/src/utils/utils.js +1 -1
- package/templates/react/components/MyModalForm.js +4 -3
- package/templates/react/components/MyModalTable.js +3 -2
- package/templates/react/components/MySearchForm.js +30 -22
- package/templates/react/components/MyTable.js +1 -1
- package/templates/react/handlebars/MySearchForm.hbs +1 -1
- package/templates/react/handlebars/MyTable.hbs +4 -1
- package/templates/react/handlebars/handleBlock.hbs +1 -0
- package/templates/react/handlebars/hookBlock.hbs +2 -2
- package/templates/react/hooks/useTableQuery.js +11 -3
- package/templates/react/utils/utils.js +4 -2
package/bin/autodev.js
CHANGED
|
@@ -52,8 +52,11 @@ program
|
|
|
52
52
|
.alias('start')
|
|
53
53
|
.description(language('开启全频道联动监控:支持 Page/Part/API 协同构筑', 'Start full-channel linked monitoring: Coordinated Page/Part/API construction'))
|
|
54
54
|
.action(() => {
|
|
55
|
+
const result = bunker.init(program.opts())
|
|
56
|
+
if (!result) {
|
|
57
|
+
return
|
|
58
|
+
}
|
|
55
59
|
bootSequence(pkg.version)
|
|
56
|
-
bunker.init(program.opts())
|
|
57
60
|
require('../src/commands/watch.js')()
|
|
58
61
|
const { config } = bunker.get()
|
|
59
62
|
if (config.proxyTarget && !config.needMock && config.enableAutoAlignment) {
|
package/package.json
CHANGED
package/src/commands/watch.js
CHANGED
|
@@ -10,13 +10,7 @@ const watch = () => {
|
|
|
10
10
|
const { copyTemplateDir } = require('../utils/utils.js')
|
|
11
11
|
const { createTaskQueue } = require('../core/task-queue.js')
|
|
12
12
|
|
|
13
|
-
const { menus, config, options,
|
|
14
|
-
|
|
15
|
-
const compilerPath = path.join(__dirname, `../core/${template}-compiler.js`)
|
|
16
|
-
if (!fs.existsSync(compilerPath)) {
|
|
17
|
-
console.error(chalk.red(language(`❌ 暂不支持 [${template}] 框架。`, `❌ [${template}] framework not supported.`)))
|
|
18
|
-
return
|
|
19
|
-
}
|
|
13
|
+
const { menus, config, options, language, apiHandler, pageHandler, partHandler } = get()
|
|
20
14
|
|
|
21
15
|
try {
|
|
22
16
|
if (config.hbsDir === '') {
|
|
@@ -46,6 +40,7 @@ const watch = () => {
|
|
|
46
40
|
|
|
47
41
|
const watcher = chokidar.watch(['./screenShot', './screenPart', './response'], {
|
|
48
42
|
persistent: true,
|
|
43
|
+
ignoreInitial: true,
|
|
49
44
|
ignored: /(^|[\/\\])\../,
|
|
50
45
|
awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 100 }
|
|
51
46
|
})
|
package/src/core/context.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const fs = require('fs')
|
|
2
2
|
const path = require('path')
|
|
3
|
+
const chalk = require('chalk')
|
|
3
4
|
const Handlebars = require('handlebars')
|
|
4
5
|
const stringify = require('json-stringify-pretty-compact')
|
|
5
6
|
|
|
@@ -26,10 +27,16 @@ module.exports = {
|
|
|
26
27
|
const tplDir = config.hbsDir !== '' ? path.join(process.cwd(), config.hbsDir) : path.join(__dirname, `../../templates/${template}`)
|
|
27
28
|
|
|
28
29
|
const compilerPath = path.join(__dirname, `./${template}-compiler.js`)
|
|
30
|
+
if (!fs.existsSync(compilerPath)) {
|
|
31
|
+
console.error(chalk.red(language(`❌ 暂不支持 [${template}] 框架。欢迎提交 Pull Request 贡献。`, `❌ [${template}] framework not supported. Welcome to contribute a Pull Request to help.`)))
|
|
32
|
+
return
|
|
33
|
+
}
|
|
29
34
|
const { index, resource } = require(compilerPath)
|
|
30
35
|
|
|
31
36
|
// 💡 物理扫描:把目录下所有的 .hbs 文件全部识别并自动注册
|
|
32
|
-
const partialsDir =
|
|
37
|
+
const partialsDir = config.hbsDir !== ''
|
|
38
|
+
? path.join(process.cwd(), config.hbsDir, 'handlebars')
|
|
39
|
+
: path.join(__dirname, `../../templates/${template}/handlebars`)
|
|
33
40
|
fs.readdirSync(partialsDir).forEach(file => {
|
|
34
41
|
if (file.endsWith('.hbs')) {
|
|
35
42
|
const name = path.basename(file, '.hbs') // 去掉后缀拿名字
|
|
@@ -54,10 +61,5 @@ module.exports = {
|
|
|
54
61
|
/**
|
|
55
62
|
* @description 全频道信号接入:获取全局上下文
|
|
56
63
|
*/
|
|
57
|
-
get: () =>
|
|
58
|
-
if (!instance) {
|
|
59
|
-
throw new Error(language('🤖 Pod 042 报警:中枢神经元尚未初始化,无法建立信号连接。', '🤖 Pod 042 Warnning:Central neuron not initialized, cannot establish signal connection.'))
|
|
60
|
-
}
|
|
61
|
-
return instance
|
|
62
|
-
}
|
|
64
|
+
get: () => instance
|
|
63
65
|
}
|
|
@@ -14,7 +14,6 @@ const resource = ({ pageConfig, resourceTpl }) => {
|
|
|
14
14
|
|
|
15
15
|
const hasTabs = pageConfig.tabs?.length > 0
|
|
16
16
|
const hasFormItems = pageConfig.formItems?.length > 0
|
|
17
|
-
const hasDateColumn = processedColumns.some(item => item.type === 'date')
|
|
18
17
|
|
|
19
18
|
const columnsData = processedColumns.map(item => {
|
|
20
19
|
delete item.type
|
|
@@ -31,7 +30,7 @@ const resource = ({ pageConfig, resourceTpl }) => {
|
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
const bodyCode = resourceTpl(viewData)
|
|
34
|
-
const importsStr = generateSmartImports({ module: 'resource', hasTabs, bodyCode, hasFormItems
|
|
33
|
+
const importsStr = generateSmartImports({ module: 'resource', hasTabs, bodyCode, hasFormItems })
|
|
35
34
|
return cleanCode(`${importsStr}\n\n${bodyCode}`)
|
|
36
35
|
}
|
|
37
36
|
|
package/src/utils/utils.js
CHANGED
|
@@ -305,7 +305,7 @@ const copyTemplateDir = (options, templateSubDir, targetSubDir) => {
|
|
|
305
305
|
* @param {boolean} hasTabs - 是否包含标签页
|
|
306
306
|
* @returns {string} 拼接后的 import 语句
|
|
307
307
|
*/
|
|
308
|
-
const generateSmartImports = ({ module, hasTabs, bodyCode, hasFormItems
|
|
308
|
+
const generateSmartImports = ({ module, hasTabs, bodyCode, hasFormItems }) => {
|
|
309
309
|
|
|
310
310
|
const hooksLib = ['useTableQuery']
|
|
311
311
|
const utilsLib = ['timeRender', 'moneyRender']
|
|
@@ -130,8 +130,9 @@ export const MyModalForm = ({ width, title, submit, record, visible, setModal, l
|
|
|
130
130
|
const [selectedTableRows, setSelectedTableRows] = useState([])
|
|
131
131
|
|
|
132
132
|
const flattenFormItems = items => {
|
|
133
|
+
if (!Array.isArray(items)) return []
|
|
133
134
|
return items.flatMap(item => {
|
|
134
|
-
if (item.
|
|
135
|
+
if (item.childItems) {
|
|
135
136
|
return flattenFormItems(item.childItems)
|
|
136
137
|
}
|
|
137
138
|
return item
|
|
@@ -181,7 +182,7 @@ export const MyModalForm = ({ width, title, submit, record, visible, setModal, l
|
|
|
181
182
|
try {
|
|
182
183
|
const values = await form.validateFields()
|
|
183
184
|
const formattedValues = { ...values }
|
|
184
|
-
formItems.forEach(item => {
|
|
185
|
+
flattenFormItems(formItems).forEach(item => {
|
|
185
186
|
const val = formattedValues[item.name]
|
|
186
187
|
if (item.type?.includes('date') && val) {
|
|
187
188
|
if (Array.isArray(val)) {
|
|
@@ -218,7 +219,7 @@ export const MyModalForm = ({ width, title, submit, record, visible, setModal, l
|
|
|
218
219
|
const tableRowSelection = useMemo(() => ({
|
|
219
220
|
selectedRowKeys: selectedTableRows.map(row => row[rowKey]),
|
|
220
221
|
onChange: (selectedRowKeys, selectedRows) => setSelectedTableRows(selectedRows),
|
|
221
|
-
}), [rowKey,
|
|
222
|
+
}), [rowKey, selectedTableRows])
|
|
222
223
|
|
|
223
224
|
return (
|
|
224
225
|
<Modal centered title={title} width={width} open={visible} onOk={handleOk} destroyOnClose onCancel={handleCancel} confirmLoading={pending}>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { MyTable } from './MyTable'
|
|
2
2
|
import { useMemo, useState } from 'react'
|
|
3
|
-
import { Modal, Space, Button } from 'antd'
|
|
4
3
|
import { MySearchForm } from './MySearchForm'
|
|
4
|
+
import { Form, Modal, Space, Button } from 'antd'
|
|
5
5
|
import { useTableQuery } from '../hooks/useTableQuery'
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -38,6 +38,7 @@ import { useTableQuery } from '../hooks/useTableQuery'
|
|
|
38
38
|
*/
|
|
39
39
|
export const MyModalTable = ({ api, onOk, title, width, footer, visible, operate, setModal, formItems, setModalForm, rowSelection, formatResponse, functionButtons, columns: modalColumns, rowKey = 'id', initialParams = {}, modalPagination = true }) => {
|
|
40
40
|
|
|
41
|
+
const [form] = Form.useForm()
|
|
41
42
|
const [pending, setPending] = useState(false)
|
|
42
43
|
|
|
43
44
|
/**
|
|
@@ -82,7 +83,7 @@ export const MyModalTable = ({ api, onOk, title, width, footer, visible, operate
|
|
|
82
83
|
💡 [地堡战术细节]
|
|
83
84
|
syncUrlParams={false}:确保弹窗内的搜索行为不会干扰全局 URL 信号。
|
|
84
85
|
*/}
|
|
85
|
-
{formItems?.length > 0 && <MySearchForm search={search} formItems={formItems} setSearch={handleSearch} syncUrlParams={false} />}
|
|
86
|
+
{formItems?.length > 0 && <MySearchForm form={form} search={search} formItems={formItems} setSearch={handleSearch} syncUrlParams={false} />}
|
|
86
87
|
{/*
|
|
87
88
|
💡 [高阶赋能逻辑]
|
|
88
89
|
functionButtons 能够获得对当前表格状态 (refresh, setLoading...) 的完全控制权。
|
|
@@ -22,6 +22,7 @@ import { Row, Col, Form, Button, Space } from 'antd'
|
|
|
22
22
|
* @param {React.ReactNode} [props.extraOperate] - 额外挂件:在按钮区域注入自定义战术单元
|
|
23
23
|
* @param {boolean} props.loading - 战场负载状态:请求中时锁定按钮,防止由于重复指令导致的逻辑冲突
|
|
24
24
|
* @param {boolean} [props.syncUrlParams=true] - 物理同步开关:是否将搜索状态实时镜像至 URL 链接中
|
|
25
|
+
* @param {Object} props.initialParams - [重要] 战术底价:定义不可被重置协议核平的核心业务参数(如 projectId)
|
|
25
26
|
*
|
|
26
27
|
* @example
|
|
27
28
|
* <MySearchForm
|
|
@@ -31,7 +32,7 @@ import { Row, Col, Form, Button, Space } from 'antd'
|
|
|
31
32
|
* formItems={[{ label: '状态', name: 'status', type: 'select', options: [...] }]}
|
|
32
33
|
* />
|
|
33
34
|
*/
|
|
34
|
-
export const MySearchForm = ({ form, search, loading, labelCol, setSearch, formItems, showLimit = 7, initialValues, customReset, extraOperate, customFinish, onValuesChange, syncUrlParams = true, defaultPageSize = 10 }) => {
|
|
35
|
+
export const MySearchForm = ({ form, search, loading, labelCol, setSearch, formItems, showLimit = 7, initialParams, initialValues, customReset, extraOperate, customFinish, onValuesChange, syncUrlParams = true, defaultPageSize = 10 }) => {
|
|
35
36
|
|
|
36
37
|
const [limit, setLimit] = useState(showLimit)
|
|
37
38
|
|
|
@@ -40,15 +41,21 @@ export const MySearchForm = ({ form, search, loading, labelCol, setSearch, formI
|
|
|
40
41
|
* @description 执行“归零协议”:清除当前所有搜索信号,并可选择性擦除 URL 中的物理记录。
|
|
41
42
|
*/
|
|
42
43
|
const handleReset = () => {
|
|
44
|
+
form.resetFields()
|
|
43
45
|
if (syncUrlParams) {
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
+
// 💡 修正逻辑:只有当 formItems 里【没有任何一项】包含这个 key 时,才视为默认参数
|
|
47
|
+
const defaultQueryEntries = Object.entries(initialValues).filter(([key, _]) => !formItems.some(item => item.name.includes(key)))
|
|
48
|
+
const defaultQuery = Object.fromEntries(defaultQueryEntries)
|
|
49
|
+
const params = new URLSearchParams(defaultQuery)
|
|
50
|
+
// 💡 执行物理重写:将 URL 恢复为仅含默认参数的状态
|
|
51
|
+
const newUrl = params.toString() ? `${window.location.pathname}?${params.toString()}` : window.location.pathname
|
|
52
|
+
window.history.replaceState(null, '', newUrl)
|
|
46
53
|
}
|
|
47
54
|
if (customReset) {
|
|
48
55
|
customReset()
|
|
49
56
|
} else {
|
|
50
57
|
// 逻辑重置:强制返回第一页,并恢复默认吞吐量
|
|
51
|
-
setSearch({ pageNo: 1, pageSize: search.pageSize ?? defaultPageSize })
|
|
58
|
+
setSearch({ pageNo: 1, pageSize: search.pageSize ?? defaultPageSize, ...initialParams })
|
|
52
59
|
}
|
|
53
60
|
}
|
|
54
61
|
|
|
@@ -59,26 +66,27 @@ export const MySearchForm = ({ form, search, loading, labelCol, setSearch, formI
|
|
|
59
66
|
*/
|
|
60
67
|
const handleFinish = values => {
|
|
61
68
|
const formattedValues = { ...values }
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
formattedValues[key] = val.valueOf()
|
|
77
|
-
}
|
|
69
|
+
Object.keys(formattedValues).forEach(key => {
|
|
70
|
+
const val = formattedValues[key]
|
|
71
|
+
const isDateType = formItems.find(item => item.name === key)?.type?.includes('date')
|
|
72
|
+
// 💡 [黑科技 1] 日期自动转换与拆解
|
|
73
|
+
if (isDateType && val) {
|
|
74
|
+
if (Array.isArray(val) && val.length > 0) {
|
|
75
|
+
// 物理拆分:当 name 为 'start,end' 格式时,自动拆解为两个独立的后端参数
|
|
76
|
+
const [startKey, endKey] = key.split(',')
|
|
77
|
+
formattedValues[startKey] = val[0].startOf('day').valueOf()// 修正为当日 00:00:00
|
|
78
|
+
formattedValues[endKey] = val[1].endOf('day').valueOf()// 修正为当日 23:59:59
|
|
79
|
+
delete formattedValues[key]
|
|
80
|
+
} else {
|
|
81
|
+
// 单日转换:转化为毫秒级数字
|
|
82
|
+
formattedValues[key] = val.valueOf()
|
|
78
83
|
}
|
|
79
|
-
}
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
if (syncUrlParams) {
|
|
80
87
|
// 💡 [黑科技 2] 镜像至 URL
|
|
81
|
-
const
|
|
88
|
+
const currentQuery = Object.fromEntries(new URLSearchParams(window.location.search).entries())
|
|
89
|
+
const params = new URLSearchParams(Object.fromEntries(Object.entries({ ...currentQuery, ...formattedValues }).filter(([key, value]) => !['', null, undefined].includes(value))))
|
|
82
90
|
window.history.replaceState(null, '', `${window.location.pathname}?${params.toString()}`)
|
|
83
91
|
}
|
|
84
92
|
if (customFinish) {
|
|
@@ -183,7 +183,7 @@ export const MyTable = ({ size, query, total, search, autoScroll, onChange, pagi
|
|
|
183
183
|
pagination={paginationConfig}
|
|
184
184
|
components={isEditable ? components : undefined}
|
|
185
185
|
// 💡 信号注入:将行监听协议绑定至底层 tr
|
|
186
|
-
onRow={(record, index) => ({ record, index, onValuesChange: lineFormChange })}
|
|
186
|
+
onRow={(record, index) => ({ record, index, ...(typeof onValuesChange === 'function' ? { onValuesChange: lineFormChange } : {}) })}
|
|
187
187
|
rowClassName={(record, index) => {
|
|
188
188
|
const externalClass = typeof rowClassName === 'function' ? rowClassName(record, index) : rowClassName
|
|
189
189
|
const classes = [externalClass]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<MySearchForm form={form} search={search} initialValues={query} loading={loading} setSearch={setSearch} formItems={ {{formItems}} } />
|
|
1
|
+
<MySearchForm form={form} search={search} initialValues={query} loading={loading} setSearch={setSearch} formItems={ {{formItems}} } initialParams={initialParams} />
|
|
@@ -27,7 +27,10 @@
|
|
|
27
27
|
window.history.replaceState(null, '', `${window.location.pathname}?${params.toString()}`)
|
|
28
28
|
setSearch(newSearch)
|
|
29
29
|
} else {
|
|
30
|
-
|
|
30
|
+
const newSearch = { ...search, pageNo: pagination.current, pageSize: pagination.pageSize, orderBy: undefined }
|
|
31
|
+
const params = new URLSearchParams(Object.fromEntries(Object.entries({ ...query, ...newSearch }).filter(([key, value]) => !['', null, undefined].includes(value))))
|
|
32
|
+
window.history.replaceState(null, '', `${window.location.pathname}?${params.toString()}`)
|
|
33
|
+
setSearch(newSearch)
|
|
31
34
|
}
|
|
32
35
|
}}
|
|
33
36
|
{{{{/raw}}}}
|
|
@@ -28,6 +28,7 @@ const {{this.action}} = () => {
|
|
|
28
28
|
{{#if hasTabs}}
|
|
29
29
|
const onTabChange = key => {
|
|
30
30
|
setActiveKey(key)
|
|
31
|
+
{{#if hasFormItems}}form.resetFields(){{/if}}
|
|
31
32
|
{{#if hasRowSelection}}setSelectedRows([]){{/if}}
|
|
32
33
|
setSearch({ pageNo: 1, pageSize: 10, type: key })
|
|
33
34
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
const
|
|
1
|
+
const initialParams = {{#if hasTabs}}{ type: tabs[0].key }{{else}}{}{{/if}}
|
|
2
2
|
|
|
3
3
|
const query = formatQuery(Object.fromEntries(new URLSearchParams(location.search).entries()), {{formItems}})
|
|
4
4
|
|
|
5
5
|
const { total, columns, loading, dataSource, search, setSearch, setDataSource, refresh } = useTableQuery({
|
|
6
6
|
api: async params => await request('/api/{{fileName}}', { method: 'POST', body: params }),
|
|
7
|
-
initialParams: { pageNo: 1, pageSize: 10, ...
|
|
7
|
+
initialParams: { pageNo: 1, pageSize: 10, ...initialParams, ...query },
|
|
8
8
|
cols:tableColumns,
|
|
9
9
|
formatResponse: response => {
|
|
10
10
|
return {
|
|
@@ -41,6 +41,8 @@ export const useTableQuery = ({ api, cols, initialParams = {}, formatResponse })
|
|
|
41
41
|
const apiRef = useRef(api)
|
|
42
42
|
// 💡 [核心防御] 信号 ID 计数器:用于物理拦截由于网络时延导致的“竞态病毒(Race Condition)”
|
|
43
43
|
const fetchIdRef = useRef(0)
|
|
44
|
+
// 💡 [核心防御] 数据解码协议引用:确保异步回调中永远能指向最新的指令地址
|
|
45
|
+
const formatResponseRef = useRef(formatResponse)
|
|
44
46
|
|
|
45
47
|
useEffect(() => {
|
|
46
48
|
apiRef.current = api
|
|
@@ -70,10 +72,16 @@ export const useTableQuery = ({ api, cols, initialParams = {}, formatResponse })
|
|
|
70
72
|
const response = await apiRef.current(search)
|
|
71
73
|
// 💡 [物理拦截] 信号校准:若当前 ID 不是最新 ID,说明该信号已过期,执行丢弃协议
|
|
72
74
|
if (currentFetchId !== fetchIdRef.current) return
|
|
73
|
-
const { data, total, columns: newCols } =
|
|
75
|
+
const { data, total, columns: newCols } = formatResponseRef.current?.(response ?? {}) || {}
|
|
74
76
|
// 💡 [性能装甲] 只有当列指纹发生变化时,才触发 React 的重绘逻辑
|
|
75
|
-
if (newCols
|
|
76
|
-
|
|
77
|
+
if (newCols) {
|
|
78
|
+
// 使用函数式状态更新,既能拿到最新的 prevCols,又无需将 columns 作为 useCallback 的依赖!
|
|
79
|
+
setColumns(prevCols => {
|
|
80
|
+
if (getColumnSchema(newCols) !== getColumnSchema(prevCols)) {
|
|
81
|
+
return newCols
|
|
82
|
+
}
|
|
83
|
+
return prevCols
|
|
84
|
+
})
|
|
77
85
|
}
|
|
78
86
|
setDataSource(data)
|
|
79
87
|
setTotal(total ?? 0)
|
|
@@ -58,7 +58,7 @@ export const initOSS = async (url, options) => {
|
|
|
58
58
|
export const formatQuery = (params, formItems) => {
|
|
59
59
|
const cleanParams = {}
|
|
60
60
|
Object.keys(params).forEach(key => {
|
|
61
|
-
if (formItems.some(item => item.name.includes(key) && item.type?.includes('date'))) {
|
|
61
|
+
if (formItems.some(item => item.name.includes(key) && item.type?.includes('date')) && params[key]) {
|
|
62
62
|
cleanParams[key] = Number(params[key])
|
|
63
63
|
} else {
|
|
64
64
|
cleanParams[key] = params[key] ?? null
|
|
@@ -225,7 +225,9 @@ export const formatUnit = (value, { k = 1024, units = ['B', 'KB', 'MB', 'GB', 'T
|
|
|
225
225
|
if (value === null || value === undefined || isNaN(value) || value === 0) {
|
|
226
226
|
return `0 ${units[0] ?? ''}`
|
|
227
227
|
}
|
|
228
|
-
const
|
|
228
|
+
const absValue = Math.abs(value)
|
|
229
|
+
// 💡 安全屏障:小于 1 的数值直接归入初始级单位,不进行对数拆解
|
|
230
|
+
const i = absValue < 1 ? 0 : Math.floor(Math.log(absValue) / Math.log(k))
|
|
229
231
|
const index = Math.min(i, units.length - 1)
|
|
230
232
|
const result = (value / Math.pow(k, index)).toFixed(decimals)
|
|
231
233
|
return `${parseFloat(result)} ${units[index]}`
|