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:
BIN
apps/admin/src/assets/imgs/user/btn-bg.png
Normal file
BIN
apps/admin/src/assets/imgs/user/btn-bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
apps/admin/src/assets/imgs/user/progress-title.png
Normal file
BIN
apps/admin/src/assets/imgs/user/progress-title.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 200 KiB |
BIN
apps/admin/src/assets/imgs/user/title-bg3.png
Normal file
BIN
apps/admin/src/assets/imgs/user/title-bg3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 153 KiB |
BIN
apps/admin/src/assets/imgs/user/title-bg4.png
Normal file
BIN
apps/admin/src/assets/imgs/user/title-bg4.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 306 KiB |
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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">
|
||||||
<!-- 题目说明 -->
|
<!-- 题目说明 -->
|
||||||
|
|||||||
@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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">
|
||||||
<!-- 卷轴背景动画容器 -->
|
<!-- 卷轴背景动画容器 -->
|
||||||
|
|||||||
Reference in New Issue
Block a user