Commit 66208290d4090ec3867c04008413f2d80251f8d1
1 parent
b39a4d77
fix():修改窗口支持25和36屏幕,修改播放器延时问题
Showing
26 changed files
with
2431 additions
and
1648 deletions
Too many changes to show.
To preserve performance only 26 of 47 files are displayed.
pom.xml
| ... | ... | @@ -95,6 +95,12 @@ |
| 95 | 95 | </profiles> |
| 96 | 96 | |
| 97 | 97 | <dependencies> |
| 98 | + <!-- httpclient --> | |
| 99 | + <dependency> | |
| 100 | + <groupId>commons-httpclient</groupId> | |
| 101 | + <artifactId>commons-httpclient</artifactId> | |
| 102 | + <version>3.1</version> | |
| 103 | + </dependency> | |
| 98 | 104 | <!-- https://mvnrepository.com/artifact/commons-net/commons-net --> |
| 99 | 105 | <dependency> |
| 100 | 106 | <groupId>commons-net</groupId> | ... | ... |
src/main/java/com/genersoft/iot/vmp/conf/security/WebSecurityConfig.java
| ... | ... | @@ -84,6 +84,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { |
| 84 | 84 | matchers.add("/api/device/query/snap/**"); |
| 85 | 85 | matchers.add("/record_proxy/*/**"); |
| 86 | 86 | matchers.add("/api/emit"); |
| 87 | + matchers.add("/api/user/getInfo"); | |
| 87 | 88 | matchers.add("/favicon.ico"); |
| 88 | 89 | matchers.add("/api/jt1078/query/test1"); |
| 89 | 90 | matchers.add("/api/jt1078/query/test"); |
| ... | ... | @@ -123,7 +124,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { |
| 123 | 124 | .authorizeRequests() |
| 124 | 125 | .requestMatchers(CorsUtils::isPreFlightRequest).permitAll() |
| 125 | 126 | .antMatchers(userSetting.getInterfaceAuthenticationExcludes().toArray(new String[0])).permitAll() |
| 126 | - .antMatchers("/api/user/login", "/index/hook/**", "/swagger-ui/**", "/doc.html").permitAll() | |
| 127 | + .antMatchers("/api/user/login","/api/user/getInfo", "/index/hook/**", "/swagger-ui/**", "/doc.html").permitAll() | |
| 127 | 128 | .anyRequest().authenticated() |
| 128 | 129 | .and() |
| 129 | 130 | .addFilterBefore(new IpWhitelistFilter(), BasicAuthenticationFilter.class) | ... | ... |
src/main/java/com/genersoft/iot/vmp/service/IUserService.java
src/main/java/com/genersoft/iot/vmp/service/impl/StremProxyService1078Impl.java
| ... | ... | @@ -66,9 +66,6 @@ public class StremProxyService1078Impl implements StremProxyService1078 { |
| 66 | 66 | if(Objects.nonNull(port)){ |
| 67 | 67 | // VideoServerApp.stopServer(port,httpPort); |
| 68 | 68 | } |
| 69 | - | |
| 70 | - | |
| 71 | - | |
| 72 | 69 | if (Objects.isNull(entity)) { |
| 73 | 70 | log.info("HttpClientPostEntity is null"); |
| 74 | 71 | } else { |
| ... | ... | @@ -77,12 +74,11 @@ public class StremProxyService1078Impl implements StremProxyService1078 { |
| 77 | 74 | |
| 78 | 75 | redisTemplate.opsForValue().set("jt1078:count:"+stream,20000,300,TimeUnit.SECONDS); |
| 79 | 76 | |
| 80 | -// streamProxyService.del("schedule", stream); | |
| 81 | 77 | resultMap.put("code", "1"); |
| 82 | 78 | resultMap.put("message", "OK"); |
| 83 | 79 | |
| 84 | 80 | return resultMap; |
| 85 | - } catch (URISyntaxException | IOException e) { | |
| 81 | + } catch (Exception e) { | |
| 86 | 82 | log.error("发送停止推流指令异常;[{}],[{}]", url, msg, e); |
| 87 | 83 | |
| 88 | 84 | resultMap.put("code", "-20"); | ... | ... |
src/main/java/com/genersoft/iot/vmp/service/impl/UserServiceImpl.java
| ... | ... | @@ -95,4 +95,10 @@ public class UserServiceImpl implements IUserService { |
| 95 | 95 | public int changePushKey(int id, String pushKey) { |
| 96 | 96 | return userMapper.changePushKey(id,pushKey); |
| 97 | 97 | } |
| 98 | + | |
| 99 | + @Override | |
| 100 | + public User selectUserByUserName(String username) { | |
| 101 | + | |
| 102 | + return userMapper.getUserByUsername( username); | |
| 103 | + } | |
| 98 | 104 | } | ... | ... |
src/main/java/com/genersoft/iot/vmp/utils/HttpClientUtil.java
0 → 100644
| 1 | +package com.genersoft.iot.vmp.utils; | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | +import com.alibaba.fastjson2.JSON; | |
| 6 | +import org.apache.commons.httpclient.HttpClient; | |
| 7 | +import org.apache.commons.httpclient.HttpStatus; | |
| 8 | +import org.apache.commons.httpclient.SimpleHttpConnectionManager; | |
| 9 | +import org.apache.commons.httpclient.methods.PostMethod; | |
| 10 | +import org.apache.commons.httpclient.methods.StringRequestEntity; | |
| 11 | +import org.apache.commons.lang3.StringUtils; | |
| 12 | +import org.apache.http.HttpEntity; | |
| 13 | +import org.apache.http.HttpResponse; | |
| 14 | +import org.apache.http.NameValuePair; | |
| 15 | +import org.apache.http.client.ClientProtocolException; | |
| 16 | +import org.apache.http.client.entity.UrlEncodedFormEntity; | |
| 17 | +import org.apache.http.client.methods.CloseableHttpResponse; | |
| 18 | +import org.apache.http.client.methods.HttpGet; | |
| 19 | +import org.apache.http.client.methods.HttpPost; | |
| 20 | +import org.apache.http.entity.StringEntity; | |
| 21 | +import org.apache.http.impl.client.CloseableHttpClient; | |
| 22 | +import org.apache.http.impl.client.HttpClientBuilder; | |
| 23 | +import org.apache.http.impl.client.HttpClients; | |
| 24 | +import org.apache.http.message.BasicNameValuePair; | |
| 25 | +import org.apache.http.protocol.HTTP; | |
| 26 | +import org.apache.http.util.EntityUtils; | |
| 27 | + | |
| 28 | +import java.io.*; | |
| 29 | +import java.lang.reflect.Field; | |
| 30 | +import java.net.ConnectException; | |
| 31 | +import java.net.HttpURLConnection; | |
| 32 | +import java.net.ProtocolException; | |
| 33 | +import java.net.URL; | |
| 34 | +import java.util.*; | |
| 35 | + | |
| 36 | +public class HttpClientUtil { | |
| 37 | + | |
| 38 | + public static String post(String url, Map<String, String> params) { | |
| 39 | + CloseableHttpClient httpclient = HttpClients.createDefault(); | |
| 40 | + HttpPost post = postForm(url, params); | |
| 41 | + String body = null; | |
| 42 | + try { | |
| 43 | + CloseableHttpResponse response2 = httpclient.execute(post); | |
| 44 | + try { | |
| 45 | + HttpEntity entity2 = response2.getEntity(); | |
| 46 | + body = EntityUtils.toString(entity2, "UTF-8"); | |
| 47 | + EntityUtils.consume(entity2); | |
| 48 | + } finally { | |
| 49 | + response2.close(); | |
| 50 | + } | |
| 51 | + } catch (ClientProtocolException e) { | |
| 52 | + // TODO Auto-generated catch block | |
| 53 | + e.printStackTrace(); | |
| 54 | + } catch (IOException e) { | |
| 55 | + // TODO Auto-generated catch block | |
| 56 | + e.printStackTrace(); | |
| 57 | + } finally { | |
| 58 | + try { | |
| 59 | + httpclient.close(); | |
| 60 | + } catch (IOException e) { | |
| 61 | + e.printStackTrace(); | |
| 62 | + } | |
| 63 | + } | |
| 64 | + | |
| 65 | + | |
| 66 | + return body; | |
| 67 | + } | |
| 68 | + | |
| 69 | + public static String get(String url) { | |
| 70 | + CloseableHttpClient httpclient = HttpClients.createDefault(); | |
| 71 | + HttpGet httpGet = new HttpGet(url); | |
| 72 | + String body = null; | |
| 73 | + try { | |
| 74 | + CloseableHttpResponse response1 = httpclient.execute(httpGet); | |
| 75 | + try { | |
| 76 | + HttpEntity entity1 = response1.getEntity(); | |
| 77 | + String charset = EntityUtils.getContentCharSet(entity1); | |
| 78 | + body = EntityUtils.toString(entity1); | |
| 79 | + EntityUtils.consume(entity1); | |
| 80 | + } finally { | |
| 81 | + response1.close(); | |
| 82 | + } | |
| 83 | + } catch (ClientProtocolException e) { | |
| 84 | + // TODO Auto-generated catch block | |
| 85 | + e.printStackTrace(); | |
| 86 | + } catch (IOException e) { | |
| 87 | + // TODO Auto-generated catch block | |
| 88 | + e.printStackTrace(); | |
| 89 | + } finally { | |
| 90 | + try { | |
| 91 | + httpclient.close(); | |
| 92 | + } catch (IOException e) { | |
| 93 | + e.printStackTrace(); | |
| 94 | + } | |
| 95 | + } | |
| 96 | + | |
| 97 | + return body; | |
| 98 | + } | |
| 99 | + | |
| 100 | + | |
| 101 | + /** | |
| 102 | + * @param url | |
| 103 | + * @param params | |
| 104 | + * @return | |
| 105 | + */ | |
| 106 | + private static HttpPost postForm(String url, Map<String, String> params) { | |
| 107 | + | |
| 108 | + HttpPost httpost = new HttpPost(url); | |
| 109 | + List<NameValuePair> nvps = new ArrayList<NameValuePair>(); | |
| 110 | + | |
| 111 | + Set<String> keySet = params.keySet(); | |
| 112 | + for (String key : keySet) { | |
| 113 | + nvps.add(new BasicNameValuePair(key, params.get(key))); | |
| 114 | + } | |
| 115 | + | |
| 116 | + try { | |
| 117 | + httpost.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8)); | |
| 118 | + } catch (UnsupportedEncodingException e) { | |
| 119 | + e.printStackTrace(); | |
| 120 | + } | |
| 121 | + | |
| 122 | + return httpost; | |
| 123 | + } | |
| 124 | + | |
| 125 | + public static String post(String url, String inMessageXml) { | |
| 126 | + | |
| 127 | + System.out.println("url..." + url); | |
| 128 | + System.out.println("inMessageXml..." + inMessageXml); | |
| 129 | + //创建httpclient工具对象 | |
| 130 | + HttpClient client = new HttpClient(); | |
| 131 | + //创建post请求方法 | |
| 132 | + PostMethod myPost = new PostMethod(url); | |
| 133 | + //设置请求超时时间 | |
| 134 | + client.setConnectionTimeout(3000 * 1000); | |
| 135 | + String responseString = null; | |
| 136 | + try { | |
| 137 | + //设置请求头部类型 | |
| 138 | + myPost.setRequestHeader("Content-Type", "text/xml"); | |
| 139 | + myPost.setRequestHeader("charset", "utf-8"); | |
| 140 | + //设置请求体,即xml文本内容,一种是直接获取xml内容字符串,一种是读取xml文件以流的形式 | |
| 141 | + myPost.setRequestEntity(new StringRequestEntity(inMessageXml, "text/xml", "utf-8")); | |
| 142 | + int statusCode = client.executeMethod(myPost); | |
| 143 | + //只有请求成功200了,才做处理 | |
| 144 | + if (statusCode == HttpStatus.SC_OK) { | |
| 145 | + InputStream inputStream = myPost.getResponseBodyAsStream(); | |
| 146 | + BufferedReader br = new BufferedReader(new InputStreamReader(inputStream, "utf-8")); | |
| 147 | + StringBuffer stringBuffer = new StringBuffer(); | |
| 148 | + String str = ""; | |
| 149 | + while ((str = br.readLine()) != null) { | |
| 150 | + stringBuffer.append(str); | |
| 151 | + } | |
| 152 | + responseString = stringBuffer.toString(); | |
| 153 | + } | |
| 154 | + } catch (Exception e) { | |
| 155 | + e.printStackTrace(); | |
| 156 | + } finally { | |
| 157 | + myPost.releaseConnection(); | |
| 158 | + ((SimpleHttpConnectionManager) client.getHttpConnectionManager()).shutdown(); | |
| 159 | + } | |
| 160 | + return responseString; | |
| 161 | + } | |
| 162 | + | |
| 163 | + | |
| 164 | + public static String doPost(String url, Object data) { | |
| 165 | + | |
| 166 | + String jsonData = JSON.toJSONString(data); | |
| 167 | + //String jsonData = JSONUtils.toJson(data); | |
| 168 | + System.out.println("url..." + url); | |
| 169 | + System.out.println("inMessageXml..." + data); | |
| 170 | + | |
| 171 | + CloseableHttpClient httpclient = HttpClientBuilder.create().build(); | |
| 172 | + HttpPost post = new HttpPost(url); | |
| 173 | + try { | |
| 174 | + StringEntity s = new StringEntity(jsonData, "utf-8"); | |
| 175 | + s.setContentEncoding("UTF-8"); | |
| 176 | + s.setContentType("application/json");//发送json数据需要设置contentType | |
| 177 | + post.setEntity(s); | |
| 178 | + HttpResponse res = httpclient.execute(post); | |
| 179 | + if (res.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { | |
| 180 | + return EntityUtils.toString(res.getEntity()); | |
| 181 | + } | |
| 182 | + } catch (Exception e) { | |
| 183 | + throw new RuntimeException(e); | |
| 184 | + } finally { | |
| 185 | + try { | |
| 186 | + httpclient.close(); | |
| 187 | + } catch (IOException e) { | |
| 188 | + e.printStackTrace(); | |
| 189 | + } | |
| 190 | + } | |
| 191 | + return null; | |
| 192 | + } | |
| 193 | + | |
| 194 | + | |
| 195 | + /** | |
| 196 | + * HttpURLConnection方式 模拟Http Get请求 | |
| 197 | + * | |
| 198 | + * @param urlStr 请求路径 | |
| 199 | + * @param paramMap 请求参数 | |
| 200 | + * @return | |
| 201 | + * @throws Exception | |
| 202 | + */ | |
| 203 | + public static String get(String urlStr, Map<String, String> paramMap) throws Exception { | |
| 204 | + urlStr = urlStr + "?" + getParamString(paramMap); | |
| 205 | + HttpURLConnection conn = null; | |
| 206 | + try { | |
| 207 | + //创建URL对象 | |
| 208 | + URL url = new URL(urlStr); | |
| 209 | + //获取URL连接 | |
| 210 | + conn = (HttpURLConnection) url.openConnection(); | |
| 211 | + //设置通用的请求属性 | |
| 212 | + setHttpUrlConnection(conn, "GET"); | |
| 213 | + //建立实际的连接 | |
| 214 | + conn.connect(); | |
| 215 | + //获取响应的内容 | |
| 216 | + return readResponseContent(conn.getInputStream()); | |
| 217 | + } finally { | |
| 218 | + if (null != conn) | |
| 219 | + conn.disconnect(); | |
| 220 | + } | |
| 221 | + } | |
| 222 | + | |
| 223 | + | |
| 224 | + /** | |
| 225 | + * HttpURLConnection方式 模拟Http Post请求 | |
| 226 | + * | |
| 227 | + * @param urlStr 请求路径 | |
| 228 | + * @return | |
| 229 | + * @throws Exception | |
| 230 | + */ | |
| 231 | + public static String postMap(String urlStr, Object object) throws Exception { | |
| 232 | + HttpURLConnection conn = null; | |
| 233 | + PrintWriter writer = null; | |
| 234 | + | |
| 235 | + try { | |
| 236 | + //创建URL对象 | |
| 237 | + URL url = new URL(urlStr); | |
| 238 | + //获取请求参数 | |
| 239 | + Map<String, String> params = objectToMap(object); | |
| 240 | + String param = getParamString(params); | |
| 241 | + //获取URL连接 | |
| 242 | + System.out.println("requestUrl:" + urlStr); | |
| 243 | + System.out.println("outputStr:" + param); | |
| 244 | + conn = (HttpURLConnection) url.openConnection(); | |
| 245 | + //设置通用请求属性 | |
| 246 | + setHttpUrlConnection(conn, "POST"); | |
| 247 | + //建立实际的连接 | |
| 248 | + conn.connect(); | |
| 249 | + //将请求参数写入请求字符流中 | |
| 250 | + writer = new PrintWriter(conn.getOutputStream()); | |
| 251 | + writer.print(param); | |
| 252 | + writer.flush(); | |
| 253 | + //读取响应的内容 | |
| 254 | + return readResponseContent(conn.getInputStream()); | |
| 255 | + } finally { | |
| 256 | + if (null != conn) | |
| 257 | + conn.disconnect(); | |
| 258 | + if (null != writer) | |
| 259 | + writer.close(); | |
| 260 | + } | |
| 261 | + } | |
| 262 | + | |
| 263 | + /** | |
| 264 | + * HttpURLConnection方式 模拟Http Post请求 | |
| 265 | + * | |
| 266 | + * @param urlStr 请求路径 | |
| 267 | + * @return | |
| 268 | + * @throws Exception | |
| 269 | + */ | |
| 270 | + public static String postByMap(String urlStr, Map<String, String> params) throws Exception { | |
| 271 | + HttpURLConnection conn = null; | |
| 272 | + PrintWriter writer = null; | |
| 273 | + | |
| 274 | + try { | |
| 275 | + //创建URL对象 | |
| 276 | + URL url = new URL(urlStr); | |
| 277 | + //获取请求参数 | |
| 278 | + String param = getParamString(params); | |
| 279 | + //获取URL连接 | |
| 280 | + System.out.println("requestUrl:" + urlStr); | |
| 281 | + System.out.println("outputStr:" + param); | |
| 282 | + conn = (HttpURLConnection) url.openConnection(); | |
| 283 | + //设置通用请求属性 | |
| 284 | + setHttpUrlConnection(conn, "POST"); | |
| 285 | + //建立实际的连接 | |
| 286 | + conn.connect(); | |
| 287 | + //将请求参数写入请求字符流中 | |
| 288 | + writer = new PrintWriter(conn.getOutputStream()); | |
| 289 | + writer.print(param); | |
| 290 | + writer.flush(); | |
| 291 | + //读取响应的内容 | |
| 292 | + return readResponseContent(conn.getInputStream()); | |
| 293 | + } finally { | |
| 294 | + if (null != conn) | |
| 295 | + conn.disconnect(); | |
| 296 | + if (null != writer) | |
| 297 | + writer.close(); | |
| 298 | + } | |
| 299 | + } | |
| 300 | + | |
| 301 | + | |
| 302 | + /** | |
| 303 | + * 转换对象为map | |
| 304 | + * | |
| 305 | + * @param object | |
| 306 | + * @param ignore | |
| 307 | + * @return | |
| 308 | + */ | |
| 309 | + public static Map<String, String> objectToMap(Object object, String... ignore) { | |
| 310 | + Map<String, String> tempMap = new LinkedHashMap<String, String>(); | |
| 311 | + //获取本类的Fields | |
| 312 | + for (Field f : object.getClass().getDeclaredFields()) { | |
| 313 | + if (!f.isAccessible()) { | |
| 314 | + f.setAccessible(true); | |
| 315 | + } | |
| 316 | + boolean ig = false; | |
| 317 | + if (ignore != null && ignore.length > 0) { | |
| 318 | + for (String i : ignore) { | |
| 319 | + if (i.equals(f.getName())) { | |
| 320 | + ig = true; | |
| 321 | + break; | |
| 322 | + } | |
| 323 | + } | |
| 324 | + } | |
| 325 | + if (ig) { | |
| 326 | + continue; | |
| 327 | + } else { | |
| 328 | + Object o = null; | |
| 329 | + try { | |
| 330 | + o = f.get(object); | |
| 331 | + } catch (IllegalArgumentException e) { | |
| 332 | + e.printStackTrace(); | |
| 333 | + } catch (IllegalAccessException e) { | |
| 334 | + e.printStackTrace(); | |
| 335 | + } | |
| 336 | + tempMap.put(f.getName(), o == null ? "" : o.toString()); | |
| 337 | + } | |
| 338 | + } | |
| 339 | + //获取基类的Fields | |
| 340 | + for (Field f : object.getClass().getFields()) { | |
| 341 | + if (!f.isAccessible()) { | |
| 342 | + f.setAccessible(true); | |
| 343 | + } | |
| 344 | + boolean ig = false; | |
| 345 | + if (ignore != null && ignore.length > 0) { | |
| 346 | + for (String i : ignore) { | |
| 347 | + if (i.equals(f.getName())) { | |
| 348 | + ig = true; | |
| 349 | + break; | |
| 350 | + } | |
| 351 | + } | |
| 352 | + } | |
| 353 | + if (ig) { | |
| 354 | + continue; | |
| 355 | + } else { | |
| 356 | + Object o = null; | |
| 357 | + try { | |
| 358 | + o = f.get(object); | |
| 359 | + } catch (IllegalArgumentException e) { | |
| 360 | + e.printStackTrace(); | |
| 361 | + } catch (IllegalAccessException e) { | |
| 362 | + e.printStackTrace(); | |
| 363 | + } | |
| 364 | + tempMap.put(f.getName(), o == null ? "" : o.toString()); | |
| 365 | + } | |
| 366 | + } | |
| 367 | + return tempMap; | |
| 368 | + } | |
| 369 | + | |
| 370 | + /** | |
| 371 | + * 将参数转为路径字符串 | |
| 372 | + * | |
| 373 | + * @param paramMap 参数 | |
| 374 | + * @return | |
| 375 | + */ | |
| 376 | + private static String getParamString(Map<String, String> paramMap) { | |
| 377 | + if (null == paramMap || paramMap.isEmpty()) { | |
| 378 | + return ""; | |
| 379 | + } | |
| 380 | + StringBuilder builder = new StringBuilder(); | |
| 381 | + for (String key : paramMap.keySet()) { | |
| 382 | + if (!key.equals("pd")) { | |
| 383 | + if (StringUtils.isNotEmpty(paramMap.get(key))) { | |
| 384 | + //传入值不为空 拼接字符串 | |
| 385 | + builder.append("&").append(key).append("=").append(paramMap.get(key)); | |
| 386 | + } else { | |
| 387 | + builder.append("&"); | |
| 388 | + } | |
| 389 | + } | |
| 390 | + | |
| 391 | + } | |
| 392 | + return new String(builder.deleteCharAt(0).toString()); | |
| 393 | + } | |
| 394 | + | |
| 395 | + | |
| 396 | + /** | |
| 397 | + * 读取响应字节流并将之转为字符串 | |
| 398 | + * | |
| 399 | + * @param in 要读取的字节流 | |
| 400 | + * @return | |
| 401 | + * @throws IOException | |
| 402 | + */ | |
| 403 | + private static String readResponseContent(InputStream in) throws IOException { | |
| 404 | + Reader reader = null; | |
| 405 | + StringBuilder content = new StringBuilder(); | |
| 406 | + try { | |
| 407 | + reader = new InputStreamReader(in, "utf-8"); | |
| 408 | + char[] buffer = new char[1024]; | |
| 409 | + int head = 0; | |
| 410 | + while ((head = reader.read(buffer)) > 0) { | |
| 411 | + content.append(new String(buffer, 0, head)); | |
| 412 | + } | |
| 413 | + String result = content.toString(); | |
| 414 | + System.out.println("readResponseContent.." + result); | |
| 415 | + return result; | |
| 416 | + } finally { | |
| 417 | + if (null != in) { | |
| 418 | + in.close(); | |
| 419 | + } | |
| 420 | + if (null != reader) { | |
| 421 | + reader.close(); | |
| 422 | + } | |
| 423 | + } | |
| 424 | + } | |
| 425 | + | |
| 426 | + /** | |
| 427 | + * 设置Http连接属性 | |
| 428 | + * | |
| 429 | + * @param conn http连接 | |
| 430 | + * @return | |
| 431 | + * @throws ProtocolException | |
| 432 | + * @throws Exception | |
| 433 | + */ | |
| 434 | + private static void setHttpUrlConnection(HttpURLConnection conn, | |
| 435 | + String requestMethod) throws ProtocolException { | |
| 436 | + conn.setRequestMethod(requestMethod); | |
| 437 | + conn.setRequestProperty("content-encoding", "UTF-8"); | |
| 438 | + conn.setRequestProperty("accept", "application/json"); | |
| 439 | + conn.setRequestProperty("Accept-Charset", "UTF-8"); | |
| 440 | + conn.setRequestProperty("Accept-Language", "zh-CN"); | |
| 441 | + | |
| 442 | + conn.setRequestProperty("User-Agent", | |
| 443 | + "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)"); | |
| 444 | + conn.setRequestProperty("Proxy-Connection", "Keep-Alive"); | |
| 445 | + | |
| 446 | + if (null != requestMethod && "POST".equals(requestMethod)) { | |
| 447 | + conn.setDoOutput(true); | |
| 448 | + conn.setDoInput(true); | |
| 449 | + } | |
| 450 | + } | |
| 451 | + | |
| 452 | + | |
| 453 | + /** | |
| 454 | + * 新http请求 | |
| 455 | + * | |
| 456 | + * @param requestUrl | |
| 457 | + * @param requestMethod | |
| 458 | + * @param object | |
| 459 | + * @return | |
| 460 | + */ | |
| 461 | + public static String httpRequest(String requestUrl, String requestMethod, Object object) { | |
| 462 | + //获取请求参数 | |
| 463 | + Map<String, String> params = objectToMap(object); | |
| 464 | + String outputStr = getParamString(params); | |
| 465 | + System.out.println("requestUrl:" + requestUrl); | |
| 466 | + System.out.println("outputStr:" + outputStr); | |
| 467 | + StringBuffer buffer = new StringBuffer(); | |
| 468 | + try { | |
| 469 | + URL url = new URL(requestUrl); | |
| 470 | + HttpURLConnection httpUrlConn = (HttpURLConnection) url.openConnection(); | |
| 471 | + httpUrlConn.setDoOutput(true); | |
| 472 | + httpUrlConn.setDoInput(true); | |
| 473 | + httpUrlConn.setUseCaches(false); | |
| 474 | + // 设置请求方式(GET/POST) | |
| 475 | + httpUrlConn.setRequestMethod(requestMethod); | |
| 476 | + httpUrlConn.connect(); | |
| 477 | + // 当有数据需要提交时 | |
| 478 | + if (null != outputStr) { | |
| 479 | + OutputStream outputStream = httpUrlConn.getOutputStream(); | |
| 480 | + // 注意编码格式,防止中文乱码 | |
| 481 | + outputStream.write(outputStr.getBytes("UTF-8")); | |
| 482 | + outputStream.close(); | |
| 483 | + } | |
| 484 | + // 将返回的输入流转换成字符串 | |
| 485 | + InputStream inputStream = httpUrlConn.getInputStream(); | |
| 486 | + InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8"); | |
| 487 | + BufferedReader bufferedReader = new BufferedReader(inputStreamReader); | |
| 488 | + | |
| 489 | + String str = null; | |
| 490 | + while ((str = bufferedReader.readLine()) != null) { | |
| 491 | + buffer.append(str); | |
| 492 | + } | |
| 493 | + bufferedReader.close(); | |
| 494 | + inputStreamReader.close(); | |
| 495 | + // 释放资源 | |
| 496 | + inputStream.close(); | |
| 497 | + inputStream = null; | |
| 498 | + httpUrlConn.disconnect(); | |
| 499 | + } catch (ConnectException ce) { | |
| 500 | + } catch (Exception e) { | |
| 501 | + e.printStackTrace(); | |
| 502 | + } | |
| 503 | + System.out.println("返回:" + buffer.toString()); | |
| 504 | + return buffer.toString(); | |
| 505 | + } | |
| 506 | + | |
| 507 | + | |
| 508 | +} | ... | ... |
src/main/java/com/genersoft/iot/vmp/vmanager/bean/UsLogin.java
0 → 100644
| 1 | +package com.genersoft.iot.vmp.vmanager.bean; | |
| 2 | + | |
| 3 | + | |
| 4 | +import org.apache.commons.lang3.builder.ToStringBuilder; | |
| 5 | +import org.apache.commons.lang3.builder.ToStringStyle; | |
| 6 | + | |
| 7 | +/** | |
| 8 | + * 岗位表 sys_post | |
| 9 | + * | |
| 10 | + * @author bsth | |
| 11 | + */ | |
| 12 | +public class UsLogin { | |
| 13 | + private static final long serialVersionUID = 1L; | |
| 14 | + | |
| 15 | + private String token; | |
| 16 | + | |
| 17 | + private String sysCode; | |
| 18 | + | |
| 19 | + public String getToken() { | |
| 20 | + return token; | |
| 21 | + } | |
| 22 | + | |
| 23 | + public void setToken(String token) { | |
| 24 | + this.token = token; | |
| 25 | + } | |
| 26 | + | |
| 27 | + public String getSysCode() { | |
| 28 | + return sysCode; | |
| 29 | + } | |
| 30 | + | |
| 31 | + public void setSysCode(String sysCode) { | |
| 32 | + this.sysCode = sysCode; | |
| 33 | + } | |
| 34 | + | |
| 35 | + @Override | |
| 36 | + public String toString() { | |
| 37 | + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) | |
| 38 | + .append("token", getToken()) | |
| 39 | + .append("sysCode", getSysCode()) | |
| 40 | + .toString(); | |
| 41 | + } | |
| 42 | +} | ... | ... |
src/main/java/com/genersoft/iot/vmp/vmanager/jt1078/platform/Jt1078OfCarController.java
| ... | ... | @@ -9,6 +9,7 @@ import com.alibaba.fastjson2.JSON; |
| 9 | 9 | import com.alibaba.fastjson2.JSONArray; |
| 10 | 10 | import com.alibaba.fastjson2.JSONException; |
| 11 | 11 | import com.alibaba.fastjson2.JSONObject; |
| 12 | +import com.genersoft.iot.vmp.common.StreamInfo; | |
| 12 | 13 | import com.genersoft.iot.vmp.conf.MediaConfig; |
| 13 | 14 | import com.genersoft.iot.vmp.conf.StreamProxyTask; |
| 14 | 15 | import com.genersoft.iot.vmp.conf.exception.ControllerException; |
| ... | ... | @@ -17,7 +18,9 @@ import com.genersoft.iot.vmp.conf.security.JwtUtils; |
| 17 | 18 | import com.genersoft.iot.vmp.conf.security.dto.JwtUser; |
| 18 | 19 | import com.genersoft.iot.vmp.jtt1078.app.VideoServerApp; |
| 19 | 20 | import com.genersoft.iot.vmp.jtt1078.publisher.PublishManager; |
| 21 | +import com.genersoft.iot.vmp.jtt1078.subscriber.RTMPPublisher; | |
| 20 | 22 | import com.genersoft.iot.vmp.media.zlm.dto.StreamPushItem; |
| 23 | +import com.genersoft.iot.vmp.service.IMediaService; | |
| 21 | 24 | import com.genersoft.iot.vmp.service.IStreamPushService; |
| 22 | 25 | import com.genersoft.iot.vmp.service.StremProxyService1078; |
| 23 | 26 | import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; |
| ... | ... | @@ -50,12 +53,14 @@ import org.slf4j.Logger; |
| 50 | 53 | import org.slf4j.LoggerFactory; |
| 51 | 54 | import org.springframework.beans.factory.annotation.Autowired; |
| 52 | 55 | import org.springframework.beans.factory.annotation.Value; |
| 56 | +import org.springframework.context.annotation.Bean; | |
| 53 | 57 | import org.springframework.core.io.InputStreamResource; |
| 54 | 58 | import org.springframework.data.redis.core.RedisTemplate; |
| 55 | 59 | import org.springframework.http.HttpHeaders; |
| 56 | 60 | import org.springframework.http.MediaType; |
| 57 | 61 | import org.springframework.http.ResponseEntity; |
| 58 | 62 | import org.springframework.scheduling.annotation.Scheduled; |
| 63 | +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; | |
| 59 | 64 | import org.springframework.util.Base64Utils; |
| 60 | 65 | import org.springframework.web.bind.annotation.*; |
| 61 | 66 | import sun.misc.Signal; |
| ... | ... | @@ -1146,13 +1151,13 @@ public class Jt1078OfCarController { |
| 1146 | 1151 | |
| 1147 | 1152 | @Nullable |
| 1148 | 1153 | private StreamContent getStreamContent(String stream) { |
| 1149 | - StreamContent streamContent = this.getStreamContentPlayURL(stream); | |
| 1154 | + StreamContent streamContent = getStreamContentPlayURL(stream); | |
| 1150 | 1155 | if (Objects.isNull(streamContent) || StringUtils.isEmpty(streamContent.getWs_flv())) { |
| 1151 | 1156 | streamContent = new StreamContent(); |
| 1152 | - String authKey = this.jt1078ConfigBean.getPushKey(); | |
| 1153 | - streamContent.setWs_flv(StringUtils.replace(this.jt1078ConfigBean.getWs() + authKey, "{stream}", stream)); | |
| 1154 | - streamContent.setWss_flv(StringUtils.replace(this.jt1078ConfigBean.getWss() + authKey, "{stream}", stream)); | |
| 1155 | - streamContent.setFlv(StringUtils.replace(this.jt1078ConfigBean.getDownloadFlv() + authKey, "{stream}", stream)); | |
| 1157 | + String authKey = jt1078ConfigBean.getPushKey(); | |
| 1158 | + streamContent.setWs_flv(StringUtils.replace(jt1078ConfigBean.getWs() + authKey, "{stream}", stream)); | |
| 1159 | + streamContent.setWss_flv(StringUtils.replace(jt1078ConfigBean.getWss() + authKey, "{stream}", stream)); | |
| 1160 | + streamContent.setFlv(StringUtils.replace(jt1078ConfigBean.getDownloadFlv() + authKey, "{stream}", stream)); | |
| 1156 | 1161 | } |
| 1157 | 1162 | return streamContent; |
| 1158 | 1163 | } | ... | ... |
src/main/java/com/genersoft/iot/vmp/vmanager/jt1078/platform/config/ThreadPoolTaskExecutorConfig.java
0 → 100644
| 1 | +package com.genersoft.iot.vmp.vmanager.jt1078.platform.config; | |
| 2 | + | |
| 3 | +import org.springframework.context.annotation.Bean; | |
| 4 | +import org.springframework.context.annotation.Configuration; | |
| 5 | +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; | |
| 6 | + | |
| 7 | +import java.util.concurrent.ThreadPoolExecutor; | |
| 8 | + | |
| 9 | +/** | |
| 10 | + * 批量播放线程池(优化配置) | |
| 11 | + * @Author WangXin | |
| 12 | + * @Data 2025/12/18 | |
| 13 | + * @Version 1.0.0 | |
| 14 | + */ | |
| 15 | +@Configuration | |
| 16 | +public class ThreadPoolTaskExecutorConfig { | |
| 17 | + | |
| 18 | + @Bean("deviceRequestExecutor") | |
| 19 | + public ThreadPoolTaskExecutor threadPoolTaskExecutor() { | |
| 20 | + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); | |
| 21 | + // 核心线程数:降低到50,减少CPU竞争 | |
| 22 | + executor.setCorePoolSize(50); | |
| 23 | + // 最大线程数:降低到100 | |
| 24 | + executor.setMaxPoolSize(100); | |
| 25 | + // 队列大小:增大到1000,让任务排队而不是创建过多线程 | |
| 26 | + executor.setQueueCapacity(1000); | |
| 27 | + // 线程名前缀,方便查日志 | |
| 28 | + executor.setThreadNamePrefix("Device-IO-"); | |
| 29 | + // 线程空闲时间:20秒后回收(更快释放资源) | |
| 30 | + executor.setKeepAliveSeconds(20); | |
| 31 | + // 拒绝策略:如果满了,由调用者线程执行(防止丢任务) | |
| 32 | + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); | |
| 33 | + // 优雅关闭:等待任务完成 | |
| 34 | + executor.setWaitForTasksToCompleteOnShutdown(true); | |
| 35 | + executor.setAwaitTerminationSeconds(60); | |
| 36 | + executor.initialize(); | |
| 37 | + return executor; | |
| 38 | + } | |
| 39 | + | |
| 40 | +} | ... | ... |
src/main/java/com/genersoft/iot/vmp/vmanager/jt1078/platform/config/TuohuaConfigBean.java
| ... | ... | @@ -179,11 +179,11 @@ public class TuohuaConfigBean { |
| 179 | 179 | List<CarData> carData = JSON.parseArray(json, CarData.class); |
| 180 | 180 | int count = 1; |
| 181 | 181 | if (CollectionUtils.isNotEmpty(carData)) { |
| 182 | - if (StringUtils.equals(profileActive, "wx-local")) { | |
| 182 | + if (StringUtils.equals(profileActive, "local")) { | |
| 183 | 183 | CarData value = carData.get(0); |
| 184 | 184 | // value.setSim("1030715050"); |
| 185 | - value.setSim2("13450328013"); | |
| 186 | - value.setSim("123456789011"); | |
| 185 | + value.setSim2("13450328009"); | |
| 186 | + value.setSim("3904517427"); | |
| 187 | 187 | map.put(value.getSim().replaceAll("^0+", ""), value); |
| 188 | 188 | if (StringUtils.isNotBlank(value.getSim2())) { |
| 189 | 189 | map.put(value.getSim2().replaceAll("^0+", ""), value); |
| ... | ... | @@ -357,11 +357,11 @@ public class TuohuaConfigBean { |
| 357 | 357 | hashMap.put("sim", formatSim(convertStr(ch.get("sim")))); |
| 358 | 358 | hashMap.put("sim2", formatSim(convertStr(ch.get("sim2")))); |
| 359 | 359 | hashMap.put("abnormalStatus", abnormalStatus); |
| 360 | - | |
| 361 | - if (StringUtils.equals(profileActive, "wx-local")) { | |
| 360 | + hashMap.put("carPlate", convertStr(ch.get("carPlate"))); | |
| 361 | + if (StringUtils.equals(profileActive, "local")) { | |
| 362 | 362 | // hashMap.put("sim","1030715050"); |
| 363 | - hashMap.put("sim", "3904517445"); | |
| 364 | - hashMap.put("sim2", "13450328013"); | |
| 363 | + hashMap.put("sim2", "13450328009"); | |
| 364 | + hashMap.put("sim", "3904517427"); | |
| 365 | 365 | } |
| 366 | 366 | return hashMap; |
| 367 | 367 | } | ... | ... |
src/main/java/com/genersoft/iot/vmp/vmanager/jt1078/platform/handler/HttpClientUtil.java
| 1 | 1 | package com.genersoft.iot.vmp.vmanager.jt1078.platform.handler; |
| 2 | 2 | |
| 3 | -import com.alibaba.fastjson2.JSON; | |
| 4 | 3 | import com.genersoft.iot.vmp.vmanager.jt1078.platform.ben.HttpClientPostEntity; |
| 5 | -import org.apache.commons.collections4.CollectionUtils; | |
| 6 | 4 | import org.apache.http.HttpEntity; |
| 7 | 5 | import org.apache.http.NameValuePair; |
| 8 | 6 | import org.apache.http.client.CookieStore; |
| 7 | +import org.apache.http.client.config.RequestConfig; | |
| 9 | 8 | import org.apache.http.client.entity.UrlEncodedFormEntity; |
| 10 | 9 | import org.apache.http.client.methods.CloseableHttpResponse; |
| 11 | 10 | import org.apache.http.client.methods.HttpGet; |
| 12 | 11 | import org.apache.http.client.methods.HttpPost; |
| 12 | +import org.apache.http.client.protocol.HttpClientContext; | |
| 13 | 13 | import org.apache.http.client.utils.URIBuilder; |
| 14 | 14 | import org.apache.http.entity.StringEntity; |
| 15 | 15 | import org.apache.http.impl.client.BasicCookieStore; |
| 16 | -import org.apache.http.impl.client.DefaultHttpClient; | |
| 16 | +import org.apache.http.impl.client.CloseableHttpClient; | |
| 17 | +import org.apache.http.impl.client.HttpClients; | |
| 18 | +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; | |
| 17 | 19 | import org.apache.http.impl.cookie.BasicClientCookie; |
| 18 | 20 | import org.apache.http.message.BasicNameValuePair; |
| 19 | -import org.apache.http.params.BasicHttpParams; | |
| 20 | -import org.apache.http.params.HttpConnectionParams; | |
| 21 | 21 | import org.apache.http.util.EntityUtils; |
| 22 | -import org.jetbrains.annotations.NotNull; | |
| 23 | 22 | import org.slf4j.Logger; |
| 24 | 23 | import org.slf4j.LoggerFactory; |
| 25 | 24 | import org.springframework.stereotype.Component; |
| 26 | 25 | |
| 26 | +import javax.annotation.PostConstruct; | |
| 27 | 27 | import java.io.IOException; |
| 28 | 28 | import java.net.URI; |
| 29 | -import java.net.URISyntaxException; | |
| 30 | -import java.util.*; | |
| 29 | +import java.util.ArrayList; | |
| 30 | +import java.util.List; | |
| 31 | +import java.util.Map; | |
| 32 | +import java.util.Set; | |
| 31 | 33 | |
| 32 | 34 | /** |
| 33 | - * @author liujun | |
| 34 | - * @date 2024年10月23日 13:25 | |
| 35 | + * 优化后的 HttpClientUtil | |
| 36 | + * 特性:支持连接池、高并发、线程安全 | |
| 35 | 37 | */ |
| 36 | 38 | @Component |
| 37 | 39 | public class HttpClientUtil { |
| 38 | 40 | private static final Logger log = LoggerFactory.getLogger(HttpClientUtil.class); |
| 39 | 41 | |
| 40 | - public HttpClientPostEntity doPost(String url, Map<String, String> params, String jsessionid) throws URISyntaxException, IOException { | |
| 41 | - long startTime = System.currentTimeMillis(); | |
| 42 | - // 创建Httpclient对象 | |
| 43 | - DefaultHttpClient httpclient = getHttpClient(); | |
| 44 | - // 定义请求的参数 | |
| 45 | - CookieStore cookieStore1 = combationCookie(jsessionid); | |
| 46 | - httpclient.setCookieStore(cookieStore1); | |
| 47 | - URIBuilder uriBuilder = new URIBuilder(url); | |
| 48 | - URI uri = uriBuilder.build(); | |
| 49 | - | |
| 50 | - // 创建http GET请求 | |
| 51 | - HttpPost httpPost = new HttpPost(uri); | |
| 52 | - List<NameValuePair> paramList = new ArrayList<>(); | |
| 53 | - if (params != null && params.size() > 0) { | |
| 54 | - Set<String> keySet = params.keySet(); | |
| 55 | - for (String key : keySet) { | |
| 56 | - paramList.add(new BasicNameValuePair(key, params.get(key))); | |
| 57 | - } | |
| 58 | - httpPost.setEntity(new UrlEncodedFormEntity(paramList)); | |
| 59 | - } | |
| 42 | + // 核心:保持一个全局单例的 HttpClient | |
| 43 | + private CloseableHttpClient httpClient; | |
| 44 | + | |
| 45 | + @PostConstruct | |
| 46 | + public void init() { | |
| 47 | + // 配置连接池 | |
| 48 | + PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(); | |
| 49 | + // 最大连接数 (设为 200,足够大) | |
| 50 | + cm.setMaxTotal(200); | |
| 51 | + // 【关键】每个路由(目标IP)的默认最大连接数 | |
| 52 | + // 必须大于 30,否则 30 个并发请求会排队!设置为 100 比较保险 | |
| 53 | + cm.setDefaultMaxPerRoute(100); | |
| 54 | + | |
| 55 | + // 配置默认请求参数 | |
| 56 | + RequestConfig requestConfig = RequestConfig.custom() | |
| 57 | + .setConnectTimeout(3000) // 连接超时 3s (握手时间) | |
| 58 | + .setSocketTimeout(10000) // 读取超时 10s (等待设备响应的时间,稍微给长点) | |
| 59 | + .setConnectionRequestTimeout(2000) // 从连接池获取连接的超时时间 | |
| 60 | + .build(); | |
| 61 | + | |
| 62 | + // 创建全局 Client | |
| 63 | + this.httpClient = HttpClients.custom() | |
| 64 | + .setConnectionManager(cm) | |
| 65 | + .setDefaultRequestConfig(requestConfig) | |
| 66 | + .build(); | |
| 67 | + } | |
| 60 | 68 | |
| 61 | - //response 对象 | |
| 69 | + /** | |
| 70 | + * 发送 POST 请求 (Form 表单格式) | |
| 71 | + */ | |
| 72 | + public HttpClientPostEntity doPost(String url, Map<String, String> params, String jsessionid) { | |
| 73 | + long startTime = System.currentTimeMillis(); | |
| 62 | 74 | CloseableHttpResponse response = null; |
| 63 | 75 | try { |
| 64 | - // 执行http get请求 | |
| 65 | - response = httpclient.execute(httpPost); | |
| 66 | - // 判断返回状态是否为200 | |
| 76 | + URIBuilder uriBuilder = new URIBuilder(url); | |
| 77 | + URI uri = uriBuilder.build(); | |
| 78 | + HttpPost httpPost = new HttpPost(uri); | |
| 79 | + | |
| 80 | + // 设置参数 | |
| 81 | + if (params != null && !params.isEmpty()) { | |
| 82 | + List<NameValuePair> paramList = new ArrayList<>(); | |
| 83 | + for (Map.Entry<String, String> entry : params.entrySet()) { | |
| 84 | + paramList.add(new BasicNameValuePair(entry.getKey(), entry.getValue())); | |
| 85 | + } | |
| 86 | + httpPost.setEntity(new UrlEncodedFormEntity(paramList, "UTF-8")); | |
| 87 | + } | |
| 88 | + | |
| 89 | + // 【关键】使用 Context 传递 Cookie,确保线程安全且互不干扰 | |
| 90 | + HttpClientContext context = HttpClientContext.create(); | |
| 91 | + context.setCookieStore(combationCookie(jsessionid)); | |
| 92 | + | |
| 93 | + // 执行请求 | |
| 94 | + response = httpClient.execute(httpPost, context); | |
| 95 | + | |
| 67 | 96 | if (response.getStatusLine().getStatusCode() == 200) { |
| 68 | - return combationReturnObj(response, httpclient,url, null,startTime); | |
| 97 | + return combationReturnObj(response, context.getCookieStore(), url, null, startTime); | |
| 98 | + } else { | |
| 99 | + log.warn("POST请求非200状态: {}, Code: {}", url, response.getStatusLine().getStatusCode()); | |
| 69 | 100 | } |
| 101 | + } catch (Exception e) { | |
| 102 | + log.error("POST Form请求异常, url: {}", url, e); | |
| 70 | 103 | } finally { |
| 71 | - if (response != null) { | |
| 72 | - response.close(); | |
| 73 | - } | |
| 74 | - httpclient.close(); | |
| 104 | + closeResponse(response); | |
| 75 | 105 | } |
| 76 | 106 | return null; |
| 77 | 107 | } |
| 78 | 108 | |
| 79 | - public HttpClientPostEntity doPost(String url, String requestBody, String jsessionid) throws URISyntaxException, IOException { | |
| 109 | + /** | |
| 110 | + * 发送 POST 请求 (JSON Body 格式) | |
| 111 | + */ | |
| 112 | + public HttpClientPostEntity doPost(String url, String requestBody, String jsessionid) { | |
| 80 | 113 | long startTime = System.currentTimeMillis(); |
| 81 | - // 创建Httpclient对象 | |
| 82 | - DefaultHttpClient httpclient = getHttpClient(); | |
| 83 | - // 定义请求的参数 | |
| 84 | - CookieStore cookieStore1 = combationCookie( jsessionid); | |
| 114 | + CloseableHttpResponse response = null; | |
| 115 | + try { | |
| 116 | + URIBuilder uriBuilder = new URIBuilder(url); | |
| 117 | + URI uri = uriBuilder.build(); | |
| 118 | + HttpPost httpPost = new HttpPost(uri); | |
| 85 | 119 | |
| 86 | - httpclient.setCookieStore(cookieStore1); | |
| 120 | + StringEntity stringEntity = new StringEntity(requestBody, "UTF-8"); | |
| 121 | + stringEntity.setContentType("application/json"); | |
| 122 | + httpPost.setEntity(stringEntity); | |
| 87 | 123 | |
| 88 | - URIBuilder uriBuilder = new URIBuilder(url); | |
| 89 | - URI uri = uriBuilder.build(); | |
| 124 | + // 使用 Context 传递 Cookie | |
| 125 | + HttpClientContext context = HttpClientContext.create(); | |
| 126 | + context.setCookieStore(combationCookie(jsessionid)); | |
| 90 | 127 | |
| 91 | - // 创建http POST请求 | |
| 92 | - HttpPost httpPost = new HttpPost(uri); | |
| 93 | - StringEntity stringEntity = new StringEntity(requestBody, "UTF-8"); | |
| 94 | - stringEntity.setContentType("application/json"); | |
| 95 | - httpPost.setEntity(stringEntity); | |
| 128 | + response = httpClient.execute(httpPost, context); | |
| 96 | 129 | |
| 97 | - //response 对象 | |
| 98 | - CloseableHttpResponse response = null; | |
| 99 | - try { | |
| 100 | - // 执行http get请求 | |
| 101 | - response = httpclient.execute(httpPost); | |
| 102 | - // 判断返回状态是否为200 | |
| 103 | 130 | if (response.getStatusLine().getStatusCode() == 200) { |
| 104 | - return combationReturnObj(response, httpclient,url,requestBody,startTime); | |
| 131 | + return combationReturnObj(response, context.getCookieStore(), url, requestBody, startTime); | |
| 132 | + } else { | |
| 133 | + log.warn("POST请求非200状态: {}, Code: {}", url, response.getStatusLine().getStatusCode()); | |
| 105 | 134 | } |
| 106 | 135 | } catch (Exception e) { |
| 107 | - log.error("请求数据异常", e); | |
| 136 | + log.error("POST JSON请求异常, url: {}", url, e); | |
| 108 | 137 | } finally { |
| 109 | - if (response != null) { | |
| 110 | - response.close(); | |
| 111 | - } | |
| 112 | - httpclient.close(); | |
| 138 | + closeResponse(response); | |
| 113 | 139 | } |
| 114 | 140 | return null; |
| 115 | 141 | } |
| 116 | 142 | |
| 117 | - | |
| 118 | - public CookieStore combationCookie(String jsessionid) { | |
| 119 | - CookieStore cookieStore1 = new BasicCookieStore(); | |
| 120 | - // cookieStore1.addCookie(new BasicClientCookie("SECKEY_ABVK", seckeyAbvk)); | |
| 121 | - // cookieStore1.addCookie(new BasicClientCookie("BMAP_SECKEY", bmapSeckey)); | |
| 122 | - cookieStore1.addCookie(new BasicClientCookie("JSESSIONID", jsessionid)); | |
| 123 | - | |
| 124 | - CookieStore cookieStore = new BasicCookieStore(); | |
| 125 | - | |
| 126 | - int size = CollectionUtils.size(cookieStore.getCookies()); | |
| 127 | - for (int i = 0; i < size; i++) { | |
| 128 | - cookieStore1.addCookie(cookieStore.getCookies().get(i)); | |
| 129 | - | |
| 130 | - } | |
| 131 | - return cookieStore1; | |
| 132 | - } | |
| 133 | - | |
| 134 | - public HttpClientPostEntity doGet(String url, String jsessionid) throws URISyntaxException, IOException { | |
| 143 | + /** | |
| 144 | + * 发送 GET 请求 | |
| 145 | + */ | |
| 146 | + public HttpClientPostEntity doGet(String url, String jsessionid) { | |
| 135 | 147 | long startTime = System.currentTimeMillis(); |
| 136 | - // 创建Httpclient对象 | |
| 137 | - DefaultHttpClient httpclient = getHttpClient(); | |
| 138 | - // 定义请求的参数 | |
| 139 | - CookieStore cookieStore1 = combationCookie(jsessionid); | |
| 140 | -// | |
| 141 | -// httpclient.setCookieStore(cookieStore1); | |
| 148 | + CloseableHttpResponse response = null; | |
| 149 | + try { | |
| 150 | + URIBuilder uriBuilder = new URIBuilder(url); | |
| 151 | + URI uri = uriBuilder.build(); | |
| 152 | + HttpGet httpGet = new HttpGet(uri); | |
| 142 | 153 | |
| 143 | - URIBuilder uriBuilder = new URIBuilder(url); | |
| 144 | - URI uri = uriBuilder.build(); | |
| 154 | + // 使用 Context 传递 Cookie | |
| 155 | + HttpClientContext context = HttpClientContext.create(); | |
| 156 | + context.setCookieStore(combationCookie(jsessionid)); | |
| 145 | 157 | |
| 146 | - // 创建http GET请求 | |
| 147 | - HttpGet httpGet = new HttpGet(uri); | |
| 148 | - httpGet.addHeader("Cookie", jsessionid); | |
| 158 | + response = httpClient.execute(httpGet, context); | |
| 149 | 159 | |
| 150 | - //response 对象 | |
| 151 | - CloseableHttpResponse response = null; | |
| 152 | - try { | |
| 153 | - // 执行http get请求 | |
| 154 | - response = httpclient.execute(httpGet); | |
| 155 | - // 判断返回状态是否为200 | |
| 156 | 160 | if (response.getStatusLine().getStatusCode() == 200) { |
| 157 | - return combationReturnObj(response, httpclient,url,null,startTime); | |
| 161 | + return combationReturnObj(response, context.getCookieStore(), url, null, startTime); | |
| 158 | 162 | } |
| 163 | + } catch (Exception e) { | |
| 164 | + log.error("GET请求异常, url: {}", url, e); | |
| 159 | 165 | } finally { |
| 160 | - if (response != null) { | |
| 161 | - response.close(); | |
| 162 | - } | |
| 163 | - httpclient.close(); | |
| 166 | + closeResponse(response); | |
| 164 | 167 | } |
| 165 | 168 | return null; |
| 166 | 169 | } |
| 167 | 170 | |
| 168 | - public boolean doGetNoResult(String url) throws URISyntaxException, IOException { | |
| 169 | - // 创建Httpclient对象 | |
| 170 | - DefaultHttpClient httpclient = getHttpClient(); | |
| 171 | - // 定义请求的参数 | |
| 172 | -// | |
| 173 | -// httpclient.setCookieStore(cookieStore1); | |
| 174 | - | |
| 175 | - URIBuilder uriBuilder = new URIBuilder(url); | |
| 176 | - URI uri = uriBuilder.build(); | |
| 177 | - | |
| 178 | - // 创建http GET请求 | |
| 179 | - HttpGet httpGet = new HttpGet(uri); | |
| 180 | - | |
| 181 | - //response 对象 | |
| 171 | + public boolean doGetNoResult(String url) { | |
| 182 | 172 | CloseableHttpResponse response = null; |
| 183 | 173 | try { |
| 184 | - log.info("url:[{}]",url); | |
| 185 | - // 执行http get请求 | |
| 186 | - response = httpclient.execute(httpGet); | |
| 187 | - // 判断返回状态是否为200 | |
| 188 | - if (response.getStatusLine().getStatusCode() == 200) { | |
| 189 | - return true; | |
| 190 | - } | |
| 174 | + log.info("url:[{}]", url); | |
| 175 | + HttpGet httpGet = new HttpGet(url); | |
| 176 | + // 注意:这里没有传 cookie,如果需要可以重载 | |
| 177 | + response = httpClient.execute(httpGet); | |
| 178 | + return response.getStatusLine().getStatusCode() == 200; | |
| 179 | + } catch (Exception e) { | |
| 180 | + log.error("doGetNoResult异常", e); | |
| 191 | 181 | } finally { |
| 192 | - if (response != null) { | |
| 193 | - response.close(); | |
| 194 | - } | |
| 195 | - httpclient.close(); | |
| 182 | + closeResponse(response); | |
| 196 | 183 | } |
| 197 | 184 | return false; |
| 198 | 185 | } |
| 199 | 186 | |
| 187 | + // --- 辅助方法 --- | |
| 188 | + | |
| 200 | 189 | /** |
| 201 | - * 检查设备是否注册 | |
| 202 | - * @param response | |
| 203 | - * @param httpclient | |
| 204 | - * @param url | |
| 205 | - * @param requestBody | |
| 206 | - * @return | |
| 207 | - * @throws IOException | |
| 190 | + * 构建 CookieStore | |
| 208 | 191 | */ |
| 209 | - @NotNull | |
| 210 | - private static HttpClientPostEntity combationReturnObj(CloseableHttpResponse response, DefaultHttpClient httpclient,String url,String requestBody,long startTime) throws IOException { | |
| 211 | - HttpEntity httpEntity = response.getEntity(); | |
| 192 | + private CookieStore combationCookie(String jsessionid) { | |
| 193 | + BasicCookieStore cookieStore = new BasicCookieStore(); | |
| 194 | + if (jsessionid != null) { | |
| 195 | + // 注意:Cookie 最好设置 Domain 和 Path,否则可能不生效,这里保持原逻辑 | |
| 196 | + BasicClientCookie cookie = new BasicClientCookie("JSESSIONID", jsessionid); | |
| 197 | + // 如果知道 domain 最好设置上,例如: cookie.setDomain("192.168.1.100"); | |
| 198 | + cookie.setPath("/"); | |
| 199 | + cookieStore.addCookie(cookie); | |
| 200 | + } | |
| 201 | + return cookieStore; | |
| 202 | + } | |
| 212 | 203 | |
| 213 | - CookieStore cookieStore = httpclient.getCookieStore(); | |
| 204 | + /** | |
| 205 | + * 处理返回结果 | |
| 206 | + */ | |
| 207 | + private HttpClientPostEntity combationReturnObj(CloseableHttpResponse response, CookieStore cookieStore, String url, String requestBody, long startTime) throws IOException { | |
| 208 | + HttpEntity httpEntity = response.getEntity(); | |
| 214 | 209 | String result = EntityUtils.toString(httpEntity, "UTF-8"); |
| 215 | 210 | |
| 216 | 211 | HttpClientPostEntity postEntity = new HttpClientPostEntity(); |
| 217 | 212 | postEntity.setCookieStore(cookieStore); |
| 218 | 213 | postEntity.setResultStr(result); |
| 219 | - log.info("url:{};requestBody:{};response :{}; 耗时: {}s ",url,requestBody,"请求成功",System.currentTimeMillis()-startTime); | |
| 214 | + | |
| 215 | + // 确保 Entity 被消耗完,释放连接回池 | |
| 216 | + EntityUtils.consume(httpEntity); | |
| 217 | + | |
| 218 | + log.info("url:{}; 耗时: {}ms", url, System.currentTimeMillis() - startTime); | |
| 220 | 219 | return postEntity; |
| 221 | 220 | } |
| 222 | 221 | |
| 223 | - | |
| 224 | 222 | /** |
| 225 | - * 获取 HttpClient,主要是封装了超时设置 | |
| 226 | - * @return | |
| 223 | + * 安全关闭 Response (注意:不要关闭 httpClient!) | |
| 227 | 224 | */ |
| 228 | - public DefaultHttpClient getHttpClient(){ | |
| 229 | - BasicHttpParams httpParams = new BasicHttpParams(); | |
| 230 | - HttpConnectionParams.setConnectionTimeout(httpParams, 5000); | |
| 231 | - HttpConnectionParams.setSoTimeout(httpParams, 15000); | |
| 232 | - DefaultHttpClient client = new DefaultHttpClient(httpParams); | |
| 233 | - return client; | |
| 225 | + private void closeResponse(CloseableHttpResponse response) { | |
| 226 | + if (response != null) { | |
| 227 | + try { | |
| 228 | + response.close(); | |
| 229 | + } catch (IOException e) { | |
| 230 | + // ignore | |
| 231 | + } | |
| 232 | + } | |
| 234 | 233 | } |
| 235 | 234 | } | ... | ... |
src/main/java/com/genersoft/iot/vmp/vmanager/user/UserController.java
| 1 | 1 | package com.genersoft.iot.vmp.vmanager.user; |
| 2 | 2 | |
| 3 | +import com.alibaba.fastjson2.JSON; | |
| 4 | +import com.alibaba.fastjson2.JSONObject; | |
| 3 | 5 | import com.genersoft.iot.vmp.conf.exception.ControllerException; |
| 4 | 6 | import com.genersoft.iot.vmp.conf.security.JwtUtils; |
| 5 | 7 | import com.genersoft.iot.vmp.conf.security.SecurityUtils; |
| ... | ... | @@ -9,7 +11,9 @@ import com.genersoft.iot.vmp.service.IUserService; |
| 9 | 11 | import com.genersoft.iot.vmp.storager.mapper.dto.Role; |
| 10 | 12 | import com.genersoft.iot.vmp.storager.mapper.dto.User; |
| 11 | 13 | import com.genersoft.iot.vmp.utils.DateUtil; |
| 14 | +import com.genersoft.iot.vmp.utils.HttpClientUtil; | |
| 12 | 15 | import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; |
| 16 | +import com.genersoft.iot.vmp.vmanager.bean.UsLogin; | |
| 13 | 17 | import com.genersoft.iot.vmp.vmanager.bean.WVPResult; |
| 14 | 18 | import com.github.pagehelper.PageInfo; |
| 15 | 19 | import io.swagger.v3.oas.annotations.Operation; |
| ... | ... | @@ -28,6 +32,7 @@ import javax.servlet.http.HttpServletRequest; |
| 28 | 32 | import javax.servlet.http.HttpServletResponse; |
| 29 | 33 | import java.time.LocalDateTime; |
| 30 | 34 | import java.util.List; |
| 35 | +import java.util.Map; | |
| 31 | 36 | |
| 32 | 37 | @Tag(name = "用户管理") |
| 33 | 38 | @RestController |
| ... | ... | @@ -44,7 +49,6 @@ public class UserController { |
| 44 | 49 | private IRoleService roleService; |
| 45 | 50 | |
| 46 | 51 | @GetMapping("/login") |
| 47 | - @PostMapping("/login") | |
| 48 | 52 | @Operation(summary = "登录", description = "登录成功后返回AccessToken, 可以从返回值获取到也可以从响应头中获取到," + |
| 49 | 53 | "后续的请求需要添加请求头 'access-token'或者放在参数里") |
| 50 | 54 | |
| ... | ... | @@ -68,6 +72,56 @@ public class UserController { |
| 68 | 72 | } |
| 69 | 73 | |
| 70 | 74 | |
| 75 | + @PostMapping("/getInfo") | |
| 76 | + public User getInfo(@RequestBody Map<String,String> map) { | |
| 77 | + String token = map.get("token"); | |
| 78 | + if (token != null) { | |
| 79 | + String sysCode = "SYSUS004"; | |
| 80 | + String url = "http://10.10.2.23:8112/prod-api/system/utilitySystem/checkToken"; | |
| 81 | +// //根据自己的网络环境自行选择访问方式 | |
| 82 | +// //外网ip http://118.113.164.50:8112 | |
| 83 | +// /prod-api/system/utilitySystem/checkToken | |
| 84 | +// //宿主机ip 10.10.2.23:8112 | |
| 85 | +// /prod-api/system/utilitySystem/checkToken | |
| 86 | +// //容器ip 172.17.0.8:8112 | |
| 87 | +// /prod-api/system/utilitySystem/checkToken | |
| 88 | + UsLogin usLogin = new UsLogin(); | |
| 89 | + usLogin.setToken(token); | |
| 90 | + usLogin.setSysCode(sysCode); | |
| 91 | + String dataJsonStr = HttpClientUtil.httpRequest(url, "POST", usLogin); | |
| 92 | + | |
| 93 | + JSONObject jsonObject = JSON.parseObject(dataJsonStr); | |
| 94 | + JSONObject dataJson = jsonObject.getJSONObject("data"); | |
| 95 | + String resCode = dataJson.getString("code"); | |
| 96 | + /** | |
| 97 | + * 登陆校验失败 | |
| 98 | + */ | |
| 99 | + if ("9999".equals(resCode) || "9998".equals(resCode)) { | |
| 100 | + throw new RuntimeException(jsonObject.getString("msgUser")); | |
| 101 | + } | |
| 102 | + | |
| 103 | + /** | |
| 104 | + * 回调数据 | |
| 105 | + */ | |
| 106 | + JSONObject resDataJson = dataJson.getJSONObject("data"); | |
| 107 | + /** | |
| 108 | + * 用户名 | |
| 109 | + */ | |
| 110 | + String username = resDataJson.getString("userName"); | |
| 111 | + | |
| 112 | + User user = userService.selectUserByUserName(username); | |
| 113 | + | |
| 114 | + if (user == null){ | |
| 115 | + throw new RuntimeException("用户不存在"); | |
| 116 | + } | |
| 117 | + | |
| 118 | + return user; | |
| 119 | + }else { | |
| 120 | + throw new RuntimeException("token无效"); | |
| 121 | + } | |
| 122 | + } | |
| 123 | + | |
| 124 | + | |
| 71 | 125 | @PostMapping("/changePassword") |
| 72 | 126 | @Operation(summary = "修改密码", security = @SecurityRequirement(name = JwtUtils.HEADER)) |
| 73 | 127 | @Parameter(name = "username", description = "用户名", required = true) | ... | ... |
src/main/resources/application-wx-local.yml
| ... | ... | @@ -54,7 +54,7 @@ spring: |
| 54 | 54 | max-lifetime: 1200000 # 是池中连接关闭后的最长生命周期(以毫秒为单位) |
| 55 | 55 | #[可选] WVP监听的HTTP端口, 网页和接口调用都是这个端口 |
| 56 | 56 | server: |
| 57 | - port: 16030 | |
| 57 | + port: 18090 | |
| 58 | 58 | # [可选] HTTPS配置, 默认不开启 |
| 59 | 59 | ssl: |
| 60 | 60 | # [可选] 是否开启HTTPS访问 |
| ... | ... | @@ -185,8 +185,10 @@ tuohua: |
| 185 | 185 | userName: yuanxiaohu |
| 186 | 186 | password: Yxiaohu1.0 |
| 187 | 187 | rest: |
| 188 | - baseURL: http://10.10.2.20:9089/webservice/rest | |
| 189 | - password: bafb2b44a07a02e5e9912f42cd197423884116a8 | |
| 188 | +# baseURL: http://10.10.2.20:9089/webservice/rest | |
| 189 | +# password: bafb2b44a07a02e5e9912f42cd197423884116a8 | |
| 190 | + baseURL: http://192.168.168.152:9089/webservice/rest | |
| 191 | + password: bafb2b44a07a02e5e9912f42cd197423884116a8 | |
| 190 | 192 | tree: |
| 191 | 193 | url: |
| 192 | 194 | company: http://${my.ip}:9088/video/tree |
| ... | ... | @@ -206,10 +208,10 @@ tuohua: |
| 206 | 208 | addPortVal: 0 |
| 207 | 209 | pushURL: http://${my.ip}:3333/new/server/{pushKey}/{port}/{httpPort} |
| 208 | 210 | stopPushURL: http://${my.ip}:3333/stop/channel/{pushKey}/{port}/{httpPort} |
| 209 | - url: http://10.10.2.20:8100/device/{0} | |
| 210 | - new_url: http://10.10.2.20:8100/device | |
| 211 | 211 | # url: http://10.10.2.20:8100/device/{0} |
| 212 | 212 | # new_url: http://10.10.2.20:8100/device |
| 213 | + url: http://192.168.168.152:8100/device/{0} | |
| 214 | + new_url: http://192.168.168.152:8100/device | |
| 213 | 215 | historyListPort: 9205 |
| 214 | 216 | history_upload: 9206 |
| 215 | 217 | playHistoryPort: 9201 | ... | ... |
web_src/build/webpack.base.conf.js
web_src/config/index.js
| ... | ... | @@ -11,14 +11,14 @@ module.exports = { |
| 11 | 11 | assetsPublicPath: "/", |
| 12 | 12 | proxyTable: { |
| 13 | 13 | "/debug": { |
| 14 | - target: "http://127.0.0.1:16030", | |
| 14 | + target: "http://127.0.0.1:18090", | |
| 15 | 15 | changeOrigin: true, |
| 16 | 16 | pathRewrite: { |
| 17 | 17 | "^/debug": "/", |
| 18 | 18 | }, |
| 19 | 19 | }, |
| 20 | 20 | "/static/snap": { |
| 21 | - target: "http://127.0.0.1:16030", | |
| 21 | + target: "http://127.0.0.1:18090", | |
| 22 | 22 | changeOrigin: true, |
| 23 | 23 | // pathRewrite: { |
| 24 | 24 | // '^/static/snap': '/static/snap' | ... | ... |
web_src/index.html
| ... | ... | @@ -10,6 +10,7 @@ |
| 10 | 10 | </head> |
| 11 | 11 | <body> |
| 12 | 12 | <script type="text/javascript" src="./static/js/jessibuca/jessibuca.js"></script> |
| 13 | + <script type="text/javascript" src="./static/EasyPlayer-pro.js"></script> | |
| 13 | 14 | <script type="text/javascript" src="./static/js/EasyWasmPlayer.js"></script> |
| 14 | 15 | <script type="text/javascript" src="./static/js/liveplayer-lib.min.js"></script> |
| 15 | 16 | <script type="text/javascript" src="./static/js/ZLMRTCClient.js"></script> | ... | ... |
web_src/package.json
| ... | ... | @@ -14,6 +14,7 @@ |
| 14 | 14 | }, |
| 15 | 15 | "dependencies": { |
| 16 | 16 | "@liveqing/liveplayer": "^2.7.10", |
| 17 | + "@wchbrad/vue-easy-tree": "^1.0.13", | |
| 17 | 18 | "axios": "^0.24.0", |
| 18 | 19 | "core-js": "^2.6.5", |
| 19 | 20 | "echarts": "^4.9.0", |
| ... | ... | @@ -24,6 +25,7 @@ |
| 24 | 25 | "moment": "^2.29.1", |
| 25 | 26 | "ol": "^6.14.1", |
| 26 | 27 | "postcss-pxtorem": "^5.1.1", |
| 28 | + "splitpanes": "^2.4.1", | |
| 27 | 29 | "uuid": "^8.3.2", |
| 28 | 30 | "v-charts": "^1.19.0", |
| 29 | 31 | "vue": "^2.6.11", |
| ... | ... | @@ -49,6 +51,7 @@ |
| 49 | 51 | "chalk": "^2.0.1", |
| 50 | 52 | "copy-webpack-plugin": "^4.6.0", |
| 51 | 53 | "css-loader": "^0.28.11", |
| 54 | + "dayjs": "^1.11.13", | |
| 52 | 55 | "extract-text-webpack-plugin": "^3.0.0", |
| 53 | 56 | "file-loader": "^1.1.4", |
| 54 | 57 | "friendly-errors-webpack-plugin": "^1.6.1", | ... | ... |
web_src/src/App.vue
| ... | ... | @@ -35,8 +35,6 @@ export default { |
| 35 | 35 | }catch (e) { |
| 36 | 36 | console.error(e) |
| 37 | 37 | } |
| 38 | - //如果没有登录状态则跳转到登录页 | |
| 39 | - this.$router.push('/login'); | |
| 40 | 38 | } |
| 41 | 39 | }, |
| 42 | 40 | |
| ... | ... | @@ -58,6 +56,9 @@ body, |
| 58 | 56 | background-color: #e9eef3; |
| 59 | 57 | height: 100%; |
| 60 | 58 | } |
| 59 | +#app .theme-picker { | |
| 60 | + display: none; | |
| 61 | +} | |
| 61 | 62 | .el-header, |
| 62 | 63 | .el-footer { |
| 63 | 64 | /* background-color: #b3c0d1; */ | ... | ... |
web_src/src/components/CarouselConfig.vue
0 → 100644
| 1 | +<template> | |
| 2 | + <el-dialog | |
| 3 | + title="轮播策略配置" | |
| 4 | + :visible.sync="visible" | |
| 5 | + width="650px" | |
| 6 | + append-to-body | |
| 7 | + @close="resetForm" | |
| 8 | + > | |
| 9 | + <el-form ref="form" :model="form" label-width="120px" :rules="rules"> | |
| 10 | + | |
| 11 | + <!-- 1. 轮播范围 --> | |
| 12 | + <el-form-item label="轮播范围" prop="sourceType"> | |
| 13 | + <el-radio-group v-model="form.sourceType"> | |
| 14 | + <el-radio label="all_online">所有在线设备 (自动同步)</el-radio> | |
| 15 | + <!-- 【修改】开放手动选择 --> | |
| 16 | + <el-radio label="custom">手动选择设备</el-radio> | |
| 17 | + </el-radio-group> | |
| 18 | + | |
| 19 | + <!-- 【新增】手动选择的树形控件 --> | |
| 20 | + <div v-show="form.sourceType === 'custom'" class="device-select-box"> | |
| 21 | + <el-input | |
| 22 | + placeholder="搜索设备名称(仅搜索已加载节点)" | |
| 23 | + v-model="filterText" | |
| 24 | + size="mini" | |
| 25 | + style="margin-bottom: 5px;"> | |
| 26 | + </el-input> | |
| 27 | + <el-tree | |
| 28 | + ref="deviceTree" | |
| 29 | + :props="treeProps" | |
| 30 | + :load="loadNode" | |
| 31 | + lazy | |
| 32 | + show-checkbox | |
| 33 | + node-key="code" | |
| 34 | + height="250px" | |
| 35 | + style="height: 250px; overflow-y: auto; border: 1px solid #dcdfe6; border-radius: 4px; padding: 5px;" | |
| 36 | + ></el-tree> | |
| 37 | + </div> | |
| 38 | + | |
| 39 | + | |
| 40 | + <div class="tip-text"> | |
| 41 | + <i class="el-icon-info"></i> | |
| 42 | + {{ form.sourceType === 'all_online' | |
| 43 | + ? '将自动从左侧设备列表中筛选状态为"在线"的设备进行循环播放。' | |
| 44 | + : '请勾选上方需要轮播的设备或通道。勾选父级设备代表选中其下所有通道。' | |
| 45 | + }} | |
| 46 | + <div style="margin-top: 5px; font-weight: bold; color: #E6A23C;">⚠️ 为保证播放流畅,轮播间隔建议设置为45秒以上</div> | |
| 47 | + </div> | |
| 48 | + </el-form-item> | |
| 49 | + | |
| 50 | + <!-- 2. 分屏布局 (保持不变) --> | |
| 51 | + <el-form-item label="分屏布局" prop="layout"> | |
| 52 | + <!-- ... 保持不变 ... --> | |
| 53 | + <el-select v-model="form.layout" placeholder="请选择布局" style="width: 100%"> | |
| 54 | + <el-option label="四分屏 (2x2)" value="4"></el-option> | |
| 55 | + <el-option label="九分屏 (3x3)" value="9"></el-option> | |
| 56 | + <el-option label="十六分屏 (4x4)" value="16"></el-option> | |
| 57 | + <el-option label="二十五分屏 (5x5)" value="25"></el-option> | |
| 58 | + <el-option label="三十六分屏 (6x6)" value="36"></el-option> | |
| 59 | + <el-option label="1+9 异形屏" value="1+9"></el-option> | |
| 60 | + <el-option label="1+11 异形屏" value="1+11"></el-option> | |
| 61 | + </el-select> | |
| 62 | + </el-form-item> | |
| 63 | + | |
| 64 | + <!-- ... 其它配置保持不变 ... --> | |
| 65 | + <el-form-item label="轮播间隔" prop="interval"> | |
| 66 | + <el-input-number v-model="form.interval" :min="30" :step="5" step-strictly controls-position="right"></el-input-number> | |
| 67 | + <span class="unit-text">秒</span> | |
| 68 | + <div style="font-size: 12px; color: #909399; margin-top: 5px;"> | |
| 69 | + 提示:为保证播放流畅,最小间隔30秒,建议设置45秒以上 | |
| 70 | + </div> | |
| 71 | + </el-form-item> | |
| 72 | + | |
| 73 | + <!-- 执行模式保持不变 --> | |
| 74 | + <el-form-item label="执行模式" prop="runMode"> | |
| 75 | + <el-radio-group v-model="form.runMode"> | |
| 76 | + <el-radio label="manual">手动控制 (立即开始,手动停止)</el-radio> | |
| 77 | + <el-radio label="schedule">定时计划 (自动启停)</el-radio> | |
| 78 | + </el-radio-group> | |
| 79 | + </el-form-item> | |
| 80 | + <!-- 时段选择保持不变 --> | |
| 81 | + <transition name="el-zoom-in-top"> | |
| 82 | + <div v-if="form.runMode === 'schedule'" class="schedule-box"> | |
| 83 | + <el-form-item label="生效时段" prop="timeRange" label-width="80px" style="margin-bottom: 0"> | |
| 84 | + <el-time-picker | |
| 85 | + is-range | |
| 86 | + v-model="form.timeRange" | |
| 87 | + range-separator="至" | |
| 88 | + start-placeholder="开始时间" | |
| 89 | + end-placeholder="结束时间" | |
| 90 | + value-format="HH:mm:ss" | |
| 91 | + style="width: 100%" | |
| 92 | + > | |
| 93 | + </el-time-picker> | |
| 94 | + </el-form-item> | |
| 95 | + </div> | |
| 96 | + </transition> | |
| 97 | + | |
| 98 | + </el-form> | |
| 99 | + <div slot="footer" class="dialog-footer"> | |
| 100 | + <el-button @click="visible = false">关 闭</el-button> | |
| 101 | + <el-button type="primary" @click="handleSave">确认并启动</el-button> | |
| 102 | + </div> | |
| 103 | + </el-dialog> | |
| 104 | +</template> | |
| 105 | + | |
| 106 | +<script> | |
| 107 | +export default { | |
| 108 | + name: "CarouselConfig", | |
| 109 | + // 【新增】接收父组件传来的设备树数据 | |
| 110 | + props: { | |
| 111 | + deviceTreeData: { | |
| 112 | + type: Array, | |
| 113 | + default: () => [] | |
| 114 | + } | |
| 115 | + }, | |
| 116 | + data() { | |
| 117 | + // 定义一个自定义校验函数 | |
| 118 | + const validateTimeRange = (rule, value, callback) => { | |
| 119 | + if (!value || value.length !== 2) { | |
| 120 | + return callback(new Error('请选择生效时段')); | |
| 121 | + } | |
| 122 | + | |
| 123 | + // 1. 辅助函数:将 HH:mm:ss 转为秒 | |
| 124 | + const toSeconds = (str) => { | |
| 125 | + const [h, m, s] = str.split(':').map(Number); | |
| 126 | + return h * 3600 + m * 60 + s; | |
| 127 | + }; | |
| 128 | + | |
| 129 | + const start = toSeconds(value[0]); | |
| 130 | + const end = toSeconds(value[1]); | |
| 131 | + let duration = end - start; | |
| 132 | + | |
| 133 | + // 处理跨天情况 (例如 23:00 到 01:00) | |
| 134 | + if (duration < 0) { | |
| 135 | + duration += 24 * 3600; | |
| 136 | + } | |
| 137 | + | |
| 138 | + // 2. 核心校验:时长必须大于间隔 | |
| 139 | + if (duration < this.form.interval) { | |
| 140 | + return callback(new Error(`时段跨度(${duration}s) 不能小于 轮播间隔(${this.form.interval}s)`)); | |
| 141 | + } | |
| 142 | + | |
| 143 | + callback(); | |
| 144 | + }; | |
| 145 | + return { | |
| 146 | + visible: false, | |
| 147 | + filterText: '', | |
| 148 | + form: { | |
| 149 | + sourceType: 'all_online', | |
| 150 | + layout: '16', | |
| 151 | + interval: 60, // 默认60秒 | |
| 152 | + runMode: 'manual', | |
| 153 | + timeRange: ['08:00:00', '18:00:00'], | |
| 154 | + selectedDevices: [] // 存储选中的设备 | |
| 155 | + }, | |
| 156 | + treeProps: { | |
| 157 | + label: 'name', | |
| 158 | + children: 'children', | |
| 159 | + isLeaf: (data) => data.type === '5' // 假设 type 5 是通道(叶子) | |
| 160 | + }, | |
| 161 | + rules: { interval: [ | |
| 162 | + { required: true, message: '间隔不能为空' }, | |
| 163 | + { type: 'number', min: 30, message: '间隔最少为30秒,以确保视频流有足够时间加载' } | |
| 164 | + ], | |
| 165 | + timeRange: [ | |
| 166 | + { required: true, validator: validateTimeRange, trigger: 'change' } // 使用自定义校验 | |
| 167 | + ] | |
| 168 | + } | |
| 169 | + }; | |
| 170 | + }, | |
| 171 | + watch: { | |
| 172 | + // 监听搜索框 | |
| 173 | + filterText(val) { | |
| 174 | + // 添加安全检查,防止树未挂载时报错 | |
| 175 | + if (this.$refs.deviceTree) { | |
| 176 | + this.$refs.deviceTree.filter(val); | |
| 177 | + } | |
| 178 | + } | |
| 179 | + }, | |
| 180 | + methods: { | |
| 181 | + loadNode(node, resolve) { | |
| 182 | + // 1. 根节点:直接返回 props 中的 deviceTreeData | |
| 183 | + if (node.level === 0) { | |
| 184 | + return resolve(this.deviceTreeData); | |
| 185 | + } | |
| 186 | + | |
| 187 | + // 2. 非根节点 | |
| 188 | + const data = node.data; | |
| 189 | + | |
| 190 | + // 如果已经有子节点(可能在左侧列表已经加载过),直接返回 | |
| 191 | + if (data.children && data.children.length > 0) { | |
| 192 | + return resolve(data.children); | |
| 193 | + } else { | |
| 194 | + // 其他情况(如已经是通道) | |
| 195 | + resolve([]); | |
| 196 | + } | |
| 197 | + }, | |
| 198 | + open(currentConfig) { | |
| 199 | + this.visible = true; | |
| 200 | + if (currentConfig) { | |
| 201 | + this.form = { ...currentConfig }; | |
| 202 | + // 如果是手动模式,需要回显选中状态 | |
| 203 | + if (this.form.sourceType === 'custom' && this.form.selectedDevices) { | |
| 204 | + this.$nextTick(() => { | |
| 205 | + // 只勾选叶子节点,element-ui会自动勾选父节点 | |
| 206 | + const keys = this.form.selectedDevices.map(d => d.code); | |
| 207 | + this.$refs.deviceTree.setCheckedKeys(keys); | |
| 208 | + }) | |
| 209 | + } | |
| 210 | + } | |
| 211 | + }, | |
| 212 | + async handleSave() { | |
| 213 | + console.log('🔴 [DEBUG] handleSave 被调用'); | |
| 214 | + | |
| 215 | + this.$refs.form.validate(async valid => { | |
| 216 | + console.log('🔴 [DEBUG] 表单校验结果:', valid); | |
| 217 | + | |
| 218 | + if (valid) { | |
| 219 | + console.log('[CarouselConfig] 校验通过,准备保存配置'); | |
| 220 | + const config = { ...this.form }; | |
| 221 | + console.log('🔴 [DEBUG] 配置对象创建完成:', config); | |
| 222 | + | |
| 223 | + if (config.sourceType === 'custom') { | |
| 224 | + console.log('🔴 [DEBUG] 进入自定义模式分支'); | |
| 225 | + // 添加安全检查 | |
| 226 | + if (!this.$refs.deviceTree) { | |
| 227 | + console.error('🔴 [DEBUG] deviceTree 未找到'); | |
| 228 | + this.$message.error("设备树未加载完成,请稍后再试"); | |
| 229 | + return; | |
| 230 | + } | |
| 231 | + | |
| 232 | + console.log('🔴 [DEBUG] 准备获取选中节点'); | |
| 233 | + // 获取所有勾选的节点(包括设备和通道) | |
| 234 | + const checkedNodes = this.$refs.deviceTree.getCheckedNodes(); | |
| 235 | + console.log(`[CarouselConfig] 选中节点数: ${checkedNodes.length}`); | |
| 236 | + console.log('🔴 [DEBUG] 选中节点:', checkedNodes); | |
| 237 | + | |
| 238 | + // 校验 | |
| 239 | + if (checkedNodes.length === 0) { | |
| 240 | + console.warn('🔴 [DEBUG] 未选中任何节点'); | |
| 241 | + this.$message.warning("请至少选择一个设备或通道!"); | |
| 242 | + return; | |
| 243 | + } | |
| 244 | + config.selectedNodes = checkedNodes; | |
| 245 | + } | |
| 246 | + | |
| 247 | + console.log('[CarouselConfig] 准备发送配置:', config); | |
| 248 | + console.log('🔴 [DEBUG] 即将发送 save 事件'); | |
| 249 | + | |
| 250 | + // 让出主线程,避免阻塞UI | |
| 251 | + await this.$nextTick(); | |
| 252 | + console.log('🔴 [DEBUG] nextTick 完成'); | |
| 253 | + | |
| 254 | + this.$emit('save', config); | |
| 255 | + console.log('🔴 [DEBUG] save 事件已发送'); | |
| 256 | + | |
| 257 | + this.visible = false; | |
| 258 | + console.log('🔴 [DEBUG] 对话框已关闭'); | |
| 259 | + } else { | |
| 260 | + console.warn('[CarouselConfig] 表单校验失败'); | |
| 261 | + } | |
| 262 | + }); | |
| 263 | + | |
| 264 | + console.log('🔴 [DEBUG] handleSave 执行完毕(validate 是异步的)'); | |
| 265 | + }, | |
| 266 | + resetForm() { | |
| 267 | + this.filterText = ''; | |
| 268 | + } | |
| 269 | + } | |
| 270 | +}; | |
| 271 | +</script> | |
| 272 | + | |
| 273 | +<style scoped> | |
| 274 | +.device-select-box { | |
| 275 | + margin-top: 10px; | |
| 276 | +} | |
| 277 | +/* 其他样式保持不变 */ | |
| 278 | +</style> | ... | ... |
web_src/src/components/CloudRecord.vue
| ... | ... | @@ -102,7 +102,7 @@ |
| 102 | 102 | <script> |
| 103 | 103 | import uiHeader from '../layout/UiHeader.vue' |
| 104 | 104 | import MediaServer from './service/MediaServer' |
| 105 | -import easyPlayer from './common/easyPlayer.vue' | |
| 105 | +import easyPlayer from './common/EasyPlayer.vue' | |
| 106 | 106 | import moment from 'moment' |
| 107 | 107 | import axios from "axios"; |
| 108 | 108 | ... | ... |
web_src/src/components/CloudRecordDetail.vue
| ... | ... | @@ -135,7 +135,7 @@ |
| 135 | 135 | <script> |
| 136 | 136 | // TODO 根据查询的时间列表设置滑轨的最大值与最小值, |
| 137 | 137 | import uiHeader from '../layout/UiHeader.vue' |
| 138 | - import player from './common/easyPlayer.vue' | |
| 138 | + import player from './common/EasyPlayer.vue' | |
| 139 | 139 | import moment from 'moment' |
| 140 | 140 | import axios from "axios"; |
| 141 | 141 | export default { | ... | ... |
web_src/src/components/DeviceList1078.vue
| 1 | 1 | <template> |
| 2 | - <div v-loading="loading" id="devicePosition" style="width:100vw; height: 91vh"> | |
| 3 | - <el-container v-loading="loading" style="height: 91vh;" element-loading-text="拼命加载中"> | |
| 4 | - <el-aside width="300px" style="background-color: #ffffff"> | |
| 5 | - <device1078-tree :tree-data="sourceValue" @node-click="nodeClick"></device1078-tree> | |
| 6 | - </el-aside> | |
| 7 | - <el-container> | |
| 8 | - <el-header height="5vh" style="text-align: left;font-size: 17px;line-height:5vh;width:90%"> | |
| 9 | - <el-tag size="small" style="margin-left: 15px;" v-if="simNodeData"> | |
| 10 | - <span v-if="channelData">{{`设备:${simNodeData.code} - 通道:${channelData.name}`}}</span> | |
| 11 | - <span v-else>{{`设备:${simNodeData.code}`}}</span> | |
| 12 | - </el-tag> | |
| 13 | - <el-tag size="small" style="margin-left: 15px;" v-if="playerIdx >= 0">下一个播放窗口 : {{ playerIdx + 1 }}</el-tag> | |
| 14 | - <i class="el-icon-s-platform btn" :class="{active:spilt==1}" @click="spiltClickFun(1)"/> | |
| 15 | - <i class="el-icon-menu btn" :class="{active:spilt==4}" @click="spiltClickFun(4)"/> | |
| 16 | - <i class="el-icon-s-grid btn" :class="{active:spilt==9}" @click="spiltClickFun(9)"/> | |
| 17 | - <i class="el-icon-full-screen btn" :class="{active:spilt==16}" @click="spiltClickFun(16)"/> | |
| 18 | - <el-button size="mini" style="margin-left: 15px;" @click="oneClickPlayback()">一键播放</el-button> | |
| 19 | - <el-button size="mini" style="margin-left: 15px;" @click="closeSelectItem()">关闭选中</el-button> | |
| 20 | - <el-button size="mini" style="margin-left: 15px;" @click="closeSelectCarItem()">一键关闭</el-button> | |
| 21 | - <el-button size="mini" style="margin-left: 15px;" @click="inspectionsDialog" v-if="patrolValue" type="danger"> | |
| 22 | - 视屏巡查中 | |
| 23 | - </el-button> | |
| 24 | - <el-button size="mini" style="margin-left: 15px;" @click="inspectionsDialog" v-else>视屏巡查</el-button> | |
| 25 | - </el-header> | |
| 26 | - <el-main style="padding: 0;"> | |
| 27 | - <!-- 视频播放器 --> | |
| 28 | - <playerListComponent | |
| 29 | - ref="playListComponent" | |
| 30 | - @playerClick="handleClick" | |
| 31 | - :video-url="videoUrl" | |
| 32 | - v-model="spilt" style="width: 100%; height: 100%;" | |
| 33 | - ></playerListComponent> | |
| 34 | - </el-main> | |
| 35 | - </el-container> | |
| 36 | - </el-container> | |
| 37 | - | |
| 38 | - <div id="carRMenu" class="rMenu"> | |
| 39 | - <ul> | |
| 40 | - <li id="m_add" @click="oneClickPlayback();">一键播放视频</li> | |
| 41 | - <li id="m_del" @click="closeSelectCarItem();">一键关闭选中车辆流</li> | |
| 42 | - </ul> | |
| 43 | - </div> | |
| 44 | - | |
| 45 | - <div id="channelCarRMenu" class="rMenu"> | |
| 46 | - <ul> | |
| 47 | - <li id="m_add" @click="oneClickPlayback();">一键播放视频</li> | |
| 48 | - <li id="m_del" @click="closeSelectCarItem();">一键关闭选中车辆流</li> | |
| 49 | - </ul> | |
| 50 | - </div> | |
| 51 | - | |
| 52 | - <el-dialog title="视屏巡查设置" width="600" append-to-body | |
| 53 | - :close-on-click-modal="false" | |
| 54 | - :visible.sync="showVideoDialog" v-loading="loading"> | |
| 55 | - <el-card class="box-card"> | |
| 56 | - <tree-transfer | |
| 57 | - :disabled="patrolValue" | |
| 58 | - style="text-align: left; display: inline-block;" | |
| 59 | - :to_data="targetValue" | |
| 60 | - :defaultExpandedKeys="expandedKeys" | |
| 61 | - node_key="id" | |
| 62 | - :filter="true" | |
| 63 | - :title="['源列表', '巡查列表']" | |
| 64 | - :from_data="sourceValue" | |
| 65 | - :defaultProps="treeProps" | |
| 66 | - :filter-node="filterNode" | |
| 67 | - class="inspections-tree" | |
| 68 | - height="500px"> | |
| 69 | - </tree-transfer> | |
| 70 | - </el-card> | |
| 71 | - <el-card class="box-card"> | |
| 72 | - <div slot="header" class="clearfix"> | |
| 73 | - <span style="font-size: math">巡查时间间隔</span> | |
| 74 | - </div> | |
| 75 | - <el-time-select | |
| 76 | - v-model="timerTime" | |
| 77 | - :picker-options="{ | |
| 78 | - start: '00:30', | |
| 79 | - step: '00:30', | |
| 80 | - end: '5:00' | |
| 81 | - }" | |
| 82 | - placeholder="选择巡查时间" | |
| 83 | - :disabled="patrolValue"> | |
| 84 | - </el-time-select> | |
| 85 | - </el-card> | |
| 86 | - <el-card class="box-card"> | |
| 87 | - <div slot="header" class="clearfix"> | |
| 88 | - <span style="font-size: math">巡查宫格数量</span> | |
| 2 | + <el-container> | |
| 3 | + <!-- 侧边栏:设备树 --> | |
| 4 | + <el-aside v-show="sidebarState"> | |
| 5 | + <vehicleList | |
| 6 | + @tree-loaded="handleTreeLoaded" | |
| 7 | + @node-click="nodeClick" | |
| 8 | + @node-contextmenu="nodeContextmenu" | |
| 9 | + @mouseover.native="showTooltip" | |
| 10 | + @mouseout.native="hideTooltip" | |
| 11 | + @click.native="hideTooltip" | |
| 12 | + @contextmenu.native="hideTooltip" | |
| 13 | + /> | |
| 14 | + | |
| 15 | + <!-- 右键菜单 --> | |
| 16 | + <el-dropdown ref="contextMenu" @command="handleCommand"> | |
| 17 | + <span class="el-dropdown-link"></span> | |
| 18 | + <el-dropdown-menu slot="dropdown"> | |
| 19 | + <el-dropdown-item command="playback">一键播放</el-dropdown-item> | |
| 20 | + </el-dropdown-menu> | |
| 21 | + </el-dropdown> | |
| 22 | + </el-aside> | |
| 23 | + | |
| 24 | + <el-container> | |
| 25 | + <el-header style="height: 5%;"> | |
| 26 | + <!-- 左侧折叠按钮 --> | |
| 27 | + <i :class="sidebarState ? 'el-icon-s-fold' : 'el-icon-s-unfold'" | |
| 28 | + @click="updateSidebarState" | |
| 29 | + style="font-size: 20px;margin-right: 10px; cursor: pointer;" | |
| 30 | + /> | |
| 31 | + | |
| 32 | + <!-- 分屏选择与通用控制 --> | |
| 33 | + <window-num-select v-model="windowNum"></window-num-select> | |
| 34 | + <el-button type="danger" size="mini" @click="closeAllVideo">全部关闭</el-button> | |
| 35 | + <el-button type="warning" size="mini" @click="closeVideo"> 关 闭</el-button> | |
| 36 | + <el-button type="primary" size="mini" icon="el-icon-full-screen" @click="toggleFullscreen"></el-button> | |
| 37 | + | |
| 38 | + <!-- 轮播控制按钮 --> | |
| 39 | + <el-button type="success" size="mini" icon="el-icon-timer" @click="openCarouselConfig"> | |
| 40 | + {{ isCarouselRunning ? '停止轮播' : '轮播设置' }} | |
| 41 | + </el-button> | |
| 42 | + | |
| 43 | + <!-- 轮播状态提示区 --> | |
| 44 | + <div v-if="isCarouselRunning" style="margin-left: 10px; font-size: 12px; display: flex; align-items: center;"> | |
| 45 | + <span v-if="isWithinSchedule" style="color: #67C23A;"> | |
| 46 | + <i class="el-icon-loading"></i> | |
| 47 | + 轮播运行中 <span style="font-size: 10px; opacity: 0.8;">(预加载模式)</span> | |
| 48 | + </span> | |
| 49 | + <span v-else style="color: #E6A23C;"> | |
| 50 | + <i class="el-icon-time"></i> | |
| 51 | + 轮播待机中 (等待生效时段) | |
| 52 | + </span> | |
| 89 | 53 | </div> |
| 90 | - <el-select v-model="patrolCell" placeholder="placeholder" :disabled="patrolValue"> | |
| 91 | - <el-option | |
| 92 | - v-for="item in patrolCellList" | |
| 93 | - :key="item" | |
| 94 | - :label="item" | |
| 95 | - :value="item"> | |
| 96 | - </el-option> | |
| 97 | - </el-select> | |
| 98 | - </el-card> | |
| 99 | - <div slot="footer" class="dialog-footer"> | |
| 100 | - <el-button type="danger" v-if="patrolValue" @click="closeInspections">关闭</el-button> | |
| 101 | - <el-button type="primary" v-else @click="openInspections">开启</el-button> | |
| 102 | - <el-button @click="showVideoDialog = false">取 消</el-button> | |
| 103 | - </div> | |
| 104 | - </el-dialog> | |
| 105 | - </div> | |
| 54 | + | |
| 55 | + <!-- 右侧信息 --> | |
| 56 | + <span class="header-right-info">{{ `下一个播放窗口 : ${windowClickIndex}` }}</span> | |
| 57 | + </el-header> | |
| 58 | + | |
| 59 | + <!-- 轮播配置弹窗 --> | |
| 60 | + <carousel-config | |
| 61 | + ref="carouselConfig" | |
| 62 | + :device-tree-data="deviceTreeData" | |
| 63 | + @save="startCarousel" | |
| 64 | + ></carousel-config> | |
| 65 | + | |
| 66 | + <!-- 视频播放主区域 --> | |
| 67 | + <el-main ref="videoMain"> | |
| 68 | + <playerListComponent | |
| 69 | + ref="playListComponent" | |
| 70 | + @playerClick="handleClick" | |
| 71 | + :video-url="videoUrl" | |
| 72 | + :videoDataList="videoDataList" | |
| 73 | + v-model="windowNum" | |
| 74 | + style="width: 100%; height: 100%;" | |
| 75 | + ></playerListComponent> | |
| 76 | + </el-main> | |
| 77 | + </el-container> | |
| 78 | + </el-container> | |
| 106 | 79 | </template> |
| 80 | + | |
| 107 | 81 | <script> |
| 108 | 82 | import tree from "vue-giant-tree"; |
| 109 | 83 | import uiHeader from "../layout/UiHeader.vue"; |
| ... | ... | @@ -111,1247 +85,682 @@ import playerListComponent from './common/PlayerListComponent.vue'; |
| 111 | 85 | import player from './common/JessVideoPlayer.vue'; |
| 112 | 86 | import DeviceTree from './common/DeviceTree.vue' |
| 113 | 87 | import treeTransfer from "el-tree-transfer"; |
| 114 | -import {parseTime} from "../../utils/ruoyi"; | |
| 115 | 88 | import Device1078Tree from "./JT1078Components/deviceList/Device1078Tree.vue"; |
| 116 | -import userService from "./service/UserService"; | |
| 89 | +import VehicleList from "./JT1078Components/deviceList/VehicleList.vue"; | |
| 90 | +import WindowNumSelect from "./WindowNumSelect.vue"; | |
| 91 | +import CarouselConfig from "./CarouselConfig.vue"; | |
| 117 | 92 | |
| 118 | 93 | export default { |
| 119 | 94 | name: "live", |
| 120 | 95 | components: { |
| 96 | + WindowNumSelect, | |
| 121 | 97 | playerListComponent, |
| 122 | 98 | Device1078Tree, |
| 99 | + VehicleList, | |
| 100 | + CarouselConfig, | |
| 123 | 101 | uiHeader, player, DeviceTree, tree, treeTransfer |
| 124 | 102 | }, |
| 125 | 103 | data() { |
| 126 | 104 | return { |
| 127 | - //车辆列表过滤 | |
| 128 | - filterText: '', | |
| 129 | - //穿梭框巡查数据-----------↓ | |
| 130 | - //穿梭框默认展开 | |
| 131 | - expandedKeys: [], | |
| 132 | - // 批次获取器 | |
| 133 | - batchFetcher: null, | |
| 134 | - //源列表数据 | |
| 135 | - sourceValue: [], | |
| 136 | - //原始sim列表 (sim对象) | |
| 137 | - simList: [], | |
| 138 | - //目标列表数据 | |
| 139 | - targetValue: [], | |
| 140 | - //巡查播放原始列表 | |
| 141 | - lastTargetValue: [], | |
| 142 | - //巡查过滤列表 (sim) | |
| 143 | - lastTargetValueFilter: [], | |
| 144 | - //现在正在播放的列表 | |
| 145 | - nowPlayArray: [], | |
| 146 | - //prop参数 | |
| 147 | - treeProps: { | |
| 148 | - children: 'children', | |
| 149 | - label: 'name', | |
| 150 | - disabled: 'disabled', | |
| 151 | - }, | |
| 152 | - //巡查按钮 | |
| 153 | - patrolValue: false, | |
| 154 | - //巡查宫格数量 | |
| 155 | - patrolCell: 9, | |
| 156 | - //巡查宫格下拉框数据 | |
| 157 | - patrolCellList: [1, 4, 9, 12], | |
| 158 | - //巡查时间 | |
| 159 | - timerTime: '00:30', | |
| 160 | - //巡查定时器 | |
| 161 | - fetchInterval: null, | |
| 162 | - //上线车辆 | |
| 163 | - onlineCar: new Map(), | |
| 164 | - //车辆数据定时器 | |
| 165 | - carInfoTimeout: null, | |
| 166 | - //车载key集合 | |
| 167 | - onlineCarKeys: [], | |
| 168 | - //树节点对象 | |
| 169 | - simNodeData: null, | |
| 170 | - videoUrl: [], | |
| 171 | - videoUrlHistory: "", | |
| 172 | - spilt: 1,//分屏 | |
| 105 | + // --- UI 状态 --- | |
| 106 | + isFullscreen: false, | |
| 107 | + sidebarState: true, | |
| 108 | + windowNum: '4', | |
| 109 | + windowClickIndex: 1, | |
| 173 | 110 | windowClickData: null, |
| 174 | - playerIdx: 0,//激活播放器 | |
| 175 | - updateLooper: 0, //数据刷新轮训标志 | |
| 176 | - count: 15, | |
| 177 | - historyLoadingFlag: true, | |
| 178 | - total: 0, | |
| 179 | - startTime: '', | |
| 180 | - endTime: '', | |
| 181 | - //channel | |
| 182 | - loading: false, | |
| 183 | - device: null, | |
| 184 | - nodes: [], | |
| 185 | - carPlayTimer: null, | |
| 186 | - carTreeNode: null, | |
| 187 | - ztreeObj: null, | |
| 188 | - ztreeNode: null, | |
| 189 | - fullscreenLoading: true, | |
| 190 | - fullscreenLoadingStyle: '', | |
| 191 | - showVideoDialog: false, | |
| 192 | - historyPlayListHtml: '', | |
| 193 | - hisotoryPlayFlag: false, | |
| 194 | - downloadURL: null, | |
| 195 | - rightMenuId: null, | |
| 196 | - channelData: null, | |
| 197 | - port: -1, | |
| 198 | - httpPort: -1, | |
| 199 | - stream: "", | |
| 200 | - sim: "", | |
| 201 | - channel: "", | |
| 202 | - setting: { | |
| 203 | - callback: { | |
| 204 | - beforeExpand: this.beforeExpand | |
| 205 | - }, | |
| 206 | - check: { | |
| 207 | - enable: false, | |
| 208 | - }, | |
| 209 | - edit: { | |
| 210 | - enable: false, | |
| 211 | - } | |
| 212 | - }, | |
| 213 | - defaultProps: { | |
| 214 | - children: 'children', | |
| 215 | - label: 'title', | |
| 216 | - name: 'title', | |
| 217 | - isLeaf: 'spread', | |
| 218 | - nameIsHTML: true, | |
| 219 | - view: { | |
| 220 | - nameIsHTML: true | |
| 221 | - } | |
| 222 | - }, | |
| 111 | + rightClickNode: null, | |
| 112 | + tooltipVisible: false, | |
| 113 | + | |
| 114 | + // --- 播放数据 --- | |
| 115 | + videoUrl: [], | |
| 116 | + videoDataList: [], | |
| 117 | + deviceTreeData: [], | |
| 118 | + deviceList: [ | |
| 119 | + "600201", "600202", "600203", "600204", "600205", | |
| 120 | + "601101", "601102", "601103", "601104", "CS-010", | |
| 121 | + ], | |
| 122 | + | |
| 123 | + // --- 轮播核心状态 --- | |
| 124 | + isCarouselRunning: false, | |
| 125 | + isWithinSchedule: true, | |
| 126 | + carouselConfig: null, | |
| 127 | + carouselTimer: null, | |
| 128 | + | |
| 129 | + // 流式缓冲相关变量 | |
| 130 | + carouselDeviceList: [], | |
| 131 | + channelBuffer: [], | |
| 132 | + deviceCursor: 0, | |
| 223 | 133 | }; |
| 224 | 134 | }, |
| 225 | 135 | mounted() { |
| 226 | - let that = this; | |
| 227 | - that.initTreeData(); | |
| 228 | - }, | |
| 229 | - created() { | |
| 230 | - this.checkPlayByParam(); | |
| 231 | - this.getCarInfoBuffer() | |
| 136 | + document.addEventListener('fullscreenchange', this.handleFullscreenChange); | |
| 137 | + window.addEventListener('beforeunload', this.handleBeforeUnload); | |
| 232 | 138 | }, |
| 233 | 139 | beforeDestroy() { |
| 234 | - if (!this.isEmpty(this.timer)) { | |
| 235 | - clearInterval(this.timer); | |
| 236 | - } | |
| 237 | - if (!this.isEmpty(this.updateLooper)){ | |
| 238 | - clearTimeout(this.updateLooper); | |
| 239 | - } | |
| 240 | - if (!this.isEmpty(this.carPlayTimer)) { | |
| 241 | - clearTimeout(this.carPlayTimer); | |
| 242 | - } | |
| 140 | + document.removeEventListener('fullscreenchange', this.handleFullscreenChange); | |
| 141 | + window.removeEventListener('beforeunload', this.handleBeforeUnload); | |
| 142 | + this.stopCarousel(); | |
| 243 | 143 | }, |
| 244 | - computed: { | |
| 245 | - liveStyle() { | |
| 246 | - let style = {width: '99%', height: '99%'} | |
| 247 | - switch (this.spilt) { | |
| 248 | - case 4: | |
| 249 | - style = {width: '49%', height: '49%'} | |
| 250 | - break | |
| 251 | - case 9: | |
| 252 | - style = {width: '32%', height: '32%'} | |
| 253 | - break | |
| 254 | - case 12: | |
| 255 | - style = {width: '24.5%', height: '32%'} | |
| 256 | - break | |
| 257 | - } | |
| 258 | - this.$nextTick(() => { | |
| 259 | - for (let i = 0; i < this.spilt; i++) { | |
| 260 | - const player = this.$refs.player | |
| 261 | - player && player[i] && player[i].updatePlayerDomSize() | |
| 262 | - } | |
| 263 | - }) | |
| 264 | - return style | |
| 144 | + // 路由离开守卫 | |
| 145 | + beforeRouteLeave(to, from, next) { | |
| 146 | + if (this.isCarouselRunning) { | |
| 147 | + this.$confirm('当前视频轮播正在进行中,离开页面将停止轮播,是否确认离开?', '提示', { | |
| 148 | + confirmButtonText: '确定离开', | |
| 149 | + cancelButtonText: '取消', | |
| 150 | + type: 'warning' | |
| 151 | + }).then(() => { | |
| 152 | + this.stopCarousel(); | |
| 153 | + next(); | |
| 154 | + }).catch(() => next(false)); | |
| 155 | + } else { | |
| 156 | + next(); | |
| 265 | 157 | } |
| 266 | 158 | }, |
| 267 | - watch: { | |
| 268 | - spilt(newValue) { | |
| 269 | - console.log("切换画幅;" + newValue) | |
| 270 | - let that = this | |
| 271 | - for (let i = 1; i <= newValue; i++) { | |
| 272 | - if (!that.$refs['player' + i]) { | |
| 273 | - continue | |
| 274 | - } | |
| 275 | - this.$nextTick(() => { | |
| 276 | - if (that.$refs['player' + i] instanceof Array) { | |
| 277 | - that.$refs['player' + i][0].resize() | |
| 278 | - } else { | |
| 279 | - that.$refs['player' + i].resize() | |
| 280 | - } | |
| 281 | - }) | |
| 282 | - } | |
| 283 | - window.localStorage.setItem('split', newValue) | |
| 284 | - }, | |
| 285 | - '$route.fullPath': 'checkPlayByParam' | |
| 286 | - }, | |
| 287 | - destroyed() { | |
| 288 | - | |
| 289 | - }, | |
| 290 | 159 | methods: { |
| 291 | - /** | |
| 292 | - * 视频窗口点击传值 | |
| 293 | - * @param data 该窗口的数据 | |
| 294 | - * @param index 该窗口的下标 | |
| 295 | - * @param len 窗口总数 | |
| 296 | - */ | |
| 297 | - handleClick(data, index, len) { | |
| 298 | - console.log(index) | |
| 299 | - this.playerIdx = index | |
| 300 | - this.windowClickData = data | |
| 301 | - }, | |
| 302 | - /** | |
| 303 | - * 统计树节点下一级有多少在线数量 | |
| 304 | - */ | |
| 305 | - statisticsOnline(data) { | |
| 306 | - for (let i in data) { | |
| 307 | - console.log(data[i].abnormalStatus === undefined && data[i].children && data[i].children.length > 0) | |
| 308 | - if (data[i].abnormalStatus === undefined && data[i].children && data[i].children.length > 0) { | |
| 309 | - data[i].onlineData = data[i].children.filter(item => item.abnormalStatus === 1); | |
| 310 | - } | |
| 160 | + // ========================================== | |
| 161 | + // 1. 拦截与权限控制 | |
| 162 | + // ========================================== | |
| 163 | + handleBeforeUnload(e) { | |
| 164 | + if (this.isCarouselRunning) { | |
| 165 | + e.preventDefault(); | |
| 166 | + e.returnValue = '轮播正在运行,确定要离开吗?'; | |
| 167 | + } | |
| 168 | + }, | |
| 169 | + | |
| 170 | + async checkCarouselPermission(actionName) { | |
| 171 | + if (!this.isCarouselRunning) return true; | |
| 172 | + try { | |
| 173 | + await this.$confirm( | |
| 174 | + `当前【视频轮播】正在运行中。\n进行"${actionName}"操作将停止轮播,是否继续?`, | |
| 175 | + '轮播运行提示', | |
| 176 | + { confirmButtonText: '停止轮播并继续', cancelButtonText: '取消', type: 'warning' } | |
| 177 | + ); | |
| 178 | + this.stopCarousel(); | |
| 179 | + return true; | |
| 180 | + } catch (e) { | |
| 181 | + this.$message.info("已取消操作,轮播继续运行"); | |
| 182 | + return false; | |
| 311 | 183 | } |
| 312 | 184 | }, |
| 313 | - /** | |
| 314 | - * 树点击事件 | |
| 315 | - */ | |
| 316 | - nodeClick(data, node) { | |
| 317 | - if (data.children && data.children.length > 0 && data.abnormalStatus) { | |
| 318 | - this.simNodeData = data | |
| 319 | - this.channelData = null | |
| 320 | - } else if (data.parent.abnormalStatus === 10){ | |
| 321 | - this.$message.error("设备未接入SIM卡") | |
| 322 | - } else if (data.parent.abnormalStatus === 20){ | |
| 323 | - this.$message.error("设备不在线") | |
| 324 | - } else if (data.children === undefined) { | |
| 325 | - let playerIdx = this.playerIdx; | |
| 326 | - this.simNodeData = node.parent.data | |
| 327 | - this.channelData = data | |
| 328 | - this.openPlay(data, playerIdx++); | |
| 329 | - if (playerIdx > (this.spilt - 1)){ | |
| 330 | - this.playerIdx = 0 | |
| 185 | + | |
| 186 | + // ========================================== | |
| 187 | + // 2. 轮播核心逻辑 (预加载 + 无限循环) | |
| 188 | + // ========================================== | |
| 189 | + | |
| 190 | + getChannels(data) { | |
| 191 | + // (保持原有逻辑不变) | |
| 192 | + let nvrLabels, rmLabels; | |
| 193 | + if (data.sim2) { | |
| 194 | + if (this.deviceList.includes(data.name)) { | |
| 195 | + nvrLabels = ['中门', '', '车前', '驾驶舱', '', '前车厢', '', '360']; | |
| 331 | 196 | } else { |
| 332 | - this.playerIdx = playerIdx | |
| 197 | + nvrLabels = ['中门', '', '车前', '驾驶舱', '前门', '前车厢', '后车厢', '360']; | |
| 333 | 198 | } |
| 199 | + rmLabels = []; | |
| 334 | 200 | } else { |
| 335 | - console.log(data) | |
| 336 | - this.$message.error("设备状态异常") | |
| 201 | + nvrLabels = ['ADAS', 'DSM', '路况', '司机', '整车前', '中门', '倒车', '前门客流', '后面客流']; | |
| 202 | + rmLabels = []; | |
| 337 | 203 | } |
| 204 | + return [ | |
| 205 | + ...nvrLabels.map((label, index) => label ? `${data.id}_${data.sim}_${index + 1}` : null).filter(Boolean), | |
| 206 | + ...rmLabels.map((label, index) => label ? `${data.id}_${data.sim2}_${index + 1}` : null).filter(Boolean) | |
| 207 | + ]; | |
| 338 | 208 | }, |
| 339 | - /** | |
| 340 | - * 模糊查询树 | |
| 341 | - */ | |
| 342 | - filterNode(value, data) { | |
| 343 | - console.log(data) | |
| 344 | - if (!value) return true; | |
| 345 | - return this.findSearKey(data, value) | |
| 346 | - }, | |
| 347 | - /** | |
| 348 | - * 递归搜索父级是否包含关键字 | |
| 349 | - */ | |
| 350 | - findSearKey(node, key) { | |
| 351 | - if (node.name.indexOf(key) !== -1) { | |
| 352 | - return true; | |
| 209 | + | |
| 210 | + openCarouselConfig() { | |
| 211 | + if (this.isCarouselRunning) { | |
| 212 | + this.stopCarousel(); | |
| 353 | 213 | } else { |
| 354 | - if (node.parent === undefined || node.parent === null) { | |
| 355 | - return false; | |
| 356 | - } else { | |
| 357 | - return this.findSearKey(node.parent, key); | |
| 358 | - } | |
| 214 | + this.$refs.carouselConfig.open(this.carouselConfig); | |
| 359 | 215 | } |
| 360 | 216 | }, |
| 217 | + | |
| 361 | 218 | /** |
| 362 | - * 处理返回的tree数据 | |
| 219 | + * 启动轮播 | |
| 363 | 220 | */ |
| 364 | - processingTreeData(data, pid, parent) { | |
| 365 | - for (let i in data) { | |
| 366 | - data[i].pid = pid | |
| 367 | - data[i].parent = parent; | |
| 368 | - if (data[i].children || (Array.isArray(data[i].children) && data[i].abnormalStatus === undefined)) { | |
| 369 | - this.processingTreeData(data[i].children, data[i].id, data[i]); | |
| 370 | - } else { | |
| 371 | - data[i].name = data[i].code | |
| 372 | - if (data[i].abnormalStatus !== 1) { | |
| 373 | - data[i].disabled = true; | |
| 374 | - let targetValue = this.targetValue; | |
| 375 | - if (targetValue.length > 0) { | |
| 376 | - this.disableItemsByName(targetValue, data[i].name); | |
| 221 | + async startCarousel(config) { | |
| 222 | + this.carouselConfig = config; | |
| 223 | + // 1. 收集设备 | |
| 224 | + let deviceNodes = []; | |
| 225 | + if (config.sourceType === 'all_online') { | |
| 226 | + const findOnlineDevices = (nodes) => { | |
| 227 | + if (!Array.isArray(nodes)) return; | |
| 228 | + nodes.forEach(node => { | |
| 229 | + if (node.abnormalStatus !== undefined && node.children && node.abnormalStatus === 1) { | |
| 230 | + deviceNodes.push(node); | |
| 377 | 231 | } |
| 378 | - } | |
| 379 | - this.addChannels(data[i]) | |
| 380 | - } | |
| 232 | + if (node.children) findOnlineDevices(node.children); | |
| 233 | + }); | |
| 234 | + }; | |
| 235 | + findOnlineDevices(this.deviceTreeData); | |
| 236 | + } else if (config.sourceType === 'custom') { | |
| 237 | + const selected = config.selectedNodes || []; | |
| 238 | + deviceNodes = selected.filter(n => n.abnormalStatus !== undefined && n.children); | |
| 239 | + } | |
| 240 | + | |
| 241 | + if (deviceNodes.length === 0) { | |
| 242 | + this.$message.warning("没有可轮播的在线设备"); | |
| 243 | + return; | |
| 381 | 244 | } |
| 245 | + | |
| 246 | + // 2. 初始化状态 | |
| 247 | + this.carouselDeviceList = deviceNodes; | |
| 248 | + this.channelBuffer = []; | |
| 249 | + this.deviceCursor = 0; | |
| 250 | + this.isCarouselRunning = true; | |
| 251 | + this.isWithinSchedule = true; | |
| 252 | + | |
| 253 | + this.$message.success(`轮播启动,覆盖 ${deviceNodes.length} 台设备`); | |
| 254 | + | |
| 255 | + // 3. 立即执行第一轮播放 | |
| 256 | + await this.executeFirstRound(); | |
| 382 | 257 | }, |
| 258 | + | |
| 383 | 259 | /** |
| 384 | - * 原始sim列表数据 (用来验证视屏巡查车辆是否在线) | |
| 385 | - * @param data 查询后台树列表 | |
| 260 | + * 执行第一轮 (不等待,立即播放) | |
| 386 | 261 | */ |
| 387 | - processingSimList(data) { | |
| 388 | - if (data && data.length > 0) { | |
| 389 | - for (let i in data) { | |
| 390 | - if (data[i].children === undefined && data[i].abnormalStatus) { | |
| 391 | - this.simList.push(data[i]); | |
| 392 | - } else if (data[i].children && data[i].children.length > 0) { | |
| 393 | - this.processingSimList(data[i].children); | |
| 394 | - } | |
| 395 | - } | |
| 262 | + async executeFirstRound() { | |
| 263 | + // 切换到配置的布局 | |
| 264 | + if (this.windowNum !== this.carouselConfig.layout) { | |
| 265 | + this.windowNum = this.carouselConfig.layout; | |
| 266 | + } | |
| 267 | + | |
| 268 | + // 获取第一批数据 | |
| 269 | + const batchData = await this.fetchNextBatchData(); | |
| 270 | + if (batchData && batchData.urls) { | |
| 271 | + this.videoUrl = batchData.urls; | |
| 272 | + this.videoDataList = batchData.infos; | |
| 273 | + // 启动后续的循环 | |
| 274 | + this.runCarouselLoop(); | |
| 275 | + } else { | |
| 276 | + this.$message.warning("获取首轮播放数据失败,尝试继续运行..."); | |
| 277 | + this.runCarouselLoop(); // 即使失败也尝试进入循环 | |
| 396 | 278 | } |
| 397 | 279 | }, |
| 280 | + | |
| 398 | 281 | /** |
| 399 | - * 处理巡查列表数据 | |
| 282 | + * 轮播循环调度器 (预加载核心) | |
| 400 | 283 | */ |
| 401 | - disableItemsByName(arr, targetName) { | |
| 402 | - arr.forEach(item => { | |
| 403 | - // 检查当前项是否是对象并且包含 name 属性且值为 targetName | |
| 404 | - if (item && typeof item === 'object' && item.name === targetName) { | |
| 405 | - item.disabled = true; | |
| 406 | - } | |
| 407 | - // 如果当前项有 children 属性且是数组,则递归调用自身 | |
| 408 | - if (item && Array.isArray(item.children)) { | |
| 409 | - this.disableItemsByName(item.children, targetName); | |
| 284 | + runCarouselLoop() { | |
| 285 | + if (!this.isCarouselRunning) return; | |
| 286 | + | |
| 287 | + const config = this.carouselConfig; | |
| 288 | + // 检查时间段 | |
| 289 | + if (config.runMode === 'schedule') { | |
| 290 | + const isInTime = this.checkTimeRange(config.timeRange[0], config.timeRange[1]); | |
| 291 | + this.isWithinSchedule = isInTime; | |
| 292 | + if (!isInTime) { | |
| 293 | + // 不在时间段,清屏并等待 10秒再检查 | |
| 294 | + if (this.videoUrl.some(u => u)) this.closeAllVideoNoConfirm(); | |
| 295 | + this.carouselTimer = setTimeout(() => this.runCarouselLoop(), 10000); | |
| 296 | + return; | |
| 410 | 297 | } |
| 411 | - }); | |
| 412 | - }, | |
| 413 | - /** | |
| 414 | - * 查询车辆信息 | |
| 415 | - */ | |
| 416 | - getCarInfoBuffer() { | |
| 417 | - this.loading = true; | |
| 418 | - this.getCarInfo() | |
| 419 | - }, | |
| 420 | - getCarInfo() { | |
| 421 | - console.log() | |
| 422 | - this.$axios({ | |
| 423 | - method: 'get', | |
| 424 | - url: `/api/jt1078/query/car/tree/${userService.getUser().role.authority == 0?'all':userService.getUser().role.authority}`, | |
| 425 | - }).then(res => { | |
| 426 | - if (res && res.data && res.data.data) { | |
| 427 | - if (res.data.data.code == 1) { | |
| 428 | - //处理数据 | |
| 429 | - this.simList = [] | |
| 430 | - this.processingSimList(res.data.data.result) | |
| 431 | - this.processingTreeData(res.data.data.result, 0); | |
| 432 | - this.statisticsOnline(res.data.data.result) | |
| 433 | - console.log(res.data.data.result) | |
| 434 | - this.sourceValue = res.data.data.result; | |
| 435 | - this.loading = false | |
| 436 | - //定时更新数据 | |
| 437 | - let this_ = this | |
| 438 | - this.carInfoTimeout = setTimeout(function () { | |
| 439 | - this_.getCarInfo() | |
| 440 | - }, 45000); | |
| 441 | - } else if (res.data.data.message) { | |
| 442 | - this.$message.error(res.data.data.message); | |
| 298 | + } else { | |
| 299 | + this.isWithinSchedule = true; | |
| 300 | + } | |
| 301 | + | |
| 302 | + // 计算时间: 间隔时间,提前 15s 请求(优化预加载时间) | |
| 303 | + const intervalSeconds = Math.max(config.interval, 30); // 最小间隔30秒,给足够时间 | |
| 304 | + const preLoadSeconds = 15; // 提前15秒开始请求,确保流有足够时间推上来 | |
| 305 | + const waitTimeForFetch = (intervalSeconds - preLoadSeconds) * 1000; | |
| 306 | + const waitTimeForSwitch = preLoadSeconds * 1000; | |
| 307 | + | |
| 308 | + // 步骤 1: 等待到预加载时间点 | |
| 309 | + this.carouselTimer = setTimeout(async () => { | |
| 310 | + if (!this.isCarouselRunning) return; | |
| 311 | + | |
| 312 | + console.log(`[轮播] 预加载下一批数据...`); | |
| 313 | + | |
| 314 | + // 显示加载提示 | |
| 315 | + const loadingMsg = this.$message({ | |
| 316 | + type: 'info', | |
| 317 | + message: '正在预加载下一批视频...', | |
| 318 | + duration: preLoadSeconds * 1000, | |
| 319 | + showClose: false | |
| 320 | + }); | |
| 321 | + | |
| 322 | + // 步骤 2: 向后端请求下一批地址 (后端秒回,但流还没好) | |
| 323 | + const nextBatch = await this.fetchNextBatchData(); | |
| 324 | + | |
| 325 | + loadingMsg.close(); | |
| 326 | + | |
| 327 | + // 步骤 3: 拿到地址后,等待流推上来 (补足剩下的时间) | |
| 328 | + this.carouselTimer = setTimeout(() => { | |
| 329 | + if (!this.isCarouselRunning) return; | |
| 330 | + | |
| 331 | + if (nextBatch && nextBatch.urls) { | |
| 332 | + console.log('[轮播] 切换画面'); | |
| 333 | + // 更新布局 (防止用户中途改了) | |
| 334 | + if (this.windowNum !== this.carouselConfig.layout) { | |
| 335 | + this.windowNum = this.carouselConfig.layout; | |
| 336 | + } | |
| 337 | + // 切换数据 -> 触发播放器复用逻辑 | |
| 338 | + this.videoUrl = nextBatch.urls; | |
| 339 | + this.videoDataList = nextBatch.infos; | |
| 340 | + | |
| 341 | + this.$message.success('已切换到下一批视频'); | |
| 342 | + } else { | |
| 343 | + this.$message.warning('预加载失败,将在下一轮重试'); | |
| 443 | 344 | } |
| 444 | - } else { | |
| 445 | - this.$message.error("请求错误,请刷新再试"); | |
| 446 | - } | |
| 447 | - this.loading = false | |
| 448 | - }).catch(error => { | |
| 449 | - this.$message.error(error.message); | |
| 450 | - }) | |
| 451 | - }, | |
| 452 | - /** | |
| 453 | - * 打开巡查设置悬浮框 | |
| 454 | - */ | |
| 455 | - inspectionsDialog() { | |
| 456 | - this.showVideoDialog = true | |
| 345 | + | |
| 346 | + // 递归进入下一轮 | |
| 347 | + this.runCarouselLoop(); | |
| 348 | + | |
| 349 | + }, waitTimeForSwitch); | |
| 350 | + | |
| 351 | + }, waitTimeForFetch); | |
| 457 | 352 | }, |
| 353 | + | |
| 458 | 354 | /** |
| 459 | - * 添加通道 | |
| 355 | + * 核心:获取下一批数据 (分批加载优化) | |
| 460 | 356 | */ |
| 461 | - addChannels(data) { | |
| 462 | - console.log(data) | |
| 463 | - if (data.sim2){ | |
| 464 | - let nvr_labels = ['中门','','车前','驾驶舱','前门','前车厢','后车厢','360']; | |
| 465 | - //'ADAS','DSM','前门客流','中门客流','360前','360后','360左','360右' | |
| 466 | - let rm_labels = []; | |
| 467 | - let children = []; | |
| 468 | - for (let i in nvr_labels) { | |
| 469 | - if (nvr_labels[i] === ''){ | |
| 470 | - continue | |
| 357 | + async fetchNextBatchData() { | |
| 358 | + // 1. 确定当前需要的通道数 | |
| 359 | + let pageSize = parseInt(this.carouselConfig.layout); | |
| 360 | + const layoutMap = { '1+5': 6, '1+7': 8, '1+9': 10, '1+11': 12 }; | |
| 361 | + if (layoutMap[this.carouselConfig.layout]) pageSize = layoutMap[this.carouselConfig.layout]; | |
| 362 | + | |
| 363 | + // 2. 补货逻辑:填充 channelBuffer | |
| 364 | + let loopSafety = 0; | |
| 365 | + const maxLoops = 50; | |
| 366 | + | |
| 367 | + while (this.channelBuffer.length < pageSize && loopSafety < maxLoops) { | |
| 368 | + loopSafety++; | |
| 369 | + | |
| 370 | + // 游标归零重置逻辑 | |
| 371 | + if (this.deviceCursor >= this.carouselDeviceList.length) { | |
| 372 | + console.log('[轮播缓冲] 列表循环,重置游标...'); | |
| 373 | + this.deviceCursor = 0; | |
| 374 | + this.rebuildOnlineList(); // 刷新在线列表 | |
| 375 | + | |
| 376 | + if (this.carouselDeviceList.length === 0) { | |
| 377 | + console.warn('[轮播] 无在线设备,无法补货'); | |
| 378 | + break; | |
| 471 | 379 | } |
| 472 | - children.push({ | |
| 473 | - id: `${data.id}_${data.sim}_${Number(i) + Number(1)}`, | |
| 474 | - pid: data.id, | |
| 475 | - name: nvr_labels[i], | |
| 476 | - disabled: data.disabled, | |
| 477 | - parent: data | |
| 478 | - }) | |
| 479 | 380 | } |
| 480 | - for (let i in rm_labels) { | |
| 481 | - if (rm_labels[i] === ''){ | |
| 482 | - continue | |
| 381 | + | |
| 382 | + const BATCH_SIZE = 10; // 增加批次大小以提高效率 | |
| 383 | + let batchCodes = []; | |
| 384 | + | |
| 385 | + // 提取一批设备 | |
| 386 | + for (let i = 0; i < BATCH_SIZE; i++) { | |
| 387 | + if (this.deviceCursor >= this.carouselDeviceList.length) break; | |
| 388 | + const device = this.carouselDeviceList[this.deviceCursor]; | |
| 389 | + if (device && device.abnormalStatus === 1) { | |
| 390 | + batchCodes.push(device); | |
| 483 | 391 | } |
| 484 | - children.push({ | |
| 485 | - id: `${data.id}_${data.sim2}_${Number(i) + Number(1)}`, | |
| 486 | - pid: data.id, | |
| 487 | - name: rm_labels[i], | |
| 488 | - disabled: data.disabled, | |
| 489 | - parent: data | |
| 490 | - }) | |
| 392 | + this.deviceCursor++; | |
| 491 | 393 | } |
| 492 | - data.children = children; | |
| 493 | - }else { | |
| 494 | - let labels = ['ADAS', 'DSM', '路况', '司机', '整车前', '中门', '倒车', '前门客流', '后面客流']; | |
| 495 | - let children = []; | |
| 496 | - for (let i in labels) { | |
| 497 | - children.push({ | |
| 498 | - id: `${data.id}_${data.sim}_${Number(i) + Number(1)}`, | |
| 499 | - pid: data.id, | |
| 500 | - name: labels[i], | |
| 501 | - disabled: data.disabled, | |
| 502 | - parent: data | |
| 503 | - }) | |
| 394 | + | |
| 395 | + // 解析通道放入 Buffer | |
| 396 | + if (batchCodes.length > 0) { | |
| 397 | + batchCodes.forEach(device => { | |
| 398 | + try { | |
| 399 | + const codes = this.getChannels(device); | |
| 400 | + if (codes && codes.length > 0) this.channelBuffer.push(...codes); | |
| 401 | + } catch (e) {} | |
| 402 | + }); | |
| 504 | 403 | } |
| 505 | - data.children = children; | |
| 506 | - } | |
| 507 | - }, | |
| 508 | - /** | |
| 509 | - * 巡查时间转换器 | |
| 510 | - */ | |
| 511 | - timerTimeConvertor() { | |
| 512 | - switch (this.timerTime) { | |
| 513 | - case "00:30": | |
| 514 | - return 30000 | |
| 515 | - case "01:00": | |
| 516 | - return 30000 * 2 | |
| 517 | - case "01:30": | |
| 518 | - return 30000 * 3 | |
| 519 | - case "02:00": | |
| 520 | - return 30000 * 4 | |
| 521 | - case "02:30": | |
| 522 | - return 30000 * 5 | |
| 523 | - case "03:00": | |
| 524 | - return 30000 * 6 | |
| 525 | - case "03:30": | |
| 526 | - return 30000 * 7 | |
| 527 | - case "04:00": | |
| 528 | - return 30000 * 8 | |
| 529 | - case "04:30": | |
| 530 | - return 30000 * 9 | |
| 531 | - case "05:00": | |
| 532 | - return 30000 * 10 | |
| 533 | - default: | |
| 534 | - return null | |
| 535 | - } | |
| 536 | - }, | |
| 537 | - /** | |
| 538 | - * 巡查开启按钮 | |
| 539 | - */ | |
| 540 | - openInspections() { | |
| 541 | - let time = this.timerTimeConvertor(); | |
| 542 | - this.spilt = this.patrolCell; | |
| 543 | - if (time == null) { | |
| 544 | - this.$message.error("时间选择错误 ==> [ " + this.timerTime + " ]") | |
| 545 | - console.log("时间选择结果为 ===> [ " + time + " ]") | |
| 546 | - } | |
| 547 | - let targetValue = this.targetValue; | |
| 548 | - if (targetValue === undefined || targetValue === null || targetValue.length === 0) { | |
| 549 | - this.$message.error("未选择巡查对象") | |
| 550 | - return | |
| 551 | - } | |
| 552 | - this.startFetching(time) | |
| 553 | - this.showVideoDialog = false | |
| 554 | - }, | |
| 555 | - /** | |
| 556 | - * 巡查树数组只获取最后一级的一维数组 | |
| 557 | - * @param array 原数组 | |
| 558 | - */ | |
| 559 | - getLastElementsOfInnerArrays(array) { | |
| 560 | - const result = []; | |
| 561 | - //递归取值 | |
| 562 | - function traverse(arr) { | |
| 563 | - for (let item of arr) { | |
| 564 | - let children = item.children; | |
| 565 | - if (children !== undefined && Array.isArray(children)) { | |
| 566 | - traverse(children); | |
| 567 | - } else if (children === undefined) { | |
| 568 | - result.push(item) | |
| 569 | - } else { | |
| 570 | - console.log("数据格式有误 ==> { " + item + " }") | |
| 404 | + | |
| 405 | + if (this.channelBuffer.length >= pageSize) break; | |
| 406 | + } | |
| 407 | + | |
| 408 | + // 3. 如果 Buffer 还是空的 | |
| 409 | + if (this.channelBuffer.length === 0) return null; | |
| 410 | + | |
| 411 | + // 4. 从 Buffer 取出数据 | |
| 412 | + const currentBatch = this.channelBuffer.splice(0, pageSize); | |
| 413 | + | |
| 414 | + // 提取 ID | |
| 415 | + const streamList = currentBatch | |
| 416 | + .map(c => c.replaceAll('_', '-')) | |
| 417 | + .map(code => code.substring(code.indexOf('-') + 1)); | |
| 418 | + | |
| 419 | + // 5. 分批请求后端以减轻压力 | |
| 420 | + const BATCH_REQUEST_SIZE = 12; // 每批最多12路 | |
| 421 | + let allResults = []; | |
| 422 | + | |
| 423 | + // 如果总数超过BATCH_REQUEST_SIZE,则分批请求 | |
| 424 | + if (streamList.length > BATCH_REQUEST_SIZE) { | |
| 425 | + console.log(`[轮播] 总共 ${streamList.length} 路视频,将分批请求`); | |
| 426 | + | |
| 427 | + for (let i = 0; i < streamList.length; i += BATCH_REQUEST_SIZE) { | |
| 428 | + const batch = streamList.slice(i, i + BATCH_REQUEST_SIZE); | |
| 429 | + console.log(`[轮播] 请求第 ${Math.floor(i/BATCH_REQUEST_SIZE)+1} 批,共 ${batch.length} 路`); | |
| 430 | + | |
| 431 | + try { | |
| 432 | + const res = await this.fetchBatchData(batch); | |
| 433 | + if (res && res.data && res.data.data) { | |
| 434 | + allResults.push(...res.data.data); | |
| 435 | + } | |
| 436 | + // 每批之间短暂休息以避免服务器压力过大 | |
| 437 | + await new Promise(resolve => setTimeout(resolve, 200)); | |
| 438 | + } catch (e) { | |
| 439 | + console.error(`[轮播] 第 ${Math.floor(i/BATCH_REQUEST_SIZE)+1} 批请求失败:`, e); | |
| 571 | 440 | } |
| 572 | 441 | } |
| 573 | - } | |
| 574 | - //开启递归取值 | |
| 575 | - traverse(array); | |
| 576 | - return result; | |
| 577 | - }, | |
| 578 | - /** | |
| 579 | - * 巡查关闭按钮 | |
| 580 | - */ | |
| 581 | - closeInspections() { | |
| 582 | - this.stopPatrol() | |
| 583 | - this.patrolValue = false | |
| 584 | - clearInterval(this.fetchInterval); | |
| 585 | - let nowPlayArray = this.nowPlayArray; | |
| 586 | - for (let index in nowPlayArray) { | |
| 587 | - this.setPlayUrl(null, index) | |
| 588 | - } | |
| 589 | - }, | |
| 590 | - /** | |
| 591 | - * 后台关闭视频巡查 | |
| 592 | - */ | |
| 593 | - stopPatrol() { | |
| 594 | - this.$axios({ | |
| 595 | - method: 'get', | |
| 596 | - url: `/api/jt1078/query/stopPatrol/request/io`, | |
| 597 | - }).then((res) => { | |
| 598 | - if (res.data.code === 0) { | |
| 599 | - this.$message.success("视频巡查已关闭") | |
| 600 | - } else { | |
| 601 | - this.$message.error("视频巡查已关闭失败, 请联系管理员"); | |
| 602 | - } | |
| 603 | - }); | |
| 604 | - }, | |
| 605 | - /** | |
| 606 | - * 后台开启视频巡查 | |
| 607 | - */ | |
| 608 | - startPatrol(data) { | |
| 609 | - this.$axios({ | |
| 610 | - method: 'post', | |
| 611 | - url: `/api/jt1078/query/startPatrol/request/io`, | |
| 612 | - data: data, | |
| 613 | - headers: { | |
| 614 | - 'Content-Type': 'application/json', // 设置请求头 | |
| 615 | - } | |
| 616 | - }).then((res) => { | |
| 617 | - if (res.data.code === 0) { | |
| 618 | - console.log("视频巡查已开启 ===》 " + res.data.msg) | |
| 619 | - this.$message.success("视频巡查已开启"); | |
| 620 | - } else { | |
| 621 | - console.log("视频巡查开启失败 ===》 " + res.data.msg) | |
| 622 | - this.$message.error("视频巡查开启失败, 请联系管理员"); | |
| 623 | - } | |
| 624 | - }); | |
| 625 | - }, | |
| 626 | - /** | |
| 627 | - * 巡查对话框取消按钮 | |
| 628 | - */ | |
| 629 | - cancel() { | |
| 630 | - this.loading = true; | |
| 631 | - this.targetValue = []; | |
| 632 | - this.timerTime = '00:30' | |
| 633 | - }, | |
| 634 | - /** | |
| 635 | - * 开启推流视频播放 | |
| 636 | - */ | |
| 637 | - openPlay(data, idxTmp, fun) { | |
| 638 | - console.log("开启视频播放入参数据 ===》 [ " + data + " ]") | |
| 639 | - let id = data.id; | |
| 640 | - if (id === undefined || id === null) { | |
| 641 | - console.log("id 内容为 :" + id) | |
| 642 | - return; | |
| 643 | - } | |
| 644 | - console.log("id 内容为 :" + id) | |
| 645 | - let arr = id.split('_'); | |
| 646 | - if (arr === undefined || arr === null || arr.length !== 3) { | |
| 647 | - console.log("split 内容为 :" + arr) | |
| 648 | - return; | |
| 649 | - } | |
| 650 | - this.$axios({ | |
| 651 | - method: 'get', | |
| 652 | - url: '/api/jt1078/query/send/request/io/' + arr[1] + '/' + arr[2] | |
| 653 | - }).then(res => { | |
| 654 | - if (res.data.code === 0 && res.data.data) { | |
| 655 | - let videoUrl; | |
| 656 | - this.downloadURL = res.data.data.flv; | |
| 657 | - if (location.protocol === "https:") { | |
| 658 | - videoUrl = res.data.data.wss_flv; | |
| 659 | - } else { | |
| 660 | - videoUrl = res.data.data.ws_flv; | |
| 661 | - } | |
| 662 | - data.playUrl = videoUrl; | |
| 663 | - this.setPlayUrl(videoUrl, idxTmp); | |
| 664 | - } else { | |
| 665 | - if (!this.isEmpty(res.data.data) && !this.isEmpty(res.data.data.msg)) { | |
| 666 | - this.$message.error(res.data.data.msg); | |
| 667 | - } else { | |
| 668 | - this.$message.error(res.data.msg); | |
| 442 | + } else { | |
| 443 | + // 少量数据直接请求 | |
| 444 | + try { | |
| 445 | + const res = await this.fetchBatchData(streamList); | |
| 446 | + if (res && res.data && res.data.data) { | |
| 447 | + allResults = res.data.data; | |
| 669 | 448 | } |
| 449 | + } catch (e) { | |
| 450 | + console.error('[轮播] 批量请求失败:', e); | |
| 670 | 451 | } |
| 671 | - if (fun) { | |
| 672 | - fun(); | |
| 673 | - } | |
| 674 | - }) | |
| 675 | - }, | |
| 676 | - /** | |
| 677 | - * 批量开启推流视频播放 | |
| 678 | - * @param data 视频推流参数集合 Array | |
| 679 | - */ | |
| 680 | - // openBatchPlay(data) { | |
| 681 | - // if (data === undefined || !Array.isArray(data)) { | |
| 682 | - // return; | |
| 683 | - // } | |
| 684 | - // console.log("批量开启推流视频播放 ----》"+data) | |
| 685 | - // let index = 0; | |
| 686 | - // this.cycleBatchPlay(data, index) | |
| 687 | - // }, | |
| 688 | - cycleBatchPlay(data, index) { | |
| 689 | - if (data === undefined || data[index] === undefined) { | |
| 690 | - return; | |
| 691 | - } | |
| 692 | - let this_i = this | |
| 693 | - this.openPlay(data[index], Number(index), function () { | |
| 694 | - index++ | |
| 695 | - this_i.cycleBatchPlay(data, index) | |
| 696 | - }); | |
| 697 | - }, | |
| 698 | - /** | |
| 699 | - * 发送请求验证车辆是否在线 | |
| 700 | - * @param item | |
| 701 | - * @returns {boolean|*} | |
| 702 | - */ | |
| 703 | - checkStatus(item) { | |
| 704 | - if (this.lastTargetValueFilter.includes(item)) { | |
| 705 | - return true | |
| 706 | - } | |
| 707 | - if (this.simList) { | |
| 708 | - let find = this.simList.find(simData => simData.sim === item && simData.abnormalStatus !== 1); | |
| 709 | - //没找到则为离线 反之在线 | |
| 710 | - console.log("find ===> " + find) | |
| 711 | - let f = (find === undefined) | |
| 712 | - if (f) { | |
| 713 | - this.lastTargetValueFilter.push(item) | |
| 714 | - } | |
| 715 | - return !f | |
| 716 | 452 | } |
| 717 | - return true; // 直接返回预定义的在线状态 | |
| 718 | - }, | |
| 719 | - /** | |
| 720 | - * 批量验证车辆在线情况 | |
| 721 | - * @param items | |
| 722 | - * @returns {*} | |
| 723 | - */ | |
| 724 | - validateItemsOnline(items) { | |
| 725 | - // 检查所有提供的 items 是否在线,并返回过滤后的在线 items | |
| 726 | - const onlineItems = items.filter(item => this.checkStatus(item.id.split('_')[1])); | |
| 727 | - if (onlineItems.length !== items.length) { | |
| 728 | - console.warn("有车辆下线"); | |
| 453 | + | |
| 454 | + // 6. 整合结果 | |
| 455 | + const urls = new Array(pageSize).fill(''); | |
| 456 | + const infos = new Array(pageSize).fill(null); | |
| 457 | + | |
| 458 | + if (allResults.length > 0) { | |
| 459 | + allResults.forEach((item, i) => { | |
| 460 | + if (i < currentBatch.length && item) { | |
| 461 | + urls[i] = item.ws_flv; | |
| 462 | + infos[i] = { | |
| 463 | + code: currentBatch[i], | |
| 464 | + name: `通道 ${i+1}`, | |
| 465 | + videoUrl: item.ws_flv | |
| 466 | + }; | |
| 467 | + } | |
| 468 | + }); | |
| 729 | 469 | } |
| 730 | - this.nowPlayArray = items; | |
| 731 | - return onlineItems; | |
| 732 | - }, | |
| 733 | - /** | |
| 734 | - * 循环从数组中取出一定数量的值 | |
| 735 | - * @param array 原数组 | |
| 736 | - * @param batchSize 取出的数量 | |
| 737 | - * @returns {function(): *[]} 取出的结果 | |
| 738 | - */ | |
| 739 | - // createBatchFetcher(array, batchSize) { | |
| 740 | - // let currentIndex = 0; | |
| 741 | - // array = array.slice(); // 创建数组副本以避免修改原始数组 | |
| 742 | - // return function fetchNextBatch() { | |
| 743 | - // const result = []; | |
| 744 | - // for (let i = 0; i < batchSize; i++) { | |
| 745 | - // if (currentIndex >= array.length) { | |
| 746 | - // currentIndex = 0; // 当达到数组末尾时重置索引 | |
| 747 | - // } | |
| 748 | - // result.push(array[currentIndex]); | |
| 749 | - // currentIndex++; | |
| 750 | - // } | |
| 751 | - // return result; | |
| 752 | - // }; | |
| 753 | - // }, | |
| 754 | - createBatchFetcher(array, batchSize) { | |
| 755 | - let currentIndex = 0; | |
| 756 | - const originalArray = array.slice(); // 创建原始数组副本 | |
| 757 | - | |
| 758 | - return function fetchNextBatch() { | |
| 759 | - const result = []; | |
| 760 | - for (let i = 0; i < batchSize && currentIndex < originalArray.length; i++) { | |
| 761 | - result.push(originalArray[currentIndex]); | |
| 762 | - currentIndex++; | |
| 763 | - } | |
| 764 | - return result; | |
| 765 | - }; | |
| 470 | + | |
| 471 | + console.log(`[轮播] 成功获取 ${allResults.length}/${pageSize} 路视频地址`); | |
| 472 | + return { urls, infos }; | |
| 766 | 473 | }, |
| 474 | + | |
| 767 | 475 | /** |
| 768 | - * 开启视频巡查 | |
| 769 | - * @param items 选择巡查的对象 | |
| 770 | - * @param time | |
| 476 | + * 分批请求数据 | |
| 771 | 477 | */ |
| 772 | - startFetching(time) { | |
| 773 | - this.lastTargetValue = this.getLastElementsOfInnerArrays(this.targetValue); | |
| 774 | - console.log("targetValue ===> " + this.targetValue); | |
| 775 | - console.log("lastTargetValue ===> " + this.lastTargetValue); | |
| 776 | - if (!this.patrolValue && this.lastTargetValue.length > 0) { | |
| 777 | - // 在初始化 batchFetcher 之前验证 items 是否全部在线 | |
| 778 | - this.lastTargetValue = this.validateItemsOnline(this.lastTargetValue); | |
| 779 | - if (this.lastTargetValue.length === 0) { | |
| 780 | - console.warn("【车辆全部下线】请重新选择"); | |
| 781 | - return; | |
| 478 | + async fetchBatchData(streamList) { | |
| 479 | + // 请求后端 (优化点:增加重试机制和超时时间) | |
| 480 | + let retryCount = 0; | |
| 481 | + const maxRetries = 2; | |
| 482 | + | |
| 483 | + while (retryCount <= maxRetries) { | |
| 484 | + try { | |
| 485 | + const res = await this.$axios({ | |
| 486 | + method: 'post', | |
| 487 | + url: '/api/jt1078/query/beachSend/request/io', | |
| 488 | + data: streamList, | |
| 489 | + timeout: 30000 // 增加超时时间到30秒,支持大批量请求 | |
| 490 | + }); | |
| 491 | + return res; | |
| 492 | + } catch (e) { | |
| 493 | + retryCount++; | |
| 494 | + if (retryCount > maxRetries) { | |
| 495 | + console.error(`预加载请求失败,已重试${maxRetries}次`, e); | |
| 496 | + this.$message.error('批量加载失败,请检查网络或设备状态'); | |
| 497 | + return null; | |
| 498 | + } | |
| 499 | + console.warn(`请求失败,${retryCount * 2}秒后重试... (第${retryCount}/${maxRetries}次)`); | |
| 500 | + await new Promise(resolve => setTimeout(resolve, retryCount * 2000)); // 递增延迟重试:2秒、4秒 | |
| 782 | 501 | } |
| 783 | - this.startPatrol(this.lastTargetValue) | |
| 784 | - this.batchFetcher = this.createBatchFetcher(this.lastTargetValue, this.spilt); | |
| 785 | - // 立即执行一次 fetchNextBatch 并等待其完成 | |
| 786 | - let data = this.fetchNextBatch(); | |
| 787 | - console.log(parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}') + " 视频巡查数组 ===》 " + data); | |
| 788 | - this.beachSendIORequest(this.convertBeachList(data)); | |
| 789 | - // 设置定时器以定期获取批次 | |
| 790 | - this.fetchInterval = setInterval(() => { | |
| 791 | - let data = this.fetchNextBatch(); | |
| 792 | - console.log(parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}') + " 视频巡查数组 ===》 " + data); | |
| 793 | - this.beachSendIORequest(this.convertBeachList(data)); | |
| 794 | - }, time); | |
| 795 | - this.patrolValue = true; | |
| 796 | - } | |
| 797 | - }, | |
| 798 | - /** | |
| 799 | - * 数组转换 (方便批量发送请求) | |
| 800 | - */ | |
| 801 | - convertBeachList(data) { | |
| 802 | - if (data && data.length > 0) { | |
| 803 | - return data.map(item => item.id.split('_').slice(1).join('-')) | |
| 804 | 502 | } |
| 805 | 503 | }, |
| 504 | + | |
| 806 | 505 | /** |
| 807 | - * 更新巡查播放列表 | |
| 506 | + * 重建在线列表 | |
| 808 | 507 | */ |
| 809 | - fetchNextBatch() { | |
| 810 | - // 在每次获取批次前验证在线状态 | |
| 811 | - const updatedItems = this.validateItemsOnline(this.lastTargetValue); | |
| 812 | - if (updatedItems.length === 0) { | |
| 813 | - this.$message.error("车辆已全部下线,已关闭 【视屏巡查】") | |
| 814 | - this.stopFetching(); | |
| 815 | - return | |
| 816 | - } | |
| 817 | - if (updatedItems.length !== this.lastTargetValue.length) { | |
| 818 | - // 更新 items 和 batchFetcher | |
| 819 | - this.lastTargetValue = updatedItems; | |
| 820 | - this.batchFetcher = this.createBatchFetcher(updatedItems, this.spilt); | |
| 508 | + rebuildOnlineList() { | |
| 509 | + let newDeviceNodes = []; | |
| 510 | + if (this.carouselConfig.sourceType === 'all_online') { | |
| 511 | + const findOnlineDevices = (nodes) => { | |
| 512 | + if (!Array.isArray(nodes)) return; | |
| 513 | + nodes.forEach(node => { | |
| 514 | + if (node.abnormalStatus !== undefined && node.children && node.abnormalStatus === 1) { | |
| 515 | + newDeviceNodes.push(node); | |
| 516 | + } | |
| 517 | + if (node.children) findOnlineDevices(node.children); | |
| 518 | + }); | |
| 519 | + }; | |
| 520 | + findOnlineDevices(this.deviceTreeData); | |
| 521 | + } else { | |
| 522 | + const selected = this.carouselConfig.selectedNodes || []; | |
| 523 | + newDeviceNodes = selected.filter(n => n.abnormalStatus !== undefined && n.abnormalStatus === 1); | |
| 821 | 524 | } |
| 822 | - const batch = this.batchFetcher(); | |
| 823 | - this.nowPlayArray = batch; | |
| 824 | - return batch | |
| 525 | + this.carouselDeviceList = newDeviceNodes; | |
| 825 | 526 | }, |
| 826 | - /** | |
| 827 | - * 关闭视频巡查 | |
| 828 | - */ | |
| 829 | - stopFetching() { | |
| 830 | - if (this.isFetching) { | |
| 831 | - clearInterval(this.fetchInterval); | |
| 832 | - this.fetchInterval = null; | |
| 833 | - this.isFetching = false; | |
| 527 | + | |
| 528 | + stopCarousel() { | |
| 529 | + this.isCarouselRunning = false; | |
| 530 | + if (this.carouselTimer) { | |
| 531 | + clearTimeout(this.carouselTimer); | |
| 532 | + this.carouselTimer = null; | |
| 834 | 533 | } |
| 534 | + this.$message.info("轮播已停止"); | |
| 835 | 535 | }, |
| 836 | - destroy(idx) { | |
| 837 | - this.clear(idx.substring(idx.length - 1)) | |
| 536 | + | |
| 537 | + closeAllVideoNoConfirm() { | |
| 538 | + this.videoUrl = []; | |
| 539 | + this.videoDataList = []; | |
| 838 | 540 | }, |
| 839 | - treeChannelClick(device, data, isCatalog) { | |
| 840 | - if (data.channelId && !isCatalog) { | |
| 841 | - if (device.online === 0) { | |
| 842 | - this.$message.error('设备离线!不允许点播'); | |
| 843 | - this.closeLoading(); | |
| 844 | - return false; | |
| 845 | - } else { | |
| 846 | - this.isSendDevicePush(data, this.patrolValue); | |
| 847 | - } | |
| 848 | - } | |
| 541 | + | |
| 542 | + checkTimeRange(startStr, endStr) { | |
| 543 | + const now = new Date(); | |
| 544 | + const current = now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds(); | |
| 545 | + const parse = (str) => { | |
| 546 | + const [h, m, s] = str.split(':').map(Number); | |
| 547 | + return h * 3600 + m * 60 + s; | |
| 548 | + }; | |
| 549 | + return current >= parse(startStr) && current <= parse(endStr); | |
| 849 | 550 | }, |
| 850 | - contextMenuEvent: function (device, event, data, isCatalog) { | |
| 551 | + | |
| 552 | + // ========================================== | |
| 553 | + // 3. 常规操作 | |
| 554 | + // ========================================== | |
| 555 | + handleTreeLoaded(data) { | |
| 556 | + this.deviceTreeData = data; | |
| 851 | 557 | }, |
| 852 | - isSendDevicePush(itemData, patrolValue) { | |
| 853 | - if (patrolValue) { | |
| 854 | - this.videoPatrolStart(itemData); | |
| 855 | - } else { | |
| 856 | - this.sendDevicePush(itemData); | |
| 558 | + | |
| 559 | + async nodeClick(data, node) { | |
| 560 | + if (!(await this.checkCarouselPermission('播放视频'))) return; | |
| 561 | + if (data.abnormalStatus === undefined && data.children === undefined) { | |
| 562 | + if (!(await this.checkCarouselPermission('切换播放'))) return; | |
| 563 | + this.getPlayStream(data); | |
| 857 | 564 | } |
| 858 | 565 | }, |
| 859 | - //通知设备上传媒体流 | |
| 860 | - sendDevicePush(itemData, fun) { | |
| 861 | - this.save(itemData) | |
| 862 | - let deviceId = itemData.deviceId; | |
| 863 | - let channelId = itemData.channelId; | |
| 864 | - if (this.isEmpty(deviceId)) { | |
| 865 | - this.$message.error("没有获取到sim卡,请检查设备是否接入"); | |
| 866 | - this.closeLoading(); | |
| 867 | - if (fun) { | |
| 868 | - fun(); | |
| 869 | - } | |
| 870 | - return; | |
| 871 | - } | |
| 872 | - console.log("通知设备推流1:" + deviceId + " : " + channelId); | |
| 873 | - let idxTmp = this.playerIdx | |
| 566 | + | |
| 567 | + getPlayStream(data) { | |
| 568 | + let stream = data.code.replace('-', '_'); | |
| 569 | + let currentIdx = this.windowClickIndex; | |
| 570 | + let arr = stream.split("_"); | |
| 571 | + // 单路播放默认主码流(0) | |
| 874 | 572 | this.$axios({ |
| 875 | 573 | method: 'get', |
| 876 | - url: '/api/jt1078/query/send/request/io/' + deviceId + '/' + channelId | |
| 574 | + url: `/api/jt1078/query/send/request/io/${arr[1]}/${arr[2]}` | |
| 877 | 575 | }).then(res => { |
| 878 | - console.log(res) | |
| 879 | - if (res.data.code === 0 && res.data.data) { | |
| 880 | - let videoUrl; | |
| 881 | - this.port = res.data.data.port; | |
| 882 | - this.httpPort = res.data.data.httpPort; | |
| 883 | - this.stream = res.data.data.stream; | |
| 884 | - console.log(res.data.data); | |
| 885 | - if (!this.isEmpty(res.data.data)) { | |
| 886 | - this.downloadURL = res.data.data.flv; | |
| 887 | - if (location.protocol === "https:") { | |
| 888 | - videoUrl = res.data.data.wss_flv; | |
| 889 | - } else { | |
| 890 | - videoUrl = res.data.data.ws_flv; | |
| 891 | - } | |
| 892 | - console.log(videoUrl); | |
| 893 | - itemData.playUrl = videoUrl; | |
| 894 | - this.setPlayUrl(videoUrl, idxTmp); | |
| 895 | - } | |
| 576 | + if (res.data.code === 0) { | |
| 577 | + const url = res.data.data.ws_flv; | |
| 578 | + const idx = currentIdx - 1; | |
| 579 | + this.$set(this.videoUrl, idx, url); | |
| 580 | + data['videoUrl'] = url; | |
| 581 | + this.$set(this.videoDataList, idx, data); | |
| 582 | + | |
| 583 | + let nextIndex = currentIdx + 1; | |
| 584 | + const max = parseInt(this.windowNum) || 4; | |
| 585 | + if (nextIndex > max) nextIndex = 1; | |
| 586 | + this.windowClickIndex = nextIndex; | |
| 896 | 587 | } else { |
| 897 | 588 | this.$message.error(res.data.msg); |
| 898 | 589 | } |
| 899 | - if (fun) { | |
| 900 | - fun(); | |
| 901 | - } | |
| 902 | - }).catch(function (e) { | |
| 903 | - if (fun) { | |
| 904 | - fun(); | |
| 905 | - } | |
| 906 | - }).finally(() => { | |
| 907 | - this.closeLoading(); | |
| 908 | 590 | }); |
| 909 | 591 | }, |
| 910 | - /** | |
| 911 | - * 播放器赋值 | |
| 912 | - * @param url 播放内容路径 | |
| 913 | - * @param idx 播放的id | |
| 914 | - */ | |
| 915 | - setPlayUrl(url, idx) { | |
| 916 | - this.$refs.playListComponent.setPlayUrl(url, idx) | |
| 917 | - }, | |
| 918 | - checkPlayByParam() { | |
| 919 | - let {deviceId, channelId} = this.$route.query | |
| 920 | - if (deviceId && channelId) { | |
| 921 | - this.sendDevicePush({deviceId, channelId}) | |
| 922 | - } | |
| 923 | - }, | |
| 924 | - shot(e) { | |
| 925 | - console.log(e) | |
| 926 | - // send({code:'image',data:e}) | |
| 927 | - var base64ToBlob = function (code) { | |
| 928 | - let parts = code.split(';base64,'); | |
| 929 | - let contentType = parts[0].split(':')[1]; | |
| 930 | - let raw = window.atob(parts[1]); | |
| 931 | - let rawLength = raw.length; | |
| 932 | - let uInt8Array = new Uint8Array(rawLength); | |
| 933 | - for (let i = 0; i < rawLength; ++i) { | |
| 934 | - uInt8Array[i] = raw.charCodeAt(i); | |
| 935 | - } | |
| 936 | - return new Blob([uInt8Array], { | |
| 937 | - type: contentType | |
| 938 | - }); | |
| 939 | - }; | |
| 940 | - let aLink = document.createElement('a'); | |
| 941 | - let blob = base64ToBlob(e); //new Blob([content]); | |
| 942 | - let evt = document.createEvent("HTMLEvents"); | |
| 943 | - evt.initEvent("click", true, true); //initEvent 不加后两个参数在FF下会报错 事件类型,是否冒泡,是否阻止浏览器的默认行为 | |
| 944 | - aLink.download = '截图'; | |
| 945 | - aLink.href = URL.createObjectURL(blob); | |
| 946 | - aLink.click(); | |
| 947 | - }, | |
| 948 | - save(item) { | |
| 949 | - let dataStr = window.localStorage.getItem('playData') || '[]' | |
| 950 | - let data = JSON.parse(dataStr); | |
| 951 | - data[this.playerIdx] = item | |
| 952 | - window.localStorage.setItem('playData', JSON.stringify(data)) | |
| 953 | - }, | |
| 954 | - clear(idx) { | |
| 955 | - let dataStr = window.localStorage.getItem('playData') || '[]' | |
| 956 | - let data = JSON.parse(dataStr); | |
| 957 | - data[idx - 1] = null; | |
| 958 | - console.log(data); | |
| 959 | - window.localStorage.setItem('playData', JSON.stringify(data)) | |
| 960 | - }, | |
| 961 | 592 | |
| 962 | - initTreeData() { | |
| 963 | - this.showLoading(); | |
| 964 | - this.$axios({ | |
| 965 | - method: 'get', | |
| 966 | - url: `/api/jt1078/query/company/tree`, | |
| 967 | - }).then((res) => { | |
| 968 | - if (res && res.data && res.data.data) { | |
| 969 | - if (res.data.data.code == 1) { | |
| 970 | - let data = res.data.data.result; | |
| 971 | - this.initDate(this.nodes, data); | |
| 972 | - } else if (res.data.data.message) { | |
| 973 | - this.$message.error(res.data.data.message); | |
| 974 | - } | |
| 975 | - } else { | |
| 976 | - this.$message.error("请求错误,请刷新再试"); | |
| 593 | + // 优化后的右键播放 | |
| 594 | + async handleCommand(command) { | |
| 595 | + if (command === 'playback') { | |
| 596 | + if (!(await this.checkCarouselPermission('切换设备播放'))) return; | |
| 597 | + if (!this.rightClickNode) { | |
| 598 | + this.$message.warning("请选择播放设备"); | |
| 599 | + return; | |
| 977 | 600 | } |
| 978 | - this.closeLoading(); | |
| 979 | - }); | |
| 980 | - }, | |
| 981 | - initDate(nodes, datas) { | |
| 982 | - if (nodes && datas) { | |
| 983 | - let len = datas.length; | |
| 984 | - for (let i = 0; i < len; i++) { | |
| 985 | - if (null == datas[i].id || undefined == datas[i].id || "" == datas[i].id) { | |
| 986 | - continue; | |
| 987 | - } | |
| 988 | - let node = this.combationNodeValue(datas[i].name, datas[i].id, datas[i].type, true, datas[i].sim, datas[i].abnormalStatus) | |
| 989 | - nodes.push(node); | |
| 990 | - if (datas[i].children) { | |
| 991 | - node.children = []; | |
| 992 | - this.initDate(node.children, datas[i].children); | |
| 601 | + | |
| 602 | + const nodeData = this.rightClickNode.data; | |
| 603 | + // 清空当前画面 | |
| 604 | + this.videoUrl = []; | |
| 605 | + this.videoDataList = []; | |
| 606 | + | |
| 607 | + const doPlay = (channels) => { | |
| 608 | + if (!channels || channels.length === 0) { | |
| 609 | + this.$message.warning("该设备下没有可用通道"); | |
| 610 | + return; | |
| 993 | 611 | } |
| 994 | - } | |
| 995 | - } | |
| 996 | - }, | |
| 997 | - combationNodeValue(name, id, type, isParent, sim, abnormalStatus) { | |
| 998 | - if (this.isEmpty(sim)) { | |
| 999 | - sim = ""; | |
| 1000 | - } | |
| 1001 | - if (abnormalStatus >= 10 && abnormalStatus < 20) { | |
| 1002 | - name = "<view style='color:red'>" + name + "</view>"; | |
| 1003 | - } else if (abnormalStatus >= 20 && abnormalStatus < 30) { | |
| 1004 | - name = "<view style='color:#ccc'>" + name + "</view>"; | |
| 1005 | - } | |
| 1006 | - return { | |
| 1007 | - name: name, | |
| 1008 | - id: id, | |
| 1009 | - abnormalStatus: abnormalStatus, | |
| 1010 | - sim: sim, | |
| 1011 | - type: type, | |
| 1012 | - isParent: isParent, | |
| 1013 | - } | |
| 1014 | - }, | |
| 1015 | - onClick(evt, treeId, treeNode) { | |
| 1016 | - this.combationChildNode(treeNode); | |
| 1017 | - }, | |
| 1018 | - beforeExpand(treeId, treeNode) { | |
| 1019 | - return true; | |
| 1020 | - }, | |
| 1021 | - combationChildNode(treeNo) { | |
| 1022 | - this.ztreeNode = treeNo; | |
| 1023 | - if (treeNo.seachChild && (treeNo.seachChild == 'true')) { | |
| 1024 | - return; | |
| 1025 | - } | |
| 1026 | - this.showLoading(); | |
| 1027 | - if (treeNo.type == 401 || treeNo.type == '401') { | |
| 1028 | - let device = new Object(); | |
| 1029 | - if (this.isEmpty(treeNo.online)) { | |
| 1030 | - treeNo.online = 1; | |
| 1031 | - } | |
| 1032 | - device.online = treeNo.online; | |
| 1033 | - if (this.spilt == 1) { | |
| 1034 | - this.videoUrl = [] | |
| 1035 | - } | |
| 1036 | - this.closeLoading(); | |
| 1037 | - let pageObj = this; | |
| 1038 | - if (!this.isEmpty(this.sim) && !this.channel && this.sim != treeNo.sim && this.channel != treeNo.id) { | |
| 1039 | - let data = new Object(); | |
| 1040 | - data.channelId = treeNo.id; | |
| 1041 | - data.sim = treeNo.sim; | |
| 1042 | - data.deviceId = treeNo.sim; | |
| 1043 | - this.sim = treeNo.sim; | |
| 1044 | - this.channel = treeNo.id; | |
| 1045 | - console.log("stop"); | |
| 1046 | - this.sendIORequestStop(data.sim, data.channelId, function () { | |
| 1047 | - let flag = pageObj.treeChannelClick(device, data, false); | |
| 1048 | - if (false == flag) { | |
| 1049 | - treeNo.online = 0; | |
| 1050 | - } | |
| 612 | + | |
| 613 | + // 自动适配布局 | |
| 614 | + const count = channels.length; | |
| 615 | + if (count <= 1) this.windowNum = '1'; | |
| 616 | + else if (count <= 4) this.windowNum = '4'; | |
| 617 | + else if (count <= 9) this.windowNum = '9'; | |
| 618 | + else if (count <= 16) this.windowNum = '16'; | |
| 619 | + else if (count <= 25) this.windowNum = '25'; | |
| 620 | + else this.windowNum = '36'; // 最大支持36 | |
| 621 | + | |
| 622 | + this.$nextTick(() => { | |
| 623 | + const streamList = channels | |
| 624 | + .map(c => c.code.replaceAll('_', '-')) | |
| 625 | + .map(code => code.substring(code.indexOf('-') + 1)); | |
| 626 | + | |
| 627 | + this.$axios({ | |
| 628 | + method: 'post', | |
| 629 | + url: '/api/jt1078/query/beachSend/request/io', | |
| 630 | + data: streamList, | |
| 631 | + headers: { 'Content-Type': 'application/json' } | |
| 632 | + }).then(res => { | |
| 633 | + const streamData = res.data.data; | |
| 634 | + if (streamData && streamData.length > 0) { | |
| 635 | + let urls = streamData.map(item => item.ws_flv); | |
| 636 | + // 填充数据 | |
| 637 | + urls.forEach((url, i) => { | |
| 638 | + this.$set(this.videoUrl, i, url); | |
| 639 | + const channelInfo = { ...channels[i], videoUrl: url }; | |
| 640 | + this.$set(this.videoDataList, i, channelInfo); | |
| 641 | + }); | |
| 642 | + this.$message.success(`成功播放 ${streamData.length} 路视频`); | |
| 643 | + } else { | |
| 644 | + this.$message.warning("服务器未返回流地址"); | |
| 645 | + } | |
| 646 | + }).catch(err => { | |
| 647 | + console.error("播放失败", err); | |
| 648 | + this.$message.error("播放请求失败"); | |
| 649 | + }); | |
| 1051 | 650 | }); |
| 651 | + }; | |
| 652 | + | |
| 653 | + if (nodeData.children && nodeData.children.length > 0) { | |
| 654 | + doPlay(nodeData.children); | |
| 1052 | 655 | } else { |
| 1053 | - let data = new Object(); | |
| 1054 | - data.channelId = treeNo.id; | |
| 1055 | - data.sim = treeNo.sim; | |
| 1056 | - data.deviceId = treeNo.sim; | |
| 1057 | - this.sim = treeNo.sim; | |
| 1058 | - this.channel = treeNo.id; | |
| 1059 | - | |
| 1060 | - let flag = pageObj.treeChannelClick(device, data, false); | |
| 1061 | - if (false == flag) { | |
| 1062 | - treeNo.online = 0; | |
| 1063 | - } | |
| 656 | + // 假设这里需要异步加载子节点,如果是同步的可以直接忽略 else | |
| 657 | + const channels = nodeData.children || []; | |
| 658 | + doPlay(channels); | |
| 1064 | 659 | } |
| 1065 | - } else if (treeNo.type == 301 || treeNo.type == '301') { | |
| 1066 | - this.addChannel(treeNo); | |
| 1067 | - this.carTreeNode = treeNo; | |
| 1068 | - this.channel = null; | |
| 1069 | - } else if (treeNo.type == 2 || treeNo.type == '2') { | |
| 1070 | - this.requestChildNode(treeNo); | |
| 1071 | - this.sim = null; | |
| 1072 | - this.channel = null; | |
| 1073 | - this.carTreeNode = null; | |
| 1074 | - } else { | |
| 1075 | - this.carTreeNode = null; | |
| 1076 | - this.sim = null; | |
| 1077 | - this.channel = null; | |
| 1078 | - this.closeLoading(); | |
| 1079 | 660 | } |
| 1080 | 661 | }, |
| 1081 | - /** | |
| 1082 | - * 添加通道 | |
| 1083 | - */ | |
| 1084 | - addChannel(treeNo) { | |
| 1085 | - let labels = ['ADAS', 'DSM', '路况', '司机', '整车前', '中门', '倒车', '前门客流', '后面客流']; | |
| 1086 | - let children = []; | |
| 1087 | - let len = labels.length; | |
| 1088 | - let i = 0; | |
| 1089 | - let pageObj = this; | |
| 1090 | - for (; i < len; i++) { | |
| 1091 | - console.log(treeNo.abnormalStatus + "==========================>" + i); | |
| 1092 | - let node = this.combationNodeValue(labels[i], i + 1, 401, false, treeNo.sim, treeNo.abnormalStatus); | |
| 1093 | - node.sim = treeNo.sim; | |
| 1094 | - node.zbh = treeNo.name; | |
| 1095 | - node.id = i + 1; | |
| 1096 | - children.push(node); | |
| 1097 | - } | |
| 1098 | - this.ztreeObj.addNodes(treeNo, 0, children, true); | |
| 1099 | - treeNo.seachChild = 'true'; | |
| 1100 | - pageObj.sim = treeNo.sim; | |
| 1101 | - this.closeLoading(); | |
| 1102 | - }, | |
| 1103 | - requestChildNode(treeNo) { | |
| 1104 | - if (treeNo.spread === 'false' || !treeNo.spread) { | |
| 1105 | - let id = treeNo.id; | |
| 1106 | - this.$axios({ | |
| 1107 | - method: 'get', | |
| 1108 | - url: `/api/jt1078/query/car/tree/` + id, | |
| 1109 | - }).then((res) => { | |
| 1110 | - if (res && res.data && res.data.data) { | |
| 1111 | - if (res.data.data.code == 1) { | |
| 1112 | - let children = []; | |
| 1113 | - this.initDate(children, res.data.data.result); | |
| 1114 | - this.ztreeObj.addNodes(treeNo, -1, children, true); | |
| 1115 | - treeNo.seachChild = 'true'; | |
| 1116 | - let _this = this; | |
| 1117 | - this.carPlayTimer = setTimeout(function () { | |
| 1118 | - _this.requestChildNode1(); | |
| 1119 | - }, 15000); | |
| 1120 | - } else if (res.data.data.message) { | |
| 1121 | - this.$message.error(res.data.data.message); | |
| 1122 | - } | |
| 1123 | - } else { | |
| 1124 | - this.$message.error("请求错误,请刷新再试"); | |
| 1125 | - } | |
| 1126 | - this.closeLoading(); | |
| 662 | + | |
| 663 | + async closeAllVideo() { | |
| 664 | + if (!(await this.checkCarouselPermission('关闭所有视频'))) return; | |
| 665 | + if (this.videoUrl.some(u => u)) { | |
| 666 | + this.$confirm('确认全部关闭直播 ?', '提示', { | |
| 667 | + confirmButtonText: '确定', | |
| 668 | + cancelButtonText: '取消', | |
| 669 | + type: 'warning' | |
| 1127 | 670 | }) |
| 671 | + .then(() => { | |
| 672 | + this.videoUrl = []; | |
| 673 | + this.videoDataList = []; | |
| 674 | + }).catch(() => { | |
| 675 | + }); | |
| 676 | + } else { | |
| 677 | + this.$message.error('没有可以关闭的视频'); | |
| 1128 | 678 | } |
| 1129 | 679 | }, |
| 1130 | - requestChildNode1() { | |
| 1131 | - this.$axios({ | |
| 1132 | - method: 'get', | |
| 1133 | - url: `/api/jt1078/query/car/tree/` + 100, | |
| 1134 | - }).then((res) => { | |
| 1135 | - if (res && res.data && res.data.data) { | |
| 1136 | - if (res.data.data.code == 1) { | |
| 1137 | - this.refreshRequestRefresh(res.data.data.result); | |
| 1138 | - } else if (res.data.data.message) { | |
| 1139 | - this.$message.error(res.data.data.message); | |
| 1140 | - } | |
| 1141 | - } else { | |
| 1142 | - this.$message.error("请求错误,请刷新再试"); | |
| 1143 | - } | |
| 1144 | - }) | |
| 1145 | - }, | |
| 1146 | - refreshRequestRefresh(nodes) { | |
| 1147 | - if (nodes) { | |
| 1148 | - let length = nodes.length; | |
| 1149 | - for (let i = 0; i < length; i++) { | |
| 1150 | - let findNode = this.ztreeObj.getNodeByParam("id", nodes[i].id + "", null); | |
| 1151 | - if (findNode) { | |
| 1152 | - findNode.name = nodes[i].name; | |
| 1153 | - if (findNode.type == 301 || findNode.type == '301') { | |
| 1154 | - findNode.name = nodes[i].name; | |
| 1155 | - } else { | |
| 1156 | - findNode.name = nodes[i].name; | |
| 1157 | - } | |
| 1158 | - this.ztreeObj.updateNode(findNode); | |
| 1159 | 680 | |
| 1160 | - if (nodes[i].children) { | |
| 1161 | - this.refreshRequestRefresh(nodes[i].children); | |
| 1162 | - } | |
| 1163 | - } | |
| 1164 | - } | |
| 681 | + async closeVideo() { | |
| 682 | + if (!(await this.checkCarouselPermission('关闭当前窗口'))) return; | |
| 683 | + const idx = Number(this.windowClickIndex) - 1; | |
| 684 | + if (this.videoUrl && this.videoUrl[idx]) { | |
| 685 | + // 设置为null而不是空字符串,确保播放器完全销毁 | |
| 686 | + this.$set(this.videoUrl, idx, null); | |
| 687 | + this.$set(this.videoDataList, idx, null); | |
| 688 | + } else { | |
| 689 | + this.$message.warning(`当前窗口 [${this.windowClickIndex}] 没有正在播放的视频`); | |
| 1165 | 690 | } |
| 1166 | 691 | }, |
| 1167 | - showLoading() { | |
| 1168 | - this.loading = true; | |
| 1169 | - this.fullscreenLoading = true; | |
| 1170 | - // this.fullscreenLoadingStyle ='display:block'; | |
| 1171 | - }, | |
| 1172 | - closeLoading() { | |
| 1173 | - this.loading = false; | |
| 1174 | - //this.fullscreenLoadingStyle ='display:none'; | |
| 1175 | - this.fullscreenLoading = false; | |
| 1176 | - console.log("已经关闭"); | |
| 1177 | - }, | |
| 1178 | - sendIORequestStop(sim, channel, fun) { | |
| 1179 | - if (this.isEmpty(sim) || this.isEmpty(channel)) { | |
| 1180 | - console.log("sim:" + sim + ";channel:" + channel); | |
| 1181 | - if (fun) { | |
| 1182 | - fun(); | |
| 1183 | - } | |
| 1184 | - return; | |
| 1185 | - } | |
| 1186 | - this.videoUrl = ['']; | |
| 1187 | - this.$axios({ | |
| 1188 | - method: 'get', | |
| 1189 | - url: `/api/jt1078/query/send/stop/io/` + sim + "/" + channel + "/" + this.stream + "/" + this.port + "/" + this.httpPort, | |
| 1190 | - }).then((res) => { | |
| 1191 | - console.log(res); | |
| 1192 | - if (fun) { | |
| 1193 | - fun(); | |
| 1194 | - } | |
| 1195 | - }); | |
| 1196 | - }, | |
| 1197 | - isEmpty(val) { | |
| 1198 | - return null == val || undefined == val || "" == val; | |
| 1199 | - }, | |
| 1200 | - close() { | |
| 1201 | - let pageObj = this; | |
| 1202 | - this.showLoading(); | |
| 1203 | - this.historyPlayListHtml = null; | |
| 1204 | - this.startTime = null; | |
| 1205 | - this.endTime = null; | |
| 1206 | - if (this.hisotoryPlayFlag) { | |
| 1207 | - this.sendIORequestStop(this.sim, this.channel, function () { | |
| 1208 | - pageObj.showVideoDialog = false; | |
| 1209 | - pageObj.videoUrl = ['']; | |
| 1210 | - pageObj.closeLoading(); | |
| 1211 | - console.log("关闭弹窗"); | |
| 1212 | - }); | |
| 692 | + | |
| 693 | + toggleFullscreen() { | |
| 694 | + const element = this.$refs.videoMain.$el; | |
| 695 | + if (!this.isFullscreen) { | |
| 696 | + if (element.requestFullscreen) element.requestFullscreen(); | |
| 697 | + else if (element.webkitRequestFullscreen) element.webkitRequestFullscreen(); | |
| 1213 | 698 | } else { |
| 1214 | - pageObj.showVideoDialog = false; | |
| 1215 | - pageObj.closeLoading(); | |
| 699 | + if (document.exitFullscreen) document.exitFullscreen(); | |
| 700 | + else if (document.webkitExitFullscreen) document.webkitExitFullscreen(); | |
| 1216 | 701 | } |
| 1217 | 702 | }, |
| 1218 | - /** | |
| 1219 | - * 视频一键播放 | |
| 1220 | - */ | |
| 1221 | - oneClickPlayback() { | |
| 1222 | - if (this.isEmpty(this.simNodeData)) { | |
| 1223 | - this.$message.error('请选择车辆'); | |
| 1224 | - return; | |
| 1225 | - } | |
| 1226 | - if (this.isEmpty(this.simNodeData.abnormalStatus)) { | |
| 1227 | - this.$message.error('请检查车辆状态'); | |
| 1228 | - return; | |
| 1229 | - } | |
| 1230 | - if (this.simNodeData.abnormalStatus != 1) { | |
| 1231 | - this.$message.error('车辆设备离线,请检查设备'); | |
| 1232 | - return; | |
| 1233 | - } | |
| 1234 | - if (this.isEmpty(this.simNodeData.sim)) { | |
| 1235 | - this.$message.error('无法获取SIM卡信息,请检查设备'); | |
| 1236 | - return; | |
| 1237 | - } | |
| 1238 | - let children = this.simNodeData.children; | |
| 1239 | - if (children && children.length > 0){ | |
| 1240 | - this.spilt = 9; | |
| 1241 | - let data = [] | |
| 1242 | - let count = 1; | |
| 1243 | - for (let i in children) { | |
| 1244 | - let split = children[i].id.split("_"); | |
| 1245 | - if (split.length === 3){ | |
| 1246 | - data.push(`${split[1]}-${split[2]}`) | |
| 1247 | - if (count++ === this.spilt){ | |
| 1248 | - break | |
| 1249 | - } | |
| 1250 | - } | |
| 1251 | - } | |
| 1252 | - if (data.length === 0){ | |
| 1253 | - this.$message.error("该设备无通道播放") | |
| 1254 | - return; | |
| 1255 | - } | |
| 1256 | - this.beachSendIORequest(data); | |
| 1257 | - }else { | |
| 1258 | - this.$message.error("该设备无通道播放") | |
| 1259 | - } | |
| 1260 | - | |
| 703 | + handleFullscreenChange() { | |
| 704 | + this.isFullscreen = !!document.fullscreenElement; | |
| 1261 | 705 | }, |
| 1262 | - /** | |
| 1263 | - * 批量发送推流请求 | |
| 1264 | - * @param data | |
| 1265 | - */ | |
| 1266 | - beachSendIORequest(data) { | |
| 1267 | - console.log(data) | |
| 1268 | - this.$axios({ | |
| 1269 | - method: 'post', | |
| 1270 | - url: '/api/jt1078/query/beachSend/request/io', | |
| 1271 | - data: data, | |
| 1272 | - headers: { | |
| 1273 | - 'Content-Type': 'application/json', // 设置请求头 | |
| 1274 | - } | |
| 1275 | - }).then( | |
| 1276 | - res => { | |
| 1277 | - let dataList = res.data.data; | |
| 1278 | - console.log(dataList); | |
| 1279 | - if (res.data.code == 0 && dataList != null && dataList.length >= 0) { | |
| 1280 | - for (let i in dataList) { | |
| 1281 | - this.$refs.playListComponent.setPlayUrl(dataList[i].ws_flv, i) | |
| 1282 | - } | |
| 1283 | - } else { | |
| 1284 | - this.$message.error(res.data.msg); | |
| 1285 | - } | |
| 1286 | - } | |
| 1287 | - ) | |
| 706 | + updateSidebarState() { | |
| 707 | + this.sidebarState = !this.sidebarState; | |
| 708 | + this.$refs.playListComponent.updateGridTemplate(this.windowNum); | |
| 1288 | 709 | }, |
| 1289 | - // playOneAllChannel(channel) { | |
| 1290 | - // if (channel == 9) { | |
| 1291 | - // return; | |
| 1292 | - // } | |
| 1293 | - // let item = new Object(); | |
| 1294 | - // item.deviceId = this.sim; | |
| 1295 | - // item.channelId = 1 + channel; | |
| 1296 | - // this.playerIdx = channel; | |
| 1297 | - // | |
| 1298 | - // this.sendDevicePush(item); | |
| 1299 | - // }, | |
| 1300 | - spiltClickFun(val) { | |
| 1301 | - this.spilt = val; | |
| 1302 | - if (val - 1 < this.playerIdx) { | |
| 1303 | - this.playerIdx = val - 1; | |
| 1304 | - } | |
| 710 | + handleClick(data, index) { | |
| 711 | + this.windowClickIndex = index + 1; | |
| 712 | + this.windowClickData = data; | |
| 1305 | 713 | }, |
| 1306 | - closeSelectItem() { | |
| 1307 | - console.log("============================>" + this.playerIdx); | |
| 1308 | - this.setPlayUrl(null, this.playerIdx) | |
| 714 | + showTooltip() { | |
| 715 | + this.tooltipVisible = true; | |
| 1309 | 716 | }, |
| 1310 | - closeSelectCarItem() { | |
| 1311 | - for (let index = 0; index < 9; index++) { | |
| 1312 | - this.setPlayUrl(null, index) | |
| 1313 | - } | |
| 717 | + hideTooltip() { | |
| 718 | + this.tooltipVisible = false; | |
| 1314 | 719 | }, |
| 1315 | - showRMenu(event) { | |
| 1316 | - if (this.isEmpty(this.rightMenuId)) { | |
| 1317 | - return; | |
| 720 | + nodeContextmenu(event, data, node) { | |
| 721 | + if (data.abnormalStatus !== undefined && data.children) { | |
| 722 | + this.rightClickNode = node; | |
| 723 | + event.preventDefault(); | |
| 724 | + const menu = this.$refs.contextMenu; | |
| 725 | + menu.show(); | |
| 726 | + menu.$el.style.position = 'fixed'; | |
| 727 | + menu.$el.style.left = `${event.clientX}px`; | |
| 728 | + menu.$el.style.top = `${event.clientY}px`; | |
| 1318 | 729 | } |
| 1319 | - let carMenu = document.getElementById(this.rightMenuId); | |
| 1320 | - carMenu.setAttribute("style", "display:block"); | |
| 1321 | - | |
| 1322 | - let x = event.clientX; | |
| 1323 | - let y = event.clientY; | |
| 1324 | - carMenu.setAttribute("style", "display:block;top:" + y + "px;left:" + x + "px"); | |
| 1325 | - document.addEventListener("click", this.hideMenu); | |
| 1326 | - console.log(this.rightMenuId); | |
| 1327 | 730 | }, |
| 1328 | - hideMenu() { | |
| 1329 | - if (this.isEmpty(this.rightMenuId)) { | |
| 1330 | - return; | |
| 1331 | - } | |
| 1332 | - let carMenu = document.getElementById(this.rightMenuId); | |
| 1333 | - carMenu.setAttribute("style", "display:none"); | |
| 1334 | - this.rightMenuId = null; | |
| 1335 | - } | |
| 1336 | 731 | } |
| 1337 | 732 | }; |
| 1338 | 733 | </script> |
| 734 | + | |
| 1339 | 735 | <style scoped> |
| 1340 | -.inspections-tree >>> .el-tree { | |
| 1341 | - padding-bottom: 22px; | |
| 736 | +.el-header { | |
| 737 | + background-color: #B3C0D1; | |
| 738 | + color: #333; | |
| 739 | + text-align: center; | |
| 740 | + display: flex; | |
| 741 | + align-items: center; | |
| 742 | + gap: 10px; | |
| 743 | + padding: 0 20px; | |
| 1342 | 744 | } |
| 1343 | 745 | |
| 1344 | -.device-list-tree >>> .el-tree { | |
| 1345 | - padding-bottom: 13px; | |
| 746 | +.header-right-info { | |
| 747 | + margin-left: auto; | |
| 748 | + font-weight: bold; | |
| 749 | + font-size: 14px; | |
| 1346 | 750 | } |
| 1347 | 751 | |
| 1348 | -.device-list-tree >>> .el-tree-node__content { | |
| 1349 | - padding-bottom: 13px; | |
| 1350 | - height: 20px; | |
| 752 | +.el-aside { | |
| 753 | + background-color: white; | |
| 754 | + color: #333; | |
| 755 | + text-align: center; | |
| 756 | + height: 100%; | |
| 757 | + width: 20%; | |
| 758 | + padding: 10px; | |
| 759 | + margin-right: 1px; | |
| 1351 | 760 | } |
| 1352 | 761 | |
| 1353 | 762 | .el-main { |
| 1354 | - background-color: rgba(0, 0, 0, 0.84); | |
| 763 | + background-color: rgba(0, 0, 0, 0.95); | |
| 1355 | 764 | color: #333; |
| 1356 | 765 | text-align: center; |
| 1357 | 766 | line-height: 160px; |
| ... | ... | @@ -1359,165 +768,35 @@ export default { |
| 1359 | 768 | margin-left: 1px; |
| 1360 | 769 | } |
| 1361 | 770 | |
| 1362 | -.device-tree-main-box { | |
| 1363 | - text-align: left; | |
| 1364 | -} | |
| 1365 | - | |
| 1366 | -.btn { | |
| 1367 | - margin: 0 10px; | |
| 1368 | -} | |
| 1369 | - | |
| 1370 | -.btn:hover { | |
| 1371 | - color: #409EFF; | |
| 1372 | -} | |
| 1373 | - | |
| 1374 | -.btn.active { | |
| 1375 | - color: #409EFF; | |
| 771 | +body > .el-container { | |
| 772 | + margin-bottom: 40px; | |
| 1376 | 773 | } |
| 1377 | 774 | |
| 1378 | -.redborder { | |
| 1379 | - border: 2px solid red !important; | |
| 775 | +.el-container { | |
| 776 | + height: 90vh; | |
| 1380 | 777 | } |
| 1381 | 778 | |
| 1382 | -.play-box { | |
| 1383 | - background-color: #000000; | |
| 1384 | - border: 2px solid #505050; | |
| 1385 | - display: flex; | |
| 1386 | - align-items: center; | |
| 1387 | - justify-content: center; | |
| 779 | +.el-container:nth-child(5) .el-aside, .el-container:nth-child(6) .el-aside { | |
| 780 | + line-height: 260px; | |
| 1388 | 781 | } |
| 1389 | 782 | |
| 1390 | -.historyListLi { | |
| 1391 | - width: 97%; | |
| 1392 | - white-space: nowrap; | |
| 1393 | - text-overflow: ellipsis; | |
| 1394 | - cursor: pointer; | |
| 1395 | - padding: 3px; | |
| 1396 | - margin-bottom: 6px; | |
| 1397 | - border: 1px solid #000000; | |
| 783 | +.el-dropdown-link { | |
| 784 | + display: none; | |
| 1398 | 785 | } |
| 1399 | 786 | |
| 1400 | -.historyListDiv { | |
| 1401 | - height: 80vh; | |
| 787 | +.el-container:nth-child(7) .el-aside { | |
| 1402 | 788 | width: 100%; |
| 1403 | - overflow-y: auto; | |
| 1404 | - overflow-x: hidden; | |
| 789 | + line-height: 320px; | |
| 1405 | 790 | } |
| 1406 | 791 | |
| 1407 | -/* 菜单的样式 */ | |
| 1408 | -.rMenu { | |
| 1409 | - position: absolute; | |
| 1410 | - top: 0; | |
| 1411 | - display: none; | |
| 1412 | - margin: 0; | |
| 792 | +.el-main:fullscreen, .el-main:-webkit-full-screen { | |
| 793 | + background-color: black; | |
| 794 | + width: 100vw; | |
| 795 | + height: 100vh; | |
| 1413 | 796 | padding: 0; |
| 1414 | - text-align: left; | |
| 1415 | - border: 1px solid #BFBFBF; | |
| 1416 | - border-radius: 3px; | |
| 1417 | - background-color: #EEE; | |
| 1418 | - box-shadow: 0 0 10px #AAA; | |
| 1419 | -} | |
| 1420 | - | |
| 1421 | -.rMenu li { | |
| 1422 | - width: 170px; | |
| 1423 | - list-style: none outside none; | |
| 1424 | - cursor: default; | |
| 1425 | - color: #666; | |
| 1426 | - margin-left: -20px; | |
| 1427 | -} | |
| 1428 | - | |
| 1429 | -.rMenu li:hover { | |
| 1430 | - color: #EEE; | |
| 1431 | - background-color: #666; | |
| 1432 | -} | |
| 1433 | - | |
| 1434 | -li#menu-item-delete, li#menu-item-rename { | |
| 1435 | - margin-top: 1px; | |
| 1436 | -} | |
| 1437 | - | |
| 1438 | -.videoList { | |
| 797 | + margin: 0; | |
| 1439 | 798 | display: flex; |
| 1440 | - flex-wrap: wrap; | |
| 1441 | - align-content: flex-start; | |
| 1442 | -} | |
| 1443 | - | |
| 1444 | -.video-item { | |
| 1445 | - position: relative; | |
| 1446 | - width: 15rem; | |
| 1447 | - height: 10rem; | |
| 1448 | - margin-right: 1rem; | |
| 1449 | - background-color: #000000; | |
| 799 | + flex-direction: column; | |
| 800 | + overflow: hidden; | |
| 1450 | 801 | } |
| 1451 | - | |
| 1452 | -.video-item-img { | |
| 1453 | - position: absolute; | |
| 1454 | - top: 0; | |
| 1455 | - bottom: 0; | |
| 1456 | - left: 0; | |
| 1457 | - right: 0; | |
| 1458 | - margin: auto; | |
| 1459 | - width: 100%; | |
| 1460 | - height: 100%; | |
| 1461 | -} | |
| 1462 | - | |
| 1463 | -.video-item-img:after { | |
| 1464 | - content: ""; | |
| 1465 | - display: inline-block; | |
| 1466 | - position: absolute; | |
| 1467 | - z-index: 2; | |
| 1468 | - top: 0; | |
| 1469 | - bottom: 0; | |
| 1470 | - left: 0; | |
| 1471 | - right: 0; | |
| 1472 | - margin: auto; | |
| 1473 | - width: 3rem; | |
| 1474 | - height: 3rem; | |
| 1475 | - background-image: url("../assets/loading.png"); | |
| 1476 | - background-size: cover; | |
| 1477 | - background-color: #000000; | |
| 1478 | -} | |
| 1479 | - | |
| 1480 | -.video-item-title { | |
| 1481 | - position: absolute; | |
| 1482 | - bottom: 0; | |
| 1483 | - color: #000000; | |
| 1484 | - background-color: #ffffff; | |
| 1485 | - line-height: 1.5rem; | |
| 1486 | - padding: 0.3rem; | |
| 1487 | - width: 14.4rem; | |
| 1488 | -} | |
| 1489 | - | |
| 1490 | -.baidumap { | |
| 1491 | - width: 100%; | |
| 1492 | - height: 100%; | |
| 1493 | - border: none; | |
| 1494 | - position: absolute; | |
| 1495 | - left: 0; | |
| 1496 | - top: 0; | |
| 1497 | - right: 0; | |
| 1498 | - bottom: 0; | |
| 1499 | - margin: auto; | |
| 1500 | -} | |
| 1501 | - | |
| 1502 | -/* 去除百度地图版权那行字 和 百度logo */ | |
| 1503 | -.baidumap > .BMap_cpyCtrl { | |
| 1504 | - display: none !important; | |
| 1505 | -} | |
| 1506 | - | |
| 1507 | -.baidumap > .anchorBL { | |
| 1508 | - display: none !important; | |
| 1509 | -} | |
| 1510 | - | |
| 1511 | -.scroll-container { | |
| 1512 | - max-height: 85vh; /* 设置最大高度为85%的视口高度 */ | |
| 1513 | - overflow-y: auto; /* 内容超出时显示垂直滚动条 */ | |
| 1514 | - overflow-x: hidden; /* 隐藏水平滚动条 */ | |
| 1515 | -} | |
| 1516 | - | |
| 1517 | -.transfer-footer { | |
| 1518 | - margin-left: 20px; | |
| 1519 | - padding: 6px 5px; | |
| 1520 | -} | |
| 1521 | - | |
| 1522 | - | |
| 1523 | 802 | </style> | ... | ... |
web_src/src/components/HistoricalRecord.vue
| 1 | 1 | <template> |
| 2 | - <div style="width: 2000px"> | |
| 3 | - <el-container v-loading="loading" style="height: 93vh;" element-loading-text="拼命加载中"> | |
| 4 | - <el-aside width="400px" style="background-color: #ffffff;height: 100%;"> | |
| 5 | - <el-main style="padding: 0;width: 100%;height: 68%;background: white;margin-bottom: 10px"> | |
| 6 | - <device1078-tree :tree-data="sourceValue" @node-click="nodeClick"></device1078-tree> | |
| 7 | - </el-main> | |
| 8 | - <el-footer style="width: 100%;height: 30%;background: grey"> | |
| 9 | - <el-form ref="form" class="historyButton" style="padding-top: 20px"> | |
| 10 | - <el-form-item label="设备信息" style="color: white;text-align: left;"> | |
| 11 | - <el-tag effect="dark" v-if="sim_channel_data"> | |
| 12 | - {{ `车辆:${sim_channel_data.pid} 通道:${sim_channel_data.name}` }} | |
| 13 | - </el-tag> | |
| 14 | - <el-tag effect="dark" v-else> | |
| 15 | - 请选择车辆通道 | |
| 16 | - </el-tag> | |
| 17 | - </el-form-item> | |
| 18 | - <el-form-item label="日期" style="color: white;"> | |
| 19 | - <el-date-picker | |
| 20 | - v-model="date" | |
| 21 | - align="right" | |
| 22 | - type="date" | |
| 23 | - style="width: 70%;" | |
| 24 | - placeholder="选择日期" | |
| 25 | - :picker-options="pickerOptions"> | |
| 26 | - </el-date-picker> | |
| 27 | - </el-form-item> | |
| 28 | - <el-form-item label="时间" style="color: white;"> | |
| 29 | - <el-time-picker | |
| 30 | - is-range | |
| 31 | - v-model="timeList" | |
| 32 | - style="width: 70%;" | |
| 33 | - range-separator="至" | |
| 34 | - start-placeholder="开始时间" | |
| 35 | - end-placeholder="结束时间" | |
| 36 | - placeholder="选择时间范围"> | |
| 37 | - </el-time-picker> | |
| 38 | - </el-form-item> | |
| 39 | - <el-form-item> | |
| 40 | - <el-button type="primary" @click="searchHistoryTimer" style="width: 70%;">搜索</el-button> | |
| 41 | - </el-form-item> | |
| 42 | - </el-form> | |
| 43 | - </el-footer> | |
| 44 | - </el-aside> | |
| 45 | - <el-container style="height: 93vh;"> | |
| 46 | - <el-main style="padding: 0;height: 65%;"> | |
| 47 | - <div class="scroll-container" | |
| 48 | - style="width: 100%;height: 99%;display: flex;flex-wrap: wrap;background-color: #000;"> | |
| 49 | - <div style="width: 99%;height: 99%;display: flex;flex-wrap: wrap;background-color: #000;"> | |
| 50 | - <div v-if="!videoUrl[0]" style="color: #ffffff;font-size: 30px;font-weight: bold;"></div> | |
| 51 | - <player ref="player" v-else :initial-play-url="videoUrl[0]" | |
| 52 | - style="width: 100%;height: 99%;"/> | |
| 53 | - </div> | |
| 2 | + <el-container style="height: 90vh; flex-direction: column;"> | |
| 3 | + <!-- Main Container with SplitPanels --> | |
| 4 | + <el-main class="layout-main"> | |
| 5 | + <splitpanes class="splitpanes-container" > | |
| 6 | + <!-- 左侧面板 --> | |
| 7 | + <pane :size="20" min-size="10px" class="aside-pane" resizable> | |
| 8 | + <device1078-tree :tree-data="sourceValue" style="width: 100%;" @node-click="nodeClick"></device1078-tree> | |
| 9 | + </pane> | |
| 10 | + | |
| 11 | + <!-- 右侧主内容 --> | |
| 12 | + <pane :size="86" class="main-pane" | |
| 13 | + v-loading="loading" | |
| 14 | + element-loading-text="拼命加载中" | |
| 15 | + element-loading-spinner="el-icon-loading" | |
| 16 | + element-loading-background="rgba(0, 0, 0, 0.8)" > | |
| 17 | + <div class="content-main"> | |
| 18 | + <historical-record-form style="text-align:left" :inline="true" v-show="showSearch" :query-params="queryParams" | |
| 19 | + @handleQuery="handleQuery" | |
| 20 | + /> | |
| 21 | + <el-row v-if="deviceData || channelData" :gutter="10" class="mb8"> | |
| 22 | + <el-col :span="1.5"> | |
| 23 | + <el-tag | |
| 24 | + effect="dark"> | |
| 25 | + {{ deviceTitle }} | |
| 26 | + </el-tag> | |
| 27 | + </el-col> | |
| 28 | + </el-row> | |
| 29 | + <history-search-table ref="historySearchTable" style="height: 100%;" :table-data="historyData" | |
| 30 | + @playHistoryVideo="clickHistoricalPlay" | |
| 31 | + @uploadHistoryVideo="uploadHistoryVideo"/> | |
| 32 | + <history-play-dialog ref="historyPlayDialog" /> | |
| 54 | 33 | </div> |
| 55 | - </el-main> | |
| 56 | - <el-footer style="width: 100%;height: 30%;background-color: white"> | |
| 57 | - <history-search-table :table-data="historyData" | |
| 58 | - @playHistoryVideo="clickHistoricalPlay" | |
| 59 | - @uploadHistoryVideo="uploadHistoryVideo"/> | |
| 60 | - </el-footer> | |
| 61 | - </el-container> | |
| 62 | - </el-container> | |
| 63 | - </div> | |
| 34 | + </pane> | |
| 35 | + </splitpanes> | |
| 36 | + </el-main> | |
| 37 | + </el-container> | |
| 38 | + | |
| 64 | 39 | </template> |
| 65 | 40 | |
| 66 | 41 | <script> |
| ... | ... | @@ -68,22 +43,36 @@ |
| 68 | 43 | //例如:import 《组件名称》 from '《组件路径》, |
| 69 | 44 | import player from "./common/JessVideoPlayer.vue"; |
| 70 | 45 | import CarTree from "./JT1078Components/cascader/CarTree.vue"; |
| 71 | -import {parseTime} from "../../utils/ruoyi"; | |
| 72 | 46 | import HistoricalData from "./JT1078Components/historical/HistoricalDataTree.vue"; |
| 73 | 47 | import HistoryList from "./JT1078Components/HistoryData.vue"; |
| 74 | 48 | import Device1078Tree from "./JT1078Components/deviceList/Device1078Tree.vue"; |
| 49 | +import HistoryPlayDialog from "./JT1078Components/HistoryPlayDialog.vue"; | |
| 50 | +import HistoricalRecordForm from "./JT1078Components/HistoryRecordFrom.vue"; | |
| 75 | 51 | import HistorySearchTable from "./JT1078Components/HistorySearchTable.vue"; |
| 76 | 52 | import userService from "./service/UserService"; |
| 53 | +import { Splitpanes, Pane } from 'splitpanes' | |
| 54 | +import {parseTime} from "../../utils/ruoyi"; | |
| 77 | 55 | |
| 78 | 56 | export default { |
| 79 | 57 | //import引入的组件需要注入到对象中才能使用" |
| 80 | - components: {HistorySearchTable, HistoryList, Device1078Tree, HistoricalData, CarTree, player}, | |
| 58 | + components: { | |
| 59 | + HistoryPlayDialog, | |
| 60 | + HistorySearchTable, HistoryList, Device1078Tree, HistoricalData, CarTree, player,HistoricalRecordForm, Splitpanes, Pane}, | |
| 81 | 61 | props: {}, |
| 82 | 62 | data() { |
| 83 | 63 | //这里存放数据" |
| 84 | 64 | return { |
| 85 | 65 | //列表定时器 |
| 86 | 66 | timer: null, |
| 67 | + open: false, | |
| 68 | + videoUrlData: { | |
| 69 | + startTime: '', | |
| 70 | + endTime: '', | |
| 71 | + sim: '', | |
| 72 | + channel: '', | |
| 73 | + device: '', | |
| 74 | + channelName: '', | |
| 75 | + }, | |
| 87 | 76 | //历史视频列表定时器 |
| 88 | 77 | historyTimer: null, |
| 89 | 78 | historyData: [], |
| ... | ... | @@ -97,7 +86,26 @@ export default { |
| 97 | 86 | loading: false, |
| 98 | 87 | //sim号和通道号,格式为:sim-channel |
| 99 | 88 | sim_channel: null, |
| 100 | - sim_channel_data: null, | |
| 89 | + channelData: null, | |
| 90 | + nodeChannelData: null, | |
| 91 | + deviceData: null, | |
| 92 | + deviceTitle: '', | |
| 93 | + showSearch: true, | |
| 94 | + queryParams: { | |
| 95 | + time: this.getTodayRange(), | |
| 96 | + }, | |
| 97 | + deviceList: [ | |
| 98 | + "600201", | |
| 99 | + "600202", | |
| 100 | + "600203", | |
| 101 | + "600204", | |
| 102 | + "600205", | |
| 103 | + "601101", | |
| 104 | + "601102", | |
| 105 | + "601103", | |
| 106 | + "601104", | |
| 107 | + "CS-010", | |
| 108 | + ], | |
| 101 | 109 | //日期快捷选择 |
| 102 | 110 | pickerOptions: { |
| 103 | 111 | disabledDate(time) { |
| ... | ... | @@ -130,30 +138,94 @@ export default { |
| 130 | 138 | //选中的日期 |
| 131 | 139 | date: null, |
| 132 | 140 | historyPlayListHtml: '', |
| 133 | - videoUrl: [] | |
| 141 | + videoUrl: [], | |
| 142 | + deviceNode: null, | |
| 134 | 143 | }; |
| 135 | 144 | }, |
| 136 | 145 | //计算属性 类似于data概念", |
| 137 | 146 | computed: {}, |
| 138 | 147 | //监控data中的数据变化", |
| 139 | - watch: {}, | |
| 148 | + watch: { | |
| 149 | + deviceNode(val) { | |
| 150 | + this.deviceNode = val | |
| 151 | + this.$refs.historySearchTable.deviceNode = val | |
| 152 | + } | |
| 153 | + }, | |
| 140 | 154 | //方法集合", |
| 141 | 155 | methods: { |
| 142 | 156 | /** |
| 157 | + * 初始时间值 | |
| 158 | + */ | |
| 159 | + getTodayRange() { | |
| 160 | + const startOfToday = new Date() | |
| 161 | + startOfToday.setHours(0, 0, 0, 0) // 设置时间为今天0点 | |
| 162 | + const endOfToday = new Date() | |
| 163 | + endOfToday.setHours(23, 59, 59, 999) // 设置时间为今天23点59分59秒999毫秒 | |
| 164 | + return [startOfToday, endOfToday] | |
| 165 | + }, | |
| 166 | + /** | |
| 143 | 167 | * 树点击事件 |
| 144 | 168 | */ |
| 145 | 169 | nodeClick(data, node) { |
| 146 | - if (data.children === undefined && data) { | |
| 170 | + if (data) { | |
| 147 | 171 | let split = data.id.split("_"); |
| 172 | + this.deviceNode = node | |
| 173 | + this.nodeChannelData = {}; | |
| 174 | + let nodeChannelDataList = []; | |
| 148 | 175 | if (split.length === 3) { |
| 149 | 176 | this.sim_channel = split[1] + '_' + split[2] |
| 150 | - this.sim_channel_data = data | |
| 151 | - console.log(data) | |
| 152 | - } else { | |
| 177 | + this.channelData = data | |
| 178 | + this.deviceTitle = `车辆:${data.pid} 通道:${data.name}` | |
| 179 | + let children = node.parent.data.children; | |
| 180 | + for (let i in children){ | |
| 181 | + const nodeChannelData = children[i]; | |
| 182 | + let ids = nodeChannelData.id.split("_"); | |
| 183 | + if (ids.length === 3){ | |
| 184 | + nodeChannelData.deviceId = ids[0]; | |
| 185 | + nodeChannelData.channelId = ids[2]; | |
| 186 | + nodeChannelDataList.push(nodeChannelData) | |
| 187 | + } | |
| 188 | + } | |
| 189 | + this.nodeChannelData = nodeChannelDataList.reduce((map, item) => { | |
| 190 | + // 以id为键,当前项为值 | |
| 191 | + map[item.channelId] = item; | |
| 192 | + return map; | |
| 193 | + }, {}); | |
| 194 | + } | |
| 195 | + // else if (data.children && data.children.length > 0 && data.abnormalStatus){ | |
| 196 | + // this.sim_channel = data.sim + '_0' | |
| 197 | + // this.deviceData = data | |
| 198 | + // this.deviceTitle = `车辆:${data.name} 全部通道` | |
| 199 | + // let children = node.data.children; | |
| 200 | + // for (let i in children){ | |
| 201 | + // const nodeChannelData = children[i]; | |
| 202 | + // let ids = nodeChannelData.id.split("_"); | |
| 203 | + // if (ids.length === 3){ | |
| 204 | + // nodeChannelData.deviceId = ids[0]; | |
| 205 | + // nodeChannelData.channelId = ids[2]; | |
| 206 | + // nodeChannelDataList.push(nodeChannelData) | |
| 207 | + // } | |
| 208 | + // } | |
| 209 | + // this.nodeChannelData = nodeChannelDataList.reduce((map, item) => { | |
| 210 | + // // 以id为键,当前项为值 | |
| 211 | + // map[item.channelId] = item; | |
| 212 | + // return map; | |
| 213 | + // }, {}); | |
| 214 | + // } | |
| 215 | + else { | |
| 153 | 216 | console.log("node click ==> ", data) |
| 154 | 217 | } |
| 155 | 218 | } |
| 156 | 219 | }, |
| 220 | + handleQuery(queryParams) { | |
| 221 | + let pageNum = this.queryParams.pageNum; | |
| 222 | + let pageSize = this.queryParams.pageSize; | |
| 223 | + console.log("handleQuery ==> ", queryParams) | |
| 224 | + this.queryParams = queryParams | |
| 225 | + this.queryParams.pageNum = pageNum | |
| 226 | + this.queryParams.pageSize = pageSize | |
| 227 | + this.searchHistoryList() | |
| 228 | + }, | |
| 157 | 229 | /** |
| 158 | 230 | * 查询车辆信息 |
| 159 | 231 | */ |
| ... | ... | @@ -224,8 +296,36 @@ export default { |
| 224 | 296 | * 添加通道 |
| 225 | 297 | */ |
| 226 | 298 | addChannels(data) { |
| 227 | - console.log(data) | |
| 228 | - if (data.sim2){ | |
| 299 | + if (this.deviceList && data.sim2 && this.deviceList.includes(data.name)){ | |
| 300 | + let nvr_labels = ['中门','','车前','驾驶舱','','前车厢','','360']; | |
| 301 | + let rm_labels = []; | |
| 302 | + let children = []; | |
| 303 | + for (let i in nvr_labels) { | |
| 304 | + if (nvr_labels[i] === ''){ | |
| 305 | + continue | |
| 306 | + } | |
| 307 | + children.push({ | |
| 308 | + id: `${data.id}_${data.sim}_${Number(i) + Number(1)}`, | |
| 309 | + pid: data.id, | |
| 310 | + name: nvr_labels[i], | |
| 311 | + disabled: data.disabled, | |
| 312 | + parent: data | |
| 313 | + }) | |
| 314 | + } | |
| 315 | + for (let i in rm_labels) { | |
| 316 | + if (rm_labels[i] === ''){ | |
| 317 | + continue | |
| 318 | + } | |
| 319 | + children.push({ | |
| 320 | + id: `${data.id}_${data.sim2}_${Number(i) + Number(1)}`, | |
| 321 | + pid: data.id, | |
| 322 | + name: rm_labels[i], | |
| 323 | + disabled: data.disabled, | |
| 324 | + parent: data | |
| 325 | + }) | |
| 326 | + } | |
| 327 | + data.children = children; | |
| 328 | + }else if (this.deviceList && data.sim2 && !this.deviceList.includes(data.name)){ | |
| 229 | 329 | let nvr_labels = ['中门','','车前','驾驶舱','前门','前车厢','后车厢','360']; |
| 230 | 330 | //'ADAS','DSM','前门客流','中门客流','360前','360后','360左','360右' |
| 231 | 331 | let rm_labels = []; |
| ... | ... | @@ -304,19 +404,29 @@ export default { |
| 304 | 404 | * 点击播放视频 |
| 305 | 405 | */ |
| 306 | 406 | clickHistoricalPlay(data) { |
| 407 | + console.log("点击播放视频 ===》 ",data) | |
| 307 | 408 | this.playHistoryItem(data) |
| 308 | 409 | }, |
| 410 | + openDialog(){ | |
| 411 | + this.$refs.historyPlayDialog.updateOpen(true) | |
| 412 | + this.$refs.historyPlayDialog.data ={ | |
| 413 | + videoUrl: "", | |
| 414 | + startTime: "", | |
| 415 | + endTime: "", | |
| 416 | + deviceId: "", | |
| 417 | + channelName: "", | |
| 418 | + channel: "" | |
| 419 | + } | |
| 420 | + }, | |
| 309 | 421 | /** |
| 310 | 422 | * 上传历史视频 |
| 311 | 423 | */ |
| 312 | 424 | uploadHistoryVideo(data){ |
| 313 | - console.log("开始上传视频 ===》 ",data) | |
| 314 | 425 | this.loading = true |
| 315 | 426 | this.$axios({ |
| 316 | 427 | method: 'get', |
| 317 | 428 | url: '/api/jt1078/query/history/uploading/' + data.name |
| 318 | 429 | }).then(res => { |
| 319 | - console.log("上传视频 ==》 ",res) | |
| 320 | 430 | this.$message.success("视频开始上传,请等待") |
| 321 | 431 | this.searchHistoryList() |
| 322 | 432 | this.loading = false |
| ... | ... | @@ -333,15 +443,12 @@ export default { |
| 333 | 443 | * 搜索历史视频 |
| 334 | 444 | */ |
| 335 | 445 | searchHistoryList() { |
| 336 | - this.getDateTime(); | |
| 337 | 446 | let simChannel = this.sim_channel; |
| 338 | - console.log(this.sim_channel) | |
| 339 | 447 | if (this.isEmpty(simChannel)) { |
| 340 | 448 | this.$message.error('请选择车辆'); |
| 341 | 449 | return; |
| 342 | 450 | } |
| 343 | 451 | let split = simChannel.split('_'); |
| 344 | - console.log("simChannel:", simChannel) | |
| 345 | 452 | let sim = split[0]; |
| 346 | 453 | if (this.isEmpty(sim)) { |
| 347 | 454 | this.$message.error('无法获取SIM卡信息,请检查设备'); |
| ... | ... | @@ -352,22 +459,25 @@ export default { |
| 352 | 459 | this.$message.error('请选择通道'); |
| 353 | 460 | return; |
| 354 | 461 | } |
| 355 | - console.log(channel); | |
| 356 | - if (this.isEmpty(this.startTime) || this.isEmpty(this.endTime)) { | |
| 462 | + if (!this.queryParams.time) { | |
| 357 | 463 | this.$message.error('请选择开始和结束时间'); |
| 358 | 464 | return; |
| 359 | 465 | } |
| 466 | + this.loading = true; | |
| 360 | 467 | this.$axios({ |
| 361 | 468 | method: 'get', |
| 362 | - url: '/api/jt1078/query/history/list/' + sim + '/' + channel + "/" + this.startTime + "/" + this.endTime | |
| 469 | + url: '/api/jt1078/query/history/list/' + sim + '/' + channel + "/" + parseTime(this.queryParams.time[0], '{y}-{m}-{d} {h}:{i}:{s}') + "/" + parseTime(this.queryParams.time[1], '{y}-{m}-{d} {h}:{i}:{s}') | |
| 363 | 470 | }).then(res => { |
| 364 | 471 | let items = res.data.data.obj.data.items; |
| 365 | 472 | if (res && res.data && res.data.data && res.data.data.obj && res.data.data.code == 1 && res.data.data.obj.data && items) { |
| 366 | 473 | for (let i in items) { |
| 367 | 474 | items[i].disabled = false; |
| 368 | 475 | items[i].countdown = 10; |
| 476 | + items[i].channelName = this.nodeChannelData[items[i].channel] ? this.nodeChannelData[items[i].channel].name : `通道${Number(items[i].channel)}` | |
| 477 | + items[i].deviceId = this.deviceData? this.deviceData.name : this.channelData.pid | |
| 369 | 478 | } |
| 370 | 479 | this.historyData = items; |
| 480 | + console.log("历史列表 ===》 ",items) | |
| 371 | 481 | } else if (res && res.data && res.data.data && res.data.data.msg) { |
| 372 | 482 | this.$message.error(res.data.data.msg); |
| 373 | 483 | } else if (items === undefined) { |
| ... | ... | @@ -377,7 +487,7 @@ export default { |
| 377 | 487 | clearInterval(this.historyTimer) |
| 378 | 488 | } |
| 379 | 489 | } |
| 380 | - this.loading = false | |
| 490 | + this.loading = false | |
| 381 | 491 | }).catch(error => { |
| 382 | 492 | console.log(error) |
| 383 | 493 | this.loading = false |
| ... | ... | @@ -386,13 +496,17 @@ export default { |
| 386 | 496 | }) |
| 387 | 497 | }, |
| 388 | 498 | /** |
| 499 | + * 获取设备通道 | |
| 500 | + */ | |
| 501 | + getDeviceChannelMap() { | |
| 502 | + | |
| 503 | + }, | |
| 504 | + /** | |
| 389 | 505 | * 时间转换 |
| 390 | 506 | */ |
| 391 | 507 | getDateTime() { |
| 392 | 508 | let date = this.date; |
| 393 | 509 | let timeList = this.timeList; |
| 394 | - console.log("date ", date) | |
| 395 | - console.log("timeList ", timeList) | |
| 396 | 510 | if (this.isEmpty(date)) { |
| 397 | 511 | this.$message.error("请选择日期") |
| 398 | 512 | return false; |
| ... | ... | @@ -414,8 +528,6 @@ export default { |
| 414 | 528 | endTime.setDate(day); |
| 415 | 529 | startTime = parseTime(startTime, '{y}-{m}-{d} {h}:{i}:{s}'); |
| 416 | 530 | endTime = parseTime(endTime, '{y}-{m}-{d} {h}:{i}:{s}'); |
| 417 | - console.log("startTime:" + startTime) | |
| 418 | - console.log("endTime:" + endTime) | |
| 419 | 531 | this.startTime = startTime; |
| 420 | 532 | this.endTime = endTime; |
| 421 | 533 | return true |
| ... | ... | @@ -425,6 +537,7 @@ export default { |
| 425 | 537 | */ |
| 426 | 538 | playHistoryItem(e) { |
| 427 | 539 | this.videoUrl = []; |
| 540 | + this.loading = true | |
| 428 | 541 | this.$axios({ |
| 429 | 542 | method: 'get', |
| 430 | 543 | url: '/api/jt1078/query/send/request/io/history/' + e.sim + '/' + e.channel + "/" + e.startTime + "/" + e.endTime + "/" + e.channelMapping |
| ... | ... | @@ -441,15 +554,22 @@ export default { |
| 441 | 554 | this.httpPort = res.data.data.httpPort; |
| 442 | 555 | this.stream = res.data.data.stream; |
| 443 | 556 | this.videoUrlHistory = videoUrl1; |
| 444 | - | |
| 445 | 557 | let itemData = new Object(); |
| 446 | 558 | itemData.deviceId = this.sim; |
| 447 | 559 | itemData.channelId = this.channel; |
| 448 | 560 | itemData.playUrl = videoUrl1; |
| 449 | - console.log(this.playerIdx); | |
| 450 | 561 | this.setPlayUrl(videoUrl1, 0); |
| 451 | 562 | this.hisotoryPlayFlag = true; |
| 452 | - | |
| 563 | + this.$refs.historyPlayDialog.updateOpen(true) | |
| 564 | + this.$refs.historyPlayDialog.data ={ | |
| 565 | + videoUrl: this.videoUrlHistory, | |
| 566 | + startTime: e.startTime, | |
| 567 | + endTime: e.endTime, | |
| 568 | + deviceId: e.deviceId, | |
| 569 | + channelName: e.channelName, | |
| 570 | + channel: e.channel, | |
| 571 | + sim: e.sim | |
| 572 | + } | |
| 453 | 573 | } else if (res.data.data && res.data.data.msg) { |
| 454 | 574 | this.$message.error(res.data.data.msg); |
| 455 | 575 | } else if (res.data.msg) { |
| ... | ... | @@ -457,7 +577,7 @@ export default { |
| 457 | 577 | } else if (res.msg) { |
| 458 | 578 | this.$message.error(res.msg); |
| 459 | 579 | } |
| 460 | - this.closeLoading(); | |
| 580 | + this.loading = false | |
| 461 | 581 | }) |
| 462 | 582 | }, |
| 463 | 583 | /** |
| ... | ... | @@ -474,8 +594,6 @@ export default { |
| 474 | 594 | }, |
| 475 | 595 | |
| 476 | 596 | shot(e) { |
| 477 | - // console.log(e) | |
| 478 | - // send({code:'image',data:e}) | |
| 479 | 597 | var base64ToBlob = function (code) { |
| 480 | 598 | let parts = code.split(';base64,'); |
| 481 | 599 | let contentType = parts[0].split(':')[1]; |
| ... | ... | @@ -498,14 +616,12 @@ export default { |
| 498 | 616 | aLink.click(); |
| 499 | 617 | }, |
| 500 | 618 | destroy(idx) { |
| 501 | - console.log(idx); | |
| 502 | 619 | this.clear(idx.substring(idx.length - 1)) |
| 503 | 620 | }, |
| 504 | 621 | |
| 505 | 622 | createdPlay() { |
| 506 | 623 | if (flvjs.isSupported()) { |
| 507 | 624 | // var videoDom = document.getElementById('myVideo') |
| 508 | - console.log(this.videoUrlHistory); | |
| 509 | 625 | let videoDom = this.$refs.myVideo |
| 510 | 626 | // 创建一个播放器实例 |
| 511 | 627 | var player = flvjs.createPlayer({ |
| ... | ... | @@ -637,6 +753,74 @@ li#menu-item-delete, li#menu-item-rename { |
| 637 | 753 | } |
| 638 | 754 | </style> |
| 639 | 755 | <style> |
| 756 | +.layout-header { | |
| 757 | + background-color: #f5f7fa; | |
| 758 | + padding: 15px; | |
| 759 | + text-align: center; | |
| 760 | + font-weight: bold; | |
| 761 | +} | |
| 762 | + | |
| 763 | +.layout-main { | |
| 764 | + flex: 1; | |
| 765 | + padding: 0; | |
| 766 | + margin: 0; | |
| 767 | +} | |
| 768 | + | |
| 769 | +.splitpanes-container { | |
| 770 | + height: 100%; | |
| 771 | + display: flex; | |
| 772 | +} | |
| 773 | + | |
| 774 | +.aside-pane { | |
| 775 | + background-color: #d3dce6; | |
| 776 | + overflow: auto; | |
| 777 | + padding: 0px; | |
| 778 | +} | |
| 779 | + | |
| 780 | +.search-input { | |
| 781 | + margin-bottom: 10px; | |
| 782 | +} | |
| 783 | + | |
| 784 | +.aside-list { | |
| 785 | + padding-left: 0; | |
| 786 | +} | |
| 787 | + | |
| 788 | +.main-pane { | |
| 789 | + background-color: #ffffff; | |
| 790 | + overflow: auto; | |
| 791 | + padding: 20px; | |
| 792 | +} | |
| 793 | + | |
| 794 | +.content-main { | |
| 795 | + background-color: #f9f9f9; | |
| 796 | + padding: 10px; | |
| 797 | + border-radius: 4px; | |
| 798 | + width: 100%; | |
| 799 | +} | |
| 800 | + | |
| 801 | +.layout-footer { | |
| 802 | + background-color: #f5f7fa; | |
| 803 | + text-align: center; | |
| 804 | + font-size: 12px; | |
| 805 | + color: #666; | |
| 806 | +} | |
| 807 | + | |
| 808 | +/* Splitpane 拖拽条样式 */ | |
| 809 | +/* .splitpanes__splitter { | |
| 810 | + width: 5px; | |
| 811 | + background-color: #ccc; | |
| 812 | + cursor: col-resize; | |
| 813 | +} | |
| 814 | +.splitpanes__splitter:hover { | |
| 815 | + background-color: #888; | |
| 816 | +} */ | |
| 817 | + | |
| 818 | +.splitpanes__pane { | |
| 819 | + display: flex; | |
| 820 | + justify-content: center; | |
| 821 | + font-family: Helvetica, Arial, sans-serif; | |
| 822 | + color: rgba(255, 255, 255, 0.6); | |
| 823 | +} | |
| 640 | 824 | .videoList { |
| 641 | 825 | display: flex; |
| 642 | 826 | flex-wrap: wrap; | ... | ... |
web_src/src/components/JT1078Components/HistoryPlayDialog.vue
0 → 100644
| 1 | +<template> | |
| 2 | + <el-dialog | |
| 3 | + :close-on-click-modal="true" | |
| 4 | + :title="`${data.deviceId} 设备 - ${data.channelName} 历史视频回放`" | |
| 5 | + :visible.sync="open" | |
| 6 | + width="90%" | |
| 7 | + center | |
| 8 | + :before-close="handleClose" | |
| 9 | + class="history-dialog-center"> | |
| 10 | + <el-container> | |
| 11 | + <el-main> | |
| 12 | + <el-card class="box-card" shadow="always" :body-style="{ height: '95%' }"> | |
| 13 | + <div class='main-play'> | |
| 14 | + <video-player :class="`video`" ref="player" | |
| 15 | + :initial-play-url="videoUrl" style="width: 100%;height: 100%;" | |
| 16 | + @getTime="getTime" | |
| 17 | + ></video-player> | |
| 18 | + </div> | |
| 19 | + </el-card> | |
| 20 | + </el-main> | |
| 21 | + </el-container> | |
| 22 | + <el-footer> | |
| 23 | + <div> | |
| 24 | + <TimeLine | |
| 25 | + ref="time_line" | |
| 26 | + @change="changeDate" | |
| 27 | + :width="width" | |
| 28 | + :mark-time="markTime" | |
| 29 | + :time-range="time_range" | |
| 30 | + :isAutoPlay="isAutoPlay" | |
| 31 | + :startMeddleTime="startMeddleTime" | |
| 32 | + @click="clickCanvas" | |
| 33 | + /> | |
| 34 | + </div> | |
| 35 | + </el-footer> | |
| 36 | + <span slot="footer" class="dialog-footer"> | |
| 37 | + <el-button @click="handleClose">取 消</el-button> | |
| 38 | + </span> | |
| 39 | + </el-dialog> | |
| 40 | +</template> | |
| 41 | + | |
| 42 | +<script> | |
| 43 | +import videoPlayer from "../common/JessVideoPlayer.vue"; | |
| 44 | +import TimeLine from './TimeLineCanvas.vue' | |
| 45 | +import dayjs from 'dayjs' | |
| 46 | +import {formattedTime} from "../../../utils/dateFormate"; | |
| 47 | +//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等), | |
| 48 | +//例如:import 《组件名称》 from '《组件路径》, | |
| 49 | +export default { | |
| 50 | + name: "HistoryPlayDialog", | |
| 51 | + //import引入的组件需要注入到对象中才能使用" | |
| 52 | + components: {TimeLine, videoPlayer}, | |
| 53 | + props: { | |
| 54 | + }, | |
| 55 | + data() { | |
| 56 | + //这里存放数据" | |
| 57 | + return { | |
| 58 | + data:{}, | |
| 59 | + videoUrl: null, | |
| 60 | + isAutoPlay: false, | |
| 61 | + width: "100%", | |
| 62 | + startMeddleTime: null, | |
| 63 | + startTime: null, | |
| 64 | + endTime: null, | |
| 65 | + time_range: [], | |
| 66 | + markTime: [], | |
| 67 | + form: { | |
| 68 | + code: '', | |
| 69 | + startTime: '', | |
| 70 | + endTime: '', | |
| 71 | + }, | |
| 72 | + channelList: [], | |
| 73 | + pickerOptions: {}, | |
| 74 | + open: false, | |
| 75 | + }; | |
| 76 | + }, | |
| 77 | + //计算属性 类似于data概念", | |
| 78 | + computed: {}, | |
| 79 | + //监控data中的数据变化", | |
| 80 | + watch: { | |
| 81 | + data(val) { | |
| 82 | + console.log('播放数据', val) | |
| 83 | + this.videoUrl = val.videoUrl | |
| 84 | + this.startMeddleTime = val.startTime | |
| 85 | + this.startTime = val.startTime | |
| 86 | + this.endTime = val.endTime | |
| 87 | + this.time_range = [val.startTime, val.endTime] | |
| 88 | + this.markTime = [ | |
| 89 | + { | |
| 90 | + beginTime: val.startTime, | |
| 91 | + endTime: val.endTime, | |
| 92 | + bgColor: "green", | |
| 93 | + text: "有视频", | |
| 94 | + }, | |
| 95 | + ] | |
| 96 | + this.form.startTime = this.startTime | |
| 97 | + this.form.endTime = this.endTime | |
| 98 | + }, | |
| 99 | + }, | |
| 100 | + //方法集合", | |
| 101 | + methods: { | |
| 102 | + getTime(time) { | |
| 103 | + // console.log('当前视频帧',time) | |
| 104 | + }, | |
| 105 | + updateOpen(flag){ | |
| 106 | + this.open = flag | |
| 107 | + }, | |
| 108 | + clickCanvas(date) { | |
| 109 | + this.$axios({ | |
| 110 | + method: 'get', | |
| 111 | + url: '/api/jt1078/query/send/request/io/history/' + this.data.sim + '/' + this.data.channel + "/" + this.data.startTime + "/" + date + "/" + undefined | |
| 112 | + }).then(res => { | |
| 113 | + if (res.data && res.data.data && res.data.data.data) { | |
| 114 | + let videoUrl1; | |
| 115 | + if (location.protocol === "https:") { | |
| 116 | + videoUrl1 = res.data.data.data.wss_flv; | |
| 117 | + } else { | |
| 118 | + videoUrl1 = res.data.data.data.ws_flv; | |
| 119 | + } | |
| 120 | + this.videoUrl = videoUrl1; | |
| 121 | + } else if (res.data.data && res.data.data.msg) { | |
| 122 | + this.$message.error(res.data.data.msg); | |
| 123 | + } else if (res.data.msg) { | |
| 124 | + this.$message.error(res.data.msg); | |
| 125 | + } else if (res.msg) { | |
| 126 | + this.$message.error(res.msg); | |
| 127 | + } | |
| 128 | + }) | |
| 129 | + }, | |
| 130 | + changeDate(date, status) { | |
| 131 | + console.log("选择时间:" + date + " 播放状态:" + status); | |
| 132 | + }, | |
| 133 | + handleClose(){ | |
| 134 | + this.open = false | |
| 135 | + } | |
| 136 | + }, | |
| 137 | + //生命周期 - 创建完成(可以访问当前this实例)", | |
| 138 | + created() { | |
| 139 | + }, | |
| 140 | + //生命周期 - 挂载完成(可以访问DOM元素)", | |
| 141 | + mounted() { | |
| 142 | + }, | |
| 143 | + beforeCreate() { | |
| 144 | + }, //生命周期 - 创建之前", | |
| 145 | + beforeMount() { | |
| 146 | + }, //生命周期 - 挂载之前", | |
| 147 | + beforeUpdate() { | |
| 148 | + }, //生命周期 - 更新之前", | |
| 149 | + updated() { | |
| 150 | + }, //生命周期 - 更新之后", | |
| 151 | + beforeDestroy() { | |
| 152 | + }, //生命周期 - 销毁之前", | |
| 153 | + destroyed() { | |
| 154 | + }, //生命周期 - 销毁完成", | |
| 155 | + activated() { | |
| 156 | + } //如果页面有keep-alive缓存功能,这个函数会触发", | |
| 157 | +}; | |
| 158 | +</script> | |
| 159 | +<style scoped> | |
| 160 | +.el-header { | |
| 161 | + background-color: #B3C0D1; | |
| 162 | + color: #333; | |
| 163 | + text-align: center; | |
| 164 | + line-height: 60px; | |
| 165 | +} | |
| 166 | + | |
| 167 | +.el-footer { | |
| 168 | + background-color: #B3C0D1; | |
| 169 | + color: #333; | |
| 170 | + text-align: center; | |
| 171 | + line-height: 60px; | |
| 172 | +} | |
| 173 | + | |
| 174 | +.el-aside { | |
| 175 | + background-color: #D3DCE6; | |
| 176 | + color: #333; | |
| 177 | + text-align: center; | |
| 178 | + line-height: 200px; | |
| 179 | + height: 75vh; | |
| 180 | +} | |
| 181 | + | |
| 182 | +.el-main { | |
| 183 | + display: flex; | |
| 184 | + justify-content: center; /* 水平居中 */ | |
| 185 | + align-items: center; /* 垂直居中 */ | |
| 186 | + background-color: #E9EEF3; | |
| 187 | + color: #333; | |
| 188 | + text-align: center; | |
| 189 | + line-height: 160px; | |
| 190 | + height: 80vh; | |
| 191 | +} | |
| 192 | + | |
| 193 | +body > .el-container { | |
| 194 | + margin-bottom: 40px; | |
| 195 | +} | |
| 196 | + | |
| 197 | +.el-container:nth-child(5) .el-aside, | |
| 198 | +.el-container:nth-child(6) .el-aside { | |
| 199 | + line-height: 260px; | |
| 200 | +} | |
| 201 | + | |
| 202 | +.main-play { | |
| 203 | + width: 100%; | |
| 204 | + height: 100%; | |
| 205 | + background-color: black; | |
| 206 | +} | |
| 207 | + | |
| 208 | +.box-card { | |
| 209 | + width: 80%; | |
| 210 | + height: 100%; | |
| 211 | +} | |
| 212 | +/* 在现有样式基础上添加 */ | |
| 213 | +.history-dialog-center { | |
| 214 | + display: flex; | |
| 215 | + justify-content: center; | |
| 216 | + align-items: center; | |
| 217 | + position: fixed; | |
| 218 | + top: 0; | |
| 219 | + left: 0; | |
| 220 | + width: 100%; | |
| 221 | + height: 100%; | |
| 222 | + margin: 0 !important; | |
| 223 | +} | |
| 224 | + | |
| 225 | +.history-dialog-center .el-dialog { | |
| 226 | + margin: 0 auto !important; | |
| 227 | + max-height: 90vh; | |
| 228 | + display: flex; | |
| 229 | + flex-direction: column; | |
| 230 | +} | |
| 231 | + | |
| 232 | +.history-dialog-center .el-dialog__body { | |
| 233 | + flex: 1; | |
| 234 | + overflow-y: auto; | |
| 235 | +} | |
| 236 | + | |
| 237 | +</style> | ... | ... |
web_src/src/components/JT1078Components/HistoryRecordFrom.vue
0 → 100644
| 1 | +<template> | |
| 2 | + <div> | |
| 3 | + <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px"> | |
| 4 | + <el-form-item> | |
| 5 | + <el-date-picker | |
| 6 | + v-model="queryParams.time" | |
| 7 | + type="datetimerange" | |
| 8 | + :picker-options="pickerOptions" | |
| 9 | + range-separator="至" | |
| 10 | + start-placeholder="开始日期" | |
| 11 | + end-placeholder="结束日期" | |
| 12 | + align="right" | |
| 13 | + > | |
| 14 | + </el-date-picker> | |
| 15 | + </el-form-item> | |
| 16 | + <el-form-item> | |
| 17 | + <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button> | |
| 18 | + <el-button icon="el-icon-refresh-right" size="mini" @click="resetQuery">重置</el-button> | |
| 19 | + </el-form-item> | |
| 20 | + </el-form> | |
| 21 | + </div> | |
| 22 | +</template> | |
| 23 | + | |
| 24 | +<script> | |
| 25 | +//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等), | |
| 26 | +//例如:import 《组件名称》 from '《组件路径》, | |
| 27 | +export default { | |
| 28 | + name: "HistoricalRecordForm", | |
| 29 | + //import引入的组件需要注入到对象中才能使用" | |
| 30 | + components: {}, | |
| 31 | + props: { | |
| 32 | + queryParams: { | |
| 33 | + type: Object, | |
| 34 | + default: {} | |
| 35 | + } | |
| 36 | + }, | |
| 37 | + data() { | |
| 38 | + //这里存放数据" | |
| 39 | + return { | |
| 40 | + // 显示搜索条件 | |
| 41 | + showSearch: true, | |
| 42 | + pickerOptions: { | |
| 43 | + shortcuts: [{ | |
| 44 | + text: '最近一周', | |
| 45 | + onClick(picker) { | |
| 46 | + const end = new Date() | |
| 47 | + const start = new Date() | |
| 48 | + start.setTime(start.getTime() - 3600 * 1000 * 24 * 7) | |
| 49 | + picker.$emit('pick', [start, end]) | |
| 50 | + } | |
| 51 | + }, { | |
| 52 | + text: '最近一个月', | |
| 53 | + onClick(picker) { | |
| 54 | + const end = new Date() | |
| 55 | + const start = new Date() | |
| 56 | + start.setTime(start.getTime() - 3600 * 1000 * 24 * 30) | |
| 57 | + picker.$emit('pick', [start, end]) | |
| 58 | + } | |
| 59 | + }, { | |
| 60 | + text: '最近三个月', | |
| 61 | + onClick(picker) { | |
| 62 | + const end = new Date() | |
| 63 | + const start = new Date() | |
| 64 | + start.setTime(start.getTime() - 3600 * 1000 * 24 * 90) | |
| 65 | + picker.$emit('pick', [start, end]) | |
| 66 | + } | |
| 67 | + }] | |
| 68 | + } | |
| 69 | + } | |
| 70 | + }, | |
| 71 | + //计算属性 类似于data概念", | |
| 72 | + computed: {}, | |
| 73 | + //监控data中的数据变化", | |
| 74 | + watch: {}, | |
| 75 | + //方法集合", | |
| 76 | + methods: { | |
| 77 | + handleQuery() { | |
| 78 | + this.$emit('handleQuery', this.queryParams) | |
| 79 | + }, | |
| 80 | + resetQuery() { | |
| 81 | + this.queryParams.time = this.getYesterdayRange() | |
| 82 | + }, | |
| 83 | + refreshQuery() { | |
| 84 | + this.$emit('refreshQuery', this.queryParams) | |
| 85 | + }, | |
| 86 | + getYesterdayRange() { | |
| 87 | + const today = new Date(); // 获取当前日期 | |
| 88 | + const yesterday = new Date(today); // 复制当前日期 | |
| 89 | + yesterday.setDate(today.getDate() - 1); // 设置为昨天 | |
| 90 | + | |
| 91 | + // 昨天的开始时间(00:00:00.000) | |
| 92 | + const startOfYesterday = new Date(yesterday); | |
| 93 | + startOfYesterday.setHours(0, 0, 0, 0); | |
| 94 | + | |
| 95 | + // 昨天的结束时间(23:59:59.999) | |
| 96 | + const endOfYesterday = new Date(yesterday); | |
| 97 | + endOfYesterday.setHours(23, 59, 59, 999); | |
| 98 | + | |
| 99 | + return [startOfYesterday, endOfYesterday]; | |
| 100 | + } | |
| 101 | + }, | |
| 102 | + //生命周期 - 创建完成(可以访问当前this实例)", | |
| 103 | + created() { | |
| 104 | + this.resetQuery() | |
| 105 | + }, | |
| 106 | + //生命周期 - 挂载完成(可以访问DOM元素)", | |
| 107 | + mounted() { | |
| 108 | + }, | |
| 109 | + beforeCreate() { | |
| 110 | + }, //生命周期 - 创建之前", | |
| 111 | + beforeMount() { | |
| 112 | + }, //生命周期 - 挂载之前", | |
| 113 | + beforeUpdate() { | |
| 114 | + }, //生命周期 - 更新之前", | |
| 115 | + updated() { | |
| 116 | + }, //生命周期 - 更新之后", | |
| 117 | + beforeDestroy() { | |
| 118 | + }, //生命周期 - 销毁之前", | |
| 119 | + destroyed() { | |
| 120 | + }, //生命周期 - 销毁完成", | |
| 121 | + activated() { | |
| 122 | + } //如果页面有keep-alive缓存功能,这个函数会触发", | |
| 123 | +} | |
| 124 | +</script> | |
| 125 | +<style scoped> | |
| 126 | + | |
| 127 | +</style> | ... | ... |
web_src/src/components/JT1078Components/HistorySearchTable.vue
| ... | ... | @@ -4,11 +4,11 @@ |
| 4 | 4 | v-if="tableData.length > 0" |
| 5 | 5 | ref="singleTable" |
| 6 | 6 | :data="tableData" |
| 7 | - :header-cell-style="{ textAlign: 'center' ,height:'20px',lineHeight:'20px' }" | |
| 7 | + :header-cell-style="{ textAlign: 'center' ,height:'100%',lineHeight:'100%' }" | |
| 8 | 8 | border |
| 9 | + height="calc(100% - 100px)" | |
| 9 | 10 | highlight-current-row |
| 10 | 11 | @current-change="handleCurrentChange" |
| 11 | - height="250" | |
| 12 | 12 | style="width: 100%;text-align: center"> |
| 13 | 13 | <el-table-column |
| 14 | 14 | type="index" |
| ... | ... | @@ -23,6 +23,16 @@ |
| 23 | 23 | label="名称"> |
| 24 | 24 | </el-table-column> |
| 25 | 25 | <el-table-column |
| 26 | + property="deviceId" | |
| 27 | + align="center" | |
| 28 | + label="设备"> | |
| 29 | + </el-table-column> | |
| 30 | + <el-table-column | |
| 31 | + property="channelName" | |
| 32 | + align="center" | |
| 33 | + label="通道名称"> | |
| 34 | + </el-table-column> | |
| 35 | + <el-table-column | |
| 26 | 36 | align="center" |
| 27 | 37 | label="日期"> |
| 28 | 38 | <template slot-scope="scope"> |
| ... | ... | @@ -65,6 +75,7 @@ |
| 65 | 75 | //例如:import 《组件名称》 from '《组件路径》, |
| 66 | 76 | export default { |
| 67 | 77 | //import引入的组件需要注入到对象中才能使用" |
| 78 | + name: "historySearchTable", | |
| 68 | 79 | components: {}, |
| 69 | 80 | props: { |
| 70 | 81 | tableData: { | ... | ... |