feat(admin): 优化用户界面组件并新增自定义按钮样式

- 新增按钮背景图片资源(btn-bg.png、progress-title.png、title-bg3.png、title-bg4.png)
- 扩展 ActionButton 组件支持自定义按钮类型,新增 custom 类型用于背景图片驱动的按钮
- 添加 btnBg 和 fontSize 属性到 ActionButton,支持动态背景和字体大小配置
- 重构 ActionButton 样式结构,分离 standard-btn 和 custom-btn 两种按钮样式
- 优化按钮禁用状态的样式处理和交互反馈
- 更新 CompetitionLayout 组件支持新的标题背景类型(titleBg3、titleBg4)
- 新增 titleTextColor 和 fontSize 属性到 CompetitionLayout,增强标题和按钮的定制化能力
- 更新所有用户页面组件(cover、draw、game、groups、rules、teams)以适配新的按钮和布局配置
This commit is contained in:
2026-03-17 17:51:12 +08:00
parent 8964fcdea7
commit 02b4720b34
12 changed files with 109 additions and 49 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

View File

@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
// import { defineProps, defineEmits } from 'vue'; import { computed } from 'vue'
import defaultBtnBg from '@/assets/imgs/user/btn-bg.png'
const props = defineProps({ const props = defineProps({
text: { text: {
@ -15,13 +16,25 @@ const props = defineProps({
default: 'primary', default: 'primary',
}, },
type: { type: {
type: String as () => 'button' | 'text', type: String as () => 'button' | 'text' | 'custom',
default: 'button', default: 'button',
}, },
btnBg: {
type: String,
default: defaultBtnBg,
},
fontSize: {
type: Number,
default: 30,
},
}) })
const emits = defineEmits(['click']) const emits = defineEmits(['click'])
const isCustom = computed(() => props.type === 'custom')
const customBg = computed(() => `url(${props.btnBg}) no-repeat center / 100% 100%`)
function handleClick() { function handleClick() {
if (!props.disabled) { if (!props.disabled) {
emits('click') emits('click')
@ -32,33 +45,52 @@ function handleClick() {
<template> <template>
<button <button
class="action-btn" class="action-btn"
:class="[theme, { 'is-disabled': disabled, 'text-btn': type === 'text' }]" :class="[
isCustom ? 'custom-btn' : 'standard-btn',
theme,
type,
{ 'is-disabled': disabled },
]"
@click="handleClick" @click="handleClick"
> >
<span v-if="type === 'text'" class="btn-text">{{ text }}</span> <span v-if="type === 'text' || type === 'custom'" class="btn-text">{{ text }}</span>
<slot v-else /> <slot v-else />
</button> </button>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.action-btn { .action-btn {
width: 50px;
height: 50px;
border-radius: 50%;
border: none;
color: white;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
border: none;
outline: none;
font-family: inherit;
&.is-disabled {
cursor: not-allowed;
filter: grayscale(1);
opacity: 0.7;
&:hover,
&:active {
transform: none !important;
box-shadow: inherit !important;
}
}
}
// standard: type="button" | type="text"
.standard-btn {
width: 50px;
height: 50px;
border-radius: 50%;
color: white;
background: #eb5e55; background: #eb5e55;
box-shadow: 0 8px 0 #fcc64f; box-shadow: 0 8px 0 #fcc64f;
.arrow-icon {
font-size: 32px;
}
&:hover { &:hover {
transform: translateY(-2px); transform: translateY(-2px);
filter: brightness(1.05); filter: brightness(1.05);
@ -70,7 +102,17 @@ function handleClick() {
box-shadow: 0 2px 0 #fcc64f; box-shadow: 0 2px 0 #fcc64f;
} }
&.text-btn { &.light {
background: white;
color: #eb5e55;
border: 2px solid #eb5e55;
&:hover {
background-color: #fff;
}
}
&.text {
width: auto; width: auto;
min-width: 180px; min-width: 180px;
padding: 0 30px; padding: 0 30px;
@ -82,35 +124,30 @@ function handleClick() {
letter-spacing: 2px; letter-spacing: 2px;
} }
} }
&.is-disabled {
cursor: not-allowed;
&:hover {
transform: none;
}
&:active {
transform: none;
}
}
} }
.light { // custom: type="custom",背景完全由 btnBg 决定
background: white; .custom-btn {
color: #eb5e55; background: v-bind(customBg);
border: 2px solid #eb5e55; background-color: transparent;
box-shadow: 0 8px 0 #fcc64f; width: 220px;
height: 60px;
box-shadow: none;
color: rgb(0, 94, 174);
.btn-text {
font-size: v-bind('`${fontSize}px`');
font-weight: bold;
letter-spacing: 2px;
margin-bottom: 7px;
}
&:hover { &:hover {
background-color: #fff; transform: scale(1.05);
transform: translateY(-3px);
box-shadow: 0 10px 0 #fcc64f;
} }
&:active { &:active {
transform: translateY(4px); transform: scale(0.95);
box-shadow: 0 2px 0 #fcc64f;
} }
} }
</style> </style>

View File

@ -4,6 +4,8 @@ import VScaleScreen from 'v-scale-screen'
import { computed, onMounted, ref, watch } from 'vue' import { computed, onMounted, ref, watch } from 'vue'
import _defaultBg from '@/assets/imgs/user/home-bg.webp' import _defaultBg from '@/assets/imgs/user/home-bg.webp'
import titleBg2 from '@/assets/imgs/user/title-bg2.webp' import titleBg2 from '@/assets/imgs/user/title-bg2.webp'
import titleBg3 from '@/assets/imgs/user/title-bg3.png'
import titleBg4 from '@/assets/imgs/user/title-bg4.png'
import _defaultTitleBg from '@/assets/imgs/user/title-bg.webp' import _defaultTitleBg from '@/assets/imgs/user/title-bg.webp'
import { useRouterPush } from '@/hooks/common/router' import { useRouterPush } from '@/hooks/common/router'
@ -36,6 +38,10 @@ interface Props {
showNextBtn2?: boolean showNextBtn2?: boolean
/** 第二个按钮的文本 */ /** 第二个按钮的文本 */
nextBtnText2?: string nextBtnText2?: string
/** title 颜色 */
titleTextColor?: string
/* 按钮字体大小 */
fontSize?: number
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
@ -52,6 +58,8 @@ const props = withDefaults(defineProps<Props>(), {
titleBgType: 1, titleBgType: 1,
showNextBtn2: false, showNextBtn2: false,
nextBtnText2: undefined, nextBtnText2: undefined,
titleTextColor: '#000000',
fontSize: 30,
}) })
const emit = defineEmits<{ const emit = defineEmits<{
@ -63,6 +71,8 @@ const emit = defineEmits<{
const titleBgUrlMapping: Record<number, string> = { const titleBgUrlMapping: Record<number, string> = {
1: _defaultTitleBg, 1: _defaultTitleBg,
2: titleBg2, 2: titleBg2,
3: titleBg3,
4: titleBg4,
} }
const { routerPushByKey } = useRouterPush() const { routerPushByKey } = useRouterPush()
@ -242,7 +252,7 @@ watch(titleBgUrl, (newUrl) => {
@error="imageLoadStatus[url] = false" @error="imageLoadStatus[url] = false"
> >
<Transition name="fade-in-delay" appear> <Transition name="fade-in-delay" appear>
<h1 v-if="showContent" class="main-title"> <h1 v-if="showContent" class="main-title" :style="{ color: titleTextColor }">
{{ title }} {{ title }}
</h1> </h1>
</Transition> </Transition>
@ -267,7 +277,8 @@ watch(titleBgUrl, (newUrl) => {
<div class="left"> <div class="left">
<ActionButton <ActionButton
class="back-btn" :style="{ visibility: showBack ? 'visible' : 'hidden' }" class="back-btn" :style="{ visibility: showBack ? 'visible' : 'hidden' }"
:type="backBtnText ? 'text' : 'button'" :text="backBtnText || ''" :theme="btnTheme" @click="handleBack" type="custom" :text="backBtnText || ''" :theme="btnTheme" :font-size="fontSize"
@click="handleBack"
> >
<SvgIcon v-if="!backBtnText" icon="mdi:arrow-left" size="40" class="arrow-icon" /> <SvgIcon v-if="!backBtnText" icon="mdi:arrow-left" size="40" class="arrow-icon" />
</ActionButton> </ActionButton>
@ -275,8 +286,9 @@ watch(titleBgUrl, (newUrl) => {
<div class="right"> <div class="right">
<ActionButton <ActionButton
v-if="showNext" :type="nextBtnText ? 'text' : 'button'" :text="nextBtnText || '下一步'" v-if="showNext" type="custom" :text="nextBtnText || '下一步'"
:theme="btnTheme" :disabled="nextDisabled" @click="handleNext" :theme="btnTheme" :disabled="nextDisabled" :font-size="fontSize"
@click="handleNext"
> >
<SvgIcon v-if="!nextBtnText" icon="mdi:arrow-right" class="arrow-icon" size="40" /> <SvgIcon v-if="!nextBtnText" icon="mdi:arrow-right" class="arrow-icon" size="40" />
</ActionButton> </ActionButton>

View File

@ -196,7 +196,8 @@ onMounted(async () => {
<template> <template>
<CompetitionLayout <CompetitionLayout
:show-back="true" :show-next="showNextButton" next-btn-text="下一步" :title="activityName" :show-back="true" :show-next="showNextButton" next-btn-text="下一步" :title="activityName"
:title-bg-type="2" @back="handleBack" @next="handleNext" :title-bg-type="2" back-btn-text="返回" @back="handleBack"
@next="handleNext"
> >
<div class="h-screen w-full overflow-y-auto font-sans"> <div class="h-screen w-full overflow-y-auto font-sans">
<div class="relative z-10 min-h-full w-full"> <div class="relative z-10 min-h-full w-full">

View File

@ -132,11 +132,14 @@ function handleBack() {
const questionMainTitle = computed(() => question.value?.QuestionList?.Name || '') const questionMainTitle = computed(() => question.value?.QuestionList?.Name || '')
const questionTitle = computed(() => question.value?.QuestionList?.QuestionContent || question.value?.QuestionList?.Name || '') const questionTitle = computed(() => question.value?.QuestionList?.QuestionContent || question.value?.QuestionList?.Name || '')
const titleBgTypeNum = computed(() => RoundType.value === 0 ? 3 : 4)
</script> </script>
<template> <template>
<CompetitionLayout <CompetitionLayout
:show-back="true" :show-next="true" :title="questionMainTitle || '抽题'" next-btn-text="开始抽题" :show-back="true" :show-next="true" back-btn-text="返回" :title="questionMainTitle || '抽题'" next-btn-text="开始抽题"
:title-bg-type="titleBgTypeNum || 3" title-text-color="#ffffff"
@next="handleCardClick(question as Api.Competition.CurrentQuestionResponse)" @back="handleBack" @next="handleCardClick(question as Api.Competition.CurrentQuestionResponse)" @back="handleBack"
> >
<div class="h-full w-full flex flex-col items-center pt-10 font-sans"> <div class="h-full w-full flex flex-col items-center pt-10 font-sans">
@ -169,7 +172,10 @@ const questionTitle = computed(() => question.value?.QuestionList?.QuestionConte
}}</span> }}</span>
</div> --> </div> -->
<!-- 禁用放大 --> <!-- 禁用放大 -->
<NImage :src="drawImg" class="h-full w-full object-cover" width="300" height="300" :preview-disabled="true" /> <NImage
:src="drawImg" class="h-full w-full object-cover" width="300" height="300"
:preview-disabled="true"
/>
</div> </div>
</div> </div>
</div> </div>

View File

@ -20,6 +20,7 @@ const { activityId, groupId, activityInfo, teamId, teamIds } = storeToRefs(activ
const currentQuestionInfo = computed(() => store.currentQuestionInfo) const currentQuestionInfo = computed(() => store.currentQuestionInfo)
const currentQuestionDetail = computed(() => store.currentQuestionDetail) const currentQuestionDetail = computed(() => store.currentQuestionDetail)
const currentQuestionMainInfo = computed(() => store.currentQuestionMainInfo) const currentQuestionMainInfo = computed(() => store.currentQuestionMainInfo)
const RoundType = computed(() => store.currentQuestionMainInfo?.Activity_Question?.RoundType)
const loading = ref(false) const loading = ref(false)
@ -246,13 +247,16 @@ async function fetchGetGameStatisticsData(_params: Api.Competition.QuestionAddPa
} }
} }
} }
const titleBgTypeNum = computed(() => RoundType.value === 0 ? 3 : 4) || 3
</script> </script>
<template> <template>
<CompetitionLayout <CompetitionLayout
action-position="top" :show-back="false" :show-next="true" :title="mainTitle || ''" action-position="top" :show-back="false" :show-next="true" :title="mainTitle || ''"
back-btn-text="返回" :next-btn-text="isStarted ? formattedTime : '开始答题'" :next-disabled="isStarted" btn-theme="light" :title-bg-type="titleBgTypeNum" title-text-color="#ffffff" back-btn-text="返回"
@back="handleBack" @next="handleNext" :next-btn-text="isStarted ? formattedTime : '开始答题'" :next-disabled="isStarted" btn-theme="light" @back="handleBack"
@next="handleNext"
> >
<div class="relative h-full w-full flex flex-col items-center overflow-hidden px-12 font-sans"> <div class="relative h-full w-full flex flex-col items-center overflow-hidden px-12 font-sans">
<!-- 题目说明 --> <!-- 题目说明 -->

View File

@ -83,7 +83,7 @@ onMounted(() => {
<template> <template>
<CompetitionLayout <CompetitionLayout
:show-back="true" :show-next="isAllEnd" next-btn-text="查看常规赛结果" next-btn-text2="查看加时赛结果" :show-back="true" back-btn-text="返回" :show-next="isAllEnd" next-btn-text="查看常规赛结果" font-size="22" next-btn-text2="查看加时赛结果"
:show-next-btn2="hasExtraTime && isAllEnd" :title-bg-type="2" title="展示组别" @back="handleBack" @next="handleNext" :show-next-btn2="hasExtraTime && isAllEnd" :title-bg-type="2" title="展示组别" @back="handleBack" @next="handleNext"
@next2="handleNext2" @next2="handleNext2"
> >

View File

@ -49,8 +49,8 @@ function handleNext() {
<template> <template>
<CompetitionLayout <CompetitionLayout
:show-back="true" :show-next="true" :title="activityName" next-btn-text="下一步" @back="handleBack" :show-back="true" back-btn-text="返回" :show-next="true" :title="activityName" next-btn-text="下一步"
@next="handleNext" @back="handleBack" @next="handleNext"
> >
<div class="items-flex-start h-full w-full flex justify-center font-sans"> <div class="items-flex-start h-full w-full flex justify-center font-sans">
<div class="rules-content-wrapper"> <div class="rules-content-wrapper">

View File

@ -87,7 +87,7 @@ onMounted(() => {
</script> </script>
<template> <template>
<CompetitionLayout :show-back="true" :show-next="true" next-btn-text="下一步" :show-title="false" @next="handleNext" @back="handleBack"> <CompetitionLayout :show-back="true" back-btn-text="返回" :show-next="true" next-btn-text="下一步" :show-title="false" @next="handleNext" @back="handleBack">
<!-- Scroll Content --> <!-- Scroll Content -->
<div class="scroll-container"> <div class="scroll-container">
<!-- 卷轴背景动画容器 --> <!-- 卷轴背景动画容器 -->