Confilct dev date 12 22 (#131)

* fix(home): 🐛 解决多次切换路由后页面卡顿的问题 #96

卸载路由时清除requestAnimationFrame

* feat:  文件存储使用Blob格式

* style: 💄 修改部分类型any为具体类型

* feat:  界面设置中模块使用瀑布流布局 #96

* fix: 🐛 md文档更换文件夹解决控制台警告

* style: 💄 switch按钮改回使用daisyui组件

* refactor: ♻️ 所有人员列表提取tableColumn

* style: 💄 奖项列表中的图片类型修复

* fix(globalConfig): 修复当前音乐项类型缺失问题

* feat:  single person not done

* feat:  可添加单人 #96

* build(.gitignore): 添加 auto-imports.d.ts 到忽略文件

* fix: 🐛 上传、下载excel文件时修复路径错误

打包成应用和网页端的baseUrl不一样,使用环境变量来表示

* fix: 🐛 导入人员列表时处理有值为空的情况

* style: 💄 改变toaster的组件

* fix: 🐛 上传文件、解析数据与存储/读取数据的处理

、
This commit is contained in:
LOG1997
2025-12-22 17:28:10 +08:00
committed by GitHub
parent 5b8682bb7c
commit f8098a9737
18 changed files with 254 additions and 179 deletions

1
.gitignore vendored
View File

@@ -82,6 +82,7 @@ web_modules/
.env.local
**/components.d.ts
**/auto-imports.d.ts
# parcel-bundler cache (https://parceljs.org/)
.cache

10
src/auto-imports.d.ts vendored
View File

@@ -1,10 +0,0 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
}

67
src/components.d.ts vendored
View File

