|
@@ -0,0 +1,345 @@
|
|
|
+<script setup lang='ts'>
|
|
|
+import { showFailToast, showSuccessToast } from 'vant'
|
|
|
+import QRCode from 'qrcode'
|
|
|
+import type { IAgoraRTCClient, ICameraVideoTrack, IMicrophoneAudioTrack } from 'agora-rtc-sdk-ng'
|
|
|
+import AgoraRTC from 'agora-rtc-sdk-ng'
|
|
|
+import { TypeMap, formatNumber, getFullPath, splitRtmpPath } from '~/pages/detail/util'
|
|
|
+import { user } from '~/store'
|
|
|
+
|
|
|
+const props = defineProps<{
|
|
|
+ // type: string
|
|
|
+ tmk_id: string
|
|
|
+ tmz_id: string
|
|
|
+}>()
|
|
|
+let truthPid = props.tmk_id === '-' ? undefined : props.tmk_id
|
|
|
+
|
|
|
+const _type_ = 'zbkc_ks'
|
|
|
+const theType = TypeMap[_type_]
|
|
|
+
|
|
|
+const router = useRouter()
|
|
|
+const route = useRoute()
|
|
|
+
|
|
|
+const showShare = ref(false)
|
|
|
+function openShare() {
|
|
|
+ showShare.value = true
|
|
|
+}
|
|
|
+
|
|
|
+const qrshow = ref(false)
|
|
|
+const qrsrc = ref('')
|
|
|
+function handleShareSelect(option: any) {
|
|
|
+ const link = location.href
|
|
|
+ switch (option.icon) {
|
|
|
+ case 'link':
|
|
|
+ navigator.clipboard.writeText(link).then(() => {
|
|
|
+ showShare.value = false
|
|
|
+ showSuccessToast('已复制到剪贴板')
|
|
|
+ }).catch(() => {
|
|
|
+ showFailToast('复制失败')
|
|
|
+ })
|
|
|
+
|
|
|
+ break
|
|
|
+ case 'qrcode':
|
|
|
+ QRCode.toDataURL(link, (err, url) => {
|
|
|
+ if (err)
|
|
|
+ showFailToast('生成二维码失败')
|
|
|
+ qrsrc.value = url
|
|
|
+ showShare.value = false
|
|
|
+ qrshow.value = true
|
|
|
+ })
|
|
|
+ break
|
|
|
+ default:
|
|
|
+ break
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const loading = ref(true)
|
|
|
+const detail = ref<any>()
|
|
|
+const rows = ref<any[]>([])
|
|
|
+
|
|
|
+const isRtcUser = ref(false)
|
|
|
+const isRtcing = ref(false)
|
|
|
+const rtcPlayerRef = ref()
|
|
|
+
|
|
|
+function fixTruthPid(pid: string) {
|
|
|
+ const routePid = truthPid
|
|
|
+ if (routePid !== pid) {
|
|
|
+ console.info('纠正pid', pid)
|
|
|
+ truthPid = pid
|
|
|
+ }
|
|
|
+ return truthPid
|
|
|
+}
|
|
|
+
|
|
|
+const rtc: {
|
|
|
+ localAudioTrack?: IMicrophoneAudioTrack
|
|
|
+ localVideoTrack?: ICameraVideoTrack
|
|
|
+ client?: IAgoraRTCClient
|
|
|
+} = {
|
|
|
+ localAudioTrack: undefined,
|
|
|
+ localVideoTrack: undefined,
|
|
|
+ client: undefined,
|
|
|
+}
|
|
|
+
|
|
|
+async function initRTC() {
|
|
|
+ const options = {
|
|
|
+ appId: detail.value.rtc_appid,
|
|
|
+ channel: detail.value.channelname,
|
|
|
+ token: detail.value.rtc_token,
|
|
|
+ uid: detail.value.uidStr,
|
|
|
+ }
|
|
|
+ console.log('rtc options : ', options)
|
|
|
+
|
|
|
+ console.group('AgoraRTC')
|
|
|
+ rtc.client = AgoraRTC.createClient({ mode: 'live', codec: 'vp8', role: 'host' })
|
|
|
+ // rtc.client.setClientRole('host')
|
|
|
+
|
|
|
+ await rtc.client.join(options.appId, options.channel, options?.token || null, options?.uid)
|
|
|
+ rtc.localAudioTrack = await AgoraRTC.createMicrophoneAudioTrack()
|
|
|
+ rtc.localVideoTrack = await AgoraRTC.createCameraVideoTrack({
|
|
|
+ encoderConfig: '1080p_1',
|
|
|
+ })
|
|
|
+ rtc.localVideoTrack.play(rtcPlayerRef.value, { mirror: false })
|
|
|
+
|
|
|
+ await rtc.client.publish([rtc.localAudioTrack, rtc.localVideoTrack])
|
|
|
+ console.groupEnd()
|
|
|
+ isRtcing.value = true
|
|
|
+}
|
|
|
+
|
|
|
+function init() {
|
|
|
+ loading.value = true
|
|
|
+ return request({
|
|
|
+ url: `/txwx/${_type_}/detail`,
|
|
|
+ data: {
|
|
|
+ [theType.id]: props.tmz_id,
|
|
|
+ live: 1,
|
|
|
+ },
|
|
|
+ }).then((res) => {
|
|
|
+ if (res?.code === '1') {
|
|
|
+ detail.value = res.data.one_info
|
|
|
+ isRtcUser.value = user.value?.user_id === detail.value.tzk_zjr_user_id
|
|
|
+ // isRtcUser.value = true
|
|
|
+ const otherData: any = {}
|
|
|
+ if (Array.isArray(theType.pid)) {
|
|
|
+ const t: string[] = []
|
|
|
+ theType.pid.forEach((item: string) => {
|
|
|
+ otherData[item] = detail.value[item]
|
|
|
+ t.push(detail.value[item])
|
|
|
+ })
|
|
|
+ fixTruthPid(t.join('-'))
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ otherData[theType.pid] = detail.value[theType.pid]
|
|
|
+ fixTruthPid(detail.value[theType.pid])
|
|
|
+ }
|
|
|
+ return request({
|
|
|
+ url: `/txwx/${_type_}/index`,
|
|
|
+ data: {
|
|
|
+ // [theType.pid]: props.tmk_id,
|
|
|
+ ...otherData,
|
|
|
+ limit: 4,
|
|
|
+ },
|
|
|
+ }).then((res) => {
|
|
|
+ if (res?.code === '1')
|
|
|
+ rows.value = res.data.page_data
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }).finally(() => {
|
|
|
+ loading.value = false
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => route.params,
|
|
|
+ () => {
|
|
|
+ init()
|
|
|
+ },
|
|
|
+ { immediate: true },
|
|
|
+)
|
|
|
+
|
|
|
+function handleGoodjob() {
|
|
|
+ if (detail.value.is_like)
|
|
|
+ return
|
|
|
+ request({
|
|
|
+ url: '/txwx/dz/add',
|
|
|
+ data: {
|
|
|
+ txwx_dz: {
|
|
|
+ td_kclx: theType?.idx,
|
|
|
+ td_dz_id: detail.value[theType.id],
|
|
|
+ td_keyword: detail.value[theType.title],
|
|
|
+ },
|
|
|
+ },
|
|
|
+ }).then((res) => {
|
|
|
+ if (res?.code === '1') {
|
|
|
+ showSuccessToast('点赞成功')
|
|
|
+ detail.value.is_like = 1
|
|
|
+ detail.value.tmz_dzl++
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+async function handleStartRtc() {
|
|
|
+ initRTC()
|
|
|
+ const rtmpUrl = splitRtmpPath(detail.value.push_rtmp_address)
|
|
|
+ const url = `https://api.sd-rtn.com/cn/v1/projects/${detail.value.rtc_appid}/rtmp-converters`
|
|
|
+ const options = {
|
|
|
+ method: 'POST',
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
+ // 'X-Request-ID': '',
|
|
|
+ 'Accept': 'application/json',
|
|
|
+ 'Authorization': 'Basic YmFhOWRkMmIxODgwNDg4ZDg5MmVmMWU4MWExNDljM2Q6MjRlNjgzNmU5MzdhNDhlNmIwMDdlYTI2NWUwNjA0NTE=',
|
|
|
+ },
|
|
|
+ body: JSON.stringify({
|
|
|
+ converter: {
|
|
|
+ name: detail.value.channelname,
|
|
|
+ transcodeOptions: {
|
|
|
+ rtcChannel: detail.value.channelname,
|
|
|
+ audioOptions: { codecProfile: 'HE-AAC', sampleRate: 48000, bitrate: 128, audioChannels: 1, rtcStreamUids: [detail.value.uidStr], volumes: [{ volume: 50, rtcStreamUid: detail.value.uidStr }] },
|
|
|
+ videoOptions: {
|
|
|
+ canvas: { width: 16 * 40, height: 9 * 40, color: 0 },
|
|
|
+ layout: [
|
|
|
+ { rtcStreamUid: detail.value.uidStr, region: { xPos: 0, yPos: 0, zIndex: 1, width: 16 * 40, height: 9 * 40 }, fillMode: 'fill' },
|
|
|
+ ],
|
|
|
+ codec: 'H.264',
|
|
|
+ codecProfile: 'main',
|
|
|
+ frameRate: 15,
|
|
|
+ gop: 30,
|
|
|
+ bitrate: 400,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ rtmpUrl,
|
|
|
+ idleTimeout: 5,
|
|
|
+ jitterBufferSizeMs: 1000,
|
|
|
+ },
|
|
|
+ }),
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await fetch(url, options)
|
|
|
+ const data = await response.json()
|
|
|
+ console.log(data)
|
|
|
+ }
|
|
|
+ catch (error) {
|
|
|
+ console.error(error)
|
|
|
+ }
|
|
|
+}
|
|
|
+function handleEndRtc() {
|
|
|
+ rtc.client?.leave()
|
|
|
+ rtc.localAudioTrack?.close()
|
|
|
+ rtc.localVideoTrack?.close()
|
|
|
+ isRtcing.value = false
|
|
|
+}
|
|
|
+
|
|
|
+function handleNavRightClick() {
|
|
|
+ if (isRtcUser.value) {
|
|
|
+ if (isRtcing.value)
|
|
|
+ handleEndRtc()
|
|
|
+ else
|
|
|
+ handleStartRtc()
|
|
|
+ }
|
|
|
+ else { openShare() }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <van-nav-bar left-arrow @click-left="() => router.back()" @click-right="handleNavRightClick">
|
|
|
+ <template #right>
|
|
|
+ <template v-if="!loading">
|
|
|
+ <span v-if="isRtcUser">{{ isRtcing ? '结束直播' : '开始直播' }}</span>
|
|
|
+ <van-icon v-else name="share-o" size="18" />
|
|
|
+ </template>
|
|
|
+ </template>
|
|
|
+ </van-nav-bar>
|
|
|
+ <van-share-sheet
|
|
|
+ v-model:show="showShare" title="立即分享给好友"
|
|
|
+ :options="[{ name: '复制链接', icon: 'link' }, { name: '二维码', icon: 'qrcode' }]" @select="handleShareSelect"
|
|
|
+ />
|
|
|
+ <van-dialog v-model:show="qrshow" title="请截图保存并分享二维码">
|
|
|
+ <div class="flex flex-col items-center py-32px">
|
|
|
+ <img :src="qrsrc">
|
|
|
+ <div>
|
|
|
+ {{ detail[theType.title] }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </van-dialog>
|
|
|
+
|
|
|
+ <div v-if="loading" class="flex justify-center pt-32px">
|
|
|
+ <van-loading type="spinner" />
|
|
|
+ </div>
|
|
|
+ <template v-else>
|
|
|
+ <div v-if="isRtcUser" ref="rtcPlayerRef" class="w-100vw h-56.25vw bg-dark-900" />
|
|
|
+ <video-player v-else :src="getFullPath(detail[theType.video])" :poster="getFullPath(detail[theType.img])" />
|
|
|
+
|
|
|
+ <div class="px-14px py-20px">
|
|
|
+ <div class="text-14px font-bold">
|
|
|
+ {{ detail[theType.title] }}
|
|
|
+ </div>
|
|
|
+ <div class="flex justify-between items-center text-14px px-2px py-12px">
|
|
|
+ <div class="text-gray-400">
|
|
|
+ {{ detail[theType.teacher] }}
|
|
|
+ </div>
|
|
|
+ <div class="flex text-hex-333 text-12px leading-12px">
|
|
|
+ <div class="flex items-center">
|
|
|
+ <van-icon name="eye-o" size="14" />
|
|
|
+ <div class="ml-2px">
|
|
|
+ {{ formatNumber(detail[theType.lll]) }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ class="flex items-center ml-6px" :class="detail.is_like === 1 ? 'text-red-500' : ''"
|
|
|
+ @click="handleGoodjob"
|
|
|
+ >
|
|
|
+ <van-icon :name="detail.is_like === 1 ? 'good-job' : 'good-job-o'" size="14" />
|
|
|
+ <div class="ml-2px">
|
|
|
+ {{ formatNumber(detail[theType.dzl]) }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="flex items-center justify-between px-14px ">
|
|
|
+ <div class="text-16px font-bold">
|
|
|
+ 相关章节
|
|
|
+ </div>
|
|
|
+ <van-icon
|
|
|
+ name="arrow" size="18"
|
|
|
+ @click="router.push({ name: 'detail-type-tmk_id-list', params: { type: _type_, tmk_id: truthPid } })"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="px-14px py-20px grid grid-cols-2 gap-10px">
|
|
|
+ <div
|
|
|
+ v-for="item in rows" :key="item[theType.id]"
|
|
|
+ class="relative w-44vw rounded-8px overflow-hidden shadow flex flex-col"
|
|
|
+ @click="router.replace(`/detail/${_type_}/${truthPid}/${item[theType.id]}`)"
|
|
|
+ >
|
|
|
+ <div class="absolute top-0 right-0 flex text-12px px-6px py-4px text-white bg-hex-00000050">
|
|
|
+ <div class="flex items-center">
|
|
|
+ <van-icon name="eye-o" size="12" />
|
|
|
+ <div class="ml-4px w-24px tracking-tighter">
|
|
|
+ {{ formatNumber(item[theType.lll]) }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="flex items-center ml-4px">
|
|
|
+ <van-icon name="good-job-o" size="12" />
|
|
|
+ <div class="ml-4px w-24px tracking-tighter">
|
|
|
+ {{ formatNumber(item[theType.dzl]) }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <img :src="getFullPath(item[theType.img])" alt="" class="w-full h-24.75vw">
|
|
|
+ <div class="p-8px flex-auto flex flex-col justify-between">
|
|
|
+ <div class="text-14px font-bold pb-8px leading-16px">
|
|
|
+ {{ item[theType.title] }}
|
|
|
+ </div>
|
|
|
+ <div class="flex justify-between text-12px ">
|
|
|
+ <div class="whitespace-nowrap mr-8px">
|
|
|
+ {{ item[theType.teacher] }}
|
|
|
+ </div>
|
|
|
+ <div>{{ item[theType.school] }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+</template>
|