feat: 增加Load From File调用File System Access功能

This commit is contained in:
Xu Zhimeng 2023-10-07 19:12:42 +08:00
parent 8c974aaf35
commit 85f4ccb995
4 changed files with 408 additions and 15 deletions

56
src/utils/FilePicker.js Normal file
View File

@ -0,0 +1,56 @@
/**
* 本地文件选择
*/
export class FilePicker {
/**
* 接口是否可用
* @returns { Boolean }
*/
static canUse() {
return !!(window.showDirectoryPicker && window.showOpenFilePicker)
}
/**
* 选择一个目录
* @returns { Promise<FileSystemDirectoryHandle> }
*/
static chooseDirectory() {
if (!this.canUse()) {
throw new Error('Not Support showDirectoryPicker')
}
return window.showDirectoryPicker()
}
/**
* 选择一个文件
* @param { Boolean } multiple
* @param { { description?: string; accept?: { [key: string]: string[]; } } } types
* @returns { Promise<FileSystemFileHandle[]> }
*/
static chooseFile(multiple, types) {
if (!this.canUse()) {
throw new Error('Not Support showOpenFilePicker')
}
const pickerOpts = {
multiple,
types,
excludeAcceptAllOption: true
}
return window.showOpenFilePicker(pickerOpts)
}
/**
* 判断一个文件是否在某个目录下
* @param {FileSystemDirectoryHandle} directoryHandle
* @param {FileSystemFileHandle} fileHandle
*/
static async isFileInDirectory(directoryHandle, fileHandle) {
const relativePaths = await directoryHandle.resolve(fileHandle)
if (relativePaths === null) {
return false
} else {
return true
}
}
}

View File

@ -31,3 +31,27 @@ export const showSaveFileModal = (data, ext) => {
} }
}) })
} }
/**
* 读文件
* @param {File} file
* @param { 'arrayBuffer' | 'text' | 'dataURL' | 'binaryString'} fileType
*/
export const readFile = (file, fileType = 'text') => {
return new Promise((resolve, reject) => {
const fileReader = new FileReader()
const category = {
arrayBuffer: 'readAsArrayBuffer',
text: 'readAsText',
dataURL: 'readAsDataURL',
binaryString: 'readAsBinaryString'
}
fileReader[category[fileType]](file)
fileReader.onload = event => {
resolve(event.target.result)
}
fileReader.onerror = error => {
reject(error)
}
})
}

117
src/utils/phdHelper.js Normal file
View File

@ -0,0 +1,117 @@
export class PHDParser {
/**
* 数据类型
*/
dataType = ''
/**
* 类型 B G
*/
fileType = ''
/**
* 质量 FULL
*/
qualify = ''
/**
* 生存时间
*/
liveTime = ''
/**
* sample 谱的文件名
*/
sampleFileName = ''
/**
* 其他文件名
*/
otherFileNames = []
/**
* 构造函数
* @param {string} text
*/
constructor(text) {
const getLine = this.readTextLine(text)
const dataTypeLine = getLine(4)
const fileTypeLine = getLine(6)
const fileNameLine = getLine(8)
const liveTimeLine = getLine(18)
this.dataType = this.getOnymousData(dataTypeLine)[0]
const fileType = this.splitLineText(fileTypeLine)
this.fileType = fileType[2]
this.qualify = fileType[4]
const liveTime = parseFloat(this.splitLineText(liveTimeLine)[3]).toFixed(1)
this.liveTime = liveTime.indexOf('.0') == -1 ? liveTime : liveTime.slice(0, -2)
// 如果是 Beta 谱
if (this.fileType == 'B') {
// 如果解析的是sample 文件,则获取其他三个文件
if (this.dataType == PHD_DATA_TYPE.SAMPLEPHD) {
const fileNames = this.getFileNames(fileNameLine)
this.sampleFileName = fileNames.splice(0, 1)[0]
this.otherFileNames = fileNames
}
}
}
/**
* 将文本按行分割
* @param {string} text
* @returns { (line: number) => string }
*/
readTextLine(text) {
const splited = text.split('\n')
return line => {
return splited[line - 1]
}
}
/**
* 获取 具名 行的数据
* @param {string} text
* @example DATA_TYPE SAMPLEPHD 返回['SAMPLEPHD']
* @returns {string[]}
*/
getOnymousData(text) {
return text.split(' ').slice(1)
}
/**
* 将一行中的文本按空格切割
* @param {string} text
*/
splitLineText(text) {
return text.replace(/\s+/g, ',').split(',')
}
/**
* 获取全部文件名
* @param {string} text
*/
getFileNames(text) {
const unHandledfileNames = this.splitLineText(text)
const fileTypes = ['S', 'D', 'G']
const fileNames = unHandledfileNames
.filter(fileName => fileName)
.map((fileName, index) => {
fileName = fileName.replace(/(\d{4})\/(\d{2})\/(\d{2})-(\d{2}):(\d{2})/, '$1$2$3_$4$5')
return `${fileName}_${fileTypes[index]}_${this.qualify}`
})
return fileNames
}
}
/**
* PHD 类型
*/
export const PHD_DATA_TYPE = {
QCPHD: 'QCPHD',
DETBKPHD: 'DETBKPHD',
SAMPLEPHD: 'SAMPLEPHD',
GASBKPHD: 'GASBKPHD'
}

