feat: ✨ 上传文件时校验是否模板文件,否则提示 #96
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -81,7 +81,7 @@ web_modules/
|
|||||||
.env.production.local
|
.env.production.local
|
||||||
.env.local
|
.env.local
|
||||||
|
|
||||||
components.d.ts
|
**/components.d.ts
|
||||||
|
|
||||||
# parcel-bundler cache (https://parceljs.org/)
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
.cache
|
.cache
|
||||||
|
|||||||
@@ -44,6 +44,7 @@
|
|||||||
"vue-draggable-plus": "^0.6.0",
|
"vue-draggable-plus": "^0.6.0",
|
||||||
"vue-i18n": "^11.2.2",
|
"vue-i18n": "^11.2.2",
|
||||||
"vue-router": "^4.5.0",
|
"vue-router": "^4.5.0",
|
||||||
|
"vue-sonner": "^2.0.9",
|
||||||
"vue-toast-notification": "^3",
|
"vue-toast-notification": "^3",
|
||||||
"vue3-colorpicker": "^2.3.0",
|
"vue3-colorpicker": "^2.3.0",
|
||||||
"xlsx": "^0.18.5",
|
"xlsx": "^0.18.5",
|
||||||
|
|||||||
19
pnpm-lock.yaml
generated
19
pnpm-lock.yaml
generated
@@ -86,6 +86,9 @@ importers:
|
|||||||
vue-router:
|
vue-router:
|
||||||
specifier: ^4.5.0
|
specifier: ^4.5.0
|
||||||
version: 4.5.0(vue@3.5.13(typescript@5.9.3))
|
version: 4.5.0(vue@3.5.13(typescript@5.9.3))
|
||||||
|
vue-sonner:
|
||||||
|
specifier: ^2.0.9
|
||||||
|
version: 2.0.9
|
||||||
vue-toast-notification:
|
vue-toast-notification:
|
||||||
specifier: ^3
|
specifier: ^3
|
||||||
version: 3.1.2(vue@3.5.13(typescript@5.9.3))
|
version: 3.1.2(vue@3.5.13(typescript@5.9.3))
|
||||||
@@ -5552,6 +5555,20 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vue: ^3.2.0
|
vue: ^3.2.0
|
||||||
|
|
||||||
|
vue-sonner@2.0.9:
|
||||||
|
resolution: {integrity: sha512-i6BokNlNDL93fpzNxN/LZSn6D6MzlO+i3qXt6iVZne3x1k7R46d5HlFB4P8tYydhgqOrRbIZEsnRd3kG7qGXyw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@nuxt/kit': ^4.0.3
|
||||||
|
'@nuxt/schema': ^4.0.3
|
||||||
|
nuxt: ^4.0.3
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@nuxt/kit':
|
||||||
|
optional: true
|
||||||
|
'@nuxt/schema':
|
||||||
|
optional: true
|
||||||
|
nuxt:
|
||||||
|
optional: true
|
||||||
|
|
||||||
vue-template-compiler@2.7.15:
|
vue-template-compiler@2.7.15:
|
||||||
resolution: {integrity: sha512-yQxjxMptBL7UAog00O8sANud99C6wJF+7kgbcwqkvA38vCGF7HWE66w0ZFnS/kX5gSoJr/PQ4/oS3Ne2pW37Og==}
|
resolution: {integrity: sha512-yQxjxMptBL7UAog00O8sANud99C6wJF+7kgbcwqkvA38vCGF7HWE66w0ZFnS/kX5gSoJr/PQ4/oS3Ne2pW37Og==}
|
||||||
|
|
||||||
@@ -11506,6 +11523,8 @@ snapshots:
|
|||||||
'@vue/devtools-api': 6.6.4
|
'@vue/devtools-api': 6.6.4
|
||||||
vue: 3.5.13(typescript@5.9.3)
|
vue: 3.5.13(typescript@5.9.3)
|
||||||
|
|
||||||
|
vue-sonner@2.0.9: {}
|
||||||
|
|
||||||
vue-template-compiler@2.7.15:
|
vue-template-compiler@2.7.15:
|
||||||
dependencies:
|
dependencies:
|
||||||
de-indent: 1.0.2
|
de-indent: 1.0.2
|
||||||
|
|||||||
1
src/components.d.ts
vendored
1
src/components.d.ts
vendored
@@ -58,6 +58,7 @@ declare module 'vue' {
|
|||||||
PopoverTrigger: typeof import('./components/ui/popover/PopoverTrigger.vue')['default']
|
PopoverTrigger: typeof import('./components/ui/popover/PopoverTrigger.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
Sonner: typeof import('./components/ui/sonner/Sonner.vue')['default']
|
||||||
SvgIcon: typeof import('./components/SvgIcon/index.vue')['default']
|
SvgIcon: typeof import('./components/SvgIcon/index.vue')['default']
|
||||||
ToTop: typeof import('./components/ToTop/index.vue')['default']
|
ToTop: typeof import('./components/ToTop/index.vue')['default']
|
||||||
}
|
}
|
||||||
|
|||||||
42
src/components/ui/sonner/Sonner.vue
Normal file
42
src/components/ui/sonner/Sonner.vue
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { ToasterProps } from "vue-sonner"
|
||||||
|
import { CircleCheckIcon, InfoIcon, Loader2Icon, OctagonXIcon, TriangleAlertIcon, XIcon } from "lucide-vue-next"
|
||||||
|
import { Toaster as Sonner } from "vue-sonner"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<ToasterProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Sonner
|
||||||
|
:class="cn('toaster group', props.class)"
|
||||||
|
:style="{
|
||||||
|
'--normal-bg': 'var(--popover)',
|
||||||
|
'--normal-text': 'var(--popover-foreground)',
|
||||||
|
'--normal-border': 'var(--border)',
|
||||||
|
'--border-radius': 'var(--radius)',
|
||||||
|
}"
|
||||||
|
v-bind="props"
|
||||||
|
>
|
||||||
|
<template #success-icon>
|
||||||
|
<CircleCheckIcon class="size-4" />
|
||||||
|
</template>
|
||||||
|
<template #info-icon>
|
||||||
|
<InfoIcon class="size-4" />
|
||||||
|
</template>
|
||||||
|
<template #warning-icon>
|
||||||
|
<TriangleAlertIcon class="size-4" />
|
||||||
|
</template>
|
||||||
|
<template #error-icon>
|
||||||
|
<OctagonXIcon class="size-4" />
|
||||||
|
</template>
|
||||||
|
<template #loading-icon>
|
||||||
|
<div>
|
||||||
|
<Loader2Icon class="size-4 animate-spin" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #close-icon>
|
||||||
|
<XIcon class="size-4" />
|
||||||
|
</template>
|
||||||
|
</Sonner>
|
||||||
|
</template>
|
||||||
1
src/components/ui/sonner/index.ts
Normal file
1
src/components/ui/sonner/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default as Toaster } from "./Sonner.vue"
|
||||||
@@ -5,8 +5,10 @@ import { useI18n } from 'vue-i18n'
|
|||||||
import CustomModal from '@/components/Dialog/index.vue'
|
import CustomModal from '@/components/Dialog/index.vue'
|
||||||
import { Loading } from '@/components/Loading'
|
import { Loading } from '@/components/Loading'
|
||||||
import ToTop from '@/components/ToTop/index.vue'
|
import ToTop from '@/components/ToTop/index.vue'
|
||||||
|
import { Toaster } from '@/components/ui/sonner'
|
||||||
import RightButton from './RightButton/index.vue'
|
import RightButton from './RightButton/index.vue'
|
||||||
import { useMounted } from './useMounted'
|
import { useMounted } from './useMounted'
|
||||||
|
import 'vue-sonner/style.css'
|
||||||
|
|
||||||
const tipDialog = ref()
|
const tipDialog = ref()
|
||||||
const { tipDesc } = useMounted(tipDialog)
|
const { tipDesc } = useMounted(tipDialog)
|
||||||
@@ -29,6 +31,7 @@ function scrollToTop() {
|
|||||||
<main ref="mainContainer" class="box-content w-screen h-screen overflow-x-hidden overflow-y-auto main-container">
|
<main ref="mainContainer" class="box-content w-screen h-screen overflow-x-hidden overflow-y-auto main-container">
|
||||||
<router-view class="h-full main-container-content" />
|
<router-view class="h-full main-container-content" />
|
||||||
</main>
|
</main>
|
||||||
|
<Toaster />
|
||||||
<RightButton class="absolute right-0 bottom-1/2" />
|
<RightButton class="absolute right-0 bottom-1/2" />
|
||||||
<CustomModal ref="tipDialog" :title="t('dialog.titleTip')" :desc="tipDesc" />
|
<CustomModal ref="tipDialog" :title="t('dialog.titleTip')" :desc="tipDesc" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export function readFileBinary(file: any): Promise<any> {
|
export function readFileBinary(file: File | Blob): Promise<string> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
reader.readAsBinaryString(file)
|
reader.readAsBinaryString(file)
|
||||||
@@ -17,3 +17,9 @@ export function readFileData(file: any): Promise<{ dataUrl: string, fileName: st
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function readLocalFileAsArraybuffer(path: string): Promise<ArrayBuffer> {
|
||||||
|
const response = await fetch(path)
|
||||||
|
const arrayBuffer = await response.arrayBuffer()
|
||||||
|
return arrayBuffer
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,239 +0,0 @@
|
|||||||
<!-- eslint-disable vue/no-parsing-error -->
|
|
||||||
<script setup lang='ts'>
|
|
||||||
import type { IPersonConfig } from '@/types/storeType'
|
|
||||||
import { storeToRefs } from 'pinia'
|
|
||||||
import { onMounted, ref } from 'vue'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import * as XLSX from 'xlsx'
|
|
||||||
import DaiysuiTable from '@/components/DaiysuiTable/index.vue'
|
|
||||||
import i18n from '@/locales/i18n'
|
|
||||||
import useStore from '@/store'
|
|
||||||
import { readFileBinary } from '@/utils/file'
|
|
||||||
import ImportExcelWorker from './importExcel.worker?worker'
|
|
||||||
|
|
||||||
const worker: Worker | null = new ImportExcelWorker()
|
|
||||||
const { t } = useI18n()
|
|
||||||
const personConfig = useStore().personConfig
|
|
||||||
const { getAllPersonList: allPersonList, getAlreadyPersonList: alreadyPersonList } = storeToRefs(personConfig)
|
|
||||||
const limitType = '.xlsx,.xls'
|
|
||||||
// const personList = ref<any[]>([])
|
|
||||||
|
|
||||||
const resetDataDialog = ref()
|
|
||||||
const delAllDataDialog = ref()
|
|
||||||
function sendMessage(message: any) {
|
|
||||||
if (worker) {
|
|
||||||
worker.postMessage(message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 方法
|
|
||||||
function startWorker(data: Event) {
|
|
||||||
sendMessage({ type: 'start', data })
|
|
||||||
}
|
|
||||||
async function handleFileChange(e: Event) {
|
|
||||||
// worker = new ImportExcelWorker()
|
|
||||||
if (worker) {
|
|
||||||
worker.onmessage = (e) => {
|
|
||||||
if (e.data.type === 'done') {
|
|
||||||
personConfig.resetPerson()
|
|
||||||
personConfig.addNotPersonList(e.data.data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const dataBinary = await readFileBinary(((e.target as HTMLInputElement).files as FileList)[0]!)
|
|
||||||
startWorker(dataBinary)
|
|
||||||
}
|
|
||||||
function exportData() {
|
|
||||||
let data = JSON.parse(JSON.stringify(allPersonList.value))
|
|
||||||
// 排除一些字段
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
|
||||||
delete data[i].x
|
|
||||||
delete data[i].y
|
|
||||||
delete data[i].id
|
|
||||||
delete data[i].createTime
|
|
||||||
delete data[i].updateTime
|
|
||||||
delete data[i].prizeId
|
|
||||||
// 修改字段名称
|
|
||||||
if (data[i].isWin) {
|
|
||||||
data[i].isWin = i18n.global.t('data.yes')
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
data[i].isWin = i18n.global.t('data.no')
|
|
||||||
}
|
|
||||||
// 格式化数组为
|
|
||||||
data[i].prizeTime = data[i].prizeTime.join(',')
|
|
||||||
data[i].prizeName = data[i].prizeName.join(',')
|
|
||||||
}
|
|
||||||
let dataString = JSON.stringify(data)
|
|
||||||
dataString = dataString
|
|
||||||
.replaceAll(/uid/g, i18n.global.t('data.number'))
|
|
||||||
.replaceAll(/isWin/g, i18n.global.t('data.isWin'))
|
|
||||||
.replaceAll(/department/g, i18n.global.t('data.department'))
|
|
||||||
.replaceAll(/name/g, i18n.global.t('data.name'))
|
|
||||||
.replaceAll(/identity/g, i18n.global.t('data.identity'))
|
|
||||||
.replaceAll(/prizeName/g, i18n.global.t('data.prizeName'))
|
|
||||||
.replaceAll(/prizeTime/g, i18n.global.t('data.prizeTime'))
|
|
||||||
|
|
||||||
data = JSON.parse(dataString)
|
|
||||||
|
|
||||||
if (data.length > 0) {
|
|
||||||
const dataBinary = XLSX.utils.json_to_sheet(data)
|
|
||||||
const dataBinaryBinary = XLSX.utils.book_new()
|
|
||||||
XLSX.utils.book_append_sheet(dataBinaryBinary, dataBinary, 'Sheet1')
|
|
||||||
XLSX.writeFile(dataBinaryBinary, 'data.xlsx')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetData() {
|
|
||||||
personConfig.resetAlreadyPerson()
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteAll() {
|
|
||||||
personConfig.deleteAllPerson()
|
|
||||||
}
|
|
||||||
|
|
||||||
function delPersonItem(row: IPersonConfig) {
|
|
||||||
personConfig.deletePerson(row)
|
|
||||||
}
|
|
||||||
|
|
||||||
const tableColumns = [
|
|
||||||
{
|
|
||||||
label: i18n.global.t('data.number'),
|
|
||||||
props: 'uid',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: i18n.global.t('data.name'),
|
|
||||||
props: 'name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: i18n.global.t('data.department'),
|
|
||||||
props: 'department',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: i18n.global.t('data.avatar'),
|
|
||||||
props: 'avatar',
|
|
||||||
formatValue(row: any) {
|
|
||||||
return row.avatar ? `<img src="${row.avatar}" alt="avatar" style="width: 50px; height: 50px;"/>` : '-'
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: i18n.global.t('data.identity'),
|
|
||||||
props: 'identity',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: i18n.global.t('data.isWin'),
|
|
||||||
props: 'isWin',
|
|
||||||
formatValue(row: IPersonConfig) {
|
|
||||||
return row.isWin ? i18n.global.t('data.yes') : i18n.global.t('data.no')
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: i18n.global.t('data.operation'),
|
|
||||||
actions: [
|
|
||||||
// {
|
|
||||||
// label: '编辑',
|
|
||||||
// type: 'btn-info',
|
|
||||||
// onClick: (row: any) => {
|
|
||||||
// delPersonItem(row)
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
label: i18n.global.t('data.delete'),
|
|
||||||
type: 'btn-error',
|
|
||||||
onClick: (row: IPersonConfig) => {
|
|
||||||
delPersonItem(row)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
onMounted(() => {
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<dialog id="my_modal_1" ref="resetDataDialog" class="border-none modal">
|
|
||||||
<div class="modal-box">
|
|
||||||
<h3 class="text-lg font-bold">
|
|
||||||
{{ t('dialog.titleTip') }}
|
|
||||||
</h3>
|
|
||||||
<p class="py-4">
|
|
||||||
{{ t('dialog.dialogResetWinner') }}
|
|
||||||
</p>
|
|
||||||
<div class="modal-action">
|
|
||||||
<form method="dialog" class="flex gap-3">
|
|
||||||
<!-- if there is a button in form, it will close the modal -->
|
|
||||||
<button class="btn" @click="resetDataDialog.close()">
|
|
||||||
{{ t('button.cancel') }}
|
|
||||||
</button>
|
|
||||||
<button class="btn" @click="resetData">
|
|
||||||
{{ t('button.confirm') }}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</dialog>
|
|
||||||
<dialog id="my_modal_1" ref="delAllDataDialog" class="border-none modal">
|
|
||||||
<div class="modal-box">
|
|
||||||
<h3 class="text-lg font-bold">
|
|
||||||
{{ t('dialog.titleTip') }}
|
|
||||||
</h3>
|
|
||||||
<p class="py-4">
|
|
||||||
{{ t('dialog.dialogDelAllPerson') }}
|
|
||||||
</p>
|
|
||||||
<div class="modal-action">
|
|
||||||
<form method="dialog" class="flex gap-3">
|
|
||||||
<!-- if there is a button in form, it will close the modal -->
|
|
||||||
<button class="btn" @click="delAllDataDialog.close()">
|
|
||||||
{{ t('button.cancel') }}
|
|
||||||
</button>
|
|
||||||
<button class="btn" @click="deleteAll">
|
|
||||||
{{ t('button.confirm') }}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</dialog>
|
|
||||||
<div class="min-w-1000px">
|
|
||||||
<h2>{{ t('viewTitle.personManagement') }}</h2>
|
|
||||||
<div class="flex gap-3">
|
|
||||||
<button class="btn btn-error btn-sm" @click="delAllDataDialog.showModal()">
|
|
||||||
{{ t('button.allDelete') }}
|
|
||||||
</button>
|
|
||||||
<div class="tooltip tooltip-bottom" :data-tip="t('tooltip.downloadTemplateTip')">
|
|
||||||
<a
|
|
||||||
class="no-underline btn btn-secondary btn-sm" :download="t('data.xlsxName')" target="_blank"
|
|
||||||
:href="`/log-lottery/${t('data.xlsxName')}`"
|
|
||||||
>{{ t('button.downloadTemplate') }}</a>
|
|
||||||
</div>
|
|
||||||
<div class="">
|
|
||||||
<label for="explore">
|
|
||||||
|
|
||||||
<div class="tooltip tooltip-bottom" :data-tip="t('tooltip.uploadExcelTip')">
|
|
||||||
<input
|
|
||||||
id="explore" type="file" class="" style="display: none" :accept="limitType"
|
|
||||||
@change="handleFileChange"
|
|
||||||
>
|
|
||||||
|
|
||||||
<span class="btn btn-primary btn-sm">{{ t('button.importData') }}</span>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-error btn-sm" @click="resetDataDialog.showModal()">
|
|
||||||
{{ t('button.resetData') }}
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-accent btn-sm" @click="exportData">
|
|
||||||
{{ t('button.exportResult') }}
|
|
||||||
</button>
|
|
||||||
<div>
|
|
||||||
<span>{{ t('table.luckyPeopleNumber') }}:</span>
|
|
||||||
<span>{{ alreadyPersonList.length }}</span>
|
|
||||||
<span> / </span>
|
|
||||||
<span>{{ allPersonList.length }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DaiysuiTable :table-columns="tableColumns" :data="allPersonList" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang='scss' scoped></style>
|
|
||||||
@@ -4,20 +4,43 @@ import { addOtherInfo } from '@/utils'
|
|||||||
interface WorkerMessage {
|
interface WorkerMessage {
|
||||||
type: 'start' | 'stop' | 'reset'
|
type: 'start' | 'stop' | 'reset'
|
||||||
data: any
|
data: any
|
||||||
|
templateData: any
|
||||||
}
|
}
|
||||||
|
|
||||||
let allData: any[] = []
|
let allData: any[] = []
|
||||||
|
|
||||||
|
function headersEqual(template: string[], actual: string[]): boolean {
|
||||||
|
return template.length === actual.length
|
||||||
|
&& template.every((value, index) => value === actual[index])
|
||||||
|
}
|
||||||
|
|
||||||
// 接收主线程消息
|
// 接收主线程消息
|
||||||
globalThis.onmessage = async (e: MessageEvent<WorkerMessage>) => {
|
globalThis.onmessage = async (e: MessageEvent<WorkerMessage>) => {
|
||||||
switch (e.data.type) {
|
switch (e.data.type) {
|
||||||
case 'start':
|
case 'start':
|
||||||
{
|
{
|
||||||
const fileData = e.data.data
|
const fileData = e.data.data
|
||||||
// const dataBinary = await readFileBinary(((fileEvent.target as HTMLInputElement).files as FileList)[0]!)
|
const templateData = e.data.templateData
|
||||||
|
|
||||||
const workBook = XLSX.read(fileData, { type: 'binary', cellDates: true })
|
const workBook = XLSX.read(fileData, { type: 'binary', cellDates: true })
|
||||||
const workSheet = workBook.Sheets[workBook.SheetNames[0]]
|
const workSheet = workBook.Sheets[workBook.SheetNames[0]]
|
||||||
const excelData = XLSX.utils.sheet_to_json(workSheet)
|
const excelData: object[] = XLSX.utils.sheet_to_json(workSheet)
|
||||||
|
|
||||||
|
const templateWorkBook = XLSX.read(templateData, { type: 'array', cellDates: true })
|
||||||
|
const templateWorkSheet = templateWorkBook.Sheets[templateWorkBook.SheetNames[0]]
|
||||||
|
const templateExcelData: object[] = XLSX.utils.sheet_to_json(templateWorkSheet)
|
||||||
|
|
||||||
|
const templateHeader = Object.keys(templateExcelData[0])
|
||||||
|
const header = Object.keys(excelData[0])
|
||||||
|
|
||||||
|
if (!headersEqual(templateHeader, header)) {
|
||||||
|
globalThis.postMessage({
|
||||||
|
type: 'error',
|
||||||
|
data: null,
|
||||||
|
message: '表头不一致,请先下载模板然后修改',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
allData = addOtherInfo(excelData)
|
allData = addOtherInfo(excelData)
|
||||||
globalThis.postMessage({
|
globalThis.postMessage({
|
||||||
type: 'done',
|
type: 'done',
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import type { Ref } from 'vue'
|
|||||||
import type { IPersonConfig } from '@/types/storeType'
|
import type { IPersonConfig } from '@/types/storeType'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { inject } from 'vue'
|
import { inject } from 'vue'
|
||||||
|
import { toast } from 'vue-sonner'
|
||||||
import * as XLSX from 'xlsx'
|
import * as XLSX from 'xlsx'
|
||||||
import { loadingKey } from '@/components/Loading'
|
import { loadingKey } from '@/components/Loading'
|
||||||
import i18n from '@/locales/i18n'
|
import i18n from '@/locales/i18n'
|
||||||
import useStore from '@/store'
|
import useStore from '@/store'
|
||||||
import { readFileBinary } from '@/utils/file'
|
import { readFileBinary, readLocalFileAsArraybuffer } from '@/utils/file'
|
||||||
import ImportExcelWorker from './importExcel.worker?worker'
|
import ImportExcelWorker from './importExcel.worker?worker'
|
||||||
|
|
||||||
export function useViewModel({ exportInputFileRef }: { exportInputFileRef: Ref<HTMLInputElement> }) {
|
export function useViewModel({ exportInputFileRef }: { exportInputFileRef: Ref<HTMLInputElement> }) {
|
||||||
@@ -48,13 +49,6 @@ export function useViewModel({ exportInputFileRef }: { exportInputFileRef: Ref<H
|
|||||||
{
|
{
|
||||||
label: i18n.global.t('data.operation'),
|
label: i18n.global.t('data.operation'),
|
||||||
actions: [
|
actions: [
|
||||||
// {
|
|
||||||
// label: '编辑',
|
|
||||||
// type: 'btn-info',
|
|
||||||
// onClick: (row: any) => {
|
|
||||||
// delPersonItem(row)
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
label: i18n.global.t('data.delete'),
|
label: i18n.global.t('data.delete'),
|
||||||
type: 'btn-error',
|
type: 'btn-error',
|
||||||
@@ -66,6 +60,18 @@ export function useViewModel({ exportInputFileRef }: { exportInputFileRef: Ref<H
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
async function getExcelTemplateContent() {
|
||||||
|
const locale = i18n.global.locale.value
|
||||||
|
if (locale === 'zhCn') {
|
||||||
|
const templateData = await readLocalFileAsArraybuffer('log-lottery/人口登记表-zhCn.xlsx')
|
||||||
|
return templateData
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const templateData = await readLocalFileAsArraybuffer('log-lottery/personListTemplate-en.xlsx')
|
||||||
|
return templateData
|
||||||
|
}
|
||||||
|
}
|
||||||
/// 向worker发送消息
|
/// 向worker发送消息
|
||||||
function sendWorkerMessage(message: any) {
|
function sendWorkerMessage(message: any) {
|
||||||
if (worker) {
|
if (worker) {
|
||||||
@@ -73,15 +79,15 @@ export function useViewModel({ exportInputFileRef }: { exportInputFileRef: Ref<H
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// 开始导入
|
/// 开始导入
|
||||||
function startWorker(data: Event) {
|
async function startWorker(data: string) {
|
||||||
loading?.show()
|
loading?.show()
|
||||||
sendWorkerMessage({ type: 'start', data })
|
getExcelTemplateContent()
|
||||||
|
sendWorkerMessage({ type: 'start', data, templateData: await getExcelTemplateContent() })
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 获取用户数据
|
* 获取用户数据
|
||||||
*/
|
*/
|
||||||
async function handleFileChange(e: Event) {
|
async function handleFileChange(e: Event) {
|
||||||
// worker = new ImportExcelWorker()
|
|
||||||
if (worker) {
|
if (worker) {
|
||||||
worker.onmessage = (e) => {
|
worker.onmessage = (e) => {
|
||||||
if (e.data.type === 'done') {
|
if (e.data.type === 'done') {
|
||||||
@@ -90,6 +96,9 @@ export function useViewModel({ exportInputFileRef }: { exportInputFileRef: Ref<H
|
|||||||
// 导入成功后清空file input
|
// 导入成功后清空file input
|
||||||
clearFileInput()
|
clearFileInput()
|
||||||
}
|
}
|
||||||
|
if (e.data.type === 'error') {
|
||||||
|
toast.warning(e.data.message || '导入错误')
|
||||||
|
}
|
||||||
loading?.hide()
|
loading?.hide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import * as XLSX from 'xlsx'
|
|
||||||
import { addOtherInfo } from '@/utils'
|
|
||||||
// 定义消息类型
|
|
||||||
interface WorkerMessage {
|
|
||||||
type: 'start' | 'stop' | 'reset'
|
|
||||||
data: any
|
|
||||||
}
|
|
||||||
|
|
||||||
let allData: any[] = []
|
|
||||||
|
|
||||||
// 接收主线程消息
|
|
||||||
globalThis.onmessage = async (e: MessageEvent<WorkerMessage>) => {
|
|
||||||
switch (e.data.type) {
|
|
||||||
case 'start':
|
|
||||||
{
|
|
||||||
const fileData = e.data.data
|
|
||||||
// const dataBinary = await readFileBinary(((fileEvent.target as HTMLInputElement).files as FileList)[0]!)
|
|
||||||
const workBook = XLSX.read(fileData, { type: 'binary', cellDates: true })
|
|
||||||
const workSheet = workBook.Sheets[workBook.SheetNames[0]]
|
|
||||||
const excelData = XLSX.utils.sheet_to_json(workSheet)
|
|
||||||
allData = addOtherInfo(excelData)
|
|
||||||
globalThis.postMessage({
|
|
||||||
type: 'done',
|
|
||||||
data: allData,
|
|
||||||
message: '读取完成',
|
|
||||||
})
|
|
||||||
break
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user