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

@@ -31,6 +31,7 @@
"lodash-es": "^4.17.21",
"lucide-vue-next": "^0.559.0",
"markdown-it": "^14.1.0",
"masonry-layout": "^4.2.2",
"pinia": "^3.0.3",
"pinia-plugin-persist": "^1.0.0",
"reka-ui": "^2.6.1",
@@ -63,6 +64,7 @@
"@types/canvas-confetti": "^1.6.4",
"@types/lodash-es": "^4.17.12",
"@types/markdown-it": "^14.1.2",
"@types/masonry-layout": "^4.2.8",
"@types/node": "^25.0.0",
"@types/three": "0.166.0",
"@typescript-eslint/eslint-plugin": "^8.49.0",

64
pnpm-lock.yaml generated
View File

@@ -47,6 +47,9 @@ importers:
markdown-it:
specifier: ^14.1.0
version: 14.1.0
masonry-layout:
specifier: ^4.2.2
version: 4.2.2
pinia:
specifier: ^3.0.3
version: 3.0.3(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3))
@@ -138,6 +141,9 @@ importers:
'@types/markdown-it':
specifier: ^14.1.2
version: 14.1.2
'@types/masonry-layout':
specifier: ^4.2.8
version: 4.2.8
'@types/node':
specifier: ^25.0.0
version: 25.0.0
@@ -1857,6 +1863,9 @@ packages:
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
'@types/jquery@3.5.33':
resolution: {integrity: sha512-SeyVJXlCZpEki5F0ghuYe+L+PprQta6nRZqhONt9F13dWBtR/ftoaIbdRQ7cis7womE+X2LKhsDdDtkkDhJS6g==}
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
@@ -1872,6 +1881,9 @@ packages:
'@types/markdown-it@14.1.2':
resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==}
'@types/masonry-layout@4.2.8':
resolution: {integrity: sha512-Et2to22C31FG1UFaHRBL6BznMOhrur3Ckr9gvR7fRVmPgxqiwCEKZtV8GpFscHyNAKhZ0QlkwXJRPnJvxZUKQw==}
'@types/mdast@4.0.4':
resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
@@ -1887,6 +1899,9 @@ packages:
'@types/node@25.0.0':
resolution: {integrity: sha512-rl78HwuZlaDIUSeUKkmogkhebA+8K1Hy7tddZuJ3D0xV8pZSfsYGTsliGUol1JPzu9EKnTxPC4L1fiWouStRew==}
'@types/sizzle@2.3.10':
resolution: {integrity: sha512-TC0dmN0K8YcWEAEfiPi5gJP14eJe30TTGjkvek3iM/1NdHHsdCA/Td6GvNndMOo/iSnIsZ4HuuhrYPDAmbxzww==}
'@types/sortablejs@1.15.9':
resolution: {integrity: sha512-7HP+rZGE2p886PKV9c9OJzLBI6BBJu1O7lJGYnPyG3fS4/duUCcngkNCjsLwIMV+WMqANe3tt4irrXHSIe68OQ==}
@@ -2881,6 +2896,9 @@ packages:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'}
desandro-matches-selector@2.0.2:
resolution: {integrity: sha512-+1q0nXhdzg1IpIJdMKalUwvvskeKnYyEe3shPRwedNcWtnhEKT3ZxvFjzywHDeGcKViIxTCAoOYQWP1qD7VNyg==}
detect-libc@1.0.3:
resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==}
engines: {node: '>=0.10'}
@@ -3245,6 +3263,9 @@ packages:
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
engines: {node: '>= 0.6'}
ev-emitter@1.1.1:
resolution: {integrity: sha512-ipiDYhdQSCZ4hSbX4rMW+XzNKMD1prg/sTvoVmSLkuQ1MVlwjJQQA+sW8tMYR3BLUr9KjodFV4pvzunvRhd33Q==}
events@3.3.0:
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
engines: {node: '>=0.8.x'}
@@ -3335,6 +3356,9 @@ packages:
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
engines: {node: '>=10'}
fizzy-ui-utils@2.0.7:
resolution: {integrity: sha512-CZXDVXQ1If3/r8s0T+v+qVeMshhfcuq0rqIFgJnrtd+Bu8GmDmqMjntjUePypVtjHXKJ6V4sw9zeyox34n9aCg==}
flat-cache@4.0.1:
resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
engines: {node: '>=16'}
@@ -3410,6 +3434,9 @@ packages:
get-intrinsic@1.2.2:
resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==}
get-size@2.0.3:
resolution: {integrity: sha512-lXNzT/h/dTjTxRbm9BXb+SGxxzkm97h/PCIKtlN/CBCxxmkkIVV21udumMS93MuVTDX583gqc94v3RjuHmI+2Q==}
get-stream@9.0.1:
resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==}
engines: {node: '>=18'}
@@ -4088,6 +4115,9 @@ packages:
markdown-table@3.0.4:
resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
masonry-layout@4.2.2:
resolution: {integrity: sha512-iGtAlrpHNyxaR19CvKC3npnEcAwszXoyJiI8ARV2ePi7fmYhIud25MHK8Zx4P0LCC4d3TNO9+rFa1KoK1OEOaA==}
mdast-util-find-and-replace@3.0.2:
resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==}
@@ -4428,6 +4458,9 @@ packages:
resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==}
engines: {node: '>= 0.8.0'}
outlayer@2.1.1:
resolution: {integrity: sha512-+GplXsCQ3VrbGujAeHEzP9SXsBmJxzn/YdDSQZL0xqBmAWBmortu2Y9Gwdp9J0bgDQ8/YNIPMoBM13nTwZfAhw==}
p-limit@3.1.0:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
engines: {node: '>=10'}
@@ -7438,6 +7471,10 @@ snapshots:
'@types/estree@1.0.8': {}
'@types/jquery@3.5.33':
dependencies:
'@types/sizzle': 2.3.10
'@types/json-schema@7.0.15': {}
'@types/linkify-it@5.0.0': {}
@@ -7453,6 +7490,10 @@ snapshots:
'@types/linkify-it': 5.0.0
'@types/mdurl': 2.0.0
'@types/masonry-layout@4.2.8':
dependencies:
'@types/jquery': 3.5.33
'@types/mdast@4.0.4':
dependencies:
'@types/unist': 3.0.3
@@ -7469,6 +7510,8 @@ snapshots:
dependencies:
undici-types: 7.16.0
'@types/sizzle@2.3.10': {}
'@types/sortablejs@1.15.9': {}
'@types/stats.js@0.17.3': {}
@@ -8640,6 +8683,8 @@ snapshots:
dequal@2.0.3: {}
desandro-matches-selector@2.0.2: {}
detect-libc@1.0.3:
optional: true
@@ -9104,6 +9149,8 @@ snapshots:
etag@1.8.1: {}
ev-emitter@1.1.1: {}
events@3.3.0:
optional: true
@@ -9219,6 +9266,10 @@ snapshots:
locate-path: 6.0.0
path-exists: 4.0.0
fizzy-ui-utils@2.0.7:
dependencies:
desandro-matches-selector: 2.0.2
flat-cache@4.0.1:
dependencies:
flatted: 3.2.9
@@ -9281,6 +9332,8 @@ snapshots:
has-symbols: 1.0.3
hasown: 2.0.0
get-size@2.0.3: {}
get-stream@9.0.1:
dependencies:
'@sec-ant/readable-stream': 0.4.1
@@ -9886,6 +9939,11 @@ snapshots:
markdown-table@3.0.4: {}
masonry-layout@4.2.2:
dependencies:
get-size: 2.0.3
outlayer: 2.1.1
mdast-util-find-and-replace@3.0.2:
dependencies:
'@types/mdast': 4.0.4
@@ -10415,6 +10473,12 @@ snapshots:
prelude-ls: 1.2.1
type-check: 0.4.0
outlayer@2.1.1:
dependencies:
ev-emitter: 1.1.1
fizzy-ui-utils: 2.0.7
get-size: 2.0.3
p-limit@3.1.0:
dependencies:
yocto-queue: 0.1.0

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