load video without click
This commit is contained in:
10
src/App.vue
10
src/App.vue
@ -1,14 +1,19 @@
|
||||
<template>
|
||||
<!-- <List>123132</List> -->
|
||||
<CameraList />
|
||||
<!-- <CameraItem-->
|
||||
<!-- name="测试球机视频"-->
|
||||
<!-- source="https://sf1-hscdn-tos.pstatp.com/obj/media-fe/xgplayer_doc_video/flv/xgplayer-demo-360p.flv"-->
|
||||
<!-- />-->
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CameraList from "./components/CameraList.vue";
|
||||
// import CameraItem from "@/components/CameraItem.vue";
|
||||
// import List from "./infinite-list-vue.umd.cjs";
|
||||
export default {
|
||||
name: "App",
|
||||
components: {
|
||||
// CameraItem,
|
||||
CameraList,
|
||||
},
|
||||
};
|
||||
@ -17,7 +22,6 @@ export default {
|
||||
<style>
|
||||
#app {
|
||||
background-color: #f6f6f6;
|
||||
//width: 100vw;
|
||||
//height: 100vh;
|
||||
//width: 100vw; //height: 100vh;
|
||||
}
|
||||
</style>
|
||||
|
@ -3,10 +3,12 @@
|
||||
<div class="camera-name">
|
||||
{{ props.name }}
|
||||
</div>
|
||||
<video ref="previewVideoRef" class="camera-player"></video>
|
||||
<video
|
||||
ref="videoElementRef"
|
||||
:class="`camera-player ${isFullscreen ? 'fullscreen-video' : ''}`"
|
||||
></video>
|
||||
<div class="overlay" @click="openPlayModal">
|
||||
<!-- <img src="../assets/stLine-play-l@3x.png" alt="" width="40" height="40" /> -->
|
||||
<span class="iconfont icon-bofang_o" style="font-size: 60px"></span>
|
||||
<!-- <span class="iconfont icon-bofang_o" style="font-size: 60px"></span>-->
|
||||
</div>
|
||||
<div v-if="showModal" class="modal-overlay" @click="closePlayModal">
|
||||
<span
|
||||
@ -15,16 +17,14 @@
|
||||
@click.stop="exitFullscreen"
|
||||
></span>
|
||||
<div ref="rotateElementRef" :class="`modal`">
|
||||
<video
|
||||
ref="videoElementRef"
|
||||
:class="`camera-player ${isFullscreen ? 'fullscreen-video' : ''}`"
|
||||
></video>
|
||||
|
||||
<div
|
||||
:class="`video-overlay ${isFullscreen ? 'fullscreen-video' : ''}`"
|
||||
@click.stop="switchPlayStatus"
|
||||
>
|
||||
<div v-if="isFullscreen" class="arrow-control">
|
||||
<div
|
||||
v-if="isFullscreen && props.name.includes('球机')"
|
||||
class="arrow-control"
|
||||
>
|
||||
<div
|
||||
:class="`up iconfont icon-up ${pressed.up ? 'pressed' : ''}`"
|
||||
@touchend="moveCamera('stop')"
|
||||
@ -67,108 +67,83 @@
|
||||
</div>
|
||||
</template>
|
||||
<script name="camera" setup>
|
||||
import { nextTick, onUnmounted, reactive, ref, toRefs } from "vue";
|
||||
import { nextTick, onMounted, onUnmounted, reactive, ref, toRefs } from "vue";
|
||||
import flvjs from "flv.js";
|
||||
import axios from "axios";
|
||||
// import { useRoute } from "vue-router";
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const props = defineProps({
|
||||
name: {
|
||||
type: String,
|
||||
},
|
||||
source: {
|
||||
type: String,
|
||||
default:
|
||||
"https://sf1-hscdn-tos.pstatp.com/obj/media-fe/xgplayer_doc_video/flv/xgplayer-demo-360p.flv",
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
const { source } = toRefs(props);
|
||||
// const route = useRoute();
|
||||
// document.title = route.query.proname;
|
||||
const showModal = ref(false);
|
||||
const isFullscreen = ref(false);
|
||||
const rotateElementRef = ref();
|
||||
const videoElementRef = ref();
|
||||
let flvPlayer;
|
||||
// const previewVideoRef = ref();
|
||||
// onMounted(() => {
|
||||
// const flvPlayer = flvjs.createPlayer({
|
||||
// type: "flv",
|
||||
// url: props.source,
|
||||
// isLive: true,
|
||||
// });
|
||||
// flvPlayer.attachMediaElement(previewVideoRef.value);
|
||||
// flvPlayer.load();
|
||||
// console.log();
|
||||
// // loadVideo(props.source);
|
||||
// });
|
||||
|
||||
onMounted(() => {
|
||||
flvPlayer = flvjs.createPlayer({
|
||||
type: "flv",
|
||||
url: props.source,
|
||||
});
|
||||
if (flvjs.isSupported()) {
|
||||
flvPlayer.attachMediaElement(videoElementRef.value);
|
||||
flvPlayer.load();
|
||||
flvPlayer.on("ERROR", flvPlayerEventHandler);
|
||||
}
|
||||
});
|
||||
const openPlayModal = () => {
|
||||
showModal.value = true;
|
||||
nextTick(() => {
|
||||
flvPlayer = flvjs.createPlayer({
|
||||
type: "flv",
|
||||
url: props.source,
|
||||
});
|
||||
if (videoElementRef.value) {
|
||||
rotateElementRef.value.appendChild(videoElementRef.value);
|
||||
}
|
||||
if (flvjs.isSupported()) {
|
||||
flvPlayer.attachMediaElement(videoElementRef.value);
|
||||
// flvPlayer.attachMediaElement();
|
||||
flvPlayer.load();
|
||||
flvPlayer.on("ERROR", flvPlayerEventHandler);
|
||||
flvPlayer.play();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// file为视频的文件对象,可使用 input[file] 进行获取
|
||||
const loadVideo = async (url) => {
|
||||
const resp = await axios.get(url);
|
||||
const file = new File([resp.data], "monitor.flv");
|
||||
console.log(file);
|
||||
return new Promise(function (resolve, reject) {
|
||||
const videoElem = document.createElement("video");
|
||||
const dataUrl = URL.createObjectURL(file);
|
||||
// 当前帧的数据是可用的
|
||||
videoElem.onloadeddata = function () {
|
||||
resolve(videoElem);
|
||||
};
|
||||
videoElem.onerror = function (e) {
|
||||
console.log(e);
|
||||
reject("video 后台加载失败");
|
||||
};
|
||||
// 设置 auto 预加载数据, 否则会出现截图为黑色图片的情况
|
||||
videoElem.setAttribute("preload", "auto");
|
||||
videoElem.src = dataUrl;
|
||||
const closePlayModal = () => {
|
||||
// flvPlayer.on("ERROR", flvPlayerEventHandler);
|
||||
// flvPlayer.pause();
|
||||
// flvPlayer.unload();
|
||||
// flvPlayer.detachMediaElement();
|
||||
// flvPlayer.destroy();
|
||||
// flvPlayer = null;
|
||||
showModal.value = false;
|
||||
nextTick(() => {
|
||||
document.querySelector(".camera-item")?.appendChild(videoElementRef.value);
|
||||
if (flvjs.isSupported()) {
|
||||
flvPlayer.pause();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const closePlayModal = () => {
|
||||
flvPlayer.on("ERROR", flvPlayerEventHandler);
|
||||
flvPlayer.pause();
|
||||
flvPlayer.unload();
|
||||
flvPlayer.detachMediaElement();
|
||||
flvPlayer.destroy();
|
||||
flvPlayer = null;
|
||||
showModal.value = false;
|
||||
};
|
||||
const videoElementRef = ref();
|
||||
// const playBtnRef = ref();
|
||||
// let flvPlayer = flvjs.createPlayer({
|
||||
// type: "flv",
|
||||
// url: "https://sf1-hscdn-tos.pstatp.com/obj/media-fe/xgplayer_doc_video/flv/xgplayer-demo-360p.flv",
|
||||
// });
|
||||
|
||||
const flvPlayerEventHandler = (e) => {
|
||||
console.log(e);
|
||||
};
|
||||
|
||||
// 切换播放状态
|
||||
const switchPlayStatus = () => {
|
||||
if (!videoElementRef.value) return;
|
||||
if (videoElementRef.value.paused) {
|
||||
console.log(flvPlayer);
|
||||
flvPlayer.play();
|
||||
} else {
|
||||
flvPlayer.pause();
|
||||
// flvPlayer.pause();
|
||||
}
|
||||
};
|
||||
const isFullscreen = ref(false);
|
||||
const rotateElementRef = ref();
|
||||
|
||||
/* 进入全屏 */
|
||||
const videoFullscreen = () => {
|
||||
const scale = screen.availWidth / videoElementRef.value.offsetHeight;
|
||||
videoElementRef.value.style.setProperty("--scale", scale);
|
||||
@ -176,6 +151,11 @@ const videoFullscreen = () => {
|
||||
isFullscreen.value = true;
|
||||
};
|
||||
|
||||
/* 退出全屏 */
|
||||
const exitFullscreen = () => {
|
||||
isFullscreen.value = false;
|
||||
};
|
||||
|
||||
const token = ref("");
|
||||
const loadToken = async () => {
|
||||
const url = new URL(source.value);
|
||||
@ -185,17 +165,23 @@ const loadToken = async () => {
|
||||
return resp.data.URLToken;
|
||||
};
|
||||
|
||||
/*方向按钮是否按下*/
|
||||
const pressed = reactive({
|
||||
left: false,
|
||||
right: false,
|
||||
up: false,
|
||||
down: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* 控制摄像头转动
|
||||
* @param arrow 上下左右和停止
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
const moveCamera = async (arrow) => {
|
||||
if (!token.value) {
|
||||
token.value = await loadToken();
|
||||
}
|
||||
|
||||
const serial = source.value.split("/flv/hls/")[1].split("_")[0];
|
||||
const url = new URL(source.value);
|
||||
await axios.get(
|
||||
@ -211,9 +197,6 @@ const moveCamera = async (arrow) => {
|
||||
}
|
||||
};
|
||||
|
||||
const exitFullscreen = () => {
|
||||
isFullscreen.value = false;
|
||||
};
|
||||
onUnmounted(() => {
|
||||
console.log("destroy player here");
|
||||
|
||||
@ -235,7 +218,7 @@ onUnmounted(() => {
|
||||
@import "../assets/fonts/iconfont.css";
|
||||
|
||||
.camera-item {
|
||||
// height: 300px;
|
||||
height: 400px;
|
||||
background-color: white;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 20px;
|
||||
@ -282,8 +265,7 @@ onUnmounted(() => {
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 999;
|
||||
// position: relative;
|
||||
background-color: rgba(153, 153, 153, 0.363);
|
||||
backdrop-filter: blur(12px);
|
||||
|
||||
.quit-fullscreen {
|
||||
position: fixed;
|
||||
@ -312,14 +294,9 @@ onUnmounted(() => {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
//background-color: #eee;
|
||||
z-index: 999999;
|
||||
left: 0;
|
||||
top: 0;
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
// justify-content: center;
|
||||
// z-index: 99;
|
||||
// position: relative;
|
||||
.fullscreen {
|
||||
position: absolute;
|
||||
z-index: 99;
|
||||
@ -354,6 +331,7 @@ onUnmounted(() => {
|
||||
right: 25px;
|
||||
bottom: 25px;
|
||||
z-index: 9999;
|
||||
backdrop-filter: blur(12px);
|
||||
|
||||
.iconfont {
|
||||
width: 32px;
|
@ -5,31 +5,18 @@
|
||||
<NSelect
|
||||
v-model:value="queryParams.query"
|
||||
:options="projectList"
|
||||
placeholder="请选择项目"
|
||||
label-field="PRONAME"
|
||||
placeholder="请选择项目"
|
||||
value-field="PROID"
|
||||
@update:value="projectClicked"
|
||||
/>
|
||||
</NConfigProvider>
|
||||
</div>
|
||||
|
||||
<!-- <select
|
||||
name="pets"
|
||||
v-model="queryParams.query"
|
||||
id="proj-select"
|
||||
@change="projectClicked"
|
||||
>
|
||||
<option value="">请选择项目</option>
|
||||
<option v-for="item in projectList" :value="item.PROID">
|
||||
{{ item.PRONAME }}
|
||||
</option>
|
||||
</select> -->
|
||||
|
||||
<div v-if="queryParams.query" class="naive-tabs">
|
||||
<NTabs type="segment">
|
||||
<NTabPane name="camera" tab="摄像头列表">
|
||||
<!-- v-if="queryParams.query && activeIndex == 0" -->
|
||||
<div class="camera-list" ref="cameraListRef">
|
||||
<div ref="cameraListRef" class="camera-list">
|
||||
<camera
|
||||
v-for="(item, index) in cameraList"
|
||||
:key="index"
|
||||
@ -37,7 +24,7 @@
|
||||
:source="item.flvUrl"
|
||||
></camera>
|
||||
<div v-if="!cameraList.length" class="empty">
|
||||
<img src="../assets/empty.png" alt="" />
|
||||
<img alt="" src="../assets/empty.png" />
|
||||
摄像头列表为空
|
||||
</div>
|
||||
</div>
|
||||
@ -45,46 +32,31 @@
|
||||
<NTabPane name="image" tab="图片列表">
|
||||
<!-- v-else-if="queryParams.query && activeIndex == 1" -->
|
||||
<div class="image-list">
|
||||
<div class="image-item" v-for="item in imageList" :key="item.PIC">
|
||||
<div v-for="item in imageList" :key="item.PIC" class="image-item">
|
||||
<!-- <img :src="`${protocol}//${host}/portal/r/${item}`" alt="" /> -->
|
||||
<!-- <img :src="`http://81.68.90.198:8088/portal/r/${item}`" alt="" /> -->
|
||||
<!-- TODO:test -->
|
||||
<!-- :src="`${protocol}//${host}/portal/r/${item.PIC}`" -->
|
||||
<NImage
|
||||
class="n-image-item"
|
||||
:src="`http://81.68.90.198:8088/portal/r/${item.PIC}`"
|
||||
:previewed-img-props="{
|
||||
draggable: true,
|
||||
style: { border: 'none' },
|
||||
}"
|
||||
:src="`http://81.68.90.198:8088/portal/r/${item.PIC}`"
|
||||
class="n-image-item"
|
||||
></NImage>
|
||||
<div class="uplode-time">{{ item.CREATEDATE }}</div>
|
||||
</div>
|
||||
<div v-if="!imageList.length" class="empty">
|
||||
<img src="../assets/empty.png" alt="" />
|
||||
<img alt="" src="../assets/empty.png" />
|
||||
图片列表为空
|
||||
</div>
|
||||
</div>
|
||||
</NTabPane>
|
||||
</NTabs>
|
||||
</div>
|
||||
<!-- <div class="switch" v-if="queryParams.query">
|
||||
<div
|
||||
:class="`switch-item ${activeIndex == 0 ? 'active' : ''}`"
|
||||
@click="activeIndex = 0"
|
||||
>
|
||||
摄像头列表
|
||||
</div>
|
||||
<div
|
||||
:class="`switch-item ${activeIndex == 1 ? 'active' : ''}`"
|
||||
@click="activeIndex = 1"
|
||||
>
|
||||
图片列表
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div v-else class="empty">
|
||||
<img src="../assets/empty.png" alt="" />
|
||||
<img alt="" src="../assets/empty.png" />
|
||||
请选择项目
|
||||
</div>
|
||||
</div>
|
||||
@ -92,16 +64,17 @@
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref } from "vue";
|
||||
import camera from "./camera.vue";
|
||||
import axios from "axios";
|
||||
import {
|
||||
NSelect,
|
||||
NConfigProvider,
|
||||
zhCN,
|
||||
NTabs,
|
||||
NTabPane,
|
||||
NImage,
|
||||
NSelect,
|
||||
NTabPane,
|
||||
NTabs,
|
||||
zhCN,
|
||||
} from "naive-ui";
|
||||
import Camera from "./CameraItem.vue";
|
||||
|
||||
const cameraLoading = ref(true);
|
||||
const projectList = ref([
|
||||
// {
|
||||
@ -120,18 +93,18 @@ const queryParams = reactive({
|
||||
cmd: "com.awspaas.user.apps.cmp_camera_list",
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
// sid: sid,
|
||||
sid: sid,
|
||||
// TODO:test
|
||||
sid: "b9e99f78-2f6e-4d1c-aea9-4364f4191d30",
|
||||
// sid: "b9e99f78-2f6e-4d1c-aea9-4364f4191d30",
|
||||
query: undefined,
|
||||
});
|
||||
// const activeIndex = ref(0);
|
||||
const loadProjectList = async () => {
|
||||
// const resp = await axios.get(`http://localhost:3000/project-list`);
|
||||
// TODO:test
|
||||
const resp = await axios.get(
|
||||
"/api/portal/r/jd?cmd=com.awspaas.user.apps.cmp_screen_getProjectList&sid=b9e99f78-2f6e-4d1c-aea9-4364f4191d30"
|
||||
);
|
||||
// const resp = await axios.get(
|
||||
// "/api/portal/r/jd?cmd=com.awspaas.user.apps.cmp_screen_getProjectList&sid=b9e99f78-2f6e-4d1c-aea9-4364f4191d30"
|
||||
// );
|
||||
// const resp = {
|
||||
// data: [{
|
||||
// "SUPERVISORID": "",
|
||||
@ -180,9 +153,9 @@ const loadProjectList = async () => {
|
||||
// "ID": "a0fb6df5-48e4-442f-8b9d-da5861c1bf41"
|
||||
// }]
|
||||
// }
|
||||
// const resp = await axios.get(
|
||||
// "./jd?cmd=com.awspaas.user.apps.cmp_screen_getProjectList&sid=" + sid
|
||||
// );
|
||||
const resp = await axios.get(
|
||||
"./jd?cmd=com.awspaas.user.apps.cmp_screen_getProjectList&sid=" + sid
|
||||
);
|
||||
return resp.data;
|
||||
};
|
||||
|
||||
@ -198,9 +171,9 @@ const projectClicked = () => {
|
||||
const loadCameraList = async () => {
|
||||
cameraLoading.value = true;
|
||||
// TODO:test
|
||||
const resp = await axios(`/api/portal/r/jd`, {
|
||||
params: queryParams,
|
||||
});
|
||||
// const resp = await axios(`/api/portal/r/jd`, {
|
||||
// params: queryParams,
|
||||
// });
|
||||
// const resp = {
|
||||
// data: {
|
||||
// "code": 0,
|
||||
@ -232,19 +205,12 @@ const loadCameraList = async () => {
|
||||
// "total": 4
|
||||
// }
|
||||
// }
|
||||
const resp = await axios.get("./jd", {
|
||||
params: queryParams,
|
||||
});
|
||||
cameraList.value = resp.data.rows;
|
||||
total.value = resp.data.total;
|
||||
cameraLoading.value = false;
|
||||
// if (total.value == 0) {
|
||||
// const rep = await axios.get(
|
||||
// `./jd?cmd=com.awspaas.user.apps.cmp_photo_list&sid=${sid}&proId=${queryParams.query}`
|
||||
// ); // 摄像头列表为空时,获取图片列表
|
||||
|
||||
// const rep = await axios.get(
|
||||
// `/api/portal/r/jd?cmd=com.awspaas.user.apps.cmp_photo_list&sid=${sid}&proId=${queryParams.query}`
|
||||
// ); // 摄像头列表为空时,获取图片列表
|
||||
// imageList.value = rep.data;
|
||||
// }
|
||||
};
|
||||
|
||||
const loadImageList = async () => {
|
||||
@ -253,7 +219,7 @@ const loadImageList = async () => {
|
||||
// ); // 摄像头列表为空时,获取图片列表
|
||||
// TODO:test
|
||||
const resp = await axios.get(
|
||||
`/api/portal/r/jd?cmd=com.awspaas.user.apps.cmp_photo_list&sid=b9e99f78-2f6e-4d1c-aea9-4364f4191d30&proId=${queryParams.query}`
|
||||
`./jd?cmd=com.awspaas.user.apps.cmp_photo_list&sid=${sid}&proId=${queryParams.query}`
|
||||
); // 摄像头列表为空时,获取图片列表
|
||||
imageList.value = resp.data;
|
||||
};
|
||||
@ -286,31 +252,29 @@ loadProjectList().then((data) => {
|
||||
projectList.value = data;
|
||||
if (projectList.value.length) return projectList.value[0].PROID;
|
||||
});
|
||||
// .then((proid) => {
|
||||
// queryParams.query = proid;
|
||||
// queryParams.pageNum = 1;
|
||||
// loadCameraList();
|
||||
// });
|
||||
</script>
|
||||
<style>
|
||||
/* body {
|
||||
background-color: #f6f6f6;
|
||||
} */
|
||||
</style>
|
||||
<style scoped lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
.app-container {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.naive-select-wrap {
|
||||
width: 750px;
|
||||
padding: 20px 20px 0px;
|
||||
}
|
||||
|
||||
.naive-tabs {
|
||||
padding: 20px 20px 0;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
width: 750px;
|
||||
height: 88px;
|
||||
@ -322,6 +286,7 @@ loadProjectList().then((data) => {
|
||||
font-size: 32px;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.switch {
|
||||
width: 686px;
|
||||
height: 76px;
|
||||
@ -332,6 +297,7 @@ loadProjectList().then((data) => {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
|
||||
.switch-item {
|
||||
width: 331px;
|
||||
height: 60px;
|
||||
@ -339,20 +305,24 @@ loadProjectList().then((data) => {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&.active {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.camera-list {
|
||||
// margin-top: 108px;
|
||||
// padding: 0px 20px 0px;
|
||||
}
|
||||
|
||||
.image-list {
|
||||
// padding: 0px 20px 0px;
|
||||
.image-item {
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
|
||||
.uplode-time {
|
||||
color: indianred;
|
||||
position: absolute;
|
||||
@ -364,6 +334,7 @@ loadProjectList().then((data) => {
|
||||
border-radius: 999px;
|
||||
padding: 0px 10px;
|
||||
}
|
||||
|
||||
// border-radius: 10px;
|
||||
// overflow: hidden;
|
||||
:deep(.n-image-item) {
|
||||
@ -375,6 +346,7 @@ loadProjectList().then((data) => {
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
// img {
|
||||
// object-fit: cover;
|
||||
// border-radius: 10px;
|
||||
@ -392,6 +364,7 @@ loadProjectList().then((data) => {
|
||||
margin: 2vw 2vw 5vw 2vw;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.empty {
|
||||
// flex: 1;
|
||||
flex-grow: 1;
|
||||
@ -399,9 +372,11 @@ loadProjectList().then((data) => {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
// height:80vh;
|
||||
}
|
||||
</style>
|
||||
|
@ -13,7 +13,7 @@
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from "vue";
|
||||
import camera from "./camera.vue";
|
||||
import camera from "./CameraItem.vue";
|
||||
|
||||
const cameraList = ref([
|
||||
{
|
||||
|
Reference in New Issue
Block a user