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

View File

@@ -1,12 +1,12 @@
<script setup lang='ts'> <script setup lang='ts'>
import type { IMusic } from '@/types/storeType' import type { IMusic } from '@/types/storeType'
import useStore from '@/store'
import { readFileData } from '@/utils/file'
import localforage from 'localforage' import localforage from 'localforage'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n' 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 { t } = useI18n()
const audioUploadToast = ref(0) // 0是不显示1是成功2是失败,3是不是图片 const audioUploadToast = ref(0) // 0是不显示1是成功2是失败,3是不是图片
@@ -74,21 +74,26 @@ onMounted(() => {
<template> <template>
<div> <div>
<div class="flex gap-3"> <PageHeader title="音乐管理">
<button class="btn btn-primary btn-sm" @click="resetMusic"> <template #buttons>
{{ t('button.reset') }} <div class="flex gap-3">
</button> <button class="btn btn-primary btn-sm" @click="resetMusic">
<label for="explore"> {{ t('button.reset') }}
<input </button>
id="explore" type="file" class="" style="display: none" :accept="limitType" <label for="explore">
@change="handleFileChange" <input
> id="explore" type="file" class="" style="display: none" :accept="limitType"
<span class="btn btn-primary btn-sm">{{ t('button.upload') }}</span> @change="handleFileChange"
</label> >
<button class="btn btn-error btn-sm" @click="deleteAll"> <span class="btn btn-primary btn-sm">{{ t('button.upload') }}</span>
{{ t('button.allDelete') }} </label>
</button> <button class="btn btn-error btn-sm" @click="deleteAll">
</div> {{ t('button.allDelete') }}
</button>
</div>
</template>
</PageHeader>
<div> <div>
<ul class="p-0"> <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"> <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 { useI18n } from 'vue-i18n'
import DaiysuiTable from '@/components/DaiysuiTable/index.vue' import DaiysuiTable from '@/components/DaiysuiTable/index.vue'
import CustomDialog from '@/components/Dialog/index.vue' import CustomDialog from '@/components/Dialog/index.vue'
import PageHeader from '@/components/PageHeader/index.vue'
import { useViewModel } from './useViewModel' import { useViewModel } from './useViewModel'
const resetDataDialogRef = ref() const resetDataDialogRef = ref()
@@ -29,43 +30,46 @@ const limitType = '.xlsx,.xls'
/> />
<div class="min-w-1000px"> <div class="min-w-1000px">
<h2>{{ t('viewTitle.personManagement') }}</h2> <PageHeader :title="t('viewTitle.personManagement')">
<div class="flex gap-3"> <template #buttons>
<button class="btn btn-error btn-sm" @click="delAllDataDialogRef.showDialog()"> <div class="flex gap-3">
{{ t('button.allDelete') }} <button class="btn btn-error btn-sm" @click="delAllDataDialogRef.showDialog()">
</button> {{ t('button.allDelete') }}
<div class="tooltip tooltip-bottom" :data-tip="t('tooltip.downloadTemplateTip')"> </button>
<a <div class="tooltip tooltip-bottom" :data-tip="t('tooltip.downloadTemplateTip')">
class="no-underline btn btn-secondary btn-sm" :download="t('data.xlsxName')" target="_blank" <a
:href="`/log-lottery/${t('data.xlsxName')}`" class="no-underline btn btn-secondary btn-sm" :download="t('data.xlsxName')" target="_blank"
>{{ t('button.downloadTemplate') }}</a> :href="`/log-lottery/${t('data.xlsxName')}`"
</div> >{{ t('button.downloadTemplate') }}</a>
<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> </div>
</label> <div class="">
</div> <label for="explore">
<button class="btn btn-error btn-sm" @click="resetDataDialogRef.showDialog()"> <div class="tooltip tooltip-bottom" :data-tip="t('tooltip.uploadExcelTip')">
{{ t('button.resetData') }} <input
</button> id="explore" ref="exportInputFileRef" type="file" class="" style="display: none"
<button class="btn btn-accent btn-sm" @click="exportData"> :accept="limitType" @change="handleFileChange"
{{ t('button.exportResult') }} >
</button>
<div> <span class="btn btn-primary btn-sm">{{ t('button.importData') }}</span>
<span>{{ t('table.luckyPeopleNumber') }}:</span> </div>
<span>{{ alreadyPersonList.length }}</span> </label>
<span>&nbsp;/&nbsp;</span> </div>
<span>{{ allPersonList.length }}</span> <button class="btn btn-error btn-sm" @click="resetDataDialogRef.showDialog()">
</div> {{ t('button.resetData') }}
</div> </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" /> <DaiysuiTable :table-columns="tableColumns" :data="allPersonList" />
</div> </div>
</template> </template>

View File

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

View File

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