notice.vue 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. <template>
  2. <view class="notice-page" :class="{ 'bg-gray': hasNotify }">
  3. <!-- 默认展示背景图 -->
  4. <image v-if="!hasNotify" class="bg" src="/static/images/deviceunlogin.png" mode="aspectFit" />
  5. <!-- Mock:后续接 WebSocket 有消息时展示 -->
  6. <!-- 动态布局优化 by xu 2025-12-29 -->
  7. <view v-else class="notify-container">
  8. <view class="notify-card"
  9. :class="{ 'has-message': item.hasMessage }"
  10. v-for="(item, index) in layoutInfo?.positions"
  11. :key="item.id + '_' + index"
  12. :style="{
  13. left: item.left + 'px',
  14. top: item.top + 'px',
  15. width: item.width + 'px',
  16. height: item.height + 'px',
  17. gap: item.gap + 'px'
  18. }">
  19. <image class="avatar" :src="item.avatar" mode="aspectFill"
  20. :style="{ width: item.avatarSize + 'px', height: item.avatarSize + 'px' }" />
  21. <view class="name" :style="{ fontSize: item.fontSize + 'px' }">{{ item.name }}</view>
  22. </view>
  23. </view>
  24. <view class="footer">
  25. <view class="footer-text">
  26. <view class="triangles">
  27. <view class="triangle" />
  28. <view class="triangle" />
  29. <view class="triangle" />
  30. </view>
  31. 请刷卡登录查看留言
  32. </view>
  33. </view>
  34. <!-- 测试按钮 by xu 2025-12-29 -->
  35. <view class="test-buttons">
  36. <view class="test-btn" @click="removeUser">-</view>
  37. <view class="test-count">{{ notifyUsers.length }}</view>
  38. <view class="test-btn" @click="addUser">+</view>
  39. <view class="test-btn reset-btn" @click="resetUsers">重置</view>
  40. </view>
  41. </view>
  42. </template>
  43. <script setup>
  44. import { computed, ref } from 'vue'
  45. const notifyUsers = ref([
  46. {
  47. id: '1',
  48. name: '张三',
  49. avatar: '/static/logo.png',
  50. hasMessage: true,
  51. },
  52. {
  53. id: '1',
  54. name: '张三',
  55. avatar: '/static/logo.png',
  56. hasMessage: true,
  57. },
  58. {
  59. id: '1',
  60. name: '张三',
  61. avatar: '/static/logo.png',
  62. hasMessage: true,
  63. },
  64. ])
  65. const hasNotify = computed(() => notifyUsers.value.length > 0)
  66. // 屏幕尺寸(px)
  67. // 原始需求:竖屏 800×1280,屏幕比例 5:8 (宽:高) = 1.6
  68. const SCREEN_WIDTH = 800
  69. const SCREEN_HEIGHT = 1280
  70. const FOOTER_HEIGHT = 50 * (SCREEN_WIDTH / 750) // 50rpx 转 px by xu 2025-12-29
  71. const HORIZONTAL_PADDING = 10 * (SCREEN_WIDTH / 750) * 2 // 左右各10rpx by xu 2025-12-29
  72. const VERTICAL_PADDING = 10 * (SCREEN_WIDTH / 750) * 2 // 上下各10rpx by xu 2025-12-29
  73. const AVAILABLE_WIDTH = SCREEN_WIDTH - HORIZONTAL_PADDING // 减去水平边距
  74. const AVAILABLE_HEIGHT = (SCREEN_HEIGHT - FOOTER_HEIGHT) - VERTICAL_PADDING // 容器高度减去垂直边距 by xu 2025-12-29
  75. const SCREEN_RATIO = AVAILABLE_HEIGHT / AVAILABLE_WIDTH // 调整后的屏幕比例
  76. console.log(`[初始化] 屏幕:${SCREEN_WIDTH}×${SCREEN_HEIGHT}, 可用:${AVAILABLE_WIDTH.toFixed(2)}×${AVAILABLE_HEIGHT.toFixed(2)}, 比例:${SCREEN_RATIO.toFixed(3)}`)
  77. // 计算最优行列数 by xu 2025-12-29
  78. // 原则:优先保证x轴贴满,y轴可以有空隙
  79. function calculateOptimalGrid(n) {
  80. if (n === 1) return { rows: 1, cols: 1 }
  81. const CARD_RATIO = 49 / 40 // 卡片宽高比
  82. let bestRows = 1, bestCols = n
  83. let bestScore = Infinity
  84. // 遍历可能的行数
  85. for (let rows = 1; rows <= n; rows++) {
  86. const cols = Math.ceil(n / rows)
  87. const capacity = rows * cols
  88. const waste = capacity - n
  89. const wasteRatio = waste / n
  90. // 计算这个组合下的卡片尺寸
  91. const maxWidth = AVAILABLE_WIDTH / cols
  92. const maxHeight = AVAILABLE_HEIGHT / rows / CARD_RATIO
  93. const isWidthLimited = maxWidth < maxHeight // x轴是否贴满
  94. const gridRatio = rows / cols
  95. const ratioDiff = Math.abs(gridRatio - SCREEN_RATIO)
  96. // 综合评分:
  97. // 1. 优先选择x轴贴满的组合(isWidthLimited=true)
  98. // 2. 其次考虑浪费比例
  99. // 3. 最后考虑屏幕比例匹配
  100. const widthPenalty = isWidthLimited ? 0 : 2.0 // x轴不贴满时重罚
  101. const score = widthPenalty + wasteRatio * 0.5 + ratioDiff * 0.3
  102. console.log(` ${rows}×${cols}: maxW=${maxWidth.toFixed(1)}, maxH=${maxHeight.toFixed(1)}, ${isWidthLimited ? 'x轴贴满' : 'y轴贴满'}, 评分=${score.toFixed(3)}`)
  103. if (score < bestScore) {
  104. bestScore = score
  105. bestRows = rows
  106. bestCols = cols
  107. }
  108. }
  109. console.log(`[行列选择] ${n}人 -> ${bestRows}×${bestCols} (容量:${bestRows * bestCols}, 浪费:${bestRows * bestCols - n}, 评分:${bestScore.toFixed(3)})`)
  110. return { rows: bestRows, cols: bestCols }
  111. }
  112. // 计算布局信息 by xu 2025-12-29
  113. const layoutInfo = computed(() => {
  114. const n = notifyUsers.value.length
  115. if (n === 0) return null
  116. const { rows, cols } = calculateOptimalGrid(n)
  117. console.log(`[布局] 人数:${n}, 行列:${rows}×${cols}`)
  118. // 卡片宽高比 by xu 2025-12-29
  119. const CARD_RATIO = 49 / 40 // 宽:高 = 40:49
  120. // 计算卡片最大尺寸(考虑宽高比)
  121. const maxWidth = AVAILABLE_WIDTH / cols // 使用可用宽度 by xu 2025-12-29
  122. const maxHeight = AVAILABLE_HEIGHT / rows / CARD_RATIO // 使用可用高度除以比例 by xu 2025-12-29
  123. let cardWidth = Math.min(maxWidth, maxHeight)
  124. console.log(`[尺寸] maxWidth:${maxWidth.toFixed(2)}, maxHeight:${maxHeight.toFixed(2)}, cardWidth:${cardWidth.toFixed(2)}`)
  125. // 单张卡片时限制最大宽度 by xu 2025-12-29
  126. if (n === 1) {
  127. const maxSingleCardWidth = 600 // 单卡最大宽度500px
  128. cardWidth = Math.min(cardWidth, maxSingleCardWidth)
  129. }
  130. const cardHeight = cardWidth * CARD_RATIO
  131. // 计算整体布局的实际宽高
  132. const totalWidth = cols * cardWidth
  133. const totalHeight = rows * cardHeight
  134. // 计算起始偏移(居中)
  135. const offsetX = (AVAILABLE_WIDTH - totalWidth) / 2 + HORIZONTAL_PADDING / 2 // 使用可用宽度+左边距 by xu 2025-12-29
  136. const offsetY = VERTICAL_PADDING / 2 + (AVAILABLE_HEIGHT - totalHeight) / 2 // 上边距+居中偏移 by xu 2025-12-29
  137. console.log(`[偏移] offsetX:${offsetX.toFixed(2)}, offsetY:${offsetY.toFixed(2)}`)
  138. console.log(`[总尺寸] totalWidth:${totalWidth.toFixed(2)}, totalHeight:${totalHeight.toFixed(2)}`)
  139. console.log(`[可用空间] AVAILABLE_WIDTH:${AVAILABLE_WIDTH.toFixed(2)}, AVAILABLE_HEIGHT:${AVAILABLE_HEIGHT.toFixed(2)}`)
  140. console.log(`[边距] HORIZONTAL_PADDING:${HORIZONTAL_PADDING.toFixed(2)}, VERTICAL_PADDING:${VERTICAL_PADDING.toFixed(2)}`)
  141. // 动态计算字体大小、间距和头像尺寸 by xu 2025-12-29
  142. // 基准:4×4时卡片宽度约194px,字体30rpx(32px)
  143. const baseCardWidth = AVAILABLE_WIDTH / 4 // 4列时的卡片宽度
  144. const baseFontSize = 30 * (SCREEN_WIDTH / 750) // 30rpx转px
  145. const fontSize = Math.max(12, cardWidth / baseCardWidth * baseFontSize) // 按比例缩放,最小12px
  146. const gap = Math.max(6, cardWidth * 0.06) // 间距为卡片宽度的6%,最小6px
  147. const avatarSize = cardWidth * 0.55 // 头像尺寸为卡片宽度的55%,保持正方形
  148. // 计算每个用户的位置
  149. const positions = notifyUsers.value.map((user, index) => {
  150. const row = Math.floor(index / cols)
  151. const col = index % cols
  152. // 计算当前行的元素数量 by xu 2025-12-29
  153. const itemsInCurrentRow = Math.min(cols, n - row * cols)
  154. // 如果当前行元素少于列数,计算居中偏移
  155. const rowCenterOffset = itemsInCurrentRow < cols
  156. ? (cols - itemsInCurrentRow) * cardWidth / 2
  157. : 0
  158. return {
  159. ...user,
  160. left: offsetX + col * cardWidth + rowCenterOffset,
  161. top: offsetY + row * cardHeight,
  162. width: cardWidth,
  163. height: cardHeight,
  164. fontSize,
  165. gap,
  166. avatarSize
  167. }
  168. })
  169. return { positions, cardWidth, cardHeight, fontSize, gap, avatarSize }
  170. })
  171. // 测试功能:增加用户 by xu 2025-12-29
  172. function addUser() {
  173. notifyUsers.value.push({
  174. id: String(notifyUsers.value.length + 1),
  175. name: '张三',
  176. avatar: '/static/logo.png',
  177. hasMessage: true,
  178. })
  179. }
  180. // 测试功能:减少用户 by xu 2025-12-29
  181. function removeUser() {
  182. if (notifyUsers.value.length > 0) {
  183. notifyUsers.value.pop()
  184. }
  185. }
  186. // 测试功能:重置用户 by xu 2025-12-29
  187. function resetUsers() {
  188. notifyUsers.value = [
  189. {
  190. id: '1',
  191. name: '张三',
  192. avatar: '/static/logo.png',
  193. hasMessage: true,
  194. },
  195. {
  196. id: '2',
  197. name: '张三',
  198. avatar: '/static/logo.png',
  199. hasMessage: true,
  200. },
  201. {
  202. id: '3',
  203. name: '张三',
  204. avatar: '/static/logo.png',
  205. hasMessage: true,
  206. },
  207. ]
  208. }
  209. </script>
  210. <style>
  211. .notice-page {
  212. width: 100vw;
  213. height: 100vh;
  214. background: #000;
  215. display: flex;
  216. align-items: center;
  217. justify-content: center;
  218. overflow: hidden;
  219. padding-bottom: 50rpx;
  220. box-sizing: border-box;
  221. }
  222. .bg-gray {
  223. background: #b2b2b2;
  224. }
  225. .bg {
  226. width: 100%;
  227. height: 100%;
  228. }
  229. .notify-container {
  230. width: 100%;
  231. height: 100%;
  232. position: relative;
  233. }
  234. .notify-card {
  235. position: absolute;
  236. box-sizing: border-box;
  237. display: flex;
  238. flex-direction: column;
  239. align-items: center;
  240. justify-content: center;
  241. border: 1px solid #888;
  242. }
  243. .avatar {
  244. border-radius: 50%;
  245. border: 2px solid #fff;
  246. box-sizing: border-box;
  247. background: #f2f2f2;
  248. }
  249. .name {
  250. color: #000;
  251. font-weight: 600;
  252. text-align: center;
  253. word-break: break-all;
  254. }
  255. .footer {
  256. position: fixed;
  257. left: 0;
  258. right: 0;
  259. bottom: 0;
  260. width: 100%;
  261. height: 50rpx;
  262. background: #b2b2b2;
  263. display: flex;
  264. align-items: center;
  265. justify-content: center;
  266. }
  267. .footer-text {
  268. color: #000;
  269. font-size: 26rpx;
  270. line-height: 1;
  271. display: inline-flex;
  272. align-items: center;
  273. }
  274. .triangles {
  275. display: inline-flex;
  276. align-items: center;
  277. gap: 6rpx;
  278. margin-right: 12rpx;
  279. }
  280. .triangle {
  281. width: 0;
  282. height: 0;
  283. border-top: 10rpx solid transparent;
  284. border-bottom: 10rpx solid transparent;
  285. border-left: 14rpx solid #000;
  286. }
  287. .test-buttons {
  288. position: fixed;
  289. right: 20rpx;
  290. bottom: 100rpx;
  291. display: flex;
  292. align-items: center;
  293. gap: 20rpx;
  294. background: rgba(0, 0, 0, 0.7);
  295. padding: 20rpx;
  296. border-radius: 10rpx;
  297. z-index: 9999;
  298. }
  299. .test-btn {
  300. width: 60rpx;
  301. height: 60rpx;
  302. background: #fff;
  303. color: #000;
  304. font-size: 40rpx;
  305. font-weight: bold;
  306. display: flex;
  307. align-items: center;
  308. justify-content: center;
  309. border-radius: 50%;
  310. cursor: pointer;
  311. }
  312. .test-count {
  313. color: #fff;
  314. font-size: 32rpx;
  315. font-weight: bold;
  316. min-width: 40rpx;
  317. text-align: center;
  318. }
  319. .reset-btn {
  320. width: auto;
  321. padding: 0 20rpx;
  322. font-size: 24rpx;
  323. border-radius: 30rpx;
  324. }
  325. </style>