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:
@@ -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
64
pnpm-lock.yaml
generated
@@ -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
1
src/components.d.ts
vendored
@@ -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']
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export interface IFileData {
|
||||
dataUrl: string
|
||||
data: string | Blob | ArrayBuffer
|
||||
fileName: string
|
||||
type: string
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
113
src/components/Waterfall/index.vue
Normal file
113
src/components/Waterfall/index.vue
Normal 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>
|
||||
@@ -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
0
src/types/fileType.ts
Normal 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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
53
src/views/Config/Person/PersonAll/columns.ts
Normal file
53
src/views/Config/Person/PersonAll/columns.ts
Normal 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)
|
||||
},
|
||||
},
|
||||
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user