@@ -1,67 +0,0 @@
/* eslint-disable */
// @ts-nocheck
// biome-ignore lint: disable
// oxlint-disable
// ------
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
Button: typeof import('./components/ui/button/Button.vue')['default']
Command: typeof import('./components/ui/command/Command.vue')['default']
CommandDialog: typeof import('./components/ui/command/CommandDialog.vue')['default']
CommandEmpty: typeof import('./components/ui/command/CommandEmpty.vue')['default']
CommandGroup: typeof import('./components/ui/command/CommandGroup.vue')['default']
CommandInput: typeof import('./components/ui/command/CommandInput.vue')['default']
CommandItem: typeof import('./components/ui/command/CommandItem.vue')['default']
CommandList: typeof import('./components/ui/command/CommandList.vue')['default']
CommandSeparator: typeof import('./components/ui/command/CommandSeparator.vue')['default']
CommandShortcut: typeof import('./components/ui/command/CommandShortcut.vue')['default']
DaiysuiTable: typeof import('./components/DaiysuiTable/index.vue')['default']
Dialog: typeof import('./components/Dialog/index.vue')['default']
DialogClose: typeof import('./components/ui/dialog/DialogClose.vue')['default']
DialogContent: typeof import('./components/ui/dialog/DialogContent.vue')['default']
DialogDescription: typeof import('./components/ui/dialog/DialogDescription.vue')['default']
DialogFooter: typeof import('./components/ui/dialog/DialogFooter.vue')['default']
DialogHeader: typeof import('./components/ui/dialog/DialogHeader.vue')['default']
DialogOverlay: typeof import('./components/ui/dialog/DialogOverlay.vue')['default']
DialogScrollContent: typeof import('./components/ui/dialog/DialogScrollContent.vue')['default']
DialogTitle: typeof import('./components/ui/dialog/DialogTitle.vue')['default']
DialogTrigger: typeof import('./components/ui/dialog/DialogTrigger.vue')['default']
DropdownMenu: typeof import('./components/ui/dropdown-menu/DropdownMenu.vue')['default']
DropdownMenuCheckboxItem: typeof import('./components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue')['default']
DropdownMenuContent: typeof import('./components/ui/dropdown-menu/DropdownMenuContent.vue')['default']
DropdownMenuGroup: typeof import('./components/ui/dropdown-menu/DropdownMenuGroup.vue')['default']
DropdownMenuItem: typeof import('./components/ui/dropdown-menu/DropdownMenuItem.vue')['default']
DropdownMenuLabel: typeof import('./components/ui/dropdown-menu/DropdownMenuLabel.vue')['default']
DropdownMenuRadioGroup: typeof import('./components/ui/dropdown-menu/DropdownMenuRadioGroup.vue')['default']
DropdownMenuRadioItem: typeof import('./components/ui/dropdown-menu/DropdownMenuRadioItem.vue')['default']
DropdownMenuSeparator: typeof import('./components/ui/dropdown-menu/DropdownMenuSeparator.vue')['default']
DropdownMenuShortcut: typeof import('./components/ui/dropdown-menu/DropdownMenuShortcut.vue')['default']
DropdownMenuSub: typeof import('./components/ui/dropdown-menu/DropdownMenuSub.vue')['default']
DropdownMenuSubContent: typeof import('./components/ui/dropdown-menu/DropdownMenuSubContent.vue')['default']
DropdownMenuSubTrigger: typeof import('./components/ui/dropdown-menu/DropdownMenuSubTrigger.vue')['default']
DropdownMenuTrigger: typeof import('./components/ui/dropdown-menu/DropdownMenuTrigger.vue')['default']
EditSeparateDialog: typeof import('./components/NumberSeparate/EditSeparateDialog.vue')['default']
FileUpload: typeof import('./components/FileUpload/index.vue')['default']
HelloWorld: typeof import('./components/HelloWorld.vue')['default']
ImageSync: typeof import('./components/ImageSync/index.vue')['default']
Loading: typeof import('./components/Loading/index.vue')['default']
PageHeader: typeof import('./components/PageHeader/index.vue')['default']
Popover: typeof import('./components/ui/popover/Popover.vue')['default']
PopoverAnchor: typeof import('./components/ui/popover/PopoverAnchor.vue')['default']
PopoverContent: typeof import('./components/ui/popover/PopoverContent.vue')['default']
PopoverTrigger: typeof import('./components/ui/popover/PopoverTrigger.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
Sonner: typeof import('./components/ui/sonner/Sonner.vue')['default']
SvgIcon: typeof import('./components/SvgIcon/index.vue')['default']
Switch: typeof import('./components/ui/switch/Switch.vue')['default']
ToTop: typeof import('./components/ToTop/index.vue')['default']
Waterfall: typeof import('./components/Waterfall/index.vue')['default']
}
}

View File

