From 2e1494c477789223ee2e4431b83f34b062cccae8 Mon Sep 17 00:00:00 2001 From: icebergfeng Date: Fri, 27 Mar 2026 19:48:33 +0800 Subject: [PATCH] release: TUILiveKit/Web/web-vite-vue3 --- Web/web-vite-vue3/package.json | 10 +- Web/web-vite-vue3/src/App.vue | 4 +- .../src/TUILiveKit/LivePusherView.vue | 8 +- .../component/LivePlayer/LivePlayerH5.vue | 18 +- .../component/LivePlayer/LivePlayerPC.vue | 50 +- .../src/TUILiveKit/i18n/en-US/index.ts | 6 + .../src/TUILiveKit/i18n/zh-CN/index.ts | 6 + .../src/TUILiveKit/utils/utils.ts | 15 + Web/web-vite-vue3/src/api/upload.ts | 5 +- .../business/components/BusinessHeader.vue | 9 +- .../business/components/BusinessSidePanel.vue | 1224 ++++++++++++----- .../business/components/BusinessToolbar.vue | 12 +- .../components/LivePlayerBusinessPC.vue | 1100 ++++++++++----- .../src/business/styles/business.scss | 4 +- Web/web-vite-vue3/src/i18n/en-US/index.ts | 1 + Web/web-vite-vue3/src/i18n/zh-CN/index.ts | 1 + Web/web-vite-vue3/src/views/live-player.vue | 2 +- .../upload-server/src/config/index.js | 2 +- .../upload-server/src/routes/uploadRouter.js | 59 +- Web/web-vite-vue3/vite.config.ts | 3 + 20 files changed, 1838 insertions(+), 701 deletions(-) diff --git a/Web/web-vite-vue3/package.json b/Web/web-vite-vue3/package.json index 34b8a05a..48c9e3b2 100644 --- a/Web/web-vite-vue3/package.json +++ b/Web/web-vite-vue3/package.json @@ -1,8 +1,9 @@ { "name": "@tencentcloud/live-uikit-vue", - "version": "5.6.0", + "version": "5.8.0", "scripts": { "dev": "vite --force", + "dev:business": "cross-env STYLE_PRESET=business vite --force", "dev:deep": "vite", "dev:local": "vite --force", "upload-server": "npm --prefix ./upload-server run start", @@ -10,13 +11,14 @@ "upload-server:bootstrap": "npm --prefix ./upload-server install", "dev:with-upload-server": "concurrently \"npm run dev\" \"npm run upload-server\"", "build": "vite build", + "build:business": "cross-env STYLE_PRESET=business vite build", "build:local": "vite build", "preview": "vite preview", "lint": "./node_modules/.bin/eslint ./src --no-error-on-unmatched-pattern" }, "dependencies": { - "@tencentcloud/tuiroom-engine-js": "~4.0.2", - "@tencentcloud/uikit-base-component-vue3": "1.3.8", + "@tencentcloud/tuiroom-engine-js": "~4.0.3", + "@tencentcloud/uikit-base-component-vue3": "1.4.0", "@tencentcloud/universal-api": "^2.0.9", "axios": "^0.27.2", "js-cookie": "^3.0.1", @@ -24,7 +26,7 @@ "pinia": "^2.0.13", "qs": "^6.10.3", "rtc-detect": "^1.0.3", - "tuikit-atomicx-vue3": "5.6.0", + "tuikit-atomicx-vue3": "5.8.1", "vue": "^3.2.25", "vue-i18n": "^9.10.2", "vue-router": "^4.0.14" diff --git a/Web/web-vite-vue3/src/App.vue b/Web/web-vite-vue3/src/App.vue index 8d5af75f..d51ed2e1 100644 --- a/Web/web-vite-vue3/src/App.vue +++ b/Web/web-vite-vue3/src/App.vue @@ -1,5 +1,5 @@ @@ -15,7 +15,7 @@ const { language } = useUIKit(); TUIRoomEngine.once('ready', () => { watch(language, () => { initRoomEngineLanguage(); - }, { immediate: true }) + }, { immediate: true }); }); diff --git a/Web/web-vite-vue3/src/TUILiveKit/LivePusherView.vue b/Web/web-vite-vue3/src/TUILiveKit/LivePusherView.vue index aa0c3de2..2e5acb12 100644 --- a/Web/web-vite-vue3/src/TUILiveKit/LivePusherView.vue +++ b/Web/web-vite-vue3/src/TUILiveKit/LivePusherView.vue @@ -212,7 +212,7 @@ import OrientationSwitch from './component/OrientationSwitch.vue'; import SettingButton from './component/SettingButton.vue'; import SpeakerVolumeSetting from './component/SpeakerVolumeSetting.vue'; import LivePusherNotification from './component/LivePusherNotification.vue'; -import { copyToClipboard } from './utils/utils'; +import { copyToClipboard, isSvgCoverUrl } from './utils/utils'; import { errorHandler } from './utils/errorHandler'; import { initRoomEngineLanguage } from '../utils/utils'; @@ -279,6 +279,12 @@ const handleLiveSettingConfirm = async (form: { liveName: string; coverUrl?: str liveName: form.liveName.trim(), coverUrl: (form.coverUrl || '').trim(), }; + if (isSvgCoverUrl(updatedForm.coverUrl)) { + TUIToast.error({ + message: t('Unsupported image format'), + }); + return; + } if (!isInLive.value || !currentLive.value?.liveId) { liveParamsEditForm.value = updatedForm; diff --git a/Web/web-vite-vue3/src/TUILiveKit/component/LivePlayer/LivePlayerH5.vue b/Web/web-vite-vue3/src/TUILiveKit/component/LivePlayer/LivePlayerH5.vue index 994b75c4..ad97ee19 100644 --- a/Web/web-vite-vue3/src/TUILiveKit/component/LivePlayer/LivePlayerH5.vue +++ b/Web/web-vite-vue3/src/TUILiveKit/component/LivePlayer/LivePlayerH5.vue @@ -51,7 +51,7 @@
import { ref, onMounted, computed, onUnmounted, watch, Teleport } from 'vue'; import TUIRoomEngine, { TUIAutoPlayCallbackInfo, TUIRoomEvents } from '@tencentcloud/tuiroom-engine-js'; -import { TUIButton, IconClose, IconLike, TUIDialog, useUIKit, TUIMessageBox } from '@tencentcloud/uikit-base-component-vue3'; +import { TUIButton, IconClose, IconLike, TUIDialog, TUIToast, useUIKit, TUIMessageBox } from '@tencentcloud/uikit-base-component-vue3'; import { LiveAudienceList, LiveCoreView, @@ -137,6 +137,18 @@ const { audienceList, fetchAudienceList } = useLiveAudienceState(); const { currentLive, joinLive, leaveLive, subscribeEvent, unsubscribeEvent } = useLiveListState(); const { loginUserInfo } = useLoginState(); const isInLive = computed(() => !!currentLive.value?.liveId); + +// Mute detection: show toast when the current user is muted by the host +const localAudience = computed(() => audienceList.value.find(item => item.userId === loginUserInfo.value?.userId)); +const isMessageMuted = computed(() => !!localAudience.value?.isMessageDisabled); +watch(isMessageMuted, (newVal, oldVal) => { + if (newVal && !oldVal) { + TUIToast.info({ message: t('You have been muted in this room') }); + } + if (!newVal && oldVal) { + TUIToast.info({ message: t('You have been unmuted in this room') }); + } +}); const { canvas } = useLiveSeatState(); const { giftInfoList, sendLikes, subscribeEvent: subscribeGiftEvent, unsubscribeEvent: unsubscribeGiftEvent } = useLiveGiftState(); const roomEngine = useRoomEngine(); @@ -267,7 +279,7 @@ function handleAutoPlayFailed(event: TUIAutoPlayCallbackInfo) { callback: () => { autoPlayFailedHandled.value = false; event.resume(); - } + }, }); } diff --git a/Web/web-vite-vue3/src/TUILiveKit/component/LivePlayer/LivePlayerPC.vue b/Web/web-vite-vue3/src/TUILiveKit/component/LivePlayer/LivePlayerPC.vue index de80c0ee..e4c35df8 100644 --- a/Web/web-vite-vue3/src/TUILiveKit/component/LivePlayer/LivePlayerPC.vue +++ b/Web/web-vite-vue3/src/TUILiveKit/component/LivePlayer/LivePlayerPC.vue @@ -3,11 +3,19 @@
- - {{ currentLive?.liveOwner.userName || currentLive?.liveOwner.userId }} + +
@@ -90,10 +98,11 @@ @@ -316,6 +339,18 @@ function handleAutoPlayFailed(event: TUIAutoPlayCallbackInfo) { cursor: pointer; } } + + .top-ended-avatar { + width: 32px; + height: 32px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.12); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + color: rgba(255, 255, 255, 0.55); + } } .main-left-center { @@ -325,7 +360,6 @@ function handleAutoPlayFailed(event: TUIAutoPlayCallbackInfo) { min-height: 0; background-color: black; overflow: hidden; - border: 1px solid var(--bg-color-operate); .live-ended-overlay { position: absolute; diff --git a/Web/web-vite-vue3/src/TUILiveKit/i18n/en-US/index.ts b/Web/web-vite-vue3/src/TUILiveKit/i18n/en-US/index.ts index f57a04fa..09abcb50 100644 --- a/Web/web-vite-vue3/src/TUILiveKit/i18n/en-US/index.ts +++ b/Web/web-vite-vue3/src/TUILiveKit/i18n/en-US/index.ts @@ -152,6 +152,9 @@ export const resource = { 'Seat does not support link mic': 'Seat does not support link mic', 'Empty seat list': 'Empty seat list', 'End Link': 'End Link', + 'BarrageList.ComeIn': 'came in', + 'BarrageList.Leave': 'left', + 'BarrageList.SendGift': 'sent', // Business theme additions (reuse existing keys when possible: 'Online viewers', 'Exit') 'Search audience...': 'Search audience...', 'No results found': 'No results found', @@ -169,6 +172,7 @@ export const resource = { 'Turn on sound': 'Turn on sound', 'Mute sound': 'Mute sound', 'Exit picture in picture': 'Exit picture in picture', + 'Not allow to enter cinema mode in fullscreen': 'Not allow to enter cinema mode in fullscreen', 'Enter cinema mode': 'Enter cinema mode', 'Exit cinema mode': 'Exit cinema mode', 'Enter full screen': 'Enter full screen', @@ -192,4 +196,6 @@ export const resource = { More: 'More', 'joined the live': 'joined the live', 'No audience yet': 'No audience yet', + 'You have been muted in this room': 'You have been muted in this room', + 'You have been unmuted in this room': 'You have been unmuted in this room', }; diff --git a/Web/web-vite-vue3/src/TUILiveKit/i18n/zh-CN/index.ts b/Web/web-vite-vue3/src/TUILiveKit/i18n/zh-CN/index.ts index 38b15a1c..4acbecbf 100644 --- a/Web/web-vite-vue3/src/TUILiveKit/i18n/zh-CN/index.ts +++ b/Web/web-vite-vue3/src/TUILiveKit/i18n/zh-CN/index.ts @@ -152,6 +152,9 @@ export const resource = { 'Seat does not support link mic': '该房间不支持连麦', 'Empty seat list': '连麦列表为空', 'End Link': '结束连麦', + 'BarrageList.ComeIn': '进入了直播间', + 'BarrageList.Leave': '离开了直播间', + 'BarrageList.SendGift': '送出了', // Business theme additions (reuse existing keys when possible: 'Online viewers', 'Exit') 'Search audience...': '搜索观众...', 'No results found': '无匹配结果', @@ -169,6 +172,7 @@ export const resource = { 'Turn on sound': '取消静音', 'Mute sound': '静音', 'Exit picture in picture': '退出画中画', + 'Not allow to enter cinema mode in fullscreen': '全屏状态下不允许进入影院模式', 'Enter cinema mode': '进入影院模式', 'Exit cinema mode': '退出影院模式', 'Enter full screen': '进入全屏', @@ -192,4 +196,6 @@ export const resource = { More: '更多', 'joined the live': '加入了直播', 'No audience yet': '暂无观众', + 'You have been muted in this room': '当前房间内,您已被禁言', + 'You have been unmuted in this room': '当前房间内,您已被解除禁言', }; diff --git a/Web/web-vite-vue3/src/TUILiveKit/utils/utils.ts b/Web/web-vite-vue3/src/TUILiveKit/utils/utils.ts index dd0e9524..fcc039fb 100644 --- a/Web/web-vite-vue3/src/TUILiveKit/utils/utils.ts +++ b/Web/web-vite-vue3/src/TUILiveKit/utils/utils.ts @@ -78,6 +78,21 @@ export async function copyToClipboard(text: string): Promise { await navigator.clipboard.writeText(text); } +export function isSvgCoverUrl(coverUrl: string): boolean { + if (!coverUrl) { + return false; + } + const normalizedUrl = coverUrl.trim().toLowerCase(); + if (!normalizedUrl) { + return false; + } + if (normalizedUrl.startsWith('data:image/svg+xml')) { + return true; + } + const urlWithoutQuery = normalizedUrl.split('#')[0].split('?')[0]; + return urlWithoutQuery.endsWith('.svg') || urlWithoutQuery.endsWith('.svgz'); +} + export function isSafariBrowser(): boolean { // Safari has several unique features const isSafari = diff --git a/Web/web-vite-vue3/src/api/upload.ts b/Web/web-vite-vue3/src/api/upload.ts index 0e49af9c..e8e7df6f 100644 --- a/Web/web-vite-vue3/src/api/upload.ts +++ b/Web/web-vite-vue3/src/api/upload.ts @@ -10,7 +10,6 @@ export const UPLOAD_ALLOWED_MIME_TYPES = [ 'image/png', 'image/gif', 'image/webp', - 'image/svg+xml', ]; const uploadHttp = axios.create({ @@ -51,8 +50,10 @@ export async function uploadImageFile(params: { type?: 'cover' | 'gift-icon' | 'gift-animation'; }): Promise { const formData = new FormData(); - formData.append('file', params.file); + // Keep `type` before `file` so the server can resolve per-type MIME rules + // as early as possible during multipart parsing. formData.append('type', params.type || 'cover'); + formData.append('file', params.file); const response = await uploadHttp.post('/api/upload/image', formData, { headers: { diff --git a/Web/web-vite-vue3/src/business/components/BusinessHeader.vue b/Web/web-vite-vue3/src/business/components/BusinessHeader.vue index 01ea73de..e437e292 100644 --- a/Web/web-vite-vue3/src/business/components/BusinessHeader.vue +++ b/Web/web-vite-vue3/src/business/components/BusinessHeader.vue @@ -29,7 +29,10 @@ @@ -602,9 +1069,13 @@ onUnmounted(() => { flex-shrink: 0; display: flex; align-items: center; - padding: 8px 10px; + padding: 10px 12px 8px; border-bottom: 1px solid var(--preset-chat-surface-divider, rgba(255, 255, 255, 0.1)); - background: linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(255, 255, 255, 0)); + background: linear-gradient( + 180deg, + color-mix(in srgb, var(--preset-tab-bar-bg, #1a2535) 58%, transparent) 0%, + color-mix(in srgb, var(--preset-tab-bar-bg, #1a2535) 36%, transparent) 100% + ); } .tab-bar-inner { @@ -613,9 +1084,10 @@ onUnmounted(() => { display: flex; border-radius: 12px; padding: 3px; - background: var(--preset-tab-track-bg, rgba(255, 255, 255, 0.085)); - border: 1px solid var(--preset-tab-track-border, rgba(255, 255, 255, 0.14)); + background: color-mix(in srgb, var(--preset-tab-track-bg, rgba(24, 35, 62, 0.78)) 84%, var(--preset-chat-surface-bg, #111a27)); + border: 1px solid color-mix(in srgb, var(--preset-tab-track-border, rgba(255, 255, 255, 0.22)) 80%, transparent); gap: 2px; + overflow: hidden; } /* Sliding pill background behind the active tab */ @@ -624,14 +1096,20 @@ onUnmounted(() => { top: 3px; bottom: 3px; border-radius: 10px; - background: var(--preset-tab-slider-bg, rgba(255, 255, 255, 0.2)); + background: var( + --preset-tab-slider-bg, + linear-gradient( + 180deg, + color-mix(in srgb, var(--preset-primary, #4c8bf5) 54%, #ffffff 46%) 0%, + color-mix(in srgb, var(--preset-primary, #4c8bf5) 72%, #13254c 28%) 100% + ) + ); box-shadow: - 0 1px 3px rgba(0, 0, 0, 0.18), - 0 0 0 1px rgba(255, 255, 255, 0.18), - inset 0 1px 0 rgba(255, 255, 255, 0.14); + 0 2px 10px color-mix(in srgb, var(--preset-primary, #4c8bf5) 24%, transparent), + inset 0 1px 0 color-mix(in srgb, var(--uikit-color-white-1, #fff) 28%, transparent); transition: - left 380ms cubic-bezier(0.34, 1.56, 0.64, 1), - width 380ms cubic-bezier(0.34, 1.56, 0.64, 1), + left 260ms cubic-bezier(0.2, 0.8, 0.2, 1), + width 260ms cubic-bezier(0.2, 0.8, 0.2, 1), opacity 200ms ease; pointer-events: none; z-index: 0; @@ -652,7 +1130,7 @@ onUnmounted(() => { cursor: pointer; font-size: 13px; font-weight: 500; - color: var(--preset-tab-btn-text, rgba(255, 255, 255, 0.38)); + color: var(--preset-tab-btn-text, rgba(255, 255, 255, 0.82)); letter-spacing: 0.01em; transition: color 280ms ease, @@ -660,7 +1138,7 @@ onUnmounted(() => { user-select: none; &:hover:not(.active) { - color: var(--preset-tab-btn-hover-text, rgba(255, 255, 255, 0.58)); + color: var(--preset-tab-btn-hover-text, rgba(255, 255, 255, 0.94)); } &:active { @@ -668,8 +1146,8 @@ onUnmounted(() => { } &.active { - color: var(--preset-tab-btn-active-text, rgba(255, 255, 255, 0.95)); - font-weight: 600; + color: var(--preset-tab-btn-active-text, #ffffff); + font-weight: 650; } .tab-icon { @@ -707,16 +1185,16 @@ onUnmounted(() => { border-radius: 999px; font-size: 11px; font-weight: 600; - background: var(--preset-tab-badge-bg, rgba(255, 255, 255, 0.06)); - color: var(--preset-tab-badge-text, rgba(255, 255, 255, 0.3)); + background: var(--preset-tab-badge-bg, rgba(255, 255, 255, 0.16)); + color: var(--preset-tab-badge-text, rgba(255, 255, 255, 0.78)); line-height: 1.4; transition: background 280ms ease, color 280ms ease; &.active { - background: var(--preset-tab-badge-active-bg, rgba(255, 255, 255, 0.1)); - color: var(--preset-tab-badge-active-text, rgba(255, 255, 255, 0.82)); + background: var(--preset-tab-badge-active-bg, color-mix(in srgb, var(--preset-primary, #4c8bf5) 55%, rgba(255, 255, 255, 0.28))); + color: var(--preset-tab-badge-active-text, #ffffff); } } @@ -766,8 +1244,6 @@ onUnmounted(() => { gap: 4px; padding: 12px 12px 16px; background: var(--preset-chat-surface-bg); - scrollbar-width: thin; - scrollbar-color: var(--preset-chat-scrollbar) transparent; user-select: text; -webkit-user-select: text; } @@ -797,7 +1273,7 @@ onUnmounted(() => { align-items: center; gap: 6px; padding: 4px 12px; - font-size: 12px; + font-size: 13px; color: var(--preset-system-msg-text); .system-icon { @@ -827,6 +1303,14 @@ onUnmounted(() => { justify-content: center; font-size: 12px; font-weight: 700; + overflow: hidden; +} + +.msg-avatar-img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; } .msg-main { @@ -872,6 +1356,8 @@ onUnmounted(() => { .msg-bubble { display: inline-block; max-width: 100%; + white-space: pre-wrap; + overflow-wrap: anywhere; word-break: break-word; padding: 7px 11px; border-radius: 10px; @@ -894,19 +1380,41 @@ onUnmounted(() => { } } +.msg-emoji { + width: 18px; + height: 18px; + vertical-align: text-bottom; + margin: 0 1px; +} + +.gift-content { + display: inline-flex; + align-items: center; + gap: 4px; +} + +.gift-prefix { + color: var(--preset-msg-content-text); +} + +.gift-name { + font-weight: 600; +} + +.gift-icon { + width: 16px; + height: 16px; + vertical-align: middle; +} + /* ── Chat Input ── */ .chat-input-shell { flex-shrink: 0; position: relative; - padding: 10px 12px; + padding: 10px 12px 12px; border-top: 1px solid var(--preset-chat-surface-divider, rgba(255, 255, 255, 0.08)); - background: - linear-gradient( - 180deg, - color-mix(in srgb, var(--preset-tab-bar-bg, #1a2535) 72%, #000 28%) 0%, - color-mix(in srgb, var(--preset-tab-bar-bg, #1a2535) 78%, #000 22%) 100% - ); + background: var(--preset-input-bar-bg, transparent); &.chat-disabled { .input-wrapper { @@ -926,14 +1434,14 @@ onUnmounted(() => { .input-wrapper { display: flex; align-items: center; - justify-content: center; + justify-content: flex-start; flex-wrap: nowrap; gap: 8px; - border-radius: 12px; + border-radius: 14px; min-height: 40px; - padding: 4px 8px; - background: var(--preset-input-wrapper-bg, rgba(255, 255, 255, 0.04)); - border: 1px solid var(--preset-input-wrapper-border, rgba(255, 255, 255, 0.1)); + padding: 4px 8px 4px 10px; + background: color-mix(in srgb, var(--preset-input-wrapper-bg, rgba(255, 255, 255, 0.04)) 88%, var(--preset-chat-surface-bg, #111a27)); + border: 1px solid color-mix(in srgb, var(--preset-input-wrapper-border, rgba(255, 255, 255, 0.1)) 92%, transparent); transition: all 180ms ease; &.focused { @@ -946,145 +1454,65 @@ onUnmounted(() => { } } -.emoji-btn { - width: 30px; - height: 30px; - min-width: 30px; - border-radius: 999px; - border: none; - display: inline-flex; - align-items: center; - justify-content: center; - align-self: center; - color: var(--preset-emoji-btn-color); - background: transparent; - cursor: pointer; - - &:hover, - &.active { - color: var(--preset-emoji-btn-active); - background: var(--preset-emoji-btn-hover-bg); - } - - svg { - width: 20px; - height: 20px; - } +:deep(.biz-barrage-input) { + flex: 1; + min-width: 0; } -.chat-text-input { - flex: 1; +:deep(.biz-barrage-input .live-message-input) { width: 100%; - min-height: 22px; - max-height: 96px; - border: none; - outline: none; - background: transparent; - color: var(--preset-chat-input-text); - font-size: 14px; - line-height: 22px; - resize: none; - overflow-y: hidden; - padding: 0; - margin: 0; - display: block; - align-self: center; - font-family: inherit; - - &::placeholder { - color: var(--preset-chat-input-placeholder); - font-size: 14px; - } } -.send-btn { - width: 30px; - height: 30px; - border: none; - border-radius: 8px; - min-width: 30px; +:deep(.biz-barrage-input .message-input-container) { + height: 32px; + min-height: 32px; + max-height: 32px; padding: 0; - flex-shrink: 0; - align-self: center; - color: var(--preset-send-btn-active-text, rgba(255, 255, 255, 0.88)); - background: var(--preset-send-btn-inactive, rgba(255, 255, 255, 0.08)); - cursor: pointer; - transition: all 180ms ease; - display: inline-flex; + border: none; + border-radius: 0; + background: transparent; + overflow: hidden; + display: flex; align-items: center; - justify-content: center; - - svg { - width: 17px; - height: 17px; - fill: currentColor; - } - - &:disabled { - cursor: not-allowed; - opacity: 0.52; - } - - &.active { - background: var(--preset-send-btn-active-bg, var(--preset-primary, #1c66e5)); - box-shadow: 0 0 0 2px var(--preset-send-btn-active-ring, rgba(28, 102, 229, 0.25)); - - &:active { - transform: scale(0.94); - } - } } -/* ── Emoji Picker ── */ - -.emoji-picker-panel { - position: absolute; - bottom: calc(100% + 6px); - left: 12px; - right: 12px; - padding: 8px; - border-radius: 12px; - background: var(--preset-emoji-panel-bg); - border: 1px solid var(--preset-emoji-panel-border); - box-shadow: var(--preset-emoji-panel-shadow); - z-index: 20; +:deep(.biz-barrage-input .input-wrapper) { + align-items: center; } -.emoji-picker-grid { - display: grid; - grid-template-columns: repeat(8, 1fr); - gap: 2px; +:deep(.biz-barrage-input .input-prefix) { + display: flex; + align-items: center; + align-self: center; } -.emoji-item { - width: 100%; - aspect-ratio: 1; - border-radius: 8px; - border: none; - background: transparent; - font-size: 22px; - cursor: pointer; +:deep(.biz-barrage-input .input-actions) { + height: 32px; display: flex; align-items: center; - justify-content: center; - - &:hover { - background: var(--preset-emoji-hover-bg); - } + margin-right: 8px; + gap: 0; } -.emoji-pop-enter-active { - transition: opacity 0.18s ease, transform 0.18s ease; +:deep(.biz-barrage-input .emoji-picker__icon) { + width: 16px; + height: 16px; + color: var(--preset-emoji-btn-color); } -.emoji-pop-leave-active { - transition: opacity 0.12s ease, transform 0.12s ease; -} +:deep(.biz-barrage-input .tiptap.ProseMirror) { + flex: 1; + min-height: 22px; + max-height: 72px; + color: var(--preset-chat-input-text); + font-size: 14px; + line-height: 22px; + font-family: inherit; -.emoji-pop-enter-from, -.emoji-pop-leave-to { - opacity: 0; - transform: translateY(6px) scale(0.97); + p.is-editor-empty:first-child::before { + color: var(--preset-chat-input-placeholder); + font-size: 14px; + } } /* ── Audience Panel ── */ @@ -1101,8 +1529,6 @@ onUnmounted(() => { min-height: 0; overflow-y: auto; padding: 10px 10px 16px; - scrollbar-width: thin; - scrollbar-color: var(--preset-list-scrollbar) transparent; } .audience-group { @@ -1261,3 +1687,119 @@ onUnmounted(() => { transform: translateX(12px); } + + diff --git a/Web/web-vite-vue3/src/business/components/BusinessToolbar.vue b/Web/web-vite-vue3/src/business/components/BusinessToolbar.vue index 9a189321..80b70e2b 100644 --- a/Web/web-vite-vue3/src/business/components/BusinessToolbar.vue +++ b/Web/web-vite-vue3/src/business/components/BusinessToolbar.vue @@ -16,12 +16,12 @@ diff --git a/Web/web-vite-vue3/src/business/styles/business.scss b/Web/web-vite-vue3/src/business/styles/business.scss index 74c70b14..15c7bae9 100644 --- a/Web/web-vite-vue3/src/business/styles/business.scss +++ b/Web/web-vite-vue3/src/business/styles/business.scss @@ -70,8 +70,8 @@ .header-left { .header-left-logo { opacity: 0.9; - width: 22px; - height: 20px; + width: 26px; + height: 24px; } .header-left-title { diff --git a/Web/web-vite-vue3/src/i18n/en-US/index.ts b/Web/web-vite-vue3/src/i18n/en-US/index.ts index 0620f014..0ab8ece1 100644 --- a/Web/web-vite-vue3/src/i18n/en-US/index.ts +++ b/Web/web-vite-vue3/src/i18n/en-US/index.ts @@ -31,4 +31,5 @@ export const resource = { 'The number of members in this room has reached the limit': 'The number of members in this room has reached the limit of the payment, please go to the Tencent Cloud Console to purchase or upgrade your plan.', 'The number of rooms has reached the limit of the payment': 'The number of rooms has reached the limit of the payment, please go to the Tencent Cloud Console to purchase or upgrade your plan.', 'Room is not existed.': 'Room is not existed, please check the room ID.', + 'Not allowed to refresh in picture-in-picture mode': 'Not allowed to refresh in picture-in-picture mode', }; diff --git a/Web/web-vite-vue3/src/i18n/zh-CN/index.ts b/Web/web-vite-vue3/src/i18n/zh-CN/index.ts index a924863a..c7ff25b2 100644 --- a/Web/web-vite-vue3/src/i18n/zh-CN/index.ts +++ b/Web/web-vite-vue3/src/i18n/zh-CN/index.ts @@ -31,4 +31,5 @@ export const resource = { 'The number of members in this room has reached the limit': '当前房间人数已达到套餐上限,请前往腾讯云控制台购买或者升级套餐包。', 'The number of rooms has reached the limit of the payment': '当前房间数量已达到套餐上限,请前往腾讯云控制台购买或者升级套餐包。', 'Room is not existed.': '房间不存在,请检查房间ID。', + 'Not allowed to refresh in picture-in-picture mode': '画中画模式下不允许刷新操作', }; diff --git a/Web/web-vite-vue3/src/views/live-player.vue b/Web/web-vite-vue3/src/views/live-player.vue index 9d60e066..70db9284 100644 --- a/Web/web-vite-vue3/src/views/live-player.vue +++ b/Web/web-vite-vue3/src/views/live-player.vue @@ -38,7 +38,7 @@ function leaveLive() { .live-player-header { flex-shrink: 0; - padding-bottom: 0; + padding-bottom: 16px; } } diff --git a/Web/web-vite-vue3/upload-server/src/config/index.js b/Web/web-vite-vue3/upload-server/src/config/index.js index ded500f7..92abe664 100644 --- a/Web/web-vite-vue3/upload-server/src/config/index.js +++ b/Web/web-vite-vue3/upload-server/src/config/index.js @@ -4,7 +4,7 @@ const dotenv = require('dotenv'); dotenv.config({ path: path.resolve(__dirname, '../../.env') }); const DEFAULT_HOST = '0.0.0.0'; -const DEFAULT_PORT = 9000; +const DEFAULT_PORT = 3071; function parsePort(value, fallback) { const parsed = Number(value); diff --git a/Web/web-vite-vue3/upload-server/src/routes/uploadRouter.js b/Web/web-vite-vue3/upload-server/src/routes/uploadRouter.js index 0ade0bb6..51fb77a7 100644 --- a/Web/web-vite-vue3/upload-server/src/routes/uploadRouter.js +++ b/Web/web-vite-vue3/upload-server/src/routes/uploadRouter.js @@ -10,11 +10,14 @@ const { asyncHandler } = require('../middleware/asyncHandler'); const logger = require('../utils/logger'); const MAX_FILE_SIZE_MB = 2; -const ALLOWED_MIME_TYPES = [ +const COVER_ALLOWED_MIME_TYPES = [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp', +]; +const ALLOWED_MIME_TYPES = [ + ...COVER_ALLOWED_MIME_TYPES, 'image/svg+xml', ]; const ALLOWED_UPLOAD_TYPES = [ @@ -22,17 +25,43 @@ const ALLOWED_UPLOAD_TYPES = [ 'gift-icon', 'gift-animation', ]; +const ALLOWED_MIME_TYPES_BY_UPLOAD_TYPE = { + cover: COVER_ALLOWED_MIME_TYPES, + 'gift-icon': ALLOWED_MIME_TYPES, + 'gift-animation': ALLOWED_MIME_TYPES, +}; + +function resolveUploadType(type) { + if (!type || !ALLOWED_UPLOAD_TYPES.includes(type)) { + return null; + } + return type; +} + +function getAllowedMimeTypesByType(type) { + return ALLOWED_MIME_TYPES_BY_UPLOAD_TYPE[type] || COVER_ALLOWED_MIME_TYPES; +} + +function getUnsupportedMimeMessage(type) { + if (type === 'cover') { + return 'Only JPG/PNG/GIF/WebP images are supported for cover'; + } + return 'Only JPG/PNG/GIF/WebP/SVG images are supported'; +} const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: MAX_FILE_SIZE_MB * 1024 * 1024 }, - fileFilter: (_req, file, callback) => { - if (ALLOWED_MIME_TYPES.includes(file.mimetype)) { + fileFilter: (req, file, callback) => { + const uploadType = resolveUploadType(req.body?.type); + const allowedMimeTypes = uploadType ? getAllowedMimeTypesByType(uploadType) : ALLOWED_MIME_TYPES; + if (allowedMimeTypes.includes(file.mimetype)) { callback(null, true); return; } - const error = new Error('Only JPG/PNG/GIF/WebP/SVG images are supported'); + const error = new Error(getUnsupportedMimeMessage(uploadType)); logger.warn('UPLOAD_FILE_FILTER', error.message, { + type: uploadType || req.body?.type, mimetype: file.mimetype, originalname: file.originalname, }); @@ -105,7 +134,27 @@ uploadRouter.post( return; } - const type = ALLOWED_UPLOAD_TYPES.includes(req.body.type) ? req.body.type : 'cover'; + const type = resolveUploadType(req.body?.type); + if (!type) { + const message = 'Missing or invalid upload type'; + logger.warn('UPLOAD_TYPE_INVALID', message, { + type: req.body?.type, + originalname: req.file.originalname, + }); + res.status(400).json({ code: -1, message }); + return; + } + const allowedMimeTypes = getAllowedMimeTypesByType(type); + if (!allowedMimeTypes.includes(req.file.mimetype)) { + const message = getUnsupportedMimeMessage(type); + logger.warn('UPLOAD_MIME_TYPE_NOT_ALLOWED', message, { + type, + mimetype: req.file.mimetype, + originalname: req.file.originalname, + }); + res.status(400).json({ code: -1, message }); + return; + } const key = generateKey(type, req.file.originalname); const result = await uploadFile(req.file.buffer, key, req.file.mimetype); diff --git a/Web/web-vite-vue3/vite.config.ts b/Web/web-vite-vue3/vite.config.ts index f8e851e1..43551e7f 100644 --- a/Web/web-vite-vue3/vite.config.ts +++ b/Web/web-vite-vue3/vite.config.ts @@ -9,6 +9,9 @@ const path = require('path'); export default defineConfig({ // Static Resource Base Path base: './' || '', base: process.env.NODE_ENV === 'production' ? './' : '/', + define: { + __STYLE_PRESET__: JSON.stringify(process.env.STYLE_PRESET || ''), + }, // Allow Vue CLI-style env names so Web and Electron can share upload config naming. envPrefix: ['VITE_', 'VUE_APP_'], resolve: {