Translate all Chinese comments and strings to English

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 20:03:19 +08:00
parent 7d5918837b
commit 1d621bf304
57 changed files with 499 additions and 499 deletions

View File

@@ -7,7 +7,7 @@
Base CSS layer with global font, validation, height, Base CSS layer with global font, validation, height,
and PrimeVue sidebar overrides. */ and PrimeVue sidebar overrides. */
/* 全域字型 */ /* Global font */
@layer base { @layer base {
html { html {
font-family: 'Roboto', sans-serif, system-ui; font-family: 'Roboto', sans-serif, system-ui;

View File

@@ -7,7 +7,7 @@
Tailwind CSS theme configuration with custom colors, Tailwind CSS theme configuration with custom colors,
font sizes, breakpoints, and animations. */ font sizes, breakpoints, and animations. */
/* 引入 Google fonts */ /* Import Google fonts */
@import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap');
@import "tailwindcss" layer(tailwind-base); @import "tailwindcss" layer(tailwind-base);

View File

@@ -69,7 +69,7 @@
.p-inputswitch .p-inputswitch-slider:before { .p-inputswitch .p-inputswitch-slider:before {
@apply !w-5 !h-5 !left-0.5 @apply !w-5 !h-5 !left-0.5
} }
/* slider 滑塊 */ /* slider */
.p-slider .p-slider-handle { .p-slider .p-slider-handle {
@apply !h-3.5 !w-3.5 !border !border-primary @apply !h-3.5 !w-3.5 !border !border-primary
} }
@@ -106,7 +106,7 @@ p-radiobutton-icon */
.p-datatable-resizable > .p-datatable-wrapper { .p-datatable-resizable > .p-datatable-wrapper {
@apply !overflow-x-visible @apply !overflow-x-visible
} }
/* 蓋住 inline style overflow-auto,才能使 thead 定位 */ /* Override inline style overflow-auto so that thead can be positioned */
.p-datatable-wrapper { .p-datatable-wrapper {
overflow: unset !important; overflow: unset !important;
} }

View File

@@ -6,7 +6,7 @@
</span> </span>
</div> </div>
<ul class="w-full min-h-10"> <ul class="w-full min-h-10">
<!-- 這裡不使用迴圈是因為src值用變數的話會沒辦法顯示svg --> <!-- Not using a loop here because SVGs won't display if src is a variable -->
<li v-if="isAdmin" id="btn_acct_mgmt" <li v-if="isAdmin" id="btn_acct_mgmt"
class="w-full h-[40px] flex py-2 px-4 hover:text-[#000000] hover:bg-[#F1F5F9] cursor-pointer class="w-full h-[40px] flex py-2 px-4 hover:text-[#000000] hover:bg-[#F1F5F9] cursor-pointer
items-center" @click="onBtnAcctMgmtClick"> items-center" @click="onBtnAcctMgmtClick">
@@ -105,7 +105,7 @@ const onBtnAcctMgmtClick = () => {
/** Handles logout with unsaved-changes confirmation for Map and Conformance pages. */ /** Handles logout with unsaved-changes confirmation for Map and Conformance pages. */
const onLogoutBtnClick = () => { const onLogoutBtnClick = () => {
if ((route.name === 'Map' || route.name === 'CheckMap') && tempFilterId.value) { if ((route.name === 'Map' || route.name === 'CheckMap') && tempFilterId.value) {
// 傳給 Map通知 Sidebar 要關閉。 // Notify Map to close the Sidebar.
emitter.emit('leaveFilter', false); emitter.emit('leaveFilter', false);
leaveFilter(false, allMapDataStore.addFilterId, false, logOut) leaveFilter(false, allMapDataStore.addFilterId, false, logOut)
} else if((route.name === 'Conformance' || route.name === 'CheckConformance') } else if((route.name === 'Conformance' || route.name === 'CheckConformance')

View File

@@ -1,4 +1,4 @@
<!-- Filled 版本的按鈕的背景是填滿的 --> <!-- The filled version of the button has a solid background -->
<template> <template>
<button class="button-filled-component w-[80px] h-[32px] rounded-full <button class="button-filled-component w-[80px] h-[32px] rounded-full
flex text-[#FFFFFF] flex text-[#FFFFFF]

View File

@@ -317,7 +317,7 @@ const convertSecToDay = (sec) => {
* @param {number} no - The trace number. * @param {number} no - The trace number.
*/ */
const openMore = async (no) => { const openMore = async (no) => {
// async await 解決非同步資料延遲傳遞導致未讀取到而出錯的問題 // Use async/await to prevent errors caused by asynchronous data not being available yet
issuesNo.value = no; issuesNo.value = no;
await conformanceStore.getConformanceIssue(no); await conformanceStore.getConformanceIssue(no);
traceId.value = await issueTraces.value[0].id; traceId.value = await issueTraces.value[0].id;
@@ -330,7 +330,7 @@ const openMore = async (no) => {
* @param {number} no - The trace number. * @param {number} no - The trace number.
*/ */
const openLoopMore = async (no) => { const openLoopMore = async (no) => {
// async await 解決非同步資料延遲傳遞導致未讀取到而出錯的問題 // Use async/await to prevent errors caused by asynchronous data not being available yet
loopNo.value = no; loopNo.value = no;
await conformanceStore.getConformanceLoop(no); await conformanceStore.getConformanceLoop(no);
looptraceId.value = await loopTraces.value[0].id; looptraceId.value = await loopTraces.value[0].id;
@@ -428,9 +428,9 @@ const setConformanceTempReportData = (newData) => {
result.timeTrend.yMin = newData.charts.time.y_axis.min; result.timeTrend.yMin = newData.charts.time.y_axis.min;
} }
setRateChartData(result.charts.rate.data); // 建立圖表 Rate Chart.js setRateChartData(result.charts.rate.data); // Build the Rate Chart.js chart
setCasesChartData(result.charts.cases.data.conforming, result.charts.cases.data.not_conforming, newData.charts.cases.x_axis.max, newData.charts.cases.x_axis.min); // 建立圖表 Cases Chart.js setCasesChartData(result.charts.cases.data.conforming, result.charts.cases.data.not_conforming, newData.charts.cases.x_axis.max, newData.charts.cases.x_axis.min); // Build the Cases Chart.js chart
if(newData.charts.time) setTimeChartData(result.timeTrend.chart, result.timeTrend.xMax, result.timeTrend.xMin, result.timeTrend.yMax, result.timeTrend.yMin); // 建立圖表 Time Chart.js if(newData.charts.time) setTimeChartData(result.timeTrend.chart, result.timeTrend.xMax, result.timeTrend.xMin, result.timeTrend.yMax, result.timeTrend.yMin); // Build the Time Chart.js chart
return result; return result;
}; };
@@ -446,8 +446,8 @@ const setRateChartData = (chartData) => {
label: 'Rate', label: 'Rate',
data: chartData, data: chartData,
fill: false, fill: false,
pointRadius: 0, // 隱藏點 pointRadius: 0, // Hide data points
pointHoverRadius: 0, // 隱藏點的 hover pointHoverRadius: 0, // Hide data points on hover
tension: 0.4, tension: 0.4,
borderColor: '#0099FF', borderColor: '#0099FF',
x: 'x', x: 'x',
@@ -467,9 +467,9 @@ const setRateChartData = (chartData) => {
} }
}, },
plugins: { plugins: {
legend: false, // 圖例 legend: false, // Hide legend
tooltip: { tooltip: {
enabled: false // 隱藏 工具提示框 enabled: false // Hide tooltip
} }
}, },
scales: { scales: {
@@ -479,17 +479,17 @@ const setRateChartData = (chartData) => {
display: false, display: false,
}, },
grid: { grid: {
display: false, // 隱藏 x 軸網格 display: false, // Hide x-axis grid lines
}, },
border: { border: {
color: '#334155', color: '#334155',
}, },
}, },
y: { y: {
beginAtZero: true, // scale 包含 0 beginAtZero: true, // Scale includes 0
suggestedMin: 0, suggestedMin: 0,
suggestedMax: 1, suggestedMax: 1,
ticks:{ // 設定間隔數值 ticks:{ // Set tick intervals
includeBounds: true, includeBounds: true,
color: '#334155', color: '#334155',
align: 'inner', align: 'inner',
@@ -500,7 +500,7 @@ const setRateChartData = (chartData) => {
}, },
}, },
grid: { grid: {
display: false, // 隱藏 y 軸網格 display: false, // Hide y-axis grid lines
}, },
border: { border: {
color: '#334155', color: '#334155',
@@ -550,7 +550,7 @@ const setCasesChartData = (conformingData, notConformingData, xMax, xMin) => {
mode: 'index', mode: 'index',
intersect: false intersect: false
}, },
legend: false, // 圖例 legend: false, // Hide legend
}, },
scales: { scales: {
x: { x: {
@@ -559,7 +559,7 @@ const setCasesChartData = (conformingData, notConformingData, xMax, xMin) => {
display: false, display: false,
}, },
grid: { grid: {
display: false, // 隱藏 x 軸網格 display: false, // Hide x-axis grid lines
}, },
border: { border: {
color: '#334155', color: '#334155',
@@ -567,7 +567,7 @@ const setCasesChartData = (conformingData, notConformingData, xMax, xMin) => {
}, },
y: { y: {
stacked: true, stacked: true,
beginAtZero: true, // scale 包含 0 beginAtZero: true, // Scale includes 0
ticks:{ ticks:{
color: '#334155', color: '#334155',
align: 'inner', align: 'inner',
@@ -578,7 +578,7 @@ const setCasesChartData = (conformingData, notConformingData, xMax, xMin) => {
}, },
}, },
grid: { grid: {
// display: false, // 隱藏 y 軸網格 // display: false, // Hide y-axis grid lines
color: function(context) { color: function(context) {
return context.tick.value === 0 ? '#334155' : null; return context.tick.value === 0 ? '#334155' : null;
}, },
@@ -647,18 +647,18 @@ const setTimeChartData = (chartData, xMax, xMin, yMax, yMin) => {
} }
}, },
plugins: { plugins: {
legend: false, // 圖例 legend: false, // Hide legend
tooltip: false, tooltip: false,
}, },
scales: { scales: {
x: { x: {
ticks: { ticks: {
maxRotation: 0, // 不旋轉 lable 0~50 maxRotation: 0, // Do not rotate labels (0~50)
color: '#334155', color: '#334155',
display: true, display: true,
}, },
grid: { grid: {
display: false, // 隱藏 x 軸網格 display: false, // Hide x-axis grid lines
}, },
title: { title: {
display: true, display: true,
@@ -667,18 +667,18 @@ const setTimeChartData = (chartData, xMax, xMin, yMax, yMin) => {
}, },
}, },
y: { y: {
beginAtZero: true, // scale 包含 0 beginAtZero: true, // Scale includes 0
max: max, max: max,
ticks: { // 設定間隔數值 ticks: { // Set tick intervals
display: false, // 隱藏數值,只顯示格線 display: false, // Hide values, only show grid lines
stepSize: max / 4, stepSize: max / 4,
}, },
grid: { grid: {
color: 'rgba(100,116,139)', color: 'rgba(100,116,139)',
drawTicks: false // 隱藏左側多的空間 drawTicks: false // Hide extra space on the left
}, },
border: { border: {
display: false, // 隱藏左側多出來的線 display: false, // Hide the extra border line on the left
}, },
title: { title: {
display: true, display: true,
@@ -705,7 +705,7 @@ watch(conformanceTempReportData, (newValue) => {
emitter.on('coverPlate', boolean => { emitter.on('coverPlate', boolean => {
isCoverPlate.value = boolean; isCoverPlate.value = boolean;
}); });
// 取得 selectTimeTange Tiem Trend 使用 // Get selectTimeRange for use by Time Trend
emitter.on('timeRangeMaxMin', newData => selectDurationTime.value = newData); emitter.on('timeRangeMaxMin', newData => selectDurationTime.value = newData);
</script> </script>
<style scoped> <style scoped>

View File

@@ -201,7 +201,7 @@ const selectCfmCtEteEnd = ref(null);
const selectCfmCtEteSEStart = ref(null); const selectCfmCtEteSEStart = ref(null);
const selectCfmCtEteSEEnd = ref(null); const selectCfmCtEteSEEnd = ref(null);
const isAlreadySubmit = ref(false); const isAlreadySubmit = ref(false);
const isSubmittedData = ref(null); // 已 Apply 後,沒有重新改變規則的 Data const isSubmittedData = ref(null); // Data after Apply, unchanged unless rules are modified
const isSubmitTask = ref(null); const isSubmitTask = ref(null);
const isSubmitStartAndEnd = ref(null); // Activity sequence const isSubmitStartAndEnd = ref(null); // Activity sequence
const isSubmitCfmSeqDirectly = ref([]); const isSubmitCfmSeqDirectly = ref([]);
@@ -247,7 +247,7 @@ const isSubmitShowDataSeq = ref({
taskEnd: null, taskEnd: null,
isStartSelected: null, isStartSelected: null,
isEndSelected: null isEndSelected: null
}); // To Start & End 連動資料 }); // Linked data for Start & End
const isSubmitShowDataPtEte = ref({ const isSubmitShowDataPtEte = ref({
task: null, task: null,
taskStart: null, taskStart: null,
@@ -348,7 +348,7 @@ watch(isSubmittedData, (newValue) => {
isSubmittedData.value = newValue; isSubmittedData.value = newValue;
}); });
// 打開 rule 檔要顯示儲存的選項、規則、時間 // When opening a rule file, display the saved options, rules, and time
watch(conformanceTempReportData, (newValue) => { watch(conformanceTempReportData, (newValue) => {
setTimeout(() => { setTimeout(() => {
if(newValue !== null) { if(newValue !== null) {
@@ -357,11 +357,11 @@ watch(conformanceTempReportData, (newValue) => {
isSubmittedData.value = newValue.rule; isSubmittedData.value = newValue.rule;
isAlreadySubmit.value = true; isAlreadySubmit.value = true;
switch (rule.type) { switch (rule.type) {
case 'contains-tasks': // Rule Type Have activity 的行為 case 'contains-tasks': // Behavior when Rule Type is Have activity
selectedRuleType.value = 'Have activity'; selectedRuleType.value = 'Have activity';
isSubmitTask.value = rule.tasks; isSubmitTask.value = rule.tasks;
break; break;
case 'start-end': // Rule Type Activity sequence 的行為 case 'start-end': // Behavior when Rule Type is Activity sequence
selectedRuleType.value = 'Activity sequence'; selectedRuleType.value = 'Activity sequence';
selectedActivitySequence.value = 'Start & End'; selectedActivitySequence.value = 'Start & End';
isSubmitStartAndEnd.value = [ isSubmitStartAndEnd.value = [
@@ -369,7 +369,7 @@ watch(conformanceTempReportData, (newValue) => {
{category: 'End', task: rule.ends_with}, {category: 'End', task: rule.ends_with},
]; ];
break; break;
case 'directly-follows': // Activity Sequence Sequence 的行為 case 'directly-follows': // Behavior when Activity Sequence is Sequence
selectedRuleType.value = 'Activity sequence'; selectedRuleType.value = 'Activity sequence';
selectedActivitySequence.value = 'Sequence'; selectedActivitySequence.value = 'Sequence';
selectedMode.value = 'Directly follows'; selectedMode.value = 'Directly follows';
@@ -391,7 +391,7 @@ watch(conformanceTempReportData, (newValue) => {
selectedActivitySequence.value = 'Sequence'; selectedActivitySequence.value = 'Sequence';
selectedMode.value = 'Self loop(s)'; selectedMode.value = 'Self loop(s)';
break; break;
case 'task-duration': // Rule Type Activity duration 的行為 case 'task-duration': // Behavior when Rule Type is Activity duration
selectedRuleType.value = 'Activity duration'; selectedRuleType.value = 'Activity duration';
selectDurationData.value = [rule.task]; selectDurationData.value = [rule.task];
isSubmitDurationData.value = [rule.task]; isSubmitDurationData.value = [rule.task];
@@ -400,7 +400,7 @@ watch(conformanceTempReportData, (newValue) => {
rule: { min: rule.min, max: rule.max}, rule: { min: rule.min, max: rule.max},
}; };
break; break;
case 'processing-time-end-to-end-whole': // Rule Type Processing time 的行為 case 'processing-time-end-to-end-whole': // Behavior when Rule Type is Processing time
selectedRuleType.value = 'Processing time'; selectedRuleType.value = 'Processing time';
selectedProcessScope.value = 'End to end'; selectedProcessScope.value = 'End to end';
selectedActSeqMore.value = 'All'; selectedActSeqMore.value = 'All';
@@ -489,7 +489,7 @@ watch(conformanceTempReportData, (newValue) => {
}; };
isSubmitShowDataPtP.value = setSubmitShowDataByStartEnd(rule.start, rule.end); isSubmitShowDataPtP.value = setSubmitShowDataByStartEnd(rule.start, rule.end);
break; break;
case 'waiting-time-end-to-end-whole': // Rule Type Waiting time 的行為 case 'waiting-time-end-to-end-whole': // Behavior when Rule Type is Waiting time
selectedRuleType.value = 'Waiting time'; selectedRuleType.value = 'Waiting time';
selectedProcessScope.value = 'End to end'; selectedProcessScope.value = 'End to end';
selectedActSeqMore.value = 'All'; selectedActSeqMore.value = 'All';
@@ -578,7 +578,7 @@ watch(conformanceTempReportData, (newValue) => {
}; };
isSubmitShowDataWtP.value = setSubmitShowDataByStartEnd(rule.start, rule.end); isSubmitShowDataWtP.value = setSubmitShowDataByStartEnd(rule.start, rule.end);
break; break;
case 'cycle-time-end-to-end-whole': // Rule Type Cycle time 的行為 case 'cycle-time-end-to-end-whole': // Behavior when Rule Type is Cycle time
selectedRuleType.value = 'Cycle time'; selectedRuleType.value = 'Cycle time';
selectedProcessScope.value = 'End to end'; selectedProcessScope.value = 'End to end';
selectedActSeqMore.value = 'All'; selectedActSeqMore.value = 'All';
@@ -651,7 +651,7 @@ function maxTotalSeconds(e) {
* select Time Reset * select Time Reset
*/ */
function selectTimeReset() { function selectTimeReset() {
// 不包含 selectDurationTime // Does not include selectDurationTime
selectConformanceTask.value = null; // Have activity selectConformanceTask.value = null; // Have activity
selectConformanceStartAndEnd.value = null; // Activity Sequence selectConformanceStartAndEnd.value = null; // Activity Sequence
selectCfmSeqStart.value = null; selectCfmSeqStart.value = null;
@@ -686,7 +686,7 @@ function selectTimeReset() {
* is submit select Reset * is submit select Reset
*/ */
function isSubmitReset() { function isSubmitReset() {
// 不包含 isSubmittedData // Does not include isSubmittedData
isSubmitTask.value = null; isSubmitTask.value = null;
isSubmitStartAndEnd.value = null; isSubmitStartAndEnd.value = null;
isSubmitCfmSeqDirectly.value = []; isSubmitCfmSeqDirectly.value = [];
@@ -731,17 +731,17 @@ function isSubmitReset() {
* Clears all form selections and resets the sidebar state. * Clears all form selections and resets the sidebar state.
*/ */
function reset() { function reset() {
// Results page Cover Plate(遮罩為 ture) // Results page cover plate (mask enabled)
emitter.emit('coverPlate', true); emitter.emit('coverPlate', true);
// is submit select Reset // is submit select Reset
isSubmitReset(); isSubmitReset();
isSubmittedData.value = null; // 已 Apply 後,沒有重新改變規則的 Data isSubmittedData.value = null; // Data after Apply, unchanged unless rules are modified
isAlreadySubmit.value = false; isAlreadySubmit.value = false;
// 其他子元件 reset // Reset other child components
selectTimeReset(); selectTimeReset();
emitter.emit('reset', null); emitter.emit('reset', null);
// reset 成功訊息 // Reset success message
$toast.success(i18next.t("Conformance.RuleCleared")); $toast.success(i18next.t("Conformance.RuleCleared"));
isShowBarOpen.value = true; isShowBarOpen.value = true;
} }
@@ -783,22 +783,22 @@ async function submitConformance() {
emitter.emit('timeRangeMaxMin', selectDurationTime.value); emitter.emit('timeRangeMaxMin', selectDurationTime.value);
switch (selectedRuleType.value) { switch (selectedRuleType.value) {
case 'Have activity': // Rule Type Have activity 的行為 case 'Have activity': // Behavior when Rule Type is Have activity
dataToSave = getHaveActivityData(); dataToSave = getHaveActivityData();
break; break;
case 'Activity sequence': // Rule Type Activity sequence 的行為 case 'Activity sequence': // Behavior when Rule Type is Activity sequence
dataToSave = getActivitySequenceData(); dataToSave = getActivitySequenceData();
break; break;
case 'Activity duration': // Rule Type Activity duration 的行為 case 'Activity duration': // Behavior when Rule Type is Activity duration
dataToSave = getActivityDurationData(); dataToSave = getActivityDurationData();
break; break;
case 'Processing time': // Rule Type Processing time 的行為 case 'Processing time': // Behavior when Rule Type is Processing time
dataToSave = getProcessingTimeData(); dataToSave = getProcessingTimeData();
break; break;
case 'Waiting time': // Rule Type Waiting time 的行為 case 'Waiting time': // Behavior when Rule Type is Waiting time
dataToSave = getWaitingTimeData(); dataToSave = getWaitingTimeData();
break; break;
case 'Cycle time': // Rule Type Cycle time 的行為 case 'Cycle time': // Behavior when Rule Type is Cycle time
dataToSave = getCycleTimeData(); dataToSave = getCycleTimeData();
break; break;
} }
@@ -812,8 +812,8 @@ async function submitConformance() {
isLoading.value = true; isLoading.value = true;
isAlreadySubmit.value = true; isAlreadySubmit.value = true;
isSubmittedData.value = dataToSave; // 已 Apply 後,沒有重新改變規則的 Data isSubmittedData.value = dataToSave; // Data after Apply, unchanged unless rules are modified
conformanceRuleData.value = dataToSave; // 給存檔的 Data conformanceRuleData.value = dataToSave; // Data for saving to file
await conformanceStore.addConformanceCheckId(dataToSave); await conformanceStore.addConformanceCheckId(dataToSave);
await conformanceStore.getConformanceReport(); await conformanceStore.getConformanceReport();
isShowBarOpen.value = false; isShowBarOpen.value = false;
@@ -1580,7 +1580,7 @@ function checkStartAndEnd(start, end) {
// created() logic // created() logic
isShowBarOpen.value = !(conformanceLogCreateCheckId.value || conformanceFilterCreateCheckId.value); isShowBarOpen.value = !(conformanceLogCreateCheckId.value || conformanceFilterCreateCheckId.value);
// 選取 list 的結果 // Result of list selection
emitter.on('actListData', (data) => { emitter.on('actListData', (data) => {
selectConformanceTask.value = data; selectConformanceTask.value = data;
}); });
@@ -1588,7 +1588,7 @@ emitter.on('actListData', (data) => {
setTaskByCategoryOnRadioEmitting(); setTaskByCategoryOnRadioEmitting();
setTaskByCategoryOnListSeqEmitting(); setTaskByCategoryOnListSeqEmitting();
// Radio 切換時,資料要清空 // Clear data when radio selection changes
emitter.on('isRadioChange', (data) => { emitter.on('isRadioChange', (data) => {
if(data) { if(data) {
selectTimeReset(); selectTimeReset();

View File

@@ -65,7 +65,7 @@ const lastItemIndex = ref(null);
const isSelect = ref(true); const isSelect = ref(true);
const datadata = computed(() => { const datadata = computed(() => {
// Activity List 要排序 // Sort the Activity List
let newData; let newData;
if(props.data !== null) { if(props.data !== null) {
newData = JSON.parse(JSON.stringify(props.data)); newData = JSON.parse(JSON.stringify(props.data));
@@ -108,10 +108,10 @@ function getComponentData() {
function onStart(evt) { function onStart(evt) {
const lastChild = evt.to.lastChild.lastChild; const lastChild = evt.to.lastChild.lastChild;
lastChild.style.display = 'none'; lastChild.style.display = 'none';
// 隱藏拖曳元素原位置 // Hide the dragged element at its original position
const originalElement = evt.item; const originalElement = evt.item;
originalElement.style.display = 'none'; originalElement.style.display = 'none';
// 拖曳最後一個元素時,倒數第二的元素的箭頭要隱藏 // When dragging the last element, hide the arrow of the second-to-last element
const listIndex = listSequence.value.length - 1; const listIndex = listSequence.value.length - 1;
if(evt.oldIndex === listIndex) lastItemIndex.value = listIndex; if(evt.oldIndex === listIndex) lastItemIndex.value = listIndex;
} }
@@ -120,16 +120,16 @@ function onStart(evt) {
* Element dragging ended * Element dragging ended
*/ */
function onEnd(evt) { function onEnd(evt) {
// 顯示拖曳元素 // Show the dragged element
const originalElement = evt.item; const originalElement = evt.item;
originalElement.style.display = ''; originalElement.style.display = '';
// 拖曳結束要顯示箭頭,但最後一個不用 // Show the arrow after drag ends, except for the last element
const lastChild = evt.item.lastChild; const lastChild = evt.item.lastChild;
const listIndex = listSequence.value.length - 1; const listIndex = listSequence.value.length - 1;
if (evt.oldIndex !== listIndex) { if (evt.oldIndex !== listIndex) {
lastChild.style.display = ''; lastChild.style.display = '';
} }
// reset: 拖曳最後一個元素時,倒數第二的元素的箭頭要隱藏 // Reset: hide the second-to-last element's arrow when dragging the last element
lastItemIndex.value = null; lastItemIndex.value = null;
} }

View File

@@ -110,7 +110,7 @@ function changeRadio() {
selectedProcessScope.value = 'End to end'; selectedProcessScope.value = 'End to end';
selectedActSeqMore.value = 'All'; selectedActSeqMore.value = 'All';
selectedActSeqFromTo.value = 'From'; selectedActSeqFromTo.value = 'From';
emitter.emit('isRadioChange', true); // Radio 切換時,資料要清空 emitter.emit('isRadioChange', true); // Clear data when switching radio buttons
} }
/** Emits event when the activity sequence radio changes. */ /** Emits event when the activity sequence radio changes. */
function changeRadioSeq() { function changeRadioSeq() {

View File

@@ -190,7 +190,7 @@ emitter.on('actListData', (data) => {
state.containstTasksData = data; state.containstTasksData = data;
}); });
emitter.on('actRadioData', (newData) => { emitter.on('actRadioData', (newData) => {
const data = JSON.parse(JSON.stringify(newData)); // 深拷貝原始 cases 的內容 const data = JSON.parse(JSON.stringify(newData)); // Deep copy the original cases data
const categoryMapping = { const categoryMapping = {
'cfmSeqStart': ['Start', 'selectCfmSeqStart', 'selectCfmSeqEnd'], 'cfmSeqStart': ['Start', 'selectCfmSeqStart', 'selectCfmSeqEnd'],
@@ -252,7 +252,7 @@ emitter.on('getListSequence', (data) => {
emitter.on('reset', (data) => { emitter.on('reset', (data) => {
reset(); reset();
}); });
// Radio 切換時,資料要清空 // Clear data when switching radio buttons
emitter.on('isRadioChange', (data) => { emitter.on('isRadioChange', (data) => {
if(data) reset(); if(data) reset();
}); });

View File

@@ -224,7 +224,7 @@ const cfmCtEteSEEndData = computed(() => {
return isStartSelected.value ? setStartAndEndData(cfmCtEteSE.value, 'start', task.value) : setTaskData(cfmCtEteSE.value, 'end'); return isStartSelected.value ? setStartAndEndData(cfmCtEteSE.value, 'start', task.value) : setTaskData(cfmCtEteSE.value, 'end');
}); });
// Watchers - 解決儲存後的 Rule 檔,無法重新更改規則之問題 // Watchers - Fix issue where saved rule files could not be re-edited
watch(() => props.isSubmitShowDataSeq, (newValue) => { watch(() => props.isSubmitShowDataSeq, (newValue) => {
taskStart.value = newValue.taskStart; taskStart.value = newValue.taskStart;
taskEnd.value = newValue.taskEnd; taskEnd.value = newValue.taskEnd;
@@ -258,7 +258,7 @@ watch(() => props.isSubmitShowDataCt, (newValue) => {
*/ */
function setTaskData(data, category) { function setTaskData(data, category) {
let newData = data.map(i => i[category]); let newData = data.map(i => i[category]);
newData = [...new Set(newData)]; // Set 是一種集合型別,只會儲存獨特的值。 newData = [...new Set(newData)]; // Set is a collection type that only stores unique values.
return newData; return newData;
} }
/** /**

View File

@@ -42,7 +42,7 @@ const durationMax = ref(null);
/** Deep-copies timeData min/max values to the Vue component boundaries. */ /** Deep-copies timeData min/max values to the Vue component boundaries. */
function setTimeValue() { function setTimeValue() {
// 深拷貝原始 timeData 的內容 // Deep copy the original timeData values
minVuemin.value = JSON.parse(JSON.stringify(timeData.value.min)); minVuemin.value = JSON.parse(JSON.stringify(timeData.value.min));
minVuemax.value = JSON.parse(JSON.stringify(timeData.value.max)); minVuemax.value = JSON.parse(JSON.stringify(timeData.value.max));
maxVuemin.value = JSON.parse(JSON.stringify(timeData.value.min)); maxVuemin.value = JSON.parse(JSON.stringify(timeData.value.min));

View File

@@ -93,7 +93,7 @@ const contentClass = ref('!bg-neutral-100 border-t border-neutral-300 h-full');
const showTraceId = ref(null); const showTraceId = ref(null);
const infiniteData = ref(null); const infiniteData = ref(null);
const maxItems = ref(false); const maxItems = ref(false);
const infiniteFinish = ref(true); // 無限滾動是否載入完成 const infiniteFinish = ref(true); // Whether infinite scroll loading is complete
const startNum = ref(0); const startNum = ref(0);
const processMap = ref({ const processMap = ref({
nodes:[], nodes:[],
@@ -121,24 +121,24 @@ const traceList = computed(() => {
const caseData = computed(() => { const caseData = computed(() => {
if(infiniteData.value !== null){ if(infiniteData.value !== null){
const data = JSON.parse(JSON.stringify(infiniteData.value)); // 深拷貝原始 cases 的內容 const data = JSON.parse(JSON.stringify(infiniteData.value)); // Deep copy the original cases data
data.forEach(item => { data.forEach(item => {
item.facets.forEach((facet, index) => { item.facets.forEach((facet, index) => {
item[`fac_${index}`] = facet.value; // 建立新的 key-value pair item[`fac_${index}`] = facet.value; // Create a new key-value pair
}); });
delete item.facets; // 刪除原本的 facets 屬性 delete item.facets; // Remove the original facets property
item.attributes.forEach((attribute, index) => { item.attributes.forEach((attribute, index) => {
item[`att_${index}`] = attribute.value; // 建立新的 key-value pair item[`att_${index}`] = attribute.value; // Create a new key-value pair
}); });
delete item.attributes; // 刪除原本的 attributes 屬性 delete item.attributes; // Remove the original attributes property
}) })
return data; return data;
} }
}); });
const columnData = computed(() => { const columnData = computed(() => {
const data = JSON.parse(JSON.stringify(props.cases)); // 深拷貝原始 cases 的內容 const data = JSON.parse(JSON.stringify(props.cases)); // Deep copy the original cases data
const facetName = facName => facName.trim().replace(/^(.)(.*)$/, (match, firstChar, restOfString) => firstChar.toUpperCase() + restOfString.toLowerCase()); const facetName = facName => facName.trim().replace(/^(.)(.*)$/, (match, firstChar, restOfString) => firstChar.toUpperCase() + restOfString.toLowerCase());
const result = [ const result = [
@@ -152,7 +152,7 @@ const columnData = computed(() => {
}); });
// watch // watch
watch(() => props.listModal, (newValue) => { // 第一次打開 Modal 要繪圖 watch(() => props.listModal, (newValue) => { // Draw the chart when the modal is opened for the first time
if(newValue) createCy(); if(newValue) createCy();
}); });
@@ -161,7 +161,7 @@ watch(() => props.taskSeq, (newValue) => {
}); });
watch(() => props.traceId, (newValue) => { watch(() => props.traceId, (newValue) => {
// 當 traceId 屬性變化時更新 showTraceId // Update showTraceId when the traceId prop changes
showTraceId.value = newValue; showTraceId.value = newValue;
}); });
@@ -210,15 +210,15 @@ async function switchCaseData(id) {
if(props.category === 'issue') result = await conformanceStore.getConformanceTraceDetail(props.listNo, id, 0); 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); else if(props.category === 'loop') result = await conformanceStore.getConformanceLoopsTraceDetail(props.listNo, id, 0);
infiniteData.value = await result; infiniteData.value = await result;
showTraceId.value = id; // getDetail 為了 case table 載入完再切換 showTraceId showTraceId.value = id; // Set after getDetail so the case table finishes loading before switching showTraceId
} }
/** /**
* Assembles the trace element nodes data for Cytoscape rendering. * Assembles the trace element nodes data for Cytoscape rendering.
*/ */
function setNodesData(){ function setNodesData(){
// 避免每次渲染都重複累加 // Clear nodes to prevent accumulation on each render
processMap.value.nodes = []; processMap.value.nodes = [];
// 將 api call 回來的資料帶進 node // Populate nodes with data returned from the API call
if(props.taskSeq !== null) { if(props.taskSeq !== null) {
props.taskSeq.forEach((node, index) => { props.taskSeq.forEach((node, index) => {
processMap.value.nodes.push({ processMap.value.nodes.push({
@@ -252,7 +252,7 @@ function setEdgesData(){
}); });
}); });
}; };
// 關係線數量筆節點少一個 // The number of edges is one less than the number of nodes
processMap.value.edges.pop(); processMap.value.edges.pop();
} }
/** /**

View File

@@ -93,7 +93,7 @@ const filteredData = ref(props.filterTaskData);
const lastItemIndex = ref(null); const lastItemIndex = ref(null);
const data = computed(() => { const data = computed(() => {
// Activity List 要排序 // Sort the Activity List
filteredData.value = filteredData.value.sort((x, y) => { filteredData.value = filteredData.value.sort((x, y) => {
const diff = y.occurrences - x.occurrences; const diff = y.occurrences - x.occurrences;
return diff !== 0 ? diff : sortNumEngZhtwForFilter(x.label, y.label); return diff !== 0 ? diff : sortNumEngZhtwForFilter(x.label, y.label);
@@ -139,10 +139,10 @@ function getComponentData() {
function onStart(evt) { function onStart(evt) {
const lastChild = evt.to.lastChild.lastChild; const lastChild = evt.to.lastChild.lastChild;
lastChild.style.display = 'none'; lastChild.style.display = 'none';
// 隱藏拖曳元素原位置 // Hide the dragged element at its original position
const originalElement = evt.item; const originalElement = evt.item;
originalElement.style.display = 'none'; originalElement.style.display = 'none';
// 拖曳最後一個元素時,倒數第二的元素的箭頭要隱藏 // When dragging the last element, hide the arrow of the second-to-last element
const listIndex = listSequence.value.length - 1; const listIndex = listSequence.value.length - 1;
if(evt.oldIndex === listIndex) lastItemIndex.value = listIndex; if(evt.oldIndex === listIndex) lastItemIndex.value = listIndex;
} }
@@ -152,16 +152,16 @@ function onStart(evt) {
* @param {Event} evt - The drag end event. * @param {Event} evt - The drag end event.
*/ */
function onEnd(evt) { function onEnd(evt) {
// 顯示拖曳元素 // Show the dragged element
const originalElement = evt.item; const originalElement = evt.item;
originalElement.style.display = ''; originalElement.style.display = '';
// 拖曳結束要顯示箭頭,但最後一個不用 // Show the arrow after drag ends, except for the last element
const lastChild = evt.item.lastChild; const lastChild = evt.item.lastChild;
const listIndex = listSequence.value.length - 1 const listIndex = listSequence.value.length - 1
if (evt.oldIndex !== listIndex) { if (evt.oldIndex !== listIndex) {
lastChild.style.display = ''; lastChild.style.display = '';
} }
// reset: 拖曳最後一個元素時,倒數第二的元素的箭頭要隱藏 // Reset: hide the second-to-last element's arrow when dragging the last element
lastItemIndex.value = null; lastItemIndex.value = null;
} }
</script> </script>

View File

@@ -15,7 +15,7 @@
</ColumnGroup> </ColumnGroup>
<Column selectionMode="single" bodyClass="!p-2 !border-0"></Column> <Column selectionMode="single" bodyClass="!p-2 !border-0"></Column>
<Column field="label" header="Activity" bodyClass="break-words !py-2 !border-0"></Column> <Column field="label" header="Activity" bodyClass="break-words !py-2 !border-0"></Column>
<Column header="進度條" bodyClass="!py-2 !border-0 min-w-[96px]"> <Column header="Progress" bodyClass="!py-2 !border-0 min-w-[96px]">
<template #body="slotProps"> <template #body="slotProps">
<div class="h-4 w-full bg-neutral-300 rounded-sm overflow-hidden"> <div class="h-4 w-full bg-neutral-300 rounded-sm overflow-hidden">
<div class="h-full bg-primary" :style="progressWidth(slotProps.data.occ_value)"></div> <div class="h-full bg-primary" :style="progressWidth(slotProps.data.occ_value)"></div>

View File

@@ -16,7 +16,7 @@
</ColumnGroup> </ColumnGroup>
<Column selectionMode="multiple" bodyClass="!p-2 !border-0"></Column> <Column selectionMode="multiple" bodyClass="!p-2 !border-0"></Column>
<Column field="label" header="Activity" bodyClass="break-words !py-2 !border-0"></Column> <Column field="label" header="Activity" bodyClass="break-words !py-2 !border-0"></Column>
<Column header="進度條" bodyClass="!py-2 !border-0 min-w-[96px]"> <Column header="Progress" bodyClass="!py-2 !border-0 min-w-[96px]">
<template #body="slotProps"> <template #body="slotProps">
<div class="h-4 w-full bg-neutral-300 rounded-sm overflow-hidden"> <div class="h-4 w-full bg-neutral-300 rounded-sm overflow-hidden">
<div class="h-full bg-primary" :style="progressWidth(slotProps.data.occ_value)"></div> <div class="h-full bg-primary" :style="progressWidth(slotProps.data.occ_value)"></div>
@@ -25,7 +25,7 @@
</Column> </Column>
<Column field="occurrences" header="Occurrences" bodyClass="!text-right !py-2 !border-0"></Column> <Column field="occurrences" header="Occurrences" bodyClass="!text-right !py-2 !border-0"></Column>
<Column field="occurrence_ratio" header="O2" bodyClass="!text-right !py-2 !border-0"></Column> <Column field="occurrence_ratio" header="O2" bodyClass="!text-right !py-2 !border-0"></Column>
<Column header="進度條" bodyClass="!py-2 !border-0 min-w-[96px]"> <Column header="Progress" bodyClass="!py-2 !border-0 min-w-[96px]">
<template #body="slotProps"> <template #body="slotProps">
<div class="h-4 w-full bg-neutral-300 rounded-sm overflow-hidden"> <div class="h-4 w-full bg-neutral-300 rounded-sm overflow-hidden">
<div class="h-full bg-primary" :style="progressWidth(slotProps.data.case_value)"></div> <div class="h-full bg-primary" :style="progressWidth(slotProps.data.case_value)"></div>

View File

@@ -42,7 +42,7 @@
</div> </div>
</template> </template>
</Column> </Column>
<Column header="進度條" bodyClass="!py-2 !border-0 min-w-[96px]"> <Column header="Progress" bodyClass="!py-2 !border-0 min-w-[96px]">
<template #body="slotProps"> <template #body="slotProps">
<div class="h-4 w-full bg-neutral-300 rounded-sm overflow-hidden"> <div class="h-4 w-full bg-neutral-300 rounded-sm overflow-hidden">
<div class="h-full bg-primary" :style="progressWidth(slotProps.data.occ_progress_bar)"></div> <div class="h-full bg-primary" :style="progressWidth(slotProps.data.occ_progress_bar)"></div>
@@ -71,7 +71,7 @@
</div> </div>
</template> </template>
</Column> </Column>
<Column header="進度條" bodyClass="!py-2 !border-0 min-w-[96px]"> <Column header="Progress" bodyClass="!py-2 !border-0 min-w-[96px]">
<template #body="slotProps"> <template #body="slotProps">
<div class="h-4 w-full bg-neutral-300 rounded-sm overflow-hidden"> <div class="h-4 w-full bg-neutral-300 rounded-sm overflow-hidden">
<div class="h-full bg-primary" :style="progressWidth(slotProps.data.occ_progress_bar)"></div> <div class="h-full bg-primary" :style="progressWidth(slotProps.data.occ_progress_bar)"></div>
@@ -152,9 +152,9 @@ const valueTypes = ['int', 'float', 'date'];
const classTypes = ['boolean', 'string']; const classTypes = ['boolean', 'string'];
const chartData = ref({}); const chartData = ref({});
const chartOptions = ref({}); const chartOptions = ref({});
const chartComplete = ref(null); // 已出圖的 chart.js 資料 const chartComplete = ref(null); // Rendered chart.js instance data
const selectArea = ref(null); const selectArea = ref(null);
const selectRange = ref(1000); // 更改 select 的切分數 const selectRange = ref(1000); // Number of divisions for the selection range
const startTime = ref(null); // PrimeVue Calendar v-model const startTime = ref(null); // PrimeVue Calendar v-model
const endTime = ref(null); // PrimeVue Calendar v-model const endTime = ref(null); // PrimeVue Calendar v-model
const startMinDate = ref(null); const startMinDate = ref(null);
@@ -162,7 +162,7 @@ const startMaxDate = ref(null);
const endMinDate = ref(null); const endMinDate = ref(null);
const endMaxDate = ref(null); const endMaxDate = ref(null);
const valueStart = ref(null); // PrimeVue InputNumber v-model const valueStart = ref(null); // PrimeVue InputNumber v-model
const valueEnd = ref(null); // PrimeVue InputNumber v-model const valueEnd = ref(null); // PrimeVue InputNumber v-model
const valueStartMin = ref(null); const valueStartMin = ref(null);
const valueStartMax = ref(null); const valueStartMax = ref(null);
const valueEndMin = ref(null); const valueEndMin = ref(null);
@@ -226,16 +226,16 @@ const attRangeData = computed(() => {
return data.sort((x, y) => y.freq - x.freq); return data.sort((x, y) => y.freq - x.freq);
}); });
// 取出選取的 Attribute radio 數值型的資料 // Get the selected Attribute radio's numeric-type data
const valueData = computed(() => { const valueData = computed(() => {
// filter 回傳陣列find 回傳遞一個找到的元素,因此使用 find 方法。 // filter returns an array, find returns the first matched element, so use find here.
if(valueTypes.includes(selectedAttName.value.type)){ if(valueTypes.includes(selectedAttName.value.type)){
const data = filterAttrs.value.find(item => item.type === selectedAttName.value.type && item.key === selectedAttName.value.key); const data = filterAttrs.value.find(item => item.type === selectedAttName.value.type && item.key === selectedAttName.value.key);
return data return data
} }
}); });
// 找出 slidrData時間格式:毫秒時間戳 // Compute slider data; time format: millisecond timestamps
const sliderDataComputed = computed(() => { const sliderDataComputed = computed(() => {
let xAxisMin; let xAxisMin;
let xAxisMax; let xAxisMax;
@@ -298,7 +298,7 @@ const attValueTypeStartEnd = computed(() => {
end = valueEnd.value; end = valueEnd.value;
break; break;
} }
const data = { // 傳給後端的資料 const data = { // Data to send to the backend
type: type, type: type,
data: { data: {
key: selectedAttName.value.key, key: selectedAttName.value.key,
@@ -385,43 +385,43 @@ function switchAttNameRadio(e) {
endTime.value = null; endTime.value = null;
valueStart.value = null; valueStart.value = null;
valueEnd.value = null; valueEnd.value = null;
if(valueData.value) { // 切換 Attribute Name if(valueData.value) { // Switch Attribute Name
// 初始化雙向綁定 // Initialize two-way bindings
selectArea.value = [0, selectRange.value]; selectArea.value = [0, selectRange.value];
const min = valueData.value.min; const min = valueData.value.min;
const max = valueData.value.max; const max = valueData.value.max;
switch (selectedAttName.value.type) { switch (selectedAttName.value.type) {
case 'dummy': //sonar-qube case 'dummy': //sonar-qube
case 'date': case 'date':
// 除了 date 外雙向綁定為空 // Clear two-way bindings except for date
valueStart.value = null; valueStart.value = null;
valueEnd.value = null; valueEnd.value = null;
// 初始化: Calendar // Initialize Calendar
startMinDate.value = new Date(min); startMinDate.value = new Date(min);
startMaxDate.value = new Date(max); startMaxDate.value = new Date(max);
endMinDate.value = new Date(min); endMinDate.value = new Date(min);
endMaxDate.value = new Date(max); endMaxDate.value = new Date(max);
// 初始化: 讓日曆的範圍等於時間軸的範圍 // Initialize: set the calendar range to match the timeline range
startTime.value = new Date(min); startTime.value = new Date(min);
endTime.value = new Date(max); endTime.value = new Date(max);
break; break;
default: default:
// date 雙向綁定為空 // Clear date two-way bindings
startTime.value = null; startTime.value = null;
endTime.value = null; endTime.value = null;
// 初始化:InputNumber // Initialize InputNumber
valueStartMin.value = min; valueStartMin.value = min;
valueStartMax.value = max; valueStartMax.value = max;
valueEndMin.value = min; valueEndMin.value = min;
valueEndMax.value = max; valueEndMax.value = max;
// 初始化: 讓 InputNumber 的範圍等於時間軸的範圍 // Initialize: set the InputNumber range to match the timeline range
valueStart.value = min; valueStart.value = min;
valueEnd.value = max; valueEnd.value = max;
break; break;
} }
// 傳給後端 // Send to backend
// attValueTypeStartEnd.value; 是否有要呼叫函數? sonar-qube // attValueTypeStartEnd.value; should this function be called? sonar-qube
// 建立圖表 // Create chart
createChart(); createChart();
} }
} }
@@ -545,7 +545,7 @@ function createChart() {
} }
}, },
plugins: { plugins: {
legend: false, // 圖例 legend: false, // Hide legend
filler: { filler: {
propagate: false propagate: false
}, },
@@ -562,10 +562,10 @@ function createChart() {
}, },
scales: { scales: {
y: { y: {
beginAtZero: true, // scale 包含 0 beginAtZero: true, // Scale includes 0
max: max, max: max,
ticks: { // 設定間隔數值 ticks: { // Set tick intervals
display: false, // 隱藏數值,只顯示格線 display: false, // Hide values, only show grid lines
stepSize: max / 4, stepSize: max / 4,
}, },
grid: { grid: {
@@ -573,7 +573,7 @@ function createChart() {
z: 1, z: 1,
}, },
border: { border: {
display: false, // 隱藏左側多出來的線 display: false, // Hide the extra border line on the left
} }
}, },
}, },
@@ -584,17 +584,17 @@ function createChart() {
ticks: { ticks: {
min: minX, min: minX,
max: maxX, max: maxX,
autoSkip: true, // 依比例判斷要不要換算時間單位 autoSkip: true, // Automatically determine whether to convert time units
maxRotation: 0, // 不旋轉 lable 0~50 maxRotation: 0, // Do not rotate labels (0~50)
color: '#334155', color: '#334155',
display: true, display: true,
source: 'labels', // 依比例彈性顯示 label 數量 source: 'labels', // Flexibly display label count proportionally
}, },
grid: { grid: {
display: false, // 隱藏 x 軸網格 display: false, // Hide x-axis grid lines
}, },
time: { time: {
minUnit: 'day', // 顯示最小單位 minUnit: 'day', // Minimum display unit
// displayFormats: { // displayFormats: {
// minute: 'HH:mm MMM d', // minute: 'HH:mm MMM d',
// hour: 'HH:mm MMM d', // hour: 'HH:mm MMM d',
@@ -609,7 +609,7 @@ function createChart() {
max: maxX, max: maxX,
ticks: { ticks: {
autoSkip: true, autoSkip: true,
maxRotation: 0, // 不旋轉 lable 0~50 maxRotation: 0, // Do not rotate labels (0~50)
color: '#334155', color: '#334155',
callback: ((value, index, values) => { callback: ((value, index, values) => {
let x; let x;
@@ -627,15 +627,15 @@ function createChart() {
default: default:
x = Math.round(value * 100) / 100; x = Math.round(value * 100) / 100;
} }
// 處理科學記號等格式轉換 // Handle scientific notation and other format conversions
// Decimal 無法處理超過 16 位數 // Decimal cannot handle numbers exceeding 16 digits
x = new Intl.NumberFormat(undefined, {useGrouping: false}).format(x); x = new Intl.NumberFormat(undefined, {useGrouping: false}).format(x);
return x return x
} }
}) })
}, },
grid: { grid: {
display: false, // 隱藏 x 軸網格 display: false, // Hide x-axis grid lines
}, },
} }
} }
@@ -648,32 +648,32 @@ function createChart() {
* @param {array} e [1, 100] * @param {array} e [1, 100]
*/ */
function changeSelectArea(e) { function changeSelectArea(e) {
// 日曆改變時,滑塊跟著改變 // When the calendar changes, the slider follows
const sliderData = sliderDataComputed.value; const sliderData = sliderDataComputed.value;
const start = sliderData[e[0].toFixed()]; const start = sliderData[e[0].toFixed()];
const end = sliderData[e[1].toFixed()]; // 取得 index須為整數。 const end = sliderData[e[1].toFixed()]; // Get the index, which must be an integer.
switch (selectedAttName.value.type) { switch (selectedAttName.value.type) {
case 'dummy': case 'dummy':
case 'date': case 'date':
startTime.value = new Date(start); startTime.value = new Date(start);
endTime.value = new Date(end); endTime.value = new Date(end);
// 重新設定 start end 日曆選取範圍 // Reset the start/end calendar selection range
endMinDate.value = new Date(start); endMinDate.value = new Date(start);
startMaxDate.value = new Date(end); startMaxDate.value = new Date(end);
break; break;
default: default:
valueStart.value = start; valueStart.value = start;
valueEnd.value = end; valueEnd.value = end;
// 重新設定 start end 日曆選取範圍 // Reset the start/end selection range
valueEndMin.value = start; valueEndMin.value = start;
valueStartMax.value = end; valueStartMax.value = end;
break; break;
} }
// 重新算圖 // Recalculate the chart mask
resizeMask(chartComplete.value); resizeMask(chartComplete.value);
// 執行 timeFrameStartEnd 才會改變數據 // Execute timeFrameStartEnd to update the data
// attValueTypeStartEnd.value; 是否有要呼叫函數? sonar-qube // attValueTypeStartEnd.value; should this function be called? sonar-qube
} }
/** /**
@@ -682,7 +682,7 @@ function changeSelectArea(e) {
* @param {string} direction start or end * @param {string} direction start or end
*/ */
function sliderValueRange(e, direction) { function sliderValueRange(e, direction) {
// 找到最鄰近的 index時間格式: 毫秒時間戳 // Find the closest index; time format: millisecond timestamps
const sliderData = sliderDataComputed.value; const sliderData = sliderDataComputed.value;
const isDateType = selectedAttName.value.type === 'date'; const isDateType = selectedAttName.value.type === 'date';
let targetTime = []; let targetTime = [];
@@ -698,9 +698,9 @@ function sliderValueRange(e, direction) {
result = result > selectRange.value ? selectRange.value : result; result = result > selectRange.value ? selectRange.value : result;
return result return result
}); });
// 改變滑塊 // Update the slider
selectArea.value = closestIndexes; selectArea.value = closestIndexes;
// 重新設定 start end 日曆選取範圍 // Reset the start/end calendar selection range
if(!isDateType) inputValue = Number(e.value.replace(/,/g, '')) ; if(!isDateType) inputValue = Number(e.value.replace(/,/g, '')) ;
if(direction === 'start') { if(direction === 'start') {
if(isDateType){ if(isDateType){
@@ -716,7 +716,7 @@ function sliderValueRange(e, direction) {
valueStartMax.value = inputValue; valueStartMax.value = inputValue;
}; };
} }
// 重新算圖 // Recalculate the chart mask
if(!isNaN(closestIndexes[0]) && !isNaN(closestIndexes[1])) resizeMask(chartComplete.value); if(!isNaN(closestIndexes[0]) && !isNaN(closestIndexes[1])) resizeMask(chartComplete.value);
else return; else return;
} }
@@ -740,7 +740,7 @@ emitter.on('map-filter-reset', value => {
onMounted(() => { onMounted(() => {
// Slider // Slider
selectArea.value = [0, selectRange.value]; // 初始化滑塊 selectArea.value = [0, selectRange.value]; // Initialize the slider
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {

View File

@@ -70,8 +70,8 @@ const { hasResultRule, temporaryData, postRuleData, ruleData, isRuleData, tempFi
*/ */
function isRule(e, index){ function isRule(e, index){
const rule = isRuleData.value[index]; const rule = isRuleData.value[index];
// 先取得 rule object // First get the rule object
// 為了讓 data 順序不亂掉,將值指向 0submitAll 時再刪掉 // To preserve data order, set the value to 0 and remove it during submitAll
if(!e) temporaryData.value[index] = 0; if(!e) temporaryData.value[index] = 0;
else temporaryData.value[index] = rule; else temporaryData.value[index] = rule;
} }
@@ -89,7 +89,7 @@ async function deleteRule(index) {
isLoading.value = true; isLoading.value = true;
tempFilterId.value = await null; tempFilterId.value = await null;
await allMapDataStore.getAllMapData(); await allMapDataStore.getAllMapData();
await allMapDataStore.getAllTrace(); // SidebarTrace 要連動 await allMapDataStore.getAllTrace(); // SidebarTrace needs to update in sync
await emit('submit-all'); await emit('submit-all');
isLoading.value = false; isLoading.value = false;
} }
@@ -104,9 +104,9 @@ async function deleteRule(index) {
/** Submits all enabled filter rules and refreshes the map data. */ /** Submits all enabled filter rules and refreshes the map data. */
async function submitAll() { async function submitAll() {
postRuleData.value = temporaryData.value.filter(item => item !== 0); // 取得 submit 的資料,有 toggle button 的話,找出並刪除陣列中為 0 的項目 postRuleData.value = temporaryData.value.filter(item => item !== 0); // Get submit data; if toggle buttons are used, find and remove items set to 0
if(!postRuleData.value?.length) return $toast.error('Not selected'); if(!postRuleData.value?.length) return $toast.error('Not selected');
await allMapDataStore.checkHasResult(); // 後端快速檢查有沒有結果 await allMapDataStore.checkHasResult(); // Quick backend check for results
if(hasResultRule.value === null) { if(hasResultRule.value === null) {
return; return;
@@ -114,7 +114,7 @@ async function submitAll() {
isLoading.value = true; isLoading.value = true;
await allMapDataStore.addTempFilterId(); await allMapDataStore.addTempFilterId();
await allMapDataStore.getAllMapData(); await allMapDataStore.getAllMapData();
await allMapDataStore.getAllTrace(); // SidebarTrace 要連動 await allMapDataStore.getAllTrace(); // SidebarTrace needs to update in sync
if(temporaryData.value[0]?.type) { if(temporaryData.value[0]?.type) {
allMapDataStore.traceId = await allMapDataStore.traces[0]?.id; allMapDataStore.traceId = await allMapDataStore.traces[0]?.id;
} }

View File

@@ -53,7 +53,7 @@ const props = defineProps(['selectValue']);
const allMapDataStore = useAllMapDataStore(); const allMapDataStore = useAllMapDataStore();
const { filterTimeframe, selectTimeFrame } = storeToRefs(allMapDataStore); const { filterTimeframe, selectTimeFrame } = storeToRefs(allMapDataStore);
const selectRange = ref(1000); // 更改 select 的切分數 const selectRange = ref(1000); // Number of divisions for the selection range
const selectArea = ref(null); const selectArea = ref(null);
const chart = ref(null); const chart = ref(null);
const canvasId = ref(null); const canvasId = ref(null);
@@ -73,12 +73,12 @@ const panelProps = ref({
const timeFrameStartEnd = computed(() => { const timeFrameStartEnd = computed(() => {
const start = getMoment(startTime.value).format('YYYY-MM-DDTHH:mm:00'); const start = getMoment(startTime.value).format('YYYY-MM-DDTHH:mm:00');
const end = getMoment(endTime.value).format('YYYY-MM-DDTHH:mm:00'); const end = getMoment(endTime.value).format('YYYY-MM-DDTHH:mm:00');
selectTimeFrame.value = [start, end]; // 傳給後端的資料 selectTimeFrame.value = [start, end]; // Data to send to the backend
return [start, end]; return [start, end];
}); });
// 找出 slidrData時間格式:毫秒時間戳 // Compute slider data; time format: millisecond timestamps
const sliderData = computed(() => { const sliderData = computed(() => {
const xAxisMin = new Date(filterTimeframe.value.x_axis.min).getTime(); const xAxisMin = new Date(filterTimeframe.value.x_axis.min).getTime();
const xAxisMax = new Date(filterTimeframe.value.x_axis.max).getTime(); const xAxisMax = new Date(filterTimeframe.value.x_axis.max).getTime();
@@ -93,16 +93,16 @@ const sliderData = computed(() => {
return data; return data;
}); });
// 加入最大、最小值 // Add the minimum and maximum values
const timeFrameData = computed(() => { const timeFrameData = computed(() => {
const data = filterTimeframe.value.data.map(i=>({x:i.x,y:i.y})) const data = filterTimeframe.value.data.map(i=>({x:i.x,y:i.y}))
// y 軸斜率計算請參考 ./public/timeFrameSlope 的圖 // See ./public/timeFrameSlope for the y-axis slope calculation diagram
// x 值為 0 ~ 11, // x values are 0 ~ 11,
// 將三的座標(ax, ay), (bx, by), (cx, cy)命名為 (a, b), (c, d), (e, f) // Name three coordinates (ax, ay), (bx, by), (cx, cy) as (a, b), (c, d), (e, f)
// 最小值: (f - b)(c - a) = (e - a)(d - b),求 b = (ed - ad - fa - fc) / (e - c - a) // Minimum: (f - b)(c - a) = (e - a)(d - b), solve for b = (ed - ad - fa - fc) / (e - c - a)
// 最大值: (f - b)(e - c) = (f - d)(e - a),求 f = (be - bc -de + da) / (a - c) // Maximum: (f - b)(e - c) = (f - d)(e - a), solve for f = (be - bc - de + da) / (a - c)
// y 軸最小值 // Y-axis minimum value
const a = 0; const a = 0;
let b; let b;
const c = 1; const c = 1;
@@ -113,7 +113,7 @@ const timeFrameData = computed(() => {
if(b < 0) { if(b < 0) {
b = 0; b = 0;
} }
// y 軸最大值 // Y-axis maximum value
const ma = 9; const ma = 9;
const mb = filterTimeframe.value.data[8].y; const mb = filterTimeframe.value.data[8].y;
const mc = 10; const mc = 10;
@@ -124,12 +124,12 @@ const timeFrameData = computed(() => {
mf = 0; mf = 0;
} }
// 添加最小值 // Add the minimum value
data.unshift({ data.unshift({
x: filterTimeframe.value.x_axis.min_base, x: filterTimeframe.value.x_axis.min_base,
y: b, y: b,
}) })
// 添加最大值 // Add the maximum value
data.push({ data.push({
x: filterTimeframe.value.x_axis.max_base, x: filterTimeframe.value.x_axis.max_base,
y: mf, y: mf,
@@ -234,13 +234,13 @@ function createChart() {
} }
}, },
plugins: { plugins: {
legend: false, // 圖例 legend: false, // Hide legend
filler: { filler: {
propagate: false propagate: false
}, },
title: false title: false
}, },
// animations: false, // 取消動畫 // animations: false, // Disable animations
animation: { animation: {
onComplete: e => { onComplete: e => {
resizeMask(e.chart); resizeMask(e.chart);
@@ -256,16 +256,16 @@ function createChart() {
max: maxX, max: maxX,
ticks: { ticks: {
autoSkip: true, autoSkip: true,
maxRotation: 0, // 不旋轉 lable 0~50 maxRotation: 0, // Do not rotate labels (0~50)
color: '#334155', color: '#334155',
display: true, display: true,
source: 'labels', source: 'labels',
}, },
grid: { grid: {
display: false, // 隱藏 x 軸網格 display: false, // Hide x-axis grid lines
}, },
time: { time: {
minUnit: 'day', // 顯示最小單位 minUnit: 'day', // Minimum display unit
// displayFormats: { // displayFormats: {
// minute: 'HH:mm MMM d', // minute: 'HH:mm MMM d',
// hour: 'HH:mm MMM d', // hour: 'HH:mm MMM d',
@@ -273,10 +273,10 @@ function createChart() {
} }
}, },
y: { y: {
beginAtZero: true, // scale 包含 0 beginAtZero: true, // Scale includes 0
max: max, max: max,
ticks: { // 設定間隔數值 ticks: { // Set tick intervals
display: false, // 隱藏數值,只顯示格線 display: false, // Hide values, only show grid lines
stepSize: max / 4, stepSize: max / 4,
}, },
grid: { grid: {
@@ -284,7 +284,7 @@ function createChart() {
z: 1, z: 1,
}, },
border: { border: {
display: false, // 隱藏左側多出來的線 display: false, // Hide the extra border line on the left
} }
}, },
}, },
@@ -303,19 +303,19 @@ function createChart() {
* @param {array} e [1, 100] * @param {array} e [1, 100]
*/ */
function changeSelectArea(e) { function changeSelectArea(e) {
// 日曆改變時,滑塊跟著改變 // When the calendar changes, the slider follows
const sliderDataVal = sliderData.value; const sliderDataVal = sliderData.value;
const start = sliderDataVal[e[0].toFixed()]; const start = sliderDataVal[e[0].toFixed()];
const end = sliderDataVal[e[1].toFixed()]; // 取得 index須為整數。 const end = sliderDataVal[e[1].toFixed()]; // Get the index, which must be an integer.
startTime.value = new Date(start); startTime.value = new Date(start);
endTime.value = new Date(end); endTime.value = new Date(end);
// 重新設定 start end 日曆選取範圍 // Reset the start/end calendar selection range
endMinDate.value = new Date(start); endMinDate.value = new Date(start);
startMaxDate.value = new Date(end); startMaxDate.value = new Date(end);
// 重新算圖 // Recalculate the chart mask
resizeMask(chart.value); resizeMask(chart.value);
// 執行 timeFrameStartEnd 才會改變數據 // Execute timeFrameStartEnd to update the data
timeFrameStartEnd.value; timeFrameStartEnd.value;
} }
@@ -325,7 +325,7 @@ function changeSelectArea(e) {
* @param {string} direction start or end * @param {string} direction start or end
*/ */
function sliderTimeRange(e, direction) { function sliderTimeRange(e, direction) {
// 找到最鄰近的 index時間格式: 毫秒時間戳 // Find the closest index; time format: millisecond timestamps
const sliderDataVal = sliderData.value; const sliderDataVal = sliderData.value;
const targetTime = [new Date(timeFrameStartEnd.value[0]).getTime(), new Date(timeFrameStartEnd.value[1]).getTime()]; const targetTime = [new Date(timeFrameStartEnd.value[0]).getTime(), new Date(timeFrameStartEnd.value[1]).getTime()];
const closestIndexes = targetTime.map(target => { const closestIndexes = targetTime.map(target => {
@@ -336,12 +336,12 @@ function sliderTimeRange(e, direction) {
return result return result
}); });
// 改變滑塊 // Update the slider
selectArea.value = closestIndexes; selectArea.value = closestIndexes;
// 重新設定 start end 日曆選取範圍 // Reset the start/end calendar selection range
if(direction === 'start') endMinDate.value = e; if(direction === 'start') endMinDate.value = e;
else if(direction === 'end') startMaxDate.value = e; else if(direction === 'end') startMaxDate.value = e;
// 重新算圖 // Recalculate the chart mask
if(!isNaN(closestIndexes[0]) && !isNaN(closestIndexes[1])) resizeMask(chart.value); if(!isNaN(closestIndexes[0]) && !isNaN(closestIndexes[1])) resizeMask(chart.value);
else return; else return;
} }
@@ -357,7 +357,7 @@ onMounted(() => {
startMaxDate.value = new Date(filterTimeframe.value.x_axis.max); startMaxDate.value = new Date(filterTimeframe.value.x_axis.max);
endMinDate.value = new Date(filterTimeframe.value.x_axis.min); endMinDate.value = new Date(filterTimeframe.value.x_axis.min);
endMaxDate.value = new Date(filterTimeframe.value.x_axis.max); endMaxDate.value = new Date(filterTimeframe.value.x_axis.max);
// 讓日曆的範圍等於時間軸的範圍 // Set the calendar range to match the timeline range
startTime.value = startMinDate.value; startTime.value = startMinDate.value;
endTime.value = startMaxDate.value; endTime.value = startMaxDate.value;
timeFrameStartEnd.value; timeFrameStartEnd.value;

View File

@@ -99,7 +99,7 @@ const processMap = ref({
const showTraceId = ref(null); const showTraceId = ref(null);
const infinitMaxItems = ref(false); const infinitMaxItems = ref(false);
const infiniteData = ref([]); const infiniteData = ref([]);
const infiniteFinish = ref(true); // 無限滾動是否載入完成 const infiniteFinish = ref(true); // Whether infinite scroll loading is complete
const chartOptions = ref(null); const chartOptions = ref(null);
const selectArea = ref([0, 1]); const selectArea = ref([0, 1]);
const cyTraceRef = ref(null); const cyTraceRef = ref(null);
@@ -138,11 +138,11 @@ const chartData = computed(() => {
const data = baseTraces.value.map(trace => getPercentLabel(trace.count / traceCountTotal.value)); const data = baseTraces.value.map(trace => getPercentLabel(trace.count / traceCountTotal.value));
const selectAreaData = baseTraces.value.map((trace, index) => index >= start && index <= end ? 'rgba(0,153,255)' : 'rgba(203, 213, 225)'); const selectAreaData = baseTraces.value.map((trace, index) => index >= start && index <= end ? 'rgba(0,153,255)' : 'rgba(203, 213, 225)');
return { // 要呈現的資料 return { // Data to display
labels, labels,
datasets: [ datasets: [
{ {
label: 'Trace', // 資料的標題標籤 label: 'Trace', // Dataset label
data, data,
backgroundColor: selectAreaData, backgroundColor: selectAreaData,
categoryPercentage: 1.0, categoryPercentage: 1.0,
@@ -153,18 +153,18 @@ const chartData = computed(() => {
}); });
const caseData = computed(() => { const caseData = computed(() => {
const data = JSON.parse(JSON.stringify(infiniteData.value)); // 深拷貝原始 cases 的內容 const data = JSON.parse(JSON.stringify(infiniteData.value)); // Deep copy the original cases data
data.forEach(item => { data.forEach(item => {
item.attributes.forEach((attribute, index) => { item.attributes.forEach((attribute, index) => {
item[`att_${index}`] = attribute.value; // 建立新的 key-value pair item[`att_${index}`] = attribute.value; // Create a new key-value pair
}); });
delete item.attributes; // 刪除原本的 attributes 屬性 delete item.attributes; // Remove the original attributes property
}) })
return data; return data;
}); });
const columnData = computed(() => { const columnData = computed(() => {
const data = JSON.parse(JSON.stringify(baseCases.value)); // 深拷貝原始 cases 的內容 const data = JSON.parse(JSON.stringify(baseCases.value)); // Deep copy the original cases data
let result = [ let result = [
{ field: 'id', header: 'Case Id' }, { field: 'id', header: 'Case Id' },
{ field: 'started_at', header: 'Start time' }, { field: 'started_at', header: 'Start time' },
@@ -184,7 +184,7 @@ const columnData = computed(() => {
watch(selectArea, (newValue, oldValue) => { watch(selectArea, (newValue, oldValue) => {
const roundValue = Math.round(newValue[1].toFixed()); const roundValue = Math.round(newValue[1].toFixed());
if(newValue[1] !== roundValue) selectArea.value[1] = roundValue; if(newValue[1] !== roundValue) selectArea.value[1] = roundValue;
if(newValue != oldValue) emit('filter-trace-selectArea', newValue); // 判斷 Apply 是否 disable if(newValue != oldValue) emit('filter-trace-selectArea', newValue); // Determine whether Apply should be disabled
}); });
watch(infinit404, (newValue) => { watch(infinit404, (newValue) => {
@@ -211,7 +211,7 @@ function barOptions(){
} }
}, },
plugins: { plugins: {
legend: { // 圖例 legend: { // Legend
display: false, display: false,
}, },
tooltip: { tooltip: {
@@ -228,8 +228,8 @@ function barOptions(){
display:false display:false
}, },
y: { y: {
ticks: { // 設定間隔數值 ticks: { // Set tick intervals
display: false, // 隱藏數值,只顯示格線 display: false, // Hide values, only show grid lines
min: 0, min: 0,
max: traceList.value[0]?.ratio, max: traceList.value[0]?.ratio,
stepSize: (traceList.value[0]?.ratio)/4, stepSize: (traceList.value[0]?.ratio)/4,
@@ -239,7 +239,7 @@ function barOptions(){
z: 1, z: 1,
}, },
border: { border: {
display: false, // 隱藏左側多出來的線 display: false, // Hide the extra border line on the left
} }
} }
} }
@@ -271,15 +271,15 @@ function progressWidth(value){
* @param {number} count - The total number of cases. * @param {number} count - The total number of cases.
*/ */
async function switchCaseData(id, count) { async function switchCaseData(id, count) {
// 點同一筆 id 不要有動作 // Do nothing if clicking the same id
if(id == showTraceId.value) return; if(id == showTraceId.value) return;
isLoading.value = true; // 都要 loading 畫面 isLoading.value = true; // Always show loading screen
infinit404.value = null; infinit404.value = null;
infinitMaxItems.value = false; infinitMaxItems.value = false;
baseInfiniteStart.value = 0; baseInfiniteStart.value = 0;
allMapDataStore.baseTraceId = id; allMapDataStore.baseTraceId = id;
infiniteData.value = await allMapDataStore.getBaseTraceDetail(); infiniteData.value = await allMapDataStore.getBaseTraceDetail();
showTraceId.value = id; // getDetail 為了 case table 載入完再切換 showTraceId showTraceId.value = id; // Set after getDetail so the case table finishes loading before switching showTraceId
createCy(); createCy();
isLoading.value = false; isLoading.value = false;
} }
@@ -288,9 +288,9 @@ async function switchCaseData(id, count) {
* Assembles the trace element nodes data for Cytoscape rendering. * Assembles the trace element nodes data for Cytoscape rendering.
*/ */
function setNodesData(){ function setNodesData(){
// 避免每次渲染都重複累加 // Clear nodes to prevent accumulation on each render
processMap.value.nodes = []; processMap.value.nodes = [];
// 將 api call 回來的資料帶進 node // Populate nodes with data returned from the API call
baseTraceTaskSeq.value.forEach((node, index) => { baseTraceTaskSeq.value.forEach((node, index) => {
processMap.value.nodes.push({ processMap.value.nodes.push({
data: { data: {
@@ -321,7 +321,7 @@ function setEdgesData(){
} }
}); });
}); });
// 關係線數量筆節點少一個 // The number of edges is one less than the number of nodes
processMap.value.edges.pop(); processMap.value.edges.pop();
} }
@@ -367,7 +367,7 @@ async function fetchData() {
} }
onMounted(() => { onMounted(() => {
isLoading.value = true; // createCy 執行完關閉 isLoading.value = true; // Will be closed after createCy finishes
setNodesData(); setNodesData();
setEdgesData(); setEdgesData();
createCy(); createCy();

View File

@@ -134,7 +134,7 @@ const { hasResultRule, temporaryData, postRuleData, ruleData, isRuleData, select
const selectFilter = { const selectFilter = {
'Filter Type': ['Sequence', 'Attributes', 'Trace', 'Timeframes'], 'Filter Type': ['Sequence', 'Attributes', 'Trace', 'Timeframes'],
// 'Filter Type': ['Sequence', 'Trace', 'Timeframes'], // 'Filter Type': ['Sequence', 'Trace', 'Timeframes'], // without Attributes
'Activity Sequence':['Have activity(s)', 'Start & End', 'Sequence'], 'Activity Sequence':['Have activity(s)', 'Start & End', 'Sequence'],
'Start & End': ['Start', 'End', 'Start & End'], 'Start & End': ['Start', 'End', 'Start & End'],
'Mode': ['Directly follows', 'Eventually follows'], 'Mode': ['Directly follows', 'Eventually follows'],
@@ -160,12 +160,12 @@ const selectFilterEnd = ref(null);
const selectFilterStartToEnd = ref(null); const selectFilterStartToEnd = ref(null);
const selectFilterEndToStart = ref(null); const selectFilterEndToStart = ref(null);
const listSeq = ref([]); const listSeq = ref([]);
//若第一次選擇 start, end 連動改變,若第一次選擇 end, 則 start 連動改變 // If start is selected first, end updates accordingly; if end is selected first, start updates accordingly
const isStartSelected = ref(null); const isStartSelected = ref(null);
const isEndSelected = ref(null); const isEndSelected = ref(null);
const isActAllTask = ref(true); const isActAllTask = ref(true);
const rowData = ref([]); const rowData = ref([]);
const selectTraceArea = ref([]); // Trace 滑快 const selectTraceArea = ref([]); // Trace slider
const isDisabled = ref(true); // Apply Button disabled setting const isDisabled = ref(true); // Apply Button disabled setting
const filterTraceViewRef = ref(null); const filterTraceViewRef = ref(null);
const tooltip = { const tooltip = {
@@ -285,7 +285,7 @@ function progressWidth(value){
return `width:${value}%;` return `width:${value}%;`
} }
//設定 Have activity(s) 內容 // Set up the Have activity(s) content
/** /**
* @param {array} data filterTaskData * @param {array} data filterTaskData
*/ */
@@ -305,7 +305,7 @@ function setHaveAct(data){
}).sort((x, y) => y.occurrences_base - x.occurrences_base); }).sort((x, y) => y.occurrences_base - x.occurrences_base);
} }
// 調整 filterStartData / filterEndData / filterStartToEndData / filterEndToStartData 的內容 // Adjust the content of filterStartData / filterEndData / filterStartToEndData / filterEndToStartData
/** /**
* @param {Array} array - filterStartToEnd or filterEndToStart data array. * @param {Array} array - filterStartToEnd or filterEndToStart data array.
*/ */
@@ -362,7 +362,7 @@ function onUpdateListSeq(newListSeq) {
isActAllTask.value = false; isActAllTask.value = false;
} }
// Start & End 若第一次選擇 start, 則 end 連動改變,若第一次選擇 end, 則 start 連動改變 // In Start & End mode, if start is selected first, end updates accordingly; if end is selected first, start updates accordingly
/** /**
* @param {object} e object contains selected row's data * @param {object} e object contains selected row's data
*/ */
@@ -387,7 +387,7 @@ function endRow(e) {
} }
} }
// 重新設定連動的 filterStartToEndData / filterEndToStartData 內容 // Reset the linked filterStartToEndData / filterEndToStartData content
/** /**
* @param {array} eventData Start or End List * @param {array} eventData Start or End List
* @param {object} rowDataVal - The selected row's data. * @param {object} rowDataVal - The selected row's data.
@@ -492,7 +492,7 @@ function reset(message) {
filterTraceViewRef.value.showTraceId = null; filterTraceViewRef.value.showTraceId = null;
filterTraceViewRef.value.selectArea = [0, filterTraceViewRef.value.traceTotal]; filterTraceViewRef.value.selectArea = [0, filterTraceViewRef.value.traceTotal];
}; };
// 成功訊息 // Success message
if(message) { if(message) {
$toast.success('Filter cleared.') $toast.success('Filter cleared.')
} }

View File

@@ -191,7 +191,7 @@
</li> </li>
</ul> </ul>
</TabPanel> </TabPanel>
<!-- shortest_traces開始迭代 --> <!-- Iterate starting from shortest_traces -->
<TabPanel v-for="([field, label], i) in fieldNamesAndLabelNames" :key="i" :header="label" <TabPanel v-for="([field, label], i) in fieldNamesAndLabelNames" :key="i" :header="label"
contentClass="text-sm"> contentClass="text-sm">
<p v-if="insights[field].length === 0" class="bg-neutral-100 p-2 rounded">No data</p> <p v-if="insights[field].length === 0" class="bg-neutral-100 p-2 rounded">No data</p>
@@ -202,7 +202,7 @@
<input type="radio" name="customRadio" :value="key2" v-model="clickedPathListIndex" <input type="radio" name="customRadio" :value="key2" v-model="clickedPathListIndex"
class="hidden peer" @click="onPathOptionClick(key2)" class="hidden peer" @click="onPathOptionClick(key2)"
/> />
<!-- 若為BPMN檢視模式則不允許點亮路徑 --> <!-- If in BPMN view mode, path highlighting is not allowed -->
<span v-if="!isBPMNOn" @click="onPathOptionClick(key2)" <span v-if="!isBPMNOn" @click="onPathOptionClick(key2)"
:class="[ :class="[
'w-[18px] h-[18px] rounded-full border-2 inline-flex items-center justify-center cursor-pointer bg-[#FFFFFF]', 'w-[18px] h-[18px] rounded-full border-2 inline-flex items-center justify-center cursor-pointer bg-[#FFFFFF]',
@@ -261,7 +261,7 @@ import getMoment from 'moment';
import i18next from '@/i18n/i18n'; import i18next from '@/i18n/i18n';
import { INSIGHTS_FIELDS_AND_LABELS } from '@/constants/constants'; import { INSIGHTS_FIELDS_AND_LABELS } from '@/constants/constants';
// 刪除第一個和第二個元素 // Remove the first and second elements
const fieldNamesAndLabelNames = [...INSIGHTS_FIELDS_AND_LABELS].slice(2); const fieldNamesAndLabelNames = [...INSIGHTS_FIELDS_AND_LABELS].slice(2);
const props = defineProps({ const props = defineProps({
@@ -332,7 +332,7 @@ function switchTab(newTab) {
*/ */
function timeLabel(time){ // sonar-qube prevent super-linear runtime due to backtracking; change * to ? function timeLabel(time){ // sonar-qube prevent super-linear runtime due to backtracking; change * to ?
// //
const label = getTimeLabel(time).replace(/\s+/g, ' '); // 將所有連續空白字符壓縮為一個空白 const label = getTimeLabel(time).replace(/\s+/g, ' '); // Collapse all consecutive whitespace into a single space
const result = label.match(/^(\d+)\s?([a-zA-Z]+)$/); // add ^ and $ to meet sonar-qube need const result = label.match(/^(\d+)\s?([a-zA-Z]+)$/); // add ^ and $ to meet sonar-qube need
return result; return result;
} }

View File

@@ -91,7 +91,7 @@ const processMap = ref({
const showTraceId = ref(null); const showTraceId = ref(null);
const infinitMaxItems = ref(false); const infinitMaxItems = ref(false);
const infiniteData = ref([]); const infiniteData = ref([]);
const infiniteFinish = ref(true); // 無限滾動是否載入完成 const infiniteFinish = ref(true); // Whether infinite scroll loading is complete
const cyTraceRef = ref(null); const cyTraceRef = ref(null);
const traceTotal = computed(() => { const traceTotal = computed(() => {
@@ -113,18 +113,18 @@ const traceList = computed(() => {
}); });
const caseData = computed(() => { const caseData = computed(() => {
const data = JSON.parse(JSON.stringify(infiniteData.value)); // 深拷貝原始 cases 的內容 const data = JSON.parse(JSON.stringify(infiniteData.value)); // Deep copy the original cases data
data.forEach(item => { data.forEach(item => {
item.attributes.forEach((attribute, index) => { item.attributes.forEach((attribute, index) => {
item[`att_${index}`] = attribute.value; // 建立新的 key-value pair item[`att_${index}`] = attribute.value; // Create a new key-value pair
}); });
delete item.attributes; // 刪除原本的 attributes 屬性 delete item.attributes; // Remove the original attributes property
}) })
return data; return data;
}); });
const columnData = computed(() => { const columnData = computed(() => {
const data = JSON.parse(JSON.stringify(props.cases)); // 深拷貝原始 cases 的內容 const data = JSON.parse(JSON.stringify(props.cases)); // Deep copy the original cases data
let result = [ let result = [
{ field: 'id', header: 'Case Id' }, { field: 'id', header: 'Case Id' },
{ field: 'started_at', header: 'Start time' }, { field: 'started_at', header: 'Start time' },
@@ -183,23 +183,23 @@ function progressWidth(value){
* @param {number} count - The total number of cases. * @param {number} count - The total number of cases.
*/ */
async function switchCaseData(id, count) { async function switchCaseData(id, count) {
// 點同一筆 id 不要有動作 // Do nothing if clicking the same id
if(id == showTraceId.value) return; if(id == showTraceId.value) return;
isLoading.value = true; // 都要 loading 畫面 isLoading.value = true; // Always show loading screen
infinit404.value = null; infinit404.value = null;
infinitMaxItems.value = false; infinitMaxItems.value = false;
showTraceId.value = id; showTraceId.value = id;
infiniteStart.value = 0; infiniteStart.value = 0;
emit('switch-Trace-Id', {id: showTraceId.value, count: count}); // 傳遞到 Map index 再關掉 loading emit('switch-Trace-Id', {id: showTraceId.value, count: count}); // Pass to Map index, which will close loading
} }
/** /**
* Assembles the trace element nodes data for Cytoscape rendering. * Assembles the trace element nodes data for Cytoscape rendering.
*/ */
function setNodesData(){ function setNodesData(){
// 避免每次渲染都重複累加 // Clear nodes to prevent accumulation on each render
processMap.value.nodes = []; processMap.value.nodes = [];
// 將 api call 回來的資料帶進 node // Populate nodes with data returned from the API call
traceTaskSeq.value.forEach((node, index) => { traceTaskSeq.value.forEach((node, index) => {
processMap.value.nodes.push({ processMap.value.nodes.push({
data: { data: {
@@ -230,7 +230,7 @@ function setEdgesData(){
} }
}); });
}); });
// 關係線數量筆節點少一個 // The number of edges is one less than the number of nodes
processMap.value.edges.pop(); processMap.value.edges.pop();
} }
@@ -249,8 +249,8 @@ function createCy(){
* create map * create map
*/ */
async function show() { async function show() {
isLoading.value = true; // createCy 執行完關閉 isLoading.value = true; // Will be closed after createCy finishes
// 因 trace api 連動,所以關閉側邊欄時讓數值歸 traces 第一筆 id // Reset to the first trace id when sidebar closes, due to trace API dependency
showTraceId.value = traces.value[0]?.id; showTraceId.value = traces.value[0]?.id;
infiniteStart.value = 0; infiniteStart.value = 0;
setNodesData(); setNodesData();

View File

@@ -8,7 +8,7 @@
<div class="my-4 border-b border-neutral-200"> <div class="my-4 border-b border-neutral-200">
<p class="h2">View</p> <p class="h2">View</p>
<ul class="space-y-3 mb-4"> <ul class="space-y-3 mb-4">
<!-- 選擇 bpmn / processmap button --> <!-- Select bpmn / processmap button -->
<li class="btn-toggle-content"> <li class="btn-toggle-content">
<span class="btn-toggle-item" :class="mapType === 'processMap'?'btn-toggle-show ':''" @click="onProcessMapClick()"> <span class="btn-toggle-item" :class="mapType === 'processMap'?'btn-toggle-show ':''" @click="onProcessMapClick()">
Process Map Process Map
@@ -17,7 +17,7 @@
BPMN Model BPMN Model
</span> </span>
</li> </li>
<!-- 選擇繪畫樣式 bezier / unbundled-bezier button--> <!-- Select drawing style: bezier / unbundled-bezier button -->
<li class="btn-toggle-content"> <li class="btn-toggle-content">
<span class="btn-toggle-item" :class="curveStyle === 'unbundled-bezier'?'btn-toggle-show ':''" @click="switchCurveStyles('unbundled-bezier')"> <span class="btn-toggle-item" :class="curveStyle === 'unbundled-bezier'?'btn-toggle-show ':''" @click="switchCurveStyles('unbundled-bezier')">
Curved Curved
@@ -26,7 +26,7 @@
Elbow Elbow
</span> </span>
</li> </li>
<!-- 直向 TB | 橫向 LR --> <!-- Vertical TB | Horizontal LR -->
<li class="btn-toggle-content"> <li class="btn-toggle-content">
<span class="btn-toggle-item" :class="rank === 'LR'?'btn-toggle-show ':''" @click="switchRank('LR')"> <span class="btn-toggle-item" :class="rank === 'LR'?'btn-toggle-show ':''" @click="switchRank('LR')">
Horizontal Horizontal
@@ -124,7 +124,7 @@ const dataLayerType = ref(null); // freq | duration
const dataLayerOption = ref(null); const dataLayerOption = ref(null);
const selectedFreq = ref(''); const selectedFreq = ref('');
const selectedDuration = ref(''); const selectedDuration = ref('');
const rank = ref('LR'); // 直向 TB | 橫向 LR const rank = ref('LR'); // Vertical TB | Horizontal LR
/** /**
* Switches the map type and emits the change event. * Switches the map type and emits the change event.

View File

@@ -163,7 +163,7 @@ onMounted(async () => {
} }
await allMapDataStore.getAllMapData(); await allMapDataStore.getAllMapData();
await getStatData(); await getStatData();
isPanel.value = false; // 預設不打開 isPanel.value = false; // Collapsed by default
}); });
</script> </script>
<style scoped> <style scoped>

View File

@@ -50,28 +50,28 @@ async function upload(event) {
const formData = new FormData(); const formData = new FormData();
let uploadFile; let uploadFile;
// 判斷是否有檔案 // Check if a file exists
if(target && target.files) { if(target && target.files) {
uploadFile = target.files[0]; uploadFile = target.files[0];
} }
// 判斷檔案大小不可超過 90MB (90(MB)*1024(KB)*1024(Bytes)=94,371,840) // File size must not exceed 90 MB (90*1024*1024 = 94,371,840 bytes)
if(uploadFile.size >= 94371840) { if(uploadFile.size >= 94371840) {
fileInput.value = ''; fileInput.value = '';
return uploadFailedFirst('size'); return uploadFailedFirst('size');
} }
// 將檔案加進 formData欄位一定要「csv // Append the file to formData; the field name must be "csv"
formData.append('csv', uploadFile); formData.append('csv', uploadFile);
// 呼叫第一階段上傳 API // Call the first-stage upload API
if(uploadFile) { if(uploadFile) {
await filesStore.upload(formData); await filesStore.upload(formData);
} }
if (uploadFile.name.endsWith('.csv')) { if (uploadFile.name.endsWith('.csv')) {
uploadFileName.value = uploadFile.name.slice(0, -4); uploadFileName.value = uploadFile.name.slice(0, -4);
} else { } else {
// 處理錯誤或無效的文件格式 // Handle error or invalid file format
uploadFileName.value = ''; // 或者其他適合的錯誤處理方式 uploadFileName.value = ''; // Or other appropriate error handling
} }
// 清除選擇文件 // Clear the selected file
if(fileInput) { if(fileInput) {
fileInput.value = ''; fileInput.value = '';
} }

View File

@@ -65,7 +65,7 @@ const toggleIsAcctMenuOpen = () => {
*/ */
function logOutButton() { function logOutButton() {
if ((route.name === 'Map' || route.name === 'CheckMap') && tempFilterId.value) { if ((route.name === 'Map' || route.name === 'CheckMap') && tempFilterId.value) {
// 傳給 Map通知 Sidebar 要關閉。 // Notify Map to close the Sidebar.
emitter.emit('leaveFilter', false); emitter.emit('leaveFilter', false);
leaveFilter(false, allMapDataStore.addFilterId, false, logOut) leaveFilter(false, allMapDataStore.addFilterId, false, logOut)
} else if((route.name === 'Conformance' || route.name === 'CheckConformance') } else if((route.name === 'Conformance' || route.name === 'CheckConformance')

View File

@@ -3,7 +3,7 @@
<div class="mx-auto px-4" :class="[showNavbarBreadcrumb? 'min-h-12': 'h-12']"> <div class="mx-auto px-4" :class="[showNavbarBreadcrumb? 'min-h-12': 'h-12']">
<div class="flex justify-between items-center flex-wrap relative" v-show="showNavbarBreadcrumb"> <div class="flex justify-between items-center flex-wrap relative" v-show="showNavbarBreadcrumb">
<div id="nav_bar_logged_in" class="flex flex-1 items-center"> <div id="nav_bar_logged_in" class="flex flex-1 items-center">
<!-- Files --> <!-- Back to Files page -->
<router-link to="/files" class="mr-4" v-if="showIcon" id="backPage"> <router-link to="/files" class="mr-4" v-if="showIcon" id="backPage">
<span class="material-symbols-outlined text-neutral-10 !leading-loose"> <span class="material-symbols-outlined text-neutral-10 !leading-loose">
arrow_back arrow_back
@@ -29,10 +29,10 @@
<UploadModal :visible="uploadModal" @closeModal="uploadModal = $event"></UploadModal> <UploadModal :visible="uploadModal" @closeModal="uploadModal = $event"></UploadModal>
</div> </div>
</div> </div>
<!-- Upload, Performance, Compare 無按鈕行為 --> <!-- Upload, Performance, Compare have no button actions -->
<div v-else-if="noShowSaveButton"></div> <div v-else-if="noShowSaveButton"></div>
<!-- Other Page: Save and Download --> <!-- Other Page: Save and Download -->
<!-- Save data 跳重新命名沒有 data 跳要不要儲存沒有動都不跳 --> <!-- Save: if data exists, prompt rename; if no data, prompt save; if unchanged, do nothing -->
<div v-else class="space-x-4"> <div v-else class="space-x-4">
<button class="btn btn-sm" :class="[ disabledSave ? 'btn-disable' : 'btn-neutral']" <button class="btn btn-sm" :class="[ disabledSave ? 'btn-disable' : 'btn-neutral']"
:disabled="disabledSave" @click="saveModal"> :disabled="disabledSave" @click="saveModal">
@@ -92,11 +92,11 @@ const { setPendingActivePage, setPreviousPage, setActivePage, setActivePageCompu
const showNavbarBreadcrumb = ref(false); const showNavbarBreadcrumb = ref(false);
const navViewData = { const navViewData = {
// 舉例:FILES: ['ALL', 'DISCOVER', 'COMPARE', 'DESIGN', 'SIMULATION'], // e.g. FILES: ['ALL', 'DISCOVER', 'COMPARE', 'DESIGN', 'SIMULATION'],
FILES: ['ALL', 'DISCOVER', 'COMPARE'], FILES: ['ALL', 'DISCOVER', 'COMPARE'],
// 舉例:DISCOVER: ['MAP', 'CONFORMANCE', 'PERFORMANCE', 'DATA'] // e.g. DISCOVER: ['MAP', 'CONFORMANCE', 'PERFORMANCE', 'DATA']
DISCOVER: ['MAP', 'CONFORMANCE', 'PERFORMANCE'], DISCOVER: ['MAP', 'CONFORMANCE', 'PERFORMANCE'],
// 舉例:COMPARE: ['PROCESS MAP', 'DASHBOARD'] // e.g. COMPARE: ['PROCESS MAP', 'DASHBOARD']
COMPARE: ['MAP', 'PERFORMANCE'], COMPARE: ['MAP', 'PERFORMANCE'],
'ACCOUNT MANAGEMENT': [], 'ACCOUNT MANAGEMENT': [],
'MY ACCOUNT': [], 'MY ACCOUNT': [],
@@ -108,7 +108,7 @@ const disabledSave = computed(() => {
switch (route.name) { switch (route.name) {
case 'Map': case 'Map':
case 'CheckMap': case 'CheckMap':
// 沒有 filter Id, 沒有暫存 tempFilterId Id 就不能存檔 // Cannot save without a filter ID or a temporary tempFilterId
return !tempFilterId.value; return !tempFilterId.value;
case 'Conformance': case 'Conformance':
case 'CheckConformance': case 'CheckConformance':
@@ -209,7 +209,7 @@ function getNavViewName() {
return; return;
} }
// 說明route.matched[1] 表示當前路由匹配的第二個路由記錄 // route.matched[1] is the second matched route record for the current route
navViewName.value = route.matched[1].name.toUpperCase(); navViewName.value = route.matched[1].name.toUpperCase();
store.filesTag = 'ALL'; store.filesTag = 'ALL';
switch (navViewName.value) { switch (navViewName.value) {
@@ -246,9 +246,9 @@ function getNavViewName() {
// Frontend is not sure which button will the user press on the modal, // Frontend is not sure which button will the user press on the modal,
// so here we need to save to a pending state // so here we need to save to a pending state
// 前端無法確定用戶稍後會按下彈窗上的哪個按鈕(取消還是確認、儲存) // The frontend cannot determine which modal button the user will press
// 因此我們需要將其保存到待處理狀態 // (cancel or confirm/save), so we save it to a pending state.
if(!shouldKeepPreviousPage.value) { // 若使用者不是按下取消按鈕或是點選按鈕時 if(!shouldKeepPreviousPage.value) { // If the user did not press cancel
setPendingActivePage(valueToSet); setPendingActivePage(valueToSet);
} }
@@ -258,8 +258,8 @@ function getNavViewName() {
/** Opens the save modal for Map or Conformance pages. */ /** Opens the save modal for Map or Conformance pages. */
async function saveModal() { async function saveModal() {
// 協助判斷 MAP, CONFORMANCE 儲存有「送出」或「取消」。 // Help determine MAP/CONFORMANCE save with "submit" or "cancel".
// 傳給 Map通知 Sidebar 要關閉。 // Notify Map to close the Sidebar.
emitter.emit('saveModal', false); emitter.emit('saveModal', false);
switch (route.name) { switch (route.name) {

View File

@@ -4,7 +4,7 @@
<div v-if="totalSeconds === 0" class="text-center"> <div v-if="totalSeconds === 0" class="text-center">
<p>0</p> <p>0</p>
</div> </div>
<!-- 這一段不是彈窗而是固定在畫面上的時間內容 --> <!-- This section shows the fixed time display, not the popup -->
<div id="cyp-timerange-show" v-else class="flex justify-center items-center gap-1"> <div id="cyp-timerange-show" v-else class="flex justify-center items-center gap-1">
<p v-show="days != 0">{{ days }}d</p> <p v-show="days != 0">{{ days }}d</p>
<p v-show="hours != 0">{{ hours }}h</p> <p v-show="hours != 0">{{ hours }}h</p>
@@ -12,7 +12,7 @@
<p v-show="seconds != 0">{{ seconds }}s</p> <p v-show="seconds != 0">{{ seconds }}s</p>
</div> </div>
</div> </div>
<!-- 以下這段落是使用者點開來才會彈出的畫面 --> <!-- The following section is the popup that appears when the user clicks to open -->
<div class="duration-container absolute left-0 top-full translate-y-2 <div class="duration-container absolute left-0 top-full translate-y-2
dhms-input-popup-container" dhms-input-popup-container"
v-show="openTimeSelect" v-show="openTimeSelect"

View File

@@ -1,6 +1,6 @@
<template> <template>
<div <div
class="relative two-imgs-container w-[18px] h-[24px] cursor-pointer mt-1 mr-2"> <!--一個relative的div承接兩個absolute的imgs元素--> class="relative two-imgs-container w-[18px] h-[24px] cursor-pointer mt-1 mr-2"> <!-- A relative div containing two absolutely positioned img elements -->
<img v-if="!isChecked" :src="ImgCheckboxGrayFrame" class="absolute" alt="checkbox"/> <img v-if="!isChecked" :src="ImgCheckboxGrayFrame" class="absolute" alt="checkbox"/>
<img v-if="isChecked" :src="ImgCheckboxBlueFrame" class="absolute" alt="checkbox"/> <img v-if="isChecked" :src="ImgCheckboxBlueFrame" class="absolute" alt="checkbox"/>
<img v-if="isChecked" :src="ImgCheckboxCheckedMark" class="absolute top-[11x] left-[2px] h-[16px] w-[14px]" alt="checkbox"/> <img v-if="isChecked" :src="ImgCheckboxCheckedMark" class="absolute top-[11x] left-[2px] h-[16px] w-[14px]" alt="checkbox"/>

View File

@@ -75,19 +75,19 @@ x: {
}, },
ticks: { ticks: {
display: true, display: true,
maxRotation: 0, // 不旋轉 lable 0~50 maxRotation: 0, // Do not rotate labels (range 0~50)
color: '#64748b', color: '#64748b',
source: 'labels', // 依比例彈性顯示 label 數量 source: 'labels', // Proportionally display the number of labels
}, },
border: { border: {
color: '#64748b', color: '#64748b',
}, },
grid: { grid: {
tickLength: 0, // 網格是否超過邊線 tickLength: 0, // Whether grid lines extend beyond the axis
} }
}, },
y: { y: {
beginAtZero: true, // scale 包含 0 beginAtZero: true, // Scale includes 0
title: { title: {
display: true, display: true,
color: '#334155', color: '#334155',
@@ -104,7 +104,7 @@ y: {
color: '#64748b', color: '#64748b',
}, },
border: { border: {
display: false, // 隱藏左側多出來的線 display: false, // Hide the extra line on the left side
}, },
}, },
}; };
@@ -121,19 +121,19 @@ x: {
}, },
ticks: { ticks: {
display: true, display: true,
maxRotation: 0, // 不旋轉 lable 0~50 maxRotation: 0, // Do not rotate labels (range 0~50)
color: '#64748b', color: '#64748b',
}, },
grid: { grid: {
color: '#64748b', color: '#64748b',
tickLength: 0, // 網格是否超過邊線 tickLength: 0, // Whether grid lines extend beyond the axis
}, },
border: { border: {
display:false, display:false,
}, },
}, },
y: { y: {
beginAtZero: true, // scale 包含 0 beginAtZero: true, // Scale includes 0
type: 'category', type: 'category',
title: { title: {
display: true, display: true,
@@ -152,7 +152,7 @@ y: {
color: '#64748b', color: '#64748b',
}, },
border: { border: {
display: false, // 隱藏左側多出來的線 display: false, // Hide the extra line on the left side
}, },
}, },
}; };
@@ -169,19 +169,19 @@ x: {
}, },
ticks: { ticks: {
display: true, display: true,
maxRotation: 0, // 不旋轉 lable 0~50 maxRotation: 0, // Do not rotate labels (range 0~50)
color: '#64748b', color: '#64748b',
}, },
grid: { grid: {
color: '#64748b', color: '#64748b',
tickLength: 0, // 網格是否超過邊線 tickLength: 0, // Whether grid lines extend beyond the axis
}, },
border: { border: {
display:false, display:false,
}, },
}, },
y: { y: {
beginAtZero: true, // scale 包含 0 beginAtZero: true, // Scale includes 0
type: 'category', type: 'category',
title: { title: {
display: true, display: true,
@@ -200,7 +200,7 @@ x: {
color: '#64748b', color: '#64748b',
}, },
border: { border: {
display: false, // 隱藏左側多出來的線 display: false, // Hide the extra line on the left side
}, },
}, },
}; };

View File

@@ -94,7 +94,7 @@ app.component('Calendar', Calendar);
app.component('Checkbox', Checkbox); app.component('Checkbox', Checkbox);
app.component('Dialog', Dialog); app.component('Dialog', Dialog);
app.component('ContextMenu', ContextMenu); app.component('ContextMenu', ContextMenu);
app.component('Draggable', draggable); // 拖曳 app.component('Draggable', draggable); // Drag and drop
app.directive('tooltip', Tooltip); app.directive('tooltip', Tooltip);
app.mount("#app"); app.mount("#app");

View File

@@ -57,17 +57,17 @@ export async function saveFilter(addFilterId, next = null) {
cancelButtonColor: '#94a3b8', cancelButtonColor: '#94a3b8',
customClass: customClass, customClass: customClass,
}); });
// 透過回傳值判斷要不要轉址 // Determine whether to redirect based on the return value
if(isConfirmed) { // 存檔成功 if(isConfirmed) { // Save succeeded
await addFilterId(fileName); await addFilterId(fileName);
// 顯示儲存完成 // Show save complete notification
if (value) { // Example of value: yes if (value) { // Example of value: yes
savedSuccessfully(value); savedSuccessfully(value);
} }
// 清空欄位 // Clear the input field
fileName = ''; fileName = '';
return true; return true;
} else { // 點擊取消或空白處,為存檔失敗。 } else { // Clicked cancel or outside the dialog; save failed.
pageAdminStore.keepPreviousPage(); pageAdminStore.keepPreviousPage();
// Not every time we have nontrivial next value // Not every time we have nontrivial next value
@@ -88,7 +88,7 @@ export async function savedSuccessfully(value) {
await Swal.fire({ await Swal.fire({
title: 'SAVE COMPLETE', title: 'SAVE COMPLETE',
html: `<span class="text-primary">${escapeHtml(value)}</span> has been saved in Lucia.`, html: `<span class="text-primary">${escapeHtml(value)}</span> has been saved in Lucia.`,
timer: 3000, // 停留 3 秒後自動關閉 timer: 3000, // Auto-close after 3 seconds
showConfirmButton: false, showConfirmButton: false,
icon: 'success', icon: 'success',
iconColor: '#0099FF', iconColor: '#0099FF',
@@ -188,15 +188,15 @@ export async function saveConformance(addConformanceCreateCheckId) {
cancelButtonColor: '#94a3b8', cancelButtonColor: '#94a3b8',
customClass: customClass, customClass: customClass,
}); });
// 透過回傳值判斷要不要轉址 // Determine whether to redirect based on the return value
if(isConfirmed) { // 存檔成功 if(isConfirmed) { // Save succeeded
await addConformanceCreateCheckId(fileName); await addConformanceCreateCheckId(fileName);
// 顯示儲存完成 // Show save complete notification
if (value) savedSuccessfully(value); if (value) savedSuccessfully(value);
// 清空欄位 // Clear the input field
fileName = ''; fileName = '';
return true; return true;
} else { // 點擊取消或空白處,為存檔失敗。 } else { // Clicked cancel or outside the dialog; save failed.
return false; return false;
} }
} }
@@ -329,7 +329,7 @@ export async function uploadFailedFirst(failureType, failureMsg, failureLoc) {
await Swal.fire({ await Swal.fire({
title: 'IMPORT FAILED', title: 'IMPORT FAILED',
html: value, html: value,
timer: 3000, // 停留 3 秒後自動關閉 timer: 3000, // Auto-close after 3 seconds
showConfirmButton: false, showConfirmButton: false,
icon: 'error', icon: 'error',
iconColor: '#FF3366', iconColor: '#FF3366',
@@ -390,7 +390,7 @@ export async function uploadFailedSecond(detail) {
await Swal.fire({ await Swal.fire({
title: 'IMPORT FAILED', title: 'IMPORT FAILED',
html: `<div class="text-left mx-3 space-y-1"><p>Error(s) detected:</p><ul class="list-disc ml-6">${srt}</ul><p>${manySrt} Please check.</p></div>`, html: `<div class="text-left mx-3 space-y-1"><p>Error(s) detected:</p><ul class="list-disc ml-6">${srt}</ul><p>${manySrt} Please check.</p></div>`,
timer: 3000, // 停留 3 秒後自動關閉 timer: 3000, // Auto-close after 3 seconds
showConfirmButton: false, showConfirmButton: false,
icon: 'error', icon: 'error',
iconColor: '#FF3366', iconColor: '#FF3366',
@@ -405,7 +405,7 @@ export async function uploadFailedSecond(detail) {
export async function uploadSuccess() { export async function uploadSuccess() {
await Swal.fire({ await Swal.fire({
title: 'IMPORT COMPLETED', title: 'IMPORT COMPLETED',
timer: 3000, // 停留 3 秒後自動關閉 timer: 3000, // Auto-close after 3 seconds
showConfirmButton: false, showConfirmButton: false,
icon: 'success', icon: 'success',
iconColor: '#0099FF', iconColor: '#0099FF',
@@ -494,9 +494,9 @@ export async function renameModal(rename, type, id, baseName) {
} }
}); });
// 改名成功 // Rename succeeded
if(isConfirmed) await rename(type, id, value); if(isConfirmed) await rename(type, id, value);
// 清空欄位 fileName = ''; // Clear the field: fileName = ''
} }
/** /**
* Shows a confirmation dialog for deleting a file and its dependents. * Shows a confirmation dialog for deleting a file and its dependents.
@@ -544,7 +544,7 @@ export async function deleteFileModal(files, type, id, name) {
export async function deleteSuccess() { export async function deleteSuccess() {
await Swal.fire({ await Swal.fire({
title: 'FILE(S) DELETED', title: 'FILE(S) DELETED',
timer: 3000, // 停留 3 秒後自動關閉 timer: 3000, // Auto-close after 3 seconds
showConfirmButton: false, showConfirmButton: false,
icon: 'success', icon: 'success',
iconColor: '#0099FF', iconColor: '#0099FF',

View File

@@ -34,12 +34,12 @@ const composeFreqTypeText = (baseText, dataLayerOption, optionValue) => {
let text = baseText; let text = baseText;
const textInt = dataLayerOption === 'rel_freq' ? baseText + optionValue * 100 + "%" : baseText + optionValue; const textInt = dataLayerOption === 'rel_freq' ? baseText + optionValue * 100 + "%" : baseText + optionValue;
const textFloat = dataLayerOption === 'rel_freq' ? baseText + (optionValue * 100).toFixed(2) + "%" : baseText + optionValue.toFixed(2); const textFloat = dataLayerOption === 'rel_freq' ? baseText + (optionValue * 100).toFixed(2) + "%" : baseText + optionValue.toFixed(2);
// 判斷是否為整數,若非整數要取小數點後面兩個值。 // Check if the value is an integer; if not, round to 2 decimal places.
text = Math.trunc(optionValue) === optionValue ? textInt : textFloat; text = Math.trunc(optionValue) === optionValue ? textInt : textFloat;
return text; return text;
}; };
// 註冊布局演算法 // Register layout algorithms
cytoscape.use(dagre); cytoscape.use(dagre);
cytoscape.use(spread); cytoscape.use(spread);
cytoscape.use(fcose); cytoscape.use(fcose);
@@ -65,7 +65,7 @@ cytoscape.use(cola);
* @returns {cytoscape.Core} The configured Cytoscape instance. * @returns {cytoscape.Core} The configured Cytoscape instance.
*/ */
export default function cytoscapeMap(mapData, dataLayerType, dataLayerOption, curveStyle, rank, graphId) { export default function cytoscapeMap(mapData, dataLayerType, dataLayerOption, curveStyle, rank, graphId) {
// 設定每個 node, edges 的顏色與樣式 // Set the color and style for each node and edge
let nodes = mapData.nodes; let nodes = mapData.nodes;
let edges = mapData.edges; let edges = mapData.edges;
@@ -73,15 +73,15 @@ export default function cytoscapeMap(mapData, dataLayerType, dataLayerOption, cu
let cy = cytoscape({ let cy = cytoscape({
container: graphId, container: graphId,
elements: { elements: {
nodes: nodes, //nodes, // 節點的資料 nodes: nodes, // Node data
edges: edges, //edges, // 關係線的資料 edges: edges, // Edge data
}, },
layout: { layout: {
name: 'dagre', name: 'dagre',
rankDir: rank, // 直向 TB | 橫向 LR, 'cytoscape-dagre' 套件裡的變數 rankDir: rank, // Vertical TB | Horizontal LR, variable from 'cytoscape-dagre' plugin
}, },
style: [ style: [
// 點擊 node 後改變的樣式 // Style changes when a node is selected
{ {
selector: 'node:selected', selector: 'node:selected',
style: { style: {
@@ -89,27 +89,27 @@ export default function cytoscapeMap(mapData, dataLayerType, dataLayerOption, cu
'border-width': '3', 'border-width': '3',
}, },
}, },
// node 節點的樣式 // Node styling
{ {
selector: 'node', selector: 'node',
style: { style: {
'label': 'label':
function (node) { // 節點要顯示的文字 function (node) { // Text to display on the node
// node.data(this.dataLayerType+"."+this.dataLayerOption) 為原先陣列 node.data.key.value // node.data(this.dataLayerType+"."+this.dataLayerOption) accesses the original array value at node.data.key.value
let optionValue = node.data(`${dataLayerType}.${dataLayerOption}`); let optionValue = node.data(`${dataLayerType}.${dataLayerOption}`);
let text = ''; let text = '';
const STRING_LIMIT = 8; const STRING_LIMIT = 8;
if (node.data('label').length > STRING_LIMIT) { if (node.data('label').length > STRING_LIMIT) {
// 若文字超過 STRING_LIMIT長度則字尾巴要加「...」style 要換兩行(\n 換行符號) // If text exceeds STRING_LIMIT, append "..." and add line breaks (\n)
// 使用 data() 是因為在 cytoscape 中從陣列轉為 function // Using data() because Cytoscape converts array data to function calls
text = `${node.data('label').substr(0, STRING_LIMIT)}...\n\n`; text = `${node.data('label').substr(0, STRING_LIMIT)}...\n\n`;
} else { // 補空白直到撐寬label的寬度這是為了統一所有label的寬度 } else { // Pad with spaces to match the label width for consistent sizing
text = `${node.data('label').padEnd(STRING_LIMIT, ' ')}\n\n` text = `${node.data('label').padEnd(STRING_LIMIT, ' ')}\n\n`
} }
// element activity 歸類在 default所以要先判斷 node 是否為 activity 才裝入文字。 // In elements, activity is categorized as default, so check if the node is an activity before adding text.
// 可使用 parseInt(整數) parseFloat(浮點數) 將字串轉為數字 // Use parseInt (integer) or parseFloat (float) to convert strings to numbers.
// Relative 要轉為百分比 % // Relative values need to be converted to percentages (%)
if (node.data('type') === 'activity') { if (node.data('type') === 'activity') {
let textDurRel; let textDurRel;
let timeLabelInt; let timeLabelInt;
@@ -120,14 +120,14 @@ export default function cytoscapeMap(mapData, dataLayerType, dataLayerOption, cu
case 'freq': // Frequency case 'freq': // Frequency
text = composeFreqTypeText(text, dataLayerOption, optionValue); text = composeFreqTypeText(text, dataLayerOption, optionValue);
break; break;
case 'duration': // Duration 除了 Relative 為百分比 % ,其他要轉變時間單位。 case 'duration': // Duration: Relative is percentage %, others need time unit conversion.
// Relative % // Relative %
textDurRel = text + (optionValue * 100).toFixed(2) + "%"; textDurRel = text + (optionValue * 100).toFixed(2) + "%";
// Timelabel // Timelabel
timeLabelInt = text + getTimeLabel(optionValue); timeLabelInt = text + getTimeLabel(optionValue);
timeLabelFloat = text + getTimeLabel(optionValue.toFixed(2)); timeLabelFloat = text + getTimeLabel(optionValue.toFixed(2));
// 判斷是否為整數,若非整數要取小數點後面兩個值。 // Check if the value is an integer; if not, round to 2 decimal places.
textTimeLabel = Math.trunc(optionValue) === optionValue ? timeLabelInt : timeLabelFloat; textTimeLabel = Math.trunc(optionValue) === optionValue ? timeLabelInt : timeLabelFloat;
text = dataLayerOption === 'rel_duration' ? textDurRel : textTimeLabel; text = dataLayerOption === 'rel_duration' ? textDurRel : textTimeLabel;
@@ -145,12 +145,12 @@ export default function cytoscapeMap(mapData, dataLayerType, dataLayerOption, cu
return node.data('type') === 'activity' ? '1' : '2'; return node.data('type') === 'activity' ? '1' : '2';
}, },
'background-image': 'data(nodeImageUrl)', 'background-image': 'data(nodeImageUrl)',
'background-opacity': 'data(backgroundOpacity)', // 透明背景 'background-opacity': 'data(backgroundOpacity)', // Transparent background
'border-opacity': 'data(borderOpacity)', // 透明邊框 'border-opacity': 'data(borderOpacity)', // Transparent border
'shape': 'data(shape)', 'shape': 'data(shape)',
'text-wrap': 'wrap', 'text-wrap': 'wrap',
'text-max-width': 'data(width)', // 在 div 內換行 'text-max-width': 'data(width)', // Wrap text within the node
'text-overflow-wrap': 'anywhere', // 在 div 內換行 'text-overflow-wrap': 'anywhere', // Allow wrapping at any position
'text-margin-x': function (node) { 'text-margin-x': function (node) {
return node.data('type') === 'activity' ? -5 : 0; return node.data('type') === 'activity' ? -5 : 0;
}, },
@@ -173,11 +173,11 @@ export default function cytoscapeMap(mapData, dataLayerType, dataLayerOption, cu
}, },
}, },
}, },
// edge 關係線的樣式 // Edge styling
{ {
selector: 'edge', selector: 'edge',
style: { style: {
'content': function (edge) { // 關係線顯示的文字 'content': function (edge) { // Text displayed on the edge
let optionValue = edge.data(`${dataLayerType}.${dataLayerOption}`); let optionValue = edge.data(`${dataLayerType}.${dataLayerOption}`);
let result = ''; let result = '';
let edgeInt; let edgeInt;
@@ -193,11 +193,11 @@ export default function cytoscapeMap(mapData, dataLayerType, dataLayerOption, cu
edgeInt = dataLayerOption === 'rel_freq' ? optionValue * 100 + "%" : optionValue; edgeInt = dataLayerOption === 'rel_freq' ? optionValue * 100 + "%" : optionValue;
edgeFloat = dataLayerOption === 'rel_freq' ? (optionValue * 100).toFixed(2) + "%" : optionValue.toFixed(2); edgeFloat = dataLayerOption === 'rel_freq' ? (optionValue * 100).toFixed(2) + "%" : optionValue.toFixed(2);
// 判斷是否為整數,若非整數要取小數點後面兩個值。 // Check if the value is an integer; if not, round to 2 decimal places.
result = Math.trunc(optionValue) === optionValue ? edgeInt : edgeFloat; result = Math.trunc(optionValue) === optionValue ? edgeInt : edgeFloat;
break; break;
case 'duration': // Duration 除了 Relative 為百分比 % ,其他要轉變時間單位。 case 'duration': // Duration: Relative is percentage %, others need time unit conversion.
// Relative % // Relative %
edgeDurRel = (optionValue * 100).toFixed(2) + "%"; edgeDurRel = (optionValue * 100).toFixed(2) + "%";
// Timelabel // Timelabel
@@ -211,10 +211,10 @@ export default function cytoscapeMap(mapData, dataLayerType, dataLayerOption, cu
return result; return result;
}, },
'curve-style': curveStyle, // unbundled-bezier | taxi 'curve-style': curveStyle, // unbundled-bezier | taxi
'overlay-opacity': 0, // overlay-opacity設置為0移除灰色陰影 'overlay-opacity': 0, // Set overlay-opacity to 0 to remove the gray shadow
'target-arrow-shape': 'triangle', // 指向目標的箭頭形狀: 三角形 'target-arrow-shape': 'triangle', // Arrow shape pointing to target: triangle
'color': 'gray', //#0066cc 'color': 'gray', //#0066cc
//'control-point-step-size':100, // 從點到點的垂直線,指定貝茲取線邊緣間的距離 //'control-point-step-size':100, // Distance between Bezier curve control points
'width': 'data(lineWidth)', 'width': 'data(lineWidth)',
'line-style': 'data(edgeStyle)', 'line-style': 'data(edgeStyle)',
"text-margin-y": "0.7rem", "text-margin-y": "0.7rem",
@@ -237,30 +237,30 @@ export default function cytoscapeMap(mapData, dataLayerType, dataLayerOption, cu
'overlay-padding': '5px', 'overlay-padding': '5px',
}, },
}, { }, {
selector: 'edge[source = target]', // 選擇 self-loop 的邊 selector: 'edge[source = target]', // Select self-loop edges
style: { style: {
'loop-direction': '0deg', // 控制 loop 的方向 'loop-direction': '0deg', // Control the loop direction
'loop-sweep': '-60deg', // 控制 loop 的弧度,這裡可以調整弧度以改變大小 'loop-sweep': '-60deg', // Control the loop arc; adjust to change size
'control-point-step-size': 50 // 控制 loop 的半徑大小,增加這個值可以增大 loop 'control-point-step-size': 50 // Control the loop radius; increase to enlarge the loop
} }
}, },
], ],
}); });
// 按下線條,線條及線條上數字有光暈效果 // When an edge is clicked, apply glow effect to the edge and its label
cy.on('tap', 'edge', function (event) { cy.on('tap', 'edge', function (event) {
cy.edges().removeClass('highlight-edge'); cy.edges().removeClass('highlight-edge');
event.target.addClass('highlight-edge'); event.target.addClass('highlight-edge');
}); });
// 按下節點光暈效果與鄰邊光暈效果 // When a node is clicked, apply glow effect to the node and adjacent edges
cy.on('tap, mousedown', 'node', function (event) { cy.on('tap, mousedown', 'node', function (event) {
useMapPathStore().onNodeClickHighlightEdges(event.target); useMapPathStore().onNodeClickHighlightEdges(event.target);
}); });
// 按下線段光暈效果與兩端點光暈效果 // When an edge is clicked, apply glow effect to the edge and both endpoint nodes
cy.on('tap, mousedown', 'edge', function (event) { cy.on('tap, mousedown', 'edge', function (event) {
useMapPathStore().onEdgeClickHighlightNodes(event.target); useMapPathStore().onEdgeClickHighlightNodes(event.target);
}); });
@@ -288,12 +288,12 @@ export default function cytoscapeMap(mapData, dataLayerType, dataLayerOption, cu
const cytoscapeStore = useCytoscapeStore(); const cytoscapeStore = useCytoscapeStore();
cy.ready(() => { cy.ready(() => {
cytoscapeStore.loadPositionsFromStorage(rank); cytoscapeStore.loadPositionsFromStorage(rank);
// 判斷localStorage是否儲存過拜訪資訊 // Check if localStorage has previously saved visit data.
// 若曾經儲存過拜訪後的座標位置則restore位置來渲染出來 // If saved node positions exist, restore them for rendering.
if (localStorage.getItem(SAVE_KEY_NAME) && JSON.parse(localStorage.getItem(SAVE_KEY_NAME))) { if (localStorage.getItem(SAVE_KEY_NAME) && JSON.parse(localStorage.getItem(SAVE_KEY_NAME))) {
const allGraphsRemembered = JSON.parse(localStorage.getItem(SAVE_KEY_NAME)); const allGraphsRemembered = JSON.parse(localStorage.getItem(SAVE_KEY_NAME));
const currentGraphNodesRemembered = const currentGraphNodesRemembered =
allGraphsRemembered[cytoscapeStore.currentGraphId] ? allGraphsRemembered[cytoscapeStore.currentGraphId][rank] : null; // 可能是undefined allGraphsRemembered[cytoscapeStore.currentGraphId] ? allGraphsRemembered[cytoscapeStore.currentGraphId][rank] : null; // May be undefined
if (currentGraphNodesRemembered) { if (currentGraphNodesRemembered) {
currentGraphNodesRemembered.forEach(nodeRemembered => { currentGraphNodesRemembered.forEach(nodeRemembered => {
const nodeToDecide = cy.getElementById(nodeRemembered.id); const nodeToDecide = cy.getElementById(nodeRemembered.id);
@@ -303,14 +303,14 @@ export default function cytoscapeMap(mapData, dataLayerType, dataLayerOption, cu
}); });
} }
} }
//存下此刻剛進入畫面時當前所有節點的座標位置 // Save the current positions of all nodes when the view is first entered
const allNodes = cy.nodes(); const allNodes = cy.nodes();
allNodes.forEach(nodeFirstlySave => { allNodes.forEach(nodeFirstlySave => {
cytoscapeStore.saveNodePosition(nodeFirstlySave.id(), nodeFirstlySave.position(), rank); cytoscapeStore.saveNodePosition(nodeFirstlySave.id(), nodeFirstlySave.position(), rank);
}); });
// 在改變節點位置後,盡可能地記錄節點線條的位置情報 // After node positions change, save the updated positions.
// rank 代表現在使用者切換的是水平方向還是垂直方向模式 // rank represents whether the user is in horizontal or vertical layout mode.
cy.on('dragfree', 'node', (event) => { cy.on('dragfree', 'node', (event) => {
const nodeToSave = event.target; const nodeToSave = event.target;
cytoscapeStore.saveNodePosition(nodeToSave.id(), nodeToSave.position(), rank); cytoscapeStore.saveNodePosition(nodeToSave.id(), nodeToSave.position(), rank);

View File

@@ -31,23 +31,23 @@ export default function cytoscapeMapTrace(nodes, edges, graphId) {
let cy = cytoscape({ let cy = cytoscape({
container: graphId, container: graphId,
elements: { elements: {
nodes: nodes, // 節點的資料 nodes: nodes, // Node data
edges: edges, // 關係線的資料 edges: edges, // Edge data
}, },
layout: { layout: {
name: 'dagre', name: 'dagre',
rankDir: 'LR' // 直向 TB | 橫向 LR, 'cytoscape-dagre' 套件裡的變數 rankDir: 'LR' // Vertical TB | Horizontal LR, variable from 'cytoscape-dagre' plugin
}, },
style: [ style: [
// node 節點的樣式 // Node styling
{ {
selector: 'node', selector: 'node',
style: { style: {
'label': 'label':
function(node) { // 節點要顯示的文字 function(node) { // Text to display on the node
let text = ''; let text = '';
// node.data('label') 為原先陣列 node.data.label // node.data('label') accesses the original array value at node.data.label
text = node.data('label').length > 18 ? `${node.data('label').substr(0,15)}...` : `${node.data('label')}`; text = node.data('label').length > 18 ? `${node.data('label').substr(0,15)}...` : `${node.data('label')}`;
return text return text
@@ -67,18 +67,18 @@ export default function cytoscapeMapTrace(nodes, edges, graphId) {
'font-size': 14, 'font-size': 14,
} }
}, },
// edge 關係線的樣式 // Edge styling
{ {
selector: 'edge', selector: 'edge',
style: { style: {
'curve-style': 'taxi', // unbundled-bezier | taxi 'curve-style': 'taxi', // unbundled-bezier | taxi
'target-arrow-shape': 'triangle', // 指向目標的箭頭形狀: 三角形 'target-arrow-shape': 'triangle', // Arrow shape pointing to target: triangle
'color': 'gray', //#0066cc 'color': 'gray', //#0066cc
'width': 'data(lineWidth)', 'width': 'data(lineWidth)',
'line-style': 'data(style)', 'line-style': 'data(style)',
} }
}, },
// 點擊 node 後改變的樣式 // Style changes when a node is selected
{ {
selector: 'node:selected', selector: 'node:selected',
style:{ style:{

View File

@@ -24,22 +24,22 @@ import getMoment from 'moment';
* with boundary points. * with boundary points.
*/ */
export function setLineChartData(baseData, xMax, xMin, isPercent, yMax, yMin) { export function setLineChartData(baseData, xMax, xMin, isPercent, yMax, yMin) {
// baseData 轉換為包含 x 和 y 屬性的物件陣列 // Convert baseData to an array of objects with x and y properties
let data = baseData.map(i => ({ x: i.x, y: i.y })); let data = baseData.map(i => ({ x: i.x, y: i.y }));
// 計算 y 軸最小值 // Calculate the Y-axis minimum value
let b = calculateYMin(baseData, isPercent, yMin, yMax); let b = calculateYMin(baseData, isPercent, yMin, yMax);
// 計算 y 軸最大值 // Calculate the Y-axis maximum value
let mf = calculateYMax(baseData, isPercent, yMin, yMax); let mf = calculateYMax(baseData, isPercent, yMin, yMax);
// 添加最小值 // Prepend the minimum value
data.unshift({ data.unshift({
x: xMin, x: xMin,
y: b, y: b,
}); });
// 添加最大值 // Append the maximum value
data.push({ data.push({
x: xMax, x: xMax,
y: mf, y: mf,
@@ -140,11 +140,11 @@ export function setBarChartData(baseData) {
* values in seconds. * values in seconds.
*/ */
export function timeRange(minTime, maxTime, amount) { export function timeRange(minTime, maxTime, amount) {
// x 軸(時間軸)的範圍是最大-最小,從最小值按照 index 累加間距到最大值 // The X-axis (time axis) range is max - min; accumulate intervals from min to max by index
const startTime = minTime; const startTime = minTime;
const endTime = maxTime; const endTime = maxTime;
let timeRange = []; // return數據初始化 let timeRange = []; // Initialize the return data array
const timeGap = (endTime - startTime) / (amount - 1); // 切分成多少段 const timeGap = (endTime - startTime) / (amount - 1); // Divide into segments
for (let i = 0; i < amount; i++) { for (let i = 0; i < amount; i++) {
timeRange.push(startTime + timeGap * i); timeRange.push(startTime + timeGap * i);
@@ -165,7 +165,7 @@ export function yTimeRange(data, yAmount, yMax) {
const yRange = []; const yRange = [];
const yGap = (1/ (yAmount-1)); const yGap = (1/ (yAmount-1));
// 貝茲曲線公式 // Cubic Bezier curve formula
const threebsr = function (t, a1, a2, a3, a4) { const threebsr = function (t, a1, a2, a3, a4) {
return ( return (
(1 - t) * (1 - t) * (1 - t) * a1 + (1 - t) * (1 - t) * (1 - t) * a1 +
@@ -204,8 +204,8 @@ export function yTimeRange(data, yAmount, yMax) {
* @returns {number} The index of the closest value in the array. * @returns {number} The index of the closest value in the array.
*/ */
export function getXIndex(data, xValue) { export function getXIndex(data, xValue) {
let closestIndex = xValue; // 假定第一个元素的索引是 0 let closestIndex = xValue; // Assume the first element index is 0
let smallestDifference = Math.abs(xValue - data[0]); // 初始差值设为第一个元素与目标数的差值 let smallestDifference = Math.abs(xValue - data[0]); // Initialize difference to the gap between the first element and target
for (let i = 0; i < data.length; i++) { for (let i = 0; i < data.length; i++) {
let difference = Math.abs(xValue - data[i]); let difference = Math.abs(xValue - data[i]);
@@ -244,7 +244,7 @@ export function formatTime(seconds) {
} }
result += `${remainingSeconds}s`; result += `${remainingSeconds}s`;
return result.trim(); // 去除最后一个空格 return result.trim(); // Remove trailing whitespace
} else { } else {
return null; return null;
} }
@@ -258,12 +258,12 @@ export function formatTime(seconds) {
export function formatMaxTwo(times) { export function formatMaxTwo(times) {
const formattedTimes = []; const formattedTimes = [];
for (let time of times) { for (let time of times) {
// 匹配數字和單位(天、小時、分鐘、秒), 假設數字不可能大於10位數 // Match numbers and units (days, hours, minutes, seconds); assume numbers have at most 10 digits
let units = time.match(/\d{1,10}[dhms]/g); let units = time.match(/\d{1,10}[dhms]/g);
let formattedTime = ''; let formattedTime = '';
let count = 0; let count = 0;
// 只保留最大的兩個單位 // Keep only the two largest units
for (let unit of units) { for (let unit of units) {
if (count >= 2) { if (count >= 2) {
break; break;
@@ -271,7 +271,7 @@ export function formatMaxTwo(times) {
formattedTime += unit + ' '; formattedTime += unit + ' ';
count++; count++;
} }
formattedTimes.push(formattedTime.trim()); // 去除末尾的空格 formattedTimes.push(formattedTime.trim()); // Remove trailing whitespace
} }
return formattedTimes; return formattedTimes;
} }

View File

@@ -95,8 +95,8 @@ export function getTimeLabel(second, fixedNumber = 0) {
const hour = 60 * 60; const hour = 60 * 60;
const minutes = 60; const minutes = 60;
// 取餘數的操作會把 second 限制在一天之內的範圍即從0到86399之間), // The modulo operation limits the value to within one day (0 to 86399),
// 意思是過了多少天後剩下多少秒。 // representing the remaining seconds after full days.
const dd = Math.floor(second / day); const dd = Math.floor(second / day);
const hh = Math.floor((second % day) / hour); const hh = Math.floor((second % day) / hour);
const mm = Math.floor((second % hour) / minutes); const mm = Math.floor((second % hour) / minutes);
@@ -226,15 +226,15 @@ export const setTimeStringFormatBaseOnTimeDifference = (minTimeStamp, maxTimeSta
let dateFormat; let dateFormat;
if (timeDifferenceInSeconds < 60) { if (timeDifferenceInSeconds < 60) {
dateFormat = 'HH:mm:ss'; // dateFormat = 'HH:mm:ss'; // Seconds range
} else if (timeDifferenceInSeconds < 3600) { } else if (timeDifferenceInSeconds < 3600) {
dateFormat = 'MM/DD HH:mm'; // 分鐘 dateFormat = 'MM/DD HH:mm'; // Minutes range
} else if (timeDifferenceInSeconds < 86400) { // 86400 = 24 小時 } else if (timeDifferenceInSeconds < 86400) { // 86400 seconds = 24 hours
dateFormat = 'MM/DD HH:mm'; // 小時 dateFormat = 'MM/DD HH:mm'; // Hours range
} else if (timeDifferenceInSeconds < 2592000) { // 2592000 = 30 } else if (timeDifferenceInSeconds < 2592000) { // 2592000 seconds = 30 days
dateFormat = 'YYYY/MM/DD'; // dateFormat = 'YYYY/MM/DD'; // Days range
} else { } else {
dateFormat = 'YYYY/MM/DD'; // dateFormat = 'YYYY/MM/DD'; // Months range
} }
return dateFormat; return dateFormat;

View File

@@ -17,7 +17,7 @@ import { PiniaPluginContext } from 'pinia';
const myPiniaPlugin = (context: PiniaPluginContext) => { const myPiniaPlugin = (context: PiniaPluginContext) => {
const { store } = context; const { store } = context;
// 在这里添加你的插件逻辑,例如: // Add your plugin logic here, for example:
store.$subscribe((mutation, state) => { store.$subscribe((mutation, state) => {
console.log(`Store mutation:`, mutation); console.log(`Store mutation:`, mutation);
console.log(`New state:`, state); console.log(`New state:`, state);

View File

@@ -27,8 +27,8 @@ import NotFound404 from '@/views/NotFound404.vue';
const routes = [ const routes = [
{ {
path: '/', // 預設進入路由 path: '/', // Default entry route
redirect: '/files', //重定向 redirect: '/files', // Redirect to /files
}, },
{ {
path: '/', path: '/',
@@ -77,7 +77,7 @@ const routes = [
component: MyAccount, component: MyAccount,
}, },
{ {
path: "/upload", // router.push({ replace: true }) 路徑不添進歷史紀錄 path: "/upload", // router.push({ replace: true }) does not add the path to history
name: "Upload", name: "Upload",
component: Upload component: Upload
}, },
@@ -86,29 +86,29 @@ const routes = [
name: "Discover", name: "Discover",
children: [ children: [
{ {
// type: log | filter,參數會有以上任一。 // type: log | filter, the parameter can be either.
// fileId: log_id | filter_id,參數會有以上任一。 // fileId: log_id | filter_id, the parameter can be either.
path: "/discover/:type/:fileId/map", path: "/discover/:type/:fileId/map",
name: "Map", name: "Map",
component: Map, component: Map,
}, },
{ {
// type: log | filter,參數會有以上任一。 // type: log | filter, the parameter can be either.
// fileId: log_id | filter_id,參數會有以上任一。 // fileId: log_id | filter_id, the parameter can be either.
path: "/discover/:type/:fileId/conformance", path: "/discover/:type/:fileId/conformance",
name: "Conformance", name: "Conformance",
component: Conformance, component: Conformance,
}, },
{ {
// type: log | filter,參數會有以上任一。 // type: log | filter, the parameter can be either.
// fileId: log_id | filter_id,參數會有以上任一。 // fileId: log_id | filter_id, the parameter can be either.
path: "/discover/:type/:fileId/performance", path: "/discover/:type/:fileId/performance",
name: "Performance", name: "Performance",
component: Performance, component: Performance,
}, },
{ {
// type: log | filter,參數會有以上任一。 // type: log | filter, the parameter can be either.
// fileId: check_id,透過 `/log-checks/{check_id}` 取得 parent data // fileId: check_id, fetches parent data via `/log-checks/{check_id}`
path: "/discover/conformance/:type/:fileId/map", path: "/discover/conformance/:type/:fileId/map",
name: "CheckMap", name: "CheckMap",
component: Map, component: Map,
@@ -117,8 +117,8 @@ const routes = [
} }
}, },
{ {
// type: log | filter,參數會有以上任一。 // type: log | filter, the parameter can be either.
// fileId: check_id,透過 `/log-checks/{check_id}` 取得 parent data // fileId: check_id, fetches parent data via `/log-checks/{check_id}`
path: "/discover/conformance/:type/:fileId/conformance", path: "/discover/conformance/:type/:fileId/conformance",
name: "CheckConformance", name: "CheckConformance",
component: Conformance, component: Conformance,
@@ -127,8 +127,8 @@ const routes = [
} }
}, },
{ {
// type: log | filter,參數會有以上任一。 // type: log | filter, the parameter can be either.
// fileId: check_id,透過 `/log-checks/{check_id}` 取得 parent data // fileId: check_id, fetches parent data via `/log-checks/{check_id}`
path: "/discover/conformance/:type/:fileId/performance", path: "/discover/conformance/:type/:fileId/performance",
name: "CheckPerformance", name: "CheckPerformance",
component: Performance, component: Performance,
@@ -169,12 +169,12 @@ const router = createRouter({
routes routes
}); });
// 全域性路由守衛 // Global navigation guard
router.beforeEach((to, from) => { router.beforeEach((to, from) => {
// to: Route: 即將要進入的目標 路由物件 // to: Route: the target route object being navigated to
// from: Route: 當前導航正要離開的路由 // from: Route: the current route being navigated away from
// 當路由到 login 時,已登入則跳轉至 Files // When navigating to the login page, redirect to Files if already logged in
if (to.name === 'Login') { if (to.name === 'Login') {
const isLoggedIn = document.cookie.split(';').some(c => c.trim().startsWith('isLuciaLoggedIn=')); const isLoggedIn = document.cookie.split(';').some(c => c.trim().startsWith('isLuciaLoggedIn='));
if (isLoggedIn) return { name: 'Files' }; if (isLoggedIn) return { name: 'Files' };

View File

@@ -47,9 +47,9 @@ export const useAcctMgmtStore = defineStore('acctMgmtStore', {
response: { response: {
deleteAccount: null, deleteAccount: null,
}, },
isOneAccountJustCreate: false, //如果有一個帳號剛剛建立,則會在列表上的第一列顯示這個帳號,並且右置一個徽章 isOneAccountJustCreate: false, // If an account was just created, display it at the top of the list with a badge
justCreateUsername: '', // unique username justCreateUsername: '', // unique username
shouldUpdateList: false, // 控制是否該刷新列表 shouldUpdateList: false, // Controls whether the list should be refreshed
}), }),
getters: {}, getters: {},
actions: { actions: {
@@ -113,9 +113,9 @@ export const useAcctMgmtStore = defineStore('acctMgmtStore', {
await loginStore.getUserData(); await loginStore.getUserData();
const loginUserData:User = loginStore.userData; const loginUserData:User = loginStore.userData;
return rawResponseData.map(user => ({ return rawResponseData.map(user => ({
...user, // 保留後端傳來的欄位 ...user, // Preserve fields from the backend response
isCurrentLoggedIn: loginUserData.username === user.username, isCurrentLoggedIn: loginUserData.username === user.username,
isDeleteHovered: false, // 針對前端顯示而額外增加的欄位 isDeleteHovered: false, // Additional fields for frontend display state
isRowHovered: false, isRowHovered: false,
isEditHovered: false, isEditHovered: false,
})); }));
@@ -267,7 +267,7 @@ export const useAcctMgmtStore = defineStore('acctMgmtStore', {
this.currentViewingUser.is_admin = response.data.roles.some(role => role.code === 'admin'); this.currentViewingUser.is_admin = response.data.roles.some(role => role.code === 'admin');
return response.status === 200; return response.status === 200;
} catch (error) { } catch (error) {
//不需要跳出錯誤,因為如果是錯誤反而是好事,表示帳號是獨一的 // No need to show an error, because an error here means the account is unique
return false; return false;
} }
}, },

View File

@@ -55,17 +55,17 @@ export const useAllMapDataStore = defineStore('allMapDataStore', {
allFilterTimeframe: {}, allFilterTimeframe: {},
allFilterTrace: [], allFilterTrace: [],
allFilterAttrs: [], allFilterAttrs: [],
hasResultRule: null, // click Apply 後檢查是否有 Data hasResultRule: null, // Whether any data remains after clicking Apply
temporaryData: [], // 沒被 apply all 的 Data temporaryData: [], // Data not yet applied via Apply All
postRuleData: [], // has-result API & temp-filters API 的 Data postRuleData: [], // Data for the has-result API and temp-filters API
ruleData: [], // Funnle view's data ruleData: [], // Funnle view's data
isRuleData: [], // toggle button data isRuleData: [], // toggle button data
allFunnelData: [], allFunnelData: [],
isUpdateFilter: false, // 是否成功儲存 Filter 檔 isUpdateFilter: false, // Whether the filter file was saved successfully
selectTimeFrame: [], // user select time start and end selectTimeFrame: [], // user select time start and end
infinite404: null, // 無限滾動式是否到底要換頁 infinite404: null, // Whether infinite scroll has reached the last page
infiniteStart: 0, // 無限滾動 case 開始的數字 infiniteStart: 0, // Starting index for infinite scroll cases
baseInfiniteStart: 0, // 無限滾動 case 開始的數字 baseInfiniteStart: 0, // Starting index for base infinite scroll cases
}), }),
getters: { getters: {
processMap: state => { processMap: state => {
@@ -273,10 +273,10 @@ export const useAllMapDataStore = defineStore('allMapDataStore', {
const min = this.allFilterTimeframe.x_axis.min; const min = this.allFilterTimeframe.x_axis.min;
const max = this.allFilterTimeframe.x_axis.max; const max = this.allFilterTimeframe.x_axis.max;
// 給 Chart.js 原始資料,格式不同的畫會錯誤輸出 // Preserve raw data for Chart.js; incorrect format causes wrong output
this.allFilterTimeframe.x_axis.min_base = min; this.allFilterTimeframe.x_axis.min_base = min;
this.allFilterTimeframe.x_axis.max_base = max; this.allFilterTimeframe.x_axis.max_base = max;
// 轉成無秒的時間格式 // Convert to a time format without seconds
this.allFilterTimeframe.x_axis.min = min !== null ? moment(min).format('YYYY/MM/DD HH:mm') : null; this.allFilterTimeframe.x_axis.min = min !== null ? moment(min).format('YYYY/MM/DD HH:mm') : null;
this.allFilterTimeframe.x_axis.max = max !== null ? moment(max).format('YYYY/MM/DD HH:mm') : null; this.allFilterTimeframe.x_axis.max = max !== null ? moment(max).format('YYYY/MM/DD HH:mm') : null;
} catch(error) { } catch(error) {

View File

@@ -29,7 +29,7 @@ export const useCompareStore = defineStore('compareStore', {
* @param { array } queryParams * @param { array } queryParams
*/ */
async getCompare(queryParams) { async getCompare(queryParams) {
// encodeURIComponent 函數用於將字串中的特殊字符進行編碼,以確保 URL 的正確性。 // encodeURIComponent encodes special characters in the string to ensure URL correctness.
const queryString = JSON.stringify(queryParams); const queryString = JSON.stringify(queryParams);
const api = `/api/compare?datasets=${encodeURIComponent(queryString)}`; const api = `/api/compare?datasets=${encodeURIComponent(queryString)}`;

View File

@@ -49,12 +49,12 @@ function getFileTypeApi(state) {
/** Pinia store for conformance checking and rule management. */ /** Pinia store for conformance checking and rule management. */
export const useConformanceStore = defineStore('conformanceStore', { export const useConformanceStore = defineStore('conformanceStore', {
state: () => ({ state: () => ({
conformanceLogId: null, // log conformanceLogId: null, // Log file
conformanceFilterId: null, // filter conformanceFilterId: null, // Filter file
conformanceLogTempCheckId: null, // log 檔存檔前的 check Id conformanceLogTempCheckId: null, // Temporary check ID for log (before saving)
conformanceFilterTempCheckId: null, // Filter 檔存檔前的 check Id conformanceFilterTempCheckId: null, // Temporary check ID for filter (before saving)
conformanceLogCreateCheckId: null, // log 檔存檔後的 check Id(rule) conformanceLogCreateCheckId: null, // Created check ID for log (rule, after saving)
conformanceFilterCreateCheckId: null, // Filter 檔存檔後的 check Id(rule) conformanceFilterCreateCheckId: null, // Created check ID for filter (rule, after saving)
allConformanceTask: [], allConformanceTask: [],
allCfmSeqStart: [], allCfmSeqStart: [],
allCfmSeqEnd: [], allCfmSeqEnd: [],
@@ -76,11 +76,11 @@ export const useConformanceStore = defineStore('conformanceStore', {
selectedActSeqMore: 'All', // radio selectedActSeqMore: 'All', // radio
selectedActSeqFromTo: 'From', // radio selectedActSeqFromTo: 'From', // radio
infinite404: null, infinite404: null,
isStartSelected: null, // start & end 連動先選擇 start isStartSelected: null, // Whether start is selected in linked start & end selection
isEndSelected: null, // start & end 連動先選擇 end isEndSelected: null, // Whether end is selected in linked start & end selection
conformanceRuleData: null, // create checkId's data to save conformanceRuleData: null, // create checkId's data to save
isUpdateConformance: false, // 成功儲存後要跳 Modal isUpdateConformance: false, // Show modal after successful save
conformanceFileName: null, // 儲存成功的 Modal conformanceFileName: null, // File name displayed in the save success modal
}), }),
getters: { getters: {
conformanceAllTasks: state => { conformanceAllTasks: state => {

View File

@@ -20,7 +20,7 @@ import moment from 'moment';
export const useConformanceInputStore = defineStore('conformanceInputStore', { export const useConformanceInputStore = defineStore('conformanceInputStore', {
state: () => ({ state: () => ({
inputDataToSave: { inputDataToSave: {
inputStart: null, // 有待釐清start 是活動的開始,還是時間的開始? inputStart: null, // TODO: clarify whether "start" means activity start or time start
inputEnd: null, inputEnd: null,
min: null, min: null,
max: null, max: null,

View File

@@ -51,7 +51,7 @@ export const useCytoscapeStore = defineStore('cytoscapeStore', {
if (!this.nodePositions[this.currentGraphId][direction]) { if (!this.nodePositions[this.currentGraphId][direction]) {
this.nodePositions[this.currentGraphId][direction] = []; this.nodePositions[this.currentGraphId][direction] = [];
} }
// 若是資訊曾經存在這張圖於localStorage // If this graph's data was previously stored in localStorage
if (localStorage.getItem(SAVE_KEY_NAME)) { if (localStorage.getItem(SAVE_KEY_NAME)) {
const nodeToSave = this.nodePositions[this.currentGraphId][direction] const nodeToSave = this.nodePositions[this.currentGraphId][direction]
.find(node => node.id === nodeId); .find(node => node.id === nodeId);

View File

@@ -147,13 +147,13 @@ export const useFilesStore = defineStore('filesStore', {
}, },
}; };
uploadloader(); // 進度條 uploadloader(); // Show loading progress bar
try { try {
const response = await apiClient.post(api, fromData, config); const response = await apiClient.post(api, fromData, config);
this.uploadId = response.data.id; this.uploadId = response.data.id;
this.$router.push({name: 'Upload'}); this.$router.push({name: 'Upload'});
Swal.close(); // 關閉進度條 Swal.close(); // Close the loading progress bar
} catch(error) { } catch(error) {
if(error.response?.status === 422) { if(error.response?.status === 422) {
// msg: 'not in UTF-8' | 'insufficient columns' | 'the csv file is empty' | 'the filename does not ends with .csv' // msg: 'not in UTF-8' | 'insufficient columns' | 'the csv file is empty' | 'the filename does not ends with .csv'
@@ -162,7 +162,7 @@ export const useFilesStore = defineStore('filesStore', {
uploadFailedFirst(detail[0].type, detail[0].msg, detail[0].loc[2]); uploadFailedFirst(detail[0].type, detail[0].msg, detail[0].loc[2]);
} else { } else {
Swal.close(); // 關閉進度條 Swal.close(); // Close the loading progress bar
apiError(error, 'Failed to upload the files.'); apiError(error, 'Failed to upload the files.');
} }
} }
@@ -189,13 +189,13 @@ export const useFilesStore = defineStore('filesStore', {
const uploadId = this.uploadId; const uploadId = this.uploadId;
const api = `/api/logs/csv-uploads/${uploadId}`; const api = `/api/logs/csv-uploads/${uploadId}`;
uploadloader(); // 進度條 uploadloader(); // Show loading progress bar
try { try {
const response = await apiClient.post(api, data); const response = await apiClient.post(api, data);
this.uploadLogId = response.data.id; this.uploadLogId = response.data.id;
Swal.close(); // 關閉進度條 Swal.close(); // Close the loading progress bar
await this.rename(); // 改檔名 await this.rename(); // Rename the file
await uploadSuccess(); await uploadSuccess();
this.$router.push({name: 'Files'}); this.$router.push({name: 'Files'});
} catch(error) { } catch(error) {
@@ -204,7 +204,7 @@ export const useFilesStore = defineStore('filesStore', {
uploadFailedSecond(detail); uploadFailedSecond(detail);
} else { } else {
Swal.close(); // 關閉進度條 Swal.close(); // Close the loading progress bar
apiError(error, 'Failed to upload the log files.'); apiError(error, 'Failed to upload the log files.');
} }
} }
@@ -216,7 +216,7 @@ export const useFilesStore = defineStore('filesStore', {
* @param {string} name - The file name. * @param {string} name - The file name.
*/ */
async rename(type, id, fileName) { async rename(type, id, fileName) {
// 先判斷有沒有 uploadLogId,有就設定 id 和 type再判斷檔案型別。 // If uploadLogId exists, set id and type accordingly; then check the file type.
if(this.uploadId && this.uploadFileName) { if(this.uploadId && this.uploadFileName) {
type = 'log'; type = 'log';
id = this.uploadLogId; id = this.uploadLogId;

View File

@@ -39,7 +39,7 @@ export const useLoginStore = defineStore('loginStore', {
const api = '/api/oauth/token'; const api = '/api/oauth/token';
const config = { const config = {
headers: { headers: {
// http post 預設的 url 編碼,非 json 格式 // Default URL-encoded format for HTTP POST, not JSON
'Content-Type':'application/x-www-form-urlencoded', 'Content-Type':'application/x-www-form-urlencoded',
}, },
}; };
@@ -48,7 +48,7 @@ export const useLoginStore = defineStore('loginStore', {
const response = await axios.post(api, this.auth, config); const response = await axios.post(api, this.auth, config);
const accessToken = response.data.access_token; const accessToken = response.data.access_token;
const refresh_token = response.data.refresh_token; const refresh_token = response.data.refresh_token;
// token 儲存在 cookie // Store the token in a cookie
setCookieWithoutExpiration("luciaToken", accessToken); setCookieWithoutExpiration("luciaToken", accessToken);
const expiryDate = new Date(); const expiryDate = new Date();
expiryDate.setMonth(expiryDate.getMonth() + 6); expiryDate.setMonth(expiryDate.getMonth() + 6);
@@ -57,9 +57,9 @@ export const useLoginStore = defineStore('loginStore', {
this.isLoggedIn = true; this.isLoggedIn = true;
setCookie("isLuciaLoggedIn", "true"); setCookie("isLuciaLoggedIn", "true");
// 大部分的情況下,預設導向至 FILES 頁面 // By default, redirect to the FILES page.
// 然而有一種情況是使用者在沒有登入的情況下貼上了某一個頁面的網址, // However, if the user pasted a URL while not logged in,
// 則在此情況下時,我們會在使用者稍後登入後,把使用者帶到剛才記住的 return-to 網址 // redirect them to the remembered return-to URL after login.
if(this.rememberedReturnToUrl !== "") { if(this.rememberedReturnToUrl !== "") {
const decodedUrl = atob(this.rememberedReturnToUrl); const decodedUrl = atob(this.rememberedReturnToUrl);
// Only allow relative paths to prevent open redirect attacks // Only allow relative paths to prevent open redirect attacks

View File

@@ -92,7 +92,7 @@ export const useMapPathStore = defineStore('mapPathStore', {
nodes: [], nodes: [],
}; // second layer index }; // second layer index
let curGraphNode, prevGraphNode, curEdge; // 配對 curGraphNode 與 nodeIndex 指向的 node let curGraphNode, prevGraphNode, curEdge; // Match curGraphNode with the node at nodeIndex
for (let nodeIndex = 0; nodeIndex < curButton[listIndex].length; nodeIndex++) { for (let nodeIndex = 0; nodeIndex < curButton[listIndex].length; nodeIndex++) {
if (nodeIndex === 0) { // special case, initialize curGraphNode if (nodeIndex === 0) { // special case, initialize curGraphNode
curGraphNode = this.startNode.outgoers('node').filter(neighborOfStart => curGraphNode = this.startNode.outgoers('node').filter(neighborOfStart =>
@@ -109,7 +109,7 @@ export const useMapPathStore = defineStore('mapPathStore', {
} }
this.insightWithPath[INSIGHTS_FIELDS_AND_LABELS[i][0]][listIndex].nodes.push(curGraphNode); this.insightWithPath[INSIGHTS_FIELDS_AND_LABELS[i][0]][listIndex].nodes.push(curGraphNode);
this.insightWithPath[INSIGHTS_FIELDS_AND_LABELS[i][0]][listIndex].edges.push(curEdge); this.insightWithPath[INSIGHTS_FIELDS_AND_LABELS[i][0]][listIndex].edges.push(curEdge);
// 特殊狀況在for迴圈之外額外插入最後一條線段 // Special case: append the last edge segment outside the loop
if (nodeIndex === curButton[listIndex].length - 1) { if (nodeIndex === curButton[listIndex].length - 1) {
const endNode = curGraphNode.outgoers('node').filter(neighbor => const endNode = curGraphNode.outgoers('node').filter(neighbor =>
neighbor.data('label').toLocaleLowerCase() === 'end' neighbor.data('label').toLocaleLowerCase() === 'end'
@@ -142,14 +142,14 @@ export const useMapPathStore = defineStore('mapPathStore', {
depthFirstSearchCreatePath(node, currentPathByNode, curPathByEdge) { depthFirstSearchCreatePath(node, currentPathByNode, curPathByEdge) {
const outgoingEdges = node.outgoers('edge'); const outgoingEdges = node.outgoers('edge');
if (outgoingEdges.length === 0) { if (outgoingEdges.length === 0) {
// 表示已經遇到尾聲 // Reached the end node
this.allPaths.push([...currentPathByNode]); this.allPaths.push([...currentPathByNode]);
this.allPathsByEdge.push([...curPathByEdge]) this.allPathsByEdge.push([...curPathByEdge])
} else { } else {
outgoingEdges.targets().forEach((targetNode) => { outgoingEdges.targets().forEach((targetNode) => {
if (!currentPathByNode.includes(targetNode)) { if (!currentPathByNode.includes(targetNode)) {
const connectingEdge = targetNode.edgesWith(currentPathByNode[currentPathByNode.length - 1]); const connectingEdge = targetNode.edgesWith(currentPathByNode[currentPathByNode.length - 1]);
// 避免loop只有當目標節點不在當前路徑中之時才繼續 // Avoid loops: only continue if the target node is not already in the current path
this.depthFirstSearchCreatePath(targetNode, [...currentPathByNode, targetNode], this.depthFirstSearchCreatePath(targetNode, [...currentPathByNode, targetNode],
[...curPathByEdge, connectingEdge] [...curPathByEdge, connectingEdge]
); );
@@ -165,13 +165,13 @@ export const useMapPathStore = defineStore('mapPathStore', {
for (let whichPath = 0; whichPath < this.allPaths.length; whichPath++) { for (let whichPath = 0; whichPath < this.allPaths.length; whichPath++) {
const curPath = this.allPaths[whichPath]; const curPath = this.allPaths[whichPath];
const curPathByEdge = this.allPathsByEdge[whichPath]; const curPathByEdge = this.allPathsByEdge[whichPath];
// 針對這個path的第一個節點找到它在insights中是對應到哪一個起點 // For the first node in this path, find which insights starting point it corresponds to
for (let i = 0; i < INSIGHTS_FIELDS_AND_LABELS.length; i++) { for (let i = 0; i < INSIGHTS_FIELDS_AND_LABELS.length; i++) {
const curButton = this.insights[INSIGHTS_FIELDS_AND_LABELS[i][0]]; const curButton = this.insights[INSIGHTS_FIELDS_AND_LABELS[i][0]];
for (let listIndex = 0; listIndex < curButton.length; listIndex++) { for (let listIndex = 0; listIndex < curButton.length; listIndex++) {
for (let nodeIndex = 0; nodeIndex < curButton[listIndex].length; nodeIndex++) { for (let nodeIndex = 0; nodeIndex < curButton[listIndex].length; nodeIndex++) {
if (curPath[1].data('label') === curButton[listIndex][nodeIndex]) { if (curPath[1].data('label') === curButton[listIndex][nodeIndex]) {
// 從 1 開始而不是從 0 開始是因為 0 的label是start字串 // Start from index 1 instead of 0 because index 0 is the "start" label
const matchResult = this.depthFirstSearchMatchTwoPaths(curPath, 1, curButton, listIndex, nodeIndex) const matchResult = this.depthFirstSearchMatchTwoPaths(curPath, 1, curButton, listIndex, nodeIndex)
if (matchResult) { if (matchResult) {
this.mapGraphPathToInsight[i] = { this.mapGraphPathToInsight[i] = {
@@ -189,39 +189,39 @@ export const useMapPathStore = defineStore('mapPathStore', {
} // end first for } // end first for
}, },
depthFirstSearchMatchTwoPaths(curPath, curPathIndex, curButton, listIndex, nodeIndex) { depthFirstSearchMatchTwoPaths(curPath, curPathIndex, curButton, listIndex, nodeIndex) {
if (listIndex >= curButton.length) { // 邊界條件檢查,防止超出範圍 if (listIndex >= curButton.length) { // Bounds check to prevent out-of-range access
return; // nodeIndex表示是當選訂了五顆按鈕之一之後清單上的第幾個path return; // nodeIndex is the path index in the list after selecting one of the five buttons
} }
if (nodeIndex >= curButton[listIndex]) { // 邊界條件檢查,防止超出範圍 if (nodeIndex >= curButton[listIndex]) { // Bounds check to prevent out-of-range access
return; // 表示清單上這個path上的第幾個節點 return; // The node index within this path in the list
} }
// 如果 `curPath` `curButton[listIndex]` 完全匹配 // If `curPath` and `curButton[listIndex]` fully match
if (curPathIndex === curPath.length || nodeIndex === curButton[listIndex].length) { if (curPathIndex === curPath.length || nodeIndex === curButton[listIndex].length) {
return true; return true;
} }
// 邊界條件檢查,防止超出範圍 // Bounds check to prevent out-of-range access
if (curPathIndex >= curPath.length || nodeIndex >= curButton[listIndex].length) { if (curPathIndex >= curPath.length || nodeIndex >= curButton[listIndex].length) {
return; return;
} }
const nodeLabel = curPath[curPathIndex].data('label'); const nodeLabel = curPath[curPathIndex].data('label');
// 如果當前節點匹配 // If the current node matches
if (nodeLabel === curButton[listIndex][nodeIndex]) { if (nodeLabel === curButton[listIndex][nodeIndex]) {
if (nodeIndex === curButton[listIndex].length - 1) { if (nodeIndex === curButton[listIndex].length - 1) {
return true; // Reach return true; // Reach
} }
//從以下兩個選項選出答案可能是true的。但也可能答案都是false // Pick the option that may return true from the two below; both could be false.
// 選項一是遞增insights的第一層的指標。這裡必須遞增path的指標 // Option 1: increment the insights first-level index; must also increment the path index.
if (this.depthFirstSearchMatchTwoPaths(curPath, curPathIndex + 1, curButton, listIndex + 1, nodeIndex)) { if (this.depthFirstSearchMatchTwoPaths(curPath, curPathIndex + 1, curButton, listIndex + 1, nodeIndex)) {
return true; return true;
} }
// 選項二是遞增insights的第一層的指標。這裡必須遞增path的指標 // Option 2: increment the insights second-level index; must also increment the path index.
if (this.depthFirstSearchMatchTwoPaths(curPath, curPathIndex + 1, curButton, listIndex, nodeIndex + 1)) { if (this.depthFirstSearchMatchTwoPaths(curPath, curPathIndex + 1, curButton, listIndex, nodeIndex + 1)) {
return true; return true;
} }
} }
return false; // 當前節點不匹配時返回 false return false; // Return false when the current node does not match
}, },
highlightClickedPath(clickedActiveTraceIndex: number, clickedPathListIndex: number) { highlightClickedPath(clickedActiveTraceIndex: number, clickedPathListIndex: number) {
this.insightWithPath[INSIGHTS_FIELDS_AND_LABELS[clickedActiveTraceIndex][0]][clickedPathListIndex].edges.forEach(edgeToHighlight => { this.insightWithPath[INSIGHTS_FIELDS_AND_LABELS[clickedActiveTraceIndex][0]][clickedPathListIndex].edges.forEach(edgeToHighlight => {

View File

@@ -323,7 +323,7 @@ onMounted(async () => {
}); });
</script> </script>
<style> <style>
/*為了讓 radio 按鈕可以置中,所以讓欄位的文字也置中 */ /* Center column text so that radio buttons are also centered */
.header-center .p-column-header-content{ .header-center .p-column-header-content{
justify-content: center; justify-content: center;
} }

View File

@@ -52,7 +52,7 @@
:class="{ :class="{
'h-[100px]': whichCurrentModal !== MODAL_CREATE_NEW, 'h-[100px]': whichCurrentModal !== MODAL_CREATE_NEW,
}" }"
> <!-- 這裡會根據究竟是create彈窗還是edit彈窗來改變Password的排版 --> > <!-- Layout of Password field changes depending on whether this is the create or edit modal -->
<span class="align-right-span flex h-full w-[122px] justify-end" <span class="align-right-span flex h-full w-[122px] justify-end"
:class="{ :class="{
'pt-[12px]': whichCurrentModal !== MODAL_CREATE_NEW, 'pt-[12px]': whichCurrentModal !== MODAL_CREATE_NEW,
@@ -100,7 +100,7 @@
</div> </div>
</div> </div>
<div v-show="false" id="confirm_pwd_row" class="input-row w-full grid grid-cols-2 grid-cols-[122px_1fr] gap-x-4 <div v-show="false" id="confirm_pwd_row" class="input-row w-full grid grid-cols-2 grid-cols-[122px_1fr] gap-x-4
mb-4 flex items-center"> <!-- 2-by-2 的格子其中左下角是一個dummy格子其沒有內容 --> mb-4 flex items-center"> <!-- 2-by-2 grid; the bottom-left cell is a dummy cell with no content -->
<span v-show="false" class="field-label w-[122px] text-sm flex items-center justify-end text-right font-medium mr-4 whitespace-nowrap" <span v-show="false" class="field-label w-[122px] text-sm flex items-center justify-end text-right font-medium mr-4 whitespace-nowrap"
:class="{ :class="{
'text-[#000000]': isPwdMatched, 'text-[#000000]': isPwdMatched,
@@ -125,14 +125,14 @@
@click="togglePwdConfirmEyeBtn" alt="eye"/> @click="togglePwdConfirmEyeBtn" alt="eye"/>
<img v-else src='@/assets/icon-eye-hide.svg' class="absolute right-[8px] cursor-pointer" @click="togglePwdConfirmEyeBtn" alt="eye"/> <img v-else src='@/assets/icon-eye-hide.svg' class="absolute right-[8px] cursor-pointer" @click="togglePwdConfirmEyeBtn" alt="eye"/>
</div> </div>
<div class="dummy-grid h-[24px]"></div> <!-- 透過 dummy-grid 來撐起高度--> <div class="dummy-grid h-[24px]"></div> <!-- Use dummy-grid to maintain the height -->
<div class="error-msg-section flex justify-start"> <div class="error-msg-section flex justify-start">
<img v-show="!isPwdMatched" src="@/assets/icon-alert.svg" alt="!" class="exclamation-img flex mr-2"> <img v-show="!isPwdMatched" src="@/assets/icon-alert.svg" alt="!" class="exclamation-img flex mr-2">
<span class="error-msg-text flex text-[#FF3366] h-[24px]"> <span class="error-msg-text flex text-[#FF3366] h-[24px]">
{{ isPwdMatched ? "" : i18next.t("AcctMgmt.PwdNotMatch") }} {{ isPwdMatched ? "" : i18next.t("AcctMgmt.PwdNotMatch") }}
</span> </span>
</div> </div>
<div class="dummy-grid h-[24px]"></div> <!-- 透過 dummy-grid 來撐起高度--> <div class="dummy-grid h-[24px]"></div> <!-- Use dummy-grid to maintain the height -->
<div class="error-msg-section flex justify-start"> <div class="error-msg-section flex justify-start">
<img v-show="!isPwdLengthValid" src="@/assets/icon-alert.svg" alt="!" class="exclamation-img flex mr-2"> <img v-show="!isPwdLengthValid" src="@/assets/icon-alert.svg" alt="!" class="exclamation-img flex mr-2">
<span class="error-msg-text flex text-[#FF3366] h-[24px]"> <span class="error-msg-text flex text-[#FF3366] h-[24px]">
@@ -142,7 +142,7 @@
</div> </div>
<div v-if="whichCurrentModal === MODAL_CREATE_NEW" class="checkbox-row w-full grid-cols-[122px_1fr] gap-x-4 flex py-2 h-[40px] my-4 items-center"> <div v-if="whichCurrentModal === MODAL_CREATE_NEW" class="checkbox-row w-full grid-cols-[122px_1fr] gap-x-4 flex py-2 h-[40px] my-4 items-center">
<div class="dummy field-label flex items-center w-[122px]"> <div class="dummy field-label flex items-center w-[122px]">
</div> <!--這裡也使用了dummy欄位去撐起空間--> </div> <!-- A dummy field is also used here to maintain spacing -->
<section id="account_create_checkboxes_section" class="flex flex-col"> <section id="account_create_checkboxes_section" class="flex flex-col">
<div class="checkbox-and-text flex"> <div class="checkbox-and-text flex">
<IconChecked :isChecked="isSetAsAdminChecked" @click="toggleIsAdmin"/> <IconChecked :isChecked="isSetAsAdminChecked" @click="toggleIsAdmin"/>
@@ -229,7 +229,7 @@ const inputPwd = ref("");
const isAccountUnique = ref(true); const isAccountUnique = ref(true);
const isEditable = ref(true); const isEditable = ref(true);
// 自從加入這段 watch 之後,填寫密碼欄位之時,就不會胡亂清空掉 account 或是 full name 欄位了。 // Since adding this watch, filling in the password field no longer clears the account or full name fields.
watch(whichCurrentModal, (newVal) => { watch(whichCurrentModal, (newVal) => {
if (newVal === MODAL_CREATE_NEW) { if (newVal === MODAL_CREATE_NEW) {
inputUserAccount.value = ''; inputUserAccount.value = '';
@@ -253,7 +253,7 @@ const validatePwdLength = () => {
} }
const onInputDoubleClick = () => { const onInputDoubleClick = () => {
// 允許編輯模式 // Enable edit mode
isEditable.value = true; isEditable.value = true;
} }
@@ -288,8 +288,8 @@ const onConfirmBtnClick = async () => {
if(!isAccountUnique.value) { if(!isAccountUnique.value) {
return; return;
} }
// 要注意的是舊的username跟新的username可以是不同的 // Note that the old username and new username can be different
// 區分有無傳入密碼的情況 // Distinguish between cases with and without a password
if(isResetPwdSectionShow.value) { if(isResetPwdSectionShow.value) {
await acctMgmtStore.editAccount( await acctMgmtStore.editAccount(
currentViewingUser.value.username, { currentViewingUser.value.username, {
@@ -315,7 +315,7 @@ const onConfirmBtnClick = async () => {
} }
const checkAccountIsUnique = async() => { const checkAccountIsUnique = async() => {
// 如果使用者沒有更動過欄位那就不用調用任何後端的API // If the user has not modified the field, no backend API call is needed
if(inputUserAccount.value === username.value) { if(inputUserAccount.value === username.value) {
return true; return true;
} }
@@ -344,14 +344,14 @@ const onInputNameFocus = () => {
const onResetPwdButtonClick = () => { const onResetPwdButtonClick = () => {
isResetPwdSectionShow.value = !isResetPwdSectionShow.value; isResetPwdSectionShow.value = !isResetPwdSectionShow.value;
// 必須清空密碼欄位輸入的字串 // Must clear the password input field
inputPwd.value = ''; inputPwd.value = '';
} }
watch( watch(
[inputPwd, inputUserAccount, inputName], [inputPwd, inputUserAccount, inputName],
([newPwd, newAccount, newName]) => { ([newPwd, newAccount, newName]) => {
// 只要[確認密碼]或[密碼]欄位有更動且所有欄位都不是空的confirm 按鈕就可點選 // Enable the confirm button when all fields are non-empty
if(newAccount.length > 0 && newName.length > 0) { if(newAccount.length > 0 && newName.length > 0) {
isConfirmDisabled.value = false; isConfirmDisabled.value = false;
} }
@@ -374,6 +374,6 @@ function onCancelBtnClick(){
<style> <style>
#modal_account_edit { #modal_account_edit {
background-color: #ffffff; background-color: #ffffff;
backdrop-filter: opacity(1); /*防止子元件繼承父元件的透明度 */ backdrop-filter: opacity(1); /* Prevent child elements from inheriting parent's opacity */
} }
</style> </style>

View File

@@ -273,7 +273,7 @@ function handleClick(tagId) {
if (isSafeTagId(tagId)) { if (isSafeTagId(tagId)) {
window.location.href = tagId; window.location.href = tagId;
} else { } else {
console.warn("不安全的 tagId: ", tagId); console.warn("Unsafe tagId: ", tagId);
} }
} }
@@ -1258,7 +1258,7 @@ function getAvgProcessTimeHorizontalBarChart(chartData, content, isSingle, xUnit
{ [secondaryTypeParam]: secondaryId } { [secondaryTypeParam]: secondaryId }
]; ];
// 取得 Compare Data // Fetch Compare Data
await compareStore.getCompare(queryParams); await compareStore.getCompare(queryParams);
avgProcessTimeByTaskHeight.value = getHorizontalBarHeight(compareDashboardData.value.time.avg_process_time_by_task); avgProcessTimeByTaskHeight.value = getHorizontalBarHeight(compareDashboardData.value.time.avg_process_time_by_task);
if(compareDashboardData.value.time.avg_waiting_time_by_edge !== null) { if(compareDashboardData.value.time.avg_waiting_time_by_edge !== null) {

View File

@@ -42,19 +42,19 @@ x: {
}, },
ticks: { ticks: {
display: true, display: true,
maxRotation: 0, // 不旋轉 lable 0~50 maxRotation: 0, // Do not rotate labels (range: 0~50)
color: '#64748b', color: '#64748b',
source: 'labels', // 依比例彈性顯示 label 數量 source: 'labels', // Dynamically display label count proportionally
}, },
border: { border: {
color: '#64748b', color: '#64748b',
}, },
grid: { grid: {
tickLength: 0, // 網格是否超過邊線 tickLength: 0, // Prevent grid lines from extending beyond the axis
} }
}, },
y: { y: {
beginAtZero: true, // scale 包含 0 beginAtZero: true, // Scale includes 0
title: { title: {
display: true, display: true,
color: '#334155', color: '#334155',
@@ -71,7 +71,7 @@ y: {
color: '#64748b', color: '#64748b',
}, },
border: { border: {
display: false, // 隱藏左側多出來的線 display: false, // Hide the extra border line on the left side
}, },
}, },
}; };
@@ -128,7 +128,7 @@ const customizeScaleChartOptionTicks = (scaleObjectToAlter, ticksOfXAxis) => {
ticks: { ticks: {
...scaleObjectToAlter.x.ticks, ...scaleObjectToAlter.x.ticks,
callback: function(value, index) { callback: function(value, index) {
// 根據不同的級距客製化 x 軸的時間刻度 // Customize x-axis time ticks based on different intervals
return ticksOfXAxis[index]; return ticksOfXAxis[index];
}, },
}, },
@@ -188,8 +188,8 @@ const getLineChartPrimeVueSetting = (chartData, content, pageName) => {
let primeVueSetData = {}; let primeVueSetData = {};
let primeVueSetOption = {}; let primeVueSetOption = {};
// 考慮 chartData.data 的dimension // Consider the dimension of chartData.data
// 當我們遇到了 Compare 頁面的案例 // When handling the Compare page case
if(pageName === "Compare"){ if(pageName === "Compare"){
datasetsPrimary = chartData.data[0].data; datasetsPrimary = chartData.data[0].data;
datasetsSecondary = chartData.data[1].data; datasetsSecondary = chartData.data[1].data;
@@ -199,7 +199,7 @@ const getLineChartPrimeVueSetting = (chartData, content, pageName) => {
label: chartData.data[0].label, label: chartData.data[0].label,
data: datasetsPrimary, data: datasetsPrimary,
fill: false, fill: false,
tension: 0, // 貝茲曲線張力 tension: 0, // Bezier curve tension
borderColor: colorPrimary, borderColor: colorPrimary,
pointBackgroundColor: colorPrimary, pointBackgroundColor: colorPrimary,
}, },
@@ -207,7 +207,7 @@ const getLineChartPrimeVueSetting = (chartData, content, pageName) => {
label: chartData.data[1].label, label: chartData.data[1].label,
data: datasetsSecondary, data: datasetsSecondary,
fill: false, fill: false,
tension: 0, // 貝茲曲線張力 tension: 0, // Bezier curve tension
borderColor: colorSecondary, borderColor: colorSecondary,
pointBackgroundColor: colorSecondary, pointBackgroundColor: colorSecondary,
} }
@@ -220,7 +220,7 @@ const getLineChartPrimeVueSetting = (chartData, content, pageName) => {
label: content.title, label: content.title,
data: datasets, data: datasets,
fill: false, fill: false,
tension: 0, // 貝茲曲線張力 tension: 0, // Bezier curve tension
borderColor: '#0099FF', borderColor: '#0099FF',
} }
]; ];
@@ -254,17 +254,17 @@ const getLineChartPrimeVueSetting = (chartData, content, pageName) => {
} }
}, },
plugins: { plugins: {
legend: false, // 圖例 legend: false, // Legend
tooltip: { tooltip: {
displayColors: true, displayColors: true,
titleFont: {weight: 'normal'}, titleFont: {weight: 'normal'},
callbacks: { callbacks: {
label: function(tooltipItem) { label: function(tooltipItem) {
// 取得數據 // Get the data
const label = tooltipItem.dataset.label || ''; const label = tooltipItem.dataset.label || '';
// 建立一個小方塊顯示顏色 // Build the tooltip label with dataset color indicator
return `${label}: ${tooltipItem.parsed.y}`; // 使用 Unicode 方塊表示顏色 return `${label}: ${tooltipItem.parsed.y}`; // Use Unicode block to represent color
}, },
}, },
}, },
@@ -275,9 +275,9 @@ const getLineChartPrimeVueSetting = (chartData, content, pageName) => {
scales: customizedScaleOption, scales: customizedScaleOption,
}; };
primeVueSetOption.scales.y.ticks.precision = 0; // y 軸顯示小數點後 0 位 primeVueSetOption.scales.y.ticks.precision = 0; // Show 0 decimal places on y-axis
primeVueSetOption.scales.y.ticks.callback = function (value, index, ticks) { primeVueSetOption.scales.y.ticks.callback = function (value, index, ticks) {
return value; //這裡的Y軸刻度沒有後綴代表時間的英文字母 return value; // Y-axis ticks here have no time unit suffix
}; };
primeVueSetDataState.value = primeVueSetData; primeVueSetDataState.value = primeVueSetData;
primeVueSetOptionsState.value = primeVueSetOption; primeVueSetOptionsState.value = primeVueSetOption;

View File

@@ -269,7 +269,7 @@ function handleClick(tagId) {
if (isSafeTagId(tagId)) { if (isSafeTagId(tagId)) {
window.location.href = tagId; window.location.href = tagId;
} else { } else {
console.warn("不安全的 tagId: ", tagId); console.warn("Unsafe tagId: ", tagId);
} }
} }
@@ -918,7 +918,7 @@ function getAvgWaitingTimeLineChart(chartData, content, yUnit) {
id = file.parent.id; id = file.parent.id;
} }
// 取得 Performance Data // Fetch Performance Data
await performanceStore.getPerformance(type, id); await performanceStore.getPerformance(type, id);
if(!performanceData.value?.time) { if(!performanceData.value?.time) {
return; return;

View File

@@ -77,7 +77,7 @@
<!-- Recently Used --> <!-- Recently Used -->
<section v-else> <section v-else>
<h2 class="h-12 font-bold py-4 mb-4 border-b border-neutral-500">Recently Used</h2> <h2 class="h-12 font-bold py-4 mb-4 border-b border-neutral-500">Recently Used</h2>
<!-- card group 最多六個--> <!-- card group, up to six items -->
<ul class="flex justify-start items-center gap-4 overflow-x-auto w-full h-[184px] scrollbar pb-4"> <ul class="flex justify-start items-center gap-4 overflow-x-auto w-full h-[184px] scrollbar pb-4">
<!-- card item v-for --> <!-- card item v-for -->
<li class="w-[216px] min-w-[216px] h-full p-4 border rounded border-neutral-300 hover:bg-primary/10 hover:border-primary duration-300 flex flex-col justify-between cursor-pointer" v-for="(file, index) in recentlyUsedFiles.slice(0, 6)" :key="file.id" @dblclick="enterDiscover(file)" :title="file.name" @contextmenu="onRightClick($event, file)" > <li class="w-[216px] min-w-[216px] h-full p-4 border rounded border-neutral-300 hover:bg-primary/10 hover:border-primary duration-300 flex flex-col justify-between cursor-pointer" v-for="(file, index) in recentlyUsedFiles.slice(0, 6)" :key="file.id" @dblclick="enterDiscover(file)" :title="file.name" @contextmenu="onRightClick($event, file)" >
@@ -485,13 +485,13 @@
async function deleteFile(type, id, name, source) { async function deleteFile(type, id, name, source) {
let srt = ''; let srt = '';
let data = []; let data = [];
// 判斷是否來自 hover icon 選單 // Check if the action comes from the hover icon menu
if(type && id && name && source === 'list-hover') { if(type && id && name && source === 'list-hover') {
selectedType.value = type; selectedType.value = type;
selectedId.value = id; selectedId.value = id;
selectedName.value = name; selectedName.value = name;
} }
// 取得相依性檔案 // Fetch dependent files
await store.getDependents(selectedType.value, selectedId.value); await store.getDependents(selectedType.value, selectedId.value);
if(dependentsData.value.length !== 0) { if(dependentsData.value.length !== 0) {
data = [...dependentsData.value]; data = [...dependentsData.value];
@@ -591,7 +591,7 @@
function getGridSortData(event) { function getGridSortData(event) {
const code = event.value.code; const code = event.value.code;
// 文字排序: 將 name 字段轉換為小寫進行比較,使用 localeCompare() 方法進行字母順序比較 // Text sorting: convert the name field to lowercase for comparison using localeCompare()
switch (code) { switch (code) {
case 'nameAscending': case 'nameAscending':
compareData.value = compareData.value.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())); compareData.value = compareData.value.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
@@ -628,7 +628,7 @@
const clickedLi = e.target.closest('li'); const clickedLi = e.target.closest('li');
if(!clickedLi || !clickedLi.id.startsWith('li')) isActive.value = null; if(!clickedLi || !clickedLi.id.startsWith('li')) isActive.value = null;
}); });
// 為 DataTable tbody 加入 .scrollbar 選擇器 // Add the .scrollbar class to the DataTable tbody
const tbodyElement = document.querySelector('.p-datatable-tbody'); const tbodyElement = document.querySelector('.p-datatable-tbody');
tbodyElement?.classList.add('scrollbar'); tbodyElement?.classList.add('scrollbar');
isLoading.value = false; isLoading.value = false;

View File

@@ -102,8 +102,8 @@ function onInputPwdFocus(){
} }
// Created logic // Created logic
// 考慮到使用者可能在未登入的情況下貼入一個頁面網址連結過來瀏覽器 // Handle the case where a user pastes a page URL into the browser without being logged in
// btoa: 對字串進行 Base64 編碼 // btoa: Base64-encode the string
if(route.query['return-to']) { if(route.query['return-to']) {
setRememberedReturnToUrl(route.query['return-to']); setRememberedReturnToUrl(route.query['return-to']);
} }

View File

@@ -33,7 +33,7 @@ import { leaveFilter, leaveConformance } from "@/module/alertModal.js";
import emitter from "@/utils/emitter"; import emitter from "@/utils/emitter";
export default { export default {
// 重新整理畫面以及第一次進入網頁時beforeRouteEnter這個hook會被執行然而beforeRouteUpdate不會被執行 // When the page is refreshed or entered for the first time, beforeRouteEnter is executed, but beforeRouteUpdate is not
// PSEUDOCODE // PSEUDOCODE
// if (not logged in) { // if (not logged in) {
// if (has refresh token) { // if (has refresh token) {
@@ -85,9 +85,9 @@ export default {
pageAdminStore.setPreviousPage(from.name); pageAdminStore.setPreviousPage(from.name);
// 離開 Map 頁時判斷是否有無資料和需要存檔 // When leaving the Map page, check if there is unsaved data
if ((from.name === 'Map' || from.name === 'CheckMap') && allMapDataStore.tempFilterId) { if ((from.name === 'Map' || from.name === 'CheckMap') && allMapDataStore.tempFilterId) {
// 傳給 Map,通知 Sidebar 要關閉。 // Notify the Map's Sidebar to close
emitter.emit('leaveFilter', false); emitter.emit('leaveFilter', false);
leaveFilter(next, allMapDataStore.addFilterId, to.path) leaveFilter(next, allMapDataStore.addFilterId, to.path)
} else if((from.name === 'Conformance' || from.name === 'CheckConformance') } else if((from.name === 'Conformance' || from.name === 'CheckConformance')

View File

@@ -94,7 +94,7 @@ import { useFilesStore } from '@/stores/files';
export default { export default {
beforeRouteEnter(to, from, next){ beforeRouteEnter(to, from, next){
// 要有 uploadID 才能進來 // An uploadID is required to enter this page
next(vm => { next(vm => {
const filesStore = useFilesStore(); const filesStore = useFilesStore();
if(filesStore.uploadId === null) { if(filesStore.uploadId === null) {
@@ -128,7 +128,7 @@ const tooltipUpload = {
4. Timestamp: The time of occurrence of a particular event, such as the start or end of an activity. 4. Timestamp: The time of occurrence of a particular event, such as the start or end of an activity.
5. Status: Activity status, such as Start or Complete. 5. Status: Activity status, such as Start or Complete.
6. Attribute: A property that can be associated with a case to provide additional information about that case.`, 6. Attribute: A property that can be associated with a case to provide additional information about that case.`,
// 暫時沒有 Resource // Resource is not available yet
// 7. Resource: A resource refers to any entity that is required to carry out a business process. This can include people, equipment, software, or any other type of asset. // 7. Resource: A resource refers to any entity that is required to carry out a business process. This can include people, equipment, software, or any other type of asset.
class: '!max-w-[400px] !text-[10px] !opacity-80', class: '!max-w-[400px] !text-[10px] !opacity-80',
autoHide: false, autoHide: false,
@@ -141,7 +141,7 @@ const columnType = [
{ name: 'Activity*', code: 'name', color: '!text-secondary', value: '', label: 'Activity', required: true }, { name: 'Activity*', code: 'name', color: '!text-secondary', value: '', label: 'Activity', required: true },
{ name: 'Activity Instance ID*', code: 'instance', color: '!text-secondary', value: '', label: 'Activity Instance ID', required: true }, { name: 'Activity Instance ID*', code: 'instance', color: '!text-secondary', value: '', label: 'Activity Instance ID', required: true },
{ name: 'Case Attribute', code: 'case_attributes', color: '!text-primary', value: '', label: 'Case Attribute', required: false }, { name: 'Case Attribute', code: 'case_attributes', color: '!text-primary', value: '', label: 'Case Attribute', required: false },
// { name: 'Resource', code: '', color: '', value: '', label: 'Resource', required: false }, // 現階段沒有,未來可能有 // { name: 'Resource', code: '', color: '', value: '', label: 'Resource', required: false }, // Not available yet; may be added in the future
{ name: 'Not Assigned', code: '', color: '!text-neutral-700', value: '', label: 'Not Assigned', required: false }, { name: 'Not Assigned', code: '', color: '!text-neutral-700', value: '', label: 'Not Assigned', required: false },
]; ];
@@ -153,8 +153,8 @@ const showEdit = ref(false);
// Computed // Computed
const isDisabled = computed(() => { const isDisabled = computed(() => {
// 1. 長度一樣,強制每一個都要選 // 1. Length must match; every column must be assigned
// 2. 不為 null undefind // 2. Must not be null or undefined
const hasValue = !selectedColumns.value.includes(undefined); const hasValue = !selectedColumns.value.includes(undefined);
const result = !(selectedColumns.value.length === uploadDetail.value?.columns.length const result = !(selectedColumns.value.length === uploadDetail.value?.columns.length
&& informData.value.length === 0 && repeatedData.value.length === 0 && hasValue); && informData.value.length === 0 && repeatedData.value.length === 0 && hasValue);
@@ -203,7 +203,7 @@ function onInput(e) {
* @returns {number} The text width in pixels. * @returns {number} The text width in pixels.
*/ */
function getTextWidth(text, e) { function getTextWidth(text, e) {
// 替換空格為不斷行的空格 // Replace spaces with non-breaking spaces
const processedText = text.replace(/ /g, '\u00a0'); const processedText = text.replace(/ /g, '\u00a0');
const hiddenSpan = document.createElement('span'); const hiddenSpan = document.createElement('span');
@@ -223,24 +223,24 @@ function getTextWidth(text, e) {
*/ */
function updateValidationData(data) { function updateValidationData(data) {
const nameOccurrences = {}; const nameOccurrences = {};
const noSortedRepeatedData = []; // 未排序的重複選擇的 data const noSortedRepeatedData = []; // Unsorted duplicate selections
const selectedData = [] // 已經選擇的 data const selectedData = [] // Already selected data
informData.value = []; // 尚未選擇的 data informData.value = []; // Not yet selected data
repeatedData.value = []; // 重複選擇的 data repeatedData.value = []; // Duplicate selections
data.forEach(item => { data.forEach(item => {
const { name, code } = item; const { name, code } = item;
if(nameOccurrences[name]) { if(nameOccurrences[name]) {
// 'Not Assigned''Case Attribute' 不列入驗證 // 'Not Assigned' and 'Case Attribute' are excluded from validation
if(!code || code === 'case_attributes') return; if(!code || code === 'case_attributes') return;
nameOccurrences[name]++; nameOccurrences[name]++;
// 重複的選項只出現一次 // Each duplicate option should only appear once
if(nameOccurrences[name] === 2){ if(nameOccurrences[name] === 2){
noSortedRepeatedData.push(item) noSortedRepeatedData.push(item)
} }
// 要按照選單的順序排序 // Sort according to the dropdown menu order
repeatedData.value = columnType.filter(column => noSortedRepeatedData.includes(column)); repeatedData.value = columnType.filter(column => noSortedRepeatedData.includes(column));
}else { }else {
nameOccurrences[name] = 1; nameOccurrences[name] = 1;
@@ -252,13 +252,13 @@ function updateValidationData(data) {
/** Resets all column selections to default. */ /** Resets all column selections to default. */
function reset() { function reset() {
// 路徑不列入歷史紀錄 // Do not add to browser history
selectedColumns.value = []; selectedColumns.value = [];
} }
/** Navigates back to the Files page without uploading. */ /** Navigates back to the Files page without uploading. */
function cancel() { function cancel() {
// 路徑不列入歷史紀錄 // Do not add to browser history
router.push({name: 'Files', replace: true}); router.push({name: 'Files', replace: true});
} }
@@ -273,7 +273,7 @@ async function submit() {
status: '', status: '',
case_attributes: [] case_attributes: []
}; };
// 給值 // Assign values
const haveValueData = selectedColumns.value.map((column, i) => { const haveValueData = selectedColumns.value.map((column, i) => {
if (column && uploadDetail.value.columns[i]) { if (column && uploadDetail.value.columns[i]) {
return { return {
@@ -285,9 +285,9 @@ async function submit() {
} }
}); });
// 取得欲更改的檔名, // Get the desired file name to change
uploadFileName.value = fileName.value; uploadFileName.value = fileName.value;
// 設定第二階段上傳的 data // Set the data for the second-stage upload
haveValueData.forEach(column => { haveValueData.forEach(column => {
if(column !== undefined) { if(column !== undefined) {
switch (column.code) { switch (column.code) {
@@ -319,7 +319,7 @@ async function submit() {
// Mounted // Mounted
onMounted(async () => { onMounted(async () => {
// 只監聯第一次 // Watch only once
const unwatch = watch(fileName, (newValue) => { const unwatch = watch(fileName, (newValue) => {
if (newValue) { if (newValue) {
const inputElement = document.getElementById('fileNameInput'); const inputElement = document.getElementById('fileNameInput');
@@ -332,13 +332,13 @@ onMounted(async () => {
); );
showEdit.value = true; showEdit.value = true;
if(uploadId.value) await filesStore.getUploadDetail(); if(uploadId.value) await filesStore.getUploadDetail();
selectedColumns.value = Array.from({ length: uploadDetail.value.columns.length }, () => columnType[columnType.length - 1]); // 預設選 Not Assigned selectedColumns.value = Array.from({ length: uploadDetail.value.columns.length }, () => columnType[columnType.length - 1]); // Default to "Not Assigned"
unwatch(); unwatch();
isLoading.value = false; isLoading.value = false;
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
// 離開頁面要刪 uploadID // Clear uploadID when leaving the page
uploadId.value = null; uploadId.value = null;
uploadFileName.value = null; uploadFileName.value = null;
}); });