并可以自动适配,如果 footer没有则上面的东西会铺满

<div class="panel">
<div class="container">
<!-- 容器内容 -->
</div>
<div class="footer">
<!-- 页脚内容 -->
</div>
</div>
.panel {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.container {
flex-grow: 1; /* 占满剩余空间 */
overflow-y: auto; /* 当内容溢出时滚动 */
}
.footer {
flex-shrink: 0; /* 不允许压缩,始终保持最小高度 */
min-height: 40px; /* 固定最小高度 */
}
源代码
import React, { useState } from 'react'
import NextImage from 'next/image'
import NoData from '@futures-simulation-trade/components/Nodata'
import AnswerIcon from '@futures-simulation-trade/assets/images/answer.svg'
import { useTranslation } from 'next-i18next'
import { observer } from 'mobx-react-lite'
import { Tooltip } from '@mantine/core'
import { imgLoader } from 'utils/index'
import { handleGoKYC, handleGotoCouponCenter } from '@/utils/goOtherPage'
import { RedEnvelopeMyPrizeInfo } from '@futures-simulation-trade/types/redPackets'
import { usePrizeMap } from '@futures-simulation-trade/hooks/usePrizeMap'
import { ActiveStatus, StackPopupTypeEnum } from '@futures-simulation-trade/types'
import { useStore } from '@futures-simulation-trade/store'
import { ActivityErrorCodeMap } from '@futures-simulation-trade/utils/errorCode'
import { requestExtract, requestGetRedEnvelopeMyPrize } from '@futures-simulation-trade/apis/redEnvelope'
import { Pagination } from '@gate/gui'
import Button from '@futures-simulation-trade/components/Button'
import useSWR from 'swr'
import styles from './index.module.css'
import { Loading } from '@gate/gui-business'
import { logger } from '@gateio/core-lib'
// 合约体验券id
const contractExperienceId = 'G10377'
const PER_PAGE_NUMBER = 20
const ONE_PAGE = 1
enum ContractExperienceType {
CLAIM_NOW = 0,
COLLECTING = 1,
USE_NOW = 2,
}
function RedEnvelopeWiningRecord() {
const { t } = useTranslation('simulation')
const { prizeMap } = usePrizeMap()
const { addStackPopup, getBuryingPointProps, activeStatus, physicalPrizes, closeTopStackPopup } = useStore()
const [pagination, setPagination] = useState({ page: 1, pageSize: PER_PAGE_NUMBER, totalPage: 0 })
const [loading, setLoading] = useState(false)
const [isShowFooter, setIsShowFooter] = useState(false)
const { data, mutate, isLoading } = useSWR(`GET_RED_PACKETS_MY_RECORD-${pagination.page}`, () =>
requestGetRedEnvelopeMyPrize({ page: pagination.page, pageSize: pagination.pageSize }).then(res => {
if (res?.code === ActivityErrorCodeMap.SUCCESS) {
setPagination({
page: res?.data?.prize?.current_page || 0,
pageSize: res?.data?.prize?.per_page || PER_PAGE_NUMBER,
totalPage: res?.data?.prize?.last_page || 0,
})
return res?.data
}
throw res?.message
})
)
const prizes: RedEnvelopeMyPrizeInfo[] = data?.prize?.data || []
const ActivityErrorMap = {
//400 不存在提取 406未完成kyc 402同一实名 403风控 404未完成7天连续入金 405领取合约体验券已过期
[ActivityErrorCodeMap.TWENTY_ONE_DAYS_DEPOSIT_UNFINISHED]: t('simulation.Game.FuturesVoucherRule', { unit: '$' }),
[ActivityErrorCodeMap.EXTRACT_NOT_EXIST]: t('simulation.Game.FuturesVoucherOnce'),
[ActivityErrorCodeMap.RISK_CONTROL]: t('simulation.Game.AccountError'),
[ActivityErrorCodeMap.SAME_REAL_NAME]: t('simulation.Game.SameAccountClaimOnce'),
[ActivityErrorCodeMap.KYC_UNFINISHED]: t('simulation.Game.NeedKYCWarning'),
[ActivityErrorCodeMap.CONTRACT_EXPERIENCE_COUPON_EXPIRED]: t('simulation.Game.Expired'),
}
const buttonMap = {
[ContractExperienceType.CLAIM_NOW]: t('simulation.Game.ClaimNow'),
[ContractExperienceType.COLLECTING]: t('simulation.Game.Collecting'),
[ContractExperienceType.USE_NOW]: t('simulation.Game.UseNow'),
}
const burialPointMap = {
[ContractExperienceType.CLAIM_NOW]: 'coupon_extract',
[ContractExperienceType.COLLECTING]: '',
[ContractExperienceType.USE_NOW]: 'coupon_view',
}
/* 领取合约体验券 */
const receiveFuturesVoucher = async () => {
setLoading(true)
try {
const res = await requestExtract()
if (res?.code === ActivityErrorCodeMap.SUCCESS) {
mutate()
return
}
if (res?.code === ActivityErrorCodeMap.KYC_UNFINISHED) {
addStackPopup({
type: StackPopupTypeEnum.TIP,
content: t('simulation.Game.NeedKYCWarning'),
data: {
confirmText: t('simulation.Game.GoKYC'),
onClick: handleGoKYC,
},
})
return
}
addStackPopup({
type: StackPopupTypeEnum.TIP,
content: ActivityErrorMap[res?.code] || res?.message || '',
})
} catch (error) {
logger.error('Simulation RedEnvelopeWiningRecord ReceiveFuturesVoucher Error', error)
} finally {
setLoading(false)
}
}
const handleFuturesVoucher = (item: RedEnvelopeMyPrizeInfo) => {
if (item.get_status === ContractExperienceType.COLLECTING) {
return
} else if (item.get_status === ContractExperienceType.CLAIM_NOW) {
receiveFuturesVoucher()
} else if (item.get_status === ContractExperienceType.USE_NOW) {
handleGotoCouponCenter()
}
}
const changePage = (page: number) => {
setPagination(pre => ({ ...pre, page }))
}
/* 体验金提示 */
const renderExperienceTips = (item: RedEnvelopeMyPrizeInfo) => {
if (item.gift_id !== contractExperienceId) return
return (
<Tooltip
label={t('simulation.Game.FuturesVoucher', { unit: '$' })}
w={200}
multiline
withArrow
arrowSize={4}
position="bottom"
events={{ hover: true, focus: true, touch: true }}
>
<NextImage loader={imgLoader} className={styles.icon} src={AnswerIcon} alt="answer icon" />
</Tooltip>
)
}
const receivePrizeInKind = () => {
// TODO 调用接口
// 已过期提示
// addStackPopup({
// type: StackPopupTypeEnum.TIP,
// title: 'Sorry',
// content: 'The event has ended for more than 15 days and the physical reward has expired',
// })
closeTopStackPopup()
addStackPopup({
type: StackPopupTypeEnum.RED_ENVELOPE_RECEIVE_REWARD,
})
}
const renderMainContent = () => {
if (isLoading) {
return (
<div className={styles.loadingContainer}>
<Loading />
</div>
)
}
if (!prizes?.length)
return (
<div className={styles.noDataContainer}>
<NoData className={styles.noData} />
</div>
)
return (
<>
<ul className={styles.wrap}>
{prizes?.map((item, index) => (
<li key={index} className={styles.wining}>
<div className={styles.winingWrap}>
<div className={styles.winingLeft}>
<NextImage
className={styles.prizeImage}
loader={imgLoader}
width={32}
height={32}
src={prizeMap[item.gift_id]?.img}
alt="ActionWinningRecord"
/>
<div className={styles.details}>
<div className={styles.prizeInfo}>
<span className={styles.name}>{prizeMap[item.gift_id]?.name}</span>
<span className={styles.icon}>{renderExperienceTips(item)}</span>
</div>
<div className={styles.time}>{item.inst_time}</div>
</div>
</div>
<div className={styles.winingRight}>
{item.gift_id === contractExperienceId && (
<Button
shape="normal"
isLoading={loading}
className={styles.claimButton}
onClick={() => handleFuturesVoucher(item)}
{...(burialPointMap[item.get_status] && getBuryingPointProps(burialPointMap[item.get_status]))}
>
<span className={styles.actionButtonText}>{buttonMap[item.get_status]}</span>
</Button>
)}
</div>
</div>
</li>
))}
</ul>
{pagination?.totalPage > ONE_PAGE && (
<Pagination
className={styles.pagination}
total={pagination?.totalPage || 0}
page={pagination?.page || 1}
size={pagination?.pageSize || PER_PAGE_NUMBER}
onChange={changePage}
/>
)}
</>
)
}
const renderFooter = () => {
// if (activeStatus !== ActiveStatus.End || !physicalPrizes.length) return
if (!physicalPrizes.length) return
return (
<div className={styles.footer}>
<Button className={styles.confirmButton} shape="secondary" onClick={receivePrizeInKind}>
领取实物奖励
</Button>
<div className={styles.confirmTips}>
You can claim the prize within 15 days after the game, and the physical reward will expire if it is not
claimed after 15 days
</div>
</div>
)
}
return (
<div className={styles.panelContainer}>
<div className={styles.container}>{renderMainContent()}</div>
{renderFooter()}
</div>
)
}
export default observer(RedEnvelopeWiningRecord)
.panelContainer {
width: 420px;
min-height: 500px;
}
.container {
width: 420px;
height: 500px;
overflow-y: auto;
margin-right: -12px;
padding-right: 12px;
}
.footer {
padding-top: 32px;
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
}
.confirmButton {
width: 100%;
}
.confirmTips {
padding-top: 12px;
font-size: 12px;
font-weight: 400;
line-height: 14.4px;
text-align: center;
color: #bfbfbf;
}
.wrap {
box-sizing: border-box;
min-height: 400px;
overflow-y: auto;
width: 100%;
}
.tip {
font-size: 12px;
font-weight: 400;
line-height: 18px;
text-align: center;
color: #cef4f0;
> span {
font-weight: 500;
margin-inline-start: 8px;
color: #f0fb4e;
cursor: pointer;
}
}
.wining {
margin-top: 24px;
font-size: 16px;
font-weight: 400;
line-height: 16px;
color: #fff;
width: 100%;
}
.winingWrap {
display: flex;
align-items: center;
justify-content: space-between;
}
.wining:first-child {
margin-top: 0;
}
.winingLeft {
display: flex;
align-items: center;
margin-inline-end: 24px;
/* width: 270px; */
flex: 1;
.prizeImage {
width: 32px;
height: 32px;
margin-inline-end: 8px;
}
.details {
display: flex;
flex-direction: column;
max-width: 252px;
.prizeInfo {
font-size: 14px;
line-height: 16px;
color: #fafafa;
margin-bottom: 4px;
.name {
max-width: 232px;
overflow: hidden;
text-overflow: ellipsis;
}
.icon {
display: inline-flex;
align-items: center;
margin-inline-start: 4px;
width: 16px;
height: 16px;
cursor: pointer;
vertical-align: text-bottom;
}
}
.time {
font-size: 12px;
color: #8c8c8c;
line-height: 14.4px;
}
}
}
.winingRight {
display: flex;
align-items: center;
overflow: hidden;
}
.claimButton {
white-space: nowrap;
}
.actionButtonText {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
.loadingContainer {
width: 100%;
height: 400px;
overflow: hidden;
display: flex;
align-items: center;
}
.noDataContainer {
width: 100%;
height: 400px;
display: flex;
align-items: center;
}
.noData {
min-height: unset;
}
.pagination {
display: flex;
justify-content: center;
margin: 32px 0 0 0;
}
@media screen and (max-width: 768px) {
.panelContainer {
width: 100%;
min-height: auto;
max-height: 64vh;
display: flex;
flex-direction: column;
}
.container {
width: 100%;
flex-grow: 1;
overflow-y: auto;
}
.wrap {
min-height: auto;
}
.actionButton {
margin-top: 8px;
margin-inline-start: 0;
}
.wining {
margin-top: 18px;
font-size: 14px;
line-height: 14px;
}
.pagination {
margin: 32px 0 20px 0;
}
.footer {
width: 100%;
min-height: 40px;
margin-bottom: 16px;
flex-shrink: 0;
}
}