feat(PageHeader): 新增 PageHeader 组件并应用于多个配置页面

新增了 PageHeader 通用组件,用于统一页面标题和操作按钮区域的布局。
该组件包含 title 属性以及 buttons 和 alerts 两个具名插槽,便于复用和维护。
已在以下页面中集成使用:
- 图片管理页(ImageConfig.vue)
- 音乐管理页(MusicConfig.vue)
- 人员管理页(PersonAll/index.vue)
- 中奖者管理页(PersonAlready.vue)
- 奖品管理页(PrizeConfig.vue)
This commit is contained in:
log1997
2025-12-02 16:07:48 +08:00
parent 5c8347ff62
commit d776017306
7 changed files with 163 additions and 111 deletions

1
src/components.d.ts vendored
View File

@@ -14,6 +14,7 @@ declare module 'vue' {
HelloWorld: typeof import('./components/HelloWorld.vue')['default']
ImageSync: typeof import('./components/ImageSync/index.vue')['default']
Loading: typeof import('./components/Loading/index.vue')['default']
PageHeader: typeof import('./components/PageHeader/index.vue')['default']
PlayMusic: typeof import('./components/PlayMusic/index.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']

View File

@@ -0,0 +1,24 @@
<script setup lang='ts'>
const props = defineProps<{
title: string
}>()
</script>
<template>
<header>
<h1 class="text-lg leading-10">
{{ props.title }}
</h1>
<div class="button-group my-4">
<slot name="buttons" />
</div>
<div class="divider mt-0" />
<div>
<slot name="alerts" />
</div>
</header>
</template>
<style scoped>
</style>

View File

@@ -1,12 +1,13 @@
<script setup lang='ts'>
import type { IImage } from '@/types/storeType'
import ImageSync from '@/components/ImageSync/index.vue'
import useStore from '@/store'
import { readFileData } from '@/utils/file'
import localforage from 'localforage'
import { storeToRefs } from 'pinia'
import { onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import ImageSync from '@/components/ImageSync/index.vue'
import PageHeader from '@/components/PageHeader/index.vue'
import useStore from '@/store'
import { readFileData } from '@/utils/file'
const { t } = useI18n()
const globalConfig = useStore().globalConfig
@@ -81,15 +82,20 @@ watch(() => imgUploadToast.value, (val) => {
</div>
<div>
<div class="">
<label for="explore">
<input
id="explore" type="file" class="" style="display: none" :accept="limitType"
@change="handleFileChange"
>
<span class="btn btn-primary btn-sm">{{ t('button.upload') }}</span>
</label>
</div>
<PageHeader title="图片管理">
<template #buttons>
<div class="">
<label for="explore">
<input
id="explore" type="file" class="" style="display: none" :accept="limitType"
@change="handleFileChange"
>
<span class="btn btn-primary btn-sm">{{ t('button.upload') }}</span>
</label>
</div>
</template>
</PageHeader>
<ul class="p-0">
<li v-for="item in localImageList" :key="item.id" class="mb-3">
<div class="flex items-center gap-8">

View File

@@ -1,12 +1,12 @@
<script setup lang='ts'>
import type { IMusic } from '@/types/storeType'
import useStore from '@/store'
import { readFileData } from '@/utils/file'
import localforage from 'localforage'
import { storeToRefs } from 'pinia'
import { onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import PageHeader from '@/components/PageHeader/index.vue'
import useStore from '@/store'
import { readFileData } from '@/utils/file'
const { t } = useI18n()
const audioUploadToast = ref(0) // 0是不显示1是成功2是失败,3是不是图片
@@ -74,21 +74,26 @@ onMounted(() => {
<template>
<div>
<div class="flex gap-3">
<button class="btn btn-primary btn-sm" @click="resetMusic">
{{ t('button.reset') }}
</button>
<label for="explore">
<input
id="explore" type="file" class="" style="display: none" :accept="limitType"
@change="handleFileChange"
>
<span class="btn btn-primary btn-sm">{{ t('button.upload') }}</span>
</label>
<button class="btn btn-error btn-sm" @click="deleteAll">
{{ t('button.allDelete') }}
</button>
</div>
<PageHeader title="音乐管理">
<template #buttons>
<div class="flex gap-3">
<button class="btn btn-primary btn-sm" @click="resetMusic">
{{ t('button.reset') }}
</button>
<label for="explore">
<input
id="explore" type="file" class="" style="display: none" :accept="limitType"
@change="handleFileChange"
>
<span class="btn btn-primary btn-sm">{{ t('button.upload') }}</span>
</label>
<button class="btn btn-error btn-sm" @click="deleteAll">
{{ t('button.allDelete') }}
</button>
</div>
</template>
</PageHeader>
<div>
<ul class="p-0">
<li v-for="item in localMusicListValue" :key="item.id" class="flex items-center gap-6 pb-2 mb-3 divide-y">

View File

@@ -4,6 +4,7 @@ import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import DaiysuiTable from '@/components/DaiysuiTable/index.vue'
import CustomDialog from '@/components/Dialog/index.vue'
import PageHeader from '@/components/PageHeader/index.vue'
import { useViewModel } from './useViewModel'
const resetDataDialogRef = ref()
@@ -29,43 +30,46 @@ const limitType = '.xlsx,.xls'
/>
<div class="min-w-1000px">
<h2>{{ t('viewTitle.personManagement') }}</h2>
<div class="flex gap-3">
<button class="btn btn-error btn-sm" @click="delAllDataDialogRef.showDialog()">
{{ t('button.allDelete') }}
</button>
<div class="tooltip tooltip-bottom" :data-tip="t('tooltip.downloadTemplateTip')">
<a
class="no-underline btn btn-secondary btn-sm" :download="t('data.xlsxName')" target="_blank"
:href="`/log-lottery/${t('data.xlsxName')}`"
>{{ t('button.downloadTemplate') }}</a>
</div>
<div class="">
<label for="explore">
<div class="tooltip tooltip-bottom" :data-tip="t('tooltip.uploadExcelTip')">
<input
id="explore" ref="exportInputFileRef" type="file" class="" style="display: none"
:accept="limitType" @change="handleFileChange"
>
<span class="btn btn-primary btn-sm">{{ t('button.importData') }}</span>
<PageHeader :title="t('viewTitle.personManagement')">
<template #buttons>
<div class="flex gap-3">
<button class="btn btn-error btn-sm" @click="delAllDataDialogRef.showDialog()">
{{ t('button.allDelete') }}
</button>
<div class="tooltip tooltip-bottom" :data-tip="t('tooltip.downloadTemplateTip')">
<a
class="no-underline btn btn-secondary btn-sm" :download="t('data.xlsxName')" target="_blank"
:href="`/log-lottery/${t('data.xlsxName')}`"
>{{ t('button.downloadTemplate') }}</a>
</div>
</label>
</div>
<button class="btn btn-error btn-sm" @click="resetDataDialogRef.showDialog()">
{{ t('button.resetData') }}
</button>
<button class="btn btn-accent btn-sm" @click="exportData">
{{ t('button.exportResult') }}
</button>
<div>
<span>{{ t('table.luckyPeopleNumber') }}:</span>
<span>{{ alreadyPersonList.length }}</span>
<span>&nbsp;/&nbsp;</span>
<span>{{ allPersonList.length }}</span>
</div>
</div>
<div class="">
<label for="explore">
<div class="tooltip tooltip-bottom" :data-tip="t('tooltip.uploadExcelTip')">
<input
id="explore" ref="exportInputFileRef" type="file" class="" style="display: none"
:accept="limitType" @change="handleFileChange"
>
<span class="btn btn-primary btn-sm">{{ t('button.importData') }}</span>
</div>
</label>
</div>
<button class="btn btn-error btn-sm" @click="resetDataDialogRef.showDialog()">
{{ t('button.resetData') }}
</button>
<button class="btn btn-accent btn-sm" @click="exportData">
{{ t('button.exportResult') }}
</button>
<div>
<span>{{ t('table.luckyPeopleNumber') }}:</span>
<span>{{ alreadyPersonList.length }}</span>
<span>&nbsp;/&nbsp;</span>
<span>{{ allPersonList.length }}</span>
</div>
</div>
</template>
</PageHeader>
<DaiysuiTable :table-columns="tableColumns" :data="allPersonList" />
</div>
</template>

View File

@@ -1,12 +1,13 @@
<!-- eslint-disable vue/no-parsing-error -->
<script setup lang='ts'>
import type { IPersonConfig } from '@/types/storeType'
import DaiysuiTable from '@/components/DaiysuiTable/index.vue'
import i18n from '@/locales/i18n'
import useStore from '@/store'
import { storeToRefs } from 'pinia'
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import DaiysuiTable from '@/components/DaiysuiTable/index.vue'
import PageHeader from '@/components/PageHeader/index.vue'
import i18n from '@/locales/i18n'
import useStore from '@/store'
const { t } = useI18n()
const personConfig = useStore().personConfig
@@ -39,8 +40,8 @@ const tableColumnsList = [
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;"/>` : '-';
}
return row.avatar ? `<img src="${row.avatar}" alt="avatar" style="width: 50px; height: 50px;"/>` : '-'
},
},
{
label: i18n.global.t('data.department'),
@@ -82,8 +83,8 @@ const tableColumnsDetail = [
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;"/>` : '-';
}
return row.avatar ? `<img src="${row.avatar}" alt="avatar" style="width: 50px; height: 50px;"/>` : '-'
},
},
{
label: i18n.global.t('data.department'),
@@ -121,21 +122,25 @@ const tableColumnsDetail = [
<template>
<div class="overflow-y-auto">
<h2>{{ t('viewTitle.winnerManagement') }}</h2>
<div class="flex items-center justify-start gap-10">
<div>
<span>{{ t('table.luckyPeopleNumber') }}</span>
<span>{{ alreadyPersonList.length }}</span>
</div>
<div class="flex flex-col">
<div class="form-control">
<label class="cursor-pointer label">
<span class="label-text">{{ t('table.detail') }}:</span>
<input v-model="isDetail" type="checkbox" class="border-solid toggle toggle-primary border-1">
</label>
<PageHeader :title="t('viewTitle.winnerManagement')">
<template #buttons>
<div class="flex items-center justify-start gap-10">
<div>
<span>{{ t('table.luckyPeopleNumber') }}</span>
<span>{{ alreadyPersonList.length }}</span>
</div>
<div class="flex flex-col">
<div class="form-control">
<label class="cursor-pointer label">
<span class="label-text">{{ t('table.detail') }}:</span>
<input v-model="isDetail" type="checkbox" class="border-solid toggle toggle-primary border-1">
</label>
</div>
</div>
</div>
</div>
</div>
</template>
</PageHeader>
<DaiysuiTable v-if="!isDetail" :table-columns="tableColumnsList" :data="alreadyPersonList" />
<DaiysuiTable v-if="isDetail" :table-columns="tableColumnsDetail" :data="alreadyPersonDetail" />

View File

@@ -1,12 +1,13 @@
<script setup lang='ts'>
import type { IPrizeConfig } from '@/types/storeType'
import EditSeparateDialog from '@/components/NumberSeparate/EditSeparateDialog.vue'
import i18n from '@/locales/i18n'
import useStore from '@/store'
import localforage from 'localforage'
import { storeToRefs } from 'pinia'
import { onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import EditSeparateDialog from '@/components/NumberSeparate/EditSeparateDialog.vue'
import PageHeader from '@/components/PageHeader/index.vue'
import i18n from '@/locales/i18n'
import useStore from '@/store'
const { t } = useI18n()
const imageDbStore = localforage.createInstance({
@@ -149,27 +150,33 @@ watch(() => prizeList.value, (val: IPrizeConfig[]) => {
<template>
<div>
<h2>{{ t('viewTitle.prizeManagement') }}</h2>
<div class="flex w-full gap-3">
<button class="btn btn-info btn-sm" @click="addPrize">
{{ t('button.add') }}
</button>
<button class="btn btn-info btn-sm" @click="resetDefault">
{{ t('button.resetDefault') }}
</button>
<button class="btn btn-error btn-sm" @click="delAll">
{{ t('button.allDelete') }}
</button>
</div>
<div role="alert" class="w-full my-4 alert alert-info">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="w-6 h-6 stroke-current shrink-0">
<path
stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>{{ t('dialog.tipResetPrize') }}</span>
</div>
<PageHeader :title="t('viewTitle.prizeManagement')">
<template #buttons>
<div class="flex w-full gap-3">
<button class="btn btn-info btn-sm" @click="addPrize">
{{ t('button.add') }}
</button>
<button class="btn btn-info btn-sm" @click="resetDefault">
{{ t('button.resetDefault') }}
</button>
<button class="btn btn-error btn-sm" @click="delAll">
{{ t('button.allDelete') }}
</button>
</div>
</template>
<template #alerts>
<div role="alert" class="w-full my-4 alert alert-info">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="w-6 h-6 stroke-current shrink-0">
<path
stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>{{ t('dialog.tipResetPrize') }}</span>
</div>
</template>
</PageHeader>
<ul class="p-0 m-0">
<li
v-for="item in prizeList" :key="item.id" class="flex gap-10"