Commit 66208290d4090ec3867c04008413f2d80251f8d1

Authored by 王鑫
1 parent b39a4d77

fix():修改窗口支持25和36屏幕,修改播放器延时问题

Too many changes to show.

To preserve performance only 26 of 47 files are displayed.

@@ -95,6 +95,12 @@ @@ -95,6 +95,12 @@
95 </profiles> 95 </profiles>
96 96
97 <dependencies> 97 <dependencies>
  98 + <!-- httpclient -->
  99 + <dependency>
  100 + <groupId>commons-httpclient</groupId>
  101 + <artifactId>commons-httpclient</artifactId>
  102 + <version>3.1</version>
  103 + </dependency>
98 <!-- https://mvnrepository.com/artifact/commons-net/commons-net --> 104 <!-- https://mvnrepository.com/artifact/commons-net/commons-net -->
99 <dependency> 105 <dependency>
100 <groupId>commons-net</groupId> 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,6 +84,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
84 matchers.add("/api/device/query/snap/**"); 84 matchers.add("/api/device/query/snap/**");
85 matchers.add("/record_proxy/*/**"); 85 matchers.add("/record_proxy/*/**");
86 matchers.add("/api/emit"); 86 matchers.add("/api/emit");
  87 + matchers.add("/api/user/getInfo");
