CameraOverviewPopover.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. <template>
  2. <el-dialog
  3. v-model="dialogVisible"
  4. width="719"
  5. :title="`预览-${overviewCameraData.name}`"
  6. center
  7. align-center
  8. class="camera-overview-popover--custom"
  9. :close-on-click-modal="false"
  10. @close="emits('update:dialogVisible', false)"
  11. >
  12. <div class="camera-overview-popover--custom__content">
  13. <main class="main">
  14. <div class="cameraVideo">
  15. <CameraLiveVideo ref="cameraLiveVideoRef" />
  16. </div>
  17. <div class="presetAddWrapper" v-if="!!overviewCameraData.isPtz">
  18. <CameraViewScale @update:ControlPerspective="activePresetToken = ''" />
  19. <CameraDirectionControl @update:ControlPerspective="activePresetToken = ''" />
  20. <ElButton type="primary" style="margin-top: 20px; width: 100px" @click="handleAddPreset" v-show="displayPresetList.length < 10">添加预置位</ElButton>
  21. </div>
  22. </main>
  23. <footer class="footer" v-if="!!overviewCameraData.isPtz && presetList.length > 0">
  24. <div class="footer-header">
  25. <div class="edit-preset-position-icon-wrapper" @click="toggleEditMode">
  26. <img :src="isEditMode ? EditPresetPositionFocusIcon : EditPresetPositionIcon" alt="编辑预置位" />
  27. <span v-show="isEditMode">完成编辑</span>
  28. </div>
  29. <div class="pagination-control" v-if="displayPresetList.length > 0">
  30. <el-button type="text" :disabled="currentPage === 1" @click="prevPage" :icon="ArrowLeft" />
  31. <span>{{ currentPage }}/{{ totalPages }}</span>
  32. <el-button type="text" :disabled="currentPage === totalPages" @click="nextPage" :icon="ArrowRight" />
  33. </div>
  34. </div>
  35. <div class="preset-position-list">
  36. <div
  37. class="preset-position-item"
  38. v-for="item in currentPageItems"
  39. :key="item.presetToken"
  40. :class="{ 'active-preset': activePresetToken === item.presetToken }"
  41. >
  42. <img
  43. :src="item.imageUrl || PresetPositionItem"
  44. alt="预置位"
  45. style="cursor: pointer"
  46. @click="handleGoToPreset(item)"
  47. />
  48. <img
  49. v-if="isEditMode"
  50. :src="DeletePresetPositionIcon"
  51. alt="删除预置位"
  52. class="delete-preset-position-icon"
  53. @click="handleDeletePreset(item)"
  54. />
  55. <span class="preset-position-name">{{ item.presetName }}</span>
  56. </div>
  57. </div>
  58. </footer>
  59. </div>
  60. </el-dialog>
  61. </template>
  62. <script lang="ts" setup>
  63. import { ref, watch, computed, onMounted } from 'vue';
  64. import { CameraDetailServer } from '@/types/camera/type';
  65. import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue';
  66. import EditPresetPositionIcon from '@/assets/icons/edit-preset-position.svg';
  67. import DeletePresetPositionIcon from '@/assets/icons/delete-preset-position.svg';
  68. import PresetPositionItem from '@/assets/icons/preset-placeholder-img.svg';
  69. import EditPresetPositionFocusIcon from '@/assets/icons/edit-preset-position-focus.svg';
  70. import { dataURLtoBlob } from '@/utils/file/base64Conver';
  71. import {
  72. getPresetListApi,
  73. PresetListResp,
  74. deletePresetApi,
  75. createPresetApi,
  76. goToPresetApi,
  77. uploadPresetImageApi,
  78. } from '@/api/camera/camera-preview';
  79. import CameraLiveVideo from '@/modules/algo-params-setting-base/components/CameraLiveVideo/CameraLiveVideo.vue';
  80. import CameraViewScale from '@/modules/algo-params-setting-base/components/CameraViewSetting/CameraViewScale.vue';
  81. import CameraDirectionControl from '@/modules/algo-params-setting-base/components/CameraDirectionControl/CameraDirectionControl.vue';
  82. import { ElMessage, ElMessageBox } from 'element-plus';
  83. const emits = defineEmits(['update:dialogVisible']);
  84. const dialogVisible = ref(false);
  85. const presetList = ref<PresetListResp[]>([]);
  86. const displayPresetList = ref<PresetListResp[]>([]);
  87. const isEditMode = ref(false);
  88. const activePresetToken = ref<string>(''); // 当前激活的预置位token
  89. const cameraLiveVideoRef = ref<InstanceType<typeof CameraLiveVideo> | null>(null); // 添加对CameraLiveVideo的引用并指定正确类型
  90. const props = defineProps<{
  91. dialogVisible: boolean;
  92. overviewCameraData: CameraDetailServer;
  93. }>();
  94. const currentPage = ref(1);
  95. const pageSize = 5;
  96. const presetPositionCount = computed(() => displayPresetList.value.length); // 总预置位数量
  97. const totalPages = computed(() => Math.ceil(presetPositionCount.value / pageSize));
  98. const currentPageItems = computed(() => {
  99. const startIdx = (currentPage.value - 1) * pageSize;
  100. const endIdx = Math.min(startIdx + pageSize, presetPositionCount.value);
  101. return displayPresetList.value.slice(startIdx, endIdx);
  102. });
  103. const prevPage = () => {
  104. if (currentPage.value > 1) {
  105. currentPage.value--;
  106. }
  107. };
  108. const nextPage = () => {
  109. if (currentPage.value < totalPages.value) {
  110. currentPage.value++;
  111. }
  112. };
  113. const toggleEditMode = () => {
  114. isEditMode.value = !isEditMode.value;
  115. };
  116. const handleGoToPreset = (item: PresetListResp) => {
  117. const cameraId = props.overviewCameraData.id;
  118. if (!cameraId) return;
  119. // 设置当前激活的预置位
  120. activePresetToken.value = item.presetToken;
  121. // 调用前往预置位的API
  122. goToPresetApi({ presetToken: item.presetToken, cameraId });
  123. };
  124. const handleDeletePreset = (item: PresetListResp) => {
  125. const cameraId = props.overviewCameraData.id;
  126. if (!cameraId) return;
  127. const index = displayPresetList.value.findIndex((preset) => preset.presetToken === item.presetToken);
  128. if (index !== -1) {
  129. ElMessageBox.confirm(
  130. '该预置位可能存在关联的电子围栏。删除该预置位将会删除对应的电子围栏信息,请确认是否删除?',
  131. '删除确认',
  132. {
  133. confirmButtonText: '确定',
  134. cancelButtonText: '取消',
  135. },
  136. ).then(async () => {
  137. await deletePresetApi(item.presetToken, String(cameraId));
  138. ElMessage.success('删除成功');
  139. await getPresetList();
  140. const currentPageStartIdx = (currentPage.value - 1) * pageSize;
  141. if (currentPageStartIdx >= displayPresetList.value.length && currentPage.value > 1) {
  142. currentPage.value--;
  143. }
  144. });
  145. }
  146. };
  147. const handleAddPreset = () => {
  148. ElMessageBox.prompt('', '添加预置位', {
  149. confirmButtonText: '确定',
  150. cancelButtonText: '取消',
  151. inputPlaceholder: '请输入预置位名称',
  152. inputValidator: (value) => {
  153. if (!value) {
  154. return '预置位名称不能为空';
  155. }
  156. const isExist = presetList.value.find((item) => item.presetName === value);
  157. if (isExist) {
  158. return '预置位名称已存在';
  159. }
  160. return true;
  161. },
  162. }).then(async ({ value }) => {
  163. const cameraId = props.overviewCameraData.id;
  164. if (!cameraId) return;
  165. let imageBase64 = '';
  166. if (cameraLiveVideoRef.value) {
  167. imageBase64 = cameraLiveVideoRef.value.captureImage();
  168. }
  169. // 未来可以在这里把imageBase64传给后端
  170. const blob = dataURLtoBlob(imageBase64);
  171. const url = await uploadPresetImageApi(blob, 'CAMERA_IMAGE');
  172. if (!url) {
  173. ElMessage.error('上传预置位图片失败');
  174. return;
  175. }
  176. const res = await createPresetApi({ presetName: value, cameraId, imageUrl: url.url });
  177. if (res) {
  178. ElMessage.success('添加预置位成功');
  179. await getPresetList();
  180. }
  181. });
  182. };
  183. const getPresetList = async () => {
  184. const cameraId = props.overviewCameraData.id;
  185. if (!cameraId) return;
  186. presetList.value = await getPresetListApi(cameraId);
  187. displayPresetList.value = [...presetList.value];
  188. };
  189. onMounted(async () => {
  190. await getPresetList();
  191. });
  192. watch(
  193. () => props.dialogVisible,
  194. (newVal) => {
  195. dialogVisible.value = newVal;
  196. if (newVal) {
  197. // 每次对话框打开时,重置编辑状态
  198. isEditMode.value = false;
  199. }
  200. },
  201. { immediate: true },
  202. );
  203. </script>
  204. <style lang="scss">
  205. .camera-overview-popover--custom {
  206. padding: 20px 24px;
  207. border-radius: 8px;
  208. box-shadow: 0px 9px 28px 8px rgba(0, 0, 0, 0.05), 0px 6px 16px 0px rgba(0, 0, 0, 0.08),
  209. 0px 3px 6px -4px rgba(0, 0, 0, 0.12);
  210. .el-dialog__header {
  211. text-align: left;
  212. color: rgba(0, 0, 0, 0.88);
  213. font-weight: bold;
  214. font-size: 16px;
  215. }
  216. &__content {
  217. display: flex;
  218. flex-direction: column;
  219. gap: 12px;
  220. width: 100%;
  221. height: 100%;
  222. .main {
  223. position: relative;
  224. flex-shrink: 0;
  225. flex-grow: 0;
  226. width: 100%;
  227. height: 377px;
  228. }
  229. .footer {
  230. display: flex;
  231. flex-direction: column;
  232. gap: 9px;
  233. .footer-header {
  234. display: flex;
  235. justify-content: space-between;
  236. align-items: center;
  237. cursor: pointer;
  238. .edit-preset-position-icon-wrapper {
  239. display: flex;
  240. align-items: center;
  241. gap: 8px;
  242. span {
  243. font-size: 12px;
  244. color: #1777ff;
  245. font-weight: 500;
  246. }
  247. }
  248. .pagination-control {
  249. display: flex;
  250. align-items: center;
  251. gap: 8px;
  252. font-size: 14px;
  253. color: rgba(0, 0, 0, 0.88);
  254. }
  255. }
  256. .preset-position-list {
  257. display: flex;
  258. width: 100%;
  259. gap: 12px;
  260. .preset-position-item {
  261. text-align: center;
  262. width: 120px;
  263. position: relative;
  264. transition: all 0.3s ease;
  265. &.active-preset {
  266. transform: scale(1.05);
  267. box-shadow: 0 0 8px 2px rgba(24, 144, 255, 0.6);
  268. border-radius: 4px;
  269. &::after {
  270. content: '';
  271. position: absolute;
  272. top: 0;
  273. left: 0;
  274. width: 100%;
  275. height: 100%;
  276. border: 2px solid #1890ff;
  277. border-radius: 4px;
  278. box-sizing: border-box;
  279. pointer-events: none;
  280. }
  281. .preset-position-name {
  282. background: rgba(24, 144, 255, 0.8);
  283. }
  284. }
  285. .delete-preset-position-icon {
  286. position: absolute;
  287. top: -4px;
  288. right: -4px;
  289. width: 16px;
  290. height: 16px;
  291. cursor: pointer;
  292. transition: all 0.3s ease;
  293. &:hover {
  294. scale: 1.2;
  295. }
  296. }
  297. img {
  298. width: 120px;
  299. height: 68px;
  300. object-fit: cover;
  301. }
  302. .preset-position-name {
  303. color: #fff;
  304. font-size: 12px;
  305. background: rgba(0, 0, 0, 0.5);
  306. padding: 2px 4px;
  307. border-radius: 2px;
  308. transition: all 0.3s ease;
  309. }
  310. }
  311. }
  312. }
  313. .cameraVideo {
  314. position: absolute;
  315. top: 0;
  316. left: 0;
  317. z-index: 8;
  318. background: #ccc;
  319. width: 100%;
  320. height: 100%;
  321. }
  322. .presetAddWrapper {
  323. position: absolute;
  324. bottom: -50px;
  325. right: -30px;
  326. flex-direction: column;
  327. display: flex;
  328. align-items: center;
  329. z-index: 10;
  330. transform: scale(0.6);
  331. .el-button {
  332. transform: scale(1.5);
  333. }
  334. }
  335. }
  336. }
  337. </style>