96 UI optimization (#122)

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

卸载路由时清除requestAnimationFrame

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

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

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

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

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

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

* style: 💄 奖项列表中的图片类型修复
This commit is contained in:
LOG1997
2025-12-18 17:32:00 +08:00
committed by GitHub
parent 92254cb750
commit 5b8682bb7c
29 changed files with 351 additions and 146 deletions

View File

@@ -0,0 +1,31 @@
# Operation Guide
## Steps
1. Upon first entry, no data will be displayed. You can choose to use default data to view the overall display effect. It is recommended to import your own data for operation. The steps are as follows:
a. Personnel Configuration - Personnel List - Download Template, download the data template and modify it with your data (please note that the header cannot be modified).
b. After modification, click 'Upload File' on the same page to upload the modified Excel table.
2. Enter the Prize Configuration to modify your prize information. Try to keep the name short for better display; "All Participants" indicates whether this award will be drawn from all participants (those who have already won can still participate); "Winners" refers to the number of people to be drawn for this award; "Already Won" cannot be edited; "Selected" means this award has been used, unselecting it will reset the award but not the winners; "Image" is the prize image displayed on the home page (you can upload images in the image list); "Left Icon" is used to adjust the order of prizes.
Completing the above two steps allows normal use.
## Function Description
1. Add Temporary Draw: There is a '+' button in the prize list on the draw page. Clicking it allows you to add a temporary draw. Note: Only one temporary draw can be added at a time. After adding successfully, the current prize will be set to the temporary prize, and after drawing, it will return to the normal prize list.
2. Music and Image List: You can upload files yourself for use. After uploading images successfully, you can select them in the prize configuration for display. After uploading music successfully, it will be added to the play list.
3. Music Playback: Left-click with the mouse to play/pause, right-click to play the next song.
4. Interface Configuration - Pattern Settings: You can use the mouse to click and customize the highlighted patterns on the home page.
5. If you do not want to display the prize list on the home page, uncheck 'Always Show Prize List' in the interface configuration.
6. When clicking buttons on the home page, the button value will not update immediately but will only update after the animation ends. This is a normal phenomenon.
## Shortcuts
Shortcuts are set up on the draw page.
| Shortcut | Description |
| --- | --- |
| Space | Enter Draw / Start / Draw Lucky Winner / Continue |
| Esc | Cancel |

View File

@@ -0,0 +1,31 @@
# 操作说明
## 步骤
1. 首次进入,没有数据展示,可以选择使用默认数据进行使用,查看整体展示效果。推荐导入自己的数据来进行操作。步骤如下:
a. 人员配置-人员名单-下载模板,下载数据模板并修改填入数据(请注意表头不可修改)。
b. 修改好后在同一个页面点击上传文件上传修改后的excel表格。
2. 进入奖品配置,修改自己的奖品信息。名称尽量短一点,方便展示;是否全员参加意指该项奖项是否从全体人员中抽取(已中奖的依然可以参与);获奖人数指该奖项要抽取的人数;已获奖人数不可编辑;已抽取被选中时指该奖项已使用,取消选择会重置该奖项,但不会重置已获奖的人;图片是在首页展示时的奖品图片(可在图片列表自己上传);左侧图标调整奖品顺序用。
完成上面两项已可以正常使用。
## 功能说明
1. 增加临时抽奖:抽奖页面的奖项列表有个‘+’号按钮,点击可临时增加抽奖,注意:一次只能增加一项临时抽奖,新增成功后当前奖项即设置为该临时奖项,抽取成功后返回正常奖项列表.
2. 音乐与图片列表,可自己上传文件进行使用,图片上传成功后就可以在奖项配置中进行选择图片展示,音乐上传成功后即加入了播放列表.
3. 音乐播放:使用鼠标左键点击是播放/暂停,使用鼠标右键点击是播放下一首.
4. 界面配置-图案设置中可使用鼠标点击自定义配置首页中的高亮图案.
5. 若不想在首页展示奖品列表,将界面配置中的'是否常显奖项列表'选中.
6. 首页点击按钮时按钮值不会立即更新,会等动画结束后才会更新为目标值,属于正常现象.
## 快捷键
在抽奖页面设置了快捷键。
| 快捷键 | 说明 |
| --- | --- |
| Space | 进入抽奖/开始/抽取幸运儿/继续 |
| Esc | 取消 |

1
src/components.d.ts vendored
View File

@@ -62,5 +62,6 @@ declare module 'vue' {
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

@@ -2,7 +2,7 @@
import type { IFileData } from './type'
import { ListMusic, Upload, X } from 'lucide-vue-next'
import { ref } from 'vue'
import { readFileAsJsonData, readFileData } from '@/utils/file'
import { getBlobObjectUrl, readFileAsJsonData, readFileData, readFileDataAsBlob } from '@/utils/file'
const props = defineProps<{
limitType?: string
@@ -21,13 +21,14 @@ async function handleFileChange(e: Event) {
if (props.mode === 'json') {
const fileRes = await readFileAsJsonData(file)
const jsonData = JSON.parse(fileRes)
fileData.value = { dataUrl: jsonData, fileName: file.name, type }
fileData.value = { data: jsonData, fileName: file.name, type }
originFileName.value = file.name
emits('uploadFile', fileData.value)
return
}
const { dataUrl, fileName } = await readFileData(file)
fileData.value = { dataUrl, fileName, type }
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)
}
@@ -46,7 +47,7 @@ function removeFile() {
@change="handleFileChange"
>
<label for="file-upload" :class="fileData ? 'cursor-not-allowed' : null" class="w-full h-52 cursor-pointer border-2 border-dashed flex items-center justify-center overflow-hidden">
<img v-if="fileData && fileData.type.includes('image')" class="w-full object-cover stroke-0" :src="fileData.dataUrl" alt="">
<img v-if="fileData && fileData.type.includes('image')" class="w-full object-cover stroke-0" :src="getBlobObjectUrl(fileData.data as Blob)" alt="">
<ListMusic v-else-if="fileData && fileData.type.includes('audio')" class="w-2/3 h-2/3 stroke-1 text-gray-500/50" />
<div v-else class="w-full h-full flex justify-center items-center flex-col gap-4">
<Upload class="w-2/3 h-2/3 stroke-1 text-gray-500/50" />

View File

@@ -1,5 +1,5 @@
export interface IFileData {
dataUrl: string
data: string | Blob | ArrayBuffer
fileName: string
type: string
}

View File

@@ -1,36 +1,35 @@
<script setup lang='ts'>
import type { IFileData } from '../FileUpload/type'
import type { IImage } from '@/types/storeType'
import localforage from 'localforage'
import { onMounted, ref } from 'vue'
const props = defineProps({
imgItem: {
type: Object,
default: () => ({}),
},
})
interface IProps {
imgItem: IImage
}
const props = defineProps<IProps>()
const imageDbStore = localforage.createInstance({
name: 'imgStore',
})
const imgUrl = ref('')
async function getImageStoreItem(item: any): Promise<string> {
async function getImageStoreItem(item: IImage): Promise<string> {
let image = ''
if (item.url === 'Storage') {
const key = item.id
const imageData = await imageDbStore.getItem(key) as any
image = imageData.dataUrl
const imageData = await imageDbStore.getItem<IFileData>(key)
image = URL.createObjectURL(imageData?.data as Blob)
}
else {
image = item.url
image = item.url as string
}
return image
}
onMounted(async () => {
const image = await getImageStoreItem(props.imgItem)
imgUrl.value = image
imgUrl.value = await getImageStoreItem(props.imgItem)
})
</script>

View File

@@ -0,0 +1,113 @@
<script setup lang="ts">
import type { Ref } from 'vue'
import { throttle } from 'lodash-es' // lodash-es 节流
import Masonry from 'masonry-layout'
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
// 布局参数 Props
interface MasonryWaterfallProps {
columnWidth?: number | string // 列宽px/选择器)
gutter?: number // 列/行间距
fitWidth?: boolean // 容器宽度自适应居中
}
// 默认参数
const props = withDefaults(defineProps<MasonryWaterfallProps>(), {
columnWidth: 120,
gutter: 16,
fitWidth: true,
})
// Vue Ref 管理 DOM 容器和 masonry 实例
const masonryContainer: Ref<HTMLDivElement | null> = ref(null)
const masonryInstance: Ref<Masonry | null> = ref(null)
// 初始化 masonry仅执行一次因卡片固定
async function initMasonry() {
if (!masonryContainer.value)
return
// 等待插槽内容(固定卡片)完全渲染
await nextTick()
// 初始化 masonry 实例(固定卡片无需销毁旧实例)
masonryInstance.value = new Masonry(masonryContainer.value, {
itemSelector: '.masonry-container > *', // 匹配所有固定子项
columnWidth: props.columnWidth,
gutter: props.gutter,
fitWidth: props.fitWidth,
initLayout: true, // 固定卡片直接初始化布局
})
}
// 刷新布局(仅用于卡片内部内容高度变化)
async function refreshLayout() {
await nextTick()
if (masonryInstance.value) {
masonryInstance.value.layout?.()
}
}
// 窗口缩放节流重排(优化性能)
const handleResize = throttle(() => {
if (masonryInstance.value) {
masonryInstance.value.layout?.()
}
}, 300)
// 生命周期:挂载时初始化,卸载时清理
onMounted(async () => {
await initMasonry()
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
// 销毁实例 + 释放内存
if (masonryInstance.value) {
masonryInstance.value.destroy?.()
masonryInstance.value = null
}
// 移除监听 + 取消节流任务
window.removeEventListener('resize', handleResize)
handleResize.cancel()
})
// 仅暴露刷新方法(适配卡片内部内容变化)
defineExpose({ refreshLayout })
</script>
<template>
<!-- masonry 容器ref 绑定接收固定插槽内容 -->
<div ref="masonryContainer" class="masonry-container">
<!-- 插槽直接传入固定的卡片/组件 -->
<slot />
</div>
</template>
<style scoped>
.masonry-container {
width: 100%;
/* max-width: 1400px; */
margin: 0 auto;
padding: 20px;
box-sizing: border-box;
}
/* 固定卡片基础样式 */
.masonry-container > * {
margin-bottom: v-bind('`${gutter}px`');
box-sizing: border-box;
break-inside: avoid; /* 兼容 Safari */
min-height: 100px; /* 避免内容过矮导致布局异常 */
}
/* 响应式适配:小屏调整列宽 */
/* @media (max-width: 768px) {
.masonry-container {
padding: 10px;
}
.masonry-container > * {
width: calc(50% - v-bind('`${gutter}px`')) !important;
}
} */
</style>

View File

@@ -1,3 +1,5 @@
import type { IFileData } from '@/components/FileUpload/type'
import type { IMusic } from '@/types/storeType'
import localforage from 'localforage'
import { storeToRefs } from 'pinia'
import { onMounted, onUnmounted, ref, watch } from 'vue'
@@ -12,7 +14,7 @@ export function usePlayMusic() {
const { getMusicList: localMusicList, getCurrentMusic: currentMusic } = storeToRefs(globalConfig)
const audio = ref(new Audio())
async function play(item: any) {
async function play(item: IMusic) {
if (!item) {
return
}
@@ -26,16 +28,18 @@ export function usePlayMusic() {
return
}
if (item.url === 'Storage') {
audioUrl = await audioDbStore.getItem(item.name) as string
const key = item.id
const audioData = await audioDbStore.getItem<IFileData>(key)
audioUrl = URL.createObjectURL(audioData?.data as Blob)
}
else {
audioUrl = item.url
audioUrl = item.url as string
}
audio.value.pause()
audio.value.src = audioUrl
audio.value.play()
}
function playMusic(item: any, skip = false) {
function playMusic(item: IMusic, skip = false) {
if (!item) {
return
}
@@ -49,7 +53,7 @@ export function usePlayMusic() {
function nextPlay() {
// 播放下一首
if (localMusicList.value.length >= 1) {
let index = localMusicList.value.findIndex((item: any) => item.name === currentMusic.value.item.name)
let index = localMusicList.value.findIndex((item: IMusic) => item.name === currentMusic.value.item.name)
index++
if (index >= localMusicList.value.length) {
index = 0
@@ -69,7 +73,7 @@ export function usePlayMusic() {
onUnmounted(() => {
audio.value.removeEventListener('ended', nextPlay)
})
watch(currentMusic, (val: any) => {
watch(currentMusic, (val: { item: IMusic, paused: boolean }) => {
if (!val.paused && audio.value) {
play(val.item)
}

0
src/types/fileType.ts Normal file
View File

View File

@@ -28,9 +28,9 @@ export interface IPrizeConfig {
count: number
isUsedCount: number
picture: {
id: string | number
id: string
name: string
url: string
url: string | Blob | ArrayBuffer
}
separateCount: {
enable: boolean
@@ -44,11 +44,11 @@ export interface IPrizeConfig {
export interface IMusic {
id: string
name: string
url: string
url: string | Blob | ArrayBuffer
}
export interface IImage {
id: string
name: string
url: string
url: string | Blob | ArrayBuffer
}

View File

@@ -8,16 +8,33 @@ export function readFileBinary(file: File | Blob): Promise<string> {
})
}
export function readFileData(file: any): Promise<{ dataUrl: string, fileName: string }> {
export function readFileData(file: File): Promise<{ data: string, fileName: string }> {
return new Promise((resolve) => {
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = (ev: any) => {
resolve({ dataUrl: ev.target.result, fileName: file.name })
resolve({ data: ev.target!.result, fileName: file.name })
}
})
}
export function readFileDataAsBlob(file: File): Promise<{ data: Blob, fileName: string }> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
// 直接使用原始文件作为 Blob
resolve({ data: file, fileName: file.name });
};
reader.onerror = () => {
reject(new Error('文件读取失败'));
};
reader.readAsArrayBuffer(file);
});
}
export async function readLocalFileAsArraybuffer(path: string): Promise<ArrayBuffer> {
const response = await fetch(path)
const arrayBuffer = await response.arrayBuffer()
@@ -33,3 +50,7 @@ export async function readFileAsJsonData(file: File | Blob): Promise<any> {
}
})
}
export function getBlobObjectUrl(blob: Blob): string {
return URL.createObjectURL(blob)
}

View File

@@ -42,7 +42,7 @@ async function uploadFile(fileData: IFileData | null) {
function submitUpload() {
if (jsonFileData.value) {
// 把文件转化为json数据
const jsonData = jsonFileData.value.dataUrl
const jsonData = jsonFileData.value.data
console.log('jsonData', jsonData)
props.importAllConfigData(jsonData)
}

View File

@@ -1,19 +1,18 @@
<script setup lang='ts'>
import { useI18n } from 'vue-i18n'
import GridWaterfall from '@/components/Waterfall/index.vue'
import { DataSetting, LayoutSetting, PatternSetting, TextSetting, ThemeSetting } from './parts'
import { useViewModel } from './useViewModel'
const { t } = useI18n()
const { resetData, topTitleValue, languageValue, textSizeValue, currentFontValue, currentTitleFontValue, titleFontSyncGlobalValue, languageList, formErr, formData, cardSizeValue, isShowPrizeListValue, isShowAvatarValue, resetPersonLayout, isRowCountChange, themeValue, backgroundImageValue, cardColorValue, luckyCardColorValue, textColorValue, patternColorValue, imageList, rowCount, cardColor, patternColor, patternList, clearPattern, resetPattern, exportAllConfigData, importAllConfigData } = useViewModel()
</script>
<template>
<div class="flex flex-col gap-4">
<div class="w-4/5 flex flex-col gap-4">
<h2>{{ t('viewTitle.globalSetting') }}</h2>
<div class="flex flex-wrap h-auto w-full gap-6">
<!-- <div class="flex flex-wrap h-auto w-full gap-6"> -->
<GridWaterfall>
<!-- 数据操作 -->
<DataSetting :reset-data="resetData" :export-all-config-data="exportAllConfigData" :import-all-config-data="importAllConfigData" />
<!-- 文本设置主标题语言文字大小 -->
@@ -55,7 +54,8 @@ const { resetData, topTitleValue, languageValue, textSizeValue, currentFontValue
:clear-pattern="clearPattern"
:reset-pattern="resetPattern"
/>
</div>
</GridWaterfall>
<!-- </div> -->
</div>
</template>

View File

@@ -17,32 +17,32 @@ const uploadVisible = ref(false)
</script>
<template>
<dialog id="my_modal_1" ref="resetDataDialogRef" class="border-none modal">
<div class="modal-box">
<h3 class="text-lg font-bold">
{{ t('dialog.titleTip') }}
</h3>
<p class="py-4">
{{ t('dialog.dialogResetAllData') }}
</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="resetDataDialogRef.close()">
{{ t(`button.cancel`) }}
</button>
<button class="btn" @click="resetData">
{{ t('button.confirm') }}
</button>
</form>
</div>
</div>
</dialog>
<UploadJsonModal v-model:visible="uploadVisible" :import-all-config-data="importAllConfigData" />
<fieldset class="p-4 border text-setting fieldset bg-base-200 border-base-300 rounded-box w-xs pb-10">
<fieldset class="p-4 border text-setting fieldset bg-base-200 border-base-300 rounded-box w-xs pb-10">
<legend class="fieldset-legend">
数据操作
</legend>
<dialog id="my_modal_1" ref="resetDataDialogRef" class="border-none modal">
<div class="modal-box">
<h3 class="text-lg font-bold">
{{ t('dialog.titleTip') }}
</h3>
<p class="py-4">
{{ t('dialog.dialogResetAllData') }}
</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="resetDataDialogRef.close()">
{{ t(`button.cancel`) }}
</button>
<button class="btn" @click="resetData">
{{ t('button.confirm') }}
</button>
</form>
</div>
</div>
</dialog>
<UploadJsonModal v-model:visible="uploadVisible" :import-all-config-data="importAllConfigData" />
<label class="flex flex-row items-center form-control">
<div class="">
<div class="label flex flex-col justify-start items-start">

View File

@@ -16,7 +16,7 @@ const isShowAvatarValue = defineModel<boolean>('isShowAvatarValue', { required:
</script>
<template>
<fieldset class="p-4 border text-setting fieldset bg-base-200 border-base-300 rounded-box w-xs pb-10">
<fieldset class="p-4 border text-setting fieldset bg-base-200 border-base-300 rounded-box w-xs pb-10">
<legend class="fieldset-legend">
布局设置
</legend>

View File

@@ -15,7 +15,7 @@ const { t } = useI18n()
</script>
<template>
<fieldset class="p-4 border text-setting fieldset bg-base-200 border-base-300 rounded-box w-xs pb-10">
<fieldset class="p-4 border text-setting fieldset bg-base-200 border-base-300 rounded-box w-xs pb-10">
<legend class="fieldset-legend">
图案设置
</legend>

View File

@@ -1,4 +1,5 @@
<script setup lang='ts'>
import type { IImage } from '@/types/storeType'
import { reactive } from 'vue'
import { ColorPicker } from 'vue3-colorpicker'
import { useI18n } from 'vue-i18n'
@@ -7,7 +8,7 @@ import { daisyuiThemes } from '@/constant/theme'
import 'vue3-colorpicker/style.css'
interface Props {
imageList: Array<{ name: string, url: string, id: string }>
imageList: Array<IImage>
}
defineProps<Props>()
const themeList = reactive(daisyuiThemes)
@@ -23,7 +24,7 @@ const patternColorValue = defineModel<string>('patternColorValue')
</script>
<template>
<fieldset class="p-4 border text-setting fieldset bg-base-200 border-base-300 rounded-box w-xs pb-10">
<fieldset class="p-4 border text-setting fieldset bg-base-200 border-base-300 rounded-box w-xs pb-10">
<legend class="fieldset-legend">
主题设置
</legend>

View File

@@ -48,7 +48,7 @@ async function uploadFile(fileData: IFileData | null) {
async function getImageDbStore() {
const keys = await imageDbStore.keys()
if (keys.length > 0) {
imageDbStore.iterate((value: { fileName: string, dataUrl: string }, key: string) => {
imageDbStore.iterate((value: { fileName: string, data: Blob }, key: string) => {
globalConfig.addImage({
id: key,
name: value.fileName,
@@ -59,10 +59,10 @@ async function getImageDbStore() {
}
function submitUpload() {
if (imageData.value) {
const { dataUrl, fileName } = imageData.value
const { data, fileName } = imageData.value
const uniqueId = uuidv4()
imageDbStore.setItem(uniqueId, {
dataUrl,
data,
fileName,
})
.then(() => {

View File

@@ -52,7 +52,7 @@ async function uploadFile(fileData: IFileData | null) {
async function getAudioDbStore() {
const keys = await audioDbStore.keys()
if (keys.length > 0) {
audioDbStore.iterate((value: { fileName: string, dataUrl: string }, key: string) => {
audioDbStore.iterate((value: { fileName: string, data: Blob }, key: string) => {
globalConfig.addMusic({
id: key,
name: value.fileName,
@@ -63,10 +63,10 @@ async function getAudioDbStore() {
}
function submitUpload() {
if (audioData.value) {
const { dataUrl, fileName } = audioData.value
const { data, fileName } = audioData.value
const uniqueId = uuidv4()
audioDbStore.setItem(uniqueId, {
dataUrl,
data,
fileName,
})
.then(() => {

View File

@@ -0,0 +1,53 @@
import type { IPersonConfig } from '@/types/storeType'
import i18n from '@/locales/i18n'
interface IColumnsProps {
handleDeletePerson: (row: IPersonConfig) => void
}
export function tableColumns(props: IColumnsProps) {
return [
{
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: i18n.global.t('data.delete'),
type: 'btn-error',
onClick: (row: IPersonConfig) => {
props.handleDeletePerson(row)
},
},
],
},
]
}

View File

@@ -10,7 +10,7 @@ import { useViewModel } from './useViewModel'
const resetDataDialogRef = ref()
const delAllDataDialogRef = ref()
const exportInputFileRef = ref()
const { resetData, deleteAll, handleFileChange, exportData, alreadyPersonList, allPersonList, tableColumns } = useViewModel({ exportInputFileRef })
const { resetData, deleteAll, handleFileChange, exportData, alreadyPersonList, allPersonList, tableColumnList } = useViewModel({ exportInputFileRef })
const { t } = useI18n()
const limitType = '.xlsx,.xls'
</script>
@@ -70,7 +70,7 @@ const limitType = '.xlsx,.xls'
</template>
</PageHeader>
<DaiysuiTable :table-columns="tableColumns" :data="allPersonList" />
<DaiysuiTable :table-columns="tableColumnList" :data="allPersonList" />
</div>
</template>

View File

@@ -8,6 +8,7 @@ import { loadingKey } from '@/components/Loading'
import i18n from '@/locales/i18n'
import useStore from '@/store'
import { readFileBinary, readLocalFileAsArraybuffer } from '@/utils/file'
import { tableColumns } from './columns'
import ImportExcelWorker from './importExcel.worker?worker'
export function useViewModel({ exportInputFileRef }: { exportInputFileRef: Ref<HTMLInputElement> }) {
@@ -15,52 +16,7 @@ export function useViewModel({ exportInputFileRef }: { exportInputFileRef: Ref<H
const loading = inject(loadingKey)
const personConfig = useStore().personConfig
const { getAllPersonList: allPersonList, getAlreadyPersonList: alreadyPersonList } = storeToRefs(personConfig)
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: i18n.global.t('data.delete'),
type: 'btn-error',
onClick: (row: IPersonConfig) => {
delPersonItem(row)
},
},
],
},
]
const tableColumnList = tableColumns({ handleDeletePerson: delPersonItem })
async function getExcelTemplateContent() {
const locale = i18n.global.locale.value
if (locale === 'zhCn') {
@@ -171,6 +127,6 @@ export function useViewModel({ exportInputFileRef }: { exportInputFileRef: Ref<H
exportData,
alreadyPersonList,
allPersonList,
tableColumns,
tableColumnList,
}
}

View File

@@ -25,7 +25,8 @@ const { alreadyPersonList, alreadyPersonDetail, isDetail, tableColumnsList, tabl
<label class="label flex items-center gap-2">
<p class="label-text">{{ t('table.detail') }}:</p>
<div class="flex items-center">
<Switch v-model="isDetail" class="cursor-pointer" />
<!-- <Switch v-model="isDetail" class="cursor-pointer" /> -->
<input v-model="isDetail" type="checkbox" :checked="isDetail" class="toggle toggle-primary">
</div>
</label>
</div>

View File

@@ -1,8 +1,8 @@
<script setup lang='ts'>
import markdownit from 'markdown-it'
import { onMounted, ref, watch } from 'vue'
import readmeEn from '@/../public/readme-en.md?raw'
import readmeZh from '@/../public/readme-zhCn.md?raw'
import readmeEn from '@/assets/md/readme-en.md?raw'
import readmeZh from '@/assets/md/readme-zhCn.md?raw'
import i18n from '@/locales/i18n'
const md = markdownit()

View File

@@ -1,29 +1,18 @@
<script setup lang='ts'>
import { ref } from 'vue'
import WaterFall from '@/components/Waterfall/index.vue'
</script>
<template>
<div>
<h2 class="text-3xl animate-pulse bg-linear-to-r from-primary via-secondary to-accent bg-clip-text text-transparent">
两京一十三省
</h2>
<h2>两京一十三省</h2>
<h2>两京一十三省</h2>
<h2>两京一十三省</h2>
<h2>两京一十三省</h2>
<h2>两京一十三省</h2>
<h2>两京一十三省</h2>
</div>
<WaterFall>
<div class="bg-pink-500 h-20 w-48" />
<div class="bg-blue-500 h-30 w-48" />
<div class="bg-yellow-500 h-40 w-48" />
<div class="bg-green-500 h-20 w-48" />
<div class="bg-red-500 h-30 w-48" />
<div class="bg-purple-500 h-50 w-48" />
</WaterFall>
</template>
<style lang='scss' scoped>
.dark-title {
color: red;
}
<style scoped>
</style>

View File

@@ -117,7 +117,7 @@ onMounted(() => {
</div>
<input
type="checkbox" :checked="temporaryPrize.isAll"
class="mt-2 border-solid checkbox checkbox-secondary border-1"
class="mt-2 border-solid checkbox checkbox-secondary border"
@change="temporaryPrize.isAll = !temporaryPrize.isAll"
>
</label>

View File

@@ -40,7 +40,7 @@ async function getImageStoreItem(item: any): Promise<string> {
if (item.url === 'Storage') {
const key = item.id
const imageData = await imageDbStore.getItem(key) as any
image = imageData.dataUrl
image = URL.createObjectURL(imageData.data)
}
else {
image = item.url