| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374 |
- <template>
- <view class="notice-page" :class="{ 'bg-gray': hasNotify }">
- <!-- 默认展示背景图 -->
- <image v-if="!hasNotify" class="bg" src="/static/images/deviceunlogin.png" mode="aspectFit" />
- <!-- Mock:后续接 WebSocket 有消息时展示 -->
- <!-- 动态布局优化 by xu 2025-12-29 -->
- <view v-else class="notify-container">
- <view class="notify-card"
- :class="{ 'has-message': item.hasMessage }"
- v-for="(item, index) in layoutInfo?.positions"
- :key="item.id + '_' + index"
- :style="{
- left: item.left + 'px',
- top: item.top + 'px',
- width: item.width + 'px',
- height: item.height + 'px',
- gap: item.gap + 'px'
- }">
- <image class="avatar" :src="item.avatar" mode="aspectFill"
- :style="{ width: item.avatarSize + 'px', height: item.avatarSize + 'px' }" />
- <view class="name" :style="{ fontSize: item.fontSize + 'px' }">{{ item.name }}</view>
- </view>
- </view>
- <view class="footer">
- <view class="footer-text">
- <view class="triangles">
- <view class="triangle" />
- <view class="triangle" />
- <view class="triangle" />
- </view>
- 请刷卡登录查看留言
- </view>
- </view>
- <!-- 测试按钮 by xu 2025-12-29 -->
- <view class="test-buttons">
- <view class="test-btn" @click="removeUser">-</view>
- <view class="test-count">{{ notifyUsers.length }}</view>
- <view class="test-btn" @click="addUser">+</view>
- <view class="test-btn reset-btn" @click="resetUsers">重置</view>
- </view>
- </view>
- </template>
- <script setup>
- import { computed, ref } from 'vue'
- const notifyUsers = ref([
- {
- id: '1',
- name: '张三',
- avatar: '/static/logo.png',
- hasMessage: true,
- },
- {
- id: '1',
- name: '张三',
- avatar: '/static/logo.png',
- hasMessage: true,
- },
- {
- id: '1',
- name: '张三',
- avatar: '/static/logo.png',
- hasMessage: true,
- },
-
- ])
- const hasNotify = computed(() => notifyUsers.value.length > 0)
- // 屏幕尺寸(px)
- // 原始需求:竖屏 800×1280,屏幕比例 5:8 (宽:高) = 1.6
- const SCREEN_WIDTH = 800
- const SCREEN_HEIGHT = 1280
- const FOOTER_HEIGHT = 50 * (SCREEN_WIDTH / 750) // 50rpx 转 px by xu 2025-12-29
- const HORIZONTAL_PADDING = 10 * (SCREEN_WIDTH / 750) * 2 // 左右各10rpx by xu 2025-12-29
- const VERTICAL_PADDING = 10 * (SCREEN_WIDTH / 750) * 2 // 上下各10rpx by xu 2025-12-29
- const AVAILABLE_WIDTH = SCREEN_WIDTH - HORIZONTAL_PADDING // 减去水平边距
- const AVAILABLE_HEIGHT = (SCREEN_HEIGHT - FOOTER_HEIGHT) - VERTICAL_PADDING // 容器高度减去垂直边距 by xu 2025-12-29
- const SCREEN_RATIO = AVAILABLE_HEIGHT / AVAILABLE_WIDTH // 调整后的屏幕比例
- console.log(`[初始化] 屏幕:${SCREEN_WIDTH}×${SCREEN_HEIGHT}, 可用:${AVAILABLE_WIDTH.toFixed(2)}×${AVAILABLE_HEIGHT.toFixed(2)}, 比例:${SCREEN_RATIO.toFixed(3)}`)
- // 计算最优行列数 by xu 2025-12-29
- // 原则:优先保证x轴贴满,y轴可以有空隙
- function calculateOptimalGrid(n) {
- if (n === 1) return { rows: 1, cols: 1 }
- const CARD_RATIO = 49 / 40 // 卡片宽高比
- let bestRows = 1, bestCols = n
- let bestScore = Infinity
- // 遍历可能的行数
- for (let rows = 1; rows <= n; rows++) {
- const cols = Math.ceil(n / rows)
- const capacity = rows * cols
- const waste = capacity - n
- const wasteRatio = waste / n
- // 计算这个组合下的卡片尺寸
- const maxWidth = AVAILABLE_WIDTH / cols
- const maxHeight = AVAILABLE_HEIGHT / rows / CARD_RATIO
- const isWidthLimited = maxWidth < maxHeight // x轴是否贴满
- const gridRatio = rows / cols
- const ratioDiff = Math.abs(gridRatio - SCREEN_RATIO)
- // 综合评分:
- // 1. 优先选择x轴贴满的组合(isWidthLimited=true)
- // 2. 其次考虑浪费比例
- // 3. 最后考虑屏幕比例匹配
- const widthPenalty = isWidthLimited ? 0 : 2.0 // x轴不贴满时重罚
- const score = widthPenalty + wasteRatio * 0.5 + ratioDiff * 0.3
- console.log(` ${rows}×${cols}: maxW=${maxWidth.toFixed(1)}, maxH=${maxHeight.toFixed(1)}, ${isWidthLimited ? 'x轴贴满' : 'y轴贴满'}, 评分=${score.toFixed(3)}`)
- if (score < bestScore) {
- bestScore = score
- bestRows = rows
- bestCols = cols
- }
- }
- console.log(`[行列选择] ${n}人 -> ${bestRows}×${bestCols} (容量:${bestRows * bestCols}, 浪费:${bestRows * bestCols - n}, 评分:${bestScore.toFixed(3)})`)
- return { rows: bestRows, cols: bestCols }
- }
- // 计算布局信息 by xu 2025-12-29
- const layoutInfo = computed(() => {
- const n = notifyUsers.value.length
- if (n === 0) return null
- const { rows, cols } = calculateOptimalGrid(n)
- console.log(`[布局] 人数:${n}, 行列:${rows}×${cols}`)
- // 卡片宽高比 by xu 2025-12-29
- const CARD_RATIO = 49 / 40 // 宽:高 = 40:49
- // 计算卡片最大尺寸(考虑宽高比)
- const maxWidth = AVAILABLE_WIDTH / cols // 使用可用宽度 by xu 2025-12-29
- const maxHeight = AVAILABLE_HEIGHT / rows / CARD_RATIO // 使用可用高度除以比例 by xu 2025-12-29
- let cardWidth = Math.min(maxWidth, maxHeight)
- console.log(`[尺寸] maxWidth:${maxWidth.toFixed(2)}, maxHeight:${maxHeight.toFixed(2)}, cardWidth:${cardWidth.toFixed(2)}`)
- // 单张卡片时限制最大宽度 by xu 2025-12-29
- if (n === 1) {
- const maxSingleCardWidth = 600 // 单卡最大宽度500px
- cardWidth = Math.min(cardWidth, maxSingleCardWidth)
- }
- const cardHeight = cardWidth * CARD_RATIO
- // 计算整体布局的实际宽高
- const totalWidth = cols * cardWidth
- const totalHeight = rows * cardHeight
- // 计算起始偏移(居中)
- const offsetX = (AVAILABLE_WIDTH - totalWidth) / 2 + HORIZONTAL_PADDING / 2 // 使用可用宽度+左边距 by xu 2025-12-29
- const offsetY = VERTICAL_PADDING / 2 + (AVAILABLE_HEIGHT - totalHeight) / 2 // 上边距+居中偏移 by xu 2025-12-29
- console.log(`[偏移] offsetX:${offsetX.toFixed(2)}, offsetY:${offsetY.toFixed(2)}`)
- console.log(`[总尺寸] totalWidth:${totalWidth.toFixed(2)}, totalHeight:${totalHeight.toFixed(2)}`)
- console.log(`[可用空间] AVAILABLE_WIDTH:${AVAILABLE_WIDTH.toFixed(2)}, AVAILABLE_HEIGHT:${AVAILABLE_HEIGHT.toFixed(2)}`)
- console.log(`[边距] HORIZONTAL_PADDING:${HORIZONTAL_PADDING.toFixed(2)}, VERTICAL_PADDING:${VERTICAL_PADDING.toFixed(2)}`)
- // 动态计算字体大小、间距和头像尺寸 by xu 2025-12-29
- // 基准:4×4时卡片宽度约194px,字体30rpx(32px)
- const baseCardWidth = AVAILABLE_WIDTH / 4 // 4列时的卡片宽度
- const baseFontSize = 30 * (SCREEN_WIDTH / 750) // 30rpx转px
- const fontSize = Math.max(12, cardWidth / baseCardWidth * baseFontSize) // 按比例缩放,最小12px
- const gap = Math.max(6, cardWidth * 0.06) // 间距为卡片宽度的6%,最小6px
- const avatarSize = cardWidth * 0.55 // 头像尺寸为卡片宽度的55%,保持正方形
- // 计算每个用户的位置
- const positions = notifyUsers.value.map((user, index) => {
- const row = Math.floor(index / cols)
- const col = index % cols
- // 计算当前行的元素数量 by xu 2025-12-29
- const itemsInCurrentRow = Math.min(cols, n - row * cols)
- // 如果当前行元素少于列数,计算居中偏移
- const rowCenterOffset = itemsInCurrentRow < cols
- ? (cols - itemsInCurrentRow) * cardWidth / 2
- : 0
- return {
- ...user,
- left: offsetX + col * cardWidth + rowCenterOffset,
- top: offsetY + row * cardHeight,
- width: cardWidth,
- height: cardHeight,
- fontSize,
- gap,
- avatarSize
- }
- })
- return { positions, cardWidth, cardHeight, fontSize, gap, avatarSize }
- })
- // 测试功能:增加用户 by xu 2025-12-29
- function addUser() {
- notifyUsers.value.push({
- id: String(notifyUsers.value.length + 1),
- name: '张三',
- avatar: '/static/logo.png',
- hasMessage: true,
- })
- }
- // 测试功能:减少用户 by xu 2025-12-29
- function removeUser() {
- if (notifyUsers.value.length > 0) {
- notifyUsers.value.pop()
- }
- }
- // 测试功能:重置用户 by xu 2025-12-29
- function resetUsers() {
- notifyUsers.value = [
- {
- id: '1',
- name: '张三',
- avatar: '/static/logo.png',
- hasMessage: true,
- },
- {
- id: '2',
- name: '张三',
- avatar: '/static/logo.png',
- hasMessage: true,
- },
- {
- id: '3',
- name: '张三',
- avatar: '/static/logo.png',
- hasMessage: true,
- },
- ]
- }
- </script>
- <style>
- .notice-page {
- width: 100vw;
- height: 100vh;
- background: #000;
- display: flex;
- align-items: center;
- justify-content: center;
- overflow: hidden;
- padding-bottom: 50rpx;
- box-sizing: border-box;
- }
- .bg-gray {
- background: #b2b2b2;
- }
- .bg {
- width: 100%;
- height: 100%;
- }
- .notify-container {
- width: 100%;
- height: 100%;
- position: relative;
- }
- .notify-card {
- position: absolute;
- box-sizing: border-box;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- border: 1px solid #888;
- }
- .avatar {
- border-radius: 50%;
- border: 2px solid #fff;
- box-sizing: border-box;
- background: #f2f2f2;
- }
- .name {
- color: #000;
- font-weight: 600;
- text-align: center;
- word-break: break-all;
- }
- .footer {
- position: fixed;
- left: 0;
- right: 0;
- bottom: 0;
- width: 100%;
- height: 50rpx;
- background: #b2b2b2;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .footer-text {
- color: #000;
- font-size: 26rpx;
- line-height: 1;
- display: inline-flex;
- align-items: center;
- }
- .triangles {
- display: inline-flex;
- align-items: center;
- gap: 6rpx;
- margin-right: 12rpx;
- }
- .triangle {
- width: 0;
- height: 0;
- border-top: 10rpx solid transparent;
- border-bottom: 10rpx solid transparent;
- border-left: 14rpx solid #000;
- }
- .test-buttons {
- position: fixed;
- right: 20rpx;
- bottom: 100rpx;
- display: flex;
- align-items: center;
- gap: 20rpx;
- background: rgba(0, 0, 0, 0.7);
- padding: 20rpx;
- border-radius: 10rpx;
- z-index: 9999;
- }
- .test-btn {
- width: 60rpx;
- height: 60rpx;
- background: #fff;
- color: #000;
- font-size: 40rpx;
- font-weight: bold;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 50%;
- cursor: pointer;
- }
- .test-count {
- color: #fff;
- font-size: 32rpx;
- font-weight: bold;
- min-width: 40rpx;
- text-align: center;
- }
- .reset-btn {
- width: auto;
- padding: 0 20rpx;
- font-size: 24rpx;
- border-radius: 30rpx;
- }
- </style>
|