@@ -47,9 +47,9 @@ const actionsColumns = computed<any[]>(() => {
<tbody v-if="data.length > 0">
<!-- row -->
<tr v-for="item in data" :key="item.id" class="hover">
<th>{{ item.id }}</th>
<!-- <th>{{ item.id }}</th> -->
<td v-for="(column, index) in dataColumns" :key="index">
<span v-if="column.formatValue" v-html="column.formatValue(item)"></span>
<span v-if="column.formatValue" v-html="column.formatValue(item)" />
<span v-else>{{ item[column.props] }}</span>
</td>
<!-- action -->

View File

@@ -0,0 +1,38 @@
<script setup lang='ts'>
import { ref } from 'vue'
const drawerTriggerRef = ref <HTMLDialogElement | null> (null)
const visible = ref(false)
function showDrawer() {
drawerTriggerRef.value?.click()
visible.value = true
}
function closeDrawer() {
drawerTriggerRef.value?.click()
visible.value = false
}
defineExpose({
showDrawer,
closeDrawer,
})
</script>
<template>
<div class="drawer drawer-end h-0 w-0">
<input id="my-drawer-1" type="checkbox" class="drawer-toggle">
<div class="drawer-content w-0 h-0 absolute">
<!-- Page content here -->
<label ref="drawerTriggerRef" for="my-drawer-1" />
</div>
<div class="drawer-side">
<label for="my-drawer-1" aria-label="close sidebar" class="drawer-overlay" />
<div v-if="visible" class="menu bg-base-200 min-h-full w-80 p-4">
<slot name="content" />
</div>
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -27,7 +27,6 @@ async function handleFileChange(e: Event) {
return
}
const { data: blobData, fileName } = await readFileDataAsBlob(file)
console.log('datafile', blobData, fileName)
fileData.value = { data: blobData, fileName, type }
originFileName.value = fileName
emits('uploadFile', fileData.value)

View File

@@ -44,12 +44,14 @@ export function useElementStyle(element: any, person: IPersonConfig, index: numb
if (person.name) {
element.children[1].textContent = person.name
}
element.children[2].style.fontSize = `${textSize * 0.5}px`
if (person.department || person.identity) {
element.children[2].innerHTML = `${person.department ? person.department : ''}<br/>${person.identity ? person.identity : ''}`
}
// element.children[2].style.fontSize = `${textSize * 0.5}px`
// if (person.department || person.identity) {
// element.children[2].innerHTML = `${person.department ? person.department : ''}<br/>${person.identity ? person.identity : ''}`
// }
element.children[2].style.fontSize = textSize * 0.5 + 'px'
element.children[2].style.fontSize = `${textSize * 0.5}px`
// 设置部门和身份的默认值
element.children[2].innerHTML = ''
if (person.department || person.identity) {
element.children[2].innerHTML = `${person.department ? person.department : ''}<br/>${person.identity ? person.identity : ''}`
}

View File

@@ -5,10 +5,8 @@ import { useI18n } from 'vue-i18n'
import CustomModal from '@/components/Dialog/index.vue'
import { Loading } from '@/components/Loading'
import ToTop from '@/components/ToTop/index.vue'
import { Toaster } from '@/components/ui/sonner'
import RightButton from './RightButton/index.vue'
import { useMounted } from './useMounted'
import 'vue-sonner/style.css'
const tipDialog = ref()
const { tipDesc } = useMounted(tipDialog)
@@ -31,7 +29,6 @@ function scrollToTop() {
<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" />
</main>
<Toaster />
<RightButton class="absolute right-0 bottom-1/2" />
<CustomModal ref="tipDialog" :title="t('dialog.titleTip')" :desc="tipDesc" />
</div>

View File

@@ -32,7 +32,7 @@ export const useGlobalConfig = defineStore('global', {
imageList: defaultImageList as IImage[],
},
currentMusic: {
item: defaultMusicList[0],
item: defaultMusicList[0] as IMusic,
paused: true,
},
}

View File

@@ -2,7 +2,7 @@ import type { IPersonConfig, IPrizeConfig } from '@/types/storeType'
import dayjs from 'dayjs'
import { defineStore } from 'pinia'
import { v4 as uuidv4 } from 'uuid'
import { computed, ref, toRaw, watch } from 'vue'
import { computed, ref, toRaw } from 'vue'
import { IndexDb } from '@/utils/dexie'
import { defaultPersonList } from './data'
import { usePrizeConfig } from './prizeConfig'
@@ -10,13 +10,13 @@ import { usePrizeConfig } from './prizeConfig'
// 获取IPersonConfig的key组成数组
export const personListKey = Object.keys(defaultPersonList[0])
export const usePersonConfig = defineStore('person', () => {
const personDb = new IndexDb('person', ['allPersonList', 'alreadyPersonList'], 1, personListKey)
const personDb = new IndexDb('person', ['allPersonList', 'alreadyPersonList'], 1, ['createTime'])
// NOTE: state
const personConfig = ref({
allPersonList: [] as IPersonConfig[],
alreadyPersonList: [] as IPersonConfig[],
})
personDb.getAllData('allPersonList').then((data) => {
personDb.getDataSortedByDateTime('allPersonList', 'createTime').then((data) => {
personConfig.value.allPersonList = data
})
personDb.getAllData('alreadyPersonList').then((data) => {
@@ -51,7 +51,7 @@ export const usePersonConfig = defineStore('person', () => {
return item.isWin === false
}))
// NOTE: action
// 添加未中奖人员
// 添加全部未中奖人员
function addNotPersonList(personList: IPersonConfig[]) {
if (personList.length <= 0) {
return
@@ -61,6 +61,20 @@ export const usePersonConfig = defineStore('person', () => {
})
personDb.setAllData('allPersonList', personList)
}
// 添加数据
function addOnePerson(person: IPersonConfig[]) {
if (person.length <= 0) {
return
}
if (person.length > 1) {
console.warn('只支持添加单个用户')
return
}
person.forEach((item: IPersonConfig) => {
personConfig.value.allPersonList.push(item)
personDb.setData('allPersonList', item)
})
}
// 添加已中奖人员
function addAlreadyPersonList(personList: IPersonConfig[], prize: IPrizeConfig | null) {
if (personList.length <= 0) {
@@ -176,6 +190,7 @@ export const usePersonConfig = defineStore('person', () => {
getAlreadyPersonDetail,
getNotPersonList,
addNotPersonList,
addOnePerson,
addAlreadyPersonList,
moveAlreadyToNot,
deletePerson,

View File

@@ -18,7 +18,7 @@ class IndexDb {
// 获取存在的key
const stores: Record<string, string> = {}
for (const tableName of tableNames) {
stores[tableName] = 'id,dateTime,type,uid' // 根据需要调整字段
stores[tableName] = `id,dateTime,type,uid,${dbKeys.join(',')}` // 根据需要调整字段
}
this.dbStore.version(this.version).stores(stores)
}
@@ -72,6 +72,12 @@ class IndexDb {
return isAsc ? allData : allData.reverse()
}
// 按 dateTime 排序获取所有数据
async getDataSortedByDateTime(tableName: string, orderTimeName: string = 'dataTime') {
const allData = await this.dbStore[tableName].orderBy(orderTimeName).toArray()
return allData
}
// 分页获取数据
async getPageData(tableName: string, pageNum: number, pageSize: number, isAsc: boolean = true) {
const allData = await this.dbStore[tableName].toArray()

View File

@@ -25,9 +25,9 @@ export function filterData(tableData: any[], localRowCount: number) {
export function addOtherInfo(personList: any[]) {
const len = personList.length
for (let i = 0; i < len; i++) {
personList[i].id = i
personList[i].createTime = dayjs(new Date()).format('YYYY-MM-DD HH:mm:ss')
personList[i].updateTime = dayjs(new Date()).format('YYYY-MM-DD HH:mm:ss')
personList[i].id = uuidv4()
personList[i].createTime = dayjs(new Date()).format('YYYY-MM-DD HH:mm:ss:ms')
personList[i].updateTime = dayjs(new Date()).format('YYYY-MM-DD HH:mm:ss:ms')
personList[i].prizeName = [] as string[]
personList[i].prizeTime = [] as string[]
personList[i].prizeId = []

View File

@@ -43,7 +43,6 @@ function submitUpload() {
if (jsonFileData.value) {
// 把文件转化为json数据
const jsonData = jsonFileData.value.data
console.log('jsonData', jsonData)
props.importAllConfigData(jsonData)
}
}

View File

@@ -0,0 +1,46 @@
<script setup lang='ts'>
defineProps<{
addOnePersonDrawerRef: any
addOnePerson: (addOnePersonDrawerRef: any, event: any) => void
}>()
const singlePersonData = defineModel<any>('singlePersonData', { required: true })
</script>
<template>
<form class="fieldset rounded-box w-xs p-4" @submit="(e) => addOnePerson(addOnePersonDrawerRef, e)">
<label class="fieldset">
<span class="label">编号</span>
<input v-model="singlePersonData.uid" type="text" class="input validator" placeholder="编号">
</label>
<fieldset class="fieldset">
<label class="label" required>姓名<span class="text-red-500">*</span></label>
<input v-model="singlePersonData.name" type="text" class="input validator" placeholder="姓名" required minlength="1">
<p class="validator-hint hidden">
请填写姓名
</p>
</fieldset>
<label class="fieldset">
<span class="label">部门</span>
<input v-model="singlePersonData.department" type="text" class="input validator" placeholder="部门">
</label>
<label class="fieldset">
<span class="label">头像</span>
<input v-model="singlePersonData.avatar" type="text" class="input validator" placeholder="头像">
</label>
<label class="fieldset">
<span class="label">身份</span>
<input v-model="singlePersonData.identity" type="text" class="input validator" placeholder="身份">
</label>
<button class="btn btn-neutral mt-4" type="submit">
确定
</button>
<button class="btn btn-ghost mt-1" type="reset" @click="addOnePersonDrawerRef.closeDrawer()">
取消
</button>
</form>
</template>
<style scoped>
</style>

View File

@@ -10,8 +10,8 @@ interface WorkerMessage {
let allData: any[] = []
function headersEqual(template: string[], actual: string[]): boolean {
return template.length === actual.length
&& template.every((value, index) => value === actual[index])
return template.length >= actual.length
&& actual.some(item => template.includes(item))
}
// 接收主线程消息

View File

@@ -4,13 +4,17 @@ import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import DaiysuiTable from '@/components/DaiysuiTable/index.vue'
import CustomDialog from '@/components/Dialog/index.vue'
import CustomDrawer from '@/components/Drawer/index.vue'
import PageHeader from '@/components/PageHeader/index.vue'
import SinglePersonContent from './components/SinglePerson.vue'
import { useViewModel } from './useViewModel'
const resetDataDialogRef = ref()
const delAllDataDialogRef = ref()
const exportInputFileRef = ref()
const { resetData, deleteAll, handleFileChange, exportData, alreadyPersonList, allPersonList, tableColumnList } = useViewModel({ exportInputFileRef })
const addOnePersonDrawerRef = ref()
const baseUrl = import.meta.env.BASE_URL
const { resetData, deleteAll, handleFileChange, exportData, addOnePerson, singlePersonData, alreadyPersonList, allPersonList, tableColumnList } = useViewModel({ exportInputFileRef })
const { t } = useI18n()
const limitType = '.xlsx,.xls'
</script>
@@ -28,6 +32,15 @@ const limitType = '.xlsx,.xls'
:desc="t('dialog.dialogDelAllPerson')"
:submit-func="deleteAll"
/>
<CustomDrawer ref="addOnePersonDrawerRef">
<template #content>
<SinglePersonContent
v-model:single-person-data="singlePersonData"
:add-one-person-drawer-ref="addOnePersonDrawerRef"
:add-one-person="addOnePerson"
/>
</template>
</CustomDrawer>
<div class="min-w-1000px">
<PageHeader :title="t('viewTitle.personManagement')">
@@ -39,7 +52,7 @@ const limitType = '.xlsx,.xls'
<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')}`"
:href="`${baseUrl}${t('data.xlsxName')}`"
>{{ t('button.downloadTemplate') }}</a>
</div>
<div class="">
@@ -60,6 +73,9 @@ const limitType = '.xlsx,.xls'
<button class="btn btn-accent btn-sm" @click="exportData">
{{ t('button.exportResult') }}
</button>
<button class="btn btn-neutral btn-sm" @click="addOnePersonDrawerRef.showDrawer()">
添加
</button>
<div>
<span>{{ t('table.luckyPeopleNumber') }}:</span>
<span>{{ alreadyPersonList.length }}</span>

View File

@@ -1,30 +1,43 @@
import type { Ref } from 'vue'
import type { IPersonConfig } from '@/types/storeType'
import { storeToRefs } from 'pinia'
import { inject } from 'vue'
import { toast } from 'vue-sonner'
import { v4 as uuidv4 } from 'uuid'
import { inject, ref, toRaw } from 'vue'
import { useToast } from 'vue-toast-notification'
import * as XLSX from 'xlsx'
import { loadingKey } from '@/components/Loading'
import i18n from '@/locales/i18n'
import useStore from '@/store'
import { addOtherInfo } from '@/utils'
import { readFileBinary, readLocalFileAsArraybuffer } from '@/utils/file'
import { tableColumns } from './columns'
import ImportExcelWorker from './importExcel.worker?worker'
type IBasePersonConfig = Pick<IPersonConfig, 'uid' | 'name' | 'department' | 'identity' | 'avatar'>
export function useViewModel({ exportInputFileRef }: { exportInputFileRef: Ref<HTMLInputElement> }) {
const toast = useToast()
const worker: Worker | null = new ImportExcelWorker()
const loading = inject(loadingKey)
const personConfig = useStore().personConfig
const { getAllPersonList: allPersonList, getAlreadyPersonList: alreadyPersonList } = storeToRefs(personConfig)
const tableColumnList = tableColumns({ handleDeletePerson: delPersonItem })
const addPersonModalVisible = ref(false)
const singlePersonData = ref<IBasePersonConfig>({
uid: '',
name: '',
department: '',
avatar: '',
identity: '',
})
async function getExcelTemplateContent() {
const locale = i18n.global.locale.value
if (locale === 'zhCn') {
const templateData = await readLocalFileAsArraybuffer('/log-lottery/人口登记表-zhCn.xlsx')
const templateData = await readLocalFileAsArraybuffer(`${import.meta.env.BASE_URL}人口登记表-zhCn.xlsx`)
return templateData
}
else {
const templateData = await readLocalFileAsArraybuffer('/log-lottery/personListTemplate-en.xlsx')
const templateData = await readLocalFileAsArraybuffer(`${import.meta.env.BASE_URL}personListTemplate-en.xlsx`)
return templateData
}
}
@@ -53,7 +66,12 @@ export function useViewModel({ exportInputFileRef }: { exportInputFileRef: Ref<H
clearFileInput()
}
if (e.data.type === 'error') {
toast.warning(e.data.message || '导入错误')
toast.open({
message: e.data.message || '导入错误',
type: 'error',
position: 'top-right',
})
// toast.warning(e.data.message || '导入错误')
}
loading?.hide()
}
@@ -67,7 +85,7 @@ export function useViewModel({ exportInputFileRef }: { exportInputFileRef: Ref<H
exportInputFileRef.value.value = ''
}
}
/// 导出数据
// 导出数据
function exportData() {
let data = JSON.parse(JSON.stringify(allPersonList.value))
// 排除一些字段
@@ -120,6 +138,17 @@ export function useViewModel({ exportInputFileRef }: { exportInputFileRef: Ref<H
function delPersonItem(row: IPersonConfig) {
personConfig.deletePerson(row)
}
function addOnePerson(addOnePersonDrawerRef: any, event: any) {
event.preventDefault()
// 表单中的验证信息清除
const personData = addOtherInfo([toRaw(singlePersonData.value)])
personData[0].id = uuidv4()
personConfig.addOnePerson(personData)
// singlePersonData.value = {} as IBasePersonConfig
addOnePersonDrawerRef.closeDrawer()
singlePersonData.value = {} as IBasePersonConfig
}
return {
resetData,
deleteAll,
@@ -128,5 +157,8 @@ export function useViewModel({ exportInputFileRef }: { exportInputFileRef: Ref<H
alreadyPersonList,
allPersonList,
tableColumnList,
addOnePerson,
addPersonModalVisible,
singlePersonData,
}
}

View File

@@ -3,11 +3,12 @@ import localforage from 'localforage'
import { cloneDeep } from 'lodash-es'
import { storeToRefs } from 'pinia'
import { onMounted, ref, watch } from 'vue'
import { toast } from 'vue-sonner'
import { useToast } from 'vue-toast-notification'
import i18n from '@/locales/i18n'
import useStore from '@/store'
export function usePrizeConfig() {
const toast = useToast()
const imageDbStore = localforage.createInstance({
name: 'imgStore',
})