This commit is contained in:
Yvan
2026-09-22 20:20:23 +08:00
parent bf33e87d72
commit ceaea78b0f
42 changed files with 2845 additions and 154 deletions
+58 -7
View File
@@ -25,7 +25,7 @@
<el-table-column label="状态" min-width="82"><template #default="{ row }"><el-tag :type="statusType(row.status)" effect="light">{{ statusText(row.status) }}</el-tag></template></el-table-column>
<el-table-column v-if="!compactTable" label="观看人数" min-width="82" prop="viewerCount" align="right" />
<el-table-column v-if="!compactTable" label="创建时间" min-width="150"><template #default="{ row }">{{ format(row.createdAt) }}</template></el-table-column>
<el-table-column label="操作" width="260" fixed="right"><template #default="{ row }"><el-button link type="primary" :icon="VideoCamera" @click="openStudio(row)">开播</el-button><el-button link type="primary" :icon="Connection" @click="openDiagnostics(row)">测试</el-button><el-button link type="primary" :icon="Edit" @click="openEdit(row)">编辑</el-button><el-dropdown trigger="click" @command="handleRoomAction($event, row)"><el-button link type="primary" :icon="MoreFilled" title="更多操作" aria-label="更多操作" /><template #dropdown><el-dropdown-menu><el-dropdown-item command="share" :icon="Share">复制分享链接</el-dropdown-item><el-dropdown-item command="delete" :icon="Delete" divided class="room-delete-action">删除直播间</el-dropdown-item></el-dropdown-menu></template></el-dropdown></template></el-table-column>
<el-table-column label="操作" width="260" fixed="right"><template #default="{ row }"><el-button link type="primary" :icon="VideoCamera" @click="openStudio(row)">开播</el-button><el-button link type="primary" :icon="Connection" @click="openDiagnostics(row)">测试</el-button><el-button link type="primary" :icon="Edit" @click="openEdit(row)">编辑</el-button><el-dropdown trigger="click" @command="handleRoomAction($event, row)"><el-button link type="primary" :icon="MoreFilled" title="更多操作" aria-label="更多操作" /><template #dropdown><el-dropdown-menu><el-dropdown-item command="studio" :icon="CopyDocument">复制主播链接</el-dropdown-item><el-dropdown-item command="viewer" :icon="Share">复制观众链接</el-dropdown-item><el-dropdown-item command="links" :icon="Connection">查看链接</el-dropdown-item><el-dropdown-item command="delete" :icon="Delete" divided class="room-delete-action">删除直播间</el-dropdown-item></el-dropdown-menu></template></el-dropdown></template></el-table-column>
</el-table>
<div class="kra-pagination"><el-pagination v-model:current-page="page" v-model:page-size="pageSize" :total="total" :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper" @change="load" /></div>
</div>
@@ -46,18 +46,30 @@
<el-form-item label="启用直播间"><el-switch v-model="form.enabled" /></el-form-item>
</el-form>
</el-drawer>
<el-dialog v-model="linksDialog" title="直播间链接" width="min(640px, calc(100vw - 32px))">
<p class="room-links-name">{{ linkedRoom?.name }} <span>#{{ linkedRoom?.id }}</span></p>
<div v-for="item in linkItems" :key="item.key" class="room-link-row">
<label :for="`room-link-${item.key}`">{{ item.label }}</label>
<el-input :id="`room-link-${item.key}`" :model-value="roomLinks[item.key]" readonly @focus="$event.target.select()" />
<el-tooltip :content="`复制${item.label}`"><el-button :icon="CopyDocument" :aria-label="`复制${item.label}`" @click="copyLink(linkedRoom, item.key)" /></el-tooltip>
<el-tooltip :content="`打开${item.label}`"><el-button :icon="TopRight" :aria-label="`打开${item.label}`" @click="openLink(roomLinks[item.key])" /></el-tooltip>
</div>
<template #footer><el-button @click="linksDialog = false">关闭</el-button></template>
</el-dialog>
</div>
</template>
<script setup>
import { computed, onMounted, reactive, ref } from 'vue'
import { Connection, Delete, Edit, MoreFilled, Plus, Refresh, Search, Share, Sort, VideoCamera } from '@element-plus/icons-vue'
import { Connection, CopyDocument, Delete, Edit, MoreFilled, Plus, Refresh, Search, Share, Sort, TopRight, VideoCamera } from '@element-plus/icons-vue'
import { useWindowSize } from '@vueuse/core'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getUrl } from '@/utils/image'
import ImageUploader from '../components/ImageUploader.vue'
import VideoUploader from '../components/VideoUploader.vue'
import { createRoom, deleteRoom, getAssetUsers, getCategories, getRooms, saveRoomSort, updateRoom } from '../api/live'
import { getRoomLinks } from '../utils/roomLinks'
import { copyLiveText } from '../utils/clipboard'
defineOptions({ name: 'LiveRooms' })
const query = reactive({ name: '', anchorName: '', categoryId: undefined, status: '', orderBy: 'sort_asc' })
@@ -67,6 +79,8 @@ const compactTable = computed(() => width.value < 1180)
const page = ref(1), pageSize = ref(10), total = ref(0), editingId = ref(0), editingStatus = ref('')
const blank = () => ({ name: '', categoryId: undefined, coverUrl: '', intro: '', sort: 0, anchorUserId: undefined, streamUrl: '', replayUrl: '', enabled: true })
const form = reactive(blank())
const linksDialog = ref(false), linkedRoom = ref(null), roomLinks = ref({ viewer: '', studio: '' })
const linkItems = [{ key: 'studio', label: '主播链接' }, { key: 'viewer', label: '观众链接' }]
const rules = { name: [{ required: true, message: '请输入直播间名称', trigger: 'blur' }], categoryId: [{ required: true, message: '请选择分类', trigger: 'change' }], anchorUserId: [{ required: true, message: '请选择主播', trigger: 'change' }], coverUrl: [{ required: true, message: '请上传直播封面', trigger: 'change' }] }
const statusText = (value) => ({ live: '直播中', offline: '未开播', disabled: '已禁用' }[value] || value)
const statusType = (value) => ({ live: 'success', offline: 'info', disabled: 'danger' }[value] || 'info')
@@ -77,14 +91,48 @@ const search = () => { page.value = 1; load() }
const reset = () => { Object.assign(query, { name: '', anchorName: '', categoryId: undefined, status: '', orderBy: 'sort_asc' }); search() }
const openCreate = () => { editingId.value = 0; editingStatus.value = ''; Object.assign(form, blank()); drawer.value = true }
const openEdit = (row) => { editingId.value = row.id; editingStatus.value = row.status; Object.assign(form, { name: row.name, categoryId: row.categoryId, coverUrl: row.coverUrl, intro: row.intro, sort: row.sort, anchorUserId: row.anchorUserId, streamUrl: row.streamUrl, replayUrl: row.replayUrl, enabled: row.enabled }); drawer.value = true }
const submit = async () => { await formRef.value?.validate(); saving.value = true; try { const res = editingId.value ? await updateRoom(editingId.value, form) : await createRoom(form); if (res.code === 0) { ElMessage.success('直播间已保存'); drawer.value = false; load() } } finally { saving.value = false } }
const submit = async () => {
await formRef.value?.validate()
saving.value = true
try {
const res = editingId.value ? await updateRoom(editingId.value, form) : await createRoom(form)
if (res.code === 0) {
ElMessage.success('直播间已保存')
drawer.value = false
showLinks(res.data || { ...form, id: editingId.value })
load()
}
} finally { saving.value = false }
}
const removeRow = async (row) => { await ElMessageBox.confirm(`确认删除直播间“${row.name}”吗?`, '删除直播间', { type: 'warning' }); const res = await deleteRoom(row.id); if (res.code === 0) { ElMessage.success('已删除'); load() } }
const persistSort = async () => { const values = Object.fromEntries(rows.value.map((item) => [item.id, item.sort])); const res = await saveRoomSort(values); if (res.code === 0) { ElMessage.success('排序已保存'); load() } }
const shareUrl = (row, studio = false) => `${window.location.origin}${window.location.pathname}#/live/${studio ? 'studio' : 'room'}/${row.id}`
const copyLink = async (row) => { await navigator.clipboard.writeText(shareUrl(row)); ElMessage.success('观看链接已复制') }
const openStudio = (row) => window.open(shareUrl(row, true), '_blank')
const showLinks = (row) => {
try {
roomLinks.value = getRoomLinks(row.id)
linkedRoom.value = row
linksDialog.value = true
} catch { ElMessage.error('无法生成直播间链接,请检查 PC 站点地址配置') }
}
const copyLink = async (row, kind) => {
try {
await copyLiveText(getRoomLinks(row.id)[kind])
ElMessage.success(kind === 'studio' ? '主播链接已复制' : '观众链接已复制')
} catch {
showLinks(row)
ElMessage.warning('复制失败,请在链接窗口中选中地址复制')
}
}
const openLink = (url) => window.open(url, '_blank', 'noopener,noreferrer')
const openStudio = (row) => {
try { openLink(getRoomLinks(row.id).studio) }
catch { ElMessage.error('无法生成主播链接,请检查 PC 站点地址配置') }
}
const openDiagnostics = (row) => window.open(`${window.location.origin}${window.location.pathname}#/live/diagnostics/${row.id}`, '_blank', 'noopener')
const handleRoomAction = (command, row) => command === 'share' ? copyLink(row) : removeRow(row)
const handleRoomAction = (command, row) => {
if (command === 'studio' || command === 'viewer') return copyLink(row, command)
if (command === 'links') return showLinks(row)
if (command === 'delete') return removeRow(row)
}
onMounted(() => { loadOptions(); load() })
</script>
@@ -94,4 +142,7 @@ onMounted(() => { loadOptions(); load() })
.anchor-cell { display: flex; align-items: center; gap: 8px; min-width: 0; }.anchor-cell span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.image-slot { display: grid; width: 100%; height: 100%; place-items: center; color: var(--el-text-color-placeholder); font-size: 11px; }
.room-delete-action { color: var(--el-color-danger); }
.room-links-name { margin: 0 0 18px; overflow-wrap: anywhere; }.room-links-name span { margin-left: 6px; color: var(--el-text-color-secondary); }
.room-link-row { display: grid; grid-template-columns: 64px minmax(0, 1fr) 32px 32px; gap: 8px; align-items: center; margin: 12px 0; }.room-link-row .el-button { width: 32px; margin: 0; }.room-link-row label { font-size: 12px; }
@media (max-width: 480px) { .room-link-row { grid-template-columns: minmax(0, 1fr) 32px 32px; }.room-link-row label { grid-column: 1 / -1; } }
</style>