| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500 |
- <template>
- <view class="notice-page">
- <image v-if="!hasNotify" class="bg" src="/static/images/deviceunlogin.png" mode="aspectFit" />
- <view v-else class="notify-container">
- <view
- v-if="studentNotifyUsers.length"
- class="notify-section section-student"
- :class="{ 'with-divider': parentUnreadUsers.length }"
- :style="{ height: sectionHeights.student + 'px' }"
- >
- <view
- v-for="(item, index) in studentLayout.positions"
- :key="'xy-' + item.id + '_' + index"
- class="notify-card student-card"
- :style="{
- left: item.left + 'px',
- top: item.top + 'px',
- width: item.width + 'px',
- height: item.height + 'px',
- gap: item.gap + 'px'
- }"
- >
- <image
- class="avatar student-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
- v-if="parentUnreadUsers.length"
- class="notify-section section-parent"
- :style="{ height: sectionHeights.parent + 'px' }"
- >
- <view
- v-for="(item, index) in parentLayout.positions"
- :key="'jz-' + item.id + '_' + index"
- class="notify-card parent-card"
- :style="{
- left: item.left + 'px',
- top: item.top + 'px',
- width: item.width + 'px',
- height: item.height + 'px',
- gap: item.gap + 'px'
- }"
- >
- <image
- class="avatar parent-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>
- <view class="footer">
- <view class="footer-text">
- <view class="triangles">
- <view class="triangle" />
- <view class="triangle" />
- <view class="triangle" />
- </view>
- 请刷卡登录查看留言
- </view>
- </view>
- </view>
- </template>
- <script setup>
- import { computed, ref } from 'vue'
- import { onLoad, onShow, onUnload } from '@dcloudio/uni-app'
- import websocketService from '@/utils/websocket'
- import { deviceApi } from '@/api/device'
- import { getImageUrl } from '@/utils/util'
- const DEFAULT_AVATAR = '/static/logo.png'
- const USE_MOCK_WHEN_EMPTY = true
- const studentNotifyUsers = ref([])
- const parentUnreadUsers = ref([])
- const deviceSn = ref('')
- const wsUnsubscribers = []
- const totalNotifyCount = computed(() => studentNotifyUsers.value.length + parentUnreadUsers.value.length)
- const hasNotify = computed(() => totalNotifyCount.value > 0)
- // 竖屏设备目标尺寸(px)
- const SCREEN_WIDTH = 800
- const SCREEN_HEIGHT = 1280
- const FOOTER_HEIGHT = 50 * (SCREEN_WIDTH / 750)
- const HORIZONTAL_PADDING = 10 * (SCREEN_WIDTH / 750) * 2
- const VERTICAL_PADDING = 10 * (SCREEN_WIDTH / 750) * 2
- const AVAILABLE_WIDTH = SCREEN_WIDTH - HORIZONTAL_PADDING
- const AVAILABLE_HEIGHT = (SCREEN_HEIGHT - FOOTER_HEIGHT) - VERTICAL_PADDING
- const CARD_RATIO = 49 / 40
- const MIN_SECTION_HEIGHT = 140
- const SECTION_INNER_PADDING_Y = 8
- function toDisplayImage(path) {
- if (!path) return DEFAULT_AVATAR
- if (typeof path !== 'string') return DEFAULT_AVATAR
- if (path.startsWith('http://') || path.startsWith('https://') || path.startsWith('data:') || path.startsWith('/')) {
- return path
- }
- return getImageUrl(path)
- }
- function toDisplayName(value, fallback = '未命名') {
- const name = String(value || '').trim()
- return name || fallback
- }
- function formatStudentList(list = []) {
- return list.map((item = {}, index) => ({
- id: item.ryid || item.xyid || `xy-${index}`,
- name: toDisplayName(item.xm, '学员'),
- avatar: toDisplayImage(item.zjzwj)
- }))
- }
- function formatParentList(list = []) {
- return list.map((item = {}, index) => ({
- id: item.ryid || item.jzid || `jz-${index}`,
- name: toDisplayName(item.xm, '家长'),
- avatar: toDisplayImage(item.yszwj)
- }))
- }
- function createMockRefreshPayload() {
- return {
- xyList: [
- { ryid: 'xy-1', xm: '张珊', zjzwj: '/static/logo.png' },
- { ryid: 'xy-2', xm: '王舞', zjzwj: '/static/logo.png' },
- { ryid: 'xy-3', xm: '李丽思', zjzwj: '/static/logo.png' }
- ],
- jzList: [
- { ryid: 'jz-1', xm: '张珊', yszwj: '/static/logo.png' },
- { ryid: 'jz-2', xm: '王舞', yszwj: '/static/logo.png' },
- { ryid: 'jz-3', xm: '李丽思', yszwj: '/static/logo.png' },
- { ryid: 'jz-4', xm: '张珊二', yszwj: '/static/logo.png' }
- ]
- }
- }
- function extractRefreshPayload(data = {}) {
- const direct = data && typeof data === 'object' ? data : {}
- if (Array.isArray(direct.xyList) || Array.isArray(direct.jzList)) {
- return direct
- }
- const ssData = direct.ssData && typeof direct.ssData === 'object' ? direct.ssData : null
- if (ssData && (Array.isArray(ssData.xyList) || Array.isArray(ssData.jzList))) {
- return ssData
- }
- const requestData = direct.request && typeof direct.request === 'object' ? direct.request : null
- if (requestData && (Array.isArray(requestData.xyList) || Array.isArray(requestData.jzList))) {
- return requestData
- }
- return {}
- }
- function calculateOptimalGrid(count, areaWidth, areaHeight, cardRatio = CARD_RATIO) {
- if (!count || areaWidth <= 0 || areaHeight <= 0) return { rows: 0, cols: 0 }
- if (count === 1) return { rows: 1, cols: 1 }
- const targetRatio = areaHeight / areaWidth
- let bestRows = 1
- let bestCols = count
- let bestScore = Number.POSITIVE_INFINITY
- for (let rows = 1; rows <= count; rows++) {
- const cols = Math.ceil(count / rows)
- const capacity = rows * cols
- const wasteRatio = (capacity - count) / count
- const maxWidth = areaWidth / cols
- const maxHeight = areaHeight / rows / cardRatio
- const isWidthLimited = maxWidth < maxHeight
- const ratioDiff = Math.abs((rows / cols) - targetRatio)
- const widthPenalty = isWidthLimited ? 0 : 2.0
- const score = widthPenalty + wasteRatio * 0.5 + ratioDiff * 0.3
- if (score < bestScore) {
- bestScore = score
- bestRows = rows
- bestCols = cols
- }
- }
- return { rows: bestRows, cols: bestCols }
- }
- function buildSectionLayout(users = [], sectionHeight = 0, options = {}) {
- const list = Array.isArray(users) ? users : []
- const count = list.length
- if (!count || sectionHeight <= 0) {
- return { positions: [] }
- }
- const avatarScale = options.avatarScale || 0.55
- const maxSingleCardWidth = options.maxSingleCardWidth || 600
- const baseCols = options.baseCols || 4
- const areaWidth = AVAILABLE_WIDTH
- const areaHeight = Math.max(0, sectionHeight - SECTION_INNER_PADDING_Y * 2)
- if (areaWidth <= 0 || areaHeight <= 0) {
- return { positions: [] }
- }
- const { rows, cols } = calculateOptimalGrid(count, areaWidth, areaHeight, CARD_RATIO)
- if (!rows || !cols) return { positions: [] }
- const maxWidth = areaWidth / cols
- const maxHeight = areaHeight / rows / CARD_RATIO
- let cardWidth = Math.min(maxWidth, maxHeight)
- if (count === 1) {
- cardWidth = Math.min(cardWidth, maxSingleCardWidth)
- }
- const cardHeight = cardWidth * CARD_RATIO
- const totalWidth = cols * cardWidth
- const totalHeight = rows * cardHeight
- const offsetX = (areaWidth - totalWidth) / 2 + HORIZONTAL_PADDING / 2
- const offsetY = (areaHeight - totalHeight) / 2 + SECTION_INNER_PADDING_Y
- const baseCardWidth = AVAILABLE_WIDTH / baseCols
- const baseFontSize = 30 * (SCREEN_WIDTH / 750)
- const fontSize = Math.max(12, (cardWidth / baseCardWidth) * baseFontSize)
- const gap = Math.max(6, cardWidth * 0.06)
- const avatarSize = cardWidth * avatarScale
- const positions = list.map((item, index) => {
- const row = Math.floor(index / cols)
- const col = index % cols
- const itemsInCurrentRow = Math.min(cols, count - row * cols)
- const rowCenterOffset = itemsInCurrentRow < cols
- ? (cols - itemsInCurrentRow) * cardWidth / 2
- : 0
- return {
- ...item,
- left: offsetX + col * cardWidth + rowCenterOffset,
- top: offsetY + row * cardHeight,
- width: cardWidth,
- height: cardHeight,
- fontSize,
- gap,
- avatarSize
- }
- })
- return { positions }
- }
- const sectionHeights = computed(() => {
- const studentCount = studentNotifyUsers.value.length
- const parentCount = parentUnreadUsers.value.length
- const total = studentCount + parentCount
- if (!total) {
- return { student: 0, parent: 0 }
- }
- if (!studentCount) {
- return { student: 0, parent: AVAILABLE_HEIGHT }
- }
- if (!parentCount) {
- return { student: AVAILABLE_HEIGHT, parent: 0 }
- }
- let studentHeight = Math.round((AVAILABLE_HEIGHT * studentCount) / total)
- studentHeight = Math.max(MIN_SECTION_HEIGHT, Math.min(studentHeight, AVAILABLE_HEIGHT - MIN_SECTION_HEIGHT))
- return {
- student: studentHeight,
- parent: AVAILABLE_HEIGHT - studentHeight
- }
- })
- const studentLayout = computed(() => buildSectionLayout(studentNotifyUsers.value, sectionHeights.value.student, {
- avatarScale: 0.56,
- maxSingleCardWidth: 260
- }))
- const parentLayout = computed(() => buildSectionLayout(parentUnreadUsers.value, sectionHeights.value.parent, {
- avatarScale: 0.48,
- maxSingleCardWidth: 220
- }))
- function resolveDeviceSn(options = {}) {
- if (options && options.sn) return String(options.sn)
- let userInfo = uni.getStorageSync('userInfo') || {}
- if (typeof userInfo === 'string') {
- try {
- userInfo = JSON.parse(userInfo)
- } catch (error) {
- userInfo = {}
- }
- }
- if (userInfo.devId) return String(userInfo.devId)
- const deviceInfo = uni.getStorageSync('deviceInfo') || {}
- if (deviceInfo.deviceId) return String(deviceInfo.deviceId)
- return 'ssDevId_a'
- }
- async function refreshNoticeUsers() {
- try {
- const result = await deviceApi.grfw_refreshDhjHome()
- const payload = extractRefreshPayload(result?.data || {})
- let xyList = Array.isArray(payload.xyList) ? payload.xyList : []
- let jzList = Array.isArray(payload.jzList) ? payload.jzList : []
- if (!xyList.length && !jzList.length && USE_MOCK_WHEN_EMPTY) {
- const mock = createMockRefreshPayload()
- xyList = mock.xyList
- jzList = mock.jzList
- }
- studentNotifyUsers.value = formatStudentList(xyList)
- parentUnreadUsers.value = formatParentList(jzList)
- } catch (error) {
- console.error('刷新电话机首页留言失败:', error)
- if (USE_MOCK_WHEN_EMPTY) {
- const mock = createMockRefreshPayload()
- studentNotifyUsers.value = formatStudentList(mock.xyList)
- parentUnreadUsers.value = formatParentList(mock.jzList)
- return
- }
- studentNotifyUsers.value = []
- parentUnreadUsers.value = []
- }
- }
- function bindNoticeSocketListeners() {
- if (wsUnsubscribers.length) return
- const offRefresh = websocketService.on('cmd:51', async () => {
- await refreshNoticeUsers()
- uni.$emit('device-message-refresh')
- })
- wsUnsubscribers.push(offRefresh)
- }
- async function ensureNoticeSocketConnected() {
- if (!deviceSn.value) return
- bindNoticeSocketListeners()
- try {
- await websocketService.ensureConnected({
- role: 'device',
- ssDev: deviceSn.value,
- heartbeat: true,
- autoReconnect: true
- })
- } catch (error) {
- console.error('notice 建立设备 websocket 失败:', error)
- }
- }
- onLoad(async (options) => {
- deviceSn.value = resolveDeviceSn(options || {})
- await refreshNoticeUsers()
- await ensureNoticeSocketConnected()
- })
- onShow(async () => {
- if (!deviceSn.value) {
- deviceSn.value = resolveDeviceSn({})
- }
- await refreshNoticeUsers()
- await ensureNoticeSocketConnected()
- })
- onUnload(() => {
- wsUnsubscribers.forEach((off) => {
- if (typeof off === 'function') off()
- })
- wsUnsubscribers.length = 0
- })
- </script>
- <style>
- .notice-page {
- width: 100vw;
- height: 100vh;
- background: #000;
- overflow: hidden;
- padding-bottom: 50rpx;
- box-sizing: border-box;
- }
- .bg {
- width: 100%;
- height: 100%;
- }
- .notify-container {
- width: 100%;
- height: 100%;
- display: flex;
- flex-direction: column;
- }
- .notify-section {
- width: 100%;
- position: relative;
- overflow: hidden;
- }
- .section-student {
- background: #fff;
- }
- .section-student.with-divider {
- border-bottom: 1px solid #b5b5b5;
- }
- .section-parent {
- background: #d9d9d9;
- }
- .notify-card {
- position: absolute;
- box-sizing: border-box;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- border: 1px solid #b3b3b3;
- }
- .student-card {
- background: #fff;
- }
- .parent-card {
- background: #d9d9d9;
- }
- .avatar {
- box-sizing: border-box;
- display: block;
- background: #f2f2f2;
- }
- .student-avatar {
- border-radius: 6rpx;
- border: 1px solid #e6e6e6;
- }
- .parent-avatar {
- border-radius: 50%;
- border: 2px solid #fff;
- }
- .name {
- color: #000;
- font-weight: 600;
- text-align: center;
- word-break: break-all;
- line-height: 1.25;
- }
- .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;
- }
- </style>
|