用電即時分佈圖表 | 首頁進度小卡 | 系統小卡樣式修改 | 歷史資料新增大類 | 設備管理修改文字

This commit is contained in:
koko 2024-11-05 15:49:19 +08:00
parent c5345db462
commit a7054c07b5
13 changed files with 223 additions and 206 deletions

View File

@ -1,2 +1,3 @@
export const GET_REALTIME_DIST_API = `/api/Energe/GetRealTimeDistribution`;
export const GET_ELECUSE_DAY_API = `/api/Energe/GetElecUseDay`; export const GET_ELECUSE_DAY_API = `/api/Energe/GetElecUseDay`;
export const GET_TAI_POWER_API = `/api/Energe/GetTaipower`; export const GET_TAI_POWER_API = `/api/Energe/GetTaipower`;

View File

@ -1,10 +1,20 @@
import { import {
GET_REALTIME_DIST_API,
GET_ELECUSE_DAY_API, GET_ELECUSE_DAY_API,
GET_TAI_POWER_API, GET_TAI_POWER_API,
} from "./api"; } from "./api";
import instance from "@/util/request"; import instance from "@/util/request";
import apihandler from "@/util/apihandler"; import apihandler from "@/util/apihandler";
export const getRealTimeDist = async () => {
const res = await instance.post(GET_REALTIME_DIST_API);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getElecUseDay = async () => { export const getElecUseDay = async () => {
const res = await instance.post(GET_ELECUSE_DAY_API); const res = await instance.post(GET_ELECUSE_DAY_API);

View File

@ -27,6 +27,7 @@
"title": "历史资料", "title": "历史资料",
"building_name": "厂区", "building_name": "厂区",
"device_name": "设备名称", "device_name": "设备名称",
"system_category": "系统类别",
"device_category": "设备类别", "device_category": "设备类别",
"category": "类别", "category": "类别",
"value": "数值", "value": "数值",
@ -243,7 +244,7 @@
"phone": "手机", "phone": "手机",
"created_at": "建立时间", "created_at": "建立时间",
"operation": "功能", "operation": "功能",
"name_placeholder": "请输入使用者名称", "name_placeholder": "请输入使用者、帐号名称",
"role_placeholder": "请输入角色名称", "role_placeholder": "请输入角色名称",
"change_password": "变更密码", "change_password": "变更密码",
"choose": "选择" "choose": "选择"

View File

@ -27,6 +27,7 @@
"title": "歷史資料", "title": "歷史資料",
"building_name": "廠區", "building_name": "廠區",
"device_name": "設備名稱", "device_name": "設備名稱",
"system_category": "系統類別",
"device_category": "設備類別", "device_category": "設備類別",
"category": "類別", "category": "類別",
"value": "數值", "value": "數值",
@ -243,7 +244,7 @@
"phone": "手機", "phone": "手機",
"created_at": "建立時間", "created_at": "建立時間",
"operation": "功能", "operation": "功能",
"name_placeholder": "請輸入使用者名稱", "name_placeholder": "請輸入使用者、帳號名稱",
"role_placeholder": "請輸入角色名稱", "role_placeholder": "請輸入角色名稱",
"change_password": "變更密碼", "change_password": "變更密碼",
"choose": "選擇" "choose": "選擇"

View File

@ -18,6 +18,7 @@
"title": "Historical Data", "title": "Historical Data",
"building_name": "Building", "building_name": "Building",
"device_name": "Device Name", "device_name": "Device Name",
"system_category": "System Category",
"device_category": "Device Category", "device_category": "Device Category",
"category": "Category", "category": "Category",
"value": "Value", "value": "Value",
@ -155,7 +156,7 @@
"phone": "Phone", "phone": "Phone",
"email": "Email", "email": "Email",
"created_at": "Creation Date", "created_at": "Creation Date",
"maintenance": "Maintenance", "maintenance": "Upkeep",
"repair": "Repair", "repair": "Repair",
"company_info": "Company Info", "company_info": "Company Info",
"repair_item": "Repair Item", "repair_item": "Repair Item",
@ -243,8 +244,8 @@
"phone": "Phone", "phone": "Phone",
"created_at": "Created Time", "created_at": "Created Time",
"operation": "Function", "operation": "Function",
"name_placeholder": "Please enter user name", "name_placeholder": "Please enter the user's name / account",
"role_placeholder": "Please enter the role name", "role_placeholder": "Please enter the role's name",
"change_password": "Change Password", "change_password": "Change Password",
"choose": "Choose" "choose": "Choose"
}, },
@ -261,8 +262,8 @@
"submit": "Submit", "submit": "Submit",
"edit": "Edit", "edit": "Edit",
"delete": "Delete", "delete": "Delete",
"deselect_all": "Deselect all", "deselect_all": "Deselect All",
"select_all": "Select all", "select_all": "Select All",
"phone_format": "Please enter the correct phone number format", "phone_format": "Please enter the correct phone number format",
"email_format": "Please enter correct email address", "email_format": "Please enter correct email address",
"password_format": "The password must be at least 8 characters long and must contain English and numbers.", "password_format": "The password must be at least 8 characters long and must contain English and numbers.",

View File

@ -147,7 +147,7 @@ const removeAccount = async (id) => {
:placeholder="t('accountManagement.name_placeholder')" :placeholder="t('accountManagement.name_placeholder')"
name="Full_name" name="Full_name"
:value="searchData" :value="searchData"
class="mr-3" class="mr-3 w-96"
/> />
<Input <Input
:placeholder="t('accountManagement.role_placeholder')" :placeholder="t('accountManagement.role_placeholder')"

View File

@ -1,93 +1,87 @@
<script setup> <script setup>
import { ref, computed } from "vue"; import { ref, computed } from "vue";
// Mock data based on the image, grouped data const equipmentData = ref({
const mockData = ref([ title: "System Status",
{ items: [
title: "Abnormal state", { label: "Auxiliary", online: 6, offline: 0, alarm: 0 },
items: [ { label: "Air Detection", online: 31, offline: 0, alarm: 2 },
[ { label: "Electricity", online: 12, offline: 0, alarm: 1 },
{ label: "Abnormal", value: 0 }, { label: "Lighting", online: 20, offline: 3, alarm: 0 },
{ label: "Return", value: 5168 }, { label: "Air Condition", online: 23, offline: 0, alarm: 0 },
], ],
[ });
{ label: "Confirmed", value: 182 },
{ label: "Unacknowledged", value: 4986 },
],
],
},
{
title: "Progress",
items: [
[
{ label: "Not dispatched", value: 5126 },
{ label: "Dispatched", value: 42 },
],
[
{ label: "Completed", value: 28 },
{ label: "Unfinished", value: 14 },
],
],
},
]);
// Compute progress value for each group const orderData = ref({
const getProgressValue = (group) => { title: "Work Order",
const total = group.reduce((sum, item) => sum + item.value, 0); items: [
return (group[0].value / total) * 100; { label: "Unassigned", value: 2 },
}; { label: "Assigned", value: 4 },
{ label: "Completed", value: 1 },
],
});
</script> </script>
<template> <template>
<div class="flex flex-wrap"> <div class="flex flex-wrap">
<div <div class="w-full sm:w-3/5 state-box-col relative ps-2">
v-for="(section, index) in mockData"
:key="index"
class="w-full sm:w-1/2 state-box-col relative px-4"
>
<div class="state-box"> <div class="state-box">
<div class="title"> <div class="title">
<img class="state-title01" src="@ASSET/img/state-title01.svg" /> <img class="state-title01" src="@ASSET/img/state-title01.svg" />
<span>{{ section.title }}</span> <span class="">{{ equipmentData.title }}</span>
<img class="state-title02" src="@ASSET/img/state-title02.svg" /> <img class="state-title02" src="@ASSET/img/state-title02.svg" />
</div> </div>
<table class="table table-sm text-center">
<div <thead>
v-for="(group, groupIndex) in section.items" <tr class="border-cyan-400 text-cyan-100">
:key="groupIndex" <th></th>
class="item" <th>Online</th>
> <th>Offline</th>
<div class="item-title"> <th>Alarm</th>
<div </tr>
v-for="(item, itemIndex) in group" </thead>
:key="itemIndex" <tbody>
class="text" <tr
v-for="(item, index) in equipmentData.items"
:key="index"
class="border-cyan-400"
> >
<div class="text-position"> <th class="px-0 text-start">{{ item.label }}</th>
<span>{{ item.label }}</span> <td>{{ item.online }}</td>
<span>{{ item.value }}</span> <td>{{ item.offline }}</td>
</div> <td>{{ item.alarm }}</td>
</div> </tr>
</div> </tbody>
<div class="state-ul"> </table>
<img src="@ASSET/img/state-ul.svg" /> </div>
<div class="box"> </div>
<div class="mark"> <div class="w-full sm:w-2/5 state-box-col relative ps-2">
<span class="w-10">{{ group[0].value }}</span> <div class="state-box">
<span <div class="title">
><img class="w-[50px]" src="@ASSET/img/state-ul-text.svg" /> <img class="state-title01" src="@ASSET/img/state-title01.svg" />
</span> <span>{{ orderData.title }}</span>
<span class="w-10">{{ group[1].value }}</span> <img class="state-title02" src="@ASSET/img/state-title02.svg" />
</div>
<progress
class="progress [&::-webkit-progress-value]:bg-red-600 [&::-moz-progress-bar]:bg-red-600"
:value="getProgressValue(group)"
max="100"
size=""
></progress>
</div>
</div>
</div> </div>
<table class="table table-sm text-center">
<thead>
<tr class="border-cyan-400 text-cyan-100">
<th></th>
<th>value</th>
</tr>
</thead>
<tbody>
<tr
v-for="(item, index) in orderData.items"
:key="index"
class="border-cyan-400"
>
<th class="px-0 text-start">
<span>{{ item.label }}</span>
</th>
<td>{{ item.value }}</td>
</tr>
</tbody>
</table>
</div> </div>
</div> </div>
</div> </div>
@ -107,7 +101,7 @@ const getProgressValue = (group) => {
} }
.state-box { .state-box {
@apply border-2 border-light-info rounded-sm py-2 px-6 text-white relative; @apply h-80 border-2 border-light-info rounded-sm py-2 px-6 text-white relative;
} }
.state-box:after { .state-box:after {
@ -133,58 +127,4 @@ const getProgressValue = (group) => {
.state-box .title .state-title02 { .state-box .title .state-title02 {
@apply w-5 ml-1.5; @apply w-5 ml-1.5;
} }
.state-box .item-title {
@apply flex justify-between items-center m-auto mb-1 relative;
}
.state-box .item-title:after {
@apply absolute right-0 -bottom-2.5 w-full h-4 bg-no-repeat bg-center z-10;
content: "";
background-image: url(@ASSET/img/text-position-line.svg);
}
.state-box .item-title .text {
@apply w-1/2 m-0 mb-1.5 flex justify-center items-center relative;
}
.state-box .item-title .text .text-position span {
@apply block text-center text-xs;
}
.state-box .item-title .text .text-position span:nth-child(2) {
text-shadow: 0px 0px 5px rgba(255, 255, 255, 0.8);
}
.mark {
@apply flex justify-between items-center mb-2.5;
}
.state-ul {
@apply relative mb-2.5;
}
.state-ul::after {
@apply absolute -left-3.5 top-0 w-4 h-16 bg-no-repeat bg-center z-10;
content: "";
background-image: url(@ASSET/img/state-ul-background01.svg);
}
.state-ul .box {
@apply absolute top-1/2 w-4/5 left-0 right-0 m-auto text-center -translate-y-1/2;
}
.progress {
@apply w-full h-3 rounded;
appearance: none;
&::-webkit-progress-bar {
border: 1px solid #ffffff;
background-color: #5eabea;
}
&::-webkit-progress-value {
border-radius: 4px;
}
}
</style> </style>

View File

@ -1,35 +1,29 @@
<script setup> <script setup>
import { ref, onMounted, nextTick } from "vue"; import { ref, onMounted, nextTick, computed } from "vue";
import * as echarts from "echarts"; import * as echarts from "echarts";
import { getRealTimeDist } from "@/apis/energy";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const chartDiv = ref(null); const chartDiv = ref(null);
const chartOption = { const chartOption = {
tooltip: { tooltip: {
trigger: "item", trigger: "item",
formatter: "{b}: {c}kWh", formatter: (p) => {
return `${p.name}: ${p.value}kWh (${p.data.percentage}%)`;
},
}, },
series: [ series: [
{ {
type: "sankey", type: "sankey",
layout: "none", layout: "none",
nodeWidth: 20, nodeWidth: 10,
nodeGap: 10, nodeGap: 10,
data: [ right: 180,
{ name: "Total", value: 100 }, data: [],
{ name: "HVAC System", value: 40 }, links: [],
{ name: "Lighting System", value: 25 },
{ name: "Elevator System", value: 15 },
{ name: "Outlets", value: 10 },
{ name: "Others", value: 10 },
],
links: [
{ source: "Total", target: "HVAC System", value: 40 },
{ source: "Total", target: "Lighting System", value: 25 },
{ source: "Total", target: "Elevator System", value: 15 },
{ source: "Total", target: "Outlets", value: 10 },
{ source: "Total", target: "Others", value: 10 },
],
emphasis: { emphasis: {
focus: "adjacency", focus: "adjacency",
}, },
@ -37,6 +31,9 @@ const chartOption = {
position: "right", position: "right",
fontSize: 14, fontSize: 14,
color: "#fff", color: "#fff",
formatter: (p) => {
return `${p.name} (${p.data.percentage}%)`;
},
}, },
itemStyle: { itemStyle: {
borderWidth: 0, borderWidth: 0,
@ -50,11 +47,42 @@ const chartOption = {
], ],
}; };
onMounted(() => { const loadData = async () => {
nextTick(() => { const res = await getRealTimeDist();
if (res.isSuccess) {
const rawData = res.data;
const totalValue = rawData.reduce((acc, item) => acc + item.value, 0);
// data
const data = [
{ name: "Total", value: totalValue, percentage: 100 },
...rawData.map((item) => ({
name: item.key,
value: item.value,
percentage: item.percentage,
})),
];
// links
const links = rawData.map((item) => ({
source: "Total",
target: item.key,
value: item.value,
percentage: item.percentage,
}));
// chartOption
chartOption.series[0].data = data;
chartOption.series[0].links = links;
//
const myChart = echarts.init(chartDiv.value); const myChart = echarts.init(chartDiv.value);
myChart.setOption(chartOption); myChart.setOption(chartOption);
}); }
};
onMounted(() => {
loadData();
}); });
</script> </script>
@ -64,19 +92,7 @@ onMounted(() => {
{{ $t("energy.elec_consumption") }} {{ $t("energy.elec_consumption") }}
</h2> </h2>
<div class="chart-container"> <div class="chart-container">
<div ref="chartDiv" class="w-full min-h-[190px] h-full"></div> <div ref="chartDiv" class="w-full min-h-[200px] h-full"></div>
</div>
<div class="text-sm mt-3.5">
<ul class="flex flex-wrap items-center text-white">
<li class="pr-5 relative z-20">
<span class="pr-3.5"> {{ $t("energy.total_elec") }} (kWh)</span>
<span class="pr-3.5">160.05</span>
</li>
<li class="pr-5 relative z-20">
<span class="pr-3.5">{{ $t("energy.green_elec") }} (kWh)</span>
<span class="pr-3.5">39.50</span>
</li>
</ul>
</div> </div>
</div> </div>
</template> </template>

View File

@ -25,8 +25,15 @@ import dayjs from "dayjs";
const { searchParams, changeParams } = useSearchParam(); const { searchParams, changeParams } = useSearchParam();
//
const store = useBuildingStore(); const store = useBuildingStore();
//
const {
items: sysMainTagItems,
changeActiveBtn: changeMainSysActiveBtn,
setItems: setMainSysItems,
selectedBtn: selectedMainSysItems,
} = useActiveBtn();
//
const { const {
items: sysTagItems, items: sysTagItems,
changeActiveBtn: changeSysActiveBtn, changeActiveBtn: changeSysActiveBtn,
@ -34,6 +41,38 @@ const {
selectedBtn: selectedSysItems, selectedBtn: selectedSysItems,
} = useActiveBtn(); } = useActiveBtn();
watch(
() => store.mainSys,
() => {
setMainSysItems(
store.mainSubSys.map(({ full_name, main_system_tag }, index) => ({
title: full_name,
key: main_system_tag,
active: searchParams.value.main_system_tag
? searchParams.value.main_system_tag === mian_system_tag
: index === 0,
}))
);
}
);
watch(
() => selectedMainSysItems,
(newVal, oldVal) => {
setSysItems(
store.subSys.filter((s) => s.main_system_tag === newVal.value?.key).map(({ full_name, sub_system_tag }, index) => ({
title: full_name,
key: sub_system_tag,
active: index === 0,
}))
);
},
{
deep: true,
immediate: true,
}
);
watch( watch(
() => selectedSysItems, () => selectedSysItems,
(newVal, oldVal) => { (newVal, oldVal) => {
@ -54,21 +93,6 @@ watch(
} }
); );
watch(
() => store.subSys,
() => {
setSysItems(
store.subSys.map(({ full_name, sub_system_tag }, index) => ({
title: full_name,
key: sub_system_tag,
active: searchParams.value.sub_system_tag
? searchParams.value.sub_system_tag === sub_system_tag
: index === 0,
}))
);
}
);
// //
const { const {
items: points, items: points,
@ -82,7 +106,7 @@ const getPoint = async (deviceList) => {
setPoints( setPoints(
res.data.map((d, index) => ({ res.data.map((d, index) => ({
...d, ...d,
title: d.points, title: d.item_name,
key: d.points, key: d.points,
active: index === 0, active: index === 0,
})) }))
@ -118,10 +142,10 @@ watch(searchParams, (newVal, oldValue) => {
}); });
onMounted(() => { onMounted(() => {
setSysItems( setMainSysItems(
store.subSys.map(({ full_name, sub_system_tag }, index) => ({ store.mainSubSys.map(({ full_name, main_system_tag }, index) => ({
title: full_name, title: full_name,
key: sub_system_tag, key: main_system_tag,
active: index === 0, active: index === 0,
})) }))
); );
@ -136,7 +160,23 @@ onBeforeMount(() => {
<div class="flex flex-col custom-border p-4 mb-4"> <div class="flex flex-col custom-border p-4 mb-4">
<!-- <HistoryFavoriteOption class="mb-4" />--> <!-- <HistoryFavoriteOption class="mb-4" />-->
<div class="flex items-center gap-4 mb-4"> <div class="flex items-center gap-4 mb-4">
<h2 class="text-lg font-bold ps-2">{{ $t("history.device_category") }} :</h2> <h2 class="text-lg font-bold ps-2">
{{ $t("history.system_category") }} :
</h2>
<ButtonGroup
:items="sysMainTagItems"
:withLine="true"
:onclick="
(e, item) => {
changeMainSysActiveBtn(item);
}
"
/>
</div>
<div class="flex items-center gap-4 mb-4">
<h2 class="text-lg font-bold ps-2">
{{ $t("history.device_category") }} :
</h2>
<ButtonGroup <ButtonGroup
:items="sysTagItems" :items="sysTagItems"
:withLine="true" :withLine="true"

View File

@ -65,9 +65,12 @@ watch(searchParams, (newValue, oldValue) => {
</script> </script>
<template> <template>
<div class="flex flex-wrap mt-3"> <div class="flex flex-wrap items-center mt-6">
<h2 class="text-lg font-bold ps-1 pe-4">
{{ $t("history.device_category") }} :
</h2>
<button <button
class="btn btn-success mr-4" class="btn btn-sm btn-success"
@click.stop.prevent="changeCheckedItem" @click.stop.prevent="changeCheckedItem"
> >
{{ checkedItem.length === store.subSys.length ? t("button.deselect_all") : t("button.select_all") }} {{ checkedItem.length === store.subSys.length ? t("button.deselect_all") : t("button.select_all") }}

View File

@ -216,7 +216,7 @@ provide("system_selectedDevice", { selectedDeviceRealtime, selectedDevice, getCu
<SystemInfoModal :data="selectedDevice" /> <SystemInfoModal :data="selectedDevice" />
<SystemFloorBar /> <SystemFloorBar />
<div class="grid grid-cols-2 gap-5 mt-8 mb-4"> <div class="grid grid-cols-2 gap-5 mt-8 mb-4">
<div class="col-span-1 h-[80vh] flex flex-col justify-start"> <div class="col-span-1 h-[79vh] flex flex-col justify-start">
<div> <div>
<div class="flex mb-4 items-center"> <div class="flex mb-4 items-center">
<span class="flex items-center mr-3" v-if="statusList?.device_normal_text"> <span class="flex items-center mr-3" v-if="statusList?.device_normal_text">
@ -237,13 +237,13 @@ provide("system_selectedDevice", { selectedDeviceRealtime, selectedDevice, getCu
</div> </div>
<SystemSubBar class="mt-2 mb-4" /> <SystemSubBar class="mt-2 mb-4" />
</div> </div>
<div class="h-full max-h-[75vh] pr-2 overflow-y-auto"> <div class="h-full pr-2 overflow-y-auto">
<RouterView /> <RouterView />
</div> </div>
</div> </div>
<div class="col-span-1 h-full flex flex-col justify-between"> <div class="col-span-1 h-full flex flex-col justify-between">
<SystemMode /> <SystemMode />
<div class="min-h-[75vh] relative"> <div class="h-full relative">
<SystemFloor <SystemFloor
:class="twMerge('absolute h-full w-full', route.query.mode === '2D' ? 'opacity-100 z-10' : 'opacity-0 z-0')" /> :class="twMerge('absolute h-full w-full', route.query.mode === '2D' ? 'opacity-100 z-10' : 'opacity-0 z-0')" />
<div <div

View File

@ -22,14 +22,14 @@ const fitToView = (forge_dbid) => {
<div class="equipment-show" v-for="d in showData" :key="d.full_name"> <div class="equipment-show" v-for="d in showData" :key="d.full_name">
<template v-if="d.device_list.length > 0"> <template v-if="d.device_list.length > 0">
<p class="title">{{ d.full_name }}</p> <p class="title">{{ d.full_name }}</p>
<div class="grid grid-cols-3 gap-5"> <div class="grid grid-cols-1 lg:grid-cols-2 gap-5">
<div class="col-auto relative" v-for="device in d.device_list" :key="device.device_guid"> <div class="col-auto relative" v-for="device in d.device_list" :key="device.device_guid">
<div class="item h-full"> <div class="item h-full">
<div class="left h-full flex flex-wrap justify-center"> <div class="left h-full flex flex-wrap justify-center">
<div class="sec02 w-full"> <div class="sec02 w-full">
<img v-if="device.device_image_url" :src="device.device_image_url" alt="" class="w-8 h-8"> <img v-if="device.device_image_url" :src="device.device_image_url" alt="" >
<span class="w-8 h-8" v-else></span> <span v-else></span>
<span class="w-32 break-all">{{ device.full_name }}</span> <span>{{ device.full_name }}</span>
</div> </div>
<div class="flex justify-between w-full self-end"> <div class="flex justify-between w-full self-end">
<div class="sec03"> <div class="sec03">
@ -60,7 +60,7 @@ const fitToView = (forge_dbid) => {
} }
.item { .item {
@apply flex items-center border border-success py-4 px-5 relative mb-5 after:absolute after:right-0 after:top-3 after:w-6 after:h-10 after:bg-[url(/src/assets/img/equipment/state-background.svg)] after:z-10; @apply flex items-center border border-success rounded shadow-emerald-600 shadow-inner py-4 px-5 relative mb-5 after:absolute after:right-0 after:top-3 after:w-6 after:h-10 after:bg-[url(/src/assets/img/equipment/state-background.svg)] after:z-10;
; ;
} }
@ -105,7 +105,7 @@ const fitToView = (forge_dbid) => {
background: url(/src/assets/img/equipment/state-title.svg) center center; background: url(/src/assets/img/equipment/state-title.svg) center center;
position: absolute; position: absolute;
right: 0; right: 0;
bottom: -10px; bottom: -18px;
height: 25px; height: 25px;
width: 105px; width: 105px;
background-repeat: no-repeat; background-repeat: no-repeat;
@ -118,10 +118,14 @@ const fitToView = (forge_dbid) => {
display: block; display: block;
border-radius: 5px; border-radius: 5px;
margin-right: 10px; margin-right: 10px;
width: 2rem !important;
height: 2rem;
} }
.equipment-show .item .sec02 span:nth-child(2) { .equipment-show .item .sec02 span:nth-child(2) {
font-size: 1.5rem; font-size: 1.4rem;
width: calc(100% - 2rem);
word-break: break-all;
} }
.equipment-show .item .sec03 { .equipment-show .item .sec03 {

View File

@ -16,7 +16,7 @@ const data = computed(() => {
const columns = [{ const columns = [{
title: t("system.attribute"), title: t("system.attribute"),
key: "points" key: "full_name"
}, },
{ {
title: t("system.value"), title: t("system.value"),