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