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