main
youys 5 days ago
commit 25be954fd7

@ -26,7 +26,7 @@
"core-js": "^3.8.3",
"crypto-browserify": "^3.12.0",
"echarts": "5.4.3",
"element-plus": "^2.4.1",
"element-plus": "2.10.4",
"generate-avatar": "1.4.10",
"js-cookie": "3.0.5",
"lodash": "^4.17.21",

@ -2,23 +2,14 @@
<block-box :title="title">
<template #extra>
<el-radio-group v-model="tabActive" size="small" @change="handleTabChange">
<el-radio-button
v-for="{ tab, key } in config"
:label="tab"
:value="key"
/>
<el-radio-button v-for="{ tab, key } in config" :label="tab" :value="key" />
</el-radio-group>
</template>
<echarts-plus
:options="
getTopOptions(
<echarts-plus :options="getTopOptions(
currentConfig.find((item) => item.key === tabActive)?.data || [],
)
"
:onClick="handleClick"
style="min-height: 250px; height: 100%"
/>
" :onClick="handleClick" style="min-height: 250px; height: 100%" />
</block-box>
</template>
@ -28,6 +19,7 @@ import { onMounted, ref } from 'vue';
import EchartsPlus from '@/components/Echarts-plus.vue';
import cardApi from '~/vgpu/api/card';
import { cloneDeep } from 'lodash';
import { formatSmartPercentage } from '@/utils';
const props = defineProps({
title: String,
@ -65,7 +57,7 @@ const getTopOptions = () => {
res +=
params[i].marker +
params[i].seriesName +
(+params[i].value).toFixed(0) +
formatSmartPercentage(params[i].value) +
`${config.unit || '%'}<br/>`;
}
return res;
@ -111,7 +103,6 @@ onMounted(async () => {
query: v.query,
})
.then((res) => {
console.log(v.query, res, 'res')
currentConfig.value[i].data = res.data.map((item) => ({
name: item.metric[v.nameKey],
value: item.value,

@ -1,4 +1,4 @@
import { timeParse } from '@/utils';
import { timeParse, formatSmartPercentage } from '@/utils';
export default ({ percent, title, unit = '%' }) => {
const value = percent.toFixed(1);
@ -232,7 +232,7 @@ export const getLineOptions = ({ data = [], unit = '%' }) => {
var res = params[0].name + '<br/>';
for (var i = 0; i < params.length; i++) {
res +=
params[i].marker + (+params[i].value).toFixed(0) + ` ${unit}<br/>`;
params[i].marker + formatSmartPercentage(params[i].value) + ` ${unit}<br/>`;
}
return res;
},
@ -240,7 +240,7 @@ export const getLineOptions = ({ data = [], unit = '%' }) => {
grid: {
top: 7, // 上边距
bottom: 20, // 下边距
left: '7%', // 左边距
left: '10%', // 左边距
right: 10, // 右边距
},
xAxis: {
@ -258,7 +258,7 @@ export const getLineOptions = ({ data = [], unit = '%' }) => {
series: [
{
data: data.map((item) => {
return item.value.toFixed(1);
return item.value;
}),
type: 'line',
areaStyle: {

@ -198,6 +198,7 @@ const columns = [
},
{
label: '使用模式',
width: 120,
value: 'mode',
render: ({ mode, type }) => (
<el-tag disable-transitions>
@ -207,26 +208,26 @@ const columns = [
}
];
const cp = useInstantVector(
[
{
label: 'vGPU超配',
count: '0',
query: `avg(sum(hami_vgpu_count{node=~"$node"}) by (instance))`,
},
{
label: '算力超配',
count: '0',
query: `avg(sum(hami_vcore_scaling{node=~"$node"}) by (instance))`,
},
{
label: '显存超配',
count: '1.5',
query: `avg(sum(hami_vmemory_scaling{node=~"$node"}) by (instance))`,
},
],
(query) => query.replaceAll('$node', props.detail.name),
);
// const cp = useInstantVector(
// [
// {
// label: 'vGPU',
// count: '0',
// query: `avg(sum(hami_vgpu_count{node=~"$node"}) by (instance))`,
// },
// {
// label: '',
// count: '0',
// query: `avg(sum(hami_vcore_scaling{node=~"$node"}) by (instance))`,
// },
// {
// label: '',
// count: '1.5',
// query: `avg(sum(hami_vmemory_scaling{node=~"$node"}) by (instance))`,
// },
// ],
// (query) => query.replaceAll('$node', props.detail.name),
// );
const gaugeConfig = useInstantVector(
[

@ -1,4 +1,4 @@
import { timeParse } from '@/utils';
import { timeParse, formatSmartPercentage } from '@/utils';
export const getRangeOptions = ({ core = [], memory = [] }) => {
return {
@ -17,7 +17,7 @@ export const getRangeOptions = ({ core = [], memory = [] }) => {
params[i].marker +
params[i].seriesName +
' : ' +
(+params[i].value).toFixed(0) +
formatSmartPercentage(params[i].value) +
`%<br/>`;
}
@ -27,7 +27,7 @@ export const getRangeOptions = ({ core = [], memory = [] }) => {
grid: {
top: 37, // 上边距
bottom: 20, // 下边距
left: '7%', // 左边距
left: '10%', // 左边距
right: 10, // 右边距
},
xAxis: {

@ -66,6 +66,7 @@ const columns = [
},
{
title: '使用模式',
width: 120,
dataIndex: 'mode',
render: ({ mode, type }) => (
<el-tag disable-transitions>
@ -79,6 +80,7 @@ const columns = [
},
{
title: '所属资源池',
width: 100,
dataIndex: 'resourcePools',
render: ({ resourcePools }) => `${resourcePools.join('、')}`,
},
@ -97,6 +99,7 @@ const columns = [
},
{
title: '算力(已分配/总量)',
width: 120,
dataIndex: 'used',
render: ({ coreTotal, coreUsed, isExternal }) => (
<span>
@ -107,6 +110,7 @@ const columns = [
{
title: '显存(已分配/总量)',
dataIndex: 'w',
width: 120,
render: ({ memoryTotal, memoryUsed, isExternal }) => (
<span>
{isExternal ? '--' : roundToDecimal(memoryUsed / 1024, 1)}/

@ -106,7 +106,7 @@ export const rangeConfigInit = [
},
{
name: 'CPU',
query: `sum(hami_container_vcore_allocated) / sum(hami_core_size) * 100`,
query: `sum(hami_core_used) / sum(hami_core_size) * 100`,
data: [],
type: 'line',
areaStyle: {
@ -140,7 +140,7 @@ export const rangeConfigInit = [
},
{
name: '内存',
query: `sum(hami_container_vmemory_allocated) / sum(hami_memory_size) * 100`,
query: `sum(hami_memory_used) / sum(hami_memory_size) * 100`,
data: [],
type: 'line',
areaStyle: {

@ -1,4 +1,4 @@
import { timeParse } from '@/utils';
import { timeParse, formatSmartPercentage } from '@/utils';
import { cloneDeep } from 'lodash';
import nodeApi from '~/vgpu/api/node';
import { ElMessage } from 'element-plus';
@ -323,9 +323,9 @@ export const getCardOptions = (list, chartWidth) => {
rich: {
cnt: {
fontSize: 10,
color: '#999'
}
}
color: '#999',
},
},
},
labelLayout: function (params) {
const isLeft = params.labelRect.x < chartWidth / 2;
@ -343,7 +343,6 @@ export const getCardOptions = (list, chartWidth) => {
})),
},
],
};
};
@ -385,7 +384,7 @@ export const getLineOptions = ({
grid: {
top: 37, // 上边距
bottom: 20, // 下边距
left: '7%', // 左边距
left: '10%', // 左边距
right: 10, // 右边距
},
xAxis: {
@ -496,7 +495,7 @@ export const getRangeOptions = (data) => {
params[i].marker +
params[i].seriesName +
' : ' +
(+params[i].value).toFixed(0) +
formatSmartPercentage(params[i].value) +
`%<br/>`;
}
@ -506,7 +505,7 @@ export const getRangeOptions = (data) => {
grid: {
top: 37, // 上边距
bottom: 20, // 下边距
left: '7%', // 左边距
left: '10%', // 左边距
right: 10, // 右边距
},
xAxis: {

@ -44,7 +44,7 @@
<Block v-for="{ title, dataSource } in rangeConfig" :title="title" :key="title">
<template #extra>
<time-picker v-model="times" type="datetimerange" size="small" :key="`time-picker-${title}`" />
<time-picker v-model="times" type="datetimerange" size="small" />
</template>
<echarts-plus :options="getRangeOptions(dataSource)" style="height: 250px" />
</Block>
@ -481,14 +481,14 @@ const fetchRangeData = () => {
}
cardApi
.getRangeVector({
...params,
query: `sum({__name__=~"alert:.*:count"})`,
})
.then((res) => {
alarmData.value = res.data[0].values;
});
// cardApi
// .getRangeVector({
// ...params,
// query: `sum({__name__=~"alert:.*:count"})`,
// })
// .then((res) => {
// alarmData.value = res.data[0].values;
// });
};

@ -92,21 +92,22 @@
grid-template-columns: repeat(5, 1fr);
gap: 12px;
li {
height: 80px;
// height: 80px;
flex-shrink: 0;
border-radius: 6px;
background: #f5f7fa;
padding: 16px;
padding: 14px;
display: flex;
align-items: center;
//&:hover {
// color: var(--el-color-primary);
// cursor: pointer;
//}
.avatar {
display: flex;
width: 48px;
height: 48px;
padding: 14px;
width: 30px;
height: 30px;
padding: 6px;
justify-content: center;
align-items: center;
flex-shrink: 0;
@ -117,16 +118,16 @@
rgba(255, 255, 255, 0) 0%,
#fff 100%
);
margin-right: 16px;
margin-right: 8px;
}
.count {
font-family: Roboto;
font-size: 20px;
font-size: 14px;
font-style: normal;
font-weight: 700;
line-height: 100%; /* 20px */
margin-top: 10px;
margin-top: 8px;
}
}
}

@ -240,7 +240,7 @@ const gaugeConfig = useInstantVector(
percent: 0,
query: ``,
totalQuery: ``,
percentQuery: `avg(sum(hami_container_vcore_allocated{node=~"$node"}) by (instance) / sum(hami_core_size{node=~"$node"}) by (instance) * 100)`,
percentQuery: `avg(sum(hami_core_used{node=~"$node"}) by (instance) / sum(hami_core_size{node=~"$node"}) by (instance) * 100)`,
total: 0,
used: 0,
unit: '核',
@ -250,7 +250,7 @@ const gaugeConfig = useInstantVector(
percent: 0,
query: ``,
totalQuery: ``,
percentQuery: `avg(sum(hami_container_vmemory_allocated{node=~"$node"}) by (instance) / sum(hami_memory_size{node=~"$node"}) by (instance) * 100)`,
percentQuery: `avg(sum(hami_memory_used{node=~"$node"}) by (instance) / sum(hami_memory_size{node=~"$node"}) by (instance) * 100)`,
total: 0,
used: 0,
unit: 'GiB',
@ -264,6 +264,7 @@ const detailColumns = [
{
label: '节点状态',
value: 'status',
width: 100,
render: ({ isSchedulable, isExternal }) => {
if (detail.value && detail.value.isSchedulable !== undefined) {
return (

@ -1,4 +1,8 @@
import { timeParse } from '@/utils';
import {
timeParse,
formatSmartPercentage,
getFirstNonEmptyArray,
} from '@/utils';
export const getRangeOptions = ({
core = [],
@ -6,6 +10,8 @@ export const getRangeOptions = ({
cpu = [],
internal = [],
}) => {
const xData = getFirstNonEmptyArray([core, memory, cpu, internal]);
return {
legend: {
// data: [],
@ -22,7 +28,8 @@ export const getRangeOptions = ({
params[i].marker +
params[i].seriesName +
' : ' +
(+params[i].value).toFixed(0) +
// (+params[i].value).toFixed(0) +
formatSmartPercentage(params[i].value) +
`%<br/>`;
}
@ -32,12 +39,12 @@ export const getRangeOptions = ({
grid: {
top: 37, // 上边距
bottom: 20, // 下边距
left: '7%', // 左边距
left: '10%', // 左边距
right: 10, // 右边距
},
xAxis: {
type: 'category',
data: core.map((item) => timeParse(+item.timestamp)),
data: xData.map((item) => timeParse(+item.timestamp)),
axisLabel: {
formatter: function (value) {
return timeParse(value, 'HH:mm');

@ -25,7 +25,7 @@
</div>
</template>
</div>
<template #footer>
<template v-if="nodeList && nodeList.length > 0" #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button :loading="btnLoading" type="primary" @click="handleOk"></el-button>
</template>
@ -128,6 +128,7 @@ const columns = [
},
{
title: '节点状态',
width: 100,
dataIndex: 'isSchedulable',
render: ({ isSchedulable, isExternal }) => (
<el-tag disable-transitions type={isExternal ? 'warning' : (isSchedulable ? 'success' : 'danger')}>
@ -173,6 +174,7 @@ const columns = [
},
{
title: '所属资源池',
width: 100,
dataIndex: 'resourcePools',
render: ({ resourcePools }) => `${resourcePools.join('、')}`,
},

@ -1,6 +1,6 @@
<template>
<back-header>
资源池管理 > {{ route.query?.name || '' }}
资源池管理 > {{ route.query?.poolName || '' }}
</back-header>
<table-plus v-loading="loading" :dataSource="list" :columns="columns" :rowAction="rowAction" :hasPagination="false"
style="margin-bottom: 15px; height: auto;" hideTag ref="table" static :hasActionBar="false">
@ -39,10 +39,6 @@ const data = computed(() => {
return result;
})
watchEffect(() => {
console.log(data.value, 'data')
})
const getList = async () => {
loading.value = true
const res = await pollApi.getDetailNodeList({ pool_id: route.params.uid })
@ -68,6 +64,7 @@ const columns = [
},
{
title: '节点状态',
width: 100,
dataIndex: 'isSchedulable',
render: ({ isSchedulable, isExternal }) => (
<el-tag disable-transitions type={isExternal ? 'warning' : (isSchedulable ? 'success' : 'danger')}>

@ -65,10 +65,6 @@ import { getRangeOptions } from './getOptions';
const props = defineProps(['data'])
watchEffect(() => {
console.log(props.data, 'data2')
})
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000);

@ -1,4 +1,8 @@
import { timeParse } from '@/utils';
import {
timeParse,
formatSmartPercentage,
getFirstNonEmptyArray,
} from '@/utils';
export const getRangeOptions = ({
core = [],
@ -6,6 +10,8 @@ export const getRangeOptions = ({
cpu = [],
internal = [],
}) => {
const xData = getFirstNonEmptyArray([core, memory, cpu, internal]);
return {
legend: {
// data: [],
@ -22,7 +28,8 @@ export const getRangeOptions = ({
params[i].marker +
params[i].seriesName +
' : ' +
(+params[i].value).toFixed(0) +
// (+params[i].value).toFixed(0) +
formatSmartPercentage(params[i].value) +
`%<br/>`;
}
@ -32,12 +39,12 @@ export const getRangeOptions = ({
grid: {
top: 37, // 上边距
bottom: 20, // 下边距
left: '7%', // 左边距
left: '10%', // 左边距
right: 10, // 右边距
},
xAxis: {
type: 'category',
data: core.map((item) => timeParse(+item.timestamp)),
data: xData.map((item) => timeParse(+item.timestamp)),
axisLabel: {
formatter: function (value) {
return timeParse(value, 'HH:mm');

@ -21,7 +21,7 @@
</div>
</div>
<div class="right">
<el-button @click="sendRouteChange(`/admin/vgpu/poll/admin/${poolId}?name=${poolName}`)"
<el-button @click="sendRouteChange(`/admin/vgpu/poll/admin/${poolId}?poolName=${poolName}`)"
type="text">查看详情</el-button>
<template v-if="index === 0 && currentPage === 1">
<el-button @click="sendRouteChange(linkUrl, 'open')" type="text">配置</el-button>
@ -137,10 +137,6 @@ const paginatedList = computed(() => {
return list.value.slice(start, end)
})
// watchEffect(()=>{
// console.log(currentPage.value, pageSize.value, 88)
// })
//
const handleSizeChange = (val) => {
pageSize.value = val

@ -41,7 +41,8 @@
</div>
</block-box>
<block-box v-for="{ title, data } in lineConfig" :key="title" :title="title">
<template v-for="({ title, data }, index) in lineConfig">
<block-box v-if="detail.deviceIds?.length || [2, 3].includes(index)" :key="title + index" :title="title">
<template #extra v-if="detail.type && detail.type.startsWith('NVIDIA')">
<time-picker v-model="times" type="datetimerange" size="small" />
</template>
@ -54,6 +55,7 @@
</template>
</div>
</block-box>
</template>
</template>
<script setup lang="jsx">
@ -298,9 +300,9 @@ onMounted(async () => {
if (foundCard) {
detail.value.type = foundCard.type;
}
if (!detail.value.deviceIds?.length) {
lineConfig.value.splice(0, 2);
}
// if (!detail.value.deviceIds?.length) {
// lineConfig.value.splice(0, 2);
// }
// const start = new Date();
// start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);

@ -58,6 +58,7 @@ const columns = [
title: '任务状态',
dataIndex: 'status',
render: ({ status, deviceIds }) => {
if (!status) return '/';
const enums = {
closed: { text: '已完成', color: '#999' },
success: { text: '运行中', color: '#2563eb' },
@ -109,22 +110,26 @@ const columns = [
},
{
title: '使用者角色',
dataIndex: 'nataskTypeme',
render: ({ taskType }) => taskType === 'big_model' ? '大模型' : '实训',
},
{
title: '用户名',
width: 100,
dataIndex: 'role',
render: ({ role }) => role || '/',
},
{
title: '所属资源池',
dataIndex: 'resourcePools',
render: ({ resourcePools }) => `${resourcePools.join('、')}`,
title: '用户名',
dataIndex: 'username',
render: ({ username }) => username || '/',
},
// {
// title: '',
// width: 100,
// dataIndex: 'resourcePools',
// render: ({ resourcePools }) => `${resourcePools?.length ? resourcePools.join('') : '/'}`,
// },
{
title: '所属节点',
dataIndex: 'nodeName',
render: ({ nodeName }) => nodeName || '/',
},
{
title: 'CPU',
@ -138,6 +143,7 @@ const columns = [
},
{
title: '分配 vGPU',
width: 100,
dataIndex: 'deviceIds',
render: ({ deviceIds }) => {
return (
@ -178,6 +184,7 @@ const columns = [
{
title: '任务创建时间',
width: 140,
dataIndex: 'createTime',
render: ({ createTime }) => timeParse(createTime),
},

@ -63,22 +63,22 @@ const topConfig = [
title: '任务资源申请 Top5',
key: 'apply',
config: [
{
tab: 'CPU',
key: 'cpu',
data: [],
nameKey: 'container_pod_uuid',
unit: '核',
query: 'topk(5, sum by(container_pod_uuid) (hami_container_vcore_allocated))',
},
{
tab: '内存',
key: 'internal',
data: [],
unit: 'GiB',
nameKey: 'container_pod_uuid',
query: 'topk(5, sum by(container_pod_uuid) (hami_container_vmemory_allocated))',
},
// {
// tab: 'CPU',
// key: 'cpu',
// data: [],
// nameKey: 'container_pod_uuid',
// unit: '',
// query: 'topk(5, sum by(container_pod_uuid) (hami_container_vcore_allocated))',
// },
// {
// tab: '',
// key: 'internal',
// data: [],
// unit: 'GiB',
// nameKey: 'container_pod_uuid',
// query: 'topk(5, sum by(container_pod_uuid) (hami_container_vmemory_allocated))',
// },
{
tab: '算力',
key: 'core',

@ -1,6 +1,5 @@
<template>
<el-date-picker
ref="pickerRef"
v-model="value"
:type="type"
range-separator="至"
@ -8,36 +7,24 @@
end-placeholder="结束时间"
unlink-panels
:shortcuts="type.includes('range') && shortcuts"
:teleported="false"
:append-to-body="false"
class="date-picker"
:disabled-date="disabledDate"
@visible-change="onVisibleChange"
v-bind="$attrs"
/>
></el-date-picker>
</template>
<script setup lang="jsx">
import { computed, defineProps, defineEmits, ref } from 'vue';
import { computed, defineProps, defineEmits } from 'vue';
import { ElDatePicker } from 'element-plus';
// props emits
const props = defineProps({
modelValue: {},
type: { type: String, default: 'date' },
parse: { type: Function, default: (times) => times },
});
const emits = defineEmits(['update:modelValue']);
// ref
const pickerRef = ref();
const visible = ref(false);
//
const onVisibleChange = (val) => {
visible.value = val;
};
const emits = defineEmits(['update:modelValue']);
//
const value = computed({
get() {
return props.modelValue;
@ -47,44 +34,105 @@ const value = computed({
},
});
//
function genShortcut(text, hoursAgo) {
return {
text,
const shortcuts = [
{
text: '前 1 小时',
value: () => {
const end = new Date();
const start = new Date(end.getTime() - hoursAgo * 3600 * 1000);
//
setTimeout(() => {
if (visible.value) {
pickerRef.value?.handleClose?.();
}
}, 0);
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 1);
return [start, end];
},
};
}
//
const shortcuts = [
genShortcut('前 1 小时', 1),
genShortcut('前 6 小时', 6),
genShortcut('前 12 小时', 12),
genShortcut('前 1 天', 24),
genShortcut('前 2 天', 48),
genShortcut('前 3 天', 72),
genShortcut('前 1 周', 168),
},
{
text: '前 6 小时',
value: () => {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 6);
return [start, end];
},
},
{
text: '前 12 小时',
value: () => {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 12);
return [start, end];
},
},
{
text: '前 1 天',
value: () => {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24);
return [start, end];
},
},
{
text: '前 2 天',
value: () => {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 2);
return [start, end];
},
},
{
text: '前 3 天',
value: () => {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 3);
return [start, end];
},
},
{
text: '前 1 周',
value: () => {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
return [start, end];
},
},
// {
// text: '',
// value: () => {
// const end = new Date();
// const start = new Date();
// start.setTime(start.getTime() - 3600 * 1000 * 24 * 14);
// return [start, end];
// },
// },
// {
// text: '',
// value: () => {
// const end = new Date();
// const start = new Date();
// start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
// return [start, end];
// },
// },
// {
// text: '',
// value: () => {
// const end = new Date();
// const start = new Date();
// start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
// return [start, end];
// },
// },
];
//
const disabledDate = (time) => {
return time.getTime() >= Date.now();
};
</script>
<style scoped>
<style>
.date-picker {
max-width: 450px;
}

@ -1,5 +1,5 @@
<template>
<div id="content" :style="{ paddingLeft: hasParentWindow || isHome ? 0 : '20px' }">
<div id="content" :class="hasParentWindow && 'impose'">
<!-- <transition name="fade-transform" mode="out-in"> -->
<!-- <keep-alive> -->
<router-view />
@ -26,7 +26,15 @@ const isHome = computed(() => route.fullPath === '/admin/home');
margin: 0;
flex: 1;
overflow: auto;
height: 100%;
padding-left: 20px;
&.impose {
margin: 0 auto;
overflow: inherit;
max-width: 1200px;
padding-left: 0px;
}
}
</style>

@ -1,6 +1,7 @@
<template>
<TopBar v-if="!hasParentWindow" />
<div class="page" :style="{ padding: hasParentWindow ? '20px 30px' : '76px 20px 20px 20px' }">
<div class="page"
:style="{ padding: hasParentWindow ? '20px 30px' : '76px 20px 20px 20px', height: hasParentWindow ? 'auto' : '100vh' }">
<sidebar v-if="!hasParentWindow && !isNoSidebar()" />
<app-main />
</div>

@ -588,3 +588,37 @@ export function parseUrl(url) {
return { pathname, query };
}
export function formatSmartPercentage(value) {
if (value === 0) return '0';
// 整数直接返回
if (Number.isInteger(value)) return value.toString();
const str = value.toString();
const decimal = str.split('.')[1] || '';
// 统计前导0的个数
let leadingZeros = 0;
for (const char of decimal) {
if (char === '0') {
leadingZeros++;
} else {
break;
}
}
const keep = leadingZeros + 2; // 0 的个数 + 1 位有效数字 + 1 位补充位
const rounded = value.toFixed(keep);
return parseFloat(rounded).toString(); // 去除多余的 0 和小数点
}
export function getFirstNonEmptyArray(arrays, defaultValue = []) {
for (const arr of arrays) {
if (arr && arr.length > 0) {
return arr;
}
}
return defaultValue;
}

Loading…
Cancel
Save