87 matchers.add("/favicon.ico"); 88 matchers.add("/favicon.ico");
88 matchers.add("/api/jt1078/query/test1"); 89 matchers.add("/api/jt1078/query/test1");
89 matchers.add("/api/jt1078/query/test"); 90 matchers.add("/api/jt1078/query/test");
@@ -123,7 +124,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @@ -123,7 +124,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
123 .authorizeRequests() 124 .authorizeRequests()
124 .requestMatchers(CorsUtils::isPreFlightRequest).permitAll() 125 .requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
125 .antMatchers(userSetting.getInterfaceAuthenticationExcludes().toArray(new String[0])).permitAll() 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 .anyRequest().authenticated() 128 .anyRequest().authenticated()
128 .and() 129 .and()
129 .addFilterBefore(new IpWhitelistFilter(), BasicAuthenticationFilter.class) 130 .addFilterBefore(new IpWhitelistFilter(), BasicAuthenticationFilter.class)
src/main/java/com/genersoft/iot/vmp/service/IUserService.java
@@ -26,4 +26,6 @@ public interface IUserService { @@ -26,4 +26,6 @@ public interface IUserService {
26 PageInfo<User> getUsers(int page, int count); 26 PageInfo<User> getUsers(int page, int count);
27 27
28 int changePushKey(int id, String pushKey); 28 int changePushKey(int id, String pushKey);
  29 +
  30 + User selectUserByUserName(String username);
29 } 31 }
src/main/java/com/genersoft/iot/vmp/service/impl/StremProxyService1078Impl.java
@@ -66,9 +66,6 @@ public class StremProxyService1078Impl implements StremProxyService1078 { @@ -66,9 +66,6 @@ public class StremProxyService1078Impl implements StremProxyService1078 {
66 if(Objects.nonNull(port)){ 66 if(Objects.nonNull(port)){
67 // VideoServerApp.stopServer(port,httpPort); 67 // VideoServerApp.stopServer(port,httpPort);
68 } 68 }
69 -  
70 -  
71 -  
72 if (Objects.isNull(entity)) { 69 if (Objects.isNull(entity)) {
73 log.info("HttpClientPostEntity is null"); 70 log.info("HttpClientPostEntity is null");
74 } else { 71 } else {
@@ -77,12 +74,11 @@ public class StremProxyService1078Impl implements StremProxyService1078 { @@ -77,12 +74,11 @@ public class StremProxyService1078Impl implements StremProxyService1078 {
77 74
78 redisTemplate.opsForValue().set("jt1078:count:"+stream,20000,300,TimeUnit.SECONDS); 75 redisTemplate.opsForValue().set("jt1078:count:"+stream,20000,300,TimeUnit.SECONDS);
79 76
80 -// streamProxyService.del("schedule", stream);  
81 resultMap.put("code", "1"); 77 resultMap.put("code", "1");
82 resultMap.put("message", "OK"); 78 resultMap.put("message", "OK");
83 79
84 return resultMap; 80 return resultMap;
85 - } catch (URISyntaxException | IOException e) { 81 + } catch (Exception e) {
86 log.error("发送停止推流指令异常;[{}],[{}]", url, msg, e); 82 log.error("发送停止推流指令异常;[{}],[{}]", url, msg, e);
87 83
88 resultMap.put("code", "-20"); 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,4 +95,10 @@ public class UserServiceImpl implements IUserService {
95 public int changePushKey(int id, String pushKey) { 95 public int changePushKey(int id, String pushKey) {
96 return userMapper.changePushKey(id,pushKey); 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,6 +9,7 @@ import com.alibaba.fastjson2.JSON;
9 import com.alibaba.fastjson2.JSONArray; 9 import com.alibaba.fastjson2.JSONArray;
10 import com.alibaba.fastjson2.JSONException; 10 import com.alibaba.fastjson2.JSONException;
11 import com.alibaba.fastjson2.JSONObject; 11 import com.alibaba.fastjson2.JSONObject;
  12 +import com.genersoft.iot.vmp.common.StreamInfo;
12 import com.genersoft.iot.vmp.conf.MediaConfig; 13 import com.genersoft.iot.vmp.conf.MediaConfig;
13 import com.genersoft.iot.vmp.conf.StreamProxyTask; 14 import com.genersoft.iot.vmp.conf.StreamProxyTask;
14 import com.genersoft.iot.vmp.conf.exception.ControllerException; 15 import com.genersoft.iot.vmp.conf.exception.ControllerException;
@@ -17,7 +18,9 @@ import com.genersoft.iot.vmp.conf.security.JwtUtils; @@ -17,7 +18,9 @@ import com.genersoft.iot.vmp.conf.security.JwtUtils;
17 import com.genersoft.iot.vmp.conf.security.dto.JwtUser; 18 import com.genersoft.iot.vmp.conf.security.dto.JwtUser;
18 import com.genersoft.iot.vmp.jtt1078.app.VideoServerApp; 19 import com.genersoft.iot.vmp.jtt1078.app.VideoServerApp;
19 import com.genersoft.iot.vmp.jtt1078.publisher.PublishManager; 20 import com.genersoft.iot.vmp.jtt1078.publisher.PublishManager;
  21 +import com.genersoft.iot.vmp.jtt1078.subscriber.RTMPPublisher;
20 import com.genersoft.iot.vmp.media.zlm.dto.StreamPushItem; 22 import com.genersoft.iot.vmp.media.zlm.dto.StreamPushItem;
  23 +import com.genersoft.iot.vmp.service.IMediaService;
21 import com.genersoft.iot.vmp.service.IStreamPushService; 24 import com.genersoft.iot.vmp.service.IStreamPushService;
22 import com.genersoft.iot.vmp.service.StremProxyService1078; 25 import com.genersoft.iot.vmp.service.StremProxyService1078;
23 import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; 26 import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
@@ -50,12 +53,14 @@ import org.slf4j.Logger; @@ -50,12 +53,14 @@ import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory; 53 import org.slf4j.LoggerFactory;
51 import org.springframework.beans.factory.annotation.Autowired; 54 import org.springframework.beans.factory.annotation.Autowired;
52 import org.springframework.beans.factory.annotation.Value; 55 import org.springframework.beans.factory.annotation.Value;
  56 +import org.springframework.context.annotation.Bean;
53 import org.springframework.core.io.InputStreamResource; 57 import org.springframework.core.io.InputStreamResource;
54 import org.springframework.data.redis.core.RedisTemplate; 58 import org.springframework.data.redis.core.RedisTemplate;
55 import org.springframework.http.HttpHeaders; 59 import org.springframework.http.HttpHeaders;
56 import org.springframework.http.MediaType; 60 import org.springframework.http.MediaType;
57 import org.springframework.http.ResponseEntity; 61 import org.springframework.http.ResponseEntity;
58 import org.springframework.scheduling.annotation.Scheduled; 62 import org.springframework.scheduling.annotation.Scheduled;
  63 +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
59 import org.springframework.util.Base64Utils; 64 import org.springframework.util.Base64Utils;
60 import org.springframework.web.bind.annotation.*; 65 import org.springframework.web.bind.annotation.*;
61 import sun.misc.Signal; 66 import sun.misc.Signal;
@@ -1146,13 +1151,13 @@ public class Jt1078OfCarController { @@ -1146,13 +1151,13 @@ public class Jt1078OfCarController {
1146 1151
1147 @Nullable 1152 @Nullable
1148 private StreamContent getStreamContent(String stream) { 1153 private StreamContent getStreamContent(String stream) {
1149 - StreamContent streamContent = this.getStreamContentPlayURL(stream); 1154 + StreamContent streamContent = getStreamContentPlayURL(stream);
1150 if (Objects.isNull(streamContent) || StringUtils.isEmpty(streamContent.getWs_flv())) { 1155 if (Objects.isNull(streamContent) || StringUtils.isEmpty(streamContent.getWs_flv())) {
1151 streamContent = new StreamContent(); 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 return streamContent; 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,11 +179,11 @@ public class TuohuaConfigBean {
179 List<CarData> carData = JSON.parseArray(json, CarData.class); 179 List<CarData> carData = JSON.parseArray(json, CarData.class);
180 int count = 1; 180 int count = 1;
181 if (CollectionUtils.isNotEmpty(carData)) { 181 if (CollectionUtils.isNotEmpty(carData)) {
182 - if (StringUtils.equals(profileActive, "wx-local")) { 182 + if (StringUtils.equals(profileActive, "local")) {
183 CarData value = carData.get(0); 183 CarData value = carData.get(0);
184 // value.setSim("1030715050"); 184 // value.setSim("1030715050");
185 - value.setSim2("13450328013");  
186 - value.setSim("123456789011"); 185 + value.setSim2("13450328009");
  186 + value.setSim("3904517427");
187 map.put(value.getSim().replaceAll("^0+", ""), value); 187 map.put(value.getSim().replaceAll("^0+", ""), value);
188 if (StringUtils.isNotBlank(value.getSim2())) { 188 if (StringUtils.isNotBlank(value.getSim2())) {
189 map.put(value.getSim2().replaceAll("^0+", ""), value); 189 map.put(value.getSim2().replaceAll("^0+", ""), value);
@@ -357,11 +357,11 @@ public class TuohuaConfigBean { @@ -357,11 +357,11 @@ public class TuohuaConfigBean {
357 hashMap.put("sim", formatSim(convertStr(ch.get("sim")))); 357 hashMap.put("sim", formatSim(convertStr(ch.get("sim"))));
358 hashMap.put("sim2", formatSim(convertStr(ch.get("sim2")))); 358 hashMap.put("sim2", formatSim(convertStr(ch.get("sim2"))));
359 hashMap.put("abnormalStatus", abnormalStatus); 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 // hashMap.put("sim","1030715050"); 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 return hashMap; 366 return hashMap;
367 } 367 }
src/main/java/com/genersoft/iot/vmp/vmanager/jt1078/platform/handler/HttpClientUtil.java
1 package com.genersoft.iot.vmp.vmanager.jt1078.platform.handler; 1 package com.genersoft.iot.vmp.vmanager.jt1078.platform.handler;
2 2
3 -import com.alibaba.fastjson2.JSON;  
4 import com.genersoft.iot.vmp.vmanager.jt1078.platform.ben.HttpClientPostEntity; 3 import com.genersoft.iot.vmp.vmanager.jt1078.platform.ben.HttpClientPostEntity;
5 -import org.apache.commons.collections4.CollectionUtils;  
6 import org.apache.http.HttpEntity; 4 import org.apache.http.HttpEntity;
7 import org.apache.http.NameValuePair; 5 import org.apache.http.NameValuePair;
8 import org.apache.http.client.CookieStore; 6 import org.apache.http.client.CookieStore;
  7 +import org.apache.http.client.config.RequestConfig;
9 import org.apache.http.client.entity.UrlEncodedFormEntity; 8 import org.apache.http.client.entity.UrlEncodedFormEntity;
10 import org.apache.http.client.methods.CloseableHttpResponse; 9 import org.apache.http.client.methods.CloseableHttpResponse;
11 import org.apache.http.client.methods.HttpGet; 10 import org.apache.http.client.methods.HttpGet;
12 import org.apache.http.client.methods.HttpPost; 11 import org.apache.http.client.methods.HttpPost;
  12 +import org.apache.http.client.protocol.HttpClientContext;
13 import org.apache.http.client.utils.URIBuilder; 13 import org.apache.http.client.utils.URIBuilder;
14 import org.apache.http.entity.StringEntity; 14 import org.apache.http.entity.StringEntity;
15 import org.apache.http.impl.client.BasicCookieStore; 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 import org.apache.http.impl.cookie.BasicClientCookie; 19 import org.apache.http.impl.cookie.BasicClientCookie;
18 import org.apache.http.message.BasicNameValuePair; 20 import org.apache.http.message.BasicNameValuePair;
19 -import org.apache.http.params.BasicHttpParams;  
20 -import org.apache.http.params.HttpConnectionParams;  
21 import org.apache.http.util.EntityUtils; 21 import org.apache.http.util.EntityUtils;
22 -import org.jetbrains.annotations.NotNull;  
23 import org.slf4j.Logger; 22 import org.slf4j.Logger;
24 import org.slf4j.LoggerFactory; 23 import org.slf4j.LoggerFactory;
25 import org.springframework.stereotype.Component; 24 import org.springframework.stereotype.Component;
26 25
  26 +import javax.annotation.PostConstruct;
27 import java.io.IOException; 27 import java.io.IOException;
28 import java.net.URI; 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 @Component 38 @Component
37 public class HttpClientUtil { 39 public class HttpClientUtil {
38 private static final Logger log = LoggerFactory.getLogger(HttpClientUtil.class); 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 CloseableHttpResponse response = null; 74 CloseableHttpResponse response = null;
63 try { 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 if (response.getStatusLine().getStatusCode() == 200) { 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 } finally { 103 } finally {
71 - if (response != null) {  
72 - response.close();  
73 - }  
74 - httpclient.close(); 104 + closeResponse(response);
75 } 105 }
76 return null; 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 long startTime = System.currentTimeMillis(); 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 if (response.getStatusLine().getStatusCode() == 200) { 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 } catch (Exception e) { 135 } catch (Exception e) {
107 - log.error("请求数据异常", e); 136 + log.error("POST JSON请求异常, url: {}", url, e);
108 } finally { 137 } finally {
109 - if (response != null) {  
110 - response.close();  
111 - }  
112 - httpclient.close(); 138 + closeResponse(response);
113 } 139 }
114 return null; 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 long startTime = System.currentTimeMillis(); 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 if (response.getStatusLine().getStatusCode() == 200) { 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 } finally { 165 } finally {
160 - if (response != null) {  
161 - response.close();  
162 - }  
163 - httpclient.close(); 166 + closeResponse(response);
164 } 167 }
165 return null; 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 CloseableHttpResponse response = null; 172 CloseableHttpResponse response = null;
183 try { 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 } finally { 181 } finally {
192 - if (response != null) {  
193 - response.close();  
194 - }  
195 - httpclient.close(); 182 + closeResponse(response);
196 } 183 }
197 return false; 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 String result = EntityUtils.toString(httpEntity, "UTF-8"); 209 String result = EntityUtils.toString(httpEntity, "UTF-8");
215 210
216 HttpClientPostEntity postEntity = new HttpClientPostEntity(); 211 HttpClientPostEntity postEntity = new HttpClientPostEntity();
217 postEntity.setCookieStore(cookieStore); 212 postEntity.setCookieStore(cookieStore);
218 postEntity.setResultStr(result); 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 return postEntity; 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 package com.genersoft.iot.vmp.vmanager.user; 1 package com.genersoft.iot.vmp.vmanager.user;
2 2
  3 +import com.alibaba.fastjson2.JSON;
  4 +import com.alibaba.fastjson2.JSONObject;
3 import com.genersoft.iot.vmp.conf.exception.ControllerException; 5 import com.genersoft.iot.vmp.conf.exception.ControllerException;
4 import com.genersoft.iot.vmp.conf.security.JwtUtils; 6 import com.genersoft.iot.vmp.conf.security.JwtUtils;
5 import com.genersoft.iot.vmp.conf.security.SecurityUtils; 7 import com.genersoft.iot.vmp.conf.security.SecurityUtils;
@@ -9,7 +11,9 @@ import com.genersoft.iot.vmp.service.IUserService; @@ -9,7 +11,9 @@ import com.genersoft.iot.vmp.service.IUserService;
9 import com.genersoft.iot.vmp.storager.mapper.dto.Role; 11 import com.genersoft.iot.vmp.storager.mapper.dto.Role;
10 import com.genersoft.iot.vmp.storager.mapper.dto.User; 12 import com.genersoft.iot.vmp.storager.mapper.dto.User;
11 import com.genersoft.iot.vmp.utils.DateUtil; 13 import com.genersoft.iot.vmp.utils.DateUtil;
  14 +import com.genersoft.iot.vmp.utils.HttpClientUtil;
12 import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; 15 import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
  16 +import com.genersoft.iot.vmp.vmanager.bean.UsLogin;
13 import com.genersoft.iot.vmp.vmanager.bean.WVPResult; 17 import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
14 import com.github.pagehelper.PageInfo; 18 import com.github.pagehelper.PageInfo;
15 import io.swagger.v3.oas.annotations.Operation; 19 import io.swagger.v3.oas.annotations.Operation;
@@ -28,6 +32,7 @@ import javax.servlet.http.HttpServletRequest; @@ -28,6 +32,7 @@ import javax.servlet.http.HttpServletRequest;
28 import javax.servlet.http.HttpServletResponse; 32 import javax.servlet.http.HttpServletResponse;
29 import java.time.LocalDateTime; 33 import java.time.LocalDateTime;
30 import java.util.List; 34 import java.util.List;
  35 +import java.util.Map;
31 36
32 @Tag(name = "用户管理") 37 @Tag(name = "用户管理")
33 @RestController 38 @RestController
@@ -44,7 +49,6 @@ public class UserController { @@ -44,7 +49,6 @@ public class UserController {
44 private IRoleService roleService; 49 private IRoleService roleService;
45 50
46 @GetMapping("/login") 51 @GetMapping("/login")
47 - @PostMapping("/login")  
48 @Operation(summary = "登录", description = "登录成功后返回AccessToken, 可以从返回值获取到也可以从响应头中获取到," + 52 @Operation(summary = "登录", description = "登录成功后返回AccessToken, 可以从返回值获取到也可以从响应头中获取到," +
49 "后续的请求需要添加请求头 'access-token'或者放在参数里") 53 "后续的请求需要添加请求头 'access-token'或者放在参数里")
50 54
@@ -68,6 +72,56 @@ public class UserController { @@ -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 @PostMapping("/changePassword") 125 @PostMapping("/changePassword")
72 @Operation(summary = "修改密码", security = @SecurityRequirement(name = JwtUtils.HEADER)) 126 @Operation(summary = "修改密码", security = @SecurityRequirement(name = JwtUtils.HEADER))
73 @Parameter(name = "username", description = "用户名", required = true) 127 @Parameter(name = "username", description = "用户名", required = true)
src/main/resources/application-wx-local.yml
@@ -54,7 +54,7 @@ spring: @@ -54,7 +54,7 @@ spring:
54 max-lifetime: 1200000 # 是池中连接关闭后的最长生命周期(以毫秒为单位) 54 max-lifetime: 1200000 # 是池中连接关闭后的最长生命周期(以毫秒为单位)
55 #[可选] WVP监听的HTTP端口, 网页和接口调用都是这个端口 55 #[可选] WVP监听的HTTP端口, 网页和接口调用都是这个端口
56 server: 56 server:
57 - port: 16030 57 + port: 18090
58 # [可选] HTTPS配置, 默认不开启 58 # [可选] HTTPS配置, 默认不开启
59 ssl: 59 ssl:
60 # [可选] 是否开启HTTPS访问 60 # [可选] 是否开启HTTPS访问
@@ -185,8 +185,10 @@ tuohua: @@ -185,8 +185,10 @@ tuohua:
185 userName: yuanxiaohu 185 userName: yuanxiaohu
186 password: Yxiaohu1.0 186 password: Yxiaohu1.0
187 rest: 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 tree: 192 tree:
191 url: 193 url:
192 company: http://${my.ip}:9088/video/tree 194 company: http://${my.ip}:9088/video/tree
@@ -206,10 +208,10 @@ tuohua: @@ -206,10 +208,10 @@ tuohua:
206 addPortVal: 0 208 addPortVal: 0
207 pushURL: http://${my.ip}:3333/new/server/{pushKey}/{port}/{httpPort} 209 pushURL: http://${my.ip}:3333/new/server/{pushKey}/{port}/{httpPort}
208 stopPushURL: http://${my.ip}:3333/stop/channel/{pushKey}/{port}/{httpPort} 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 # url: http://10.10.2.20:8100/device/{0} 211 # url: http://10.10.2.20:8100/device/{0}
212 # new_url: http://10.10.2.20:8100/device 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 historyListPort: 9205 215 historyListPort: 9205
214 history_upload: 9206 216 history_upload: 9206
215 playHistoryPort: 9201 217 playHistoryPort: 9201
web_src/build/webpack.base.conf.js
@@ -40,6 +40,7 @@ module.exports = { @@ -40,6 +40,7 @@ module.exports = {
40 { 40 {
41 test: /\.js$/, 41 test: /\.js$/,
42 loader: 'babel-loader', 42 loader: 'babel-loader',
  43 + exclude: /node_modules[\\/]@wchbrad[\\/]vue-easy-tree/,
43 include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')] 44 include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
44 }, 45 },
45 { 46 {
web_src/config/index.js
@@ -11,14 +11,14 @@ module.exports = { @@ -11,14 +11,14 @@ module.exports = {
11 assetsPublicPath: "/", 11 assetsPublicPath: "/",
12 proxyTable: { 12 proxyTable: {
13 "/debug": { 13 "/debug": {
14 - target: "http://127.0.0.1:16030", 14 + target: "http://127.0.0.1:18090",
15 changeOrigin: true, 15 changeOrigin: true,
16 pathRewrite: { 16 pathRewrite: {
17 "^/debug": "/", 17 "^/debug": "/",
18 }, 18 },
19 }, 19 },
20 "/static/snap": { 20 "/static/snap": {
21 - target: "http://127.0.0.1:16030", 21 + target: "http://127.0.0.1:18090",
22 changeOrigin: true, 22 changeOrigin: true,
23 // pathRewrite: { 23 // pathRewrite: {
24 // '^/static/snap': '/static/snap' 24 // '^/static/snap': '/static/snap'
web_src/index.html
@@ -10,6 +10,7 @@ @@ -10,6 +10,7 @@
10 </head> 10 </head>
11 <body> 11 <body>
12 <script type="text/javascript" src="./static/js/jessibuca/jessibuca.js"></script> 12 <script type="text/javascript" src="./static/js/jessibuca/jessibuca.js"></script>
  13 + <script type="text/javascript" src="./static/EasyPlayer-pro.js"></script>
13 <script type="text/javascript" src="./static/js/EasyWasmPlayer.js"></script> 14 <script type="text/javascript" src="./static/js/EasyWasmPlayer.js"></script>
14 <script type="text/javascript" src="./static/js/liveplayer-lib.min.js"></script> 15 <script type="text/javascript" src="./static/js/liveplayer-lib.min.js"></script>
15 <script type="text/javascript" src="./static/js/ZLMRTCClient.js"></script> 16 <script type="text/javascript" src="./static/js/ZLMRTCClient.js"></script>
web_src/package.json
@@ -14,6 +14,7 @@ @@ -14,6 +14,7 @@
14 }, 14 },
15 "dependencies": { 15 "dependencies": {
16 "@liveqing/liveplayer": "^2.7.10", 16 "@liveqing/liveplayer": "^2.7.10",
  17 + "@wchbrad/vue-easy-tree": "^1.0.13",
17 "axios": "^0.24.0", 18 "axios": "^0.24.0",
18 "core-js": "^2.6.5", 19 "core-js": "^2.6.5",
19 "echarts": "^4.9.0", 20 "echarts": "^4.9.0",
@@ -24,6 +25,7 @@ @@ -24,6 +25,7 @@
24 "moment": "^2.29.1", 25 "moment": "^2.29.1",
25 "ol": "^6.14.1", 26 "ol": "^6.14.1",
26 "postcss-pxtorem": "^5.1.1", 27 "postcss-pxtorem": "^5.1.1",
  28 + "splitpanes": "^2.4.1",
27 "uuid": "^8.3.2", 29 "uuid": "^8.3.2",
28 "v-charts": "^1.19.0", 30 "v-charts": "^1.19.0",
29 "vue": "^2.6.11", 31 "vue": "^2.6.11",
@@ -49,6 +51,7 @@ @@ -49,6 +51,7 @@
49 "chalk": "^2.0.1", 51 "chalk": "^2.0.1",
50 "copy-webpack-plugin": "^4.6.0", 52 "copy-webpack-plugin": "^4.6.0",
51 "css-loader": "^0.28.11", 53 "css-loader": "^0.28.11",
  54 + "dayjs": "^1.11.13",
52 "extract-text-webpack-plugin": "^3.0.0", 55 "extract-text-webpack-plugin": "^3.0.0",
53 "file-loader": "^1.1.4", 56 "file-loader": "^1.1.4",
54 "friendly-errors-webpack-plugin": "^1.6.1", 57 "friendly-errors-webpack-plugin": "^1.6.1",
web_src/src/App.vue
@@ -35,8 +35,6 @@ export default { @@ -35,8 +35,6 @@ export default {
35 }catch (e) { 35 }catch (e) {
36 console.error(e) 36 console.error(e)
37 } 37 }
38 - //如果没有登录状态则跳转到登录页  
39 - this.$router.push('/login');  
40 } 38 }
41 }, 39 },
42 40
@@ -58,6 +56,9 @@ body, @@ -58,6 +56,9 @@ body,
58 background-color: #e9eef3; 56 background-color: #e9eef3;
59 height: 100%; 57 height: 100%;
60 } 58 }
  59 +#app .theme-picker {
  60 + display: none;
  61 +}
61 .el-header, 62 .el-header,
62 .el-footer { 63 .el-footer {
63 /* background-color: #b3c0d1; */ 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,7 +102,7 @@
102 <script> 102 <script>
103 import uiHeader from '../layout/UiHeader.vue' 103 import uiHeader from '../layout/UiHeader.vue'
104 import MediaServer from './service/MediaServer' 104 import MediaServer from './service/MediaServer'
105 -import easyPlayer from './common/easyPlayer.vue' 105 +import easyPlayer from './common/EasyPlayer.vue'
106 import moment from 'moment' 106 import moment from 'moment'
107 import axios from "axios"; 107 import axios from "axios";
108 108
web_src/src/components/CloudRecordDetail.vue
@@ -135,7 +135,7 @@ @@ -135,7 +135,7 @@
135 <script> 135 <script>
136 // TODO 根据查询的时间列表设置滑轨的最大值与最小值, 136 // TODO 根据查询的时间列表设置滑轨的最大值与最小值,
137 import uiHeader from '../layout/UiHeader.vue' 137 import uiHeader from '../layout/UiHeader.vue'
138 - import player from './common/easyPlayer.vue' 138 + import player from './common/EasyPlayer.vue'
139 import moment from 'moment' 139 import moment from 'moment'
140 import axios from "axios"; 140 import axios from "axios";
141 export default { 141 export default {
web_src/src/components/DeviceList1078.vue
1 <template> 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 </div> 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 </template> 79 </template>
  80 +
107 <script> 81 <script>
108 import tree from "vue-giant-tree"; 82 import tree from "vue-giant-tree";
109 import uiHeader from "../layout/UiHeader.vue"; 83 import uiHeader from "../layout/UiHeader.vue";
@@ -111,1247 +85,682 @@ import playerListComponent from &#39;./common/PlayerListComponent.vue&#39;; @@ -111,1247 +85,682 @@ import playerListComponent from &#39;./common/PlayerListComponent.vue&#39;;
111 import player from './common/JessVideoPlayer.vue'; 85 import player from './common/JessVideoPlayer.vue';
112 import DeviceTree from './common/DeviceTree.vue' 86 import DeviceTree from './common/DeviceTree.vue'
113 import treeTransfer from "el-tree-transfer"; 87 import treeTransfer from "el-tree-transfer";
114 -import {parseTime} from "../../utils/ruoyi";  
115 import Device1078Tree from "./JT1078Components/deviceList/Device1078Tree.vue"; 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 export default { 93 export default {
119 name: "live", 94 name: "live",
120 components: { 95 components: {
  96 + WindowNumSelect,
121 playerListComponent, 97 playerListComponent,
122 Device1078Tree, 98 Device1078Tree,
  99 + VehicleList,
  100 + CarouselConfig,
123 uiHeader, player, DeviceTree, tree, treeTransfer 101 uiHeader, player, DeviceTree, tree, treeTransfer
124 }, 102 },
125 data() { 103 data() {
126 return { 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 windowClickData: null, 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 mounted() { 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 beforeDestroy() { 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 methods: { 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 } else { 196 } else {
332 - this.playerIdx = playerIdx 197 + nvrLabels = ['中门', '', '车前', '驾驶舱', '前门', '前车厢', '后车厢', '360'];
333 } 198 }
  199 + rmLabels = [];
334 } else { 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 } else { 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 this.$axios({ 572 this.$axios({
875 method: 'get', 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 }).then(res => { 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 } else { 587 } else {
897 this.$message.error(res.data.msg); 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 } else { 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 } else { 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 </script> 733 </script>
  734 +
1339 <style scoped> 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 .el-main { 762 .el-main {
1354 - background-color: rgba(0, 0, 0, 0.84); 763 + background-color: rgba(0, 0, 0, 0.95);
1355 color: #333; 764 color: #333;
1356 text-align: center; 765 text-align: center;
1357 line-height: 160px; 766 line-height: 160px;
@@ -1359,165 +768,35 @@ export default { @@ -1359,165 +768,35 @@ export default {
1359 margin-left: 1px; 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 width: 100%; 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 padding: 0; 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 display: flex; 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 </style> 802 </style>
web_src/src/components/HistoricalRecord.vue
1 <template> 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 </div> 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 </template> 39 </template>
65 40
66 <script> 41 <script>
@@ -68,22 +43,36 @@ @@ -68,22 +43,36 @@
68 //例如:import 《组件名称》 from '《组件路径》, 43 //例如:import 《组件名称》 from '《组件路径》,
69 import player from "./common/JessVideoPlayer.vue"; 44 import player from "./common/JessVideoPlayer.vue";
70 import CarTree from "./JT1078Components/cascader/CarTree.vue"; 45 import CarTree from "./JT1078Components/cascader/CarTree.vue";
71 -import {parseTime} from "../../utils/ruoyi";  
72 import HistoricalData from "./JT1078Components/historical/HistoricalDataTree.vue"; 46 import HistoricalData from "./JT1078Components/historical/HistoricalDataTree.vue";
73 import HistoryList from "./JT1078Components/HistoryData.vue"; 47 import HistoryList from "./JT1078Components/HistoryData.vue";
74 import Device1078Tree from "./JT1078Components/deviceList/Device1078Tree.vue"; 48 import Device1078Tree from "./JT1078Components/deviceList/Device1078Tree.vue";
  49 +import HistoryPlayDialog from "./JT1078Components/HistoryPlayDialog.vue";
  50 +import HistoricalRecordForm from "./JT1078Components/HistoryRecordFrom.vue";
75 import HistorySearchTable from "./JT1078Components/HistorySearchTable.vue"; 51 import HistorySearchTable from "./JT1078Components/HistorySearchTable.vue";
76 import userService from "./service/UserService"; 52 import userService from "./service/UserService";
  53 +import { Splitpanes, Pane } from 'splitpanes'
  54 +import {parseTime} from "../../utils/ruoyi";
77 55
78 export default { 56 export default {
79 //import引入的组件需要注入到对象中才能使用" 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 props: {}, 61 props: {},
82 data() { 62 data() {
83 //这里存放数据" 63 //这里存放数据"
84 return { 64 return {
85 //列表定时器 65 //列表定时器
86 timer: null, 66 timer: null,
  67 + open: false,
  68 + videoUrlData: {
  69 + startTime: '',
  70 + endTime: '',
  71 + sim: '',
  72 + channel: '',
  73 + device: '',
  74 + channelName: '',
  75 + },
87 //历史视频列表定时器 76 //历史视频列表定时器
88 historyTimer: null, 77 historyTimer: null,
89 historyData: [], 78 historyData: [],
@@ -97,7 +86,26 @@ export default { @@ -97,7 +86,26 @@ export default {
97 loading: false, 86 loading: false,
98 //sim号和通道号,格式为:sim-channel 87 //sim号和通道号,格式为:sim-channel
99 sim_channel: null, 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 pickerOptions: { 110 pickerOptions: {
103 disabledDate(time) { 111 disabledDate(time) {
@@ -130,30 +138,94 @@ export default { @@ -130,30 +138,94 @@ export default {
130 //选中的日期 138 //选中的日期
131 date: null, 139 date: null,
132 historyPlayListHtml: '', 140 historyPlayListHtml: '',
133 - videoUrl: [] 141 + videoUrl: [],
  142 + deviceNode: null,
134 }; 143 };
135 }, 144 },
136 //计算属性 类似于data概念", 145 //计算属性 类似于data概念",
137 computed: {}, 146 computed: {},
138 //监控data中的数据变化", 147 //监控data中的数据变化",
139 - watch: {}, 148 + watch: {
  149 + deviceNode(val) {
  150 + this.deviceNode = val
  151 + this.$refs.historySearchTable.deviceNode = val
  152 + }
  153 + },
140 //方法集合", 154 //方法集合",
141 methods: { 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 nodeClick(data, node) { 169 nodeClick(data, node) {
146 - if (data.children === undefined && data) { 170 + if (data) {
147 let split = data.id.split("_"); 171 let split = data.id.split("_");
  172 + this.deviceNode = node
  173 + this.nodeChannelData = {};
  174 + let nodeChannelDataList = [];
148 if (split.length === 3) { 175 if (split.length === 3) {
149 this.sim_channel = split[1] + '_' + split[2] 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 console.log("node click ==> ", data) 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,8 +296,36 @@ export default {
224 * 添加通道 296 * 添加通道
225 */ 297 */
226 addChannels(data) { 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 let nvr_labels = ['中门','','车前','驾驶舱','前门','前车厢','后车厢','360']; 329 let nvr_labels = ['中门','','车前','驾驶舱','前门','前车厢','后车厢','360'];
230 //'ADAS','DSM','前门客流','中门客流','360前','360后','360左','360右' 330 //'ADAS','DSM','前门客流','中门客流','360前','360后','360左','360右'
231 let rm_labels = []; 331 let rm_labels = [];
@@ -304,19 +404,29 @@ export default { @@ -304,19 +404,29 @@ export default {
304 * 点击播放视频 404 * 点击播放视频
305 */ 405 */
306 clickHistoricalPlay(data) { 406 clickHistoricalPlay(data) {
  407 + console.log("点击播放视频 ===》 ",data)
307 this.playHistoryItem(data) 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 uploadHistoryVideo(data){ 424 uploadHistoryVideo(data){
313 - console.log("开始上传视频 ===》 ",data)  
314 this.loading = true 425 this.loading = true
315 this.$axios({ 426 this.$axios({
316 method: 'get', 427 method: 'get',
317 url: '/api/jt1078/query/history/uploading/' + data.name 428 url: '/api/jt1078/query/history/uploading/' + data.name
318 }).then(res => { 429 }).then(res => {
319 - console.log("上传视频 ==》 ",res)  
320 this.$message.success("视频开始上传,请等待") 430 this.$message.success("视频开始上传,请等待")
321 this.searchHistoryList() 431 this.searchHistoryList()
322 this.loading = false 432 this.loading = false
@@ -333,15 +443,12 @@ export default { @@ -333,15 +443,12 @@ export default {
333 * 搜索历史视频 443 * 搜索历史视频
334 */ 444 */
335 searchHistoryList() { 445 searchHistoryList() {
336 - this.getDateTime();  
337 let simChannel = this.sim_channel; 446 let simChannel = this.sim_channel;
338 - console.log(this.sim_channel)  
339 if (this.isEmpty(simChannel)) { 447 if (this.isEmpty(simChannel)) {
340 this.$message.error('请选择车辆'); 448 this.$message.error('请选择车辆');
341 return; 449 return;
342 } 450 }
343 let split = simChannel.split('_'); 451 let split = simChannel.split('_');
344 - console.log("simChannel:", simChannel)  
345 let sim = split[0]; 452 let sim = split[0];
346 if (this.isEmpty(sim)) { 453 if (this.isEmpty(sim)) {
347 this.$message.error('无法获取SIM卡信息,请检查设备'); 454 this.$message.error('无法获取SIM卡信息,请检查设备');
@@ -352,22 +459,25 @@ export default { @@ -352,22 +459,25 @@ export default {
352 this.$message.error('请选择通道'); 459 this.$message.error('请选择通道');
353 return; 460 return;
354 } 461 }
355 - console.log(channel);  
356 - if (this.isEmpty(this.startTime) || this.isEmpty(this.endTime)) { 462 + if (!this.queryParams.time) {
357 this.$message.error('请选择开始和结束时间'); 463 this.$message.error('请选择开始和结束时间');
358 return; 464 return;
359 } 465 }
  466 + this.loading = true;
360 this.$axios({ 467 this.$axios({
361 method: 'get', 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 }).then(res => { 470 }).then(res => {
364 let items = res.data.data.obj.data.items; 471 let items = res.data.data.obj.data.items;
365 if (res && res.data && res.data.data && res.data.data.obj && res.data.data.code == 1 && res.data.data.obj.data && items) { 472 if (res && res.data && res.data.data && res.data.data.obj && res.data.data.code == 1 && res.data.data.obj.data && items) {
366 for (let i in items) { 473 for (let i in items) {
367 items[i].disabled = false; 474 items[i].disabled = false;
368 items[i].countdown = 10; 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 this.historyData = items; 479 this.historyData = items;
  480 + console.log("历史列表 ===》 ",items)
371 } else if (res && res.data && res.data.data && res.data.data.msg) { 481 } else if (res && res.data && res.data.data && res.data.data.msg) {
372 this.$message.error(res.data.data.msg); 482 this.$message.error(res.data.data.msg);
373 } else if (items === undefined) { 483 } else if (items === undefined) {
@@ -377,7 +487,7 @@ export default { @@ -377,7 +487,7 @@ export default {
377 clearInterval(this.historyTimer) 487 clearInterval(this.historyTimer)
378 } 488 }
379 } 489 }
380 - this.loading = false 490 + this.loading = false
381 }).catch(error => { 491 }).catch(error => {
382 console.log(error) 492 console.log(error)
383 this.loading = false 493 this.loading = false
@@ -386,13 +496,17 @@ export default { @@ -386,13 +496,17 @@ export default {
386 }) 496 })
387 }, 497 },
388 /** 498 /**
  499 + * 获取设备通道
  500 + */
  501 + getDeviceChannelMap() {
  502 +
  503 + },
  504 + /**
389 * 时间转换 505 * 时间转换
390 */ 506 */
391 getDateTime() { 507 getDateTime() {
392 let date = this.date; 508 let date = this.date;
393 let timeList = this.timeList; 509 let timeList = this.timeList;
394 - console.log("date ", date)  
395 - console.log("timeList ", timeList)  
396 if (this.isEmpty(date)) { 510 if (this.isEmpty(date)) {
397 this.$message.error("请选择日期") 511 this.$message.error("请选择日期")
398 return false; 512 return false;
@@ -414,8 +528,6 @@ export default { @@ -414,8 +528,6 @@ export default {
414 endTime.setDate(day); 528 endTime.setDate(day);
415 startTime = parseTime(startTime, '{y}-{m}-{d} {h}:{i}:{s}'); 529 startTime = parseTime(startTime, '{y}-{m}-{d} {h}:{i}:{s}');
416 endTime = parseTime(endTime, '{y}-{m}-{d} {h}:{i}:{s}'); 530 endTime = parseTime(endTime, '{y}-{m}-{d} {h}:{i}:{s}');
417 - console.log("startTime:" + startTime)  
418 - console.log("endTime:" + endTime)  
419 this.startTime = startTime; 531 this.startTime = startTime;
420 this.endTime = endTime; 532 this.endTime = endTime;
421 return true 533 return true
@@ -425,6 +537,7 @@ export default { @@ -425,6 +537,7 @@ export default {
425 */ 537 */
426 playHistoryItem(e) { 538 playHistoryItem(e) {
427 this.videoUrl = []; 539 this.videoUrl = [];
  540 + this.loading = true
428 this.$axios({ 541 this.$axios({
429 method: 'get', 542 method: 'get',
430 url: '/api/jt1078/query/send/request/io/history/' + e.sim + '/' + e.channel + "/" + e.startTime + "/" + e.endTime + "/" + e.channelMapping 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,15 +554,22 @@ export default {
441 this.httpPort = res.data.data.httpPort; 554 this.httpPort = res.data.data.httpPort;
442 this.stream = res.data.data.stream; 555 this.stream = res.data.data.stream;
443 this.videoUrlHistory = videoUrl1; 556 this.videoUrlHistory = videoUrl1;
444 -  
445 let itemData = new Object(); 557 let itemData = new Object();
446 itemData.deviceId = this.sim; 558 itemData.deviceId = this.sim;
447 itemData.channelId = this.channel; 559 itemData.channelId = this.channel;
448 itemData.playUrl = videoUrl1; 560 itemData.playUrl = videoUrl1;
449 - console.log(this.playerIdx);  
450 this.setPlayUrl(videoUrl1, 0); 561 this.setPlayUrl(videoUrl1, 0);
451 this.hisotoryPlayFlag = true; 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 } else if (res.data.data && res.data.data.msg) { 573 } else if (res.data.data && res.data.data.msg) {
454 this.$message.error(res.data.data.msg); 574 this.$message.error(res.data.data.msg);
455 } else if (res.data.msg) { 575 } else if (res.data.msg) {
@@ -457,7 +577,7 @@ export default { @@ -457,7 +577,7 @@ export default {
457 } else if (res.msg) { 577 } else if (res.msg) {
458 this.$message.error(res.msg); 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,8 +594,6 @@ export default {
474 }, 594 },
475 595
476 shot(e) { 596 shot(e) {
477 - // console.log(e)  
478 - // send({code:'image',data:e})  
479 var base64ToBlob = function (code) { 597 var base64ToBlob = function (code) {
480 let parts = code.split(';base64,'); 598 let parts = code.split(';base64,');
481 let contentType = parts[0].split(':')[1]; 599 let contentType = parts[0].split(':')[1];
@@ -498,14 +616,12 @@ export default { @@ -498,14 +616,12 @@ export default {
498 aLink.click(); 616 aLink.click();
499 }, 617 },
500 destroy(idx) { 618 destroy(idx) {
501 - console.log(idx);  
502 this.clear(idx.substring(idx.length - 1)) 619 this.clear(idx.substring(idx.length - 1))
503 }, 620 },
504 621
505 createdPlay() { 622 createdPlay() {
506 if (flvjs.isSupported()) { 623 if (flvjs.isSupported()) {
507 // var videoDom = document.getElementById('myVideo') 624 // var videoDom = document.getElementById('myVideo')
508 - console.log(this.videoUrlHistory);  
509 let videoDom = this.$refs.myVideo 625 let videoDom = this.$refs.myVideo
510 // 创建一个播放器实例 626 // 创建一个播放器实例
511 var player = flvjs.createPlayer({ 627 var player = flvjs.createPlayer({
@@ -637,6 +753,74 @@ li#menu-item-delete, li#menu-item-rename { @@ -637,6 +753,74 @@ li#menu-item-delete, li#menu-item-rename {
637 } 753 }
638 </style> 754 </style>
639 <style> 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 .videoList { 824 .videoList {
641 display: flex; 825 display: flex;
642 flex-wrap: wrap; 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,11 +4,11 @@
4 v-if="tableData.length > 0" 4 v-if="tableData.length > 0"
5 ref="singleTable" 5 ref="singleTable"
6 :data="tableData" 6 :data="tableData"
7 - :header-cell-style="{ textAlign: 'center' ,height:'20px',lineHeight:'20px' }" 7 + :header-cell-style="{ textAlign: 'center' ,height:'100%',lineHeight:'100%' }"
8 border 8 border
  9 + height="calc(100% - 100px)"
9 highlight-current-row 10 highlight-current-row
10 @current-change="handleCurrentChange" 11 @current-change="handleCurrentChange"
11 - height="250"  
12 style="width: 100%;text-align: center"> 12 style="width: 100%;text-align: center">
13 <el-table-column 13 <el-table-column
14 type="index" 14 type="index"
@@ -23,6 +23,16 @@ @@ -23,6 +23,16 @@
23 label="名称"> 23 label="名称">
24 </el-table-column> 24 </el-table-column>
25 <el-table-column 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 align="center" 36 align="center"
27 label="日期"> 37 label="日期">
28 <template slot-scope="scope"> 38 <template slot-scope="scope">
@@ -65,6 +75,7 @@ @@ -65,6 +75,7 @@
65 //例如:import 《组件名称》 from '《组件路径》, 75 //例如:import 《组件名称》 from '《组件路径》,
66 export default { 76 export default {
67 //import引入的组件需要注入到对象中才能使用" 77 //import引入的组件需要注入到对象中才能使用"
  78 + name: "historySearchTable",
68 components: {}, 79 components: {},
69 props: { 80 props: {
70 tableData: { 81 tableData: {