diff --git a/packages/tuikit-atomicx-vue3-electron/package.json b/packages/tuikit-atomicx-vue3-electron/package.json
index 7dd5c9e..17ba3ad 100644
--- a/packages/tuikit-atomicx-vue3-electron/package.json
+++ b/packages/tuikit-atomicx-vue3-electron/package.json
@@ -1,6 +1,6 @@
{
"name": "tuikit-atomicx-vue3-electron",
- "version": "5.10.0",
+ "version": "6.3.0",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -40,9 +40,9 @@
"@tencentcloud/lite-chat": "^1.6.4",
"@tencentcloud/chat-uikit-engine-lite": "~1.0.7",
"@tencentcloud/tui-core-lite": "~1.0.1",
- "@tencentcloud/tuiroom-engine-electron": "~4.1.1",
- "@tencentcloud/uikit-base-component-vue3": "~1.4.3",
- "trtc-electron-sdk": "13.3.801-alpha.3",
+ "@tencentcloud/tuiroom-engine-electron": "~4.2.0",
+ "@tencentcloud/uikit-base-component-vue3": "~1.4.5",
+ "trtc-electron-sdk": "13.3.802-beta.1",
"vue": "^3.4.0"
},
"dependencies": {
diff --git a/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/BattlePanel.vue b/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/BattlePanel.vue
index 24693ff..cd4d6cd 100644
--- a/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/BattlePanel.vue
+++ b/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/BattlePanel.vue
@@ -15,6 +15,7 @@
v-if="!isUserInvited(user.userId, user.liveId)"
size="small"
type="primary"
+ :disabled="pendingInviteLiveIds.has(user.liveId) || hasPendingConnectionInvite"
@click="handleSendBattleRequest(user)"
>
{{ t('Invite battle') }}
@@ -30,93 +31,430 @@
-
-
- {{ t('Are you sure you want to exit the battle') }}
-
-
- {{ t('Cancel') }}
-
-
- {{ t('End battle') }}
-
-
-
-
+
+
-
-
-->
+
+
+
diff --git a/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/ConfigSettingPanel.vue b/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/ConfigSettingPanel.vue
index 1761432..d762504 100644
--- a/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/ConfigSettingPanel.vue
+++ b/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/ConfigSettingPanel.vue
@@ -2,9 +2,9 @@
@@ -68,7 +68,7 @@
diff --git a/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/RecommendHostList.vue b/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/RecommendHostList.vue
index d200b6b..93e0916 100644
--- a/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/RecommendHostList.vue
+++ b/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/RecommendHostList.vue
@@ -7,7 +7,7 @@
@@ -55,13 +55,33 @@ import { IconRefresh, useUIKit, TUIToast, TOAST_TYPE } from '@tencentcloud/uikit
import { TUIErrorCode } from '@tencentcloud/tuiroom-engine-electron';
import { ref, onMounted, onUnmounted, computed } from 'vue';
import { useCoHostState } from '../../states/CoHostState';
-import { useLiveListState } from '../../states/LiveListState';
import { Avatar } from '../Avatar';
import { CoHostStatus } from '../../types';
const { t } = useUIKit();
-const { liveListCursor, fetchLiveList } = useLiveListState();
-const { coHostStatus, invitees, candidates } = useCoHostState();
+const { coHostStatus, connected, invitees, candidates, candidatesCursor, getCoHostCandidates } = useCoHostState();
+
+/**
+ * Merge invitees and candidates into a unified list for rendering.
+ * - Exclude users that are already connected (they should not appear in "Invite more").
+ * - De-duplicate by `${userId}-${liveId}` so the same user never appears twice
+ * when they exist in both `invitees` and `candidates`.
+ * Invitees come first to preserve the "already invited" visual order.
+ */
+const displayUserList = computed(() => {
+ const connectedKeys = new Set(connected.value.map(u => `${u.userId}-${u.liveId}`));
+ const seen = new Set
();
+ const result: typeof invitees.value = [];
+ for (const user of [...invitees.value, ...candidates.value]) {
+ const key = `${user.userId}-${user.liveId}`;
+ if (connectedKeys.has(key) || seen.has(key)) {
+ continue;
+ }
+ seen.add(key);
+ result.push(user);
+ }
+ return result;
+});
const recommendHostListContentRef = ref(null);
const loadMoreRef = ref(null);
@@ -69,7 +89,7 @@ let intersectionObserver: IntersectionObserver | null = null;
const refreshInviteesLoading = ref(false);
const loadMoreLoading = ref(false);
-const hasMoreLive = computed(() => liveListCursor.value !== '');
+const hasMoreLive = computed(() => candidatesCursor.value !== '');
async function handleRefreshInvitees() {
refreshInviteesLoading.value = true;
if (recommendHostListContentRef.value) {
@@ -77,10 +97,7 @@ async function handleRefreshInvitees() {
}
Promise.all([
new Promise((resolve) => setTimeout(resolve, 500)),
- fetchLiveList({
- cursor: '',
- count: 20,
- }),
+ getCoHostCandidates(''),
])
.catch(error => {
if (error.code === TUIErrorCode.ERR_FREQ_LIMIT) {
@@ -101,10 +118,7 @@ onMounted(() => {
if (item.isIntersecting && !loadMoreLoading.value && hasMoreLive.value) {
loadMoreLoading.value = true;
try {
- await fetchLiveList({
- cursor: liveListCursor.value,
- count: 20,
- });
+ await getCoHostCandidates(candidatesCursor.value);
} catch (error) {
console.error('Load more users failed:', error);
} finally {
diff --git a/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/battleAutoStart.ts b/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/battleAutoStart.ts
new file mode 100644
index 0000000..15ffbd9
--- /dev/null
+++ b/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/battleAutoStart.ts
@@ -0,0 +1,57 @@
+import { ref, computed } from 'vue';
+
+// ----------------------------------------------------------------------------
+// Shared "battle auto-start in progress" signal between BattlePanel and
+// ConnectionPanel.
+//
+// Background:
+// A PK initiated from the Battle tab is a two-phase flow. An "Invite battle"
+// first establishes a plain co-host connection (`withBattle: true`), and only
+// AFTER every accepter has actually connected does BattlePanel fire ONE
+// aggregated `requestBattle` (`needResponse: false`) to truly start the PK.
+// Between "connection established" and "onBattleStarted" the CoHostPanel
+// auto-switches to the Connection tab, whose footer shows a "Start battle"
+// button. Clicking it in that window issues a SECOND `requestBattle` for the
+// same round, which the SDK rejects as a duplicate and surfaces a
+// "Request battle failed" toast.
+//
+// Fix:
+// BattlePanel owns the auto-start lifecycle and keeps this flag in sync at
+// every transition of its round bookkeeping (invites pending, accepters
+// waiting to connect, the connection-ready safety timer armed, or the
+// aggregated requestBattle in flight). ConnectionPanel reads it to disable
+// its "Start battle" button (and to guard the handler) for the whole
+// auto-start window, so the two `requestBattle` paths can never collide.
+// Note: this intentionally does NOT block sending more "Invite battle"
+// invitations to other hosts — multiple hosts may be invited at once and
+// every accepter joins the same aggregated PK.
+//
+// Why module-level + reactive:
+// The CoHostPanel dialog uses an internal `v-if`, so both panels mount and
+// unmount together; a module-level Vue `ref` is the single source of truth
+// that survives those cycles and is shared across both panels. Mirrors the
+// same pattern used by `inviteMutex.ts`.
+//
+// Mirrors the identical module in
+// `uikit-component-vue3/.../battleAutoStart.ts` to keep the three-end
+// (Web kit / Mac kit / Win demo) PK invite flow behavior aligned.
+// ----------------------------------------------------------------------------
+
+const battleAutoStartInProgress = ref(false);
+
+// Set by BattlePanel at every transition of its auto-start bookkeeping.
+export const setBattleAutoStartInProgress = (value: boolean) => {
+ battleAutoStartInProgress.value = value;
+};
+
+// Defensive reset, called when the local host leaves the co-host connection or
+// the live room (and when a battle starts/ends), so a stale flag can never
+// leave "Start battle" disabled into the next session.
+export const resetBattleAutoStart = () => {
+ battleAutoStartInProgress.value = false;
+};
+
+export const useBattleAutoStart = () => {
+ const isBattleAutoStartInProgress = computed(() => battleAutoStartInProgress.value);
+ return { isBattleAutoStartInProgress };
+};
diff --git a/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/constants.ts b/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/constants.ts
index 2a67d0e..1d8488c 100644
--- a/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/constants.ts
+++ b/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/constants.ts
@@ -1,3 +1,18 @@
export const ERROR_MESSAGE = {
100412: 'there is no one valid room for battle',
};
+
+/**
+ * Default timeout (in seconds) for outbound co-host connection invitations
+ * dispatched via `requestHostConnection`. The same value is mirrored into
+ * `extensionInfo` so the invitee can render a matching countdown.
+ */
+export const COHOST_REQUEST_TIMEOUT_SECONDS = 30;
+
+/**
+ * Default timeout (in seconds) for outbound battle (PK) invitations
+ * dispatched via `requestBattle`. Kept as a separate constant from
+ * `COHOST_REQUEST_TIMEOUT_SECONDS` so the two flows can diverge later
+ * without affecting each other.
+ */
+export const BATTLE_REQUEST_TIMEOUT_SECONDS = 30;
diff --git a/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/i18n/en-US/index.ts b/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/i18n/en-US/index.ts
index 3007f95..4884c0b 100644
--- a/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/i18n/en-US/index.ts
+++ b/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/i18n/en-US/index.ts
@@ -1,7 +1,5 @@
export const resource = {
'Host Battle': 'Host Battle',
- 'Host Connection': 'Host Connection',
- 'CoHost': 'Co-host',
'Current seat': 'Current seat',
'Seat is empty': 'Seat is empty',
'Invite more': 'Invite more',
@@ -16,6 +14,10 @@ export const resource = {
'Exit connection': 'Exit connection',
'No hosts available to invite': 'No hosts available to invite',
'Disconnect': 'Disconnect',
+ // Co-host audio mute toggle. Dedicated keys so they never collide with the
+ // global 'Mute'/'Unmute' used by the audience chat-ban menu.
+ 'Mute Audio': 'Mute Audio',
+ 'Unmute Audio': 'Unmute Audio',
'Cancel': 'Cancel',
'Invite': 'Invite',
'Inviting': 'Inviting',
@@ -30,8 +32,9 @@ export const resource = {
'Are you sure you want to exit the connection': 'Are you sure you want to exit the connection?',
'Layout Template': 'Layout Template',
'Dynamic Grid Layout': 'Dynamic Grid Layout',
- 'Dynamic Grid9 Layout': 'Dynamic Grid9 Layout',
+ 'Dynamic Grid9 Layout': 'Dynamic Grid Layout',
'Dynamic 1v6 Layout': 'Dynamic 1v6 Layout',
+ 'Landscape Fixed 2 Seats Layout': 'Landscape Fixed 2 Seats Layout',
// Toast messages with specific error codes
'Send co-host request failed, Room not exist': 'Send co-host request failed, Room does not exist',
'Send co-host request failed, Room is connecting': 'Send co-host request failed, Room is connecting',
@@ -41,8 +44,6 @@ export const resource = {
'Co-host invitation sent to user': 'Co-host invitation sent',
'Co-host invitation cancelled for user': 'Co-host invitation cancelled',
'Co-host request cancelled by user': 'Co-host request cancelled',
- 'Co-host request rejected by user': 'Co-host request rejected',
- 'Co-host request timeout for user': 'Co-host request timeout',
'Co-host user joined event': 'Co-host user joined',
'Co-host user left event': '{{ userName }} Co-host user left',
// Battle Panel content
@@ -72,8 +73,6 @@ export const resource = {
'Send battle request failed': 'Send battle request failed',
'Battle invitation sent to user': 'PK invitation has been sent to {{ userName }}',
'Battle request cancelled by user': '{{ userName }} canceled the PK request',
- 'Battle request rejected by user': '{{ userName }} rejected the PK request',
- 'Battle request timeout for user': 'PK request timeout for {{ userName }}',
'Anchor battle settings': 'Anchor battle settings',
'Battle duration': 'Battle duration',
'Number minutes': '{{number}} minutes',
diff --git a/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/i18n/zh-CN/index.ts b/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/i18n/zh-CN/index.ts
index 328a182..ed6cd4b 100644
--- a/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/i18n/zh-CN/index.ts
+++ b/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/i18n/zh-CN/index.ts
@@ -1,7 +1,6 @@
export const resource = {
'Host Battle': '主播 PK',
'Host Connection': '主播连线',
- 'CoHost': '连主播',
'No application for co-host': '暂无连主播申请',
'Current seat': '当前麦位',
'Seat is empty': '麦位为空',
@@ -17,6 +16,10 @@ export const resource = {
'Start battle': '发起 PK',
'Exit connection': '退出连线',
'Disconnect': '断开连接',
+ // Co-host audio mute toggle. Dedicated keys so they never collide with the
+ // global 'Mute'/'Unmute' used by the audience chat-ban menu.
+ 'Mute Audio': '静音',
+ 'Unmute Audio': '取消静音',
'Cancel': '取消',
'Invite': '邀请',
'Inviting': '邀请中',
@@ -31,8 +34,9 @@ export const resource = {
'Are you sure you want to exit the connection': '确定要退出连线吗?',
'Layout Template': '布局模板',
'Dynamic Grid Layout': '动态网格布局',
- 'Dynamic Grid9 Layout': '动态宫格布局',
+ 'Dynamic Grid9 Layout': '动态九宫格布局',
'Dynamic 1v6 Layout': '动态1v6布局',
+ 'Landscape Fixed 2 Seats Layout': '横屏 2 人固定布局',
// Toast messages with specific error codes
'Send co-host request failed, Room not exist': '发起连线失败,对方主播房间不存在',
'Send co-host request failed, Room is connecting': '发起连线失败,对方主播正在建立连线中',
@@ -41,8 +45,6 @@ export const resource = {
// Success messages with parameters - using simple strings for now
'Co-host invitation sent to user': '已向{{ userName }}发送连线邀请',
'Co-host request cancelled by user': '{{ userName }}取消了连线请求',
- 'Co-host request rejected by user': '{{ userName }}已拒绝连线',
- 'Co-host request timeout for user': '发给{{ userName }}的连线请求无应答',
'Co-host user joined event': '{{ userName }}已加入连线',
'Co-host user left event': '{{ userName }}已离开连线',
// Battle Panel content
@@ -72,9 +74,7 @@ export const resource = {
'Send battle request failed': '发起PK失败',
'Battle invitation sent to user': '已向{{ userName }}发送PK邀请',
'Battle request cancelled by user': '{{ userName }}取消了PK请求',
- 'Battle request rejected by user': '{{ userName }}已拒绝PK',
- 'Battle request timeout for user': '发给{{ userName }}的PK请求无应答',
- 'Anchor battle settings': '主播PK设置',
+ 'Anchor battle settings': '主播 PK 设置',
'Battle duration': '发起 PK 时长',
'Number minutes': '{{number}} 分钟',
'Connection Layout': '连线布局',
diff --git a/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/index.ts b/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/index.ts
index 5e0857e..d6ad132 100644
--- a/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/index.ts
+++ b/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/index.ts
@@ -1,7 +1,7 @@
-// import CoHostPanelPC from './CoHostPanel.vue';
-// import { addI18n } from '../../i18n';
-// import { enResource, zhResource } from './i18n';
-// addI18n('en-US', { translation: enResource });
-// addI18n('zh-CN', { translation: zhResource });
-// const CoHostPanel = CoHostPanelPC;
-// export { CoHostPanel };
+import CoHostPanelPC from './CoHostPanel.vue';
+import { addI18n } from '../../i18n';
+import { enResource, zhResource } from './i18n';
+addI18n('en-US', { translation: enResource });
+addI18n('zh-CN', { translation: zhResource });
+const CoHostPanel = CoHostPanelPC;
+export { CoHostPanel };
diff --git a/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/inviteMutex.ts b/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/inviteMutex.ts
new file mode 100644
index 0000000..0e4606c
--- /dev/null
+++ b/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/inviteMutex.ts
@@ -0,0 +1,62 @@
+import { ref, computed, type Ref, type ComputedRef } from 'vue';
+import type { SeatUserInfo } from '../../types';
+
+// ----------------------------------------------------------------------------
+// Mutual exclusion between the two CoHost invite kinds: "Invite battle" (PK)
+// and "Invite connection" (plain co-host).
+//
+// Background:
+// Both invites are issued through the SAME SDK call (`requestHostConnection`)
+// and are only distinguished by `extensionInfo.withBattle`. Both pending
+// invites land in the SAME authoritative, reactive `invitees` list, which
+// does NOT carry the `withBattle` flag. So the invite kind cannot be told
+// apart from `invitees` alone and must be recorded separately.
+//
+// Rule:
+// Once a PK invite is outstanding, ALL "Invite connection" buttons are
+// disabled (but more PK invites may still be sent), and vice-versa. The two
+// kinds are mutually exclusive globally; the same kind may be sent multiple
+// times. When every invite of a kind settles (accept / reject / cancel /
+// timeout / join), `invitees` empties out and the mutual exclusion lifts
+// automatically.
+//
+// Why module-level + reactive:
+// The parent CoHostPanel dialog mounts BattlePanel and ConnectionPanel at the
+// same time but uses an internal `v-if`, so both tabs must share one source
+// of truth that also survives the dialog's mount/unmount cycles. A
+// module-level Vue `ref` satisfies both.
+//
+// Mirrors `uikit-component-vue3/.../CoHostPanel/inviteMutex.ts` to keep the
+// three-end (Web kit / Mac kit / Win demo) invite behavior aligned.
+// ----------------------------------------------------------------------------
+
+export type InviteType = 'battle' | 'connection';
+
+// Module-level reactive map: liveId -> the invite kind last sent to it.
+// Created once on module evaluation and shared by every panel instance.
+const inviteTypeByLiveId = ref