feat: live module and jenkins pipeline to harbor deploy
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<div class="live-page">
|
||||
<div class="kra-search-box">
|
||||
<el-form :inline="true" :model="query" class="live-search-form" @submit.prevent="search">
|
||||
<el-form-item label="直播间"><el-input v-model="query.name" clearable placeholder="名称或房间号" /></el-form-item>
|
||||
<el-form-item label="主播"><el-input v-model="query.anchorName" clearable placeholder="主播昵称" /></el-form-item>
|
||||
<el-form-item label="分类"><el-select v-model="query.categoryId" clearable placeholder="全部分类"><el-option v-for="item in categories" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item>
|
||||
<el-form-item label="直播状态"><el-select v-model="query.status" clearable placeholder="全部状态"><el-option label="直播中" value="live" /><el-option label="未开播" value="offline" /><el-option label="已禁用" value="disabled" /></el-select></el-form-item>
|
||||
<el-form-item class="live-search-actions"><el-button type="primary" :icon="Search" @click="search">查询</el-button><el-button :icon="Refresh" @click="reset">重置</el-button></el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="kra-table-box live-table-box">
|
||||
<div class="kra-btn-list">
|
||||
<el-button type="primary" :icon="Plus" @click="openCreate">新建直播间</el-button>
|
||||
<el-button :icon="Sort" @click="persistSort">保存排序</el-button>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="rows" row-key="id">
|
||||
<el-table-column label="排序号" min-width="78" align="center"><template #default="{ row }"><el-input-number v-model="row.sort" class="live-sort-input" :min="0" :max="9999" controls-position="right" size="small" /></template></el-table-column>
|
||||
<el-table-column label="房间编号" prop="id" min-width="82" align="right" />
|
||||
<el-table-column label="直播间名称" min-width="170" show-overflow-tooltip><template #default="{ row }"><strong>{{ row.name }}</strong></template></el-table-column>
|
||||
<el-table-column v-if="!compactTable" label="封面" min-width="88"><template #default="{ row }"><el-image class="live-cover" :src="getUrl(row.coverUrl)" fit="cover"><template #error><div class="image-slot">暂无</div></template></el-image></template></el-table-column>
|
||||
<el-table-column label="所属分类" prop="categoryName" min-width="110" show-overflow-tooltip />
|
||||
<el-table-column label="主播" min-width="130"><template #default="{ row }"><div class="anchor-cell"><el-avatar :size="30" :src="getUrl(row.anchorAvatar)">{{ row.anchorName?.slice(0, 1) }}</el-avatar><span>{{ row.anchorName || `用户 ${row.anchorUserId}` }}</span></div></template></el-table-column>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<el-drawer v-model="drawer" destroy-on-close size="640px" :show-close="false">
|
||||
<template #header><div class="live-drawer-header"><strong>{{ editingId ? '编辑直播间' : '新建直播间' }}</strong><div><el-button @click="drawer = false">取消</el-button><el-button type="primary" :loading="saving" @click="submit">保存</el-button></div></div></template>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" class="live-drawer-form" label-width="110px">
|
||||
<div class="live-form-section">基础信息</div>
|
||||
<el-form-item label="直播间名称" prop="name"><el-input v-model="form.name" maxlength="40" show-word-limit /></el-form-item>
|
||||
<el-form-item label="展示排序"><el-input-number v-model="form.sort" :min="0" :max="9999" /></el-form-item>
|
||||
<el-form-item label="直播分类" prop="categoryId"><el-select v-model="form.categoryId" :disabled="editingStatus === 'live'" placeholder="选择分类"><el-option v-for="item in categories" :key="item.id" :label="item.name" :value="item.id" /></el-select><div v-if="editingStatus === 'live'" class="live-muted">直播过程中不能更换分类</div></el-form-item>
|
||||
<el-form-item label="主播" prop="anchorUserId"><el-select v-model="form.anchorUserId" filterable placeholder="选择 Kra 用户"><el-option v-for="item in users" :key="item.userId" :label="`${item.nickname || item.username} (#${item.userId})`" :value="item.userId" /></el-select></el-form-item>
|
||||
<el-form-item label="直播封面" prop="coverUrl"><ImageUploader v-model="form.coverUrl" :max-mb="5" /></el-form-item>
|
||||
<el-form-item label="直播简介"><el-input v-model="form.intro" type="textarea" :rows="4" maxlength="500" show-word-limit /></el-form-item>
|
||||
<div class="live-form-section">播放与状态</div>
|
||||
<el-form-item label="外部直播流地址"><el-input v-model="form.streamUrl" placeholder="可选,填写后观看端优先播放此地址" /></el-form-item>
|
||||
<el-form-item label="回放视频"><VideoUploader v-model="form.replayUrl" /></el-form-item>
|
||||
<el-form-item label="启用直播间"><el-switch v-model="form.enabled" /></el-form-item>
|
||||
</el-form>
|
||||
</el-drawer>
|
||||
</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 { 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'
|
||||
|
||||
defineOptions({ name: 'LiveRooms' })
|
||||
const query = reactive({ name: '', anchorName: '', categoryId: undefined, status: '' })
|
||||
const rows = ref([]), categories = ref([]), users = ref([]), loading = ref(false), saving = ref(false), drawer = ref(false), formRef = ref()
|
||||
const { width } = useWindowSize()
|
||||
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 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')
|
||||
const format = (value) => value ? new Date(value).toLocaleString('zh-CN') : '-'
|
||||
const load = async () => { loading.value = true; try { const res = await getRooms({ page: page.value, pageSize: pageSize.value, ...query }); if (res.code === 0) { rows.value = res.data.list || []; total.value = res.data.total || 0 } } finally { loading.value = false } }
|
||||
const loadOptions = async () => { const [c, u] = await Promise.all([getCategories({ page: 1, pageSize: 100, orderBy: 'sort_asc' }), getAssetUsers({ page: 1, pageSize: 100, orderBy: 'created_desc' })]); if (c.code === 0) categories.value = c.data.list || []; if (u.code === 0) users.value = u.data.list || [] }
|
||||
const search = () => { page.value = 1; load() }
|
||||
const reset = () => { Object.assign(query, { name: '', anchorName: '', categoryId: undefined, status: '' }); 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 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 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)
|
||||
onMounted(() => { loadOptions(); load() })
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@use '../styles/live.scss';
|
||||
.room-name { display: grid; min-width: 0; gap: 5px; }.room-name strong { overflow: hidden; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }.room-name span { overflow: hidden; color: var(--el-text-color-secondary); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.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); }
|
||||
</style>
|
||||
Reference in New Issue
Block a user