feat: upload done.
This commit is contained in:
@@ -18,6 +18,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* vue-toast-notification */
|
/* vue-toast-notification */
|
||||||
|
.v-toast {
|
||||||
|
@apply z-[99999]
|
||||||
|
}
|
||||||
.v-toast__item {
|
.v-toast__item {
|
||||||
@apply min-h-[48px] rounded-full
|
@apply min-h-[48px] rounded-full
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,29 +4,30 @@
|
|||||||
<div class="py-5">
|
<div class="py-5">
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class=" h-full flex flex-col justify-center items-center p-4 space-y-4">
|
<label for="uploadFiles">
|
||||||
<IconUploarding class="loader-arrow-upward"></IconUploarding>
|
<div class=" h-full flex flex-col justify-center items-center p-4 space-y-4 relative">
|
||||||
<p class="text-neutral-900">Click or drag a file here.</p>
|
<input id="uploadFiles" class=" absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer" type="file" accept="text/csv" @change="upload($event)">
|
||||||
<p class="text-neutral-500">(Only <span class="text-primary">.csv</span> is supported)</p>
|
<IconUploarding class="loader-arrow-upward"></IconUploarding>
|
||||||
<router-link class="btn btn-lg btn-c-primary" :to="{name: 'Upload', replace: true}">下一頁,路徑不添進歷史紀錄</router-link>
|
<p class="text-neutral-900">Click or drag a file here.</p>
|
||||||
<p class="btn btn-lg btn-c-primary" @click="uploadFailde">上傳失敗 Modal</p>
|
<p class="text-neutral-500">(Only <span class="text-primary">.csv</span> is supported)</p>
|
||||||
<p class="btn btn-lg btn-c-primary" @click="uploadSuccess">上傳成功 Modal</p>
|
</div>
|
||||||
<p class="btn btn-lg btn-c-primary" @click="uploadConfirm">是否上傳 Modal</p>
|
</label>
|
||||||
<label class="btn btn-sm btn-neutral cursor-pointer">
|
|
||||||
hidden
|
|
||||||
<input id="uploadFiles" class="" type="file" @change="upload($event)">
|
|
||||||
Upload
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import files from '../../stores/files';
|
|
||||||
import IconUploarding from '../icons/IconUploarding.vue';
|
import IconUploarding from '../icons/IconUploarding.vue';
|
||||||
import { uploadFailde, uploadSuccess, uploadConfirm } from '@/module/alertModal.js'
|
import { uploadFailed } from '@/module/alertModal.js'
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import FilesStore from '@/stores/files.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['uploadModal'],
|
props: ['uploadModal'],
|
||||||
|
setup() {
|
||||||
|
const filesStore = FilesStore();
|
||||||
|
const { uploadFileName } = storeToRefs(filesStore);
|
||||||
|
|
||||||
|
return { filesStore, uploadFileName }
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
contentClass: 'h-full',
|
contentClass: 'h-full',
|
||||||
@@ -36,27 +37,28 @@ export default {
|
|||||||
IconUploarding,
|
IconUploarding,
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
uploadFailde,
|
async upload(event) {
|
||||||
uploadSuccess,
|
const fileInput = document.getElementById('uploadFiles');
|
||||||
uploadConfirm,
|
const target = event.target;
|
||||||
upload(event) {
|
const formData = new FormData();
|
||||||
const target = event.target
|
let uploadFile;
|
||||||
console.log(target.files);
|
|
||||||
let file;
|
|
||||||
let form;
|
|
||||||
|
|
||||||
if(target && target.files) files.value = target.files[0];
|
// 判斷是否有檔案
|
||||||
if(file.value) {
|
if(target && target.files) uploadFile = target.files[0];
|
||||||
try {
|
// 判斷檔案大小不可超過 90MB (90(MB)*1024(KB)*1024(Bytes)=94,371,840)
|
||||||
|
if(uploadFile.size >= 94371840) return uploadFailed('size')
|
||||||
} catch(error) {
|
// 將檔案加進 formData,欄位一定要「csv」
|
||||||
console.log('error', error);
|
formData.append('csv', uploadFile);
|
||||||
form.value?.reset();
|
// 呼叫第一階段上傳 API
|
||||||
file.value = null;
|
if(uploadFile) await this.filesStore.upload(formData);
|
||||||
}
|
this.uploadFileName = (uploadFile.name).match(/(.*)\.csv/)[1];
|
||||||
}
|
// 清除選擇文件
|
||||||
|
if(fileInput) fileInput.value = '';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
this.$emit('closeModal', false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ app.use(VueAxios, axios);
|
|||||||
app.use(VueSweetalert2);
|
app.use(VueSweetalert2);
|
||||||
app.use(ToastPlugin, { // use `this.$toast` in Vue.js
|
app.use(ToastPlugin, { // use `this.$toast` in Vue.js
|
||||||
position: 'bottom',
|
position: 'bottom',
|
||||||
duration: 3000,
|
duration: 99999999,
|
||||||
});
|
});
|
||||||
app.use(PrimeVue);
|
app.use(PrimeVue);
|
||||||
app.component('Sidebar', Sidebar);
|
app.component('Sidebar', Sidebar);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Swal from 'sweetalert2';
|
import Swal from 'sweetalert2';
|
||||||
import AllMapDataStore from '@/stores/allMapData.js';
|
import AllMapDataStore from '@/stores/allMapData.js';
|
||||||
import ConformanceStore from '@/stores/conformance.js';
|
import ConformanceStore from '@/stores/conformance.js';
|
||||||
import LoginStore from '@/stores/login.js';
|
import FilesStore from '@/stores/files.js';
|
||||||
|
|
||||||
const customClass = {
|
const customClass = {
|
||||||
container: '!z-[99999]',
|
container: '!z-[99999]',
|
||||||
@@ -168,13 +168,36 @@ export async function leaveConformance(next, addConformanceCreateCheckId, toPath
|
|||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* Upload failde
|
* Upload failde
|
||||||
* @param { string } value
|
* @param { string } failureType
|
||||||
|
* @param { string } failureMsg
|
||||||
*/
|
*/
|
||||||
export async function uploadFailde(value) {
|
export async function uploadFailed(failureType, failureMsg) {
|
||||||
value = value ? value : '';
|
// msg: 'not in UTF-8' | 'insufficient columns' | 'the csv file is empty' | 'the filename does not ends with .csv'
|
||||||
|
// type: 'encoding' | 'insufficient_columns' | 'empty' | 'name_suffix'
|
||||||
|
let value = '';
|
||||||
|
switch (failureType) {
|
||||||
|
case 'size':
|
||||||
|
value = '檔案大小不可超過 90MB';
|
||||||
|
break;
|
||||||
|
case 'encoding':
|
||||||
|
value = 'not in UTF-8';
|
||||||
|
break;
|
||||||
|
case 'insufficient_columns':
|
||||||
|
value = 'insufficient columns';
|
||||||
|
break;
|
||||||
|
case 'empty':
|
||||||
|
value = 'the csv file is empty';
|
||||||
|
break;
|
||||||
|
case 'name_suffix':
|
||||||
|
value = 'the filename does not ends with .csv';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
value = failureMsg;
|
||||||
|
break;
|
||||||
|
}
|
||||||
await Swal.fire({
|
await Swal.fire({
|
||||||
title: 'UPLOAD FAILED',
|
title: 'UPLOAD FAILED',
|
||||||
html: `(錯誤樣態由前後端自行確認,呈現於此處) e.g.您的資料格式錯誤,請檢查資料後再重新上傳。`,
|
html: value,
|
||||||
timer: 5000, // 停留5秒後自動關閉
|
timer: 5000, // 停留5秒後自動關閉
|
||||||
showConfirmButton: false,
|
showConfirmButton: false,
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
@@ -197,8 +220,12 @@ export async function uploadSuccess() {
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* Confirm whether to upload the file */
|
* Confirm whether to upload the file
|
||||||
export async function uploadConfirm() {
|
* @param { object } fetchData
|
||||||
|
*/
|
||||||
|
export async function uploadConfirm(fetchData) {
|
||||||
|
const filesStore = FilesStore();
|
||||||
|
|
||||||
const result = await Swal.fire({
|
const result = await Swal.fire({
|
||||||
title: 'ARE YOU SURE?',
|
title: 'ARE YOU SURE?',
|
||||||
html: 'After uploading, you won’t be able to modify labels.',
|
html: 'After uploading, you won’t be able to modify labels.',
|
||||||
@@ -210,10 +237,10 @@ export async function uploadConfirm() {
|
|||||||
showCancelButton: true,
|
showCancelButton: true,
|
||||||
cancelButtonText: 'No',
|
cancelButtonText: 'No',
|
||||||
cancelButtonColor: '#94a3b8',
|
cancelButtonColor: '#94a3b8',
|
||||||
customClass: customClass
|
customClass: customClass,
|
||||||
})
|
})
|
||||||
if(result.isConfirmed) {
|
if(result.isConfirmed) {
|
||||||
uploadloader(); // 跑馬燈
|
filesStore.uploadLog(fetchData);
|
||||||
} else if(result.dismiss === 'cancel') {
|
} else if(result.dismiss === 'cancel') {
|
||||||
// 什麼都不做
|
// 什麼都不做
|
||||||
} else if(result.dismiss === 'backdrop') {
|
} else if(result.dismiss === 'backdrop') {
|
||||||
@@ -226,10 +253,9 @@ export async function uploadConfirm() {
|
|||||||
export async function uploadloader() {
|
export async function uploadloader() {
|
||||||
await Swal.fire({
|
await Swal.fire({
|
||||||
html: '<span class="loaderBar mt-7"></span>',
|
html: '<span class="loaderBar mt-7"></span>',
|
||||||
timer: 5000, // 停留5秒後自動關閉
|
// timer: 5000, // 停留5秒後自動關閉
|
||||||
showConfirmButton: false,
|
showConfirmButton: false,
|
||||||
allowOutsideClick: false,
|
allowOutsideClick: false,
|
||||||
// allowOutsideClick: () => !Swal.isLoading()
|
customClass: customClass,
|
||||||
customClass: customClass
|
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,37 +2,11 @@ import { defineStore } from "pinia";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import apiError from '@/module/apiError.js';
|
import apiError from '@/module/apiError.js';
|
||||||
|
import Swal from 'sweetalert2';
|
||||||
|
import { uploadFailed, uploadloader, uploadSuccess } from '@/module/alertModal.js'
|
||||||
|
|
||||||
export default defineStore('filesStore', {
|
export default defineStore('filesStore', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
allFilter: [
|
|
||||||
{
|
|
||||||
log: {},
|
|
||||||
fileType: '',
|
|
||||||
ownerName: '',
|
|
||||||
}
|
|
||||||
],
|
|
||||||
allConformanceLog: [
|
|
||||||
{
|
|
||||||
log: {},
|
|
||||||
fileType: '',
|
|
||||||
ownerName: '',
|
|
||||||
}
|
|
||||||
],
|
|
||||||
allConformanceFilter: [
|
|
||||||
{
|
|
||||||
filter: {},
|
|
||||||
fileType: '',
|
|
||||||
ownerName: '',
|
|
||||||
}
|
|
||||||
],
|
|
||||||
allEventLog: [
|
|
||||||
{
|
|
||||||
parentLog: '',
|
|
||||||
fileType: '',
|
|
||||||
ownerName: '',
|
|
||||||
}
|
|
||||||
],
|
|
||||||
allEventFiles: [
|
allEventFiles: [
|
||||||
{
|
{
|
||||||
parentLog: '',
|
parentLog: '',
|
||||||
@@ -48,6 +22,10 @@ export default defineStore('filesStore', {
|
|||||||
},
|
},
|
||||||
filesTag: 'ALL',
|
filesTag: 'ALL',
|
||||||
httpStatus: 200,
|
httpStatus: 200,
|
||||||
|
uploadId: null,
|
||||||
|
allUploadDetail: null,
|
||||||
|
uploadLogId: null,
|
||||||
|
uploadFileName: null,
|
||||||
}),
|
}),
|
||||||
getters: {
|
getters: {
|
||||||
/**
|
/**
|
||||||
@@ -62,105 +40,14 @@ export default defineStore('filesStore', {
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Get upload preview
|
||||||
|
*/
|
||||||
|
uploadDetail: state => {
|
||||||
|
return state.allUploadDetail;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
/**
|
|
||||||
* Fetch event logs api
|
|
||||||
*/
|
|
||||||
async fetchEventLog() {
|
|
||||||
const api = '/api/logs';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await axios.get(api);
|
|
||||||
|
|
||||||
this.allEventLog = response.data;
|
|
||||||
this.allEventLog.map(o => {
|
|
||||||
o.icon = 'work_history';
|
|
||||||
o.parentLog = o.name;
|
|
||||||
o.fileType = "Log";
|
|
||||||
o.ownerName = o.owner.name;
|
|
||||||
o.updated_base = o.updated_at;
|
|
||||||
o.accessed_base = o.accessed_at;
|
|
||||||
o.updated_at = moment(o.updated_at).utcOffset('+08:00').format('YYYY-MM-DD HH:mm');
|
|
||||||
o.accessed_at = o.accessed_at ? moment(o.accessed_at).utcOffset('+08:00').format('YYYY-MM-DD HH:mm') : null;
|
|
||||||
return this.allEventLog
|
|
||||||
})
|
|
||||||
} catch(error) {
|
|
||||||
apiError(error, 'Failed to load the logs.');
|
|
||||||
};
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Fetch filters api
|
|
||||||
*/
|
|
||||||
async fetchFilter() {
|
|
||||||
const api = '/api/filters';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await axios.get(api);
|
|
||||||
|
|
||||||
this.allFilter = response.data;
|
|
||||||
this.allFilter.map(o => {
|
|
||||||
o.icon = 'tornado';
|
|
||||||
o.parentLog = o.log.name;
|
|
||||||
o.fileType = "Filter";
|
|
||||||
o.ownerName = o.owner.name;
|
|
||||||
o.updated_base = o.updated_at;
|
|
||||||
o.accessed_base = o.accessed_at;
|
|
||||||
o.updated_at = moment(o.updated_at).utcOffset('+08:00').format('YYYY-MM-DD HH:mm');
|
|
||||||
o.accessed_at = o.accessed_at ? moment(o.accessed_at).utcOffset('+08:00').format('YYYY-MM-DD HH:mm') : null;
|
|
||||||
});
|
|
||||||
} catch(error) {
|
|
||||||
apiError(error, 'Failed to load the filters.');
|
|
||||||
};
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Fetch Conformance Log api
|
|
||||||
*/
|
|
||||||
async fetchConformanceLog() {
|
|
||||||
const api = '/api/log-checks';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await axios.get(api);
|
|
||||||
|
|
||||||
this.allConformanceLog = response.data;
|
|
||||||
this.allConformanceLog.map(o => {
|
|
||||||
o.icon = 'local_police';
|
|
||||||
o.parentLog = o.log.name;
|
|
||||||
o.fileType = "Rule";
|
|
||||||
o.ownerName = o.owner.name;
|
|
||||||
o.updated_base = o.updated_at;
|
|
||||||
o.accessed_base = o.accessed_at;
|
|
||||||
o.updated_at = moment(o.updated_at).utcOffset('+08:00').format('YYYY-MM-DD HH:mm');
|
|
||||||
o.accessed_at = o.accessed_at ? moment(o.accessed_at).utcOffset('+08:00').format('YYYY-MM-DD HH:mm') : null;
|
|
||||||
});
|
|
||||||
} catch(error) {
|
|
||||||
apiError(error, 'Failed to load the filters.');
|
|
||||||
};
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Fetch Conformance Filter api
|
|
||||||
*/
|
|
||||||
async fetchConformanceFilter() {
|
|
||||||
const api = '/api/filter-checks';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await axios.get(api);
|
|
||||||
|
|
||||||
this.allConformanceFilter = response.data;
|
|
||||||
this.allConformanceFilter.map(o => {
|
|
||||||
o.icon = 'local_police';
|
|
||||||
o.parentLog = o.filter.name;
|
|
||||||
o.fileType = "Rule";
|
|
||||||
o.ownerName = o.owner.name;
|
|
||||||
o.updated_base = o.updated_at;
|
|
||||||
o.accessed_base = o.accessed_at;
|
|
||||||
o.updated_at = moment(o.updated_at).utcOffset('+08:00').format('YYYY-MM-DD HH:mm');
|
|
||||||
o.accessed_at = o.accessed_at ? moment(o.accessed_at).utcOffset('+08:00').format('YYYY-MM-DD HH:mm') : null;
|
|
||||||
});
|
|
||||||
} catch(error) {
|
|
||||||
apiError(error, 'Failed to load the filters.');
|
|
||||||
};
|
|
||||||
},
|
|
||||||
/**
|
/**
|
||||||
* Fetch All Files api
|
* Fetch All Files api
|
||||||
*/
|
*/
|
||||||
@@ -192,6 +79,9 @@ export default defineStore('filesStore', {
|
|||||||
fileType = 'Rule';
|
fileType = 'Rule';
|
||||||
parentLog = o.parent.name;
|
parentLog = o.parent.name;
|
||||||
break;
|
break;
|
||||||
|
case 'design':
|
||||||
|
icon = 'shape_line';
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
o.icon = icon;
|
o.icon = icon;
|
||||||
o.parentLog = parentLog;
|
o.parentLog = parentLog;
|
||||||
@@ -206,7 +96,92 @@ export default defineStore('filesStore', {
|
|||||||
apiError(error, 'Failed to load the files.');
|
apiError(error, 'Failed to load the files.');
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
// fetchRule(){o.icon = local_police}
|
/**
|
||||||
// fetchDesign(){o.icon = shape_line}
|
* Uploads a CSV log file. 第一階段上傳
|
||||||
|
* @param {Object} fromData
|
||||||
|
*/
|
||||||
|
async upload(fromData) {
|
||||||
|
const api = '/api/logs/csv-uploads';
|
||||||
|
const config = {
|
||||||
|
data: true,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
uploadloader(); // 進度條
|
||||||
|
try {
|
||||||
|
const response = await axios.post(api, fromData, config);
|
||||||
|
this.uploadId = response.data.id;
|
||||||
|
this.$router.push({name: 'Upload'});
|
||||||
|
Swal.close(); // 關閉進度條
|
||||||
|
} catch(error) {
|
||||||
|
if(error.response.status === 422) {
|
||||||
|
// msg: 'not in UTF-8' | 'insufficient columns' | 'the csv file is empty' | 'the filename does not ends with .csv'
|
||||||
|
// type: 'encoding' | 'insufficient_columns' | 'empty' | 'name_suffix'
|
||||||
|
uploadFailed(error.response.data.detail[0].type, error.response.data.detail[0].msg);
|
||||||
|
} else {
|
||||||
|
Swal.close(); // 關閉進度條
|
||||||
|
apiError(error, 'Failed to upload the files.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Fetch upload detail
|
||||||
|
*/
|
||||||
|
async getUploadDetail() {
|
||||||
|
const uploadId = this.uploadId;
|
||||||
|
const api = `/api/logs/csv-uploads/${uploadId}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(api);
|
||||||
|
this.allUploadDetail = response.data.preview;
|
||||||
|
} catch(error) {
|
||||||
|
apiError(error, 'Failed to get upload detail.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Add a Log from an Uploaded CSV Log File. 第二階段上傳
|
||||||
|
* @param {Object} data
|
||||||
|
*/
|
||||||
|
async uploadLog(data) {
|
||||||
|
const uploadId = this.uploadId;
|
||||||
|
const api = `/api/logs/csv-uploads/${uploadId}`;
|
||||||
|
|
||||||
|
uploadloader(); // 進度條
|
||||||
|
try {
|
||||||
|
const response = await axios.post(api, data);
|
||||||
|
this.uploadLogId = response.data.id;
|
||||||
|
Swal.close(); // 關閉進度條
|
||||||
|
await this.rename(); // 改檔名
|
||||||
|
await uploadSuccess();
|
||||||
|
this.$router.push({name: 'Files'});
|
||||||
|
} catch(error) {
|
||||||
|
console.log('error:', error);
|
||||||
|
if(error.response.status === 422) {
|
||||||
|
uploadFailed(error.response.data.detail[0].type, error.response.data.detail[0].msg);
|
||||||
|
} else {
|
||||||
|
Swal.close(); // 關閉進度條
|
||||||
|
apiError(error, 'Failed to upload the log files.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Rename a Log
|
||||||
|
*/
|
||||||
|
async rename() {
|
||||||
|
const id = this.uploadLogId;
|
||||||
|
const api = `/api/logs/${id}/name`;
|
||||||
|
const data = {"name": this.uploadFileName};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.put(api, data);
|
||||||
|
console.log('response:', response);
|
||||||
|
this.uploadFileName = null;
|
||||||
|
} catch(error) {
|
||||||
|
console.log('error:', error);
|
||||||
|
apiError(error, 'Failed to rename.');
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -176,6 +176,8 @@
|
|||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
this.store.fetchAllFiles();
|
this.store.fetchAllFiles();
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,86 +1,255 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="h-screen-main w-full px-4 flex flex-col justify-between items-start">
|
<section class="h-screen-main w-full px-4 flex flex-col justify-between items-start">
|
||||||
<!-- Upload Content -->
|
<!-- Upload Content -->
|
||||||
<div class="w-full">
|
<div class="w-full h-[calc(100%_-_64px)]">
|
||||||
<!-- File name -->
|
<!-- File name -->
|
||||||
<p class="font-bold text-base leading-[48px] border-b border-neutral-300">File Name(點擊可以改名字)</p>
|
<p id="uploadFileName" class="font-bold text-base leading-[48px] border-b border-neutral-300 cursor-pointer focus-visible:outline-primary/50 focus-visible:outline-0 " contentEditable="true">{{ uploadFileName }}<span class="material-symbols-outlined align-text-bottom text-lg" contentEditable="false">stylus</span></p>
|
||||||
<!-- Upload notification -->
|
<!-- Upload notification -->
|
||||||
<div class="flex justify-start items-center space-x-2 ml-2 py-2">
|
<div class="flex justify-start items-center space-x-2 ml-2 py-2">
|
||||||
<span class="material-symbols-outlined text-neutral-700" v-tooltip.right="tooltipUpload">info</span>
|
<span class="material-symbols-outlined text-neutral-700 cursor-pointer" v-tooltip.right="tooltipUpload">info</span>
|
||||||
<span class="w-px h-7 bg-neutral-300"></span>
|
<span class="w-px h-7 bg-neutral-300"></span>
|
||||||
<div class="flex items-center">
|
|
||||||
<span class="material-symbols-outlined text-primary">notifications</span>
|
|
||||||
<span class="material-symbols-outlined material-fill text-danger">warning</span>
|
|
||||||
</div>
|
|
||||||
<!-- Upload text -->
|
<!-- Upload text -->
|
||||||
<div class="flex justify-start items-center">
|
<div>
|
||||||
<p class="text-primary text-sm">
|
<div v-if="isInform" class="flex justify-start items-center space-x-2 duration-700">
|
||||||
Please verify the label for each column before uploading.
|
<span class="material-symbols-outlined text-primary">notifications</span>
|
||||||
</p>
|
<p class="text-primary text-sm">
|
||||||
<p class="text-danger text-sm">
|
Please verify the label for each column before uploading.
|
||||||
Need to select [A], [B], [C].
|
|
||||||
</p>
|
|
||||||
<div class=" space-y-1">
|
|
||||||
<p class="text-danger text-sm">
|
|
||||||
[Case ID] has been assigned.
|
|
||||||
</p>
|
|
||||||
<p class="text-danger text-sm">
|
|
||||||
[Case ID], [Activity] have been assigned.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else class="flex justify-start items-center space-x-2 duration-700">
|
||||||
|
<span class="material-symbols-outlined material-fill text-danger">warning</span>
|
||||||
|
<p class="text-danger text-sm">
|
||||||
|
Need to select
|
||||||
|
<span v-for="(item, index) in informData" :key="index">[ {{ item }} ]<span v-if="index !== informData.length - 1">, </span></span>.
|
||||||
|
</p>
|
||||||
|
<div v-if="repeatedData.length !== 0" class="duration-700">
|
||||||
|
<p v-if="repeatedData.length === 1" class="text-danger text-sm">
|
||||||
|
[ {{repeatedData[0]}} ] has been assigned.
|
||||||
|
</p>
|
||||||
|
<p v-else class="text-danger text-sm">
|
||||||
|
<span v-for="(item, index) in repeatedData" :key="index">[ {{ item }} ]<span v-if="index !== repeatedData.length - 1">, </span></span>
|
||||||
|
have been assigned.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<!-- Upload table -->
|
<!-- Upload table -->
|
||||||
|
<div class="overflow-y-auto overflow-x-auto scrollbar max-h-[calc(100%_-_97px)]">
|
||||||
|
<table class="text-sm w-full table-fixed border-separate border-spacing-0">
|
||||||
|
<thead class="sticky top-0 bg-neutral-10">
|
||||||
|
<tr>
|
||||||
|
<td v-for="(item, index) in uploadDetail?.columns" :key="index" class="border border-neutral-500 p-2">{{ item }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td v-for="(item, index) in uploadDetail?.columns" :key="index" class="px-2 py-1 bg-neutral-300 border border-neutral-500">
|
||||||
|
<Dropdown
|
||||||
|
v-model="selectedColumns[index]"
|
||||||
|
:options="columnType"
|
||||||
|
optionLabel="name"
|
||||||
|
placeholder="Not Assigned"
|
||||||
|
class="w-full !border-neutral-500"
|
||||||
|
:data-type="item"
|
||||||
|
:inputId="index.toString()"
|
||||||
|
:inputClass="selectedColumns[index]?.color"
|
||||||
|
inputClass="!text-sm">
|
||||||
|
<template #option="slotProps">
|
||||||
|
<div :class="slotProps.option.color" class="text-sm">
|
||||||
|
<span>{{ slotProps.option.name }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Dropdown>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(item, index) in uploadDetail?.data" :key="index">
|
||||||
|
<td v-for="(itemDetail, key) in item" :key="key" class="border border-neutral-500 p-2 truncate break-keep">{{ itemDetail }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Upload button -->
|
<!-- Upload button -->
|
||||||
<div class="w-full text-right space-x-4 px-8 py-4">
|
<div class="w-full text-right space-x-4 px-8 py-4">
|
||||||
<button type="button" class="btn btn-sm btn-neutral" @click="reset">Cancel</button>
|
<button type="button" class="btn btn-sm btn-neutral" @click="cancel">Cancel</button>
|
||||||
<button type="button" class="btn btn-sm" @click="submit" :disabled="isDisabled" :class="isDisabled ? 'btn-disable' : 'btn-neutral'">Upload</button>
|
<button type="button" class="btn btn-sm btn-neutral" @click="reset">Reset</button>
|
||||||
|
<button type="button" class="btn btn-sm" @click="submit" :disabled="isDisabled" :class="isDisabled ? 'btn-disable' : 'btn-neutral'">Upload</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import LoadingStore from '@/stores/loading.js';
|
import LoadingStore from '@/stores/loading.js';
|
||||||
|
import FilesStore from '@/stores/files.js';
|
||||||
|
import { uploadFailed, uploadSuccess, uploadConfirm } from '@/module/alertModal.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
setup() {
|
setup() {
|
||||||
const loadingStore = LoadingStore();
|
const loadingStore = LoadingStore();
|
||||||
|
const filesStore = FilesStore();
|
||||||
const { isLoading } = storeToRefs(loadingStore);
|
const { isLoading } = storeToRefs(loadingStore);
|
||||||
|
const { uploadDetail, uploadId, uploadFileName } = storeToRefs(filesStore);
|
||||||
|
|
||||||
return { isLoading }
|
return { isLoading, filesStore, uploadDetail, uploadId, uploadFileName }
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isDisabled: true,
|
|
||||||
tooltipUpload: {
|
tooltipUpload: {
|
||||||
value: `1. Case ID: A unique identifier for each case.
|
value: `1. Case ID: A unique identifier for each case.
|
||||||
2. Activity: A process step executed by either a system (automated) or humans (manual).
|
2. Activity: A process step executed by either a system (automated) or humans (manual).
|
||||||
3. Activity Instance ID: A unique identifier for a single occurrence of an activity.
|
3. Activity Instance ID: A unique identifier for a single occurrence of an activity.
|
||||||
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.`,
|
||||||
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]',
|
class: '!max-w-[400px] !text-[10px]',
|
||||||
autoHide: false,
|
autoHide: false,
|
||||||
}
|
},
|
||||||
|
columnType: [
|
||||||
|
{ name: 'Case ID*', code: 'case_id', color: '!text-secondary', value: '' },
|
||||||
|
{ name: 'Timestamp*', code: 'timestamp', color: '!text-secondary', value: '' },
|
||||||
|
{ name: 'Status*', code: 'status', color: '!text-secondary', value: '' },
|
||||||
|
{ name: 'Activity*', code: 'name', color: '!text-secondary', value: '' },
|
||||||
|
{ name: 'Activity Instance ID*', code: 'instance', color: '!text-secondary', value: '' },
|
||||||
|
{ name: 'Case Attribute', code: 'case_attributes', color: '!text-primary', value: '' },
|
||||||
|
// { name: 'Resource', code: '', color: '', value: '' }, // 現階段沒有,未來可能有
|
||||||
|
{ name: 'Not Assigned', code: '', color: '!text-neutral-700', value: '' },
|
||||||
|
],
|
||||||
|
selectedColumns: [],
|
||||||
|
isInform: true, // true: 藍字提示; false: 紅字提示
|
||||||
|
informData: [], // 紅字提示,尚未選擇的 type
|
||||||
|
repeatedData: [], // 紅字提示,重複選擇的 type
|
||||||
|
baseTypeData: ['Case ID*', 'Timestamp*', 'Status*', 'Activity*', 'Activity Instance ID*'],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isDisabled: function() {
|
||||||
|
// 1. 長度一樣,強制每一個都要選
|
||||||
|
// 2. 不為 null undefind
|
||||||
|
let hasValue = !this.selectedColumns.includes(undefined);
|
||||||
|
let result = !(this.selectedColumns.length === this.uploadDetail?.columns.length && this.informData.length === 0 && this.repeatedData.length === 0 && hasValue) ? true : false;
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
selectedColumns: {
|
||||||
|
deep: true, // 監聽陣列內部的變化
|
||||||
|
handler(newVal, oldVal) {
|
||||||
|
this.updateValidationData(newVal);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
uploadFailed,
|
||||||
|
uploadSuccess,
|
||||||
|
uploadConfirm,
|
||||||
|
/**
|
||||||
|
* 驗證,根據新的 selectedColumns 更新 isInform、informData 和 repeatedData
|
||||||
|
* @param {Array} data
|
||||||
|
*/
|
||||||
|
updateValidationData(data) {
|
||||||
|
const nameOccurrences = {};
|
||||||
|
let selectedData = [] // 已經選擇的 data
|
||||||
|
this.informData = []; // 尚未選擇的 data
|
||||||
|
this.repeatedData = []; // 重複選擇的 data
|
||||||
|
|
||||||
|
data.forEach(item => {
|
||||||
|
const { name, code } = item;
|
||||||
|
|
||||||
|
if(nameOccurrences[name]) {
|
||||||
|
// 'Not Assigned'、'Case Attribute' 不列入驗證
|
||||||
|
if(!code || code === 'case_attributes') return;
|
||||||
|
nameOccurrences[name]++;
|
||||||
|
this.repeatedData.push(name);
|
||||||
|
}else {
|
||||||
|
nameOccurrences[name] = 1;
|
||||||
|
selectedData.push(name);
|
||||||
|
this.informData = this.baseTypeData.filter(item => !selectedData.includes(item));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.isInform = (this.informData.length === 0 && this.repeatedData.length === 0) ? true : false;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Reset Button
|
||||||
|
*/
|
||||||
|
reset() {
|
||||||
|
// 路徑不列入歷史紀錄
|
||||||
|
this.selectedColumns = [];
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* Cancel Button
|
* Cancel Button
|
||||||
*/
|
*/
|
||||||
reset() {},
|
cancel() {
|
||||||
|
// 路徑不列入歷史紀錄
|
||||||
|
this.$router.push({name: 'Files', replace: true});
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* Upload Button
|
* Upload Button
|
||||||
*/
|
*/
|
||||||
submit() {},
|
async submit() {
|
||||||
|
// Post API Data
|
||||||
|
let fetchData = {
|
||||||
|
timestamp: '',
|
||||||
|
case_id: '',
|
||||||
|
name: '',
|
||||||
|
instance: '',
|
||||||
|
status: '',
|
||||||
|
case_attributes: []
|
||||||
|
};
|
||||||
|
// 給值
|
||||||
|
let haveValueData = this.selectedColumns.map((column, i) => {
|
||||||
|
if (column && this.uploadDetail.columns[i]) {
|
||||||
|
return {
|
||||||
|
name: column.name,
|
||||||
|
code: column.code,
|
||||||
|
color: column.color,
|
||||||
|
value: this.uploadDetail.columns[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 取得欲更改的檔名
|
||||||
|
this.uploadFileName = document.querySelector('#uploadFileName').firstChild.textContent;
|
||||||
|
// 設定第二階段上傳的 data
|
||||||
|
haveValueData.forEach(column => {
|
||||||
|
switch (column.code) {
|
||||||
|
case 'timestamp':
|
||||||
|
fetchData.timestamp = column.value;
|
||||||
|
break;
|
||||||
|
case 'case_id':
|
||||||
|
fetchData.case_id = column.value;
|
||||||
|
break;
|
||||||
|
case 'name':
|
||||||
|
fetchData.name = column.value;
|
||||||
|
break;
|
||||||
|
case 'instance':
|
||||||
|
fetchData.instance = column.value;
|
||||||
|
break;
|
||||||
|
case 'status':
|
||||||
|
fetchData.status = column.value;
|
||||||
|
break;
|
||||||
|
case 'case_attributes':
|
||||||
|
fetchData.case_attributes.push(column.value);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.uploadConfirm(fetchData);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
// 要有 uploadID 才能進來
|
||||||
|
this.isInform = true; // 初始為藍字提示
|
||||||
|
this.filesStore.getUploadDetail();
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
},
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
// 離開頁面要刪 uploadID
|
||||||
|
this.uploadId = null;
|
||||||
|
this.uploadFileName = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// beforeRouteUpdate(){}
|
// beforeRouteUpdate(){}
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ module.exports = {
|
|||||||
transparent: 'transparent',
|
transparent: 'transparent',
|
||||||
current: 'currentColor',
|
current: 'currentColor',
|
||||||
'primary': '#0099FF',
|
'primary': '#0099FF',
|
||||||
// 'secondary': '#FFAA44',
|
'secondary': '#FFAA44',
|
||||||
'cfm-primary': '#0099FF',
|
'cfm-primary': '#0099FF',
|
||||||
'cfm-secondary': '#FFAA44',
|
'cfm-secondary': '#FFAA44',
|
||||||
'neutral': {
|
'neutral': {
|
||||||
|
|||||||
Reference in New Issue
Block a user