View File

@ -1,7 +1,67 @@
<template> <template>
<div> <div>
<custom-modal v-model="visible" :width="1200" title="Load Data From File"> <custom-modal v-model="visible" :width="1200" title="Load Data From File">
<!-- 支持 File System Access 的情况 -->
<a-table <a-table
v-if="canUseFilePicker"
:dataSource="list"
:columns="columns"
:pagination="false"
bordered
:scroll="{ y: 450 }"
>
<template slot="sampleData" slot-scope="text, record">
<div
class="file-name file-ellipsis"
:title="text && text.fileName"
@dblclick="useFilePicker('sampleFileName', record)"
>
{{ text && text.fileName }}
</div>
</template>
<template slot="gasBkData" slot-scope="text, record">
<div
:class="['file-ellipsis', !record.gasFileName ? 'file-name-color' : '']"
:title="text && text.fileName"
@dblclick="useFilePicker('gasFileName', record)"
>
{{ text && text.fileName }}
</div>
</template>
<template slot="detBkData" slot-scope="text, record">
<div
:class="['file-ellipsis', !record.detFileName ? 'file-name-color' : '']"
:title="text && text.fileName"
@dblclick="useFilePicker('detFileName', record)"
>
{{ text && text.fileName }}
</div>
</template>
<template slot="qcData" slot-scope="text, record">
<div
:class="['file-ellipsis', !record.qcFileName ? 'file-name-color' : '']"
:title="text && text.fileName"
@dblclick="useFilePicker('qcFileName', record)"
>
{{ text && text.fileName }}
</div>
</template>
<template slot="status" slot-scope="text, record">
<span
:class="[
record.sampleFileName
? record.fileType == 'B' && !(record.gasFileName && record.detFileName)
? 'status_false'
: 'status_true'
: '',
'status',
]"
></span>
</template>
</a-table>
<!-- 不支持 File System Access 的情况 -->
<a-table
v-else
:data-source="list" :data-source="list"
:columns="columns" :columns="columns"
:loading="loading_list" :loading="loading_list"
@ -13,7 +73,7 @@
<div class="file-name file-ellipsis" :title="text" @dblclick="handleFileSelect('_S_', record)"> <div class="file-name file-ellipsis" :title="text" @dblclick="handleFileSelect('_S_', record)">
{{ text }} {{ text }}
</div> </div>
<!-- <phd-select type="file" @change="handleFileChange(record, 'sampleData', $event)" @select="handleFileSelect" :title="text && text.name"> <!-- <phd-select type="file" @change="handleFileChange(record, 'sampleData', $event)" @select="handleFileSelect" :title="text && text.fileName">
{{ text }} {{ text }}
</phd-select> --> </phd-select> -->
</template> </template>
@ -141,9 +201,12 @@
<script> <script>
import JSZip from 'jszip' import JSZip from 'jszip'
import FileSaver from 'file-saver'
import PhdSelect from '../PHDSelect.vue'
import { getAction, postAction } from '../../../../api/manage' import { getAction, postAction } from '../../../../api/manage'
import { FilePicker } from '@/utils/FilePicker'
import { readFile } from '@/utils/file'
import { PHDParser, PHD_DATA_TYPE } from '@/utils/phdHelper'
import ModalMixin from '@/mixins/ModalMixin'
const columns = [ const columns = [
{ {
title: 'SampleData', title: 'SampleData',
@ -208,8 +271,11 @@ const columns_file = [
align: 'left', align: 'left',
}, },
] ]
const canUseFilePicker = FilePicker.canUse()
export default { export default {
components: { PhdSelect }, mixins: [ModalMixin],
props: { props: {
value: { value: {
type: Boolean, type: Boolean,
@ -223,7 +289,7 @@ export default {
columns_file, columns_file,
loading_file: false, loading_file: false,
loading_list: false, loading_list: false,
list: this.getInitialList(), list: [],
ipagination: { ipagination: {
current: 1, current: 1,
pageSize: 10, pageSize: 10,
@ -247,6 +313,8 @@ export default {
selectionRows_edit: [], selectionRows_edit: [],
tableType: 'multiple', tableType: 'multiple',
searchName: '', searchName: '',
canUseFilePicker,
} }
}, },
methods: { methods: {
@ -290,6 +358,137 @@ export default {
this.tableType = str === '_S_' ? 'multiple' : 'single' this.tableType = str === '_S_' ? 'multiple' : 'single'
this.getSpectrumFile({ pageNo: 1, pageSize: 10 }) this.getSpectrumFile({ pageNo: 1, pageSize: 10 })
}, },
//
async useFilePicker(column, record) {
// sampleFile
if (column !== 'sampleFileName' && (!record.sampleFileName || record.fileType !== 'B')) {
return
}
if (this.directoryHanlder) {
this.chooseFile(column, record)
} else {
try {
this.directoryHanlder = await FilePicker.chooseDirectory()
this.chooseFile(column, record)
} catch {}
}
},
async chooseFile(column, record) {
try {
const [fileHandle] = await FilePicker.chooseFile(false, [{ accept: { 'text/phd': ['.phd'] } }])
try {
const isFileInDirectory = await FilePicker.isFileInDirectory(this.directoryHanlder, fileHandle)
if (!isFileInDirectory) {
this.$message.warn('File and Directory Not in Same')
return
}
const file = await fileHandle.getFile()
const text = await readFile(file)
const phdParser = new PHDParser(text)
console.log('%c [ phdParser ]-313', 'font-size:13px; background:pink; color:#bf2c9f;', phdParser)
const match = this.fileNameAndColumnMatch(column, phdParser.dataType)
if (!match) {
this.$message.warn('File Type Error')
return
}
record.fileType = phdParser.fileType
// sample
if (column == 'sampleFileName') {
const { sampleFileName, otherFileNames, liveTime } = phdParser
record.sampleFileName = {
file,
fileName: `${sampleFileName}_${liveTime}.PHD`,
}
const iter = await this.directoryHanlder.values()
const fileList = []
let result = await iter.next()
while (!result.done) {
const fileHandle = result.value
const file = await fileHandle.getFile()
fileList.push(file)
result = await iter.next()
}
const nameKeys = ['detFileName', 'gasFileName']
otherFileNames.forEach((otherFileName, index) => {
// sample
const findFile = fileList.find((file) => {
return file.name.includes(otherFileName)
})
if (findFile) {
record[nameKeys[index]] = {
file: findFile,
fileName: findFile.name,
}
} else {
record[columns[index]] = undefined
}
})
// QC
let qcFileInfo = undefined
const qcFileList = fileList.filter((file) => file.name.includes('_Q_'))
const compareFileName = sampleFileName.slice(0, 23)
for (const qcFile of qcFileList) {
if (qcFile.name.slice(0, 23) <= compareFileName) {
qcFileInfo = {
file: qcFile,
fileName: qcFile.name,
}
} else {
break
}
}
record.qcFileName = qcFileInfo
}
// sample
else {
record[column] = {
file,
fileName: file.name,
}
}
} catch (error) {
console.log(error)
}
} catch {}
},
/**
* 判断文件和列是否匹配
* @param {*} fileType
* @param {*} dataType
*/
fileNameAndColumnMatch(column, dataType) {
let currDataType = ''
switch (column) {
case 'sampleFileName':
currDataType = PHD_DATA_TYPE.SAMPLEPHD
break
case 'gasFileName':
currDataType = PHD_DATA_TYPE.GASBKPHD
break
case 'detFileName':
currDataType = PHD_DATA_TYPE.DETBKPHD
break
case 'qcFileName':
currDataType = PHD_DATA_TYPE.QCPHD
break
}
return currDataType == dataType
},
onSearchFileName() { onSearchFileName() {
this.getSpectrumFile({ pageNo: 1, pageSize: 10 }) this.getSpectrumFile({ pageNo: 1, pageSize: 10 })
}, },
@ -429,15 +628,9 @@ export default {
handleCancel() { handleCancel() {
this.visible = false this.visible = false
}, },
},
computed: { beforeModalOpen() {
visible: { this.handleReset()
get() {
return this.value
},
set(val) {
this.$emit('input', val)
},
}, },
}, },
} }
@ -451,7 +644,7 @@ export default {
border-radius: 50%; border-radius: 50%;
margin-top: 8px; margin-top: 8px;
background-color: #00e170; background-color: transparent;
} }
.status_true { .status_true {
background-color: #00e170; background-color: #00e170;
@ -525,6 +718,9 @@ export default {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
user-select: none;
height: 40px;
line-height: 40px;
} }
/deep/.ant-table-tbody .ant-table-row td { /deep/.ant-table-tbody .ant-table-row td {
cursor: pointer; cursor: pointer;