feat: 添加Loading效果

This commit is contained in:
LOG1997
2025-10-12 22:30:54 +08:00
parent ebbed65253
commit 27fd0768c1
11 changed files with 266 additions and 155 deletions

View File

@@ -1,11 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { onMounted, ref } from 'vue' import { onMounted, provide, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { loadingKey, loadingState } from '@/components/Loading'
import PlayMusic from '@/components/PlayMusic/index.vue' import PlayMusic from '@/components/PlayMusic/index.vue'
import useStore from '@/store' import useStore from '@/store'
import { themeChange } from '@/utils' import { themeChange } from '@/utils'
provide(loadingKey, loadingState)
const { t } = useI18n() const { t } = useI18n()
const globalConfig = useStore().globalConfig const globalConfig = useStore().globalConfig
const prizeConfig = useStore().prizeConfig const prizeConfig = useStore().prizeConfig
@@ -14,7 +16,6 @@ const { getTheme: localTheme } = storeToRefs(globalConfig)
const { getPrizeConfig: prizeList } = storeToRefs(prizeConfig) const { getPrizeConfig: prizeList } = storeToRefs(prizeConfig)
const tipDialog = ref() const tipDialog = ref()
// 设置当前奖列表 // 设置当前奖列表
function setCurrentPrize() { function setCurrentPrize() {
if (prizeList.value.length <= 0) { if (prizeList.value.length <= 0) {

1
src/components.d.ts vendored
View File

@@ -13,6 +13,7 @@ declare module 'vue' {
EditSeparateDialog: typeof import('./components/NumberSeparate/EditSeparateDialog.vue')['default'] EditSeparateDialog: typeof import('./components/NumberSeparate/EditSeparateDialog.vue')['default']
HelloWorld: typeof import('./components/HelloWorld.vue')['default'] HelloWorld: typeof import('./components/HelloWorld.vue')['default']
ImageSync: typeof import('./components/ImageSync/index.vue')['default'] ImageSync: typeof import('./components/ImageSync/index.vue')['default']
Loading: typeof import('./components/Loading/index.vue')['default']
PlayMusic: typeof import('./components/PlayMusic/index.vue')['default'] PlayMusic: typeof import('./components/PlayMusic/index.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']

View File

@@ -0,0 +1,5 @@
import type { LoadingOptions } from './loading-context'
import Loading from './index.vue'
import { loadingKey, loadingState } from './loading-context'
export { Loading, loadingKey, LoadingOptions, loadingState }

View File

@@ -0,0 +1,22 @@
<script setup lang='ts'>
import type { LoadingOptions } from './loading-context'
import { inject } from 'vue'
import { loadingKey } from './loading-context'
// 注入全局状态
const loading = inject(loadingKey) as LoadingOptions
// 解构状态(响应式)
const { visible, text } = loading
</script>
<template>
<div v-if="visible" class="fixed top-0 left-0 w-screen h-screen bg-[rgba(0,0,0,0.5)] flex flex-col gap-6 justify-center items-center z-50">
<span v-if="visible" class="loading loading-spinner loading-xl" />
<span>{{ text ? text : '加载中' }}</span>
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,61 @@
// src/contexts/loading-context.ts
import type { InjectionKey, Ref } from 'vue'
import { ref } from 'vue'
// 定义 Loading 配置类型
export interface LoadingOptions {
visible: Ref<boolean>
text: Ref<string>
fullscreen: Ref<boolean>
zIndex: Ref<number>
count: Ref<number>
show: (options?: Partial<{ text: string, fullscreen: boolean, zIndex: number }>) => void
hide: () => void
}
// 注入密钥Symbol 确保唯一性)
export const loadingKey: InjectionKey<LoadingOptions> = Symbol('loading')
// 全局状态(单例)
const visible = ref(false)
const text = ref('')
const fullscreen = ref(true)
const zIndex = ref(9999)
const count = ref(0)
// 显示 Loading
function show(options?: Partial<{ text: string, fullscreen: boolean, zIndex: number }>) {
count.value++
if (count.value > 1)
return
visible.value = true
if (options) {
text.value = options.text || ''
fullscreen.value = options.fullscreen ?? true
zIndex.value = options.zIndex || 9999
}
}
// 隐藏 Loading
function hide() {
if (count.value <= 0)
return
count.value--
if (count.value === 0) {
visible.value = false
text.value = ''
fullscreen.value = true
zIndex.value = 9999
}
}
// 导出全局状态(供根组件提供)
export const loadingState: LoadingOptions = {
visible,
text,
fullscreen,
zIndex,
count,
show,
hide,
}

View File

@@ -1,12 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import ToTop from '@/components/ToTop/index.vue'
import { useScroll } from '@vueuse/core' import { useScroll } from '@vueuse/core'
// import Header from './Header/index.vue'; // import Header from './Header/index.vue';
// import Footer from './Footer/index.vue'; // import Footer from './Footer/index.vue';
import { ref } from 'vue' import { ref } from 'vue'
import { Loading } from '@/components/Loading'
import ToTop from '@/components/ToTop/index.vue'
const mainContainer = ref<HTMLElement | null>(null) const mainContainer = ref<HTMLElement | null>(null)
const { y } = useScroll(mainContainer) const { y } = useScroll(mainContainer)
function scrollToTop() { function scrollToTop() {
@@ -19,6 +19,7 @@ function scrollToTop() {
<!-- <header class="shadow-2xl head-container h-14"> <!-- <header class="shadow-2xl head-container h-14">
<Header></Header> <Header></Header>
</header> --> </header> -->
<Loading />
<ToTop v-if="y > 400" @click="scrollToTop" /> <ToTop v-if="y > 400" @click="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" />

View File

@@ -1,20 +1,20 @@
import svgIcon from '@/components/SvgIcon/index.vue' // pinia
import i18n from '@/locales/i18n' import { createPinia } from 'pinia'
// pinia持久化
import piniaPluginPersist from 'pinia-plugin-persist'
import * as THREE from 'three' import * as THREE from 'three'
import { createApp } from 'vue' import { createApp } from 'vue'
import VueDOMPurifyHTML from 'vue-dompurify-html' import VueDOMPurifyHTML from 'vue-dompurify-html'
import svgIcon from '@/components/SvgIcon/index.vue'
import i18n from '@/locales/i18n'
// svg全局组件// 路由
import router from '@/router'
import App from './App.vue' import App from './App.vue'
import './style.css' import './style.css'
import './style/markdown.css' import './style/markdown.css'
import './style/style.scss' import './style/style.scss'
// 全局svg组件 // 全局svg组件
import 'virtual:svg-icons-register' import 'virtual:svg-icons-register'
// svg全局组件// 路由
import router from '@/router'
// pinia
import { createPinia } from 'pinia'
// pinia持久化
import piniaPluginPersist from 'pinia-plugin-persist'
const app = createApp(App) const app = createApp(App)
const pinia = createPinia() const pinia = createPinia()

View File

@@ -27,6 +27,11 @@ globalThis.onmessage = async (e: MessageEvent<WorkerMessage>) => {
break break
} }
default: default:
globalThis.postMessage({
type: 'fail',
data: null,
message: '读取失败',
})
break break
} }
} }

View File

@@ -6,12 +6,12 @@ import DaiysuiTable from '@/components/DaiysuiTable/index.vue'
import CustomDialog from '@/components/Dialog/index.vue' import CustomDialog from '@/components/Dialog/index.vue'
import { useViewModel } from './useViewModel' import { useViewModel } from './useViewModel'
const { resetData, deleteAll, handleFileChange, exportData, alreadyPersonList, allPersonList, tableColumns } = useViewModel()
const { t } = useI18n()
const limitType = '.xlsx,.xls'
const resetDataDialogRef = ref() const resetDataDialogRef = ref()
const delAllDataDialogRef = ref() const delAllDataDialogRef = ref()
const exportInputFileRef = ref()
const { resetData, deleteAll, handleFileChange, exportData, alreadyPersonList, allPersonList, tableColumns } = useViewModel({ exportInputFileRef })
const { t } = useI18n()
const limitType = '.xlsx,.xls'
</script> </script>
<template> <template>
@@ -45,8 +45,8 @@ const delAllDataDialogRef = ref()
<div class="tooltip tooltip-bottom" :data-tip="t('tooltip.uploadExcelTip')"> <div class="tooltip tooltip-bottom" :data-tip="t('tooltip.uploadExcelTip')">
<input <input
id="explore" type="file" class="" style="display: none" :accept="limitType" id="explore" ref="exportInputFileRef" type="file" class="" style="display: none"
@change="handleFileChange" :accept="limitType" @change="handleFileChange"
> >
<span class="btn btn-primary btn-sm">{{ t('button.importData') }}</span> <span class="btn btn-primary btn-sm">{{ t('button.importData') }}</span>

View File

@@ -1,13 +1,17 @@
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 * as XLSX from 'xlsx' import * as XLSX from 'xlsx'
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 } from '@/utils/file'
import ImportExcelWorker from './importExcel.worker?worker' import ImportExcelWorker from './importExcel.worker?worker'
export function useViewModel() { export function useViewModel({ exportInputFileRef }: { exportInputFileRef: Ref<HTMLInputElement> }) {
const worker: Worker | null = new ImportExcelWorker() const worker: Worker | null = new ImportExcelWorker()
const loading = inject(loadingKey)
const personConfig = useStore().personConfig const personConfig = useStore().personConfig
const { getAllPersonList: allPersonList, getAlreadyPersonList: alreadyPersonList } = storeToRefs(personConfig) const { getAllPersonList: allPersonList, getAlreadyPersonList: alreadyPersonList } = storeToRefs(personConfig)
const tableColumns = [ const tableColumns = [
@@ -70,6 +74,7 @@ export function useViewModel() {
} }
/// 开始导入 /// 开始导入
function startWorker(data: Event) { function startWorker(data: Event) {
loading?.show()
sendWorkerMessage({ type: 'start', data }) sendWorkerMessage({ type: 'start', data })
} }
/** /**
@@ -82,12 +87,21 @@ export function useViewModel() {
if (e.data.type === 'done') { if (e.data.type === 'done') {
personConfig.resetPerson() personConfig.resetPerson()
personConfig.addNotPersonList(e.data.data) personConfig.addNotPersonList(e.data.data)
// 导入成功后清空file input
clearFileInput()
} }
loading?.hide()
} }
} }
const dataBinary = await readFileBinary(((e.target as HTMLInputElement).files as FileList)[0]!) const dataBinary = await readFileBinary(((e.target as HTMLInputElement).files as FileList)[0]!)
startWorker(dataBinary) startWorker(dataBinary)
} }
// 清空file input
function clearFileInput() {
if (exportInputFileRef.value) {
exportInputFileRef.value.value = ''
}
}
/// 导出数据 /// 导出数据
function exportData() { function exportData() {
let data = JSON.parse(JSON.stringify(allPersonList.value)) let data = JSON.parse(JSON.stringify(allPersonList.value))

1
src/vite-env.d.ts vendored
View File

@@ -9,3 +9,4 @@ declare module '*.vue' {
declare module 'sparticles' declare module 'sparticles'
declare module 'three-trackballcontrols' declare module 'three-trackballcontrols'
declare module 'virtual:svg-icons-register'