Migrate all Vue components from Options API to <script setup>

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 17:10:06 +08:00
parent a619be7881
commit 3b7b6ae859
61 changed files with 10835 additions and 11750 deletions

View File

@@ -1,5 +1,5 @@
<template>
<Dialog :visible="listModal" @update:visible="$emit('closeModal', $event)" modal :style="{ width: '90vw', height: '90vh' }" :contentClass="contentClass">
<Dialog :visible="listModal" @update:visible="emit('closeModal', $event)" modal :style="{ width: '90vw', height: '90vh' }" :contentClass="contentClass">
<template #header>
<div class=" py-5">
<p class="text-base font-bold">Non-conformance Issue</p>
@@ -61,219 +61,225 @@
</div>
</Dialog>
</template>
<script>
<script setup>
import { ref, computed, watch, nextTick, useTemplateRef } from 'vue';
import { storeToRefs } from 'pinia';
import { useConformanceStore } from '@/stores/conformance';
import cytoscapeMapTrace from '@/module/cytoscapeMapTrace.js';
export default {
props: ['listModal', 'listNo', 'traceId', 'firstCases', 'listTraces', 'taskSeq', 'cases', 'category'],
setup() {
const conformanceStore = useConformanceStore();
const { infinite404 } = storeToRefs(conformanceStore);
const props = defineProps(['listModal', 'listNo', 'traceId', 'firstCases', 'listTraces', 'taskSeq', 'cases', 'category']);
const emit = defineEmits(['closeModal']);
return { infinite404, conformanceStore }
},
data() {
const conformanceStore = useConformanceStore();
const { infinite404 } = storeToRefs(conformanceStore);
// template ref
const cfmTrace = useTemplateRef('cfmTrace');
// data
const contentClass = ref('!bg-neutral-100 border-t border-neutral-300 h-full');
const showTraceId = ref(null);
const infiniteData = ref(null);
const maxItems = ref(false);
const infiniteFinish = ref(true); // 無限滾動是否載入完成
const startNum = ref(0);
const processMap = ref({
nodes:[],
edges:[],
});
// computed
const traceTotal = computed(() => {
return traceList.value.length;
});
const traceList = computed(() => {
const sum = props.listTraces.map(trace => trace.count).reduce((acc, cur) => acc + cur, 0);
return props.listTraces.map(trace => {
return {
contentClass: '!bg-neutral-100 border-t border-neutral-300 h-full',
showTraceId: null,
infiniteData: null,
maxItems: false,
infiniteFinish: true, // 無限滾動是否載入完成
startNum: 0,
processMap:{
nodes:[],
edges:[],
},
}
},
computed: {
traceTotal: function() {
return this.traceList.length;
},
traceList: function() {
const sum = this.listTraces.map(trace => trace.count).reduce((acc, cur) => acc + cur, 0);
id: trace.id,
value: Number((getPercentLabel(trace.count / sum))),
count: trace.count.toLocaleString('en-US'),
count_base: trace.count,
ratio: getPercentLabel(trace.count / sum),
};
}).sort((x, y) => x.id - y.id);
});
return this.listTraces.map(trace => {
return {
id: trace.id,
value: Number((this.getPercentLabel(trace.count / sum))),
count: trace.count.toLocaleString('en-US'),
count_base: trace.count,
ratio: this.getPercentLabel(trace.count / sum),
};
}).sort((x, y) => x.id - y.id);
},
caseData: function() {
if(this.infiniteData !== null){
const data = JSON.parse(JSON.stringify(this.infiniteData)); // 深拷貝原始 cases 的內容
data.forEach(item => {
item.facets.forEach((facet, index) => {
item[`fac_${index}`] = facet.value; // 建立新的 key-value pair
});
delete item.facets; // 刪除原本的 facets 屬性
item.attributes.forEach((attribute, index) => {
item[`att_${index}`] = attribute.value; // 建立新的 key-value pair
});
delete item.attributes; // 刪除原本的 attributes 屬性
})
return data;
}
},
columnData: function() {
const data = JSON.parse(JSON.stringify(this.cases)); // 深拷貝原始 cases 的內容
const facetName = facName => facName.trim().replace(/^(.)(.*)$/, (match, firstChar, restOfString) => firstChar.toUpperCase() + restOfString.toLowerCase());
const result = [
{ field: 'id', header: 'Case Id' },
{ field: 'started_at', header: 'Start time' },
{ field: 'completed_at', header: 'End time' },
...data[0].facets.map((fac, index) => ({ field: `fac_${index}`, header: facetName(fac.name) })),
...data[0].attributes.map((att, index) => ({ field: `att_${index}`, header: att.key })),
];
return result
},
},
watch: {
listModal: function(newValue) { // 第一次打開 Modal 要繪圖
if(newValue) this.createCy();
},
taskSeq: function(newValue){
if (newValue !== null) this.createCy();
},
traceId: function(newValue) {
// 當 traceId 屬性變化時更新 showTraceId
this.showTraceId = newValue;
},
showTraceId: function(newValue, oldValue) {
const isScrollTop = document.querySelector('.infiniteTable');
if(isScrollTop && typeof isScrollTop.scrollTop !== 'undefined') if(newValue !== oldValue) isScrollTop.scrollTop = 0;
},
firstCases: function(newValue, oldValue){
this.infiniteData = newValue;
},
infinite404: function(newValue, oldValue){
if (newValue === 404) this.maxItems = true;
},
},
methods: {
/**
* Number to percentage
* @param {number} val 原始數字
* @returns {string} 轉換完成的百分比字串
*/
getPercentLabel(val){
if((val * 100).toFixed(1) >= 100) return 100;
else return parseFloat((val * 100).toFixed(1));
},
/**
* set progress bar width
* @param {number} value 百分比數字
* @returns {string} 樣式的寬度設定
*/
progressWidth(value){
return `width:${value}%;`
},
/**
* switch case data
* @param {number} id case id
*/
async switchCaseData(id) {
if(id == this.showTraceId) return;
this.infinite404 = null;
this.maxItems = false;
this.startNum = 0;
let result;
if(this.category === 'issue') result = await this.conformanceStore.getConformanceTraceDetail(this.listNo, id, 0);
else if(this.category === 'loop') result = await this.conformanceStore.getConformanceLoopsTraceDetail(this.listNo, id, 0);
this.infiniteData = await result;
this.showTraceId = id; // 放 getDetail 為了 case table 載入完再切換 showTraceId
},
/**
* 將 trace element nodes 資料彙整
*/
setNodesData(){
// 避免每次渲染都重複累加
this.processMap.nodes = [];
// 將 api call 回來的資料帶進 node
if(this.taskSeq !== null) {
this.taskSeq.forEach((node, index) => {
this.processMap.nodes.push({
data: {
id: index,
label: node,
backgroundColor: '#CCE5FF',
bordercolor: '#003366',
shape: 'round-rectangle',
height: 80,
width: 100
}
});
});
};
},
/**
* 將 trace edge line 資料彙整
*/
setEdgesData(){
this.processMap.edges = [];
if(this.taskSeq !== null) {
this.taskSeq.forEach((edge, index) => {
this.processMap.edges.push({
data: {
source: `${index}`,
target: `${index + 1}`,
lineWidth: 1,
style: 'solid'
}
});
});
};
// 關係線數量筆節點少一個
this.processMap.edges.pop();
},
/**
* create trace cytoscape's map
*/
createCy(){
this.$nextTick(() => {
const graphId = this.$refs.cfmTrace;
this.setNodesData();
this.setEdgesData();
if(graphId !== null) cytoscapeMapTrace(this.processMap.nodes, this.processMap.edges, graphId);
const caseData = computed(() => {
if(infiniteData.value !== null){
const data = JSON.parse(JSON.stringify(infiniteData.value)); // 深拷貝原始 cases 的內容
data.forEach(item => {
item.facets.forEach((facet, index) => {
item[`fac_${index}`] = facet.value; // 建立新的 key-value pair
});
},
/**
* 無限滾動: 載入數據
*/
async fetchData() {
try {
this.infiniteFinish = false;
this.startNum += 20
const result = await this.conformanceStore.getConformanceTraceDetail(this.listNo, this.showTraceId, this.startNum);
this.infiniteData = await [...this.infiniteData, ...result];
this.infiniteFinish = await true;
} catch(error) {
console.error('Failed to load data:', error);
}
},
/**
* 無限滾動: 監聽 scroll 有沒有滾到底部
* @param {element} event 監聽時回傳的事件
*/
handleScroll(event) {
if(this.maxItems || this.infiniteData.length < 20 || this.infiniteFinish === false) return;
delete item.facets; // 刪除原本的 facets 屬性
const container = event.target;
const overScrollHeight = container.scrollTop + container.clientHeight + 20 >= container.scrollHeight;
item.attributes.forEach((attribute, index) => {
item[`att_${index}`] = attribute.value; // 建立新的 key-value pair
});
delete item.attributes; // 刪除原本的 attributes 屬性
})
return data;
}
});
if (overScrollHeight) this.fetchData();
},
},
const columnData = computed(() => {
const data = JSON.parse(JSON.stringify(props.cases)); // 深拷貝原始 cases 的內容
const facetName = facName => facName.trim().replace(/^(.)(.*)$/, (match, firstChar, restOfString) => firstChar.toUpperCase() + restOfString.toLowerCase());
const result = [
{ field: 'id', header: 'Case Id' },
{ field: 'started_at', header: 'Start time' },
{ field: 'completed_at', header: 'End time' },
...data[0].facets.map((fac, index) => ({ field: `fac_${index}`, header: facetName(fac.name) })),
...data[0].attributes.map((att, index) => ({ field: `att_${index}`, header: att.key })),
];
return result
});
// watch
watch(() => props.listModal, (newValue) => { // 第一次打開 Modal 要繪圖
if(newValue) createCy();
});
watch(() => props.taskSeq, (newValue) => {
if (newValue !== null) createCy();
});
watch(() => props.traceId, (newValue) => {
// 當 traceId 屬性變化時更新 showTraceId
showTraceId.value = newValue;
});
watch(showTraceId, (newValue, oldValue) => {
const isScrollTop = document.querySelector('.infiniteTable');
if(isScrollTop && typeof isScrollTop.scrollTop !== 'undefined') if(newValue !== oldValue) isScrollTop.scrollTop = 0;
});
watch(() => props.firstCases, (newValue) => {
infiniteData.value = newValue;
});
watch(infinite404, (newValue) => {
if (newValue === 404) maxItems.value = true;
});
// methods
/**
* Number to percentage
* @param {number} val 原始數字
* @returns {string} 轉換完成的百分比字串
*/
function getPercentLabel(val){
if((val * 100).toFixed(1) >= 100) return 100;
else return parseFloat((val * 100).toFixed(1));
}
/**
* set progress bar width
* @param {number} value 百分比數字
* @returns {string} 樣式的寬度設定
*/
function progressWidth(value){
return `width:${value}%;`
}
/**
* switch case data
* @param {number} id case id
*/
async function switchCaseData(id) {
if(id == showTraceId.value) return;
infinite404.value = null;
maxItems.value = false;
startNum.value = 0;
let result;
if(props.category === 'issue') result = await conformanceStore.getConformanceTraceDetail(props.listNo, id, 0);
else if(props.category === 'loop') result = await conformanceStore.getConformanceLoopsTraceDetail(props.listNo, id, 0);
infiniteData.value = await result;
showTraceId.value = id; // 放 getDetail 為了 case table 載入完再切換 showTraceId
}
/**
* 將 trace element nodes 資料彙整
*/
function setNodesData(){
// 避免每次渲染都重複累加
processMap.value.nodes = [];
// 將 api call 回來的資料帶進 node
if(props.taskSeq !== null) {
props.taskSeq.forEach((node, index) => {
processMap.value.nodes.push({
data: {
id: index,
label: node,
backgroundColor: '#CCE5FF',
bordercolor: '#003366',
shape: 'round-rectangle',
height: 80,
width: 100
}
});
});
};
}
/**
* 將 trace edge line 資料彙整
*/
function setEdgesData(){
processMap.value.edges = [];
if(props.taskSeq !== null) {
props.taskSeq.forEach((edge, index) => {
processMap.value.edges.push({
data: {
source: `${index}`,
target: `${index + 1}`,
lineWidth: 1,
style: 'solid'
}
});
});
};
// 關係線數量筆節點少一個
processMap.value.edges.pop();
}
/**
* create trace cytoscape's map
*/
function createCy(){
nextTick(() => {
const graphId = cfmTrace.value;
setNodesData();
setEdgesData();
if(graphId !== null) cytoscapeMapTrace(processMap.value.nodes, processMap.value.edges, graphId);
});
}
/**
* 無限滾動: 載入數據
*/
async function fetchData() {
try {
infiniteFinish.value = false;
startNum.value += 20
const result = await conformanceStore.getConformanceTraceDetail(props.listNo, showTraceId.value, startNum.value);
infiniteData.value = await [...infiniteData.value, ...result];
infiniteFinish.value = await true;
} catch(error) {
console.error('Failed to load data:', error);
}
}
/**
* 無限滾動: 監聽 scroll 有沒有滾到底部
* @param {element} event 監聽時回傳的事件
*/
function handleScroll(event) {
if(maxItems.value || infiniteData.value.length < 20 || infiniteFinish.value === false) return;
const container = event.target;
const overScrollHeight = container.scrollTop + container.clientHeight + 20 >= container.scrollHeight;
if (overScrollHeight) fetchData();
}
</script>
<style scoped>