Commit f92fd6ac57410277e647cc75523c503ea9fb90a7

Authored by wuxw
1 parent ce5e1a2a

开发我的小区下的功能

Showing 64 changed files with 8758 additions and 0 deletions
public/index.html 0 → 100644
  1 +<!DOCTYPE html>
  2 +<html lang="en">
  3 +<head>
  4 + <meta charset="utf-8">
  5 + <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6 + <meta name="viewport" content="width=device-width,initial-scale=1.0">
  7 + <link rel="icon" href="<%= BASE_URL %>favicon.ico">
  8 + <title><%= htmlWebpackPlugin.options.title %></title>
  9 + <!-- 全局脚本引入 -->
  10 + <script>
  11 + // 脚本加载错误处理
  12 + window.scriptLoadErrors = [];
  13 +
  14 + function handleScriptError(scriptName, error) {
  15 + console.error('脚本加载失败:', scriptName, error);
  16 + window.scriptLoadErrors.push({ name: scriptName, error: error.message });
  17 + }
  18 + </script>
  19 + <script src="https://map.qq.com/api/gljs?v=1.exp&key="></script>
  20 +
  21 + <!-- Qs 库 - 主 CDN -->
  22 + <script src="https://cdn.bootcdn.net/ajax/libs/qs/6.10.3/qs.min.js"
  23 + onerror="handleScriptError('Qs-bootcdn', new Error('bootcdn 加载失败'))"></script>
  24 + <script src="/js/jessibuca/jessibuca.js"></script>
  25 +
  26 +
  27 +
  28 + <!-- 可以在这里添加更多全局脚本 -->
  29 +</head>
  30 +<body>
  31 + <noscript>
  32 + <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
  33 + </noscript>
  34 + <div id="app"></div>
  35 + <!-- built files will be auto injected -->
  36 +</body>
  37 +
  38 +</html>
0 39 \ No newline at end of file
... ...
public/js/jessibuca/jessibuca.d.ts 0 → 100644
  1 +declare namespace Jessibuca {
  2 +
  3 + /** 超时信息 */
  4 + enum TIMEOUT {
  5 + /** 当play()的时候,如果没有数据返回 */
  6 + loadingTimeout = 'loadingTimeout',
  7 + /** 当播放过程中,如果超过timeout之后没有数据渲染 */
  8 + delayTimeout = 'delayTimeout',
  9 + }
  10 +
  11 + /** 错误信息 */
  12 + enum ERROR {
  13 + /** 播放错误,url 为空的时候,调用 play 方法 */
  14 + playError = 'playError',
  15 + /** http 请求失败 */
  16 + fetchError = 'fetchError',
  17 + /** websocket 请求失败 */
  18 + websocketError = 'websocketError',
  19 + /** webcodecs 解码 h265 失败 */
  20 + webcodecsH265NotSupport = 'webcodecsH265NotSupport',
  21 + /** mediaSource 解码 h265 失败 */
  22 + mediaSourceH265NotSupport = 'mediaSourceH265NotSupport',
  23 + /** wasm 解码失败 */
  24 + wasmDecodeError = 'wasmDecodeError',
  25 + }
  26 +
  27 + interface Config {
  28 + /**
  29 + * 播放器容器
  30 + * * 若为 string ,则底层调用的是 document.getElementById('id')
  31 + * */
  32 + container: HTMLElement | string;
  33 + /**
  34 + * 设置最大缓冲时长,单位秒,播放器会自动消除延迟
  35 + */
  36 + videoBuffer?: number;
  37 + /**
  38 + * worker地址
  39 + * * 默认引用的是根目录下面的decoder.js文件 ,decoder.js 与 decoder.wasm文件必须是放在同一个目录下面。 */
  40 + decoder?: string;
  41 + /**
  42 + * 是否不使用离屏模式(提升渲染能力)
  43 + */
  44 + forceNoOffscreen?: boolean;
  45 + /**
  46 + * 是否开启当页面的'visibilityState'变为'hidden'的时候,自动暂停播放。
  47 + */
  48 + hiddenAutoPause?: boolean;
  49 + /**
  50 + * 是否有音频,如果设置`false`,则不对音频数据解码,提升性能。
  51 + */
  52 + hasAudio?: boolean;
  53 + /**
  54 + * 设置旋转角度,只支持,0(默认),180,270 三个值
  55 + */
  56 + rotate?: boolean;
  57 + /**
  58 + * 1. 当为`true`的时候:视频画面做等比缩放后,高或宽对齐canvas区域,画面不被拉伸,但有黑边。 等同于 `setScaleMode(1)`
  59 + * 2. 当为`false`的时候:视频画面完全填充canvas区域,画面会被拉伸。等同于 `setScaleMode(0)`
  60 + */
  61 + isResize?: boolean;
  62 + /**
  63 + * 1. 当为`true`的时候:视频画面做等比缩放后,完全填充canvas区域,画面不被拉伸,没有黑边,但画面显示不全。等同于 `setScaleMode(2)`
  64 + */
  65 + isFullResize?: boolean;
  66 + /**
  67 + * 1. 当为`true`的时候:ws协议不检验是否以.flv为依据,进行协议解析。
  68 + */
  69 + isFlv?: boolean;
  70 + /**
  71 + * 是否开启控制台调试打
  72 + */
  73 + debug?: boolean;
  74 + /**
  75 + * 1. 设置超时时长, 单位秒
  76 + * 2. 在连接成功之前(loading)和播放中途(heart),如果超过设定时长无数据返回,则回调timeout事件
  77 + */
  78 + timeout?: number;
  79 + /**
  80 + * 1. 设置超时时长, 单位秒
  81 + * 2. 在连接成功之前,如果超过设定时长无数据返回,则回调timeout事件
  82 + */
  83 + heartTimeout?: number;
  84 + /**
  85 + * 1. 设置超时时长, 单位秒
  86 + * 2. 在连接成功之前,如果超过设定时长无数据返回,则回调timeout事件
  87 + */
  88 + loadingTimeout?: number;
  89 + /**
  90 + * 是否支持屏幕的双击事件,触发全屏,取消全屏事件
  91 + */
  92 + supportDblclickFullscreen?: boolean;
  93 + /**
  94 + * 是否显示网
  95 + */
  96 + showBandwidth?: boolean;
  97 + /**
  98 + * 配置操作按钮
  99 + */
  100 + operateBtns?: {
  101 + /** 是否显示全屏按钮 */
  102 + fullscreen?: boolean;
  103 + /** 是否显示截图按钮 */
  104 + screenshot?: boolean;
  105 + /** 是否显示播放暂停按钮 */
  106 + play?: boolean;
  107 + /** 是否显示声音按钮 */
  108 + audio?: boolean;
  109 + /** 是否显示录制按 */
  110 + record?: boolean;
  111 + };
  112 + /**
  113 + * 开启屏幕常亮,在手机浏览器上, canvas标签渲染视频并不会像video标签那样保持屏幕常亮
  114 + */
  115 + keepScreenOn?: boolean;
  116 + /**
  117 + * 是否开启声音,默认是关闭声音播放的
  118 + */
  119 + isNotMute?: boolean;
  120 + /**
  121 + * 加载过程中文案
  122 + */
  123 + loadingText?: string;
  124 + /**
  125 + * 背景图片
  126 + */
  127 + background?: string;
  128 + /**
  129 + * 是否开启MediaSource硬解码
  130 + * * 视频编码只支持H.264视频(Safari on iOS不支持)
  131 + * * 不支持 forceNoOffscreen 为 false (开启离屏渲染)
  132 + */
  133 + useMSE?: boolean;
  134 + /**
  135 + * 是否开启Webcodecs硬解码
  136 + * * 视频编码只支持H.264视频 (需在chrome 94版本以上,需要https或者localhost环境)
  137 + * * 支持 forceNoOffscreen 为 false (开启离屏渲染)
  138 + * */
  139 + useWCS?: boolean;
  140 + /**
  141 + * 是否开启键盘快捷键
  142 + * 目前支持的键盘快捷键有:esc -> 退出全屏;arrowUp -> 声音增加;arrowDown -> 声音减少;
  143 + */
  144 + hotKey?: boolean;
  145 + /**
  146 + * 在使用MSE或者Webcodecs 播放H265的时候,是否自动降级到wasm模式。
  147 + * 设置为false 则直接关闭播放,抛出Error 异常,设置为true 则会自动切换成wasm模式播放。
  148 + */
  149 + autoWasm?: boolean;
  150 + /**
  151 + * heartTimeout 心跳超时之后自动再播放,不再抛出异常,而直接重新播放视频地址。
  152 + */
  153 + heartTimeoutReplay?: boolean,
  154 + /**
  155 + * heartTimeoutReplay 从试次数,超过之后,不再自动播放
  156 + */
  157 + heartTimeoutReplayTimes?: number,
  158 + /**
  159 + * loadingTimeout loading之后自动再播放,不再抛出异常,而直接重新播放视频地址。
  160 + */
  161 + loadingTimeoutReplay?: boolean,
  162 + /**
  163 + * heartTimeoutReplay 从试次数,超过之后,不再自动播放
  164 + */
  165 + loadingTimeoutReplayTimes?: number
  166 + /**
  167 + * wasm解码报错之后,不再抛出异常,而是直接重新播放视频地址。
  168 + */
  169 + wasmDecodeErrorReplay?: boolean,
  170 + /**
  171 + * https://github.com/langhuihui/jessibuca/issues/152 解决方案
  172 + * 例如:WebGL图像预处理默认每次取4字节的数据,但是540x960分辨率下的U、V分量宽度是540/2=270不能被4整除,导致绿屏。
  173 + */
  174 + openWebglAlignment?: boolean
  175 + }
  176 +}
  177 +
  178 +
  179 +declare class Jessibuca {
  180 +
  181 + constructor(config?: Jessibuca.Config);
  182 +
  183 + /**
  184 + * 是否开启控制台调试打印
  185 + @example
  186 + // 开启
  187 + jessibuca.setDebug(true)
  188 + // 关闭
  189 + jessibuca.setDebug(false)
  190 + */
  191 + setDebug(flag: boolean): void;
  192 +
  193 + /**
  194 + * 静音
  195 + @example
  196 + jessibuca.mute()
  197 + */
  198 + mute(): void;
  199 +
  200 + /**
  201 + * 取消静音
  202 + @example
  203 + jessibuca.cancelMute()
  204 + */
  205 + cancelMute(): void;
  206 +
  207 + /**
  208 + * 留给上层用户操作来触发音频恢复的方法。
  209 + *
  210 + * iPhone,chrome等要求自动播放时,音频必须静音,需要由一个真实的用户交互操作来恢复,不能使用代码。
  211 + *
  212 + * https://developers.google.com/web/updates/2017/09/autoplay-policy-changes
  213 + */
  214 + audioResume(): void;
  215 +
  216 + /**
  217 + *
  218 + * 设置超时时长, 单位秒
  219 + * 在连接成功之前和播放中途,如果超过设定时长无数据返回,则回调timeout事件
  220 +
  221 + @example
  222 + jessibuca.setTimeout(10)
  223 +
  224 + jessibuca.on('timeout',function(){
  225 + //
  226 + });
  227 + */
  228 + setTimeout(): void;
  229 +
  230 + /**
  231 + * @param mode
  232 + * 0 视频画面完全填充canvas区域,画面会被拉伸 等同于参数 `isResize` 为false
  233 + *
  234 + * 1 视频画面做等比缩放后,高或宽对齐canvas区域,画面不被拉伸,但有黑边 等同于参数 `isResize` 为true
  235 + *
  236 + * 2 视频画面做等比缩放后,完全填充canvas区域,画面不被拉伸,没有黑边,但画面显示不全 等同于参数 `isFullResize` 为true
  237 + @example
  238 + jessibuca.setScaleMode(0)
  239 +
  240 + jessibuca.setScaleMode(1)
  241 +
  242 + jessibuca.setScaleMode(2)
  243 + */
  244 + setScaleMode(mode: number): void;
  245 +
  246 + /**
  247 + * 暂停播放
  248 + *
  249 + * 可以在pause 之后,再调用 `play()`方法就继续播放之前的流。
  250 + @example
  251 + jessibuca.pause().then(()=>{
  252 + console.log('pause success')
  253 +
  254 + jessibuca.play().then(()=>{
  255 +
  256 + }).catch((e)=>{
  257 +
  258 + })
  259 +
  260 + }).catch((e)=>{
  261 + console.log('pause error',e);
  262 + })
  263 + */
  264 + pause(): Promise<void>;
  265 +
  266 + /**
  267 + * 关闭视频,不释放底层资源
  268 + @example
  269 + jessibuca.close();
  270 + */
  271 + close(): void;
  272 +
  273 + /**
  274 + * 关闭视频,释放底层资源
  275 + @example
  276 + jessibuca.destroy()
  277 + */
  278 + destroy(): void;
  279 +
  280 + /**
  281 + * 清理画布为黑色背景
  282 + @example
  283 + jessibuca.clearView()
  284 + */
  285 + clearView(): void;
  286 +
  287 + /**
  288 + * 播放视频
  289 + @example
  290 +
  291 + jessibuca.play('url').then(()=>{
  292 + console.log('play success')
  293 + }).catch((e)=>{
  294 + console.log('play error',e)
  295 + })
  296 + //
  297 + jessibuca.play()
  298 + */
  299 + play(url?: string): Promise<void>;
  300 +
  301 + /**
  302 + * 重新调整视图大小
  303 + */
  304 + resize(): void;
  305 +
  306 + /**
  307 + * 设置最大缓冲时长,单位秒,播放器会自动消除延迟。
  308 + *
  309 + * 等同于 `videoBuffer` 参数。
  310 + *
  311 + @example
  312 + // 设置 200ms 缓冲
  313 + jessibuca.setBufferTime(0.2)
  314 + */
  315 + setBufferTime(time: number): void;
  316 +
  317 + /**
  318 + * 设置旋转角度,只支持,0(默认) ,180,270 三个值。
  319 + *
  320 + * > 可用于实现监控画面小窗和全屏效果,由于iOS没有全屏API,此方法可以模拟页面内全屏效果而且多端效果一致。 *
  321 + @example
  322 + jessibuca.setRotate(0)
  323 +
  324 + jessibuca.setRotate(90)
  325 +
  326 + jessibuca.setRotate(270)
  327 + */
  328 + setRotate(deg: number): void;
  329 +
  330 + /**
  331 + *
  332 + * 设置音量大小,取值0 — 1
  333 + *
  334 + * > 区别于 mute 和 cancelMute 方法,虽然设置setVolume(0) 也能达到 mute方法,但是mute 方法是不调用底层播放音频的,能提高性能。而setVolume(0)只是把声音设置为0 ,以达到效果。
  335 + * @param volume 当为0时,完全无声;当为1时,最大音量,默认值
  336 + @example
  337 + jessibuca.setVolume(0.2)
  338 +
  339 + jessibuca.setVolume(0)
  340 +
  341 + jessibuca.setVolume(1)
  342 + */
  343 + setVolume(volume: number): void;
  344 +
  345 + /**
  346 + * 返回是否加载完毕
  347 + @example
  348 + var result = jessibuca.hasLoaded()
  349 + console.log(result) // true
  350 + */
  351 + hasLoaded(): boolean;
  352 +
  353 + /**
  354 + * 开启屏幕常亮,在手机浏览器上, canvas标签渲染视频并不会像video标签那样保持屏幕常亮。
  355 + * H5目前在chrome\edge 84, android chrome 84及以上有原生亮屏API, 需要是https页面
  356 + * 其余平台为模拟实现,此时为兼容实现,并不保证所有浏览器都支持
  357 + @example
  358 + jessibuca.setKeepScreenOn()
  359 + */
  360 + setKeepScreenOn(): boolean;
  361 +
  362 + /**
  363 + * 全屏(取消全屏)播放视频
  364 + @example
  365 + jessibuca.setFullscreen(true)
  366 + //
  367 + jessibuca.setFullscreen(false)
  368 + */
  369 + setFullscreen(flag: boolean): void;
  370 +
  371 + /**
  372 + *
  373 + * 截图,调用后弹出下载框保存截图
  374 + * @param filename 可选参数, 保存的文件名, 默认 `时间戳`
  375 + * @param format 可选参数, 截图的格式,可选png或jpeg或者webp ,默认 `png`
  376 + * @param quality 可选参数, 当格式是jpeg或者webp时,压缩质量,取值0 ~ 1 ,默认 `0.92`
  377 + * @param type 可选参数, 可选download或者base64或者blob,默认`download`
  378 +
  379 + @example
  380 +
  381 + jessibuca.screenshot("test","png",0.5)
  382 +
  383 + const base64 = jessibuca.screenshot("test","png",0.5,'base64')
  384 +
  385 + const fileBlob = jessibuca.screenshot("test",'blob')
  386 + */
  387 + screenshot(filename?: string, format?: string, quality?: number, type?: string): void;
  388 +
  389 + /**
  390 + * 开始录制。
  391 + * @param fileName 可选,默认时间戳
  392 + * @param fileType 可选,默认webm,支持webm 和mp4 格式
  393 +
  394 + @example
  395 + jessibuca.startRecord('xxx','webm')
  396 + */
  397 + startRecord(fileName: string, fileType: string): void;
  398 +
  399 + /**
  400 + * 暂停录制并下载。
  401 + @example
  402 + jessibuca.stopRecordAndSave()
  403 + */
  404 + stopRecordAndSave(): void;
  405 +
  406 + /**
  407 + * 返回是否正在播放中状态。
  408 + @example
  409 + var result = jessibuca.isPlaying()
  410 + console.log(result) // true
  411 + */
  412 + isPlaying(): boolean;
  413 +
  414 + /**
  415 + * 返回是否静音。
  416 + @example
  417 + var result = jessibuca.isMute()
  418 + console.log(result) // true
  419 + */
  420 + isMute(): boolean;
  421 +
  422 + /**
  423 + * 返回是否正在录制。
  424 + @example
  425 + var result = jessibuca.isRecording()
  426 + console.log(result) // true
  427 + */
  428 + isRecording(): boolean;
  429 +
  430 +
  431 + /**
  432 + * 监听 jessibuca 初始化事件
  433 + * @example
  434 + * jessibuca.on("load",function(){console.log('load')})
  435 + */
  436 + on(event: 'load', callback: () => void): void;
  437 +
  438 + /**
  439 + * 视频播放持续时间,单位ms
  440 + * @example
  441 + * jessibuca.on('timeUpdate',function (ts) {console.log('timeUpdate',ts);})
  442 + */
  443 + on(event: 'timeUpdate', callback: () => void): void;
  444 +
  445 + /**
  446 + * 当解析出视频信息时回调,2个回调参数
  447 + * @example
  448 + * jessibuca.on("videoInfo",function(data){console.log('width:',data.width,'height:',data.width)})
  449 + */
  450 + on(event: 'videoInfo', callback: (data: {
  451 + /** 视频宽 */
  452 + width: number;
  453 + /** 视频高 */
  454 + height: number;
  455 + }) => void): void;
  456 +
  457 + /**
  458 + * 当解析出音频信息时回调,2个回调参数
  459 + * @example
  460 + * jessibuca.on("audioInfo",function(data){console.log('numOfChannels:',data.numOfChannels,'sampleRate',data.sampleRate)})
  461 + */
  462 + on(event: 'audioInfo', callback: (data: {
  463 + /** 声频通道 */
  464 + numOfChannels: number;
  465 + /** 采样率 */
  466 + sampleRate: number;
  467 + }) => void): void;
  468 +
  469 + /**
  470 + * 信息,包含错误信息
  471 + * @example
  472 + * jessibuca.on("log",function(data){console.log('data:',data)})
  473 + */
  474 + on(event: 'log', callback: () => void): void;
  475 +
  476 + /**
  477 + * 错误信息
  478 + * @example
  479 + * jessibuca.on("error",function(error){
  480 + if(error === Jessibuca.ERROR.fetchError){
  481 + //
  482 + }
  483 + else if(error === Jessibuca.ERROR.webcodecsH265NotSupport){
  484 + //
  485 + }
  486 + console.log('error:',error)
  487 + })
  488 + */
  489 + on(event: 'error', callback: (err: Jessibuca.ERROR) => void): void;
  490 +
  491 + /**
  492 + * 当前网速, 单位KB 每秒1次,
  493 + * @example
  494 + * jessibuca.on("kBps",function(data){console.log('kBps:',data)})
  495 + */
  496 + on(event: 'kBps', callback: (value: number) => void): void;
  497 +
  498 + /**
  499 + * 渲染开始
  500 + * @example
  501 + * jessibuca.on("start",function(){console.log('start render')})
  502 + */
  503 + on(event: 'start', callback: () => void): void;
  504 +
  505 + /**
  506 + * 当设定的超时时间内无数据返回,则回调
  507 + * @example
  508 + * jessibuca.on("timeout",function(error){console.log('timeout:',error)})
  509 + */
  510 + on(event: 'timeout', callback: (error: Jessibuca.TIMEOUT) => void): void;
  511 +
  512 + /**
  513 + * 当play()的时候,如果没有数据返回,则回调
  514 + * @example
  515 + * jessibuca.on("loadingTimeout",function(){console.log('timeout')})
  516 + */
  517 + on(event: 'loadingTimeout', callback: () => void): void;
  518 +
  519 + /**
  520 + * 当播放过程中,如果超过timeout之后没有数据渲染,则抛出异常。
  521 + * @example
  522 + * jessibuca.on("delayTimeout",function(){console.log('timeout')})
  523 + */
  524 + on(event: 'delayTimeout', callback: () => void): void;
  525 +
  526 + /**
  527 + * 当前是否全屏
  528 + * @example
  529 + * jessibuca.on("fullscreen",function(flag){console.log('is fullscreen',flag)})
  530 + */
  531 + on(event: 'fullscreen', callback: () => void): void;
  532 +
  533 + /**
  534 + * 触发播放事件
  535 + * @example
  536 + * jessibuca.on("play",function(flag){console.log('play')})
  537 + */
  538 + on(event: 'play', callback: () => void): void;
  539 +
  540 + /**
  541 + * 触发暂停事件
  542 + * @example
  543 + * jessibuca.on("pause",function(flag){console.log('pause')})
  544 + */
  545 + on(event: 'pause', callback: () => void): void;
  546 +
  547 + /**
  548 + * 触发声音事件,返回boolean值
  549 + * @example
  550 + * jessibuca.on("mute",function(flag){console.log('is mute',flag)})
  551 + */
  552 + on(event: 'mute', callback: () => void): void;
  553 +
  554 + /**
  555 + * 流状态统计,流开始播放后回调,每秒1次。
  556 + * @example
  557 + * jessibuca.on("stats",function(s){console.log("stats is",s)})
  558 + */
  559 + on(event: 'stats', callback: (stats: {
  560 + /** 当前缓冲区时长,单位毫秒 */
  561 + buf: number;
  562 + /** 当前视频帧率 */
  563 + fps: number;
  564 + /** 当前音频码率,单位byte */
  565 + abps: number;
  566 + /** 当前视频码率,单位byte */
  567 + vbps: number;
  568 + /** 当前视频帧pts,单位毫秒 */
  569 + ts: number;
  570 + }) => void): void;
  571 +
  572 + /**
  573 + * 渲染性能统计,流开始播放后回调,每秒1次。
  574 + * @param performance 0: 表示卡顿,1: 表示流畅,2: 表示非常流程
  575 + * @example
  576 + * jessibuca.on("performance",function(performance){console.log("performance is",performance)})
  577 + */
  578 + on(event: 'performance', callback: (performance: 0 | 1 | 2) => void): void;
  579 +
  580 + /**
  581 + * 录制开始的事件
  582 +
  583 + * @example
  584 + * jessibuca.on("recordStart",function(){console.log("record start")})
  585 + */
  586 + on(event: 'recordStart', callback: () => void): void;
  587 +
  588 + /**
  589 + * 录制结束的事件
  590 +
  591 + * @example
  592 + * jessibuca.on("recordEnd",function(){console.log("record end")})
  593 + */
  594 + on(event: 'recordEnd', callback: () => void): void;
  595 +
  596 + /**
  597 + * 录制的时候,返回的录制时长,1s一次
  598 +
  599 + * @example
  600 + * jessibuca.on("recordingTimestamp",function(timestamp){console.log("recordingTimestamp is",timestamp)})
  601 + */
  602 + on(event: 'recordingTimestamp', callback: (timestamp: number) => void): void;
  603 +
  604 + /**
  605 + * 监听调用play方法 经过 初始化-> 网络请求-> 解封装 -> 解码 -> 渲染 一系列过程的时间消耗
  606 + * @param event
  607 + * @param callback
  608 + */
  609 + on(event: 'playToRenderTimes', callback: (times: {
  610 + playInitStart: number, // 1 初始化
  611 + playStart: number, // 2 初始化
  612 + streamStart: number, // 3 网络请求
  613 + streamResponse: number, // 4 网络请求
  614 + demuxStart: number, // 5 解封装
  615 + decodeStart: number, // 6 解码
  616 + videoStart: number, // 7 渲染
  617 + playTimestamp: number,// playStart- playInitStart
  618 + streamTimestamp: number,// streamStart - playStart
  619 + streamResponseTimestamp: number,// streamResponse - streamStart
  620 + demuxTimestamp: number, // demuxStart - streamResponse
  621 + decodeTimestamp: number, // decodeStart - demuxStart
  622 + videoTimestamp: number,// videoStart - decodeStart
  623 + allTimestamp: number // videoStart - playInitStart
  624 + }) => void): void
  625 +
  626 + /**
  627 + * 监听方法
  628 + *
  629 + @example
  630 +
  631 + jessibuca.on("load",function(){console.log('load')})
  632 + */
  633 + on(event: string, callback: Function): void;
  634 +
  635 +}
  636 +
  637 +export default Jessibuca;
... ...
public/js/jessibuca/jessibuca.js 0 → 100644
No preview for this file type
public/js/jessibuca/renderer.js 0 → 100644
  1 +!(function () {
  2 + /**
  3 + * @param opt
  4 + * container: DOM 容器
  5 + * contextOptions:
  6 + * videoBuffer:
  7 + * forceNoGL:
  8 + * isNotMute:
  9 + * decoder:
  10 + * @constructor
  11 + */
  12 + function Jessibuca(opt) {
  13 + this._opt = opt;
  14 +
  15 + if (typeof opt.container === "string") {
  16 + this._opt.container = document.getElementById(opt.container);
  17 + }
  18 + if (!this._opt.container) {
  19 + throw new Error('Jessibuca need container option');
  20 + return;
  21 + }
  22 +
  23 + this._canvasElement = document.createElement("canvas");
  24 + this._canvasElement.style.position = "absolute";
  25 + this._canvasElement.style.top = 0;
  26 + this._canvasElement.style.left = 0;
  27 + this._opt.container.appendChild(this._canvasElement);
  28 + this._container = this._opt.container;
  29 + this._container.style.overflow = "hidden";
  30 + this._containerOldPostion = {
  31 + position: this._container.style.position,
  32 + top: this._container.style.top,
  33 + left: this._container.style.left,
  34 + width: this._container.style.width,
  35 + height: this._container.style.height
  36 + }
  37 + if (this._containerOldPostion.position != "absolute") {
  38 + this._container.style.position = "relative"
  39 + }
  40 + this._opt.videoBuffer = opt.videoBuffer || 0;
  41 + this._opt.text = opt.text || '';
  42 + //
  43 + this._opt.isResize = opt.isResize === false ? opt.isResize : true;
  44 + this._opt.isFullResize = opt.isFullResize === true ? opt.isFullResize : false;
  45 + this._opt.isDebug = opt.debug === true;
  46 + this._opt.timeout = typeof opt.timeout === 'number' ? opt.timeout : 30;
  47 + this._opt.supportDblclickFullscreen = opt.supportDblclickFullscreen === true;
  48 + this._opt.showBandwidth = opt.showBandwidth === true;
  49 + this._opt.operateBtns = Object.assign({
  50 + fullscreen: false,
  51 + screenshot: false,
  52 + play: false,
  53 + audio: false
  54 + }, opt.operateBtns || {});
  55 + this._opt.keepScreenOn = opt.keepScreenOn === true;
  56 +
  57 + if (!opt.forceNoGL) this._initContextGL();
  58 + this._audioContext = new (window.AudioContext || window.webkitAudioContext)();
  59 + this._audioEnabled(true);
  60 + if (!opt.isNotMute) this._audioEnabled(false);
  61 + if (this._contextGL) {
  62 + this._initProgram();
  63 + this._initBuffers();
  64 + this._initTextures();
  65 + }
  66 + this._onresize = () => this.resize();
  67 + this._onfullscreenchange = () => this._fullscreenchange();
  68 + window.addEventListener("resize", this._onresize);
  69 + document.addEventListener('fullscreenchange', this._onfullscreenchange);
  70 + this._decoderWorker = new Worker(opt.decoder || 'ff.js')
  71 + var _this = this;
  72 + this._hasLoaded = false;
  73 + this._stats = {
  74 + buf: 0,
  75 + fps: 0,
  76 + abps: '',
  77 + vbps: '',
  78 + ts: ''
  79 + };
  80 +
  81 + if (this._opt.supportDblclickFullscreen) {
  82 + this._canvasElement.addEventListener('dblclick', function () {
  83 + _this.fullscreen = !_this.fullscreen;
  84 + }, false);
  85 + }
  86 + this.onPlay = noop;
  87 + this.onPause = noop;
  88 + this.onRecord = noop;
  89 + this.onFullscreen = noop;
  90 + this.onMute = noop;
  91 + this.onLoad = noop;
  92 + this.onLog = noop;
  93 + this.onError = noop;
  94 + this.onTimeUpdate = noop;
  95 + this.onInitSize = noop;
  96 + this._onMessage();
  97 + this._initDom();
  98 + this._initStatus();
  99 + this._initEventListener();
  100 + this._hideBtns();
  101 + //
  102 + this._initWakeLock();
  103 + this._enableWakeLock();
  104 + };
  105 +
  106 + function noop() {
  107 +
  108 + }
  109 +
  110 + Jessibuca.prototype._initDom = function () {
  111 + var playBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAYAAABPYyMiAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAAASAAAAEgARslrPgAAARVJREFUSMe9laEOglAUhs+5k9lJFpsJ5QWMJoNGbEY0mEy+gr6GNo0a3SiQCegMRILzGdw4hl+Cd27KxPuXb2zA/91z2YXoGRERkX4fvN3A2QxUiv4dFM3n8jZRBLbbVfd+ubJuF4xjiCyXkksueb1uSKCIZYGLBTEx8ekEoV7PkICeVgs8HiGyXoO2bUigCDM4HoPnM7bI8wwJ6Gk0sEXbLSay30Oo2TQkoGcwgFCSQMhxDAvoETEscDiQkJC4LjMz8+XyZ4HrFYWjEQqHQ1asWGWZfmdFAsVINxuw00HhbvfpydpvxWkKTqdYaRCUfUPJCdzv4Gr1uqfli0tOIAzByUT/iCrL6+84y3Bw+D6ui5Ou+jwA8FnIO++FACgAAAAldEVYdGRhdGU6Y3JlYXRlADIwMjEtMDEtMDhUMTY6NDI6NTMrMDg6MDCKP7wnAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIxLTAxLTA4VDE2OjQyOjUzKzA4OjAw+2IEmwAAAEl0RVh0c3ZnOmJhc2UtdXJpAGZpbGU6Ly8vaG9tZS9hZG1pbi9pY29uLWZvbnQvdG1wL2ljb25fZ2Y3MDBzN2IzZncvYm9mYW5nLnN2Z8fICi0AAAAASUVORK5CYII=';
  112 + var pauseBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAYAAABPYyMiAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAAASAAAAEgARslrPgAAAHVJREFUSMftkCESwCAMBEOnCtdXVMKHeC7oInkEeQJXkRoEZWraipxZc8lsQqQZBACAlIS1oqGhhTCdu3oyxyyMcdRf79c5J7SWDBky+z4173rbJvR+VF/e/qwKqIAKqMBDgZyFzAQCoZTpxq7HLDyOrw/9b07l3z4dDnI2IAAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMS0wMS0wOFQxNjo0Mjo1MyswODowMIo/vCcAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjEtMDEtMDhUMTY6NDI6NTMrMDg6MDD7YgSbAAAASnRFWHRzdmc6YmFzZS11cmkAZmlsZTovLy9ob21lL2FkbWluL2ljb24tZm9udC90bXAvaWNvbl9nZjcwMHM3YjNmdy96YW50aW5nLnN2ZxqNZJkAAAAASUVORK5CYII=';
  113 + var screenshotBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAYAAABPYyMiAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAAASAAAAEgARslrPgAAAaxJREFUSMfNlLFOAkEQhmevAZMjR6OGRBJKsFBzdkYNpYSaWkopIOFRCBWh1ieA+ALGRgutjK0HzV2H5SX7W/zsmY3cnTEhcZovOzcz9+/s7Ir8d4OGht7fBwAgjvEri2OTl1ffSf0xAMBxRIkS1e3Se3+vcszEMe/6OqmT/aN2m1wsNu/o5YVsNHI7BgA4PCRfXzfXCwKy1RLbcXZG9nrkzc12jvT8nPU/PtatOThgAx8fuS4WyZ0de2e+T87n5OcnuVqRsxl5cpImQDnKUc7DA1fVqpimZCu+vCSjiNH9PlmpJNTQ0INBErfeafZRAakC6FWKfH9nwU7H/l6rGdqCOx3y7c3U+aOARsMMp+1vNskwTLjulB23XJL1epqA9OshIiKeJxAIoug7UyA4OuLi6Ynr52deu+NjOy4MSc9Ln8rMDpTLybBpaOjdXbJUIqdTm8a/t2fn/RSQewR24HicTLmGhnbdzcPquvYtGY3+PIR24UKBUXd35v6Sk4lN47+9NXm/FBAEedfGTjw9JYdDm76fm6+hoS8ujGAxT6L9Im7bTKeurvIEb92+AES1b6x283XSAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDIxLTAxLTA4VDE2OjQyOjUzKzA4OjAwij+8JwAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyMS0wMS0wOFQxNjo0Mjo1MyswODowMPtiBJsAAABJdEVYdHN2ZzpiYXNlLXVyaQBmaWxlOi8vL2hvbWUvYWRtaW4vaWNvbi1mb250L3RtcC9pY29uX2dmNzAwczdiM2Z3L2NhbWVyYS5zdmeyubWEAAAAAElFTkSuQmCC';
  114 + var fullscreenBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAYAAABPYyMiAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAAASAAAAEgARslrPgAAALZJREFUSMftVbsORUAQVSj8DomChvh3lU5CoSVCQq2RObeYu8XG3deVoHCak81kds7Oaz3vxRcAAMwztOg6vX9d6/3XFQQC+b7iAoFhYE7Tvx9EIFAcy/ftO3MQGAQkCfM4MmeZWyajiLnvmYuCeduMAuSzvRBVYNluFHCssSgFp7Sq9ALKkjnPf9ubRtkDL27HNT3QtsY9cAjsNAVheHIKBOwD2wpxFHDbJpwmaHH2L1iWx+2BDy8RbXXtqbRBAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDIxLTAxLTA4VDE2OjQyOjUzKzA4OjAwij+8JwAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyMS0wMS0wOFQxNjo0Mjo1MyswODowMPtiBJsAAABTdEVYdHN2ZzpiYXNlLXVyaQBmaWxlOi8vL2hvbWUvYWRtaW4vaWNvbi1mb250L3RtcC9pY29uX2dmNzAwczdiM2Z3L3F1YW5waW5nenVpZGFodWEuc3ZnTBoI7AAAAABJRU5ErkJggg==';
  115 + var minScreenBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAYAAABPYyMiAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAAASAAAAEgARslrPgAAAYJJREFUSMfdVbGKwkAQnQn+geAfWBixUTsVgp3YGKxSWflVNmIjARULwc5KO40ipNHWRgs/wGLniucKa+Jd5ODuuGle5u3szGRmd5bor4iIiMhuB3Sc+HXXBdp2/Lpta7v4dccRJUrUdhtNQIkSVa3C8HwG1uumg34f2OnEB+h0tF1Sv5b+YIsttpZLEhKSdhvscPi8IXFF74GJiYnHY7Cex8zMvFgkbInjmJnv98kqoO30vmhLtaRMB60WtEbDNDudgMUiKiQSzfjOMzFxoQAyCPSfw7/nQZ/PUYnpNGV6OR6BmYzJbzYIoBQCzGaRBDQvJCTdLnTLolg5HN5t6f8V1h/oUT4PrVKJWBotmEzQw+vV3J9Ow851P2/BaoX9Yfh0BrJZYKlk8uUyHOpDeLuBHwzMBJtN2PV6IPUhXK9Nf5cLMAxfluanrmGkRBggtRo03wfq66P/6CsJAnOg+f6rgfZI4BGYiYlHIx048eR6krcnq34kkj1GuVz8+jceo9+SD5A8yGh8CTq7AAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDIxLTAxLTA4VDE2OjQyOjUzKzA4OjAwij+8JwAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyMS0wMS0wOFQxNjo0Mjo1MyswODowMPtiBJsAAABNdEVYdHN2ZzpiYXNlLXVyaQBmaWxlOi8vL2hvbWUvYWRtaW4vaWNvbi1mb250L3RtcC9pY29uX2dmNzAwczdiM2Z3L3p1aXhpYW9odWEuc3ZnoCFr0AAAAABJRU5ErkJggg==';
  116 + var quietBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAYAAABPYyMiAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAAASAAAAEgARslrPgAAAR9JREFUSMfVlD0LglAYhe9VkwgNihpsjbYQf4JTS7+iuaGxpcGfJjS0NFRLk2NDi6MogafhJGRIX9yEzvJwrx/nvPd9VYh/F3LkyBuN2g3J1QoAgCQhPe/Hxq5Lo+0WlfJ9dYYAgGaTDAIyy/BUnwcwWJlhcLnZkN2ugIBAuy2kkEL2ep8F73S4kjfFcfn6cMj9KLodrWVBiXyf75tMyOOR+4MBOZ8XLXzorboA5UpnM/J0Ivd7+vX7xX2asqGpVKtFXi5sqWmypXefrfIWAACmU/JwKCoun8hu9zA0uk6u13wgirg+n7+bAcsibbt6SB3n9TQXPxwAwHJJpum7M6BcDDQa0SgMaw9QPkJNIxcLMo4ZcDz+eYDqQFLWbqxKV57EtW1WtMbmAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDIxLTAxLTA4VDE2OjQyOjUzKzA4OjAwij+8JwAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyMS0wMS0wOFQxNjo0Mjo1MyswODowMPtiBJsAAABKdEVYdHN2ZzpiYXNlLXVyaQBmaWxlOi8vL2hvbWUvYWRtaW4vaWNvbi1mb250L3RtcC9pY29uX2dmNzAwczdiM2Z3L2ppbmd5aW4uc3ZnIlMYaQAAAABJRU5ErkJggg==';
  117 + var playAudioBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAYAAABPYyMiAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAAASAAAAEgARslrPgAAAU5JREFUSMftkzGKwlAURf9PULBQwULSCKK1bZAgNuoaFFyAC3AdZg0uQCwshWzAShEEO7Gy0soUCu9Occ3An5nMGCfdzGsO7+Xy3/03iVL/lbAAACiVIBCI77O37Vi9QCDZbEqLm03ycEBUAoHk818v7nYpul5Jz4tf8HBKYa1mcjwmbzd8rG8NFIsU7ffk8UjmcjE3XK+RtB4G2PT75GbDeblMttumfjSKMRCGLxsQCKTReE9KIJDJxDw/SmKxiOZWWh+ntrSlre2WXRAorbTSrZapip7X66kbMKtQUFBQCENznsmQ93vqBhh5r8fO85jAcsnIrcce1yV3uxgD8zl5uZgU+dGBVlrp6GbTKRPwffaDAek45Gz2/M0AAJ0OeTol+w0rFYrOZ3K1MhNJEjEAwHF4cBA8Z8B1zcXV6msv+JMR2yaHQ1LrXx/8Z+sNRxsWcwZeb6UAAAAldEVYdGRhdGU6Y3JlYXRlADIwMjEtMDEtMDhUMTY6NDI6NTMrMDg6MDCKP7wnAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIxLTAxLTA4VDE2OjQyOjUzKzA4OjAw+2IEmwAAAEt0RVh0c3ZnOmJhc2UtdXJpAGZpbGU6Ly8vaG9tZS9hZG1pbi9pY29uLWZvbnQvdG1wL2ljb25fZ2Y3MDBzN2IzZncvc2hlbmd5aW4uc3ZnFog1MQAAAABJRU5ErkJggg==';
  118 + var recordBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAYAAABPYyMiAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAAASAAAAEgARslrPgAAAPRJREFUSMflVDEOwjAQO0e8gr2sZYVunREbD6ISfAgmkBjpC/hBEQ+AtTWD6QAI0gBlqRfLp+TiXC5n1nXgMUCS5HBoNBqj6IOMMFwuEpsNAABl6d3HihWrOJaBsuRPkGW+c929HAxuYefb6L+R0ZgkMrJYiItCnCT1sl5Y1jwXj0bNniJNJWqujfX7LyrwJh8AYDxWgulU0dPp20IFlxoODm61kpE4VnS9/puBXyPYgH7LbKY3PhwUnUw+NdC4CdW9+71UgyZspwIBB9No3O0klktxUahyx+Pz+lYG0Xzu84lXRqTqwRQAGAzns8R223gUdxZXGcAK5Hp0ClIAAAAldEVYdGRhdGU6Y3JlYXRlADIwMjEtMDEtMDhUMTY6NDI6NTMrMDg6MDCKP7wnAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIxLTAxLTA4VDE2OjQyOjUzKzA4OjAw+2IEmwAAAE50RVh0c3ZnOmJhc2UtdXJpAGZpbGU6Ly8vaG9tZS9hZG1pbi9pY29uLWZvbnQvdG1wL2ljb25fZ2Y3MDBzN2IzZncvbHV6aGlzaGlwaW4uc3Zn5Zd7GQAAAABJRU5ErkJggg==';
  119 + var recordingBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAYAAABPYyMiAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAAASAAAAEgARslrPgAAAahJREFUSMdjYBjpgBFd4NZK+f+soQYG//T+yzFuUFUl2cApjEWM/758UZvysPDn3127GBkZGBgY/v4l6ICb9xTWsRbp6/9f9W8N44Jz5xgCGI4wfGFiIttrR/5n/3/U3KyR8rj8t0RdHS5lcAv+//yXzzhZTY1ii2FAmsGZocna+maD3GnWY62tNzbJBbDOffLkxie5eJYwa2uYMhaigzb2/zyGguPH/y9mTGKYYGlJUIMiYxDjHCen/4oMDAxznJzg4k8Z/jP+l5LCCAFCQP30Y5dfXVZWDI7/zzIs8PNjNGJ4/7/r+XNKA4rkoNZ4/lj0V9TmzUxJv0J+F+jrM3YyvPq/acsWujmA2oBkB9y4LifLxhoa+teAzYFtwtWr/8sZxBj9fHxo7oCbprJ72MqOHWNgZGBkYFy1isGGoZahTFSU0hAgOhcQnfph4P7/df9T9u1jPMn4nyHmxIn/bAzLGe7GxTHsZyj+f+zpUwYGBmmG6bQsiMr+L/v/rqlJY9Njm9889fW4lGEUxXCHwAomUgH3vxBG8c+f1WWf9P98sns3oaJ4FAAAbtWqHTT84QYAAAAldEVYdGRhdGU6Y3JlYXRlADIwMjEtMDEtMDhUMTY6MzU6MjMrMDg6MDBLHbvEAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIxLTAxLTA4VDE2OjM1OjIzKzA4OjAwOkADeAAAAE50RVh0c3ZnOmJhc2UtdXJpAGZpbGU6Ly8vaG9tZS9hZG1pbi9pY29uLWZvbnQvdG1wL2ljb25fcTM1YTFhNHBtY2MvbHV6aGlzaGlwaW4uc3Zn6xlv1QAAAABJRU5ErkJggg==';
  120 + var gifBase64 = 'data:image/gif;base64,R0lGODlhgACAAKIAAP///93d3bu7u5mZmQAA/wAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQFBQAEACwCAAIAfAB8AAAD/0i63P4wygYqmDjrzbtflvWNZGliYXiubKuloivPLlzReD7al+7/Eh5wSFQIi8hHYBkwHUmD6CD5YTJLz49USuVYraRsZ7vtar7XnQ1Kjpoz6LRHvGlz35O4nEPP2O94EnpNc2sef1OBGIOFMId/inB6jSmPdpGScR19EoiYmZobnBCIiZ95k6KGGp6ni4wvqxilrqBfqo6skLW2YBmjDa28r6Eosp27w8Rov8ekycqoqUHODrTRvXsQwArC2NLF29UM19/LtxO5yJd4Au4CK7DUNxPebG4e7+8n8iv2WmQ66BtoYpo/dvfacBjIkITBE9DGlMvAsOIIZjIUAixliv9ixYZVtLUos5GjwI8gzc3iCGghypQqrbFsme8lwZgLZtIcYfNmTJ34WPTUZw5oRxdD9w0z6iOpO15MgTh1BTTJUKos39jE+o/KS64IFVmsFfYT0aU7capdy7at27dw48qdS7eu3bt480I02vUbX2F/JxYNDImw4GiGE/P9qbhxVpWOI/eFKtlNZbWXuzlmG1mv58+gQ4seTbq06dOoU6vGQZJy0FNlMcV+czhQ7SQmYd8eMhPs5BxVdfcGEtV3buDBXQ+fURxx8oM6MT9P+Fh6dOrH2zavc13u9JXVJb520Vp8dvC76wXMuN5Sepm/1WtkEZHDefnzR9Qvsd9+/wi8+en3X0ntYVcSdAE+UN4zs7ln24CaLagghIxBaGF8kFGoIYV+Ybghh841GIyI5ICIFoklJsigihmimJOLEbLYIYwxSgigiZ+8l2KB+Ml4oo/w8dijjcrouCORKwIpnJIjMnkkksalNeR4fuBIm5UEYImhIlsGCeWNNJphpJdSTlkml1jWeOY6TnaRpppUctcmFW9mGSaZceYopH9zkjnjUe59iR5pdapWaGqHopboaYua1qije67GJ6CuJAAAIfkEBQUABAAsCgACAFcAMAAAA/9Iutz+ML5Ag7w46z0r5WAoSp43nihXVmnrdusrv+s332dt4Tyo9yOBUJD6oQBIQGs4RBlHySSKyczVTtHoidocPUNZaZAr9F5FYbGI3PWdQWn1mi36buLKFJvojsHjLnshdhl4L4IqbxqGh4gahBJ4eY1kiX6LgDN7fBmQEJI4jhieD4yhdJ2KkZk8oiSqEaatqBekDLKztBG2CqBACq4wJRi4PZu1sA2+v8C6EJexrBAD1AOBzsLE0g/V1UvYR9sN3eR6lTLi4+TlY1wz6Qzr8u1t6FkY8vNzZTxaGfn6mAkEGFDgL4LrDDJDyE4hEIbdHB6ESE1iD4oVLfLAqPETIsOODwmCDJlv5MSGJklaS6khAQAh+QQFBQAEACwfAAIAVwAwAAAD/0i63P5LSAGrvTjrNuf+YKh1nWieIumhbFupkivPBEzR+GnnfLj3ooFwwPqdAshAazhEGUXJJIrJ1MGOUamJ2jQ9QVltkCv0XqFh5IncBX01afGYnDqD40u2z76JK/N0bnxweC5sRB9vF34zh4gjg4uMjXobihWTlJUZlw9+fzSHlpGYhTminKSepqebF50NmTyor6qxrLO0L7YLn0ALuhCwCrJAjrUqkrjGrsIkGMW/BMEPJcphLgDaABjUKNEh29vdgTLLIOLpF80s5xrp8ORVONgi8PcZ8zlRJvf40tL8/QPYQ+BAgjgMxkPIQ6E6hgkdjoNIQ+JEijMsasNY0RQix4gKP+YIKXKkwJIFF6JMudFEAgAh+QQFBQAEACw8AAIAQgBCAAAD/kg0PPowykmrna3dzXvNmSeOFqiRaGoyaTuujitv8Gx/661HtSv8gt2jlwIChYtc0XjcEUnMpu4pikpv1I71astytkGh9wJGJk3QrXlcKa+VWjeSPZHP4Rtw+I2OW81DeBZ2fCB+UYCBfWRqiQp0CnqOj4J1jZOQkpOUIYx/m4oxg5cuAaYBO4Qop6c6pKusrDevIrG2rkwptrupXB67vKAbwMHCFcTFxhLIt8oUzLHOE9Cy0hHUrdbX2KjaENzey9Dh08jkz8Tnx83q66bt8PHy8/T19vf4+fr6AP3+/wADAjQmsKDBf6AOKjS4aaHDgZMeSgTQcKLDhBYPEswoA1BBAgAh+QQFBQAEACxOAAoAMABXAAAD7Ei6vPOjyUkrhdDqfXHm4OZ9YSmNpKmiqVqykbuysgvX5o2HcLxzup8oKLQQix0UcqhcVo5ORi+aHFEn02sDeuWqBGCBkbYLh5/NmnldxajX7LbPBK+PH7K6narfO/t+SIBwfINmUYaHf4lghYyOhlqJWgqDlAuAlwyBmpVnnaChoqOkpaanqKmqKgGtrq+wsbA1srW2ry63urasu764Jr/CAb3Du7nGt7TJsqvOz9DR0tPU1TIA2ACl2dyi3N/aneDf4uPklObj6OngWuzt7u/d8fLY9PXr9eFX+vv8+PnYlUsXiqC3c6PmUUgAACH5BAUFAAQALE4AHwAwAFcAAAPpSLrc/m7IAau9bU7MO9GgJ0ZgOI5leoqpumKt+1axPJO1dtO5vuM9yi8TlAyBvSMxqES2mo8cFFKb8kzWqzDL7Xq/4LB4TC6bz1yBes1uu9uzt3zOXtHv8xN+Dx/x/wJ6gHt2g3Rxhm9oi4yNjo+QkZKTCgGWAWaXmmOanZhgnp2goaJdpKGmp55cqqusrZuvsJays6mzn1m4uRAAvgAvuBW/v8GwvcTFxqfIycA3zA/OytCl0tPPO7HD2GLYvt7dYd/ZX99j5+Pi6tPh6+bvXuTuzujxXens9fr7YPn+7egRI9PPHrgpCQAAIfkEBQUABAAsPAA8AEIAQgAAA/lIutz+UI1Jq7026h2x/xUncmD5jehjrlnqSmz8vrE8u7V5z/m5/8CgcEgsGo/IpHLJbDqf0Kh0ShBYBdTXdZsdbb/Yrgb8FUfIYLMDTVYz2G13FV6Wz+lX+x0fdvPzdn9WeoJGAYcBN39EiIiKeEONjTt0kZKHQGyWl4mZdREAoQAcnJhBXBqioqSlT6qqG6WmTK+rsa1NtaGsuEu6o7yXubojsrTEIsa+yMm9SL8osp3PzM2cStDRykfZ2tfUtS/bRd3ewtzV5pLo4eLjQuUp70Hx8t9E9eqO5Oku5/ztdkxi90qPg3x2EMpR6IahGocPCxp8AGtigwQAIfkEBQUABAAsHwBOAFcAMAAAA/9Iutz+MMo36pg4682J/V0ojs1nXmSqSqe5vrDXunEdzq2ta3i+/5DeCUh0CGnF5BGULC4tTeUTFQVONYAs4CfoCkZPjFar83rBx8l4XDObSUL1Ott2d1U4yZwcs5/xSBB7dBMBhgEYfncrTBGDW4WHhomKUY+QEZKSE4qLRY8YmoeUfkmXoaKInJ2fgxmpqqulQKCvqRqsP7WooriVO7u8mhu5NacasMTFMMHCm8qzzM2RvdDRK9PUwxzLKdnaz9y/Kt8SyR3dIuXmtyHpHMcd5+jvWK4i8/TXHff47SLjQvQLkU+fG29rUhQ06IkEG4X/Rryp4mwUxSgLL/7IqFETB8eONT6ChCFy5ItqJomES6kgAQAh+QQFBQAEACwKAE4AVwAwAAAD/0i63A4QuEmrvTi3yLX/4MeNUmieITmibEuppCu3sDrfYG3jPKbHveDktxIaF8TOcZmMLI9NyBPanFKJp4A2IBx4B5lkdqvtfb8+HYpMxp3Pl1qLvXW/vWkli16/3dFxTi58ZRcChwIYf3hWBIRchoiHiotWj5AVkpIXi4xLjxiaiJR/T5ehoomcnZ+EGamqq6VGoK+pGqxCtaiiuJVBu7yaHrk4pxqwxMUzwcKbyrPMzZG90NGDrh/JH8t72dq3IN1jfCHb3L/e5ebh4ukmxyDn6O8g08jt7tf26ybz+m/W9GNXzUQ9fm1Q/APoSWAhhfkMAmpEbRhFKwsvCsmosRIHx444PoKcIXKkjIImjTzjkQAAIfkEBQUABAAsAgA8AEIAQgAAA/VIBNz+8KlJq72Yxs1d/uDVjVxogmQqnaylvkArT7A63/V47/m2/8CgcEgsGo/IpHLJbDqf0Kh0Sj0FroGqDMvVmrjgrDcTBo8v5fCZki6vCW33Oq4+0832O/at3+f7fICBdzsChgJGeoWHhkV0P4yMRG1BkYeOeECWl5hXQ5uNIAOjA1KgiKKko1CnqBmqqk+nIbCkTq20taVNs7m1vKAnurtLvb6wTMbHsUq4wrrFwSzDzcrLtknW16tI2tvERt6pv0fi48jh5h/U6Zs77EXSN/BE8jP09ZFA+PmhP/xvJgAMSGBgQINvEK5ReIZhQ3QEMTBLAAAh+QQFBQAEACwCAB8AMABXAAAD50i6DA4syklre87qTbHn4OaNYSmNqKmiqVqyrcvBsazRpH3jmC7yD98OCBF2iEXjBKmsAJsWHDQKmw571l8my+16v+CweEwum8+hgHrNbrvbtrd8znbR73MVfg838f8BeoB7doN0cYZvaIuMjY6PkJGSk2gClgJml5pjmp2YYJ6dX6GeXaShWaeoVqqlU62ir7CXqbOWrLafsrNctjIDwAMWvC7BwRWtNsbGFKc+y8fNsTrQ0dK3QtXAYtrCYd3eYN3c49/a5NVj5eLn5u3s6e7x8NDo9fbL+Mzy9/T5+tvUzdN3Zp+GBAAh+QQJBQAEACwCAAIAfAB8AAAD/0i63P4wykmrvTjrzbv/YCiOZGmeaKqubOu+cCzPdArcQK2TOL7/nl4PSMwIfcUk5YhUOh3M5nNKiOaoWCuWqt1Ou16l9RpOgsvEMdocXbOZ7nQ7DjzTaeq7zq6P5fszfIASAYUBIYKDDoaGIImKC4ySH3OQEJKYHZWWi5iZG0ecEZ6eHEOio6SfqCaqpaytrpOwJLKztCO2jLi1uoW8Ir6/wCHCxMG2x7muysukzb230M6H09bX2Nna29zd3t/g4cAC5OXm5+jn3Ons7eba7vHt2fL16tj2+QL0+vXw/e7WAUwnrqDBgwgTKlzIsKHDh2gGSBwAccHEixAvaqTYcFCjRoYeNyoM6REhyZIHT4o0qPIjy5YTTcKUmHImx5cwE85cmJPnSYckK66sSAAj0aNIkypdyrSp06dQo0qdSrWq1atYs2rdyrWr169gwxZJAAA7';
  121 + var playBigBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwEAYAAAAHkiXEAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAAASAAAAEgARslrPgAAByBJREFUeNrlXFlIVV0U3vsaaINmZoX0YAR6y8oGMkKLoMESSjBoUJEoIogoIggigoryIQoKGqi3Roh6TKGBIkNEe6hMgzTNKLPSUlMrNdvrf/juurlP5zpc7znb+r+X755pn7W+Pe+9zpVimIEUKVKJiUIKKWRqKs5OmwZOTBQkSFBUFK5HR+tPt7WBOzpwX3U1jquqwGVleK6iQkoppSQy7a8xEBERLVwIPnsWXF9PrqCxEXzxInjpUrDH47YO0h2hw8JwtG4deN8+8OzZA0vl7Vt/iZZCCtnUhPPt7fp9o0fjvpgYHHu9uD8+Hsdsh52hggTV1uLg2DHwpUvSIz3S093ttE4hB5qSxYuRAc+f910im5vBFy6As7LALORQ7RgzBullZIBPngQ3NPRt1+vXeH7NGtN69u8oERFFRIDPnQMrZe8YZ0huLhwMDzdjb1gYC4zj4uKAeaFIkbpxAwfWvse48FOngp89s7eeS1p2Nlg63vQF7Y8iRWrlSthZXR2wZhAR0dy55gwlIqI5c8AfPtgbeuUKHIqKMi3soP3z1UzwiRP2NbqtDbxsmXuGacK3tOgG/fwJ3rbNtIDO+J2ZiQzp6ND97uzE+RUrHDaAmxprif/+HQasXm1aKKcBPxcsADc1/VEjFClS8+eH7oXcuSpSpJ480V/Y0wPOyjItjNtgofWmiPHuHa7Hxg79RUT0e1Rjxb/X1ASnDw9vf/3S9bl1K/iEFSlSixbZdz7Xr5t2fLgBuuTn2xfUjRsHmVBYGNg6gWpo+FtHNU4DuowYAZ3Ky+11GzOm/4SIiGjDBvuczM52zAHua4iI6OpVcGEheO1a8PCdP/j9CNRyKFKk9u4doBDWCRXXBOcE0GekgVBUhPuSk00LPTAdCwp0+3n0GBER4AFenbQiJ8cdg7dvpwGB5xunT4PHjTMtuL0/qan29q9fH+AB62jnyxe31moGlwFWNDbCzq1bcez+snLffr14odtrMzrCBet6/Pnz7hoabAZY8fgxT5iGRwbs36/b19kJHjnS49+BEkIIMXmy/vjt26YdCA4pKdgHKC2Fo5cvh2xiFBTu3NGPw8Ox/5CW5tG3/hi8VffokRmDQwUeNOTlwc/KSmRIbq67djx9Cm5p+W2akEKmpfnaSt5zZdTXY8+0udmQcg5h0iQwD3MfPgRPn+7UG6GjUjiqrNSver0eVIWEBP85EiSIN7H/dSxZAuY1roMHHRt02OqamOhrgnoN46SQQn76ZFoad8Hj8kOH4D/PZJOSQvYKW11jYnxNkHWK3NFhWhKz8HrB9+7xaCU06fYKIiBBgiIjfRlgHTf/j+NlNMTFgceOHXJSJEgQ9wXCVyOk9AlvLfEDWDT6X+DAAXSiHz8OOSkppJCRkfrJ9vYR+NHaql8wNV42jVevUFJ37kQ8kHX8PlRMmOD/SYIEtbZ69IAkvsATs38dP36ADx8GJyc7IzyD+xbhqxE1Nb4a8PKlfiE+HsOxyEgYZI1A+9tRUADetQtNTF2dU29CJ84Twhkz9KtVVb4+oKxMvxAWxjM101KFBvX1qNmbNkHwNWucFl4HT/QmTvSfIkGCSks9HC2MsxxzyTekp5uWLjh0dYHz88FeL2ry5ctm7LHq2NMD7rXUg6rC0cKM9+/BfQS1hghDXg1VpEjdvasvLpqHf3VWs/P+/QA3Lltm75jz8T7BZQAvn9tscJgWXpEiNWuWvd2bNwcQwONbnq6p0R8oLnYnA7Zs6Vvw7m7Yd/z4gDe5DQH2Xrum29/SwoObfh7cts1egFWrnDU4Lg785g2Ytx4LC2H4zJmmhe3XD5+dsJsD1xhHjgwwgfBwPFBXpydQXe3uFqXzfU9o7ZUSXFRkX/IHMcENGKXgixY27fBwA8TZudO+5dixY4gJ37xpyQVfvEtmpmnHTQMFMiUFevBeL6OkZMg1GQlER4P5wwTGt29g65bmvw/4HShanD+5mjIlxC+cNw/cKxqYw7RDHZY9TOEXXpEiVVurC8+jtJUrnTNAkSK1fDle2NWlG9DeDs7IMC2UM35zU2Mt8Urhel6eywalp+vCMzhM++hRDlo1LeCg/dNGNdy5Wtt4LvEuCv+HodqHCu/e2Y8Cyss5aNW0sAPzh8fx1uEkgyMGHWxqgjM8NhYGWoNSraMnvm6+89aXDHjmap1AMUpKcD9/+D2MAYNzcsD9fRDNsZMcwsedfehiPJFeUhJ4925wWVnfdvFHiDt2gEM/MXT+rwp47UMKKeT27Ti7Zw+YA6UCgbdKKyr8cTVSSCEbG3Ge/5yDwWtD48fjfv6rAl7C6LUeb4uvX8FnzuD5U6ewjP35s9M6uQaUJP4Qgz8E4SbJ2sk5BV5jevAAvHmzqS9/hs0XJxBi1CgOWtVjVnlHKSEB16Oj/wgoE0L8LsFcM169AldV8Q4UjouKULKtNch9/AdsEf6XQYgIsAAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMS0wMS0xMlQxMTo1NjowNSswODowMGcMj/QAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjEtMDEtMTJUMTE6NTY6MDUrMDg6MDAWUTdIAAAASXRFWHRzdmc6YmFzZS11cmkAZmlsZTovLy9ob21lL2FkbWluL2ljb24tZm9udC90bXAvaWNvbl9wZHMzeWYxNGczYi9ib2Zhbmcuc3Zn11us5wAAAABJRU5ErkJggg==';
  122 +
  123 + function _setStyle(dom, cssObj) {
  124 + Object.keys(cssObj).forEach(function (key) {
  125 + dom.style[key] = cssObj[key];
  126 + })
  127 + }
  128 +
  129 + var doms = {};
  130 +
  131 + var fragment = document.createDocumentFragment();
  132 + var btnWrap = document.createElement('div');
  133 + var control1 = document.createElement('div');
  134 + var control2 = document.createElement('div');
  135 + var textDom = document.createElement('div');
  136 + var speedDom = document.createElement('div');
  137 + var playDom = document.createElement('div');
  138 + var playBigDom = document.createElement('div');
  139 + var pauseDom = document.createElement('div');
  140 + var screenshotsDom = document.createElement('div');
  141 + var fullscreenDom = document.createElement('div');
  142 + var minScreenDom = document.createElement('div');
  143 + var loadingDom = document.createElement('div');
  144 + var loadingTextDom = document.createElement('div');
  145 + var quietAudioDom = document.createElement('div');
  146 + var playAudioDom = document.createElement('div');
  147 + var recordDom = document.createElement('div');
  148 + var recordingDom = document.createElement('div');
  149 + var bgDom = document.createElement('div');
  150 +
  151 + loadingTextDom.innerText = this._opt.loadingText || '';
  152 + textDom.innerText = this._opt.text || '';
  153 + speedDom.innerText = '';
  154 + playDom.title = '播放';
  155 + pauseDom.title = '暂停';
  156 + screenshotsDom.title = '截屏';
  157 + fullscreenDom.title = '全屏';
  158 + minScreenDom.title = '退出全屏';
  159 + quietAudioDom.title = '静音';
  160 + playAudioDom.title = '取消静音';
  161 + recordDom.title = '录制';
  162 + recordingDom.title = '取消录制';
  163 +
  164 + var wrapStyle = {
  165 + height: '38px',
  166 + zIndex: 11,
  167 + position: 'absolute',
  168 + left: 0,
  169 + bottom: 0,
  170 + width: '100%',
  171 + background: 'rgba(0,0,0)'
  172 + };
  173 +
  174 + var bgStyle = {
  175 + position: 'absolute',
  176 + width: '100%',
  177 + height: '100%',
  178 + };
  179 +
  180 + if (this._opt.background) {
  181 + bgStyle = Object.assign({}, bgStyle, {
  182 + backgroundRepeat: "no-repeat",
  183 + backgroundPosition: "center",
  184 + backgroundSize: '100%',
  185 + backgroundImage: "url('" + this._opt.background + "')"
  186 + })
  187 + }
  188 +
  189 + //
  190 + var loadingStyle = {
  191 + position: 'absolute',
  192 + width: '100%',
  193 + height: '100%',
  194 + textAlign: 'center',
  195 + color: "#fff",
  196 + display: 'none',
  197 + backgroundImage: "url('" + gifBase64 + "')",
  198 + backgroundRepeat: "no-repeat",
  199 + backgroundPosition: "center",
  200 + backgroundSize: "40px 40px",
  201 + };
  202 +
  203 + var playBigStyle = {
  204 + position: 'absolute',
  205 + width: '100%',
  206 + height: '100%',
  207 + display: 'none',
  208 + background: 'rgba(0,0,0,0.4)',
  209 + backgroundImage: "url('" + playBigBase64 + "')",
  210 + backgroundRepeat: "no-repeat",
  211 + backgroundPosition: "center",
  212 + backgroundSize: "48px 48px",
  213 + cursor: "pointer"
  214 + };
  215 +
  216 + var loadingTextStyle = {
  217 + position: 'absolute',
  218 + width: "100%",
  219 + top: '60%',
  220 + textAlign: 'center',
  221 + }
  222 + var controlStyle = {
  223 + position: 'absolute',
  224 + top: 0,
  225 + height: '100%',
  226 + display: 'flex',
  227 + alignItems: 'center',
  228 + };
  229 + var styleObj = {
  230 + display: 'none',
  231 + position: 'relative',
  232 + fontSize: '13px',
  233 + color: '#fff',
  234 + lineHeight: '20px',
  235 + marginLeft: '5px',
  236 + marginRight: '5px',
  237 + userSelect: 'none'
  238 + };
  239 + var styleObj2 = {
  240 + display: 'none',
  241 + position: 'relative',
  242 + width: '16px',
  243 + height: '16px',
  244 + marginLeft: '8px',
  245 + marginRight: '8px',
  246 + backgroundRepeat: "no-repeat",
  247 + backgroundPosition: "center",
  248 + backgroundSize: '100%',
  249 + cursor: 'pointer',
  250 + };
  251 + _setStyle(bgDom, bgStyle);
  252 + _setStyle(btnWrap, wrapStyle);
  253 + _setStyle(loadingDom, loadingStyle);
  254 + _setStyle(playBigDom, playBigStyle);
  255 + _setStyle(loadingTextDom, loadingTextStyle);
  256 + _setStyle(control1, Object.assign({}, controlStyle, {
  257 + left: 0
  258 + }));
  259 + _setStyle(control2, Object.assign({}, controlStyle, {
  260 + right: 0
  261 + }));
  262 + _setStyle(textDom, styleObj);
  263 + _setStyle(speedDom, styleObj);
  264 + _setStyle(playDom, Object.assign({}, styleObj2, {
  265 + backgroundImage: "url('" + playBase64 + "')",
  266 + }));
  267 +
  268 + _setStyle(pauseDom, Object.assign({}, styleObj2, {
  269 + backgroundImage: "url('" + pauseBase64 + "')"
  270 + }));
  271 +
  272 + _setStyle(screenshotsDom, Object.assign({}, styleObj2, {
  273 + backgroundImage: "url('" + screenshotBase64 + "')"
  274 + }));
  275 +
  276 + _setStyle(fullscreenDom, Object.assign({}, styleObj2, {
  277 + backgroundImage: "url('" + fullscreenBase64 + "')"
  278 + }));
  279 +
  280 + _setStyle(minScreenDom, Object.assign({}, styleObj2, {
  281 + backgroundImage: "url('" + minScreenBase64 + "')"
  282 + }));
  283 +
  284 + _setStyle(quietAudioDom, Object.assign({}, styleObj2, {
  285 + backgroundImage: "url('" + quietBase64 + "')"
  286 + }));
  287 +
  288 + _setStyle(playAudioDom, Object.assign({}, styleObj2, {
  289 + backgroundImage: "url('" + playAudioBase64 + "')"
  290 + }));
  291 +
  292 + _setStyle(recordDom, Object.assign({}, styleObj2, {
  293 + backgroundImage: "url('" + recordBase64 + "')"
  294 + }));
  295 +
  296 + _setStyle(recordingDom, Object.assign({}, styleObj2, {
  297 + backgroundImage: "url('" + recordingBase64 + "')"
  298 + }));
  299 +
  300 + loadingDom.appendChild(loadingTextDom);
  301 + if (this._opt.text) {
  302 + control1.appendChild(textDom);
  303 + doms.textDom = textDom;
  304 + }
  305 + if (this._opt.showBandwidth) {
  306 + control1.appendChild(speedDom);
  307 + doms.speedDom = speedDom;
  308 + }
  309 +
  310 + // record
  311 + //control2.appendChild(recordingDom);
  312 + //control2.appendChild(recordDom);
  313 +
  314 + // screenshots
  315 + if (this._opt.operateBtns.screenshot) {
  316 + control2.appendChild(screenshotsDom);
  317 + doms.screenshotsDom = screenshotsDom;
  318 + }
  319 +
  320 + // play stop
  321 + if (this._opt.operateBtns.play) {
  322 + control2.appendChild(playDom);
  323 + control2.appendChild(pauseDom);
  324 + doms.playDom = playDom;
  325 + doms.pauseDom = pauseDom;
  326 + }
  327 +
  328 + // audio
  329 + if (this._opt.operateBtns.audio) {
  330 + control2.appendChild(playAudioDom);
  331 + control2.appendChild(quietAudioDom);
  332 + doms.playAudioDom = playAudioDom;
  333 + doms.quietAudioDom = quietAudioDom;
  334 + }
  335 +
  336 + // fullscreen
  337 + if (this._opt.operateBtns.fullscreen) {
  338 + control2.appendChild(fullscreenDom);
  339 + control2.appendChild(minScreenDom);
  340 + doms.fullscreenDom = fullscreenDom;
  341 + doms.minScreenDom = minScreenDom;
  342 + }
  343 +
  344 + btnWrap.appendChild(control1);
  345 + btnWrap.appendChild(control2);
  346 +
  347 + fragment.appendChild(bgDom);
  348 + doms.bgDom = bgDom;
  349 + fragment.appendChild(loadingDom);
  350 + doms.loadingDom = loadingDom;
  351 + if (this._showControl()) {
  352 + fragment.appendChild(btnWrap);
  353 + }
  354 + if (this._opt.operateBtns.play) {
  355 + fragment.appendChild(playBigDom);
  356 + doms.playBigDom = playBigDom;
  357 + }
  358 + this._container.appendChild(fragment);
  359 + this._doms = doms;
  360 + };
  361 +
  362 + Jessibuca.prototype._initWakeLock = function () {
  363 + this._wakeLock = null;
  364 + var _this = this;
  365 + var handleWakeLock = () => {
  366 + if (this._wakeLock !== null && "visible" === document.visibilityState) {
  367 + _this._enableWakeLock();
  368 + }
  369 + };
  370 +
  371 + document.addEventListener('visibilitychange', handleWakeLock);
  372 + document.addEventListener('fullscreenchange', handleWakeLock);
  373 + };
  374 +
  375 + Jessibuca.prototype._enableWakeLock = function () {
  376 + if (this._opt.keepScreenOn) {
  377 + if ("wakeLock" in navigator) {
  378 + var _this = this;
  379 + navigator.wakeLock.request("screen").then((lock) => {
  380 + _this._wakeLock = lock;
  381 + _this._wakeLock.addEventListener('release', function () {
  382 + });
  383 + })
  384 + }
  385 + }
  386 + };
  387 +
  388 +
  389 + Jessibuca.prototype._initGainNode = function () {
  390 + var gainNode = this._audioContext.createGain();
  391 + var _this = this;
  392 + var source;
  393 + if (!navigator.mediaDevices.getUserMedia) {
  394 + console.log('getUserMedia not supported on your browser!');
  395 + return;
  396 + }
  397 +
  398 + navigator.mediaDevices.getUserMedia(
  399 + // constraints - only audio needed for this app
  400 + {
  401 + audio: true
  402 + },
  403 +
  404 + // Success callback
  405 + function (stream) {
  406 + source = _this._audioContext.createMediaStreamSource(stream);
  407 + source.connect(gainNode);
  408 + gainNode.connect(_this._audioContext.destination);
  409 + _this._gainNode = gainNode;
  410 + },
  411 +
  412 + // Error callback
  413 + function (err) {
  414 + console.log('The following gUM error occurred: ' + err);
  415 + }
  416 + );
  417 + };
  418 +
  419 + Jessibuca.prototype._showControl = function () {
  420 + var result = false;
  421 +
  422 + var hasBtnShow = false;
  423 + Object.keys(this._opt.operateBtns).forEach((key) => {
  424 + if (this._opt.operateBtns[key]) {
  425 + hasBtnShow = true;
  426 + }
  427 + });
  428 +
  429 + if (this._opt.showBandwidth || this._opt.text || hasBtnShow) {
  430 + result = true;
  431 + }
  432 +
  433 + return result;
  434 + };
  435 +
  436 + Jessibuca.prototype._onMessage = function () {
  437 + var _this = this;
  438 + this._decoderWorker.onmessage = function (event) {
  439 + var msg = event.data;
  440 + switch (msg.cmd) {
  441 + case "init":
  442 + _this._opt.isDebug && console.log("decoder worker init")
  443 + _this.setBufferTime(_this._opt.videoBuffer);
  444 + if (!_this._hasLoaded) {
  445 + _this._opt.isDebug && console.log("has loaded");
  446 + _this._hasLoaded = true;
  447 + _this.onLoad();
  448 + _this._trigger('load');
  449 + }
  450 + break
  451 + case "initSize":
  452 + _this._canvasElement.width = msg.w;
  453 + _this._canvasElement.height = msg.h;
  454 + _this.onInitSize();
  455 + _this.resize();
  456 + _this._trigger('videoInfo', {w: msg.w, h: msg.h});
  457 + if (_this.isWebGL()) {
  458 +
  459 + } else {
  460 + _this._initRGB(msg.w, msg.h)
  461 + }
  462 + break
  463 + case "render":
  464 + if (_this._contextGL) {
  465 + _this._drawNextOutputPictureGL(msg.output);
  466 + } else {
  467 + _this._drawNextOutputPictureRGBA(msg.buffer);
  468 + }
  469 + if (_this.loading) {
  470 + _this.loading = false;
  471 + _this.playing = true;
  472 + _this._opt.isDebug && console.log("clear check loading timeout");
  473 + _this._clearCheckLoading();
  474 + }
  475 + _this._trigger('timeUpdate', msg.ts);
  476 + _this.onTimeUpdate(msg.ts);
  477 + _this._updateStats({bps: msg.bps, ts: msg.ts});
  478 + _this._checkHeart();
  479 + break
  480 + case "initAudio":
  481 + _this._initAudioPlay(msg.frameCount, msg.samplerate, msg.channels)
  482 + _this._trigger('audioInfo', {
  483 + numOfChannels: msg.channels, // 声频通道
  484 + length: msg.frameCount, // 帧数
  485 + sampleRate: msg.samplerate // 采样率
  486 + });
  487 + break
  488 + case "playAudio":
  489 + _this._playAudio(msg.buffer)
  490 + break
  491 + case "print":
  492 + _this.onLog(msg.text)
  493 + this._trigger('log', msg.text);
  494 + _this._opt.isDebug && console.log(msg.text);
  495 + break
  496 + case "printErr":
  497 + _this.onLog(msg.text);
  498 + this._trigger('log', msg.text);
  499 + _this.onError(msg.text);
  500 + this._trigger('error', msg.text);
  501 + _this._opt.isDebug && console.error(msg.text);
  502 + break;
  503 + case "initAudioPlanar":
  504 + _this._initAudioPlanar(msg);
  505 + _this._trigger('audioInfo', {
  506 + numOfChannels: msg.channels, // 声频通道
  507 + length: undefined, // 帧数
  508 + sampleRate: msg.samplerate // 采样率
  509 + });
  510 + break;
  511 + default:
  512 + _this._opt.isDebug && console.log(msg);
  513 + _this[msg.cmd](msg)
  514 + }
  515 + };
  516 + };
  517 +
  518 + Jessibuca.prototype._initEventListener = function () {
  519 + var _this = this;
  520 +
  521 + this._doms.playDom && this._doms.playDom.addEventListener('click', function (e) {
  522 + e.stopPropagation();
  523 + _this.play();
  524 + }, false);
  525 +
  526 + this._doms.playBigDom && this._doms.playBigDom.addEventListener('click', function (e) {
  527 + e.stopPropagation();
  528 + _this.play();
  529 + }, false);
  530 +
  531 + this._doms.pauseDom && this._doms.pauseDom.addEventListener('click', function (e) {
  532 + e.stopPropagation();
  533 + _this.pause();
  534 + }, false);
  535 +
  536 + // screenshots
  537 + this._doms.screenshotsDom && this._doms.screenshotsDom.addEventListener('click', function (e) {
  538 + e.stopPropagation();
  539 + var filename = _this._opt.text + '' + _now();
  540 + _this._screenshot(filename);
  541 + }, false);
  542 + //
  543 + this._doms.fullscreenDom && this._doms.fullscreenDom.addEventListener('click', function (e) {
  544 + e.stopPropagation();
  545 + _this.fullscreen = true;
  546 + }, false);
  547 + //
  548 + this._doms.minScreenDom && this._doms.minScreenDom.addEventListener('click', function (e) {
  549 + e.stopPropagation();
  550 + _this.fullscreen = false;
  551 + }, false);
  552 + //
  553 + this._doms.recordDom && this._doms.recordDom.addEventListener('click', function (e) {
  554 + e.stopPropagation();
  555 + _this.recording = true;
  556 + }, false);
  557 + //
  558 + this._doms.recordingDom && this._doms.recordingDom.addEventListener('click', function (e) {
  559 + e.stopPropagation();
  560 + _this.recording = false;
  561 + }, false);
  562 +
  563 + this._doms.quietAudioDom && this._doms.quietAudioDom.addEventListener('click', function (e) {
  564 + e.stopPropagation();
  565 + _this.cancelMute();
  566 + }, false);
  567 +
  568 + this._doms.playAudioDom && this._doms.playAudioDom.addEventListener('click', function (e) {
  569 + e.stopPropagation();
  570 + _this.mute();
  571 + }, false);
  572 + };
  573 + /**
  574 + * set debug
  575 + * @param flag
  576 + */
  577 + Jessibuca.prototype.setDebug = function (flag) {
  578 + this._opt.isDebug = !!flag;
  579 + };
  580 + /**
  581 + * mute
  582 + */
  583 + Jessibuca.prototype.mute = function () {
  584 + this._audioEnabled(false);
  585 + this.quieting = true;
  586 + };
  587 +
  588 + /**
  589 + * cancel mute
  590 + */
  591 + Jessibuca.prototype.cancelMute = function () {
  592 + this._audioEnabled(true);
  593 + this.quieting = false;
  594 + };
  595 +
  596 + /**
  597 + * 设置旋转角度
  598 + */
  599 + Jessibuca.prototype.setRotate = function (deg) {
  600 +
  601 + };
  602 +
  603 + Jessibuca.prototype._initStatus = function () {
  604 + this._loading = true;
  605 + this.loading = true;
  606 + this._recording = false;
  607 + this.recording = false;
  608 + this._playing = false;
  609 + this.playing = false;
  610 + this._quieting = this._opt.isNotMute ? false : true;
  611 + this.quieting = this._opt.isNotMute ? false : true;
  612 + this._fullscreen = false;
  613 + this.fullscreen = false;
  614 + }
  615 +
  616 + Jessibuca.prototype._initBtns = function () {
  617 + // show
  618 + _domToggle(this._doms.pauseDom, true);
  619 + _domToggle(this._doms.screenshotsDom, true);
  620 + _domToggle(this._doms.fullscreenDom, true);
  621 + _domToggle(this._doms.quietAudioDom, true);
  622 + _domToggle(this._doms.textDom, true);
  623 + _domToggle(this._doms.speedDom, true);
  624 + _domToggle(this._doms.recordDom, true);
  625 + // hide
  626 + _domToggle(this._doms.loadingDom, false);
  627 + _domToggle(this._doms.playDom, false);
  628 + _domToggle(this._doms.playBigDom, false);
  629 + _domToggle(this._doms.bgDom, false);
  630 + };
  631 +
  632 + Jessibuca.prototype._hideBtns = function () {
  633 + var _this = this;
  634 + Object.keys(this._doms).forEach(function (dom) {
  635 + if (dom !== 'bgDom') {
  636 + _domToggle(_this._doms[dom], false);
  637 + }
  638 + })
  639 + };
  640 +
  641 + function _checkFull() {
  642 + var isFull = document.fullscreenElement || window.webkitFullscreenElement || document.msFullscreenElement;
  643 + if (isFull === undefined) isFull = false;
  644 + return !!isFull;
  645 + }
  646 +
  647 + Jessibuca.prototype._updateStats = function (options) {
  648 + options = options || {};
  649 +
  650 + if (!this._startBpsTime) {
  651 + this._startBpsTime = _now();
  652 + }
  653 + var _nowTime = _now();
  654 + var timestamp = _nowTime - this._startBpsTime;
  655 +
  656 + if (timestamp < 1 * 1000) {
  657 + this._bps += (options.bps || 0);
  658 + this._stats.fps += 1;
  659 + this._stats.vbps += parseInt((options.bps || 0));
  660 + return;
  661 + }
  662 + this._stats.ts = options.ts;
  663 + this._doms.speedDom && (this._doms.speedDom.innerText = _bpsSize(this._bps));
  664 + this._trigger('bps', this._bps);
  665 + this._trigger('stats', this._stats);
  666 + this._trigger('performance', _fpsStatus(this._stats.fps));
  667 + this._bps = 0;
  668 + this._stats.fps = 0;
  669 + this._stats.vbps = 0;
  670 + this._startBpsTime = _nowTime;
  671 + };
  672 +
  673 +
  674 + Jessibuca.prototype._checkHeart = function () {
  675 + if (this._checkHeartTimeout) {
  676 + clearTimeout(this._checkHeartTimeout);
  677 + this._checkHeartTimeout = null;
  678 + }
  679 + var _this = this;
  680 + this._checkHeartTimeout = setTimeout(function () {
  681 + _this._opt.isDebug && console.log('check heart timeout');
  682 + _this._trigger('timeout');
  683 + _this.recording = false;
  684 + _this.playing = false;
  685 + _this._close();
  686 + }, this._opt.timeout * 1000);
  687 + };
  688 +
  689 + Jessibuca.prototype._checkLoading = function () {
  690 + if (this._checkLoadingTimeout) {
  691 + clearTimeout(this._checkLoadingTimeout);
  692 + this._checkLoadingTimeout = null;
  693 + }
  694 + var _this = this;
  695 + this._checkLoadingTimeout = setTimeout(function () {
  696 + _this._opt.isDebug && console.log('check loading timeout');
  697 + _this._trigger('timeout');
  698 + _this.playing = false;
  699 + _this._close();
  700 + _domToggle(_this._doms.loadingDom, false);
  701 + }, this._opt.timeout * 1000);
  702 + };
  703 +
  704 + Jessibuca.prototype._clearCheckLoading = function () {
  705 + if (this._checkLoadingTimeout) {
  706 + clearTimeout(this._checkLoadingTimeout);
  707 + this._checkLoadingTimeout = null;
  708 + }
  709 + };
  710 +
  711 + Jessibuca.prototype._initCheckVariable = function () {
  712 + this._startBpsTime = '';
  713 + this._bps = 0;
  714 + if (this._checkHeartTimeout) {
  715 + clearTimeout(this._checkHeartTimeout);
  716 + this._checkHeartTimeout = null;
  717 + }
  718 + }
  719 + //
  720 + Jessibuca.prototype._initAudioPlanar = function (msg) {
  721 + var channels = msg.channels
  722 + var samplerate = msg.samplerate
  723 + var context = this._audioContext;
  724 + var isPlaying = false;
  725 + var audioBuffers = [];
  726 + if (!context) return false;
  727 + var _this = this
  728 + this._playAudio = function (buffer) {
  729 + var frameCount = buffer[0][0].length
  730 + var audioBuffer = context.createBuffer(channels, frameCount * buffer.length, samplerate);
  731 + var copyToCtxBuffer = function (fromBuffer) {
  732 + for (var channel = 0; channel < channels; channel++) {
  733 + var nowBuffering = audioBuffer.getChannelData(channel);
  734 + for (var j = 0; j < buffer.length; j++) {
  735 + for (var i = 0; i < frameCount; i++) {
  736 + nowBuffering[i + j * frameCount] = fromBuffer[j][channel][i]
  737 + }
  738 + //postMessage({ cmd: "setBufferA", buffer: fromBuffer[j] }, '*', fromBuffer[j].map(x => x.buffer))
  739 + }
  740 + }
  741 + }
  742 + var playNextBuffer = function () {
  743 + isPlaying = false;
  744 + //console.log("~", audioBuffers.length)
  745 + if (audioBuffers.length) {
  746 + playAudio(audioBuffers.shift());
  747 + }
  748 + //if (audioBuffers.length > 1) audioBuffers.shift();
  749 + };
  750 + var playAudio = function (fromBuffer) {
  751 + if (!fromBuffer) return
  752 + if (isPlaying) {
  753 + audioBuffers.push(fromBuffer);
  754 + //console.log(audioBuffers.length)
  755 + return;
  756 + }
  757 + isPlaying = true;
  758 + copyToCtxBuffer(fromBuffer);
  759 + var source = context.createBufferSource();
  760 + source.buffer = audioBuffer;
  761 + source.connect(context.destination);
  762 + // source.onended = playNextBuffer;
  763 + source.start();
  764 + };
  765 + _this._playAudio = playAudio
  766 + _this.audioInterval = setInterval(playNextBuffer, audioBuffer.duration * 1000);
  767 + playAudio(buffer)
  768 + };
  769 + }
  770 +
  771 + function _unlock(context) {
  772 + context.resume();
  773 + var source = context.createBufferSource();
  774 + source.buffer = context.createBuffer(1, 1, 22050);
  775 + source.connect(context.destination);
  776 + if (source.noteOn)
  777 + source.noteOn(0);
  778 + else
  779 + source.start(0);
  780 + }
  781 +
  782 + function _domToggle(dom, toggle) {
  783 + if (dom) {
  784 + dom.style.display = toggle ? 'block' : "none";
  785 + }
  786 + }
  787 +
  788 + function _dataURLToFile(dataURL) {
  789 + const arr = dataURL.split(",");
  790 + const bstr = atob(arr[1]);
  791 + const type = arr[0].replace("data:", "").replace(";base64", "")
  792 + let n = bstr.length, u8arr = new Uint8Array(n);
  793 + while (n--) {
  794 + u8arr[n] = bstr.charCodeAt(n);
  795 + }
  796 + return new File([u8arr], 'file', {type});
  797 + }
  798 +
  799 + function _downloadImg(content, fileName) {
  800 + const aLink = document.createElement("a");
  801 + aLink.download = fileName;
  802 + aLink.href = URL.createObjectURL(content);
  803 + aLink.click();
  804 + URL.revokeObjectURL(content);
  805 + }
  806 +
  807 + function _bpsSize(value) {
  808 + if (null == value || value === '') {
  809 + return "0 KB/S";
  810 + }
  811 + var srcsize = parseFloat(value);
  812 + var size = srcsize / 1024;
  813 + size = size.toFixed(2);
  814 + return size + 'KB/S';
  815 + }
  816 +
  817 + function _fpsStatus(fps) {
  818 + var result = 0;
  819 + if (fps >= 24) {
  820 + result = 2;
  821 + } else if (fps >= 15) {
  822 + result = 1;
  823 + }
  824 +
  825 + return result;
  826 + }
  827 +
  828 + /**
  829 + * set audio
  830 + * @param flag
  831 + */
  832 + Jessibuca.prototype._audioEnabled = function (flag) {
  833 + if (flag) {
  834 + _unlock(this._audioContext)
  835 + this._audioEnabled = function (flag) {
  836 + if (flag) {
  837 + // 恢复
  838 + this._audioContext.resume();
  839 +
  840 + } else {
  841 + // 暂停
  842 + this._audioContext.suspend();
  843 + }
  844 + }
  845 + } else {
  846 + this._audioContext.suspend();
  847 + }
  848 + }
  849 +
  850 + Jessibuca.prototype._playAudio = function (data) {
  851 + var context = this._audioContext;
  852 + var isPlaying = false;
  853 + var isDecoding = false;
  854 + if (!context) return false;
  855 + var audioBuffers = [];
  856 + var decodeQueue = []
  857 + var _this = this
  858 + var playNextBuffer = function (e) {
  859 + if (audioBuffers.length) {
  860 + playBuffer(audioBuffers.shift())
  861 + }
  862 + };
  863 + var playBuffer = function (buffer) {
  864 + isPlaying = true;
  865 + var audioBufferSouceNode = context.createBufferSource();
  866 + audioBufferSouceNode.buffer = buffer;
  867 + audioBufferSouceNode.connect(context.destination);
  868 + // audioBufferSouceNode.onended = playNextBuffer;
  869 + audioBufferSouceNode.start();
  870 + if (!_this.audioInterval) {
  871 + _this.audioInterval = setInterval(playNextBuffer, buffer.duration * 1000 - 1);
  872 + }
  873 + }
  874 + var decodeAudio = function () {
  875 + if (decodeQueue.length) {
  876 + context.decodeAudioData(decodeQueue.shift(), tryPlay, decodeAudio);
  877 + } else {
  878 + isDecoding = false
  879 + }
  880 + }
  881 + var tryPlay = function (buffer) {
  882 + decodeAudio()
  883 + if (isPlaying) {
  884 + audioBuffers.push(buffer);
  885 + } else {
  886 + playBuffer(buffer)
  887 + }
  888 + }
  889 + var playAudio = function (data) {
  890 + decodeQueue.push(...data)
  891 + if (!isDecoding) {
  892 + isDecoding = true
  893 + decodeAudio()
  894 + }
  895 + }
  896 + this._playAudio = playAudio
  897 + playAudio(data)
  898 + }
  899 + Jessibuca.prototype._initAudioPlay = function (frameCount, samplerate, channels) {
  900 + var context = this._audioContext;
  901 + var isPlaying = false;
  902 + var audioBuffers = [];
  903 + if (!context) return false;
  904 + var _this = this
  905 + var resampled = samplerate < 22050;
  906 + if (resampled) {
  907 + _this._opt.isDebug && console.log("resampled!")
  908 + }
  909 + var audioBuffer = resampled ? context.createBuffer(channels, frameCount << 1, samplerate << 1) : context.createBuffer(channels, frameCount, samplerate);
  910 + var playNextBuffer = function () {
  911 + isPlaying = false;
  912 + //console.log("~", audioBuffers.length)
  913 + if (audioBuffers.length) {
  914 + playAudio(audioBuffers.shift());
  915 + }
  916 + };
  917 +
  918 + var copyToCtxBuffer = channels > 1 ? function (fromBuffer) {
  919 + for (var channel = 0; channel < channels; channel++) {
  920 + var nowBuffering = audioBuffer.getChannelData(channel);
  921 + if (resampled) {
  922 + for (var i = 0; i < frameCount; i++) {
  923 + nowBuffering[i * 2] = nowBuffering[i * 2 + 1] = fromBuffer[i * (channel + 1)] / 32768;
  924 + }
  925 + } else
  926 + for (var i = 0; i < frameCount; i++) {
  927 + nowBuffering[i] = fromBuffer[i * (channel + 1)] / 32768;
  928 + }
  929 +
  930 + }
  931 + } : function (fromBuffer) {
  932 + var nowBuffering = audioBuffer.getChannelData(0);
  933 + for (var i = 0; i < nowBuffering.length; i++) {
  934 + nowBuffering[i] = fromBuffer[i] / 32768;
  935 + }
  936 + };
  937 + var playAudio = function (fromBuffer) {
  938 + if (isPlaying) {
  939 + audioBuffers.push(fromBuffer);
  940 + return;
  941 + }
  942 + isPlaying = true;
  943 + copyToCtxBuffer(fromBuffer);
  944 + var source = context.createBufferSource();
  945 + source.buffer = audioBuffer;
  946 + source.connect(context.destination);
  947 + if (!_this.audioInterval) {
  948 + _this.audioInterval = setInterval(playNextBuffer, audioBuffer.duration * 1000);
  949 + }
  950 + source.start();
  951 + };
  952 + this._playAudio = playAudio;
  953 + }
  954 + /**
  955 + * Returns true if the canvas supports WebGL
  956 + */
  957 + Jessibuca.prototype.isWebGL = function () {
  958 + return !!this._contextGL;
  959 + };
  960 + /**
  961 + * set timeout
  962 + * @param time
  963 + */
  964 + Jessibuca.prototype.setTimeout = function (time) {
  965 + if (typeof time === 'number') {
  966 + this._opt.timeout = Number(time);
  967 + }
  968 + };
  969 +
  970 + /**
  971 + * @desc 视频缩放模式, 当视频分辨率比例与canvas显示区域比例不同时,缩放效果不同:
  972 + 0 视频画面完全填充canvas区域,画面会被拉伸
  973 + 1 视频画面做等比缩放后,高或宽对齐canvas区域,画面不被拉伸,但有黑边(默认)
  974 + 2 视频画面做等比缩放后,完全填充canvas区域,画面不被拉伸,没有黑边,但画面显示不全
  975 + * @param type
  976 + *
  977 + */
  978 + Jessibuca.prototype.setScaleMode = function (type) {
  979 + if (type === 0) {
  980 + this._opt.isFullResize = false;
  981 + this._opt.isResize = false;
  982 + } else if (type === 1) {
  983 + this._opt.isFullResize = false;
  984 + this._opt.isResize = true;
  985 + } else if (type === 2) {
  986 + this._opt.isFullResize = true;
  987 + }
  988 + this.resize();
  989 + };
  990 +
  991 + /**
  992 + * Create the GL context from the canvas element
  993 + */
  994 + Jessibuca.prototype._initContextGL = function () {
  995 + var canvas = this._canvasElement;
  996 + var gl = null;
  997 +
  998 + var validContextNames = ["webgl", "experimental-webgl", "moz-webgl", "webkit-3d"];
  999 + var nameIndex = 0;
  1000 +
  1001 + while (!gl && nameIndex < validContextNames.length) {
  1002 + var contextName = validContextNames[nameIndex];
  1003 +
  1004 + try {
  1005 + var contextOptions = {preserveDrawingBuffer: true};
  1006 + if (this._opt.contextOptions) {
  1007 + contextOptions = Object.assign(contextOptions, this._opt.contextOptions);
  1008 + }
  1009 +
  1010 + gl = canvas.getContext(contextName, contextOptions);
  1011 + } catch (e) {
  1012 + gl = null;
  1013 + }
  1014 +
  1015 + if (!gl || typeof gl.getParameter !== "function") {
  1016 + gl = null;
  1017 + }
  1018 +
  1019 + ++nameIndex;
  1020 + }
  1021 + ;
  1022 +
  1023 + this._contextGL = gl;
  1024 + };
  1025 +
  1026 + /**
  1027 + * Initialize GL shader program
  1028 + */
  1029 + Jessibuca.prototype._initProgram = function () {
  1030 + var gl = this._contextGL;
  1031 +
  1032 + var vertexShaderScript = [
  1033 + 'attribute vec4 vertexPos;',
  1034 + 'attribute vec4 texturePos;',
  1035 + 'varying vec2 textureCoord;',
  1036 +
  1037 + 'void main()',
  1038 + '{',
  1039 + 'gl_Position = vertexPos;',
  1040 + 'textureCoord = texturePos.xy;',
  1041 + '}'
  1042 + ].join('\n');
  1043 +
  1044 + var fragmentShaderScript = [
  1045 + 'precision highp float;',
  1046 + 'varying highp vec2 textureCoord;',
  1047 + 'uniform sampler2D ySampler;',
  1048 + 'uniform sampler2D uSampler;',
  1049 + 'uniform sampler2D vSampler;',
  1050 + 'const mat4 YUV2RGB = mat4',
  1051 + '(',
  1052 + '1.1643828125, 0, 1.59602734375, -.87078515625,',
  1053 + '1.1643828125, -.39176171875, -.81296875, .52959375,',
  1054 + '1.1643828125, 2.017234375, 0, -1.081390625,',
  1055 + '0, 0, 0, 1',
  1056 + ');',
  1057 +
  1058 + 'void main(void) {',
  1059 + 'highp float y = texture2D(ySampler, textureCoord).r;',
  1060 + 'highp float u = texture2D(uSampler, textureCoord).r;',
  1061 + 'highp float v = texture2D(vSampler, textureCoord).r;',
  1062 + 'gl_FragColor = vec4(y, u, v, 1) * YUV2RGB;',
  1063 + '}'
  1064 + ].join('\n');
  1065 +
  1066 + var vertexShader = gl.createShader(gl.VERTEX_SHADER);
  1067 + gl.shaderSource(vertexShader, vertexShaderScript);
  1068 + gl.compileShader(vertexShader);
  1069 + if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
  1070 + this._opt.isDebug && console.log('Vertex shader failed to compile: ' + gl.getShaderInfoLog(vertexShader));
  1071 + }
  1072 +
  1073 + var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
  1074 + gl.shaderSource(fragmentShader, fragmentShaderScript);
  1075 + gl.compileShader(fragmentShader);
  1076 + if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
  1077 + this._opt.isDebug && console.log('Fragment shader failed to compile: ' + gl.getShaderInfoLog(fragmentShader));
  1078 + }
  1079 +
  1080 + var program = gl.createProgram();
  1081 + gl.attachShader(program, vertexShader);
  1082 + gl.attachShader(program, fragmentShader);
  1083 + gl.linkProgram(program);
  1084 + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
  1085 + this._opt.isDebug && console.log('Program failed to compile: ' + gl.getProgramInfoLog(program));
  1086 + }
  1087 +
  1088 + gl.useProgram(program);
  1089 +
  1090 + this._shaderProgram = program;
  1091 + };
  1092 +
  1093 + /**
  1094 + * Initialize vertex buffers and attach to shader program
  1095 + */
  1096 + Jessibuca.prototype._initBuffers = function () {
  1097 + var gl = this._contextGL;
  1098 + var program = this._shaderProgram;
  1099 +
  1100 + var vertexPosBuffer = gl.createBuffer();
  1101 + gl.bindBuffer(gl.ARRAY_BUFFER, vertexPosBuffer);
  1102 + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([1, 1, -1, 1, 1, -1, -1, -1]), gl.STATIC_DRAW);
  1103 +
  1104 + var vertexPosRef = gl.getAttribLocation(program, 'vertexPos');
  1105 + gl.enableVertexAttribArray(vertexPosRef);
  1106 + gl.vertexAttribPointer(vertexPosRef, 2, gl.FLOAT, false, 0, 0);
  1107 +
  1108 + var texturePosBuffer = gl.createBuffer();
  1109 + gl.bindBuffer(gl.ARRAY_BUFFER, texturePosBuffer);
  1110 + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([1, 0, 0, 0, 1, 1, 0, 1]), gl.STATIC_DRAW);
  1111 +
  1112 + var texturePosRef = gl.getAttribLocation(program, 'texturePos');
  1113 + gl.enableVertexAttribArray(texturePosRef);
  1114 + gl.vertexAttribPointer(texturePosRef, 2, gl.FLOAT, false, 0, 0);
  1115 +
  1116 + this._texturePosBuffer = texturePosBuffer;
  1117 + };
  1118 +
  1119 + /**
  1120 + * Initialize GL textures and attach to shader program
  1121 + */
  1122 + Jessibuca.prototype._initTextures = function () {
  1123 + var gl = this._contextGL;
  1124 + var program = this._shaderProgram;
  1125 +
  1126 + var yTextureRef = this._initTexture();
  1127 + var ySamplerRef = gl.getUniformLocation(program, 'ySampler');
  1128 + gl.uniform1i(ySamplerRef, 0);
  1129 + this._yTextureRef = yTextureRef;
  1130 +
  1131 + var uTextureRef = this._initTexture();
  1132 + var uSamplerRef = gl.getUniformLocation(program, 'uSampler');
  1133 + gl.uniform1i(uSamplerRef, 1);
  1134 + this._uTextureRef = uTextureRef;
  1135 +
  1136 + var vTextureRef = this._initTexture();
  1137 + var vSamplerRef = gl.getUniformLocation(program, 'vSampler');
  1138 + gl.uniform1i(vSamplerRef, 2);
  1139 + this._vTextureRef = vTextureRef;
  1140 + };
  1141 +
  1142 + /**
  1143 + * Create and configure a single texture
  1144 + */
  1145 + Jessibuca.prototype._initTexture = function () {
  1146 + var gl = this._contextGL;
  1147 +
  1148 + var textureRef = gl.createTexture();
  1149 + gl.bindTexture(gl.TEXTURE_2D, textureRef);
  1150 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
  1151 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
  1152 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  1153 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
  1154 + gl.bindTexture(gl.TEXTURE_2D, null);
  1155 +
  1156 + return textureRef;
  1157 + };
  1158 +
  1159 + /**
  1160 + * Draw picture data to the canvas.
  1161 + * If this object is using WebGL, the data must be an I420 formatted ArrayBuffer,
  1162 + * Otherwise, data must be an RGBA formatted ArrayBuffer.
  1163 + */
  1164 + Jessibuca.prototype._drawNextOutputPicture = function (data) {
  1165 + if (this._contextGL) {
  1166 + this._drawNextOutputPictureGL(data);
  1167 + } else {
  1168 + this._drawNextOutputPictureRGBA(data);
  1169 + }
  1170 + };
  1171 +
  1172 + /**
  1173 + * Draw the next output picture using WebGL
  1174 + */
  1175 + Jessibuca.prototype._drawNextOutputPictureGL = function (data) {
  1176 + var gl = this._contextGL;
  1177 + var texturePosBuffer = this._texturePosBuffer;
  1178 + var yTextureRef = this._yTextureRef;
  1179 + var uTextureRef = this._uTextureRef;
  1180 + var vTextureRef = this._vTextureRef;
  1181 + var croppingParams = this.croppingParams
  1182 + var width = this._canvasElement.width
  1183 + var height = this._canvasElement.height
  1184 + if (croppingParams) {
  1185 + gl.viewport(0, 0, croppingParams.width, croppingParams.height);
  1186 + var tTop = croppingParams.top / height;
  1187 + var tLeft = croppingParams.left / width;
  1188 + var tBottom = croppingParams.height / height;
  1189 + var tRight = croppingParams.width / width;
  1190 + var texturePosValues = new Float32Array([tRight, tTop, tLeft, tTop, tRight, tBottom, tLeft, tBottom]);
  1191 +
  1192 + gl.bindBuffer(gl.ARRAY_BUFFER, texturePosBuffer);
  1193 + gl.bufferData(gl.ARRAY_BUFFER, texturePosValues, gl.DYNAMIC_DRAW);
  1194 + } else {
  1195 + gl.viewport(0, 0, this._canvasElement.width, this._canvasElement.height);
  1196 + }
  1197 + gl.activeTexture(gl.TEXTURE0);
  1198 + gl.bindTexture(gl.TEXTURE_2D, yTextureRef);
  1199 + gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, width, height, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, data[0]);
  1200 +
  1201 + gl.activeTexture(gl.TEXTURE1);
  1202 + gl.bindTexture(gl.TEXTURE_2D, uTextureRef);
  1203 + gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, width / 2, height / 2, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, data[1]);
  1204 +
  1205 + gl.activeTexture(gl.TEXTURE2);
  1206 + gl.bindTexture(gl.TEXTURE_2D, vTextureRef);
  1207 + gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, width / 2, height / 2, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, data[2]);
  1208 +
  1209 + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
  1210 + };
  1211 +
  1212 + /**
  1213 + * Draw next output picture using ARGB data on a 2d canvas.
  1214 + */
  1215 + Jessibuca.prototype._drawNextOutputPictureRGBA = function (data) {
  1216 + this.imageData.data.set(data);
  1217 + var croppingParams = this.croppingParams
  1218 + if (!croppingParams) {
  1219 + this.ctx2d.putImageData(this.imageData, 0, 0);
  1220 + } else {
  1221 + this.ctx2d.putImageData(this.imageData, -croppingParams.left, -croppingParams.top, 0, 0, croppingParams.width, croppingParams.height);
  1222 + }
  1223 + };
  1224 + Jessibuca.prototype.ctx2d = null;
  1225 + Jessibuca.prototype.imageData = null;
  1226 + Jessibuca.prototype._initRGB = function (width, height) {
  1227 + this.ctx2d = this._canvasElement.getContext('2d');
  1228 + this.imageData = this.ctx2d.getImageData(0, 0, width, height);
  1229 + this.clear = function () {
  1230 + this.ctx2d.clearRect(0, 0, width, height)
  1231 + };
  1232 + };
  1233 +
  1234 + Jessibuca.prototype.pause = function () {
  1235 + this._close();
  1236 + if (this.loading) {
  1237 + _domToggle(this._doms.loadingDom, false);
  1238 + }
  1239 + this.recording = false;
  1240 + this.playing = false;
  1241 + };
  1242 +
  1243 + Jessibuca.prototype._close = function () {
  1244 + if (this.audioInterval) {
  1245 + clearInterval(this.audioInterval)
  1246 + }
  1247 + delete this._playAudio
  1248 + this._decoderWorker.postMessage({cmd: "close"})
  1249 +
  1250 + if (this._wakeLock) {
  1251 + this._wakeLock.release();
  1252 + this._wakeLock = null;
  1253 + }
  1254 +
  1255 + // this._contextGL.clear(this._contextGL.COLOR_BUFFER_BIT);
  1256 + this._initCheckVariable();
  1257 + }
  1258 + /**
  1259 + * destroy
  1260 + * @desc delete worker,
  1261 + */
  1262 + Jessibuca.prototype.destroy = function () {
  1263 + // destroy
  1264 + this._decoderWorker.terminate()
  1265 + window.removeEventListener("resize", this._onresize);
  1266 + window.removeEventListener('fullscreenchange', this._onfullscreenchange);
  1267 + this._initCheckVariable();
  1268 + this._clearCheckLoading();
  1269 + this._off();
  1270 + this._hasLoaded = false;
  1271 + // remove dom
  1272 + while (this._container.firstChild) {
  1273 + this._container.removeChild(this._container.firstChild);
  1274 + }
  1275 + if (this._wakeLock) {
  1276 + this._wakeLock.release();
  1277 + }
  1278 + }
  1279 +
  1280 + /**
  1281 + * 清理画布为黑色背景
  1282 + * 用于canvas重用进行多个流切换播放时,将上一个画面清理
  1283 + * 避免后一个视频播放之前出现前一个视频最后一个画面
  1284 + */
  1285 + Jessibuca.prototype.clearView = function () {
  1286 + this._contextGL.clear(this._contextGL.COLOR_BUFFER_BIT);
  1287 + };
  1288 + /**
  1289 + * play
  1290 + * @param url
  1291 + */
  1292 + Jessibuca.prototype.play = function (url) {
  1293 + if (!this.playUrl && !url) {
  1294 + return;
  1295 + }
  1296 + var needDelay = false;
  1297 + if (url) {
  1298 + if (this.playUrl) {
  1299 + this._close();
  1300 + needDelay = true;
  1301 + this._contextGL.clear(this._contextGL.COLOR_BUFFER_BIT);
  1302 + }
  1303 + this.loading = true;
  1304 + _domToggle(this._doms.bgDom, false);
  1305 + this._checkLoading();
  1306 + this.playUrl = url;
  1307 + } else if (this.playUrl) {
  1308 + // retry
  1309 + if (this.loading) {
  1310 + this._hideBtns();
  1311 + _domToggle(this._doms.fullscreenDom, true);
  1312 + _domToggle(this._doms.pauseDom, true);
  1313 + _domToggle(this._doms.loadingDom, true);
  1314 + this._checkLoading();
  1315 + } else {
  1316 + this.playing = true;
  1317 + }
  1318 + }
  1319 + this._initCheckVariable();
  1320 +
  1321 + if (needDelay) {
  1322 + var _this = this;
  1323 + setTimeout(function () {
  1324 + _this._decoderWorker.postMessage({cmd: "play", url: _this.playUrl, isWebGL: _this.isWebGL()})
  1325 + }, 300);
  1326 + } else {
  1327 + this._decoderWorker.postMessage({cmd: "play", url: this.playUrl, isWebGL: this.isWebGL()})
  1328 + }
  1329 + };
  1330 + /**
  1331 + * has loaded
  1332 + * @returns {boolean}
  1333 + */
  1334 + Jessibuca.prototype.hasLoaded = function () {
  1335 + return this._hasLoaded;
  1336 + };
  1337 +
  1338 + Object.defineProperty(Jessibuca.prototype, "fullscreen", {
  1339 + set(value) {
  1340 + if (value) {
  1341 + if (!_checkFull()) {
  1342 + this._container.requestFullscreen();
  1343 + }
  1344 + _domToggle(this._doms.minScreenDom, true);
  1345 + _domToggle(this._doms.fullscreenDom, false);
  1346 + } else {
  1347 + if (_checkFull()) {
  1348 + document.exitFullscreen();
  1349 + }
  1350 + _domToggle(this._doms.minScreenDom, false);
  1351 + _domToggle(this._doms.fullscreenDom, true);
  1352 + }
  1353 +
  1354 + if (this._fullscreen !== value) {
  1355 + this.onFullscreen(value);
  1356 + this._trigger('fullscreen', value);
  1357 + }
  1358 + this._fullscreen = value;
  1359 + },
  1360 + get() {
  1361 + return this._fullscreen;
  1362 + }
  1363 + });
  1364 +
  1365 + Object.defineProperty(Jessibuca.prototype, 'playing', {
  1366 + set(value) {
  1367 + if (value) {
  1368 + _domToggle(this._doms.playBigDom, false);
  1369 + _domToggle(this._doms.playDom, false);
  1370 + _domToggle(this._doms.pauseDom, true);
  1371 +
  1372 + _domToggle(this._doms.screenshotsDom, true);
  1373 + _domToggle(this._doms.recordDom, true);
  1374 + if (this._quieting) {
  1375 + _domToggle(this._doms.quietAudioDom, true);
  1376 + _domToggle(this._doms.playAudioDom, false);
  1377 + } else {
  1378 + _domToggle(this._doms.quietAudioDom, false);
  1379 + _domToggle(this._doms.playAudioDom, true);
  1380 + }
  1381 + } else {
  1382 + this._doms.speedDom && (this._doms.speedDom.innerText = '');
  1383 + if (this.playUrl) {
  1384 + _domToggle(this._doms.playDom, true);
  1385 + _domToggle(this._doms.playBigDom, true);
  1386 + _domToggle(this._doms.pauseDom, false);
  1387 + }
  1388 +
  1389 + // 在停止状态下录像,截屏,音量是非激活,只有播放,最大化时可点击
  1390 + _domToggle(this._doms.recordDom, false);
  1391 + _domToggle(this._doms.recordingDom, false);
  1392 + _domToggle(this._doms.screenshotsDom, false);
  1393 + _domToggle(this._doms.quietAudioDom, false);
  1394 + _domToggle(this._doms.playAudioDom, false);
  1395 + }
  1396 +
  1397 + if (this._playing !== value) {
  1398 + if (value) {
  1399 + this.onPlay();
  1400 + this._trigger('play');
  1401 + } else {
  1402 + this.onPause();
  1403 + this._trigger('pause');
  1404 + }
  1405 + }
  1406 + this._playing = value;
  1407 + },
  1408 + get() {
  1409 + return this._playing;
  1410 + }
  1411 + });
  1412 +
  1413 + Object.defineProperty(Jessibuca.prototype, 'recording', {
  1414 + set(value) {
  1415 + if (value) {
  1416 + _domToggle(this._doms.recordDom, false);
  1417 + _domToggle(this._doms.recordingDom, true);
  1418 + } else {
  1419 + _domToggle(this._doms.recordDom, true);
  1420 + _domToggle(this._doms.recordingDom, false);
  1421 +
  1422 + }
  1423 + if (this._recording !== value) {
  1424 + this.onRecord(value);
  1425 + this._trigger('record', value);
  1426 + this._recording = value;
  1427 + }
  1428 + },
  1429 + get() {
  1430 + return this._recording;
  1431 + }
  1432 + });
  1433 +
  1434 + Object.defineProperty(Jessibuca.prototype, 'quieting', {
  1435 + set(value) {
  1436 + if (value) {
  1437 + _domToggle(this._doms.quietAudioDom, true);
  1438 + _domToggle(this._doms.playAudioDom, false);
  1439 + } else {
  1440 + _domToggle(this._doms.quietAudioDom, false);
  1441 + _domToggle(this._doms.playAudioDom, true);
  1442 + }
  1443 + if (this._quieting !== value) {
  1444 + this.onMute(value);
  1445 + this._trigger('mute', value);
  1446 + }
  1447 + this._quieting = value;
  1448 + },
  1449 + get() {
  1450 + return this._quieting;
  1451 + }
  1452 + });
  1453 +
  1454 + Object.defineProperty(Jessibuca.prototype, 'loading', {
  1455 + set(value) {
  1456 + if (value) {
  1457 + this._hideBtns();
  1458 + _domToggle(this._doms.fullscreenDom, true);
  1459 + _domToggle(this._doms.pauseDom, true);
  1460 + _domToggle(this._doms.loadingDom, true);
  1461 + } else {
  1462 + this._initBtns();
  1463 + }
  1464 + this._loading = value;
  1465 + },
  1466 + get() {
  1467 + return this._loading;
  1468 + }
  1469 + });
  1470 +
  1471 + /**
  1472 + * resize
  1473 + */
  1474 + Jessibuca.prototype.resize = function () {
  1475 + var width = this._container.clientWidth;
  1476 + var height = this._container.clientHeight;
  1477 + if (this._showControl()) {
  1478 + height -= 38;
  1479 + }
  1480 + var resizeWidth = this._canvasElement.width;
  1481 + var resizeHeight = this._canvasElement.height;
  1482 + var wScale = width / resizeWidth;
  1483 + var hScale = height / resizeHeight;
  1484 + var scale = wScale > hScale ? hScale : wScale;
  1485 + if (!this._opt.isResize) {
  1486 + if (wScale !== hScale) {
  1487 + scale = wScale + ',' + hScale;
  1488 + }
  1489 + }
  1490 + //
  1491 + if (this._opt.isFullResize) {
  1492 + scale = wScale > hScale ? wScale : hScale;
  1493 + }
  1494 +
  1495 + this._opt.isDebug && console.log('wScale', wScale, 'hScale', hScale, 'scale', scale);
  1496 + this._canvasElement.style.transform = "scale(" + scale + ")"
  1497 + this._canvasElement.style.left = ((width - resizeWidth) / 2) + "px"
  1498 + this._canvasElement.style.top = ((height - resizeHeight) / 2) + "px"
  1499 + }
  1500 +
  1501 + Jessibuca.prototype._fullscreenchange = function () {
  1502 + this.fullscreen = _checkFull();
  1503 + }
  1504 +
  1505 + /**
  1506 + * change buffer
  1507 + * @param buffer
  1508 + */
  1509 + Jessibuca.prototype.changeBuffer = function (buffer) {
  1510 + this._stats.buf = Number(buffer) * 1000;
  1511 + this._decoderWorker.postMessage({cmd: "setVideoBuffer", time: Number(buffer)});
  1512 + };
  1513 + /**
  1514 + * 设置最大缓冲时长,单位秒,播放器会自动消除延迟。
  1515 + * @param buffer
  1516 + */
  1517 + Jessibuca.prototype.setBufferTime = function (buffer) {
  1518 + this.changeBuffer(buffer);
  1519 + };
  1520 +
  1521 + /**
  1522 + * 设置音量大小,取值0.0 — 1.0
  1523 + * 当为0.0时,完全无声
  1524 + * 当为1.0时,最大音量,默认值
  1525 + * @param volume
  1526 + */
  1527 + Jessibuca.prototype.setVolume = function (volume) {
  1528 + if (this._gainNode) {
  1529 + this._gainNode.gain.setValueAtTime(volume, this._audioContext.currentTime);
  1530 + }
  1531 + };
  1532 +
  1533 + /**
  1534 + * 开启屏幕常亮, 在play前调用
  1535 + * 在手机浏览器上, canvas标签渲染视频并不会像video标签那样保持屏幕常亮
  1536 + * H5目前在chrome\edge 84, android chrome 84及以上有原生亮屏API, 需要是https页面
  1537 + * 其余平台为模拟实现,此时为兼容实现,并不保证所有浏览器都支持
  1538 + */
  1539 + Jessibuca.prototype.setKeepScreenOn = function () {
  1540 + this._opt.keepScreenOn = true;
  1541 + };
  1542 +
  1543 +
  1544 + /**
  1545 + * set fullscreen
  1546 + * @param flag
  1547 + */
  1548 + Jessibuca.prototype.setFullscreen = function (flag) {
  1549 + var fullscreen = !!flag;
  1550 + if (this.fullscreen !== fullscreen) {
  1551 + this.fullscreen = fullscreen;
  1552 + }
  1553 + };
  1554 +
  1555 + function _now() {
  1556 + return new Date().getTime();
  1557 + }
  1558 +
  1559 + Jessibuca.prototype._screenshot = function (filename, format, quality) {
  1560 + filename = filename || _now();
  1561 + var formatType = {
  1562 + png: 'image/png',
  1563 + jpeg: 'image/jpeg',
  1564 + webp: 'image/webp'
  1565 + };
  1566 + var encoderOptions = 0.92;
  1567 +
  1568 + if (typeof quality !== 'undefined') {
  1569 + encoderOptions = Number(quality);
  1570 + }
  1571 +
  1572 + var dataURL = this._canvasElement.toDataURL(formatType[format] || formatType.png, encoderOptions);
  1573 + _downloadImg(_dataURLToFile(dataURL), filename);
  1574 + }
  1575 +
  1576 + /**
  1577 + * 截图,调用后弹出下载框保存截图
  1578 + * @param filename 保存的文件名 默认时间戳
  1579 + * @param format 截图的格式,可选png或jpeg或者webp
  1580 + * @param quality 可选参数,当格式是jpeg或者webp时,压缩质量,取值0.0 ~ 1.0
  1581 + */
  1582 + Jessibuca.prototype.screenshot = function (filename, format, quality) {
  1583 + this._screenshot(filename, format, quality);
  1584 + };
  1585 +
  1586 +
  1587 + var eventSplitter = /\s+/;
  1588 +
  1589 + // Execute callbacks
  1590 + function _callEach(list, args, context) {
  1591 + if (list) {
  1592 + for (var i = 0, len = list.length; i < len; i += 1) {
  1593 + list[i].apply(context, args);
  1594 + }
  1595 + }
  1596 + }
  1597 +
  1598 + /**
  1599 + *
  1600 + * @param events
  1601 + * @param callback
  1602 + * @returns {Jessibuca}
  1603 + */
  1604 + Jessibuca.prototype.on = function (events, callback) {
  1605 + var cache, event, list;
  1606 + if (!callback) return this;
  1607 + cache = this.__events || (this.__events = {});
  1608 + events = events.split(eventSplitter);
  1609 + while (event = events.shift()) {
  1610 + list = cache[event] || (cache[event] = []);
  1611 + list.push(callback);
  1612 + }
  1613 + return this;
  1614 + };
  1615 + /**
  1616 + *
  1617 + * @param events
  1618 + * @param callback
  1619 + * @returns {Jessibuca}
  1620 + * @private
  1621 + */
  1622 + Jessibuca.prototype._off = function () {
  1623 + var cache;
  1624 + if (!(cache = this.__events)) return this;
  1625 + delete this.__events;
  1626 + return this;
  1627 + };
  1628 +
  1629 + /**
  1630 + *
  1631 + * @param events
  1632 + * @returns {Jessibuca}
  1633 + * @private
  1634 + */
  1635 + Jessibuca.prototype._trigger = function (events) {
  1636 + var cache, event, all, list, i, len, rest = [], args;
  1637 + if (!(cache = this.__events)) return this;
  1638 + events = events.split(eventSplitter);
  1639 + // Fill up `rest` with the callback arguments. Since we're only copying
  1640 + // the tail of `arguments`, a loop is much faster than Array#slice.
  1641 + for (i = 1, len = arguments.length; i < len; i++) {
  1642 + rest[i - 1] = arguments[i];
  1643 + }
  1644 + // For each event, walk through the list of callbacks twice, first to
  1645 + // trigger the event, then to trigger any `"all"` callbacks.
  1646 + while (event = events.shift()) {
  1647 + if (list = cache[event]) list = list.slice();
  1648 + // Execute event callbacks.
  1649 + _callEach(list, rest, this);
  1650 + }
  1651 + return this;
  1652 + }
  1653 +
  1654 + if (typeof define === 'function') {
  1655 + define(function () {
  1656 + return Jessibuca;
  1657 + });
  1658 + } else if (typeof exports !== 'undefined') {
  1659 + module.exports = Jessibuca;
  1660 + } else {
  1661 + window.Jessibuca = Jessibuca;
  1662 + }
  1663 +})();
... ...
src/api/car/carStructureApi.js 0 → 100644
  1 +import request from '@/utils/request'
  2 +import { getCommunityId } from '@/api/community/communityApi'
  3 +
  4 +// 查询楼栋和单元树形数据
  5 +export function queryFloorAndUnits(params) {
  6 + return new Promise((resolve, reject) => {
  7 + const communityId = getCommunityId()
  8 + request({
  9 + url: '/floor.queryFloorAndUnits',
  10 + method: 'get',
  11 + params: {
  12 + ...params,
  13 + communityId
  14 + }
  15 + }).then(response => {
  16 + const res = response.data
  17 + resolve(res)
  18 + }).catch(error => {
  19 + reject(error)
  20 + })
  21 + })
  22 +}
  23 +
  24 +// 查询车位结构列表
  25 +export function listCarStructure(params) {
  26 + return new Promise((resolve, reject) => {
  27 + const communityId = getCommunityId()
  28 + request({
  29 + url: '/car.listCarStructure',
  30 + method: 'get',
  31 + params: {
  32 + ...params,
  33 + communityId
  34 + }
  35 + }).then(response => {
  36 + const res = response.data
  37 + resolve({
  38 + data: res.data,
  39 + total: res.total
  40 + })
  41 + }).catch(error => {
  42 + reject(error)
  43 + })
  44 + })
  45 +}
  46 +
  47 +// 获取当前小区ID
  48 +export function getCurrentCommunityId() {
  49 + return getCommunityId()
  50 +}
0 51 \ No newline at end of file
... ...
src/api/fee/payFeeQrcodeApi.js 0 → 100644
  1 +import request from '@/utils/request'
  2 +
  3 +// 获取支付二维码列表
  4 +export function listPayFeeQrcode(params) {
  5 + return new Promise((resolve, reject) => {
  6 + request({
  7 + url: '/payFeeQrcode.listPayFeeQrcode',
  8 + method: 'get',
  9 + params
  10 + }).then(response => {
  11 + const res = response.data
  12 + resolve(res)
  13 + }).catch(error => {
  14 + reject(error)
  15 + })
  16 + })
  17 +}
  18 +
  19 +// 添加支付二维码
  20 +export function savePayFeeQrcode(data) {
  21 + return new Promise((resolve, reject) => {
  22 + request({
  23 + url: '/payFeeQrcode.savePayFeeQrcode',
  24 + method: 'post',
  25 + data
  26 + }).then(response => {
  27 + const res = response.data
  28 + resolve(res)
  29 + }).catch(error => {
  30 + reject(error)
  31 + })
  32 + })
  33 +}
  34 +
  35 +// 更新支付二维码
  36 +export function updatePayFeeQrcode(data) {
  37 + return new Promise((resolve, reject) => {
  38 + request({
  39 + url: '/payFeeQrcode.updatePayFeeQrcode',
  40 + method: 'post',
  41 + data
  42 + }).then(response => {
  43 + const res = response.data
  44 + resolve(res)
  45 + }).catch(error => {
  46 + reject(error)
  47 + })
  48 + })
  49 +}
  50 +
  51 +// 删除支付二维码
  52 +export function deletePayFeeQrcode(data) {
  53 + return new Promise((resolve, reject) => {
  54 + request({
  55 + url: '/payFeeQrcode.deletePayFeeQrcode',
  56 + method: 'post',
  57 + data
  58 + }).then(response => {
  59 + const res = response.data
  60 + resolve(res)
  61 + }).catch(error => {
  62 + reject(error)
  63 + })
  64 + })
  65 +}
0 66 \ No newline at end of file
... ...
src/api/machine/videoControlApi.js 0 → 100644
  1 +import request from '@/utils/request'
  2 +
  3 +// 获取监控区域列表
  4 +export function getMonitorAreas(params) {
  5 + return new Promise((resolve, reject) => {
  6 + request({
  7 + url: '/iot.getOpenApi',
  8 + method: 'get',
  9 + params: {
  10 + ...params,
  11 + iotApiCode: 'listMonitorAreaBmoImpl'
  12 + }
  13 + }).then(response => {
  14 + const res = response.data
  15 + resolve(res)
  16 + }).catch(error => {
  17 + reject(error)
  18 + })
  19 + })
  20 +}
  21 +
  22 +// 获取监控设备列表
  23 +export function getMonitorMachines(params) {
  24 + return new Promise((resolve, reject) => {
  25 + request({
  26 + url: '/iot.getOpenApi',
  27 + method: 'get',
  28 + params: {
  29 + ...params,
  30 + iotApiCode: 'listMonitorMachineBmoImpl'
  31 + }
  32 + }).then(response => {
  33 + const res = response.data
  34 + resolve(res)
  35 + }).catch(error => {
  36 + reject(error)
  37 + })
  38 + })
  39 +}
  40 +
  41 +// 获取视频播放地址
  42 +export function getPlayVideoUrl(params) {
  43 + return new Promise((resolve, reject) => {
  44 + request({
  45 + url: '/iot.getOpenApi',
  46 + method: 'get',
  47 + params: {
  48 + ...params,
  49 + iotApiCode: 'getPlayVideoUrlBmoImpl'
  50 + }
  51 + }).then(response => {
  52 + const res = response.data
  53 + resolve(res)
  54 + }).catch(error => {
  55 + reject(error)
  56 + })
  57 + })
  58 +}
0 59 \ No newline at end of file
... ...
src/api/room/listPropertyRightRegistrationDetailApi.js 0 → 100644
  1 +import request from '@/utils/request'
  2 +
  3 +/**
  4 + * 获取产权登记详情列表
  5 + * @param {Object} params 查询参数
  6 + * @returns {Promise}
  7 + */
  8 +export function listPropertyRightRegistrationDetail(params) {
  9 + return new Promise((resolve, reject) => {
  10 + request({
  11 + url: '/propertyRightRegistrationDetail.listPropertyRightRegistrationDetail',
  12 + method: 'get',
  13 + params
  14 + }).then(response => {
  15 + const res = response.data
  16 + resolve(res)
  17 + }).catch(error => {
  18 + reject(error)
  19 + })
  20 + })
  21 +}
  22 +
  23 +/**
  24 + * 更新产权登记详情
  25 + * @param {Object} data 更新数据
  26 + * @returns {Promise}
  27 + */
  28 +export function updatePropertyRightRegistrationDetail(data) {
  29 + return new Promise((resolve, reject) => {
  30 + request({
  31 + url: '/propertyRightRegistrationDetail.updatePropertyRightRegistrationDetail',
  32 + method: 'post',
  33 + data
  34 + }).then(response => {
  35 + const res = response.data
  36 + resolve(res)
  37 + }).catch(error => {
  38 + reject(error)
  39 + })
  40 + })
  41 +}
  42 +
  43 +/**
  44 + * 获取产权登记详情
  45 + * @param {String} prrdId 详情ID
  46 + * @returns {Promise}
  47 + */
  48 +export function getPropertyRightRegistrationDetail(prrdId) {
  49 + return new Promise((resolve, reject) => {
  50 + request({
  51 + url: '/propertyRightRegistrationDetail.getPropertyRightRegistrationDetail',
  52 + method: 'get',
  53 + params: { prrdId }
  54 + }).then(response => {
  55 + const res = response.data
  56 + resolve(res)
  57 + }).catch(error => {
  58 + reject(error)
  59 + })
  60 + })
  61 +}
0 62 \ No newline at end of file
... ...
src/api/room/propertyRightRegistrationManageApi.js 0 → 100644
  1 +import request from '@/utils/request'
  2 +import { getCommunityId } from '@/api/community/communityApi'
  3 +
  4 +/**
  5 + * 获取产权登记列表
  6 + * @param {Object} params 查询参数
  7 + * @returns {Promise}
  8 + */
  9 +export function listPropertyRightRegistration(params) {
  10 + return new Promise((resolve, reject) => {
  11 + const communityId = getCommunityId()
  12 + request({
  13 + url: '/propertyRightRegistration.listPropertyRightRegistration',
  14 + method: 'get',
  15 + params: {
  16 + ...params,
  17 + communityId
  18 + }
  19 + }).then(response => {
  20 + const res = response.data
  21 + resolve({
  22 + data: res.data,
  23 + total: res.total,
  24 + records: res.records
  25 + })
  26 + }).catch(error => {
  27 + reject(error)
  28 + })
  29 + })
  30 +}
  31 +
  32 +/**
  33 + * 添加产权登记
  34 + * @param {Object} data 产权登记数据
  35 + * @returns {Promise}
  36 + */
  37 +export function savePropertyRightRegistration(data) {
  38 + return new Promise((resolve, reject) => {
  39 + const communityId = getCommunityId()
  40 + request({
  41 + url: '/propertyRightRegistration.savePropertyRightRegistration',
  42 + method: 'post',
  43 + data: {
  44 + ...data,
  45 + communityId
  46 + }
  47 + }).then(response => {
  48 + const res = response.data
  49 + resolve(res)
  50 + }).catch(error => {
  51 + reject(error)
  52 + })
  53 + })
  54 +}
  55 +
  56 +/**
  57 + * 更新产权登记
  58 + * @param {Object} data 更新数据
  59 + * @returns {Promise}
  60 + */
  61 +export function updatePropertyRightRegistration(data) {
  62 + return new Promise((resolve, reject) => {
  63 + const communityId = getCommunityId()
  64 + request({
  65 + url: '/propertyRightRegistration.updatePropertyRightRegistration',
  66 + method: 'post',
  67 + data: {
  68 + ...data,
  69 + communityId
  70 + }
  71 + }).then(response => {
  72 + const res = response.data
  73 + resolve(res)
  74 + }).catch(error => {
  75 + reject(error)
  76 + })
  77 + })
  78 +}
  79 +
  80 +/**
  81 + * 删除产权登记
  82 + * @param {String} prrId 产权登记ID
  83 + * @returns {Promise}
  84 + */
  85 +export function deletePropertyRightRegistration(prrId) {
  86 + return new Promise((resolve, reject) => {
  87 + const communityId = getCommunityId()
  88 + request({
  89 + url: '/propertyRightRegistration.deletePropertyRightRegistration',
  90 + method: 'post',
  91 + data: {
  92 + prrId,
  93 + communityId
  94 + }
  95 + }).then(response => {
  96 + const res = response.data
  97 + resolve(res)
  98 + }).catch(error => {
  99 + reject(error)
  100 + })
  101 + })
  102 +}
0 103 \ No newline at end of file
... ...
src/api/room/roomStructureApi.js 0 → 100644
  1 +import request from '@/utils/request'
  2 +import { getCommunityId } from '@/api/community/communityApi'
  3 +
  4 +/**
  5 + * 查询楼层和单元树数据
  6 + * @returns {Promise}
  7 + */
  8 +export function queryFloorAndUnits() {
  9 + return new Promise((resolve, reject) => {
  10 + request({
  11 + url: '/floor.queryFloorAndUnits',
  12 + method: 'get',
  13 + params: {
  14 + communityId: getCommunityId()
  15 + }
  16 + }).then(response => {
  17 + resolve(response.data)
  18 + }).catch(error => {
  19 + reject(error)
  20 + })
  21 + })
  22 +}
  23 +
  24 +/**
  25 + * 查询房间结构列表
  26 + * @param {Object} params 查询参数
  27 + * @param {number} params.page 页码
  28 + * @param {number} params.row 每页条数
  29 + * @param {string} params.unitId 单元ID
  30 + * @returns {Promise}
  31 + */
  32 +export function listRoomStructure(params) {
  33 + return new Promise((resolve, reject) => {
  34 + request({
  35 + url: '/room.listRoomStructure',
  36 + method: 'get',
  37 + params: {
  38 + ...params,
  39 + communityId: getCommunityId()
  40 + }
  41 + }).then(response => {
  42 + const res = response.data
  43 + resolve({
  44 + data: res.data,
  45 + total: res.total
  46 + })
  47 + }).catch(error => {
  48 + reject(error)
  49 + })
  50 + })
  51 +}
  52 +
  53 +/**
  54 + * 获取房间状态字典
  55 + * @returns {Promise}
  56 + */
  57 +export function getRoomStateDict() {
  58 + return new Promise((resolve, reject) => {
  59 + request({
  60 + url: '/dict.getDict',
  61 + method: 'get',
  62 + params: {
  63 + dictType: 'room_state'
  64 + }
  65 + }).then(response => {
  66 + resolve(response.data)
  67 + }).catch(error => {
  68 + reject(error)
  69 + })
  70 + })
  71 +}
0 72 \ No newline at end of file
... ...
src/api/system/assetImportLogApi.js 0 → 100644
  1 +import request from '@/utils/request'
  2 +
  3 +/**
  4 + * Query asset import logs
  5 + * @param {Object} params
  6 + * @param {number} params.page - Page number
  7 + * @param {number} params.row - Page size
  8 + * @param {string} params.communityId - Community ID
  9 + * @returns {Promise} Promise object represents the import logs
  10 + */
  11 +export function queryAssetImportLog(params) {
  12 + return new Promise((resolve, reject) => {
  13 + request({
  14 + url: '/assetImportLog/queryAssetImportLog',
  15 + method: 'get',
  16 + params
  17 + }).then(response => {
  18 + const res = response.data
  19 + resolve(res)
  20 + }).catch(error => {
  21 + reject(error)
  22 + })
  23 + })
  24 +}
0 25 \ No newline at end of file
... ...
src/api/system/assetImportLogDetailApi.js 0 → 100644
  1 +import request from '@/utils/request'
  2 +
  3 +/**
  4 + * 查询资产导入日志详情
  5 + * @param {Object} params 查询参数
  6 + * @returns {Promise} 请求Promise
  7 + */
  8 +export function queryAssetImportLogDetail(params) {
  9 + return new Promise((resolve, reject) => {
  10 + request({
  11 + url: '/assetImportLogDetail/queryAssetImportLogDetail',
  12 + method: 'get',
  13 + params
  14 + }).then(response => {
  15 + const res = response.data
  16 + resolve(res)
  17 + }).catch(error => {
  18 + reject(error)
  19 + })
  20 + })
  21 +}
  22 +
  23 +/**
  24 + * 查询资产导入日志类型
  25 + * @param {Object} params 查询参数
  26 + * @returns {Promise} 请求Promise
  27 + */
  28 +export function queryAssetImportLogType(params) {
  29 + return new Promise((resolve, reject) => {
  30 + request({
  31 + url: '/log.queryAssetImportLogType',
  32 + method: 'get',
  33 + params
  34 + }).then(response => {
  35 + const res = response.data
  36 + resolve(res)
  37 + }).catch(error => {
  38 + reject(error)
  39 + })
  40 + })
  41 +}
0 42 \ No newline at end of file
... ...
src/api/system/downloadTempFileApi.js 0 → 100644
  1 +import request from '@/utils/request'
  2 +import { getCommunityId } from '@/api/community/communityApi'
  3 +
  4 +// 获取下载文件列表
  5 +export function listUserDownloadFile(params) {
  6 + return new Promise((resolve, reject) => {
  7 + request({
  8 + url: '/userDownloadFile.listUserDownloadFile',
  9 + method: 'get',
  10 + params: {
  11 + ...params,
  12 + communityId: getCommunityId()
  13 + }
  14 + }).then(response => {
  15 + const res = response.data
  16 + resolve(res)
  17 + }).catch(error => {
  18 + reject(error)
  19 + })
  20 + })
  21 +}
  22 +
  23 +// 删除下载文件
  24 +export function deleteUserDownloadFile(data) {
  25 + return new Promise((resolve, reject) => {
  26 + request({
  27 + url: '/userDownloadFile.deleteUserDownloadFile',
  28 + method: 'post',
  29 + data: {
  30 + ...data,
  31 + communityId: getCommunityId()
  32 + }
  33 + }).then(response => {
  34 + const res = response.data
  35 + resolve(res)
  36 + }).catch(error => {
  37 + reject(error)
  38 + })
  39 + })
  40 +}
0 41 \ No newline at end of file
... ...
src/api/system/feePrintPageManageApi.js 0 → 100644
  1 +import request from '@/utils/request'
  2 +import { getCommunityId } from '@/api/community/communityApi'
  3 +
  4 +// 获取收据模板列表
  5 +export function listFeePrintPage(params) {
  6 + return new Promise((resolve, reject) => {
  7 + request({
  8 + url: '/feePrintPage.listFeePrintPage',
  9 + method: 'get',
  10 + params: {
  11 + ...params,
  12 + communityId: getCommunityId()
  13 + }
  14 + }).then(response => {
  15 + const res = response.data
  16 + resolve(res)
  17 + }).catch(error => {
  18 + reject(error)
  19 + })
  20 + })
  21 +}
  22 +
  23 +// 获取收据页面模板列表
  24 +export function listFeePrintPageTemplate(params) {
  25 + return new Promise((resolve, reject) => {
  26 + request({
  27 + url: '/feePrintPageTemplate.listFeePrintPageTemplate',
  28 + method: 'get',
  29 + params: {
  30 + ...params,
  31 + communityId: getCommunityId()
  32 + }
  33 + }).then(response => {
  34 + const res = response.data
  35 + resolve(res)
  36 + }).catch(error => {
  37 + reject(error)
  38 + })
  39 + })
  40 +}
  41 +
  42 +// 添加收据模板
  43 +export function saveFeePrintPage(data) {
  44 + return new Promise((resolve, reject) => {
  45 + request({
  46 + url: '/feePrintPage.saveFeePrintPage',
  47 + method: 'post',
  48 + data: {
  49 + ...data,
  50 + communityId: getCommunityId()
  51 + }
  52 + }).then(response => {
  53 + const res = response.data
  54 + resolve(res)
  55 + }).catch(error => {
  56 + reject(error)
  57 + })
  58 + })
  59 +}
  60 +
  61 +// 更新收据模板
  62 +export function updateFeePrintPage(data) {
  63 + return new Promise((resolve, reject) => {
  64 + request({
  65 + url: '/feePrintPage.updateFeePrintPage',
  66 + method: 'post',
  67 + data: {
  68 + ...data,
  69 + communityId: getCommunityId()
  70 + }
  71 + }).then(response => {
  72 + const res = response.data
  73 + resolve(res)
  74 + }).catch(error => {
  75 + reject(error)
  76 + })
  77 + })
  78 +}
  79 +
  80 +// 删除收据模板
  81 +export function deleteFeePrintPage(data) {
  82 + return new Promise((resolve, reject) => {
  83 + request({
  84 + url: '/feePrintPage.deleteFeePrintPage',
  85 + method: 'post',
  86 + data: {
  87 + ...data,
  88 + communityId: getCommunityId()
  89 + }
  90 + }).then(response => {
  91 + const res = response.data
  92 + resolve(res)
  93 + }).catch(error => {
  94 + reject(error)
  95 + })
  96 + })
  97 +}
  98 +
  99 +// 更新收据模板状态
  100 +export function updateFeePrintPageState(data) {
  101 + return new Promise((resolve, reject) => {
  102 + request({
  103 + url: '/feePrintPage.updateFeePrintPage',
  104 + method: 'post',
  105 + data: {
  106 + ...data,
  107 + communityId: getCommunityId()
  108 + }
  109 + }).then(response => {
  110 + const res = response.data
  111 + resolve(res)
  112 + }).catch(error => {
  113 + reject(error)
  114 + })
  115 + })
  116 +}
0 117 \ No newline at end of file
... ...
src/components/TestScript.vue 0 → 100644
  1 +<template>
  2 + <div class="test-script">
  3 + <h3>全局脚本测试</h3>
  4 + <div class="test-results">
  5 + <p><strong>Qs 库状态:</strong> {{ qsStatus }}</p>
  6 + <p><strong>Qs 版本:</strong> {{ qsVersion }}</p>
  7 + <p><strong>测试结果:</strong> {{ testResult }}</p>
  8 + </div>
  9 + <el-button @click="testQs" type="primary">测试 Qs 功能</el-button>
  10 + </div>
  11 +</template>
  12 +
  13 +<script>
  14 +export default {
  15 + name: 'TestScript',
  16 + data() {
  17 + return {
  18 + qsStatus: '检查中...',
  19 + qsVersion: '未知',
  20 + testResult: '未测试'
  21 + }
  22 + },
  23 + mounted() {
  24 + this.checkQsStatus()
  25 + },
  26 + methods: {
  27 + checkQsStatus() {
  28 + // 检查全局 Qs
  29 + if (typeof window.Qs !== 'undefined') {
  30 + this.qsStatus = '已加载'
  31 + this.qsVersion = window.Qs.VERSION || '未知版本'
  32 + } else {
  33 + this.qsStatus = '未加载'
  34 + }
  35 +
  36 + // 检查 Vue 原型上的 Qs
  37 + if (this.$qs) {
  38 + this.qsStatus += ' (Vue 原型已挂载)'
  39 + }
  40 + },
  41 + testQs() {
  42 + try {
  43 + // 测试 Qs 功能
  44 + const testObj = { name: 'test', value: 123 }
  45 + const queryString = this.$qs.stringify(testObj)
  46 + const parsedObj = this.$qs.parse(queryString)
  47 +
  48 + this.testResult = `成功! 序列化: ${queryString}, 解析: ${JSON.stringify(parsedObj)}`
  49 + console.log('Qs 测试成功:', { queryString, parsedObj })
  50 + } catch (error) {
  51 + this.testResult = `失败: ${error.message}`
  52 + console.error('Qs 测试失败:', error)
  53 + }
  54 + }
  55 + }
  56 +}
  57 +</script>
  58 +
  59 +<style scoped>
  60 +.test-script {
  61 + padding: 20px;
  62 + border: 1px solid #ddd;
  63 + border-radius: 4px;
  64 + margin: 20px;
  65 +}
  66 +
  67 +.test-results {
  68 + margin: 15px 0;
  69 + padding: 10px;
  70 + background-color: #f5f5f5;
  71 + border-radius: 4px;
  72 +}
  73 +
  74 +.test-results p {
  75 + margin: 5px 0;
  76 +}
  77 +</style>
0 78 \ No newline at end of file
... ...
src/components/fee/addPayFeeQrcode.vue 0 → 100644
  1 +<template>
  2 + <el-dialog
  3 + :title="$t('payFeeQrcode.add.title')"
  4 + :visible.sync="visible"
  5 + width="50%"
  6 + @close="handleClose"
  7 + >
  8 + <el-form
  9 + ref="form"
  10 + :model="form"
  11 + :rules="rules"
  12 + label-width="120px"
  13 + >
  14 + <el-form-item
  15 + :label="$t('payFeeQrcode.form.qrcodeName')"
  16 + prop="qrcodeName"
  17 + >
  18 + <el-input
  19 + v-model="form.qrcodeName"
  20 + :placeholder="$t('payFeeQrcode.placeholder.qrcodeName')"
  21 + />
  22 + </el-form-item>
  23 +
  24 + <el-form-item
  25 + :label="$t('payFeeQrcode.form.queryWay')"
  26 + prop="queryWay"
  27 + >
  28 + <el-select
  29 + v-model="form.queryWay"
  30 + :placeholder="$t('payFeeQrcode.placeholder.queryWay')"
  31 + style="width:100%"
  32 + >
  33 + <el-option
  34 + v-for="item in queryWayOptions"
  35 + :key="item.value"
  36 + :label="item.label"
  37 + :value="item.value"
  38 + />
  39 + </el-select>
  40 + </el-form-item>
  41 +
  42 + <el-form-item
  43 + :label="$t('payFeeQrcode.form.smsValidate')"
  44 + prop="smsValidate"
  45 + >
  46 + <el-select
  47 + v-model="form.smsValidate"
  48 + :placeholder="$t('payFeeQrcode.placeholder.smsValidate')"
  49 + style="width:100%"
  50 + >
  51 + <el-option
  52 + :label="$t('common.yes')"
  53 + value="ON"
  54 + />
  55 + <el-option
  56 + :label="$t('common.no')"
  57 + value="OFF"
  58 + />
  59 + </el-select>
  60 + </el-form-item>
  61 +
  62 + <el-form-item
  63 + :label="$t('payFeeQrcode.form.customFee')"
  64 + prop="customFee"
  65 + >
  66 + <el-select
  67 + v-model="form.customFee"
  68 + :placeholder="$t('payFeeQrcode.placeholder.customFee')"
  69 + style="width:100%"
  70 + >
  71 + <el-option
  72 + :label="$t('common.yes')"
  73 + value="ON"
  74 + />
  75 + <el-option
  76 + :label="$t('common.no')"
  77 + value="OFF"
  78 + />
  79 + </el-select>
  80 + </el-form-item>
  81 +
  82 + <el-form-item
  83 + :label="$t('payFeeQrcode.form.preFee')"
  84 + prop="preFee"
  85 + >
  86 + <el-select
  87 + v-model="form.preFee"
  88 + :placeholder="$t('payFeeQrcode.placeholder.preFee')"
  89 + style="width:100%"
  90 + >
  91 + <el-option
  92 + :label="$t('common.yes')"
  93 + value="ON"
  94 + />
  95 + <el-option
  96 + :label="$t('common.no')"
  97 + value="OFF"
  98 + />
  99 + </el-select>
  100 + </el-form-item>
  101 +
  102 + <el-form-item
  103 + :label="$t('payFeeQrcode.form.content')"
  104 + prop="content"
  105 + >
  106 + <el-input
  107 + v-model="form.content"
  108 + type="textarea"
  109 + :rows="3"
  110 + :placeholder="$t('payFeeQrcode.placeholder.content')"
  111 + />
  112 + </el-form-item>
  113 + </el-form>
  114 +
  115 + <span slot="footer" class="dialog-footer">
  116 + <el-button @click="visible = false">
  117 + {{ $t('common.cancel') }}
  118 + </el-button>
  119 + <el-button
  120 + type="primary"
  121 + @click="handleSubmit"
  122 + >
  123 + {{ $t('common.confirm') }}
  124 + </el-button>
  125 + </span>
  126 + </el-dialog>
  127 +</template>
  128 +
  129 +<script>
  130 +import { savePayFeeQrcode } from '@/api/fee/payFeeQrcodeApi'
  131 +import { getCommunityId } from '@/api/community/communityApi'
  132 +
  133 +export default {
  134 + name: 'AddPayFeeQrcode',
  135 + data() {
  136 + return {
  137 + visible: false,
  138 + form: {
  139 + qrcodeName: '',
  140 + queryWay: '',
  141 + smsValidate: 'OFF',
  142 + customFee: 'OFF',
  143 + preFee: 'OFF',
  144 + content: '',
  145 + state: 'ON',
  146 + feeType: 'OWNER',
  147 + communityId: ''
  148 + },
  149 + rules: {
  150 + qrcodeName: [
  151 + { required: true, message: this.$t('payFeeQrcode.rules.qrcodeName'), trigger: 'blur' },
  152 + { max: 128, message: this.$t('payFeeQrcode.rules.qrcodeNameMax'), trigger: 'blur' }
  153 + ],
  154 + queryWay: [
  155 + { required: true, message: this.$t('payFeeQrcode.rules.queryWay'), trigger: 'change' }
  156 + ],
  157 + smsValidate: [
  158 + { required: true, message: this.$t('payFeeQrcode.rules.smsValidate'), trigger: 'change' }
  159 + ],
  160 + customFee: [
  161 + { required: true, message: this.$t('payFeeQrcode.rules.customFee'), trigger: 'change' }
  162 + ],
  163 + preFee: [
  164 + { required: true, message: this.$t('payFeeQrcode.rules.preFee'), trigger: 'change' }
  165 + ],
  166 + content: [
  167 + { required: true, message: this.$t('payFeeQrcode.rules.content'), trigger: 'blur' }
  168 + ]
  169 + },
  170 + queryWayOptions: [
  171 + { value: '1001', label: this.$t('payFeeQrcode.queryWay.byPhone') },
  172 + { value: '2002', label: this.$t('payFeeQrcode.queryWay.byHouse') },
  173 + { value: '3003', label: this.$t('payFeeQrcode.queryWay.byBoth') }
  174 + ]
  175 + }
  176 + },
  177 + methods: {
  178 + open() {
  179 + this.visible = true
  180 + this.form.communityId = getCommunityId()
  181 + this.$nextTick(() => {
  182 + this.$refs.form && this.$refs.form.resetFields()
  183 + })
  184 + },
  185 + handleClose() {
  186 + this.$refs.form.resetFields()
  187 + },
  188 + handleSubmit() {
  189 + this.$refs.form.validate(async valid => {
  190 + if (valid) {
  191 + try {
  192 + await savePayFeeQrcode(this.form)
  193 + this.$message.success(this.$t('payFeeQrcode.message.addSuccess'))
  194 + this.visible = false
  195 + this.$emit('success')
  196 + } catch (error) {
  197 + this.$message.error(error.message || this.$t('payFeeQrcode.message.addFailed'))
  198 + }
  199 + }
  200 + })
  201 + }
  202 + }
  203 +}
  204 +</script>
0 205 \ No newline at end of file
... ...
src/components/fee/deletePayFeeQrcode.vue 0 → 100644
  1 +<template>
  2 + <el-dialog
  3 + :title="$t('payFeeQrcode.delete.title')"
  4 + :visible.sync="visible"
  5 + width="30%"
  6 + @close="handleClose"
  7 + >
  8 + <div class="text-center">
  9 + <i class="el-icon-warning" style="font-size: 60px; color: #E6A23C;"></i>
  10 + <p style="margin: 20px 0; font-size: 16px;">
  11 + {{ $t('payFeeQrcode.delete.confirmText') }}
  12 + </p>
  13 + </div>
  14 +
  15 + <span slot="footer" class="dialog-footer">
  16 + <el-button @click="visible = false">
  17 + {{ $t('common.cancel') }}
  18 + </el-button>
  19 + <el-button
  20 + type="primary"
  21 + @click="handleConfirm"
  22 + >
  23 + {{ $t('common.confirm') }}
  24 + </el-button>
  25 + </span>
  26 + </el-dialog>
  27 +</template>
  28 +
  29 +<script>
  30 +import { deletePayFeeQrcode } from '@/api/fee/payFeeQrcodeApi'
  31 +import { getCommunityId } from '@/api/community/communityApi'
  32 +
  33 +export default {
  34 + name: 'DeletePayFeeQrcode',
  35 + data() {
  36 + return {
  37 + visible: false,
  38 + currentData: {
  39 + pfqId: '',
  40 + communityId: ''
  41 + }
  42 + }
  43 + },
  44 + methods: {
  45 + open(data) {
  46 + this.visible = true
  47 + this.currentData = {
  48 + pfqId: data.pfqId,
  49 + communityId: getCommunityId()
  50 + }
  51 + },
  52 + handleClose() {
  53 + this.currentData = {
  54 + pfqId: '',
  55 + communityId: ''
  56 + }
  57 + },
  58 + async handleConfirm() {
  59 + try {
  60 + await deletePayFeeQrcode(this.currentData)
  61 + this.$message.success(this.$t('payFeeQrcode.message.deleteSuccess'))
  62 + this.visible = false
  63 + this.$emit('success')
  64 + } catch (error) {
  65 + this.$message.error(error.message || this.$t('payFeeQrcode.message.deleteFailed'))
  66 + }
  67 + }
  68 + }
  69 +}
  70 +</script>
0 71 \ No newline at end of file
... ...
src/components/fee/editPayFeeQrcode.vue 0 → 100644
  1 +<template>
  2 + <el-dialog
  3 + :title="$t('payFeeQrcode.edit.title')"
  4 + :visible.sync="visible"
  5 + width="50%"
  6 + @close="handleClose"
  7 + >
  8 + <el-form
  9 + ref="form"
  10 + :model="form"
  11 + :rules="rules"
  12 + label-width="120px"
  13 + >
  14 + <el-form-item
  15 + :label="$t('payFeeQrcode.form.qrcodeName')"
  16 + prop="qrcodeName"
  17 + >
  18 + <el-input
  19 + v-model="form.qrcodeName"
  20 + :placeholder="$t('payFeeQrcode.placeholder.qrcodeName')"
  21 + />
  22 + </el-form-item>
  23 +
  24 + <el-form-item
  25 + :label="$t('payFeeQrcode.form.queryWay')"
  26 + prop="queryWay"
  27 + >
  28 + <el-select
  29 + v-model="form.queryWay"
  30 + :placeholder="$t('payFeeQrcode.placeholder.queryWay')"
  31 + style="width:100%"
  32 + >
  33 + <el-option
  34 + v-for="item in queryWayOptions"
  35 + :key="item.value"
  36 + :label="item.label"
  37 + :value="item.value"
  38 + />
  39 + </el-select>
  40 + </el-form-item>
  41 +
  42 + <el-form-item
  43 + :label="$t('payFeeQrcode.form.smsValidate')"
  44 + prop="smsValidate"
  45 + >
  46 + <el-select
  47 + v-model="form.smsValidate"
  48 + :placeholder="$t('payFeeQrcode.placeholder.smsValidate')"
  49 + style="width:100%"
  50 + >
  51 + <el-option
  52 + :label="$t('common.yes')"
  53 + value="ON"
  54 + />
  55 + <el-option
  56 + :label="$t('common.no')"
  57 + value="OFF"
  58 + />
  59 + </el-select>
  60 + </el-form-item>
  61 +
  62 + <el-form-item
  63 + :label="$t('payFeeQrcode.form.customFee')"
  64 + prop="customFee"
  65 + >
  66 + <el-select
  67 + v-model="form.customFee"
  68 + :placeholder="$t('payFeeQrcode.placeholder.customFee')"
  69 + style="width:100%"
  70 + >
  71 + <el-option
  72 + :label="$t('common.yes')"
  73 + value="ON"
  74 + />
  75 + <el-option
  76 + :label="$t('common.no')"
  77 + value="OFF"
  78 + />
  79 + </el-select>
  80 + </el-form-item>
  81 +
  82 + <el-form-item
  83 + :label="$t('payFeeQrcode.form.preFee')"
  84 + prop="preFee"
  85 + >
  86 + <el-select
  87 + v-model="form.preFee"
  88 + :placeholder="$t('payFeeQrcode.placeholder.preFee')"
  89 + style="width:100%"
  90 + >
  91 + <el-option
  92 + :label="$t('common.yes')"
  93 + value="ON"
  94 + />
  95 + <el-option
  96 + :label="$t('common.no')"
  97 + value="OFF"
  98 + />
  99 + </el-select>
  100 + </el-form-item>
  101 +
  102 + <el-form-item
  103 + :label="$t('payFeeQrcode.form.content')"
  104 + prop="content"
  105 + >
  106 + <el-input
  107 + v-model="form.content"
  108 + type="textarea"
  109 + :rows="3"
  110 + :placeholder="$t('payFeeQrcode.placeholder.content')"
  111 + />
  112 + </el-form-item>
  113 + </el-form>
  114 +
  115 + <span slot="footer" class="dialog-footer">
  116 + <el-button @click="visible = false">
  117 + {{ $t('common.cancel') }}
  118 + </el-button>
  119 + <el-button
  120 + type="primary"
  121 + @click="handleSubmit"
  122 + >
  123 + {{ $t('common.confirm') }}
  124 + </el-button>
  125 + </span>
  126 + </el-dialog>
  127 +</template>
  128 +
  129 +<script>
  130 +import { updatePayFeeQrcode } from '@/api/fee/payFeeQrcodeApi'
  131 +import { getCommunityId } from '@/api/community/communityApi'
  132 +
  133 +export default {
  134 + name: 'EditPayFeeQrcode',
  135 + data() {
  136 + return {
  137 + visible: false,
  138 + form: {
  139 + pfqId: '',
  140 + qrcodeName: '',
  141 + queryWay: '',
  142 + smsValidate: '',
  143 + customFee: '',
  144 + preFee: '',
  145 + content: '',
  146 + state: '',
  147 + feeType: '',
  148 + communityId: ''
  149 + },
  150 + rules: {
  151 + qrcodeName: [
  152 + { required: true, message: this.$t('payFeeQrcode.rules.qrcodeName'), trigger: 'blur' },
  153 + { max: 128, message: this.$t('payFeeQrcode.rules.qrcodeNameMax'), trigger: 'blur' }
  154 + ],
  155 + queryWay: [
  156 + { required: true, message: this.$t('payFeeQrcode.rules.queryWay'), trigger: 'change' }
  157 + ],
  158 + smsValidate: [
  159 + { required: true, message: this.$t('payFeeQrcode.rules.smsValidate'), trigger: 'change' }
  160 + ],
  161 + customFee: [
  162 + { required: true, message: this.$t('payFeeQrcode.rules.customFee'), trigger: 'change' }
  163 + ],
  164 + preFee: [
  165 + { required: true, message: this.$t('payFeeQrcode.rules.preFee'), trigger: 'change' }
  166 + ],
  167 + content: [
  168 + { required: true, message: this.$t('payFeeQrcode.rules.content'), trigger: 'blur' },
  169 + { max: 512, message: this.$t('payFeeQrcode.rules.contentMax'), trigger: 'blur' }
  170 + ],
  171 + pfqId: [
  172 + { required: true, message: this.$t('payFeeQrcode.rules.pfqId'), trigger: 'blur' }
  173 + ]
  174 + },
  175 + queryWayOptions: [
  176 + { value: '1001', label: this.$t('payFeeQrcode.queryWay.byPhone') },
  177 + { value: '2002', label: this.$t('payFeeQrcode.queryWay.byHouse') },
  178 + { value: '3003', label: this.$t('payFeeQrcode.queryWay.byBoth') }
  179 + ]
  180 + }
  181 + },
  182 + methods: {
  183 + open(data) {
  184 + this.visible = true
  185 + this.form = { ...data }
  186 + this.form.communityId = getCommunityId()
  187 + this.$nextTick(() => {
  188 + this.$refs.form && this.$refs.form.clearValidate()
  189 + })
  190 + },
  191 + handleClose() {
  192 + this.$refs.form.resetFields()
  193 + },
  194 + handleSubmit() {
  195 + this.$refs.form.validate(async valid => {
  196 + if (valid) {
  197 + try {
  198 + await updatePayFeeQrcode(this.form)
  199 + this.$message.success(this.$t('payFeeQrcode.message.editSuccess'))
  200 + this.visible = false
  201 + this.$emit('success')
  202 + } catch (error) {
  203 + this.$message.error(error.message || this.$t('payFeeQrcode.message.editFailed'))
  204 + }
  205 + }
  206 + })
  207 + }
  208 + }
  209 +}
  210 +</script>
0 211 \ No newline at end of file
... ...
src/components/fee/viewPayFeeQrcode.vue 0 → 100644
  1 +<template>
  2 + <el-dialog
  3 + :title="$t('payFeeQrcode.view.title')"
  4 + :visible.sync="visible"
  5 + width="400px"
  6 + @close="handleClose"
  7 + >
  8 + <div class="text-center">
  9 + <div id="qrcode" style="width: 200px; height: 200px; margin: 0 auto;"></div>
  10 + <p style="margin: 15px 0; font-size: 16px; font-weight: bold;">
  11 + {{ form.qrcodeName }}
  12 + </p>
  13 + <p style="color: #999;">
  14 + {{ $t('payFeeQrcode.view.tip') }}
  15 + </p>
  16 + </div>
  17 +
  18 + <span slot="footer" class="dialog-footer">
  19 + <el-button @click="visible = false">
  20 + {{ $t('common.close') }}
  21 + </el-button>
  22 + </span>
  23 + </el-dialog>
  24 +</template>
  25 +
  26 +<script>
  27 +import QRCode from 'qrcodejs2'
  28 +
  29 +export default {
  30 + name: 'ViewPayFeeQrcode',
  31 + data() {
  32 + return {
  33 + visible: false,
  34 + form: {
  35 + qrcodeName: '',
  36 + qrCodeUrl: ''
  37 + },
  38 + qrcode: null
  39 + }
  40 + },
  41 + methods: {
  42 + open(data) {
  43 + this.visible = true
  44 + this.form = { ...data }
  45 + this.$nextTick(() => {
  46 + this.generateQRCode()
  47 + })
  48 + },
  49 + handleClose() {
  50 + if (this.qrcode) {
  51 + this.qrcode.clear()
  52 + document.getElementById('qrcode').innerHTML = ''
  53 + }
  54 + this.form = {
  55 + qrcodeName: '',
  56 + qrCodeUrl: ''
  57 + }
  58 + },
  59 + generateQRCode() {
  60 + if (this.qrcode) {
  61 + this.qrcode.clear()
  62 + document.getElementById('qrcode').innerHTML = ''
  63 + }
  64 +
  65 + if (this.form.qrCodeUrl) {
  66 + this.qrcode = new QRCode(document.getElementById('qrcode'), {
  67 + text: this.form.qrCodeUrl,
  68 + width: 200,
  69 + height: 200,
  70 + colorDark: '#000000',
  71 + colorLight: '#ffffff',
  72 + correctLevel: QRCode.CorrectLevel.L
  73 + })
  74 + }
  75 + }
  76 + },
  77 + beforeDestroy() {
  78 + if (this.qrcode) {
  79 + this.qrcode.clear()
  80 + }
  81 + }
  82 +}
  83 +</script>
  84 +
  85 +<style scoped>
  86 +#qrcode {
  87 + display: flex;
  88 + justify-content: center;
  89 + align-items: center;
  90 + border: 1px solid #eee;
  91 + padding: 10px;
  92 +}
  93 +</style>
0 94 \ No newline at end of file
... ...
src/components/machine/cameraControlVideo.vue 0 → 100644
  1 +<template>
  2 + <div class="camera-control-video">
  3 + <el-row>
  4 + <el-col :span="12">
  5 + <h3>{{ $t('cameraControlVideo.camera') }}</h3>
  6 + </el-col>
  7 + <el-col :span="12" class="text-right">
  8 + <el-button-group>
  9 + <el-button size="small" :type="cameraControlVideoInfo.cameraCount === 4 ? 'primary' : ''"
  10 + @click="_changeCount(4)">
  11 + {{ $t('cameraControlVideo.fourWay') }}
  12 + </el-button>
  13 + <el-button size="small" :type="cameraControlVideoInfo.cameraCount === 6 ? 'primary' : ''"
  14 + @click="_changeCount(6)">
  15 + {{ $t('cameraControlVideo.sixWay') }}
  16 + </el-button>
  17 + </el-button-group>
  18 + </el-col>
  19 + </el-row>
  20 + <el-row class="margin-top">
  21 + <template v-if="cameraControlVideoInfo.cameraCount === 4">
  22 + <el-col v-for="(item, index) in cameraControlVideoInfo.machines" :key="index" :span="12">
  23 + <div class="form-group">
  24 + <div :id="item.id" style="border: 1px solid #dee2e6;">
  25 + <img width="100%" src="/img/init.jpg" height="300px" style="border: 1;" />
  26 + </div>
  27 + <div class="flex justify-between margin-top-sm" style="font-size: 14px;">
  28 + <div class="margin-left-sm">
  29 + <i class="el-icon-bell" style="color: #007bff;"></i>{{ item.machineName }}
  30 + </div>
  31 + <el-button size="small" type="primary" @click="_openSelectVideo(item)">
  32 + {{ $t('common.select') }}
  33 + </el-button>
  34 + </div>
  35 + </div>
  36 + </el-col>
  37 + </template>
  38 + <template v-else-if="cameraControlVideoInfo.cameraCount === 6">
  39 + <el-col v-for="(item, index) in cameraControlVideoInfo.machines" :key="index" :span="8">
  40 + <div class="form-group">
  41 + <div :id="item.id" style="border: 1px solid #dee2e6;">
  42 + <img width="100%" src="/img/init.jpg" height="300px" style="border: 1;" />
  43 + </div>
  44 + <div class="flex justify-between margin-top-sm" style="font-size: 14px;">
  45 + <div class="margin-left-sm">
  46 + <i class="el-icon-bell" style="color: #007bff;"></i>{{ item.machineName }}
  47 + </div>
  48 + <el-button size="small" type="primary" @click="_openSelectVideo(item)">
  49 + {{ $t('common.select') }}
  50 + </el-button>
  51 + </div>
  52 + </div>
  53 + </el-col>
  54 + </template>
  55 + </el-row>
  56 + <select-video-machine ref="selectVideoMachine" />
  57 + </div>
  58 +</template>
  59 +
  60 +<script>
  61 +import SelectVideoMachine from './selectVideoMachine.vue'
  62 +
  63 +export default {
  64 + name: 'CameraControlVideo',
  65 + components: {
  66 + SelectVideoMachine
  67 + },
  68 + data() {
  69 + return {
  70 + cameraControlVideoInfo: {
  71 + machines: [],
  72 + cameraCount: 4
  73 + }
  74 + }
  75 + },
  76 + created() {
  77 + const cameraCount = this.$route.query.cameraCount
  78 + if (cameraCount) {
  79 + this.cameraControlVideoInfo.cameraCount = parseInt(cameraCount)
  80 + }
  81 + this._initCamera()
  82 + },
  83 + methods: {
  84 + _initCamera() {
  85 + const machines = []
  86 + const cameraCount = this.cameraControlVideoInfo.cameraCount
  87 + for (let i = 0; i < cameraCount; i++) {
  88 + machines.push({
  89 + id: 'cameraVideo' + (i + 1) + 'Div',
  90 + machineName: '',
  91 + url: ''
  92 + })
  93 + }
  94 + this.cameraControlVideoInfo.machines = machines
  95 + },
  96 + _openSelectVideo(item) {
  97 + item.callback = (machine) => {
  98 + item.machineName = machine.machineName
  99 + if (item.jessibuca) {
  100 + try {
  101 + item.jessibuca.destroy()
  102 + } catch (err) {
  103 + console.error(err)
  104 + }
  105 + }
  106 + this.$refs.selectVideoMachine.getPlayVideoUrl(machine.machineId)
  107 + .then(url => {
  108 + const image = document.getElementById(item.id)
  109 + const jessibuca = new window.Jessibuca({
  110 + container: image,
  111 + videoBuffer: 0.2,
  112 + isResize: false,
  113 + text: "",
  114 + loadingText: "",
  115 + useMSE: false,
  116 + debug: false,
  117 + isNotMute: false,
  118 + supportDblclickFullscreen: true,
  119 + operateBtns: {
  120 + fullscreen: true,
  121 + screenshot: true,
  122 + play: true,
  123 + audio: false,
  124 + recorder: false
  125 + },
  126 + })
  127 + item.jessibuca = jessibuca
  128 + jessibuca.play(url)
  129 + })
  130 + .catch(error => {
  131 + console.error(error)
  132 + this.$message.error(this.$t('cameraControlVideo.getVideoUrlError'))
  133 + })
  134 + }
  135 + this.$refs.selectVideoMachine.open(item)
  136 + },
  137 + _changeCount(count) {
  138 + this.cameraControlVideoInfo.cameraCount = count
  139 + this.$router.push({
  140 + path: this.$route.path,
  141 + query: { cameraCount: count }
  142 + })
  143 + this._initCamera()
  144 + }
  145 + }
  146 +}
  147 +</script>
  148 +
  149 +<style scoped>
  150 +.camera-control-video {
  151 + padding: 20px;
  152 + padding-top: 0;
  153 +}
  154 +
  155 +.text-right {
  156 + text-align: right;
  157 +}
  158 +
  159 +.margin-top {
  160 + margin-top: 20px;
  161 +}
  162 +
  163 +.margin-top-sm {
  164 + margin-top: 10px;
  165 +}
  166 +
  167 +.margin-left-sm {
  168 + margin-left: 10px;
  169 +}
  170 +
  171 +.flex {
  172 + display: flex;
  173 +}
  174 +
  175 +.justify-between {
  176 + justify-content: space-between;
  177 +}
  178 +
  179 +.form-group {
  180 + margin-bottom: 15px;
  181 +}
  182 +</style>
0 183 \ No newline at end of file
... ...
src/components/machine/selectVideoMachine.vue 0 → 100644
  1 +<template>
  2 + <el-dialog
  3 + :visible.sync="dialogVisible"
  4 + :title="$t('selectVideoMachine.title')"
  5 + width="80%"
  6 + @close="handleClose"
  7 + >
  8 + <el-row>
  9 + <el-col :span="12" class="border-right">
  10 + <div class="text-center">
  11 + {{ $t('selectVideoMachine.monitorArea') }}
  12 + </div>
  13 + <div class="padding padding-top-xs">
  14 + <div
  15 + v-for="(item,index) in selectVideoMachineInfo.areas"
  16 + :key="index"
  17 + class="padding overflow-hidden"
  18 + :class="{'select': selectVideoMachineInfo.maId === item.maId}"
  19 + @click="_changeArea(item)"
  20 + >
  21 + <div>{{item.maName}}</div>
  22 + </div>
  23 + </div>
  24 + </el-col>
  25 + <el-col :span="12">
  26 + <div class="text-center">
  27 + {{ $t('selectVideoMachine.camera') }}
  28 + </div>
  29 + <div class="padding padding-top-xs">
  30 + <div
  31 + v-for="(item,index) in selectVideoMachineInfo.machines"
  32 + :key="index"
  33 + class="padding overflow-hidden"
  34 + :class="{'select': selectVideoMachineInfo.machineId === item.machineId}"
  35 + @click="_changeMachine(item)"
  36 + >
  37 + <div>{{item.machineName}}</div>
  38 + </div>
  39 + </div>
  40 + </el-col>
  41 + </el-row>
  42 + </el-dialog>
  43 +</template>
  44 +
  45 +<script>
  46 +import { getMonitorAreas, getMonitorMachines, getPlayVideoUrl } from '@/api/machine/videoControlApi'
  47 +import { getCommunityId } from '@/api/community/communityApi'
  48 +
  49 +export default {
  50 + name: 'SelectVideoMachine',
  51 + data() {
  52 + return {
  53 + dialogVisible: false,
  54 + selectVideoMachineInfo: {
  55 + areas: [],
  56 + machines: [],
  57 + maId: '',
  58 + machineId: '',
  59 + curMachine: {}
  60 + },
  61 + communityId: ''
  62 + }
  63 + },
  64 + created() {
  65 + this.communityId = getCommunityId()
  66 + },
  67 + methods: {
  68 + open(machine) {
  69 + this._clearSelectVideoMachine()
  70 + this.selectVideoMachineInfo.curMachine = machine
  71 + this._loadSelectAreas()
  72 + this.dialogVisible = true
  73 + },
  74 + async getPlayVideoUrl(machineId) {
  75 + const params = {
  76 + page: 1,
  77 + row: 1,
  78 + communityId: this.communityId,
  79 + machineId: machineId
  80 + }
  81 + const res = await getPlayVideoUrl(params)
  82 + return res.data
  83 + },
  84 + async _loadSelectAreas() {
  85 + try {
  86 + const params = {
  87 + page: 1,
  88 + row: 100,
  89 + communityId: this.communityId
  90 + }
  91 + const res = await getMonitorAreas(params)
  92 + this.selectVideoMachineInfo.areas = res.data
  93 + if (res.data.length > 0) {
  94 + this._changeArea(res.data[0])
  95 + }
  96 + } catch (error) {
  97 + this.$message.error(this.$t('selectVideoMachine.loadAreasError'))
  98 + }
  99 + },
  100 + async _loadSelectVideoMachines() {
  101 + try {
  102 + const params = {
  103 + page: 1,
  104 + row: 100,
  105 + communityId: this.communityId,
  106 + maId: this.selectVideoMachineInfo.maId
  107 + }
  108 + const res = await getMonitorMachines(params)
  109 + this.selectVideoMachineInfo.machines = res.data
  110 + } catch (error) {
  111 + this.$message.error(this.$t('selectVideoMachine.loadMachinesError'))
  112 + }
  113 + },
  114 + _changeArea(area) {
  115 + this.selectVideoMachineInfo.maId = area.maId
  116 + this._loadSelectVideoMachines()
  117 + },
  118 + _changeMachine(machine) {
  119 + this.selectVideoMachineInfo.machineId = machine.machineId
  120 + this.selectVideoMachineInfo.curMachine.callback(machine)
  121 + this.handleClose()
  122 + },
  123 + _clearSelectVideoMachine() {
  124 + this.selectVideoMachineInfo = {
  125 + areas: [],
  126 + machines: [],
  127 + maId: '',
  128 + machineId: '',
  129 + curMachine: {}
  130 + }
  131 + },
  132 + handleClose() {
  133 + this.dialogVisible = false
  134 + this._clearSelectVideoMachine()
  135 + }
  136 + }
  137 +}
  138 +</script>
  139 +
  140 +<style scoped>
  141 +.border-right {
  142 + border-right: 1px solid #dee2e6;
  143 +}
  144 +.text-center {
  145 + text-align: center;
  146 + font-weight: bold;
  147 + margin-bottom: 10px;
  148 +}
  149 +.padding {
  150 + padding: 10px;
  151 +}
  152 +.padding-top-xs {
  153 + padding-top: 5px;
  154 +}
  155 +.overflow-hidden {
  156 + overflow: hidden;
  157 +}
  158 +.select {
  159 + background-color: #f5f7fa;
  160 + color: #409eff;
  161 + cursor: pointer;
  162 +}
  163 +</style>
0 164 \ No newline at end of file
... ...
src/components/room/addPropertyRightRegistration.vue 0 → 100644
  1 +<template>
  2 + <el-dialog :title="$t('propertyRightRegistration.add.title')" :visible.sync="visible" width="70%"
  3 + @close="handleClose">
  4 + <el-form ref="form" :model="formData" label-width="120px">
  5 + <el-row :gutter="20">
  6 + <el-col :span="12">
  7 + <el-form-item :label="$t('propertyRightRegistration.add.floor')" prop="floorId"
  8 + :rules="[{ required: true, message: $t('propertyRightRegistration.add.floorRequired'), trigger: 'change' }]">
  9 + <el-select v-model="formData.floorId" :placeholder="$t('propertyRightRegistration.add.floorPlaceholder')"
  10 + style="width:100%" @change="handleFloorChange">
  11 + <el-option v-for="item in floors" :key="item.floorId"
  12 + :label="`${item.floorNum}${$t('propertyRightRegistration.add.floorUnit')}`" :value="item.floorId" />
  13 + </el-select>
  14 + </el-form-item>
  15 + </el-col>
  16 + <el-col :span="12">
  17 + <el-form-item :label="$t('propertyRightRegistration.add.unit')" prop="unitId"
  18 + :rules="[{ required: true, message: $t('propertyRightRegistration.add.unitRequired'), trigger: 'change' }]">
  19 + <el-select v-model="formData.unitId" :placeholder="$t('propertyRightRegistration.add.unitPlaceholder')"
  20 + style="width:100%" @change="handleUnitChange">
  21 + <el-option v-for="item in units" :key="item.unitId"
  22 + :label="`${item.unitNum}${$t('propertyRightRegistration.add.unitUnit')}`" :value="item.unitId" />
  23 + </el-select>
  24 + </el-form-item>
  25 + </el-col>
  26 + </el-row>
  27 +
  28 + <el-row :gutter="20">
  29 + <el-col :span="12">
  30 + <el-form-item :label="$t('propertyRightRegistration.add.room')" prop="roomId"
  31 + :rules="[{ required: true, message: $t('propertyRightRegistration.add.roomRequired'), trigger: 'change' }]">
  32 + <el-select v-model="formData.roomId" :placeholder="$t('propertyRightRegistration.add.roomPlaceholder')"
  33 + style="width:100%">
  34 + <el-option v-for="item in rooms" :key="item.roomId" :label="item.roomNum" :value="item.roomId" />
  35 + </el-select>
  36 + </el-form-item>
  37 + </el-col>
  38 + <el-col :span="12">
  39 + <el-form-item :label="$t('propertyRightRegistration.add.name')" prop="name" :rules="[
  40 + { required: true, message: $t('propertyRightRegistration.add.nameRequired'), trigger: 'blur' },
  41 + { min: 2, max: 64, message: $t('propertyRightRegistration.add.nameLength'), trigger: 'blur' }
  42 + ]">
  43 + <el-input v-model.trim="formData.name" :placeholder="$t('propertyRightRegistration.add.namePlaceholder')" />
  44 + </el-form-item>
  45 + </el-col>
  46 + </el-row>
  47 +
  48 + <el-row :gutter="20">
  49 + <el-col :span="12">
  50 + <el-form-item :label="$t('propertyRightRegistration.add.link')" prop="link"
  51 + :rules="[{ required: true, message: $t('propertyRightRegistration.add.linkRequired'), trigger: 'blur' }]">
  52 + <el-input v-model.trim="formData.link" :placeholder="$t('propertyRightRegistration.add.linkPlaceholder')" />
  53 + </el-form-item>
  54 + </el-col>
  55 + <el-col :span="12">
  56 + <el-form-item :label="$t('propertyRightRegistration.add.idCard')" prop="idCard" :rules="[
  57 + { required: true, message: $t('propertyRightRegistration.add.idCardRequired'), trigger: 'blur' },
  58 + ]">
  59 + <el-input v-model.trim="formData.idCard"
  60 + :placeholder="$t('propertyRightRegistration.add.idCardPlaceholder')" />
  61 + </el-form-item>
  62 + </el-col>
  63 + </el-row>
  64 +
  65 + <el-form-item :label="$t('propertyRightRegistration.add.address')" prop="address" :rules="[
  66 + { required: true, message: $t('propertyRightRegistration.add.addressRequired'), trigger: 'blur' },
  67 + { max: 255, message: $t('propertyRightRegistration.add.addressMax'), trigger: 'blur' }
  68 + ]">
  69 + <el-input v-model.trim="formData.address"
  70 + :placeholder="$t('propertyRightRegistration.add.addressPlaceholder')" />
  71 + </el-form-item>
  72 +
  73 + <el-form-item :label="$t('propertyRightRegistration.add.idCardPhotos')" prop="idCardPhotos"
  74 + :rules="[{ required: true, message: $t('propertyRightRegistration.add.idCardPhotosRequired'), trigger: 'change' }]">
  75 + <upload-image-url ref="idCardUpload" :image-count="2" @notifyUploadCoverImage="handleIdCardChange" />
  76 + <p class="tip">{{ $t('propertyRightRegistration.add.idCardPhotosTip') }}</p>
  77 + </el-form-item>
  78 +
  79 + <el-form-item :label="$t('propertyRightRegistration.add.housePurchasePhotos')" prop="housePurchasePhotos"
  80 + :rules="[{ required: true, message: $t('propertyRightRegistration.add.housePurchasePhotosRequired'), trigger: 'change' }]">
  81 + <upload-image-url ref="housePurchaseUpload" :image-count="10" @notifyUploadCoverImage="handleHousePurchaseChange" />
  82 + <p class="tip">{{ $t('propertyRightRegistration.add.housePurchasePhotosTip') }}</p>
  83 + </el-form-item>
  84 +
  85 + <el-row :gutter="20">
  86 + <el-col :span="12">
  87 + <el-form-item :label="$t('propertyRightRegistration.add.isTrue')" prop="isTrue"
  88 + :rules="[{ required: true, message: $t('propertyRightRegistration.add.isTrueRequired'), trigger: 'change' }]">
  89 + <el-select v-model="formData.isTrue" :placeholder="$t('propertyRightRegistration.add.isTruePlaceholder')"
  90 + style="width:100%" @change="handleIsTrueChange">
  91 + <el-option :label="$t('common.yes')" value="true" />
  92 + <el-option :label="$t('common.no')" value="false" />
  93 + </el-select>
  94 + </el-form-item>
  95 + </el-col>
  96 + <el-col v-if="formData.isTrue === 'true'" :span="12">
  97 + <el-form-item :label="$t('propertyRightRegistration.add.repairPhotos')" prop="repairPhotos"
  98 + :rules="[{ required: true, message: $t('propertyRightRegistration.add.repairPhotosRequired'), trigger: 'change' }]">
  99 + <upload-image-url ref="repairUpload" :image-count="3" @notifyUploadCoverImage="handleRepairChange" />
  100 + </el-form-item>
  101 + </el-col>
  102 + </el-row>
  103 +
  104 + <el-row :gutter="20">
  105 + <el-col :span="12">
  106 + <el-form-item :label="$t('propertyRightRegistration.add.flag')" prop="flag"
  107 + :rules="[{ required: true, message: $t('propertyRightRegistration.add.flagRequired'), trigger: 'change' }]">
  108 + <el-select v-model="formData.flag" :placeholder="$t('propertyRightRegistration.add.flagPlaceholder')"
  109 + style="width:100%" @change="handleFlagChange">
  110 + <el-option :label="$t('common.yes')" value="0" />
  111 + <el-option :label="$t('common.no')" value="1" />
  112 + </el-select>
  113 + </el-form-item>
  114 + </el-col>
  115 + <el-col v-if="formData.flag === '0'" :span="12">
  116 + <el-form-item :label="$t('propertyRightRegistration.add.deedTaxPhotos')" prop="deedTaxPhotos"
  117 + :rules="[{ required: true, message: $t('propertyRightRegistration.add.deedTaxPhotosRequired'), trigger: 'change' }]">
  118 + <upload-image-url ref="deedTaxUpload" :image-count="3" @notifyUploadCoverImage="handleDeedTaxChange" />
  119 + </el-form-item>
  120 + </el-col>
  121 + </el-row>
  122 + </el-form>
  123 +
  124 + <div slot="footer" class="dialog-footer">
  125 + <el-button @click="visible = false">{{ $t('common.cancel') }}</el-button>
  126 + <el-button type="primary" @click="handleSubmit">{{ $t('common.confirm') }}</el-button>
  127 + </div>
  128 + </el-dialog>
  129 +</template>
  130 +
  131 +<script>
  132 +import { getCommunityId } from '@/api/community/communityApi'
  133 +import { getFloors, getUnits, queryRooms } from '@/api/room/roomApi'
  134 +import { savePropertyRightRegistration } from '@/api/room/propertyRightRegistrationManageApi'
  135 +import UploadImageUrl from '@/components/upload/UploadImageUrl'
  136 +
  137 +export default {
  138 + name: 'AddPropertyRightRegistration',
  139 + components: {
  140 + UploadImageUrl
  141 + },
  142 + data() {
  143 + return {
  144 + visible: false,
  145 + formData: {
  146 + prrId: '',
  147 + roomId: '',
  148 + floorId: '',
  149 + unitId: '',
  150 + name: '',
  151 + link: '',
  152 + idCard: '',
  153 + address: '',
  154 + isTrue: '',
  155 + flag: '',
  156 + state: '0',
  157 + idCardPhotos: [],
  158 + housePurchasePhotos: [],
  159 + repairPhotos: [],
  160 + deedTaxPhotos: [],
  161 + communityId: getCommunityId()
  162 + },
  163 + floors: [],
  164 + units: [],
  165 + rooms: []
  166 + }
  167 + },
  168 + methods: {
  169 + open() {
  170 + this.visible = true
  171 + this.getFloors()
  172 + this.$nextTick(() => {
  173 + this.$refs.form && this.$refs.form.resetFields()
  174 + this.$refs.idCardUpload && this.$refs.idCardUpload.clearImages()
  175 + this.$refs.housePurchaseUpload && this.$refs.housePurchaseUpload.clearImages()
  176 + this.$refs.repairUpload && this.$refs.repairUpload.clearImages()
  177 + this.$refs.deedTaxUpload && this.$refs.deedTaxUpload.clearImages()
  178 + })
  179 + },
  180 + handleClose() {
  181 + this.$refs.form.resetFields()
  182 + this.formData = {
  183 + prrId: '',
  184 + roomId: '',
  185 + floorId: '',
  186 + unitId: '',
  187 + name: '',
  188 + link: '',
  189 + idCard: '',
  190 + address: '',
  191 + isTrue: '',
  192 + flag: '',
  193 + state: '0',
  194 + idCardPhotos: [],
  195 + housePurchasePhotos: [],
  196 + repairPhotos: [],
  197 + deedTaxPhotos: [],
  198 + communityId: getCommunityId()
  199 + }
  200 + },
  201 + async getFloors() {
  202 + try {
  203 + const params = {
  204 + communityId: this.formData.communityId,
  205 + page: 1,
  206 + row: 50
  207 + }
  208 + const data = await getFloors(params)
  209 + this.floors = data.apiFloorDataVoList || []
  210 + } catch (error) {
  211 + console.error('获取楼栋数据失败:', error)
  212 + }
  213 + },
  214 + async handleFloorChange(floorId) {
  215 + try {
  216 + const params = {
  217 + floorId,
  218 + communityId: this.formData.communityId,
  219 + page: 1,
  220 + row: 50
  221 + }
  222 + const data = await getUnits(params)
  223 + this.units = data || []
  224 + this.formData.unitId = ''
  225 + this.rooms = []
  226 + this.formData.roomId = ''
  227 + } catch (error) {
  228 + console.error('获取单元数据失败:', error)
  229 + }
  230 + },
  231 + async handleUnitChange(unitId) {
  232 + try {
  233 + const params = {
  234 + unitId,
  235 + communityId: this.formData.communityId,
  236 + page: 1,
  237 + row: 50
  238 + }
  239 + const data = await queryRooms(params)
  240 + this.rooms = data.rooms || []
  241 + this.formData.roomId = ''
  242 + } catch (error) {
  243 + console.error('获取房间数据失败:', error)
  244 + }
  245 + },
  246 + handleIsTrueChange(val) {
  247 + if (val !== 'true') {
  248 + this.formData.repairPhotos = []
  249 + this.$refs.repairUpload && this.$refs.repairUpload.clearImages()
  250 + }
  251 + },
  252 + handleFlagChange(val) {
  253 + if (val !== '0') {
  254 + this.formData.deedTaxPhotos = []
  255 + this.$refs.deedTaxUpload && this.$refs.deedTaxUpload.clearImages()
  256 + }
  257 + },
  258 + handleIdCardChange(photos) {
  259 + this.formData.idCardPhotos = photos
  260 + },
  261 + handleHousePurchaseChange(photos) {
  262 + this.formData.housePurchasePhotos = photos
  263 + },
  264 + handleRepairChange(photos) {
  265 + this.formData.repairPhotos = photos
  266 + },
  267 + handleDeedTaxChange(photos) {
  268 + this.formData.deedTaxPhotos = photos
  269 + },
  270 +
  271 + async handleSubmit() {
  272 + try {
  273 + await this.$refs.form.validate()
  274 + this.loading = true
  275 +
  276 + const params = {
  277 + ...this.formData,
  278 + idCardPhotos: this.formData.idCardPhotos,
  279 + housePurchasePhotos: this.formData.housePurchasePhotos,
  280 + repairPhotos: this.formData.repairPhotos,
  281 + deedTaxPhotos: this.formData.deedTaxPhotos
  282 + }
  283 +
  284 + await savePropertyRightRegistration(params)
  285 + this.$message.success(this.$t('propertyRightRegistration.add.success'))
  286 + this.visible = false
  287 + this.$emit('success')
  288 + } catch (error) {
  289 + console.error('保存失败:', error)
  290 + if (error !== 'validate') {
  291 + this.$message.error(this.$t('propertyRightRegistration.add.error'))
  292 + }
  293 + } finally {
  294 + this.loading = false
  295 + }
  296 + }
  297 + }
  298 +}
  299 +</script>
  300 +
  301 +<style scoped>
  302 +.tip {
  303 + color: #999;
  304 + font-size: 12px;
  305 + margin-top: 5px;
  306 +}
  307 +</style>
0 308 \ No newline at end of file
... ...
src/components/room/deletePropertyRightRegistration.vue 0 → 100644
  1 +<template>
  2 + <el-dialog
  3 + :title="$t('propertyRightRegistration.delete.title')"
  4 + :visible.sync="visible"
  5 + width="30%"
  6 + @close="visible = false"
  7 + >
  8 + <div class="delete-content">
  9 + <p>{{ $t('propertyRightRegistration.delete.confirm') }}</p>
  10 + <p class="delete-tip">{{ $t('propertyRightRegistration.delete.tip') }}</p>
  11 + </div>
  12 +
  13 + <div slot="footer" class="dialog-footer">
  14 + <el-button @click="visible = false">{{ $t('common.cancel') }}</el-button>
  15 + <el-button type="danger" @click="handleConfirm">{{ $t('common.confirm') }}</el-button>
  16 + </div>
  17 + </el-dialog>
  18 +</template>
  19 +
  20 +<script>
  21 +import { getCommunityId } from '@/api/community/communityApi'
  22 +import { deletePropertyRightRegistration } from '@/api/room/propertyRightRegistrationManageApi'
  23 +
  24 +export default {
  25 + name: 'DeletePropertyRightRegistration',
  26 + data() {
  27 + return {
  28 + visible: false,
  29 + deleteData: {
  30 + prrId: '',
  31 + communityId: getCommunityId()
  32 + }
  33 + }
  34 + },
  35 + methods: {
  36 + open(data) {
  37 + this.visible = true
  38 + this.deleteData = {
  39 + prrId: data.prrId,
  40 + communityId: getCommunityId()
  41 + }
  42 + },
  43 + async handleConfirm() {
  44 + try {
  45 + const res = await deletePropertyRightRegistration(this.deleteData)
  46 + if (res.code === 0) {
  47 + this.$message.success(this.$t('propertyRightRegistration.delete.success'))
  48 + this.visible = false
  49 + this.$emit('success')
  50 + } else {
  51 + this.$message.error(res.msg || this.$t('propertyRightRegistration.delete.error'))
  52 + }
  53 + } catch (error) {
  54 + console.error('删除失败:', error)
  55 + this.$message.error(this.$t('propertyRightRegistration.delete.error'))
  56 + }
  57 + }
  58 + }
  59 +}
  60 +</script>
  61 +
  62 +<style scoped>
  63 +.delete-content {
  64 + text-align: center;
  65 + font-size: 16px;
  66 + padding: 20px 0;
  67 +}
  68 +
  69 +.delete-tip {
  70 + color: #f56c6c;
  71 + margin-top: 10px;
  72 + font-size: 14px;
  73 +}
  74 +
  75 +.dialog-footer {
  76 + text-align: right;
  77 +}
  78 +</style>
0 79 \ No newline at end of file
... ...
src/components/room/editPropertyRightRegistration.vue 0 → 100644
  1 +<template>
  2 + <el-dialog :title="$t('propertyRightRegistration.edit.title')" :visible.sync="visible" width="70%"
  3 + @close="handleClose">
  4 + <el-form ref="form" :model="formData" label-width="120px">
  5 + <el-row :gutter="20">
  6 + <el-col :span="12">
  7 + <el-form-item :label="$t('propertyRightRegistration.edit.floor')" prop="floorId"
  8 + :rules="[{ required: true, message: $t('propertyRightRegistration.edit.floorRequired'), trigger: 'change' }]">
  9 + <el-select v-model="formData.floorId" :placeholder="$t('propertyRightRegistration.edit.floorPlaceholder')"
  10 + style="width:100%" @change="handleFloorChange">
  11 + <el-option v-for="item in floors" :key="item.floorId"
  12 + :label="`${item.floorNum}${$t('propertyRightRegistration.edit.floorUnit')}`" :value="item.floorId" />
  13 + </el-select>
  14 + </el-form-item>
  15 + </el-col>
  16 + <el-col :span="12">
  17 + <el-form-item :label="$t('propertyRightRegistration.edit.unit')" prop="unitId"
  18 + :rules="[{ required: true, message: $t('propertyRightRegistration.edit.unitRequired'), trigger: 'change' }]">
  19 + <el-select v-model="formData.unitId" :placeholder="$t('propertyRightRegistration.edit.unitPlaceholder')"
  20 + style="width:100%" @change="handleUnitChange">
  21 + <el-option v-for="item in units" :key="item.unitId"
  22 + :label="`${item.unitNum}${$t('propertyRightRegistration.edit.unitUnit')}`" :value="item.unitId" />
  23 + </el-select>
  24 + </el-form-item>
  25 + </el-col>
  26 + </el-row>
  27 +
  28 + <el-row :gutter="20">
  29 + <el-col :span="12">
  30 + <el-form-item :label="$t('propertyRightRegistration.edit.room')" prop="roomId"
  31 + :rules="[{ required: true, message: $t('propertyRightRegistration.edit.roomRequired'), trigger: 'change' }]">
  32 + <el-select v-model="formData.roomId" :placeholder="$t('propertyRightRegistration.edit.roomPlaceholder')"
  33 + style="width:100%">
  34 + <el-option v-for="item in rooms" :key="item.roomId" :label="item.roomNum" :value="item.roomId" />
  35 + </el-select>
  36 + </el-form-item>
  37 + </el-col>
  38 + <el-col :span="12">
  39 + <el-form-item :label="$t('propertyRightRegistration.edit.name')" prop="name" :rules="[
  40 + { required: true, message: $t('propertyRightRegistration.edit.nameRequired'), trigger: 'blur' },
  41 + { min: 2, max: 64, message: $t('propertyRightRegistration.edit.nameLength'), trigger: 'blur' }
  42 + ]">
  43 + <el-input v-model.trim="formData.name"
  44 + :placeholder="$t('propertyRightRegistration.edit.namePlaceholder')" />
  45 + </el-form-item>
  46 + </el-col>
  47 + </el-row>
  48 +
  49 + <el-row :gutter="20">
  50 + <el-col :span="12">
  51 + <el-form-item :label="$t('propertyRightRegistration.edit.link')" prop="link"
  52 + :rules="[{ required: true, message: $t('propertyRightRegistration.edit.linkRequired'), trigger: 'blur' }]">
  53 + <el-input v-model.trim="formData.link"
  54 + :placeholder="$t('propertyRightRegistration.edit.linkPlaceholder')" />
  55 + </el-form-item>
  56 + </el-col>
  57 + <el-col :span="12">
  58 + <el-form-item :label="$t('propertyRightRegistration.edit.idCard')" prop="idCard" :rules="[
  59 + { required: true, message: $t('propertyRightRegistration.edit.idCardRequired'), trigger: 'blur' },
  60 + ]">
  61 + <el-input v-model.trim="formData.idCard"
  62 + :placeholder="$t('propertyRightRegistration.edit.idCardPlaceholder')" />
  63 + </el-form-item>
  64 + </el-col>
  65 + </el-row>
  66 +
  67 + <el-form-item :label="$t('propertyRightRegistration.edit.address')" prop="address" :rules="[
  68 + { required: true, message: $t('propertyRightRegistration.edit.addressRequired'), trigger: 'blur' },
  69 + { max: 255, message: $t('propertyRightRegistration.edit.addressMax'), trigger: 'blur' }
  70 + ]">
  71 + <el-input v-model.trim="formData.address"
  72 + :placeholder="$t('propertyRightRegistration.edit.addressPlaceholder')" />
  73 + </el-form-item>
  74 +
  75 + <div class="dialog-footer">
  76 + <el-button @click="visible = false">{{ $t('common.cancel') }}</el-button>
  77 + <el-button type="primary" @click="handleSubmit">{{ $t('common.confirm') }}</el-button>
  78 + </div>
  79 + </el-form>
  80 + </el-dialog>
  81 +</template>
  82 +
  83 +<script>
  84 +import { getCommunityId } from '@/api/community/communityApi'
  85 +import { getFloors, getUnits, queryRooms } from '@/api/room/roomApi'
  86 +import { updatePropertyRightRegistration } from '@/api/room/propertyRightRegistrationManageApi'
  87 +
  88 +export default {
  89 + name: 'EditPropertyRightRegistration',
  90 + data() {
  91 + return {
  92 + visible: false,
  93 + formData: {
  94 + prrId: '',
  95 + roomId: '',
  96 + floorId: '',
  97 + unitId: '',
  98 + name: '',
  99 + link: '',
  100 + idCard: '',
  101 + address: '',
  102 + communityId: getCommunityId()
  103 + },
  104 + floors: [],
  105 + units: [],
  106 + rooms: []
  107 + }
  108 + },
  109 + methods: {
  110 + open(data) {
  111 + this.visible = true
  112 + this.formData = {
  113 + prrId: data.prrId,
  114 + roomId: data.roomId,
  115 + floorId: data.floorId,
  116 + unitId: data.unitId,
  117 + name: data.name,
  118 + link: data.link,
  119 + idCard: data.idCard,
  120 + address: data.address,
  121 + communityId: getCommunityId()
  122 + }
  123 + this.getFloors()
  124 + },
  125 + handleClose() {
  126 + this.$refs.form.resetFields()
  127 + },
  128 + async getFloors() {
  129 + try {
  130 + const params = {
  131 + communityId: this.formData.communityId,
  132 + page: 1,
  133 + row: 50
  134 + }
  135 + const data = await getFloors(params)
  136 + this.floors = data.apiFloorDataVoList || []
  137 + this.getUnits()
  138 + } catch (error) {
  139 + console.error('获取楼栋数据失败:', error)
  140 + }
  141 + },
  142 + async getUnits() {
  143 + try {
  144 + const params = {
  145 + floorId: this.formData.floorId,
  146 + communityId: this.formData.communityId,
  147 + page: 1,
  148 + row: 50
  149 + }
  150 + const data = await getUnits(params)
  151 + this.units = data || []
  152 + this.getRooms()
  153 + } catch (error) {
  154 + console.error('获取单元数据失败:', error)
  155 + }
  156 + },
  157 + async getRooms() {
  158 + try {
  159 + const params = {
  160 + unitId: this.formData.unitId,
  161 + communityId: this.formData.communityId,
  162 + page: 1,
  163 + row: 50
  164 + }
  165 + const data = await queryRooms(params)
  166 + this.rooms = data.rooms || []
  167 + } catch (error) {
  168 + console.error('获取房间数据失败:', error)
  169 + }
  170 + },
  171 + handleFloorChange(floorId) {
  172 + this.formData.unitId = ''
  173 + this.formData.roomId = ''
  174 + this.units = []
  175 + this.rooms = []
  176 + if (floorId) {
  177 + this.getUnits()
  178 + }
  179 + },
  180 + handleUnitChange(unitId) {
  181 + this.formData.roomId = ''
  182 + this.rooms = []
  183 + if (unitId) {
  184 + this.getRooms()
  185 + }
  186 + },
  187 +
  188 + async handleSubmit() {
  189 + try {
  190 + await this.$refs.form.validate()
  191 +
  192 + const params = {
  193 + ...this.formData,
  194 + flag: '0' // 0表示修改操作
  195 + }
  196 +
  197 + const res = await updatePropertyRightRegistration(params)
  198 + if (res.code === 0) {
  199 + this.$message.success(this.$t('propertyRightRegistration.edit.success'))
  200 + this.visible = false
  201 + this.$emit('success')
  202 + } else {
  203 + this.$message.error(res.msg || this.$t('propertyRightRegistration.edit.error'))
  204 + }
  205 + } catch (error) {
  206 + if (error !== 'validate') {
  207 + console.error('修改失败:', error)
  208 + this.$message.error(this.$t('propertyRightRegistration.edit.error'))
  209 + }
  210 + }
  211 + }
  212 + }
  213 +}
  214 +</script>
  215 +
  216 +<style scoped>
  217 +.dialog-footer {
  218 + text-align: right;
  219 + margin-top: 20px;
  220 +}
  221 +</style>
0 222 \ No newline at end of file
... ...
src/components/room/editPropertyRightRegistrationDetail.vue 0 → 100644
  1 +<template>
  2 + <el-dialog :title="$t('propertyRightDetail.edit.title')" :visible.sync="visible" width="70%"
  3 + :before-close="handleClose">
  4 + <el-form ref="form" :model="editPropertyRightRegistrationDetailInfo" label-width="120px">
  5 + <el-form-item :label="$t('propertyRightDetail.edit.materialType')">
  6 + <el-input v-model="editPropertyRightRegistrationDetailInfo.securitiesName"
  7 + :placeholder="$t('propertyRightDetail.edit.materialTypePlaceholder')" disabled />
  8 + </el-form-item>
  9 +
  10 + <template v-if="editPropertyRightRegistrationDetailInfo.securities === '001'">
  11 + <el-form-item :label="$t('propertyRightDetail.edit.idCardPhoto')">
  12 + <upload-image-url ref="idCardUpload" :image-count="2" @notifyUploadCoverImage="handleIdCardImageChange" />
  13 + <p class="help-block">*{{ $t('propertyRightDetail.edit.idCardPhotoTip') }}*</p>
  14 + </el-form-item>
  15 + </template>
  16 +
  17 + <template v-if="editPropertyRightRegistrationDetailInfo.securities === '002'">
  18 + <el-form-item :label="$t('propertyRightDetail.edit.houseContract')">
  19 + <upload-image-url ref="housePurchaseUpload" :image-count="10" @notifyUploadCoverImage="handleHousePurchaseImageChange" />
  20 + <p class="help-block">*{{ $t('propertyRightDetail.edit.houseContractTip') }}*</p>
  21 + </el-form-item>
  22 + </template>
  23 +
  24 + <template v-if="editPropertyRightRegistrationDetailInfo.securities === '003'">
  25 + <el-form-item :label="$t('propertyRightDetail.edit.repairFund')">
  26 + <el-select v-model="editPropertyRightRegistrationDetailInfo.isTrue" style="width: 100%"
  27 + :placeholder="$t('propertyRightDetail.edit.repairFundPlaceholder')">
  28 + <el-option value="" disabled :label="$t('propertyRightDetail.edit.repairFundPlaceholder')" />
  29 + <el-option :label="$t('common.yes')" value="true" />
  30 + <el-option :label="$t('common.no')" value="false" />
  31 + </el-select>
  32 + </el-form-item>
  33 +
  34 + <template v-if="editPropertyRightRegistrationDetailInfo.isTrue === 'true'">
  35 + <el-form-item :label="$t('propertyRightDetail.edit.repairFundPhoto')">
  36 + <upload-image-url ref="repairUpload" :image-count="3" @notifyUploadCoverImage="handleRepairImageChange" />
  37 + </el-form-item>
  38 + </template>
  39 + </template>
  40 +
  41 + <template v-if="editPropertyRightRegistrationDetailInfo.securities === '004'">
  42 + <el-form-item :label="$t('propertyRightDetail.edit.deedTax')">
  43 + <el-select v-model="editPropertyRightRegistrationDetailInfo.isTrue" style="width: 100%"
  44 + :placeholder="$t('propertyRightDetail.edit.deedTaxPlaceholder')">
  45 + <el-option value="" disabled :label="$t('propertyRightDetail.edit.deedTaxPlaceholder')" />
  46 + <el-option :label="$t('common.yes')" value="true" />
  47 + <el-option :label="$t('common.no')" value="false" />
  48 + </el-select>
  49 + </el-form-item>
  50 +
  51 + <template v-if="editPropertyRightRegistrationDetailInfo.isTrue === 'true'">
  52 + <el-form-item :label="$t('propertyRightDetail.edit.deedTaxPhoto')">
  53 + <upload-image-url ref="deedTaxUpload" :image-count="3" @notifyUploadCoverImage="handleDeedTaxImageChange" />
  54 + </el-form-item>
  55 + </template>
  56 + </template>
  57 +
  58 + <el-form-item class="dialog-footer">
  59 + <el-button type="primary" @click="editPropertyRightRegistrationDetail">
  60 + <i class="el-icon-check"></i>
  61 + {{ $t('common.save') }}
  62 + </el-button>
  63 + <el-button @click="handleClose">
  64 + <i class="el-icon-close"></i>
  65 + {{ $t('common.cancel') }}
  66 + </el-button>
  67 + </el-form-item>
  68 + </el-form>
  69 + </el-dialog>
  70 +</template>
  71 +
  72 +<script>
  73 +import { updatePropertyRightRegistrationDetail } from '@/api/room/listPropertyRightRegistrationDetailApi'
  74 +import UploadImageUrl from '@/components/upload/UploadImageUrl'
  75 +import { getCommunityId } from '@/api/community/communityApi'
  76 +
  77 +export default {
  78 + name: 'EditPropertyRightRegistrationDetail',
  79 + components: {
  80 + UploadImageUrl
  81 + },
  82 + props: {
  83 + visible: {
  84 + type: Boolean,
  85 + default: false
  86 + }
  87 + },
  88 + data() {
  89 + return {
  90 + editPropertyRightRegistrationDetailInfo: {
  91 + prrdId: '',
  92 + prrId: '',
  93 + securities: '',
  94 + securitiesName: '',
  95 + idCardUrl: '',
  96 + housePurchaseUrl: '',
  97 + repairUrl: '',
  98 + deedTaxUrl: '',
  99 + isTrue: '',
  100 + idCardPhotos: [],
  101 + housePurchasePhotos: [],
  102 + repairPhotos: [],
  103 + deedTaxPhotos: [],
  104 + communityId: ''
  105 + }
  106 + }
  107 + },
  108 + methods: {
  109 + open(row) {
  110 + this.resetForm()
  111 + this.editPropertyRightRegistrationDetailInfo = { ...row }
  112 + this.editPropertyRightRegistrationDetailInfo.communityId = getCommunityId()
  113 + this.loadPhotos()
  114 + this.visible = true
  115 + },
  116 + loadPhotos() {
  117 + if (this.editPropertyRightRegistrationDetailInfo.securities === '001' && this.editPropertyRightRegistrationDetailInfo.idCardUrl) {
  118 + const urls = this.editPropertyRightRegistrationDetailInfo.idCardUrl.trim()
  119 + if (urls) {
  120 + this.$refs.idCardUpload.setImages(urls.split(','))
  121 + }
  122 + }
  123 + if (this.editPropertyRightRegistrationDetailInfo.securities === '002' && this.editPropertyRightRegistrationDetailInfo.housePurchaseUrl) {
  124 + const urls = this.editPropertyRightRegistrationDetailInfo.housePurchaseUrl.trim()
  125 + if (urls) {
  126 + this.$refs.housePurchaseUpload.setImages(urls.split(','))
  127 + }
  128 + }
  129 + if (this.editPropertyRightRegistrationDetailInfo.securities === '003' && this.editPropertyRightRegistrationDetailInfo.repairUrl) {
  130 + const urls = this.editPropertyRightRegistrationDetailInfo.repairUrl.trim()
  131 + if (urls) {
  132 + this.$refs.repairUpload.setImages(urls.split(','))
  133 + }
  134 + }
  135 + if (this.editPropertyRightRegistrationDetailInfo.securities === '004' && this.editPropertyRightRegistrationDetailInfo.deedTaxUrl) {
  136 + const urls = this.editPropertyRightRegistrationDetailInfo.deedTaxUrl.trim()
  137 + if (urls) {
  138 + this.$refs.deedTaxUpload.setImages(urls.split(','))
  139 + }
  140 + }
  141 + },
  142 + handleIdCardImageChange(photos) {
  143 + this.editPropertyRightRegistrationDetailInfo.idCardPhotos = photos
  144 + },
  145 + handleHousePurchaseImageChange(photos) {
  146 + this.editPropertyRightRegistrationDetailInfo.housePurchasePhotos = photos
  147 + },
  148 + handleRepairImageChange(photos) {
  149 + this.editPropertyRightRegistrationDetailInfo.repairPhotos = photos
  150 + },
  151 + handleDeedTaxImageChange(photos) {
  152 + this.editPropertyRightRegistrationDetailInfo.deedTaxPhotos = photos
  153 + },
  154 + editPropertyRightRegistrationDetail() {
  155 + if (!this.validateForm()) {
  156 + return
  157 + }
  158 +
  159 + const params = { ...this.editPropertyRightRegistrationDetailInfo }
  160 + if (params.securities === '001') {
  161 + params.idCardUrl = params.idCardPhotos && params.idCardPhotos.length > 0 ? params.idCardPhotos.join(',') : ''
  162 + } else if (params.securities === '002') {
  163 + params.housePurchaseUrl = params.housePurchasePhotos && params.housePurchasePhotos.length > 0 ? params.housePurchasePhotos.join(',') : ''
  164 + } else if (params.securities === '003') {
  165 + params.repairUrl = params.repairPhotos && params.repairPhotos.length > 0 ? params.repairPhotos.join(',') : ''
  166 + } else if (params.securities === '004') {
  167 + params.deedTaxUrl = params.deedTaxPhotos && params.deedTaxPhotos.length > 0 ? params.deedTaxPhotos.join(',') : ''
  168 + }
  169 +
  170 + updatePropertyRightRegistrationDetail(params)
  171 + .then(() => {
  172 + this.$message.success(this.$t('common.updateSuccess'))
  173 + this.$emit('success')
  174 + this.handleClose()
  175 + })
  176 + .catch(error => {
  177 + this.$message.error(error.message || this.$t('common.updateFailed'))
  178 + })
  179 + },
  180 + validateForm() {
  181 + if (!this.editPropertyRightRegistrationDetailInfo.securities) {
  182 + this.$message.error(this.$t('propertyRightDetail.edit.materialTypeRequired'))
  183 + return false
  184 + }
  185 + return true
  186 + },
  187 + handleClose() {
  188 + this.visible = false
  189 + },
  190 + resetForm() {
  191 + this.editPropertyRightRegistrationDetailInfo = {
  192 + prrdId: '',
  193 + prrId: '',
  194 + securities: '',
  195 + securitiesName: '',
  196 + idCardUrl: '',
  197 + housePurchaseUrl: '',
  198 + repairUrl: '',
  199 + deedTaxUrl: '',
  200 + isTrue: '',
  201 + idCardPhotos: [],
  202 + housePurchasePhotos: [],
  203 + repairPhotos: [],
  204 + deedTaxPhotos: [],
  205 + communityId: ''
  206 + }
  207 + }
  208 + }
  209 +}
  210 +</script>
  211 +
  212 +<style lang="scss" scoped>
  213 +.dialog-footer {
  214 + text-align: right;
  215 + margin-top: 20px;
  216 +}
  217 +
  218 +.help-block {
  219 + color: #f56c6c;
  220 + font-size: 12px;
  221 + margin-top: 5px;
  222 +}
  223 +</style>
0 224 \ No newline at end of file
... ...
src/components/room/examinePropertyRightRegistration.vue 0 → 100644
  1 +<template>
  2 + <el-dialog :title="$t('propertyRightRegistration.examine.title')" :visible.sync="visible" width="50%"
  3 + @close="handleClose">
  4 + <el-form ref="form" :model="formData" label-width="120px">
  5 + <el-form-item :label="$t('propertyRightRegistration.examine.room')">
  6 + <el-input v-model="formData.allNum" disabled
  7 + :placeholder="$t('propertyRightRegistration.examine.roomPlaceholder')" />
  8 + </el-form-item>
  9 +
  10 + <el-form-item :label="$t('propertyRightRegistration.examine.state')" prop="state"
  11 + :rules="[{ required: true, message: $t('propertyRightRegistration.examine.stateRequired'), trigger: 'change' }]">
  12 + <el-select v-model="formData.state" :placeholder="$t('propertyRightRegistration.examine.statePlaceholder')"
  13 + style="width:100%">
  14 + <el-option v-for="item in stateOptions" :key="item.statusCd" :label="item.name" :value="item.statusCd"
  15 + :disabled="item.statusCd === '0'" />
  16 + </el-select>
  17 + </el-form-item>
  18 +
  19 + <el-form-item :label="$t('propertyRightRegistration.examine.remark')">
  20 + <el-input v-model="formData.remark" type="textarea" :rows="3"
  21 + :placeholder="$t('propertyRightRegistration.examine.remarkPlaceholder')" />
  22 + </el-form-item>
  23 + </el-form>
  24 +
  25 + <div slot="footer" class="dialog-footer">
  26 + <el-button @click="visible = false">{{ $t('common.cancel') }}</el-button>
  27 + <el-button type="primary" @click="handleSubmit">{{ $t('common.confirm') }}</el-button>
  28 + </div>
  29 + </el-dialog>
  30 +</template>
  31 +
  32 +<script>
  33 +import { getDict ,getCommunityId} from '@/api/community/communityApi'
  34 +import { updatePropertyRightRegistration } from '@/api/room/propertyRightRegistrationManageApi'
  35 +
  36 +export default {
  37 + name: 'ExaminePropertyRightRegistration',
  38 + data() {
  39 + return {
  40 + visible: false,
  41 + formData: {
  42 + prrId: '',
  43 + roomId: '',
  44 + floorNum: '',
  45 + unitNum: '',
  46 + roomNum: '',
  47 + allNum: '',
  48 + state: '',
  49 + remark: '',
  50 + flag: '1'
  51 + },
  52 + stateOptions: []
  53 + }
  54 + },
  55 + methods: {
  56 + async open(data) {
  57 + this.visible = true
  58 + await this.getStateOptions()
  59 +
  60 + this.formData = {
  61 + prrId: data.prrId,
  62 + roomId: data.roomId,
  63 + floorNum: data.floorNum,
  64 + unitNum: data.unitNum,
  65 + roomNum: data.roomNum,
  66 + allNum: `${data.floorNum}-${data.unitNum}-${data.roomNum}`,
  67 + state: data.state === '0' ? '' : data.state,
  68 + remark: data.remark || '',
  69 + flag: '1'
  70 + }
  71 + },
  72 + async getStateOptions() {
  73 + try {
  74 + this.stateOptions = await getDict('property_right_registration', 'state')
  75 + } catch (error) {
  76 + console.error('获取审核状态失败:', error)
  77 + this.$message.error(this.$t('propertyRightRegistration.examine.fetchStateError'))
  78 + }
  79 + },
  80 + handleClose() {
  81 + this.$refs.form.resetFields()
  82 + this.formData = {
  83 + prrId: '',
  84 + roomId: '',
  85 + floorNum: '',
  86 + unitNum: '',
  87 + roomNum: '',
  88 + allNum: '',
  89 + state: '',
  90 + remark: '',
  91 + flag: '1'
  92 + }
  93 + },
  94 + async handleSubmit() {
  95 + try {
  96 + await this.$refs.form.validate()
  97 +
  98 + const params = {
  99 + ...this.formData,
  100 + communityId: getCommunityId()
  101 + }
  102 +
  103 + const res = await updatePropertyRightRegistration(params)
  104 + if (res.code === 0) {
  105 + this.$message.success(this.$t('propertyRightRegistration.examine.success'))
  106 + this.visible = false
  107 + this.$emit('success')
  108 + } else {
  109 + this.$message.error(res.msg || this.$t('propertyRightRegistration.examine.error'))
  110 + }
  111 + } catch (error) {
  112 + if (error !== 'validate') {
  113 + console.error('审核失败:', error)
  114 + this.$message.error(this.$t('propertyRightRegistration.examine.error'))
  115 + }
  116 + }
  117 + }
  118 + }
  119 +}
  120 +</script>
  121 +
  122 +<style scoped>
  123 +.dialog-footer {
  124 + text-align: right;
  125 +}
  126 +</style>
0 127 \ No newline at end of file
... ...
src/components/room/floorUnitAllTree.vue 0 → 100644
  1 +<template>
  2 + <el-card class="tree-card">
  3 + <el-tree
  4 + ref="tree"
  5 + :data="treeData"
  6 + :props="defaultProps"
  7 + node-key="id"
  8 + :default-expanded-keys="defaultExpandedKeys"
  9 + :highlight-current="true"
  10 + @node-click="handleNodeClick"
  11 + >
  12 + <template #default="{ node, data }">
  13 + <span class="custom-tree-node">
  14 + <img v-if="data.icon" :src="data.icon" class="tree-icon" />
  15 + <span>{{ node.label }}</span>
  16 + </span>
  17 + </template>
  18 + </el-tree>
  19 + </el-card>
  20 +</template>
  21 +
  22 +<script>
  23 +import { queryFloorAndUnits } from '@/api/room/roomStructureApi'
  24 +import { getCommunityId } from '@/api/community/communityApi'
  25 +
  26 +export default {
  27 + name: 'FloorUnitAllTree',
  28 + props: {
  29 + callBackListener: {
  30 + type: String,
  31 + default: ''
  32 + }
  33 + },
  34 + data() {
  35 + return {
  36 + floorUnitTreeInfo: {
  37 + units: [],
  38 + floorId: ''
  39 + },
  40 + treeData: [],
  41 + defaultProps: {
  42 + children: 'children',
  43 + label: 'text'
  44 + },
  45 + defaultExpandedKeys: [],
  46 + communityId: ''
  47 + }
  48 + },
  49 + created() {
  50 + this.communityId = getCommunityId()
  51 + this.loadFloorAndUnits()
  52 + },
  53 + methods: {
  54 + async loadFloorAndUnits() {
  55 + try {
  56 + const params = {
  57 + communityId: this.communityId
  58 + }
  59 + const data = await queryFloorAndUnits(params)
  60 + this.floorUnitTreeInfo.units = data
  61 + this.initTreeData()
  62 + } catch (error) {
  63 + console.error('Failed to load floor and units:', error)
  64 + }
  65 + },
  66 + initTreeData() {
  67 + this.treeData = this.doTreeData()
  68 + if (this.treeData.length > 0 && this.treeData[0].children) {
  69 + this.defaultExpandedKeys = [this.treeData[0].id]
  70 + }
  71 + },
  72 + doTreeData() {
  73 + const treeData = []
  74 + const units = this.floorUnitTreeInfo.units
  75 +
  76 + // Build first level menu (floors)
  77 + units.forEach(item => {
  78 + const existingFloor = treeData.find(floor => floor.floorId === item.floorId)
  79 + if (!existingFloor) {
  80 + const floorItem = {
  81 + id: `f_${item.floorId}`,
  82 + floorId: item.floorId,
  83 + floorNum: item.floorNum,
  84 + icon: "/img/floor.png",
  85 + text: `${item.floorNum}${this.$t('floorUnitTree.building')}(${item.floorName})`,
  86 + children: this.doTreeChildrenData(item.floorId)
  87 + }
  88 + treeData.push(floorItem)
  89 + }
  90 + })
  91 +
  92 + return treeData
  93 + },
  94 + doTreeChildrenData(floorId) {
  95 + const children = []
  96 + const units = this.floorUnitTreeInfo.units
  97 +
  98 + units.forEach(item => {
  99 + if (item.floorId === floorId && item.unitId) {
  100 + children.push({
  101 + id: `u_${item.unitId}`,
  102 + unitId: item.unitId,
  103 + text: `${item.unitNum}${this.$t('floorUnitTree.unit')}`,
  104 + icon: "/img/unit.png"
  105 + })
  106 + }
  107 + })
  108 +
  109 + return children
  110 + },
  111 + handleNodeClick(data) {
  112 + if (data.id.startsWith('f_')) {
  113 + this.$emit('switchFloor', {
  114 + floorId: data.floorId
  115 + })
  116 + } else {
  117 + this.$emit('switchUnit', {
  118 + unitId: data.unitId
  119 + })
  120 + }
  121 + },
  122 + refreshTree(params) {
  123 + if (params) {
  124 + this.floorUnitTreeInfo.floorId = params.floorId
  125 + }
  126 + this.loadFloorAndUnits()
  127 + }
  128 + }
  129 +}
  130 +</script>
  131 +
  132 +<style lang="scss" scoped>
  133 +.tree-card {
  134 + height: 100%;
  135 +
  136 + .custom-tree-node {
  137 + display: flex;
  138 + align-items: center;
  139 +
  140 + .tree-icon {
  141 + width: 16px;
  142 + height: 16px;
  143 + margin-right: 5px;
  144 + }
  145 + }
  146 +
  147 + :deep(.el-tree-node__content) {
  148 + height: 36px;
  149 + }
  150 +}
  151 +</style>
0 152 \ No newline at end of file
... ...
src/components/room/uploadImageUrl.vue 0 → 100644
  1 +<template>
  2 + <div class="upload-image-wrapper">
  3 + <div v-for="(image, index) in photos" :key="index" class="image-preview">
  4 + <el-image
  5 + :src="getImageUrl(image)"
  6 + fit="cover"
  7 + class="preview-image"
  8 + :preview-src-list="previewList"
  9 + >
  10 + <div slot="error" class="image-error">
  11 + <img src="/img/noPhoto.jpg" class="error-image">
  12 + </div>
  13 + </el-image>
  14 + <i class="el-icon-delete delete-icon" @click="removeImage(index)"></i>
  15 + </div>
  16 +
  17 + <div
  18 + v-if="photos.length < imageCount"
  19 + class="upload-button"
  20 + @click="triggerUpload"
  21 + >
  22 + <i class="el-icon-plus"></i>
  23 + </div>
  24 +
  25 + <input
  26 + type="file"
  27 + ref="fileInput"
  28 + accept="image/*"
  29 + class="file-input"
  30 + @change="handleFileChange"
  31 + />
  32 + </div>
  33 +</template>
  34 +
  35 +<script>
  36 +import { uploadFile } from '@/api/common'
  37 +
  38 +export default {
  39 + name: 'UploadImageUrl',
  40 + props: {
  41 + imageCount: {
  42 + type: Number,
  43 + default: 99
  44 + }
  45 + },
  46 + data() {
  47 + return {
  48 + photos: [], // 用于显示的图片数组
  49 + photosUrl: [] // 包含fileId和url的对象数组
  50 + }
  51 + },
  52 + computed: {
  53 + previewList() {
  54 + return this.photos.map(item => this.getImageUrl(item))
  55 + }
  56 + },
  57 + methods: {
  58 + getImageUrl(item) {
  59 + if (typeof item === 'string') {
  60 + if (item.startsWith('http') || item.startsWith('https') || item.startsWith('data:')) {
  61 + return item
  62 + }
  63 + return `/callComponent/download/getFile/file?fileId=${item}&communityId=-1&time=${new Date()}`
  64 + }
  65 + return item.url || ''
  66 + },
  67 + triggerUpload() {
  68 + this.$refs.fileInput.click()
  69 + },
  70 + handleFileChange(event) {
  71 + const file = event.target.files[0]
  72 + if (!file) return
  73 +
  74 + if (file.size > 2 * 1024 * 1024) {
  75 + this.$message.error(this.$t('upload.imageSizeLimit'))
  76 + return
  77 + }
  78 +
  79 + const reader = new FileReader()
  80 + reader.onload = (e) => {
  81 + const base64Image = e.target.result
  82 + this.photos.push(base64Image)
  83 + this.uploadFile(file)
  84 + }
  85 + reader.readAsDataURL(file)
  86 + event.target.value = null
  87 + },
  88 + uploadFile(file) {
  89 + const formData = new FormData()
  90 + formData.append('uploadFile', file)
  91 + formData.append('communityId', this.$store.getters.communityId)
  92 +
  93 + uploadFile(formData)
  94 + .then(response => {
  95 + const newPhoto = {
  96 + fileId: response.data.fileId,
  97 + url: response.data.url
  98 + }
  99 + this.photosUrl.push(newPhoto)
  100 + this.$emit('change', this.photosUrl)
  101 + })
  102 + .catch(error => {
  103 + this.$message.error(error.message || this.$t('upload.failed'))
  104 + this.photos.pop()
  105 + })
  106 + },
  107 + removeImage(index) {
  108 + this.photos.splice(index, 1)
  109 + this.photosUrl.splice(index, 1)
  110 + this.$emit('change', this.photosUrl)
  111 + },
  112 + setPhotos(photos) {
  113 + this.photos = [...photos]
  114 + this.photosUrl = photos.map(photo => ({
  115 + fileId: photo,
  116 + url: `/callComponent/download/getFile/file?fileId=${photo}&communityId=-1&time=${new Date()}`
  117 + }))
  118 + this.$emit('change', this.photosUrl)
  119 + },
  120 + clear() {
  121 + this.photos = []
  122 + this.photosUrl = []
  123 + this.$emit('change', this.photosUrl)
  124 + }
  125 + }
  126 +}
  127 +</script>
  128 +
  129 +<style lang="scss" scoped>
  130 +.upload-image-wrapper {
  131 + display: flex;
  132 + flex-wrap: wrap;
  133 + gap: 10px;
  134 +
  135 + .image-preview {
  136 + position: relative;
  137 + width: 100px;
  138 + height: 100px;
  139 +
  140 + .preview-image {
  141 + width: 100%;
  142 + height: 100%;
  143 + border: 1px solid #dcdfe6;
  144 + border-radius: 4px;
  145 + }
  146 +
  147 + .delete-icon {
  148 + position: absolute;
  149 + top: -8px;
  150 + right: -8px;
  151 + font-size: 16px;
  152 + color: #f56c6c;
  153 + background: #fff;
  154 + border-radius: 50%;
  155 + cursor: pointer;
  156 + }
  157 + }
  158 +
  159 + .upload-button {
  160 + width: 100px;
  161 + height: 100px;
  162 + display: flex;
  163 + justify-content: center;
  164 + align-items: center;
  165 + border: 1px dashed #c0ccda;
  166 + border-radius: 4px;
  167 + background-color: #fbfdff;
  168 + color: #8c939d;
  169 + font-size: 28px;
  170 + cursor: pointer;
  171 +
  172 + &:hover {
  173 + border-color: #409eff;
  174 + }
  175 + }
  176 +
  177 + .file-input {
  178 + display: none;
  179 + }
  180 +
  181 + .image-error {
  182 + width: 100%;
  183 + height: 100%;
  184 + display: flex;
  185 + justify-content: center;
  186 + align-items: center;
  187 + background-color: #f5f7fa;
  188 +
  189 + .error-image {
  190 + width: 100%;
  191 + height: 100%;
  192 + object-fit: cover;
  193 + }
  194 + }
  195 +}
  196 +</style>
0 197 \ No newline at end of file
... ...
src/components/room/viewImage.vue 0 → 100644
  1 +<template>
  2 + <div class="image-viewer" v-show="visible" @click.self="close">
  3 + <div class="image-container">
  4 + <el-image
  5 + :src="imageUrl"
  6 + fit="contain"
  7 + :style="{width: imgWidth + 'px', height: imgHeight + 'px'}"
  8 + >
  9 + <div slot="error" class="image-error">
  10 + <img src="/img/noPhoto.jpg" :style="{width: imgWidth + 'px', height: imgHeight + 'px'}">
  11 + </div>
  12 + </el-image>
  13 + <i class="el-icon-close close-icon" @click="close"></i>
  14 + </div>
  15 + </div>
  16 +</template>
  17 +
  18 +<script>
  19 +export default {
  20 + name: 'ViewImage',
  21 + data() {
  22 + return {
  23 + visible: false,
  24 + imageUrl: '',
  25 + imgWidth: 800,
  26 + imgHeight: 600
  27 + }
  28 + },
  29 + methods: {
  30 + open(url) {
  31 + this.imageUrl = url
  32 + this.visible = true
  33 + this.calculateImageSize(url)
  34 + document.body.style.overflow = 'hidden'
  35 + },
  36 + close() {
  37 + this.visible = false
  38 + this.imageUrl = ''
  39 + document.body.style.overflow = ''
  40 + },
  41 + calculateImageSize(url) {
  42 + const img = new Image()
  43 + img.src = url
  44 + img.onload = () => {
  45 + const maxWidth = window.innerWidth * 0.8
  46 + const maxHeight = window.innerHeight * 0.8
  47 + const ratio = img.width / img.height
  48 +
  49 + if (img.width > maxWidth) {
  50 + this.imgWidth = maxWidth
  51 + this.imgHeight = maxWidth / ratio
  52 + } else if (img.height > maxHeight) {
  53 + this.imgHeight = maxHeight
  54 + this.imgWidth = maxHeight * ratio
  55 + } else {
  56 + this.imgWidth = img.width
  57 + this.imgHeight = img.height
  58 + }
  59 + }
  60 + img.onerror = () => {
  61 + this.imgWidth = 800
  62 + this.imgHeight = 600
  63 + }
  64 + }
  65 + }
  66 +}
  67 +</script>
  68 +
  69 +<style lang="scss" scoped>
  70 +.image-viewer {
  71 + position: fixed;
  72 + top: 0;
  73 + left: 0;
  74 + width: 100%;
  75 + height: 100%;
  76 + background-color: rgba(0, 0, 0, 0.8);
  77 + z-index: 9999;
  78 + display: flex;
  79 + justify-content: center;
  80 + align-items: center;
  81 +
  82 + .image-container {
  83 + position: relative;
  84 + background-color: #fff;
  85 + padding: 20px;
  86 + border-radius: 4px;
  87 + max-width: 90vw;
  88 + max-height: 90vh;
  89 +
  90 + .close-icon {
  91 + position: absolute;
  92 + top: 10px;
  93 + right: 10px;
  94 + font-size: 24px;
  95 + color: #f56c6c;
  96 + cursor: pointer;
  97 + z-index: 1;
  98 + background-color: rgba(255, 255, 255, 0.7);
  99 + border-radius: 50%;
  100 + padding: 5px;
  101 + }
  102 + }
  103 +
  104 + .image-error {
  105 + display: flex;
  106 + justify-content: center;
  107 + align-items: center;
  108 + background-color: #f5f7fa;
  109 + width: 100%;
  110 + height: 100%;
  111 + }
  112 +}
  113 +</style>
0 114 \ No newline at end of file
... ...
src/components/system/addFeePrintPage.vue 0 → 100644
  1 +<template>
  2 + <el-dialog
  3 + :title="$t('feePrintPageManage.add.title')"
  4 + :visible.sync="visible"
  5 + width="50%"
  6 + @close="handleClose"
  7 + >
  8 + <el-form
  9 + ref="form"
  10 + :model="formData"
  11 + :rules="rules"
  12 + label-width="120px"
  13 + >
  14 + <el-form-item
  15 + :label="$t('feePrintPageManage.add.pageName')"
  16 + prop="pageName"
  17 + >
  18 + <el-input
  19 + v-model="formData.pageName"
  20 + :placeholder="$t('feePrintPageManage.add.pageNamePlaceholder')"
  21 + />
  22 + </el-form-item>
  23 + <el-form-item
  24 + :label="$t('feePrintPageManage.add.template')"
  25 + prop="pageUrl"
  26 + >
  27 + <el-select
  28 + v-model="formData.pageUrl"
  29 + :placeholder="$t('feePrintPageManage.add.templatePlaceholder')"
  30 + style="width:100%"
  31 + >
  32 + <el-option
  33 + v-for="item in templates"
  34 + :key="item.templateId"
  35 + :label="item.name"
  36 + :value="item.templateId"
  37 + />
  38 + </el-select>
  39 + </el-form-item>
  40 + </el-form>
  41 + <span slot="footer" class="dialog-footer">
  42 + <el-button @click="visible = false">
  43 + {{ $t('common.cancel') }}
  44 + </el-button>
  45 + <el-button
  46 + type="primary"
  47 + :loading="loading"
  48 + @click="handleSubmit"
  49 + >
  50 + {{ $t('common.confirm') }}
  51 + </el-button>
  52 + </span>
  53 + </el-dialog>
  54 +</template>
  55 +
  56 +<script>
  57 +import { getCommunityId } from '@/api/community/communityApi'
  58 +import { saveFeePrintPage, listFeePrintPageTemplate } from '@/api/system/feePrintPageManageApi'
  59 +
  60 +export default {
  61 + name: 'AddFeePrintPage',
  62 + data() {
  63 + return {
  64 + visible: false,
  65 + loading: false,
  66 + communityId: '',
  67 + formData: {
  68 + pageName: '',
  69 + pageUrl: '',
  70 + communityId: ''
  71 + },
  72 + templates: [],
  73 + rules: {
  74 + pageName: [
  75 + { required: true, message: this.$t('feePrintPageManage.validate.pageNameRequired'), trigger: 'blur' },
  76 + { max: 128, message: this.$t('feePrintPageManage.validate.pageNameMaxLength'), trigger: 'blur' }
  77 + ],
  78 + pageUrl: [
  79 + { required: true, message: this.$t('feePrintPageManage.validate.templateRequired'), trigger: 'change' }
  80 + ]
  81 + }
  82 + }
  83 + },
  84 + methods: {
  85 + open() {
  86 + this.communityId = getCommunityId()
  87 + this.formData.communityId = this.communityId
  88 + this.getTemplates()
  89 + this.visible = true
  90 + },
  91 + async getTemplates() {
  92 + try {
  93 + const params = {
  94 + page: 1,
  95 + row: 50,
  96 + communityId: this.communityId
  97 + }
  98 + const { data } = await listFeePrintPageTemplate(params)
  99 + this.templates = data
  100 + } catch (error) {
  101 + console.error('获取模板列表失败:', error)
  102 + }
  103 + },
  104 + handleSubmit() {
  105 + this.$refs.form.validate(async valid => {
  106 + if (valid) {
  107 + try {
  108 + this.loading = true
  109 + await saveFeePrintPage(this.formData)
  110 + this.$message.success(this.$t('feePrintPageManage.add.success'))
  111 + this.$emit('success')
  112 + this.visible = false
  113 + } catch (error) {
  114 + console.error('添加失败:', error)
  115 + } finally {
  116 + this.loading = false
  117 + }
  118 + }
  119 + })
  120 + },
  121 + handleClose() {
  122 + this.$refs.form.resetFields()
  123 + this.formData = {
  124 + pageName: '',
  125 + pageUrl: '',
  126 + communityId: this.communityId
  127 + }
  128 + }
  129 + }
  130 +}
  131 +</script>
0 132 \ No newline at end of file
... ...
src/components/system/deleteDownloadTempFile.vue 0 → 100644
  1 +<template>
  2 + <el-dialog
  3 + :title="$t('deleteDownloadTempFile.title')"
  4 + :visible.sync="visible"
  5 + width="30%"
  6 + @close="handleClose"
  7 + >
  8 + <div style="text-align: center;">
  9 + <p>{{ $t('deleteDownloadTempFile.confirmMessage') }}</p>
  10 + </div>
  11 + <span slot="footer" class="dialog-footer">
  12 + <el-button @click="handleClose">{{ $t('deleteDownloadTempFile.cancel') }}</el-button>
  13 + <el-button type="primary" @click="handleDelete">{{ $t('deleteDownloadTempFile.confirm') }}</el-button>
  14 + </span>
  15 + </el-dialog>
  16 +</template>
  17 +
  18 +<script>
  19 +import { deleteUserDownloadFile } from '@/api/system/downloadTempFileApi'
  20 +import { getCommunityId } from '@/api/community/communityApi'
  21 +
  22 +export default {
  23 + name: 'DeleteDownloadTempFile',
  24 + data() {
  25 + return {
  26 + visible: false,
  27 + currentFile: null
  28 + }
  29 + },
  30 + methods: {
  31 + open(file) {
  32 + this.currentFile = file
  33 + this.visible = true
  34 + },
  35 + handleClose() {
  36 + this.visible = false
  37 + this.currentFile = null
  38 + },
  39 + async handleDelete() {
  40 + try {
  41 + const params = {
  42 + ...this.currentFile,
  43 + communityId: getCommunityId()
  44 + }
  45 + await deleteUserDownloadFile(params)
  46 + this.$emit('success')
  47 + this.$message.success(this.$t('deleteDownloadTempFile.deleteSuccess'))
  48 + this.handleClose()
  49 + } catch (error) {
  50 + this.$message.error(this.$t('deleteDownloadTempFile.deleteFailed'))
  51 + }
  52 + }
  53 + }
  54 +}
  55 +</script>
0 56 \ No newline at end of file
... ...
src/components/system/deleteFeePrintPage.vue 0 → 100644
  1 +<template>
  2 + <el-dialog
  3 + :title="$t('feePrintPageManage.delete.title')"
  4 + :visible.sync="visible"
  5 + width="30%"
  6 + @close="handleClose"
  7 + >
  8 + <div class="delete-content">
  9 + <i class="el-icon-warning" style="color:#E6A23C;font-size:24px;vertical-align:middle;"></i>
  10 + <span style="margin-left:10px;vertical-align:middle;">
  11 + {{ $t('feePrintPageManage.delete.confirmText') }}
  12 + </span>
  13 + </div>
  14 + <span slot="footer" class="dialog-footer">
  15 + <el-button @click="visible = false">
  16 + {{ $t('common.cancel') }}
  17 + </el-button>
  18 + <el-button
  19 + type="primary"
  20 + :loading="loading"
  21 + @click="handleConfirm"
  22 + >
  23 + {{ $t('common.confirm') }}
  24 + </el-button>
  25 + </span>
  26 + </el-dialog>
  27 +</template>
  28 +
  29 +<script>
  30 +import { getCommunityId } from '@/api/community/communityApi'
  31 +import { deleteFeePrintPage } from '@/api/system/feePrintPageManageApi'
  32 +
  33 +export default {
  34 + name: 'DeleteFeePrintPage',
  35 + data() {
  36 + return {
  37 + visible: false,
  38 + loading: false,
  39 + communityId: '',
  40 + currentRow: {
  41 + pageId: '',
  42 + communityId: ''
  43 + }
  44 + }
  45 + },
  46 + methods: {
  47 + open(row) {
  48 + this.communityId = getCommunityId()
  49 + this.currentRow = {
  50 + pageId: row.pageId,
  51 + communityId: this.communityId
  52 + }
  53 + this.visible = true
  54 + },
  55 + async handleConfirm() {
  56 + try {
  57 + this.loading = true
  58 + await deleteFeePrintPage(this.currentRow)
  59 + this.$message.success(this.$t('feePrintPageManage.delete.success'))
  60 + this.$emit('success')
  61 + this.visible = false
  62 + } catch (error) {
  63 + console.error('删除失败:', error)
  64 + } finally {
  65 + this.loading = false
  66 + }
  67 + },
  68 + handleClose() {
  69 + this.currentRow = {
  70 + pageId: '',
  71 + communityId: ''
  72 + }
  73 + }
  74 + }
  75 +}
  76 +</script>
  77 +
  78 +<style scoped>
  79 +.delete-content {
  80 + padding: 10px 20px;
  81 + font-size: 16px;
  82 +}
  83 +</style>
0 84 \ No newline at end of file
... ...
src/components/system/editFeePrintPage.vue 0 → 100644
  1 +<template>
  2 + <el-dialog
  3 + :title="$t('feePrintPageManage.edit.title')"
  4 + :visible.sync="visible"
  5 + width="50%"
  6 + @close="handleClose"
  7 + >
  8 + <el-form
  9 + ref="form"
  10 + :model="formData"
  11 + :rules="rules"
  12 + label-width="120px"
  13 + >
  14 + <el-form-item
  15 + :label="$t('feePrintPageManage.edit.pageName')"
  16 + prop="pageName"
  17 + >
  18 + <el-input
  19 + v-model="formData.pageName"
  20 + :placeholder="$t('feePrintPageManage.edit.pageNamePlaceholder')"
  21 + />
  22 + </el-form-item>
  23 + <el-form-item
  24 + :label="$t('feePrintPageManage.edit.template')"
  25 + prop="pageUrl"
  26 + >
  27 + <el-select
  28 + v-model="formData.pageUrl"
  29 + :placeholder="$t('feePrintPageManage.edit.templatePlaceholder')"
  30 + style="width:100%"
  31 + >
  32 + <el-option
  33 + v-for="item in templates"
  34 + :key="item.templateId"
  35 + :label="item.name"
  36 + :value="item.templateId"
  37 + />
  38 + </el-select>
  39 + </el-form-item>
  40 + </el-form>
  41 + <span slot="footer" class="dialog-footer">
  42 + <el-button @click="visible = false">
  43 + {{ $t('common.cancel') }}
  44 + </el-button>
  45 + <el-button
  46 + type="primary"
  47 + :loading="loading"
  48 + @click="handleSubmit"
  49 + >
  50 + {{ $t('common.confirm') }}
  51 + </el-button>
  52 + </span>
  53 + </el-dialog>
  54 +</template>
  55 +
  56 +<script>
  57 +import { getCommunityId } from '@/api/community/communityApi'
  58 +import { updateFeePrintPage, listFeePrintPageTemplate } from '@/api/system/feePrintPageManageApi'
  59 +
  60 +export default {
  61 + name: 'EditFeePrintPage',
  62 + data() {
  63 + return {
  64 + visible: false,
  65 + loading: false,
  66 + communityId: '',
  67 + formData: {
  68 + pageId: '',
  69 + pageName: '',
  70 + pageUrl: '',
  71 + communityId: ''
  72 + },
  73 + templates: [],
  74 + rules: {
  75 + pageName: [
  76 + { required: true, message: this.$t('feePrintPageManage.validate.pageNameRequired'), trigger: 'blur' },
  77 + { max: 128, message: this.$t('feePrintPageManage.validate.pageNameMaxLength'), trigger: 'blur' }
  78 + ],
  79 + pageUrl: [
  80 + { required: true, message: this.$t('feePrintPageManage.validate.templateRequired'), trigger: 'change' }
  81 + ],
  82 + pageId: [
  83 + { required: true, message: this.$t('feePrintPageManage.validate.pageIdRequired'), trigger: 'blur' }
  84 + ]
  85 + }
  86 + }
  87 + },
  88 + methods: {
  89 + open(row) {
  90 + this.communityId = getCommunityId()
  91 + this.formData = {
  92 + pageId: row.pageId,
  93 + pageName: row.pageName,
  94 + pageUrl: row.pageUrl,
  95 + communityId: this.communityId
  96 + }
  97 + this.getTemplates()
  98 + this.visible = true
  99 + },
  100 + async getTemplates() {
  101 + try {
  102 + const params = {
  103 + page: 1,
  104 + row: 50,
  105 + communityId: this.communityId
  106 + }
  107 + const { data } = await listFeePrintPageTemplate(params)
  108 + this.templates = data
  109 + } catch (error) {
  110 + console.error('获取模板列表失败:', error)
  111 + }
  112 + },
  113 + handleSubmit() {
  114 + this.$refs.form.validate(async valid => {
  115 + if (valid) {
  116 + try {
  117 + this.loading = true
  118 + await updateFeePrintPage(this.formData)
  119 + this.$message.success(this.$t('feePrintPageManage.edit.success'))
  120 + this.$emit('success')
  121 + this.visible = false
  122 + } catch (error) {
  123 + console.error('更新失败:', error)
  124 + } finally {
  125 + this.loading = false
  126 + }
  127 + }
  128 + })
  129 + },
  130 + handleClose() {
  131 + this.$refs.form.resetFields()
  132 + this.formData = {
  133 + pageId: '',
  134 + pageName: '',
  135 + pageUrl: '',
  136 + communityId: this.communityId
  137 + }
  138 + }
  139 + }
  140 +}
  141 +</script>
0 142 \ No newline at end of file
... ...
src/components/upload/UploadImageUrl.md 0 → 100644
  1 +# UploadImageUrl 组件文档
  2 +
  3 +## 组件概述
  4 +
  5 +`UploadImageUrl` 是一个图片上传组件,支持上传本地图片并显示预览,同时可以删除已上传的图片。
  6 +
  7 +## 基本用法
  8 +
  9 +```vue
  10 +<template>
  11 + <upload-image-url
  12 + :image-count="3"
  13 + @notifyUploadCoverImage="handleImageUpload"
  14 + />
  15 +</template>
  16 +
  17 +<script>
  18 +import UploadImageUrl from '@/components/staff/UploadImageUrl.vue'
  19 +
  20 +export default {
  21 + components: {
  22 + UploadImageUrl
  23 + },
  24 + methods: {
  25 + handleImageUpload(urls) {
  26 + // 处理上传后的图片URL数组
  27 + console.log(urls)
  28 + }
  29 + }
  30 +}
  31 +</script>
  32 +```
  33 +
  34 +## Props
  35 +
  36 +| 参数名 | 说明 | 类型 | 默认值 |
  37 +|------------|--------------------|----------|--------|
  38 +| imageCount | 允许上传的最大图片数量 | Number | 1 |
  39 +
  40 +## Events
  41 +
  42 +| 事件名 | 说明 | 回调参数 |
  43 +|----------------------|--------------------------|-----------------------|
  44 +| notifyUploadCoverImage | 图片上传或删除时触发 | 当前所有图片的URL数组 |
  45 +
  46 +## 方法
  47 +
  48 +通过 ref 可以调用组件的方法:
  49 +
  50 +```vue
  51 +<template>
  52 + <upload-image-url ref="uploader" />
  53 +</template>
  54 +
  55 +<script>
  56 +export default {
  57 + methods: {
  58 + clearImages() {
  59 + this.$refs.uploader.clearImages()
  60 + },
  61 + setImages(images) {
  62 + this.$refs.uploader.setImages(images)
  63 + }
  64 + }
  65 +}
  66 +</script>
  67 +```
  68 +
  69 +| 方法名 | 说明 | 参数 |
  70 +|-------------|--------------------------|--------------------------|
  71 +| clearImages | 清空所有已上传的图片 | 无 |
  72 +| setImages | 设置要显示的图片 | images: Array<string> (图片URL数组) |
  73 +
  74 +## 样式定制
  75 +
  76 +组件使用 SCSS 编写样式,可以通过以下类名进行样式覆盖:
  77 +
  78 +- `.upload-image-container` - 整个上传容器
  79 +- `.image-item` - 单个图片项容器
  80 +- `.delete-icon` - 删除按钮
  81 +- `.upload-button` - 上传按钮
  82 +
  83 +## 注意事项
  84 +
  85 +1. 组件限制上传图片大小不超过 2MB
  86 +2. 上传的图片会自动转换为 base64 格式进行预览
  87 +3. 组件内部会维护两个数组:
  88 + - `photos`: 用于预览的图片数组(包含 base64 数据)
  89 + - `photosUrl`: 上传后的图片 URL 数组
  90 +4. 当图片 URL 以 `http` 开头时,组件会直接使用该 URL 进行显示
  91 +5. 当图片数据包含 `base64,` 时,组件会将其视为 base64 数据进行显示
  92 +
  93 +## 示例
  94 +
  95 +### 基本使用
  96 +
  97 +```vue
  98 +<upload-image-url />
  99 +```
  100 +
  101 +### 限制上传数量
  102 +
  103 +```vue
  104 +<upload-image-url :image-count="5" />
  105 +```
  106 +
  107 +### 获取上传结果
  108 +
  109 +```vue
  110 +<template>
  111 + <upload-image-url @notifyUploadCoverImage="handleUpload" />
  112 +</template>
  113 +
  114 +<script>
  115 +export default {
  116 + methods: {
  117 + handleUpload(urls) {
  118 + console.log('上传的图片URL:', urls)
  119 + // 可以将这些URL保存到表单数据中
  120 + }
  121 + }
  122 +}
  123 +</script>
  124 +```
  125 +
  126 +### 设置初始图片
  127 +
  128 +```vue
  129 +<template>
  130 + <upload-image-url ref="uploader" />
  131 +</template>
  132 +
  133 +<script>
  134 +export default {
  135 + mounted() {
  136 + // 设置初始图片
  137 + this.$refs.uploader.setImages([
  138 + 'https://example.com/image1.jpg',
  139 + 'https://example.com/image2.jpg'
  140 + ])
  141 + }
  142 +}
  143 +</script>
  144 +```
  145 +
  146 +### 清空图片
  147 +
  148 +```vue
  149 +<template>
  150 + <upload-image-url ref="uploader" />
  151 + <button @click="clear">清空图片</button>
  152 +</template>
  153 +
  154 +<script>
  155 +export default {
  156 + methods: {
  157 + clear() {
  158 + this.$refs.uploader.clearImages()
  159 + }
  160 + }
  161 +}
  162 +</script>
  163 +```
0 164 \ No newline at end of file
... ...
src/i18n/commonLang.js
... ... @@ -11,6 +11,7 @@ export const messages = {
11 11 delete: 'Delete',
12 12 confirm: 'Confirm',
13 13 cancel: 'Cancel',
  14 + close: 'Close',
14 15 back: 'Back',
15 16 warning: 'Warning',
16 17 deleteConfirm: 'Are you sure to delete this record?',
... ... @@ -50,6 +51,7 @@ export const messages = {
50 51 print:'Print',
51 52 year:'Year',
52 53 month:'Month',
  54 + examine:'Examine',
53 55 }
54 56 },
55 57 zh: {
... ... @@ -64,6 +66,7 @@ export const messages = {
64 66 delete: '删除',
65 67 confirm: '确定',
66 68 cancel: '取消',
  69 + close: '关闭',
67 70 back: '返回',
68 71 warning: '警告',
69 72 deleteConfirm: '确定要删除这条记录吗?',
... ... @@ -103,6 +106,7 @@ export const messages = {
103 106 print:'打印',
104 107 year:'年',
105 108 month:'月',
  109 + examine:'审核',
106 110 }
107 111 }
108 112 }
109 113 \ No newline at end of file
... ...
src/i18n/communityI18n.js 0 → 100644
  1 +import { messages as roomStructureMessages } from '../views/room/roomStructureLang'
  2 +import { messages as carStructureMessages } from '../views/car/carStructureLang'
  3 +import { messages as propertyRightRegistrationManageMessages } from '../views/room/propertyRightRegistrationManageLang'
  4 +import { messages as listPropertyRightRegistrationDetailMessages } from '../views/room/listPropertyRightRegistrationDetailLang'
  5 +export const messages = {
  6 + en: {
  7 + ...roomStructureMessages.en,
  8 + ...carStructureMessages.en,
  9 + ...propertyRightRegistrationManageMessages.en,
  10 + ...listPropertyRightRegistrationDetailMessages.en,
  11 + },
  12 + zh: {
  13 + ...roomStructureMessages.zh,
  14 + ...carStructureMessages.zh,
  15 + ...propertyRightRegistrationManageMessages.zh,
  16 + ...listPropertyRightRegistrationDetailMessages.zh,
  17 + }
  18 +}
0 19 \ No newline at end of file
... ...
src/i18n/index.js
... ... @@ -144,6 +144,7 @@ import { messages as carI18n } from &#39;./carI18n&#39;
144 144 import { messages as scmI18n } from './scmI18n'
145 145 import { messages as userI18n } from './userI18n'
146 146 import { messages as systemI18n } from './systemI18n'
  147 +import { messages as communityI18n } from './communityI18n'
147 148  
148 149 Vue.use(VueI18n)
149 150  
... ... @@ -286,6 +287,7 @@ const messages = {
286 287 ...scmI18n.en,
287 288 ...userI18n.en,
288 289 ...systemI18n.en,
  290 + ...communityI18n.en,
289 291 },
290 292 zh: {
291 293 ...loginMessages.zh,
... ... @@ -422,6 +424,7 @@ const messages = {
422 424 ...scmI18n.zh,
423 425 ...userI18n.zh,
424 426 ...systemI18n.zh,
  427 + ...communityI18n.zh,
425 428 }
426 429 }
427 430  
... ...
src/i18n/machineI18n.js
... ... @@ -7,6 +7,7 @@ import { messages as equipmentAccountMessages } from &#39;../views/machine/equipment
7 7 import { messages as addEquipmentAccountMessages } from '../views/machine/addEquipmentAccountLang'
8 8 import { messages as editEquipmentAccountMessages } from '../views/machine/editEquipmentAccountLang'
9 9 import { messages as equipmentAccountDetailMessages } from '../views/machine/equipmentAccountDetailLang'
  10 +import { messages as videoControlMessages } from '../views/machine/videoControlLang'
10 11 export const messages ={
11 12 en:{
12 13 ...machineTypeTreeManageMessages.en,
... ... @@ -18,6 +19,7 @@ export const messages ={
18 19 ...accessControlInoutMessages.en,
19 20 ...machinePrinterManageMessages.en,
20 21 ...printerRuleMessages.en,
  22 + ...videoControlMessages.en,
21 23 },
22 24 zh:{
23 25 ...machineTypeTreeManageMessages.zh,
... ... @@ -29,5 +31,6 @@ export const messages ={
29 31 ...accessControlInoutMessages.zh,
30 32 ...machinePrinterManageMessages.zh,
31 33 ...printerRuleMessages.zh,
  34 + ...videoControlMessages.zh,
32 35 }
33 36 }
34 37 \ No newline at end of file
... ...
src/i18n/systemI18n.js
... ... @@ -6,6 +6,12 @@ import { messages as paymentPoolMessages } from &#39;../views/system/paymentPoolLang
6 6 import { messages as operateDataLogMessages } from '../views/system/operateDataLogLang'
7 7 import { messages as historyFeeDetailImportMessages } from '../views/system/historyFeeDetailImportLang'
8 8 import { messages as feePrintSpecManageMessages } from '../views/system/feePrintSpecManageLang'
  9 +import { messages as feePrintPageManageMessages } from '../views/system/feePrintPageManageLang'
  10 +import { messages as downloadTempFileMessages } from '../views/system/downloadTempFileLang'
  11 +import { messages as assetImportLogMessages } from '../views/system/assetImportLogLang'
  12 +import { messages as assetImportLogDetailMessages } from '../views/system/assetImportLogDetailLang'
  13 +import { messages as payFeeQrcodeMessages } from '../views/fee/payFeeQrcodeLang'
  14 +
9 15 export const messages = {
10 16 en: {
11 17 ...communitySettingManageMessages.en,
... ... @@ -16,6 +22,11 @@ export const messages = {
16 22 ...operateDataLogMessages.en,
17 23 ...historyFeeDetailImportMessages.en,
18 24 ...feePrintSpecManageMessages.en,
  25 + ...feePrintPageManageMessages.en,
  26 + ...downloadTempFileMessages.en,
  27 + ...assetImportLogMessages.en,
  28 + ...assetImportLogDetailMessages.en,
  29 + ...payFeeQrcodeMessages.en,
19 30 },
20 31 zh: {
21 32 ...communitySettingManageMessages.zh,
... ... @@ -26,5 +37,10 @@ export const messages = {
26 37 ...operateDataLogMessages.zh,
27 38 ...historyFeeDetailImportMessages.zh,
28 39 ...feePrintSpecManageMessages.zh,
  40 + ...feePrintPageManageMessages.zh,
  41 + ...downloadTempFileMessages.zh,
  42 + ...assetImportLogMessages.zh,
  43 + ...assetImportLogDetailMessages.zh,
  44 + ...payFeeQrcodeMessages.zh,
29 45 }
30 46 }
31 47 \ No newline at end of file
... ...
src/main.js
... ... @@ -6,6 +6,12 @@ import &#39;element-ui/lib/theme-chalk/index.css&#39;
6 6 import i18n from './i18n'
7 7 import {getCommunityName,getCommunityId} from '@/api/community/communityApi'
8 8  
  9 +// 验证全局脚本是否正确加载
  10 +console.log('检查全局脚本加载状态:')
  11 +console.log('Qs 库:', typeof window.Qs !== 'undefined' ? '已加载' : '未加载')
  12 +if (typeof window.Qs !== 'undefined') {
  13 + console.log('Qs 版本:', window.Qs.VERSION || '未知版本')
  14 +}
9 15  
10 16 Vue.prototype.getCommunityId = function(){
11 17 return getCommunityId()
... ...
src/router/communityRouter.js 0 → 100644
  1 +export default [
  2 + {
  3 + path: '/pages/property/roomStructure',
  4 + name: '/pages/property/roomStructure',
  5 + component: () => import('@/views/room/roomStructureList.vue')
  6 + },
  7 + {
  8 + path: '/pages/property/carStructure',
  9 + name: '/pages/property/carStructure',
  10 + component: () => import('@/views/car/carStructureList.vue')
  11 + },
  12 + {
  13 + path: '/pages/property/propertyRightRegistrationManage',
  14 + name: '/pages/property/propertyRightRegistrationManage',
  15 + component: () => import('@/views/room/propertyRightRegistrationManageList.vue')
  16 + },
  17 + {
  18 + path: '/views/room/listPropertyRightRegistrationDetail',
  19 + name: '/views/room/listPropertyRightRegistrationDetail',
  20 + component: () => import('@/views/room/listPropertyRightRegistrationDetailList.vue')
  21 + },
  22 +]
0 23 \ No newline at end of file
... ...
src/router/index.js
... ... @@ -14,6 +14,7 @@ import carRouter from &#39;./carRouter&#39;
14 14 import scmRouter from './scmRouter'
15 15 import userRouter from './userRouter'
16 16 import systemRouter from './systemRouter'
  17 +import communityRouter from './communityRouter'
17 18  
18 19 Vue.use(VueRouter)
19 20  
... ... @@ -636,6 +637,7 @@ const routes = [
636 637 ...scmRouter,
637 638 ...userRouter,
638 639 ...systemRouter,
  640 + ...communityRouter,
639 641 // 其他子路由可以在这里添加
640 642 ]
641 643 },
... ...
src/router/machineRouter.js
... ... @@ -49,4 +49,9 @@ export default [
49 49 name: '/pages/print/printerRule',
50 50 component: () => import('@/views/machine/printerRuleList.vue')
51 51 },
  52 + {
  53 + path: '/pages/property/videoControl',
  54 + name: '/pages/property/videoControl',
  55 + component: () => import('@/views/machine/videoControlList.vue')
  56 + },
52 57 ]
53 58 \ No newline at end of file
... ...
src/router/systemRouter.js
... ... @@ -49,4 +49,29 @@ export default [
49 49 name: '/pages/property/feePrintSpecManage',
50 50 component: () => import('@/views/system/feePrintSpecManageList.vue')
51 51 },
  52 + {
  53 + path: '/pages/property/feePrintPageManage',
  54 + name: '/pages/property/feePrintPageManage',
  55 + component: () => import('@/views/system/feePrintPageManageList.vue')
  56 + },
  57 + {
  58 + path: '/pages/property/downloadTempFile',
  59 + name: '/pages/property/downloadTempFile',
  60 + component: () => import('@/views/system/downloadTempFileList.vue')
  61 + },
  62 + {
  63 + path: '/pages/property/assetImportLog',
  64 + name: '/pages/property/assetImportLog',
  65 + component: () => import('@/views/system/assetImportLogList.vue')
  66 + },
  67 + {
  68 + path: '/views/system/assetImportLogDetail',
  69 + name: '/views/system/assetImportLogDetail',
  70 + component: () => import('@/views/system/assetImportLogDetailList.vue')
  71 + },
  72 + {
  73 + path: '/pages/fee/payFeeQrcode',
  74 + name: '/pages/fee/payFeeQrcode',
  75 + component: () => import('@/views/fee/payFeeQrcodeList.vue')
  76 + },
52 77 ]
53 78 \ No newline at end of file
... ...
src/views/car/carStructureLang.js 0 → 100644
  1 +export const messages = {
  2 + en: {
  3 + carStructure: {
  4 + noOwner: 'No Owner',
  5 + oweAmount: 'Arrears',
  6 + yuan: 'Yuan',
  7 + searchPlaceholder: 'Please enter house number building-unit-room such as 1-1-1',
  8 + fetchError: 'Failed to fetch car structure data'
  9 + },
  10 + floorUnitTree: {
  11 + noBuilding: 'No building currently',
  12 + building: 'Building',
  13 + unit: 'Unit',
  14 + fetchError: 'Failed to fetch floor and unit data'
  15 + }
  16 + },
  17 + zh: {
  18 + carStructure: {
  19 + noOwner: '无',
  20 + oweAmount: '欠费',
  21 + yuan: '元',
  22 + searchPlaceholder: '请输入房屋编号 楼栋-单元-房屋 如1-1-1',
  23 + fetchError: '获取车位结构数据失败'
  24 + },
  25 + floorUnitTree: {
  26 + noBuilding: '当前没有楼栋',
  27 + building: '栋',
  28 + unit: '单元',
  29 + fetchError: '获取楼栋单元数据失败'
  30 + }
  31 + }
  32 +}
0 33 \ No newline at end of file
... ...
src/views/car/carStructureList.vue 0 → 100644
  1 +<template>
  2 + <div class="car-structure-container">
  3 + <el-row :gutter="20">
  4 + <el-col :span="4">
  5 + <floor-unit-tree ref="floorUnitTree" @switchFloorUnit="switchFloorUnit" />
  6 + </el-col>
  7 + <el-col :span="20">
  8 + <el-card class="box-card margin-bottom car-list-card">
  9 + <el-row :gutter="10" class="margin-top">
  10 + <el-col v-for="(car, index) in carStructureInfo.cars" :key="index" :span="4"
  11 + class="text-center margin-bottom padding car-card" :style="{ 'background-color': getBgColor(car) }"
  12 + @dblclick.native="toSimplifyAcceptance(car)">
  13 + <div>{{ car.areaNum }}-{{ car.num }}</div>
  14 + <div>{{ car.carNum }}</div>
  15 + <div>{{ car.floorNum }}-{{ car.unitNum }}-{{ car.roomNum }}</div>
  16 + <div>{{ car.ownerName || $t('carStructure.noOwner') }}</div>
  17 + <div>
  18 + <span>{{ $t('carStructure.oweAmount') }}</span>:{{ car.oweAmount }}
  19 + <span>{{ $t('carStructure.yuan') }}</span>
  20 + </div>
  21 + </el-col>
  22 + </el-row>
  23 + </el-card>
  24 + </el-col>
  25 + </el-row>
  26 + </div>
  27 +</template>
  28 +
  29 +<script>
  30 +import floorUnitTree from '@/components/room/floorUnitTree'
  31 +import { listCarStructure } from '@/api/car/carStructureApi'
  32 +import { getCommunityId } from '@/api/community/communityApi'
  33 +
  34 +export default {
  35 + name: 'CarStructureList',
  36 + components: {
  37 + floorUnitTree
  38 + },
  39 + data() {
  40 + return {
  41 + carStructureInfo: {
  42 + cars: []
  43 + },
  44 + communityId: ''
  45 + }
  46 + },
  47 + created() {
  48 + this.communityId = getCommunityId()
  49 + },
  50 + methods: {
  51 + switchFloorUnit(data) {
  52 + if (!data.unitId) {
  53 + return
  54 + }
  55 + this.loadCars(data.unitId)
  56 + },
  57 + async loadCars(unitId) {
  58 + try {
  59 + const params = {
  60 + page: 1,
  61 + row: 100,
  62 + unitId: unitId,
  63 + communityId: this.communityId
  64 + }
  65 + const { data } = await listCarStructure(params)
  66 + this.carStructureInfo.cars = data
  67 + } catch (error) {
  68 + this.$message.error(this.$t('carStructure.fetchError'))
  69 + }
  70 + },
  71 + getBgColor(car) {
  72 + if (!car.ownerName) {
  73 + return "#1AB394"
  74 + }
  75 + if (car.oweAmount > 0) {
  76 + return "#DC3545"
  77 + }
  78 + return "#1296db"
  79 + },
  80 + toSimplifyAcceptance(car) {
  81 + const date = new Date()
  82 + this.$store.dispatch('app/saveData', {
  83 + key: "JAVA110_IS_BACK",
  84 + value: date.getTime()
  85 + })
  86 + this.$store.dispatch('app/saveData', {
  87 + key: 'simplifyAcceptanceSearch',
  88 + value: {
  89 + searchType: '1',
  90 + searchValue: `${car.floorNum}-${car.unitNum}-${car.roomNum}`,
  91 + searchPlaceholder: this.$t('carStructure.searchPlaceholder')
  92 + }
  93 + })
  94 + this.$router.push('/pages/property/simplifyAcceptance?tab=业务受理')
  95 + }
  96 + }
  97 +}
  98 +</script>
  99 +
  100 +<style lang="scss" scoped>
  101 +.car-structure-container {
  102 + padding: 20px;
  103 +
  104 + .car-list-card {
  105 + height: 100%;
  106 + min-height: calc(100vh - 120px);
  107 + }
  108 +
  109 + .car-card {
  110 + color: #fff;
  111 + border-radius: 5px;
  112 + cursor: pointer;
  113 + min-height: 120px;
  114 + display: flex;
  115 + flex-direction: column;
  116 + justify-content: center;
  117 + transition: all 0.3s;
  118 +
  119 + &:hover {
  120 + transform: scale(1.05);
  121 + box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  122 + }
  123 + }
  124 +
  125 + .margin-top {
  126 + margin-top: 10px;
  127 + }
  128 +
  129 + .margin-bottom {
  130 + margin-bottom: 20px;
  131 + }
  132 +
  133 + .padding {
  134 + padding: 10px;
  135 + }
  136 +
  137 + .text-center {
  138 + text-align: center;
  139 + }
  140 +}
  141 +</style>
0 142 \ No newline at end of file
... ...
src/views/fee/payFeeQrcodeLang.js 0 → 100644
  1 +export const messages = {
  2 + en: {
  3 + payFeeQrcode: {
  4 + search: {
  5 + title: 'Search Condition',
  6 + qrcodeName: 'QR Code Name'
  7 + },
  8 + list: {
  9 + title: 'Payment QR Code List'
  10 + },
  11 + table: {
  12 + qrcodeName: 'Name',
  13 + queryWay: 'Query Method',
  14 + smsValidate: 'Validation',
  15 + customFee: 'Custom Payment',
  16 + feeType: 'Display Fee',
  17 + state: 'Status',
  18 + createStaffName: 'Creator',
  19 + createTime: 'Create Time'
  20 + },
  21 + queryWay: {
  22 + byPhone: 'By Owner Phone',
  23 + byHouse: 'By House',
  24 + byBoth: 'By House or Owner Phone'
  25 + },
  26 + feeType: {
  27 + owner: 'Owner Fee',
  28 + house: 'House Fee'
  29 + },
  30 + operation: {
  31 + viewQrcode: 'QR Code'
  32 + },
  33 + form: {
  34 + qrcodeName: 'Name',
  35 + queryWay: 'Query Method',
  36 + smsValidate: 'Validation',
  37 + customFee: 'Custom Payment',
  38 + preFee: 'Prepayment',
  39 + content: 'Prompt Content'
  40 + },
  41 + placeholder: {
  42 + qrcodeName: 'Please enter name',
  43 + queryWay: 'Please select query method',
  44 + smsValidate: 'Please select validation',
  45 + customFee: 'Please select custom payment',
  46 + preFee: 'Please select prepayment',
  47 + content: 'Please enter prompt content'
  48 + },
  49 + rules: {
  50 + qrcodeName: 'Name cannot be empty',
  51 + qrcodeNameMax: 'Name cannot exceed 128 characters',
  52 + queryWay: 'Please select query method',
  53 + smsValidate: 'Please select validation',
  54 + customFee: 'Please select custom payment',
  55 + preFee: 'Please select prepayment',
  56 + content: 'Prompt content cannot be empty',
  57 + contentMax: 'Prompt content cannot exceed 512 characters',
  58 + pfqId: 'ID cannot be empty'
  59 + },
  60 + add: {
  61 + title: 'Add Payment QR Code'
  62 + },
  63 + edit: {
  64 + title: 'Edit Payment QR Code'
  65 + },
  66 + delete: {
  67 + title: 'Delete Confirmation',
  68 + confirmText: 'Are you sure to delete this payment QR code?'
  69 + },
  70 + view: {
  71 + title: 'Payment QR Code',
  72 + tip: 'Please take a screenshot and post it at the cashier'
  73 + },
  74 + message: {
  75 + addSuccess: 'Added successfully',
  76 + addFailed: 'Failed to add',
  77 + editSuccess: 'Modified successfully',
  78 + editFailed: 'Failed to modify',
  79 + deleteSuccess: 'Deleted successfully',
  80 + deleteFailed: 'Failed to delete',
  81 + fetchError: 'Failed to get data'
  82 + }
  83 + }
  84 + },
  85 + zh: {
  86 + payFeeQrcode: {
  87 + search: {
  88 + title: '查询条件',
  89 + qrcodeName: '二维码名称'
  90 + },
  91 + list: {
  92 + title: '支付二维码列表'
  93 + },
  94 + table: {
  95 + qrcodeName: '名称',
  96 + queryWay: '查询方式',
  97 + smsValidate: '验证',
  98 + customFee: '自定义缴费',
  99 + feeType: '展示费用',
  100 + state: '状态',
  101 + createStaffName: '创建人',
  102 + createTime: '创建时间'
  103 + },
  104 + queryWay: {
  105 + byPhone: '按业主手机号',
  106 + byHouse: '按房屋',
  107 + byBoth: '按房屋或者业主手机号'
  108 + },
  109 + feeType: {
  110 + owner: '业主费用',
  111 + house: '房屋费用'
  112 + },
  113 + operation: {
  114 + viewQrcode: '二维码'
  115 + },
  116 + form: {
  117 + qrcodeName: '名称',
  118 + queryWay: '查询方式',
  119 + smsValidate: '验证',
  120 + customFee: '自定义缴费',
  121 + preFee: '预交费',
  122 + content: '提示内容'
  123 + },
  124 + placeholder: {
  125 + qrcodeName: '请输入名称',
  126 + queryWay: '请选择查询方式',
  127 + smsValidate: '请选择验证',
  128 + customFee: '请选择自定义缴费',
  129 + preFee: '请选择预交费',
  130 + content: '请输入提示内容'
  131 + },
  132 + rules: {
  133 + qrcodeName: '名称不能为空',
  134 + qrcodeNameMax: '名称不能超过128个字符',
  135 + queryWay: '请选择查询方式',
  136 + smsValidate: '请选择验证',
  137 + customFee: '请选择自定义缴费',
  138 + preFee: '请选择预交费',
  139 + content: '提示内容不能为空',
  140 + contentMax: '提示内容不能超过512个字符',
  141 + pfqId: '编号不能为空'
  142 + },
  143 + add: {
  144 + title: '添加支付二维码'
  145 + },
  146 + edit: {
  147 + title: '修改支付二维码'
  148 + },
  149 + delete: {
  150 + title: '删除确认',
  151 + confirmText: '确定删除该支付二维码吗?'
  152 + },
  153 + view: {
  154 + title: '支付二维码',
  155 + tip: '请截图贴在收银台'
  156 + },
  157 + message: {
  158 + addSuccess: '添加成功',
  159 + addFailed: '添加失败',
  160 + editSuccess: '修改成功',
  161 + editFailed: '修改失败',
  162 + deleteSuccess: '删除成功',
  163 + deleteFailed: '删除失败',
  164 + fetchError: '获取数据失败'
  165 + }
  166 + }
  167 + }
  168 +}
0 169 \ No newline at end of file
... ...
src/views/fee/payFeeQrcodeList.vue 0 → 100644
  1 +<template>
  2 + <div class="pay-fee-qrcode-container">
  3 + <!-- Search Condition -->
  4 + <el-card class="search-wrapper">
  5 + <div slot="header" class="flex justify-between">
  6 + <span>{{ $t('payFeeQrcode.search.title') }}</span>
  7 + </div>
  8 + <el-row :gutter="20">
  9 + <el-col :span="6">
  10 + <el-input v-model="searchForm.qrcodeName" :placeholder="$t('payFeeQrcode.search.qrcodeName')" clearable />
  11 + </el-col>
  12 + <el-col :span="6">
  13 + <el-button type="primary" @click="handleSearch">
  14 + <i class="el-icon-search"></i>
  15 + {{ $t('common.search') }}
  16 + </el-button>
  17 + <el-button @click="handleReset">
  18 + <i class="el-icon-refresh"></i>
  19 + {{ $t('common.reset') }}
  20 + </el-button>
  21 + </el-col>
  22 + </el-row>
  23 + </el-card>
  24 +
  25 + <!-- Data List -->
  26 + <el-card class="list-wrapper">
  27 + <div slot="header" class="flex justify-between">
  28 + <span>{{ $t('payFeeQrcode.list.title') }}</span>
  29 + <el-button type="primary" size="small" class="float-right" @click="handleAdd">
  30 + <i class="el-icon-plus"></i>
  31 + {{ $t('common.add') }}
  32 + </el-button>
  33 + </div>
  34 +
  35 + <el-table v-loading="loading" :data="tableData" border style="width: 100%">
  36 + <el-table-column prop="qrcodeName" :label="$t('payFeeQrcode.table.qrcodeName')" align="center" />
  37 + <el-table-column prop="queryWay" :label="$t('payFeeQrcode.table.queryWay')" align="center">
  38 + <template slot-scope="scope">
  39 + <span v-if="scope.row.queryWay === '1001'">
  40 + {{ $t('payFeeQrcode.queryWay.byPhone') }}
  41 + </span>
  42 + <span v-else-if="scope.row.queryWay === '2002'">
  43 + {{ $t('payFeeQrcode.queryWay.byHouse') }}
  44 + </span>
  45 + <span v-else>
  46 + {{ $t('payFeeQrcode.queryWay.byBoth') }}
  47 + </span>
  48 + </template>
  49 + </el-table-column>
  50 + <el-table-column prop="smsValidate" :label="$t('payFeeQrcode.table.smsValidate')" align="center">
  51 + <template slot-scope="scope">
  52 + {{ scope.row.smsValidate === 'ON' ? $t('common.yes') : $t('common.no') }}
  53 + </template>
  54 + </el-table-column>
  55 + <el-table-column prop="customFee" :label="$t('payFeeQrcode.table.customFee')" align="center">
  56 + <template slot-scope="scope">
  57 + {{ scope.row.customFee === 'ON' ? $t('common.yes') : $t('common.no') }}
  58 + </template>
  59 + </el-table-column>
  60 + <el-table-column prop="feeType" :label="$t('payFeeQrcode.table.feeType')" align="center">
  61 + <template slot-scope="scope">
  62 + {{ scope.row.feeType === 'OWNER' ? $t('payFeeQrcode.feeType.owner') : $t('payFeeQrcode.feeType.house') }}
  63 + </template>
  64 + </el-table-column>
  65 + <el-table-column prop="state" :label="$t('payFeeQrcode.table.state')" align="center">
  66 + <template slot-scope="scope">
  67 + <el-tag :type="scope.row.state === 'ON' ? 'success' : 'danger'">
  68 + {{ scope.row.state === 'ON' ? $t('common.enabled') : $t('common.disabled') }}
  69 + </el-tag>
  70 + </template>
  71 + </el-table-column>
  72 + <el-table-column prop="createStaffName" :label="$t('payFeeQrcode.table.createStaffName')" align="center" />
  73 + <el-table-column prop="createTime" :label="$t('payFeeQrcode.table.createTime')" align="center" />
  74 + <el-table-column :label="$t('common.operation')" align="center" width="250">
  75 + <template slot-scope="scope">
  76 + <el-button size="mini" @click="handleView(scope.row)">
  77 + {{ $t('payFeeQrcode.operation.viewQrcode') }}
  78 + </el-button>
  79 + <el-button size="mini" type="primary" @click="handleEdit(scope.row)">
  80 + {{ $t('common.edit') }}
  81 + </el-button>
  82 + <el-button size="mini" type="danger" @click="handleDelete(scope.row)">
  83 + {{ $t('common.delete') }}
  84 + </el-button>
  85 + </template>
  86 + </el-table-column>
  87 + </el-table>
  88 +
  89 + <el-pagination :current-page="pagination.current" :page-sizes="[10, 20, 30, 50]" :page-size="pagination.size"
  90 + :total="pagination.total" layout="total, sizes, prev, pager, next, jumper" @size-change="handleSizeChange"
  91 + @current-change="handleCurrentChange" />
  92 + </el-card>
  93 +
  94 + <!-- Components -->
  95 + <add-pay-fee-qrcode ref="addDialog" @success="handleSuccess" />
  96 + <edit-pay-fee-qrcode ref="editDialog" @success="handleSuccess" />
  97 + <delete-pay-fee-qrcode ref="deleteDialog" @success="handleSuccess" />
  98 + <view-pay-fee-qrcode ref="viewDialog" />
  99 + </div>
  100 +</template>
  101 +
  102 +<script>
  103 +import { listPayFeeQrcode } from '@/api/fee/payFeeQrcodeApi'
  104 +import AddPayFeeQrcode from '@/components/fee/addPayFeeQrcode'
  105 +import EditPayFeeQrcode from '@/components/fee/editPayFeeQrcode'
  106 +import DeletePayFeeQrcode from '@/components/fee/deletePayFeeQrcode'
  107 +import ViewPayFeeQrcode from '@/components/fee/viewPayFeeQrcode'
  108 +import { getCommunityId } from '@/api/community/communityApi'
  109 +
  110 +export default {
  111 + name: 'PayFeeQrcodeList',
  112 + components: {
  113 + AddPayFeeQrcode,
  114 + EditPayFeeQrcode,
  115 + DeletePayFeeQrcode,
  116 + ViewPayFeeQrcode
  117 + },
  118 + data() {
  119 + return {
  120 + loading: false,
  121 + searchForm: {
  122 + qrcodeName: '',
  123 + customFee: '',
  124 + preFee: ''
  125 + },
  126 + tableData: [],
  127 + pagination: {
  128 + current: 1,
  129 + size: 10,
  130 + total: 0
  131 + },
  132 + communityId: ''
  133 + }
  134 + },
  135 + created() {
  136 + this.communityId = getCommunityId()
  137 + this.getList()
  138 + },
  139 + methods: {
  140 + async getList() {
  141 + try {
  142 + this.loading = true
  143 + const params = {
  144 + page: this.pagination.current,
  145 + row: this.pagination.size,
  146 + qrcodeName: this.searchForm.qrcodeName,
  147 + communityId: this.communityId
  148 + }
  149 + const { data, total } = await listPayFeeQrcode(params)
  150 + this.tableData = data
  151 + this.pagination.total = total
  152 + } catch (error) {
  153 + this.$message.error(this.$t('payFeeQrcode.fetchError'))
  154 + } finally {
  155 + this.loading = false
  156 + }
  157 + },
  158 + handleSearch() {
  159 + this.pagination.current = 1
  160 + this.getList()
  161 + },
  162 + handleReset() {
  163 + this.searchForm = {
  164 + qrcodeName: '',
  165 + customFee: '',
  166 + preFee: ''
  167 + }
  168 + this.handleSearch()
  169 + },
  170 + handleAdd() {
  171 + this.$refs.addDialog.open()
  172 + },
  173 + handleEdit(row) {
  174 + this.$refs.editDialog.open(row)
  175 + },
  176 + handleDelete(row) {
  177 + this.$refs.deleteDialog.open(row)
  178 + },
  179 + handleView(row) {
  180 + this.$refs.viewDialog.open(row)
  181 + },
  182 + handleSuccess() {
  183 + this.getList()
  184 + },
  185 + handleSizeChange(val) {
  186 + this.pagination.size = val
  187 + this.getList()
  188 + },
  189 + handleCurrentChange(val) {
  190 + this.pagination.current = val
  191 + this.getList()
  192 + }
  193 + }
  194 +}
  195 +</script>
  196 +
  197 +<style lang="scss" scoped>
  198 +.pay-fee-qrcode-container {
  199 + padding: 20px;
  200 +
  201 + .search-wrapper {
  202 + margin-bottom: 20px;
  203 +
  204 + .el-input {
  205 + width: 100%;
  206 + }
  207 + }
  208 +
  209 + .list-wrapper {
  210 + .float-right {
  211 + float: right;
  212 + }
  213 + }
  214 +
  215 + .el-pagination {
  216 + margin-top: 20px;
  217 + text-align: right;
  218 + }
  219 +}
  220 +</style>
0 221 \ No newline at end of file
... ...
src/views/machine/videoControlLang.js 0 → 100644
  1 +export const messages = {
  2 + en: {
  3 + videoControl: {
  4 + title: 'Video Control'
  5 + },
  6 + cameraControlVideo: {
  7 + camera: 'Camera',
  8 + fourWay: 'Four Way',
  9 + sixWay: 'Six Way',
  10 + getVideoUrlError: 'Failed to get video URL'
  11 + },
  12 + selectVideoMachine: {
  13 + title: 'Select Video Machine',
  14 + monitorArea: 'Monitor Area',
  15 + camera: 'Camera',
  16 + loadAreasError: 'Failed to load monitor areas',
  17 + loadMachinesError: 'Failed to load monitor machines'
  18 + }
  19 + },
  20 + zh: {
  21 + videoControl: {
  22 + title: '视频控制'
  23 + },
  24 + cameraControlVideo: {
  25 + camera: '摄像头',
  26 + fourWay: '四路',
  27 + sixWay: '六路',
  28 + getVideoUrlError: '获取视频地址失败'
  29 + },
  30 + selectVideoMachine: {
  31 + title: '选择视频设备',
  32 + monitorArea: '监控区域',
  33 + camera: '摄像头',
  34 + loadAreasError: '加载监控区域失败',
  35 + loadMachinesError: '加载监控设备失败'
  36 + }
  37 + }
  38 +}
0 39 \ No newline at end of file
... ...
src/views/machine/videoControlList.vue 0 → 100644
  1 +<template>
  2 + <div class="video-control-container">
  3 + <el-card class="box-card">
  4 +
  5 + <el-row :gutter="20">
  6 + <el-col :span="24">
  7 + <camera-control-video ref="cameraControl" />
  8 + </el-col>
  9 + </el-row>
  10 + </el-card>
  11 + </div>
  12 +</template>
  13 +
  14 +<script>
  15 +import CameraControlVideo from '@/components/machine/cameraControlVideo.vue'
  16 +
  17 +export default {
  18 + name: 'VideoControlList',
  19 + components: {
  20 + CameraControlVideo
  21 + },
  22 + data() {
  23 + return {
  24 + communityId: ''
  25 + }
  26 + },
  27 + created() {
  28 + this.getCommunityId()
  29 + },
  30 + methods: {
  31 + async getCommunityId() {
  32 + try {
  33 + const { getCommunityId } = await import('@/api/community/communityApi')
  34 + this.communityId = getCommunityId()
  35 + } catch (error) {
  36 + console.error('Failed to get communityId:', error)
  37 + }
  38 + }
  39 + }
  40 +}
  41 +</script>
  42 +
  43 +<style lang="scss" scoped>
  44 +.video-control-container {
  45 + padding: 20px;
  46 +
  47 + .box-card {
  48 + margin-bottom: 20px;
  49 + }
  50 +
  51 + .clearfix {
  52 + font-size: 18px;
  53 + font-weight: bold;
  54 + }
  55 +}
  56 +</style>
0 57 \ No newline at end of file
... ...
src/views/room/listPropertyRightRegistrationDetailLang.js 0 → 100644
  1 +export const messages = {
  2 + en: {
  3 + propertyRightDetail: {
  4 + title: 'Property Registration Details',
  5 + table: {
  6 + id: 'ID',
  7 + materialType: 'Material Type',
  8 + isPayment: 'Payment Status',
  9 + image: 'Images',
  10 + createTime: 'Create Time',
  11 + operation: 'Operations'
  12 + },
  13 + edit: {
  14 + title: 'Edit Property Registration',
  15 + materialType: 'Material Type',
  16 + materialTypePlaceholder: 'Required, please enter material type',
  17 + materialTypeRequired: 'Material type is required',
  18 + idCardPhoto: 'ID Card Photos',
  19 + idCardPhotoTip: 'Please upload front and back photos of ID card',
  20 + houseContract: 'House Purchase Contract',
  21 + houseContractTip: 'Please upload up to 10 house purchase contract photos',
  22 + repairFund: 'Repair Fund Payment',
  23 + repairFundPlaceholder: 'Please select repair fund payment status',
  24 + repairFundPhoto: 'Repair Fund Proof',
  25 + deedTax: 'Deed Tax Payment',
  26 + deedTaxPlaceholder: 'Please select deed tax payment status',
  27 + deedTaxPhoto: 'Deed Tax Proof'
  28 + },
  29 + fetchError: 'Failed to fetch property registration details'
  30 + }
  31 + },
  32 + zh: {
  33 + propertyRightDetail: {
  34 + title: '产权登记详情',
  35 + table: {
  36 + id: 'ID',
  37 + materialType: '材料类型',
  38 + isPayment: '是否缴费',
  39 + image: '图片',
  40 + createTime: '创建时间',
  41 + operation: '操作'
  42 + },
  43 + edit: {
  44 + title: '修改产权登记',
  45 + materialType: '材料类型',
  46 + materialTypePlaceholder: '必填,请填写材料类型',
  47 + materialTypeRequired: '材料类型不能为空',
  48 + idCardPhoto: '身份证照片',
  49 + idCardPhotoTip: '请上传正反两张身份证照片',
  50 + houseContract: '购房合同',
  51 + houseContractTip: '请上传不超过十张购房合同照片',
  52 + repairFund: '维修基金缴纳',
  53 + repairFundPlaceholder: '请选择维修基金缴纳状态',
  54 + repairFundPhoto: '维修基金证明',
  55 + deedTax: '契税缴纳',
  56 + deedTaxPlaceholder: '请选择契税缴纳状态',
  57 + deedTaxPhoto: '契税证明'
  58 + },
  59 + fetchError: '获取产权登记详情失败'
  60 + }
  61 + }
  62 +}
0 63 \ No newline at end of file
... ...
src/views/room/listPropertyRightRegistrationDetailList.vue 0 → 100644
  1 +<template>
  2 + <div class="property-right-detail-container">
  3 + <el-card class="box-card">
  4 + <div slot="header" class="clearfix">
  5 + <div class="card-header">
  6 + <div>
  7 + <span>{{ listPropertyRightRegistrationDetailInfo.conditions.floorNum }}-{{
  8 + listPropertyRightRegistrationDetailInfo.conditions.unitNum }}-{{
  9 + listPropertyRightRegistrationDetailInfo.conditions.roomNum }}</span>
  10 + <span>{{ $t('propertyRightDetail.title') }}</span>
  11 + </div>
  12 + <div class="card-tools">
  13 + <el-button type="primary" size="small" @click="_goBack">
  14 + <i class="el-icon-close"></i>
  15 + <span>{{ $t('common.back') }}</span>
  16 + </el-button>
  17 + </div>
  18 + </div>
  19 + </div>
  20 +
  21 + <el-table :data="listPropertyRightRegistrationDetailInfo.propertyRightRegistrationDetails" border
  22 + style="width: 100%" v-loading="loading">
  23 + <el-table-column prop="prrdId" :label="$t('propertyRightDetail.table.id')" align="center" />
  24 + <el-table-column prop="securitiesName" :label="$t('propertyRightDetail.table.materialType')" align="center" />
  25 + <el-table-column :label="$t('propertyRightDetail.table.isPayment')" align="center">
  26 + <template slot-scope="scope">
  27 + {{ scope.row.isTrue === '-1' ? '--' : scope.row.isTrue === 'true' ? $t('common.yes') : $t('common.no') }}
  28 + </template>
  29 + </el-table-column>
  30 + <el-table-column :label="$t('propertyRightDetail.table.image')" align="center">
  31 + <template slot-scope="scope">
  32 + <div v-if="scope.row.securities === '001'">
  33 + <div v-for="(item, index) in scope.row.idCardUrlShow" :key="index" class="image-container"
  34 + @click="showImg(item)">
  35 + <el-image :src="item" :preview-src-list="scope.row.idCardUrlShow" fit="cover"
  36 + style="width: 50px; height: 50px">
  37 + <div slot="error" class="image-slot">
  38 + <img src="/img/noPhoto.jpg" style="width: 50px; height: 50px">
  39 + </div>
  40 + </el-image>
  41 + <img src="/img/icon-bigimg.png" class="preview-icon">
  42 + </div>
  43 + </div>
  44 + <div v-else-if="scope.row.securities === '002'">
  45 + <div v-for="(item, index) in scope.row.housePurchaseUrlShow" :key="index" class="image-container"
  46 + @click="showImg(item)">
  47 + <el-image :src="item" :preview-src-list="scope.row.housePurchaseUrlShow" fit="cover"
  48 + style="width: 50px; height: 50px">
  49 + <div slot="error" class="image-slot">
  50 + <img src="/img/noPhoto.jpg" style="width: 50px; height: 50px">
  51 + </div>
  52 + </el-image>
  53 + <img src="/img/icon-bigimg.png" class="preview-icon">
  54 + </div>
  55 + </div>
  56 + <div v-else-if="scope.row.securities === '003'">
  57 + <div v-for="(item, index) in scope.row.repairUrlShow" :key="index" class="image-container"
  58 + @click="showImg(item)">
  59 + <el-image :src="item" :preview-src-list="scope.row.repairUrlShow" fit="cover"
  60 + style="width: 50px; height: 50px">
  61 + <div slot="error" class="image-slot">
  62 + <img src="/img/noPhoto.jpg" style="width: 50px; height: 50px">
  63 + </div>
  64 + </el-image>
  65 + <img src="/img/icon-bigimg.png" class="preview-icon">
  66 + </div>
  67 + </div>
  68 + <div v-else-if="scope.row.securities === '004'">
  69 + <div v-for="(item, index) in scope.row.deedTaxUrlShow" :key="index" class="image-container"
  70 + @click="showImg(item)">
  71 + <el-image :src="item" :preview-src-list="scope.row.deedTaxUrlShow" fit="cover"
  72 + style="width: 50px; height: 50px">
  73 + <div slot="error" class="image-slot">
  74 + <img src="/img/noPhoto.jpg" style="width: 50px; height: 50px">
  75 + </div>
  76 + </el-image>
  77 + <img src="/img/icon-bigimg.png" class="preview-icon">
  78 + </div>
  79 + </div>
  80 + </template>
  81 + </el-table-column>
  82 + <el-table-column prop="createTime" :label="$t('propertyRightDetail.table.createTime')" align="center" />
  83 + <el-table-column :label="$t('common.operation')" align="center" width="150">
  84 + <template slot-scope="scope">
  85 + <el-button size="mini" type="primary" @click="_openEditPropertyRightRegistrationDetailModel(scope.row)">
  86 + {{ $t('common.edit') }}
  87 + </el-button>
  88 + </template>
  89 + </el-table-column>
  90 + </el-table>
  91 +
  92 + <el-pagination :current-page.sync="page.current" :page-sizes="[10, 20, 30, 50]" :page-size="page.size"
  93 + :total="page.total" layout="total, sizes, prev, pager, next, jumper" @size-change="handleSizeChange"
  94 + @current-change="handleCurrentChange" />
  95 + </el-card>
  96 +
  97 + <edit-property-right-registration-detail ref="editPropertyRightRegistrationDetail" @success="handleSuccess" />
  98 + <view-image ref="viewImage" />
  99 + </div>
  100 +</template>
  101 +
  102 +<script>
  103 +import { listPropertyRightRegistrationDetail } from '@/api/room/listPropertyRightRegistrationDetailApi'
  104 +import { getCommunityId } from '@/api/community/communityApi'
  105 +import EditPropertyRightRegistrationDetail from '@/components/room/editPropertyRightRegistrationDetail'
  106 +import ViewImage from '@/components/system/viewImage'
  107 +
  108 +export default {
  109 + name: 'ListPropertyRightRegistrationDetail',
  110 + components: {
  111 + EditPropertyRightRegistrationDetail,
  112 + ViewImage
  113 + },
  114 + data() {
  115 + return {
  116 + loading: false,
  117 + listPropertyRightRegistrationDetailInfo: {
  118 + propertyRightRegistrationDetails: [],
  119 + conditions: {
  120 + prrId: '',
  121 + securities: '',
  122 + floorNum: '',
  123 + unitNum: '',
  124 + roomNum: '',
  125 + isTrue: '',
  126 + communityId: ''
  127 + }
  128 + },
  129 + page: {
  130 + current: 1,
  131 + size: 10,
  132 + total: 0
  133 + }
  134 + }
  135 + },
  136 + created() {
  137 + this.listPropertyRightRegistrationDetailInfo.conditions.communityId = getCommunityId()
  138 + this.listPropertyRightRegistrationDetailInfo.conditions.prrId = this.$route.query.prrId
  139 + this.listPropertyRightRegistrationDetailInfo.conditions.floorNum = this.$route.query.floorNum
  140 + this.listPropertyRightRegistrationDetailInfo.conditions.unitNum = this.$route.query.unitNum
  141 + this.listPropertyRightRegistrationDetailInfo.conditions.roomNum = this.$route.query.roomNum
  142 + this.getList()
  143 + },
  144 + methods: {
  145 + async getList() {
  146 + try {
  147 + this.loading = true
  148 + const params = {
  149 + page: this.page.current,
  150 + row: this.page.size,
  151 + ...this.listPropertyRightRegistrationDetailInfo.conditions
  152 + }
  153 + const { data, total } = await listPropertyRightRegistrationDetail(params)
  154 + this.listPropertyRightRegistrationDetailInfo.propertyRightRegistrationDetails = data.map(item => {
  155 + if (item.securities === '001' && item.idCardUrl) {
  156 + item.idCardUrl = item.idCardUrl.split(',')
  157 + item.idCardUrlShow = item.idCardUrl.map(url => `/callComponent/download/getFile/file?fileId=${url}&communityId=-1&time=${new Date()}`)
  158 + }
  159 + if (item.securities === '002' && item.housePurchaseUrl) {
  160 + item.housePurchaseUrl = item.housePurchaseUrl.split(',')
  161 + item.housePurchaseUrlShow = item.housePurchaseUrl.map(url => `/callComponent/download/getFile/file?fileId=${url}&communityId=-1&time=${new Date()}`)
  162 + }
  163 + if (item.securities === '003' && item.repairUrl) {
  164 + item.repairUrl = item.repairUrl.split(',')
  165 + item.repairUrlShow = item.repairUrl.map(url => `/callComponent/download/getFile/file?fileId=${url}&communityId=-1&time=${new Date()}`)
  166 + }
  167 + if (item.securities === '004' && item.deedTaxUrl) {
  168 + item.deedTaxUrl = item.deedTaxUrl.split(',')
  169 + item.deedTaxUrlShow = item.deedTaxUrl.map(url => `/callComponent/download/getFile/file?fileId=${url}&communityId=-1&time=${new Date()}`)
  170 + }
  171 + return item
  172 + })
  173 + this.page.total = total
  174 + } catch (error) {
  175 + this.$message.error(this.$t('propertyRightDetail.fetchError'))
  176 + } finally {
  177 + this.loading = false
  178 + }
  179 + },
  180 + handleSizeChange(val) {
  181 + this.page.size = val
  182 + this.getList()
  183 + },
  184 + handleCurrentChange(val) {
  185 + this.page.current = val
  186 + this.getList()
  187 + },
  188 + _openEditPropertyRightRegistrationDetailModel(row) {
  189 + this.$refs.editPropertyRightRegistrationDetail.open(row)
  190 + },
  191 + showImg(url) {
  192 + this.$refs.viewImage.open(url)
  193 + },
  194 + _goBack() {
  195 + this.$router.go(-1)
  196 + },
  197 + handleSuccess() {
  198 + this.getList()
  199 + }
  200 + }
  201 +}
  202 +</script>
  203 +
  204 +<style lang="scss" scoped>
  205 +.property-right-detail-container {
  206 + padding: 20px;
  207 +
  208 + .box-card {
  209 + margin-bottom: 20px;
  210 +
  211 + .card-header {
  212 + display: flex;
  213 + align-items: center;
  214 + justify-content: space-between;
  215 +
  216 + span {
  217 + font-size: 18px;
  218 + font-weight: bold;
  219 + }
  220 + }
  221 + }
  222 +
  223 + .image-container {
  224 + position: relative;
  225 + display: inline-block;
  226 + margin-right: 10px;
  227 +
  228 + .preview-icon {
  229 + position: absolute;
  230 + right: 0;
  231 + bottom: 0;
  232 + width: 20px;
  233 + height: 20px;
  234 + }
  235 + }
  236 +
  237 + .el-pagination {
  238 + margin-top: 20px;
  239 + text-align: right;
  240 + }
  241 +}
  242 +</style>
0 243 \ No newline at end of file
... ...
src/views/room/propertyRightRegistrationManageLang.js 0 → 100644
  1 +export const messages = {
  2 + zh: {
  3 + propertyRightRegistration: {
  4 + search: {
  5 + title: '查询条件',
  6 + roomId: '房屋ID',
  7 + allNum: '房屋编号(楼栋-单元-房屋)',
  8 + name: '姓名',
  9 + link: '联系方式',
  10 + idCard: '身份证号',
  11 + address: '地址',
  12 + state: '审核状态',
  13 + floor: '楼栋',
  14 + unit: '单元'
  15 + },
  16 + list: {
  17 + title: '房屋产权'
  18 + },
  19 + table: {
  20 + prrId: '房屋产权ID',
  21 + roomId: '房屋ID',
  22 + roomNum: '房屋编号',
  23 + name: '姓名',
  24 + link: '联系方式',
  25 + idCard: '身份证号',
  26 + address: '地址',
  27 + state: '状态'
  28 + },
  29 + add: {
  30 + title: '添加产权登记',
  31 + floor: '楼栋',
  32 + floorRequired: '请选择楼栋',
  33 + floorPlaceholder: '请选择楼栋',
  34 + floorUnit: '号楼',
  35 + unit: '单元',
  36 + unitRequired: '请选择单元',
  37 + unitPlaceholder: '请选择单元',
  38 + unitUnit: '单元',
  39 + room: '房屋',
  40 + roomRequired: '请选择房屋',
  41 + roomPlaceholder: '请选择房屋',
  42 + name: '姓名',
  43 + nameRequired: '请输入姓名',
  44 + namePlaceholder: '请输入姓名',
  45 + nameLength: '姓名长度在2到64个字符之间',
  46 + link: '联系方式',
  47 + linkRequired: '请输入联系方式',
  48 + linkPlaceholder: '请输入联系方式',
  49 + idCard: '身份证号',
  50 + idCardRequired: '请输入身份证号',
  51 + idCardPlaceholder: '请输入身份证号',
  52 + idCardFormatError: '身份证格式不正确',
  53 + address: '地址',
  54 + addressRequired: '请输入地址',
  55 + addressPlaceholder: '请输入地址',
  56 + addressMax: '地址长度不能超过255个字符',
  57 + idCardPhotos: '身份证照片',
  58 + idCardPhotosRequired: '请上传身份证照片',
  59 + idCardPhotosTip: '*请上传正反两张身份证照片*',
  60 + housePurchasePhotos: '购房合同照片',
  61 + housePurchasePhotosRequired: '请上传购房合同照片',
  62 + housePurchasePhotosTip: '*请上传不超过十张购房合同图片*',
  63 + isTrue: '维修基金是否缴纳',
  64 + isTrueRequired: '请选择维修基金是否缴纳',
  65 + isTruePlaceholder: '请选择维修基金是否缴纳',
  66 + repairPhotos: '维修基金照片',
  67 + repairPhotosRequired: '请上传维修基金照片',
  68 + flag: '契税是否缴纳',
  69 + flagRequired: '请选择契税是否缴纳',
  70 + flagPlaceholder: '请选择契税是否缴纳',
  71 + deedTaxPhotos: '契税证明照片',
  72 + deedTaxPhotosRequired: '请上传契税证明照片',
  73 + success: '添加成功',
  74 + error: '添加失败'
  75 + },
  76 + edit: {
  77 + title: '修改产权登记',
  78 + floor: '楼栋',
  79 + floorRequired: '请选择楼栋',
  80 + floorPlaceholder: '请选择楼栋',
  81 + floorUnit: '号楼',
  82 + unit: '单元',
  83 + unitRequired: '请选择单元',
  84 + unitPlaceholder: '请选择单元',
  85 + unitUnit: '单元',
  86 + room: '房屋',
  87 + roomRequired: '请选择房屋',
  88 + roomPlaceholder: '请选择房屋',
  89 + name: '姓名',
  90 + nameRequired: '请输入姓名',
  91 + namePlaceholder: '请输入姓名',
  92 + nameLength: '姓名长度在2到64个字符之间',
  93 + link: '联系方式',
  94 + linkRequired: '请输入联系方式',
  95 + linkPlaceholder: '请输入联系方式',
  96 + idCard: '身份证号',
  97 + idCardRequired: '请输入身份证号',
  98 + idCardPlaceholder: '请输入身份证号',
  99 + idCardFormatError: '身份证格式不正确',
  100 + address: '地址',
  101 + addressRequired: '请输入地址',
  102 + addressPlaceholder: '请输入地址',
  103 + addressMax: '地址长度不能超过255个字符',
  104 + success: '修改成功',
  105 + error: '修改失败'
  106 + },
  107 + examine: {
  108 + title: '产权登记审核',
  109 + room: '房屋',
  110 + roomPlaceholder: '房屋信息',
  111 + state: '状态',
  112 + stateRequired: '请选择状态',
  113 + statePlaceholder: '请选择状态',
  114 + remark: '审核意见',
  115 + remarkPlaceholder: '请输入审核意见',
  116 + success: '审核成功',
  117 + error: '审核失败',
  118 + fetchStateError: '获取审核状态失败'
  119 + },
  120 + delete: {
  121 + title: '删除确认',
  122 + confirm: '确定删除该房屋产权登记信息吗?',
  123 + tip: '删除后将无法恢复,请谨慎操作!',
  124 + success: '删除成功',
  125 + error: '删除失败'
  126 + },
  127 + fetchError: '获取数据失败'
  128 + },
  129 + },
  130 + en: {
  131 + propertyRightRegistration: {
  132 + search: {
  133 + title: 'Search Conditions',
  134 + roomId: 'Room ID',
  135 + allNum: 'Room Number(Building-Unit-Room)',
  136 + name: 'Name',
  137 + link: 'Contact',
  138 + idCard: 'ID Card',
  139 + address: 'Address',
  140 + state: 'Audit Status',
  141 + floor: 'Building',
  142 + unit: 'Unit'
  143 + },
  144 + list: {
  145 + title: 'Property Right'
  146 + },
  147 + table: {
  148 + prrId: 'Property Right ID',
  149 + roomId: 'Room ID',
  150 + roomNum: 'Room Number',
  151 + name: 'Name',
  152 + link: 'Contact',
  153 + idCard: 'ID Card',
  154 + address: 'Address',
  155 + state: 'Status'
  156 + },
  157 + add: {
  158 + title: 'Add Property Registration',
  159 + floor: 'Building',
  160 + floorRequired: 'Please select building',
  161 + floorPlaceholder: 'Please select building',
  162 + floorUnit: 'Building',
  163 + unit: 'Unit',
  164 + unitRequired: 'Please select unit',
  165 + unitPlaceholder: 'Please select unit',
  166 + unitUnit: 'Unit',
  167 + room: 'Room',
  168 + roomRequired: 'Please select room',
  169 + roomPlaceholder: 'Please select room',
  170 + name: 'Name',
  171 + nameRequired: 'Please enter name',
  172 + namePlaceholder: 'Please enter name',
  173 + nameLength: 'Name length should be between 2 and 64 characters',
  174 + link: 'Contact',
  175 + linkRequired: 'Please enter contact',
  176 + linkPlaceholder: 'Please enter contact',
  177 + idCard: 'ID Card',
  178 + idCardRequired: 'Please enter ID card',
  179 + idCardPlaceholder: 'Please enter ID card',
  180 + idCardFormatError: 'Invalid ID card format',
  181 + address: 'Address',
  182 + addressRequired: 'Please enter address',
  183 + addressPlaceholder: 'Please enter address',
  184 + addressMax: 'Address length cannot exceed 255 characters',
  185 + idCardPhotos: 'ID Card Photos',
  186 + idCardPhotosRequired: 'Please upload ID card photos',
  187 + idCardPhotosTip: '*Please upload front and back photos of ID card*',
  188 + housePurchasePhotos: 'House Purchase Contract Photos',
  189 + housePurchasePhotosRequired: 'Please upload house purchase contract photos',
  190 + housePurchasePhotosTip: '*Please upload up to ten house purchase contract photos*',
  191 + isTrue: 'Maintenance Fund Paid',
  192 + isTrueRequired: 'Please select whether maintenance fund is paid',
  193 + isTruePlaceholder: 'Please select whether maintenance fund is paid',
  194 + repairPhotos: 'Maintenance Fund Photos',
  195 + repairPhotosRequired: 'Please upload maintenance fund photos',
  196 + flag: 'Deed Tax Paid',
  197 + flagRequired: 'Please select whether deed tax is paid',
  198 + flagPlaceholder: 'Please select whether deed tax is paid',
  199 + deedTaxPhotos: 'Deed Tax Certificate Photos',
  200 + deedTaxPhotosRequired: 'Please upload deed tax certificate photos',
  201 + success: 'Added successfully',
  202 + error: 'Failed to add'
  203 + },
  204 + edit: {
  205 + title: 'Edit Property Registration',
  206 + floor: 'Building',
  207 + floorRequired: 'Please select building',
  208 + floorPlaceholder: 'Please select building',
  209 + floorUnit: 'Building',
  210 + unit: 'Unit',
  211 + unitRequired: 'Please select unit',
  212 + unitPlaceholder: 'Please select unit',
  213 + unitUnit: 'Unit',
  214 + room: 'Room',
  215 + roomRequired: 'Please select room',
  216 + roomPlaceholder: 'Please select room',
  217 + name: 'Name',
  218 + nameRequired: 'Please enter name',
  219 + namePlaceholder: 'Please enter name',
  220 + nameLength: 'Name length should be between 2 and 64 characters',
  221 + link: 'Contact',
  222 + linkRequired: 'Please enter contact',
  223 + linkPlaceholder: 'Please enter contact',
  224 + idCard: 'ID Card',
  225 + idCardRequired: 'Please enter ID card',
  226 + idCardPlaceholder: 'Please enter ID card',
  227 + idCardFormatError: 'Invalid ID card format',
  228 + address: 'Address',
  229 + addressRequired: 'Please enter address',
  230 + addressPlaceholder: 'Please enter address',
  231 + addressMax: 'Address length cannot exceed 255 characters',
  232 + success: 'Updated successfully',
  233 + error: 'Failed to update'
  234 + },
  235 + examine: {
  236 + title: 'Property Registration Audit',
  237 + room: 'Room',
  238 + roomPlaceholder: 'Room information',
  239 + state: 'Status',
  240 + stateRequired: 'Please select status',
  241 + statePlaceholder: 'Please select status',
  242 + remark: 'Audit Opinion',
  243 + remarkPlaceholder: 'Please enter audit opinion',
  244 + success: 'Audit successful',
  245 + error: 'Audit failed',
  246 + fetchStateError: 'Failed to get audit status'
  247 + },
  248 + delete: {
  249 + title: 'Delete Confirmation',
  250 + confirm: 'Are you sure to delete this property registration?',
  251 + tip: 'The data cannot be recovered after deletion, please operate carefully!',
  252 + success: 'Deleted successfully',
  253 + error: 'Failed to delete'
  254 + },
  255 + fetchError: 'Failed to fetch data'
  256 + }
  257 + }
  258 +
  259 +}
0 260 \ No newline at end of file
... ...
src/views/room/propertyRightRegistrationManageList.vue 0 → 100644
  1 +<template>
  2 + <div class="property-right-registration-container">
  3 + <!-- 查询条件 -->
  4 + <el-card class="search-wrapper">
  5 + <div slot="header" class="flex justify-between">
  6 + <span>{{ $t('propertyRightRegistration.search.title') }}</span>
  7 + <el-button type="text" style="float: right; padding: 3px 0" @click="toggleMoreCondition">
  8 + {{ showMoreCondition ? $t('common.hide') : $t('common.more') }}
  9 + </el-button>
  10 + </div>
  11 + <el-row :gutter="20">
  12 + <el-col :span="6">
  13 + <el-input v-model="searchForm.roomId" :placeholder="$t('propertyRightRegistration.search.roomId')"
  14 + clearable />
  15 + </el-col>
  16 + <el-col :span="8">
  17 + <el-input v-model="searchForm.allNum" :placeholder="$t('propertyRightRegistration.search.allNum')"
  18 + clearable />
  19 + </el-col>
  20 + <el-col :span="6">
  21 + <el-input v-model="searchForm.name" :placeholder="$t('propertyRightRegistration.search.name')" clearable />
  22 + </el-col>
  23 + <el-col :span="4">
  24 + <el-button type="primary" @click="handleSearch">
  25 + {{ $t('common.search') }}
  26 + </el-button>
  27 + <el-button @click="handleReset">
  28 + {{ $t('common.reset') }}
  29 + </el-button>
  30 + </el-col>
  31 + </el-row>
  32 +
  33 + <!-- 更多查询条件 -->
  34 + <div v-show="showMoreCondition">
  35 + <el-row :gutter="20" class="mt-20">
  36 + <el-col :span="6">
  37 + <el-input v-model="searchForm.link" :placeholder="$t('propertyRightRegistration.search.link')" clearable />
  38 + </el-col>
  39 + <el-col :span="8">
  40 + <el-input v-model="searchForm.idCard" :placeholder="$t('propertyRightRegistration.search.idCard')"
  41 + clearable />
  42 + </el-col>
  43 + <el-col :span="6">
  44 + <el-input v-model="searchForm.address" :placeholder="$t('propertyRightRegistration.search.address')"
  45 + clearable />
  46 + </el-col>
  47 + </el-row>
  48 +
  49 + <el-row :gutter="20" class="mt-20">
  50 + <el-col :span="6">
  51 + <el-select v-model="searchForm.state" :placeholder="$t('propertyRightRegistration.search.state')"
  52 + style="width:100%">
  53 + <el-option v-for="item in states" :key="item.statusCd" :label="item.name" :value="item.statusCd" />
  54 + </el-select>
  55 + </el-col>
  56 + <el-col :span="8">
  57 + <el-select v-model="searchForm.floorId" :placeholder="$t('propertyRightRegistration.search.floor')"
  58 + style="width:100%" @change="handleFloorChange">
  59 + <el-option v-for="item in floors" :key="item.floorId" :label="item.floorName" :value="item.floorId" />
  60 + </el-select>
  61 + </el-col>
  62 + <el-col :span="6">
  63 + <el-select v-model="searchForm.unitId" :placeholder="$t('propertyRightRegistration.search.unit')"
  64 + style="width:100%">
  65 + <el-option v-for="item in units" :key="item.unitId"
  66 + :label="`${item.unitNum}${$t('propertyRightRegistration.unit')}`" :value="item.unitId" />
  67 + </el-select>
  68 + </el-col>
  69 + </el-row>
  70 + </div>
  71 + </el-card>
  72 +
  73 + <!-- 数据列表 -->
  74 + <el-card class="list-wrapper">
  75 + <div slot="header" class="flex justify-between">
  76 + <span>{{ $t('propertyRightRegistration.list.title') }}</span>
  77 + <el-button type="primary" size="small" style="float: right" @click="handleAdd">
  78 + <i class="el-icon-plus" />
  79 + {{ $t('common.add') }}
  80 + </el-button>
  81 + </div>
  82 +
  83 + <el-table v-loading="loading" :data="tableData" border style="width: 100%">
  84 + <el-table-column prop="prrId" :label="$t('propertyRightRegistration.table.prrId')" align="center" />
  85 + <el-table-column prop="roomId" :label="$t('propertyRightRegistration.table.roomId')" align="center" />
  86 + <el-table-column :label="$t('propertyRightRegistration.table.roomNum')" align="center">
  87 + <template slot-scope="scope">
  88 + {{ `${scope.row.floorNum}-${scope.row.unitNum}-${scope.row.roomNum}` }}
  89 + </template>
  90 + </el-table-column>
  91 + <el-table-column prop="name" :label="$t('propertyRightRegistration.table.name')" align="center" />
  92 + <el-table-column prop="link" :label="$t('propertyRightRegistration.table.link')" align="center" />
  93 + <el-table-column prop="idCard" :label="$t('propertyRightRegistration.table.idCard')" align="center" />
  94 + <el-table-column prop="address" :label="$t('propertyRightRegistration.table.address')" align="center" />
  95 + <el-table-column prop="stateName" :label="$t('propertyRightRegistration.table.state')" align="center" />
  96 + <el-table-column :label="$t('common.operation')" align="center" width="300">
  97 + <template slot-scope="scope">
  98 + <el-button v-if="scope.row.state !== '1'" size="mini" @click="handleExamine(scope.row)">
  99 + {{ $t('common.examine') }}
  100 + </el-button>
  101 + <el-button size="mini" type="primary" @click="handleEdit(scope.row)">
  102 + {{ $t('common.edit') }}
  103 + </el-button>
  104 + <el-button size="mini" type="info" @click="handleDetail(scope.row)">
  105 + {{ $t('common.detail') }}
  106 + </el-button>
  107 + <el-button size="mini" type="danger" @click="handleDelete(scope.row)">
  108 + {{ $t('common.delete') }}
  109 + </el-button>
  110 + </template>
  111 + </el-table-column>
  112 + </el-table>
  113 +
  114 + <el-pagination :current-page="pagination.current" :page-sizes="[10, 20, 30, 50]" :page-size="pagination.size"
  115 + :total="pagination.total" layout="total, sizes, prev, pager, next, jumper" @size-change="handleSizeChange"
  116 + @current-change="handleCurrentChange" />
  117 + </el-card>
  118 +
  119 + <!-- 子组件 -->
  120 + <add-property-right-registration ref="addForm" @success="handleSuccess" />
  121 + <examine-property-right-registration ref="examineForm" @success="handleSuccess" />
  122 + <edit-property-right-registration ref="editForm" @success="handleSuccess" />
  123 + <delete-property-right-registration ref="deleteForm" @success="handleSuccess" />
  124 + </div>
  125 +</template>
  126 +
  127 +<script>
  128 +import { getDict } from '@/api/community/communityApi'
  129 +import { getCommunityId } from '@/api/community/communityApi'
  130 +import { listPropertyRightRegistration } from '@/api/room/propertyRightRegistrationManageApi'
  131 +import { queryFloors, queryUnits } from '@/api/room/roomApi'
  132 +import AddPropertyRightRegistration from '@/components/room/addPropertyRightRegistration'
  133 +import ExaminePropertyRightRegistration from '@/components/room/examinePropertyRightRegistration'
  134 +import EditPropertyRightRegistration from '@/components/room/editPropertyRightRegistration'
  135 +import DeletePropertyRightRegistration from '@/components/room/deletePropertyRightRegistration'
  136 +
  137 +export default {
  138 + name: 'PropertyRightRegistrationManageList',
  139 + components: {
  140 + AddPropertyRightRegistration,
  141 + ExaminePropertyRightRegistration,
  142 + EditPropertyRightRegistration,
  143 + DeletePropertyRightRegistration
  144 + },
  145 + data() {
  146 + return {
  147 + loading: false,
  148 + showMoreCondition: false,
  149 + searchForm: {
  150 + roomId: '',
  151 + allNum: '',
  152 + name: '',
  153 + link: '',
  154 + idCard: '',
  155 + address: '',
  156 + state: '',
  157 + floorId: '',
  158 + unitId: '',
  159 + communityId: ''
  160 + },
  161 + tableData: [],
  162 + pagination: {
  163 + current: 1,
  164 + size: 10,
  165 + total: 0
  166 + },
  167 + states: [],
  168 + floors: [],
  169 + units: []
  170 + }
  171 + },
  172 + created() {
  173 + this.searchForm.communityId = getCommunityId()
  174 + this.getList()
  175 + this.getDictData()
  176 + this.getFloors()
  177 + },
  178 + methods: {
  179 + async getList() {
  180 + try {
  181 + this.loading = true
  182 + const params = {
  183 + ...this.searchForm,
  184 + page: this.pagination.current,
  185 + row: this.pagination.size
  186 + }
  187 +
  188 + // 处理allNum
  189 + if (params.allNum && params.allNum.split('-').length === 3) {
  190 + const [floorNum, unitNum, roomNum] = params.allNum.split('-')
  191 + params.floorNum = floorNum.trim()
  192 + params.unitNum = unitNum.trim()
  193 + params.roomNum = roomNum.trim()
  194 + } else {
  195 + params.floorNum = ''
  196 + params.unitNum = ''
  197 + params.roomNum = ''
  198 + }
  199 +
  200 + const { data, total } = await listPropertyRightRegistration(params)
  201 + this.tableData = data
  202 + this.pagination.total = total
  203 + } catch (error) {
  204 + console.error('获取列表失败:', error)
  205 + this.$message.error(this.$t('propertyRightRegistration.fetchError'))
  206 + } finally {
  207 + this.loading = false
  208 + }
  209 + },
  210 + async getDictData() {
  211 + try {
  212 + this.states = await getDict('property_right_registration', 'state')
  213 + } catch (error) {
  214 + console.error('获取字典数据失败:', error)
  215 + }
  216 + },
  217 + async getFloors() {
  218 + try {
  219 + const params = {
  220 + communityId: this.searchForm.communityId,
  221 + page: 1,
  222 + row: 50
  223 + }
  224 + const data = await queryFloors(params)
  225 + this.floors = data.apiFloorDataVoList || []
  226 + } catch (error) {
  227 + console.error('获取楼栋数据失败:', error)
  228 + }
  229 + },
  230 + async handleFloorChange(floorId) {
  231 + try {
  232 + const params = {
  233 + floorId,
  234 + communityId: this.searchForm.communityId,
  235 + page: 1,
  236 + row: 50
  237 + }
  238 + const data = await queryUnits(params)
  239 + this.units = data || []
  240 + this.searchForm.unitId = ''
  241 + } catch (error) {
  242 + console.error('获取单元数据失败:', error)
  243 + }
  244 + },
  245 + handleSearch() {
  246 + this.pagination.current = 1
  247 + this.getList()
  248 + },
  249 + handleReset() {
  250 + this.searchForm = {
  251 + roomId: '',
  252 + allNum: '',
  253 + name: '',
  254 + link: '',
  255 + idCard: '',
  256 + address: '',
  257 + state: '',
  258 + floorId: '',
  259 + unitId: '',
  260 + communityId: getCommunityId()
  261 + }
  262 + this.units = []
  263 + this.handleSearch()
  264 + },
  265 + toggleMoreCondition() {
  266 + this.showMoreCondition = !this.showMoreCondition
  267 + },
  268 + handleAdd() {
  269 + this.$refs.addForm.open()
  270 + },
  271 + handleExamine(row) {
  272 + this.$refs.examineForm.open(row)
  273 + },
  274 + handleEdit(row) {
  275 + this.$refs.editForm.open(row)
  276 + },
  277 + handleDetail(row) {
  278 + this.$router.push({
  279 + path: '/views/room/listPropertyRightRegistrationDetail',
  280 + query: {
  281 + prrId: row.prrId,
  282 + floorNum: row.floorNum,
  283 + unitNum: row.unitNum,
  284 + roomNum: row.roomNum
  285 + }
  286 + })
  287 + },
  288 + handleDelete(row) {
  289 + this.$refs.deleteForm.open(row)
  290 + },
  291 + handleSuccess() {
  292 + this.getList()
  293 + },
  294 + handleSizeChange(val) {
  295 + this.pagination.size = val
  296 + this.getList()
  297 + },
  298 + handleCurrentChange(val) {
  299 + this.pagination.current = val
  300 + this.getList()
  301 + }
  302 + }
  303 +}
  304 +</script>
  305 +
  306 +<style lang="scss" scoped>
  307 +.property-right-registration-container {
  308 + padding: 0;
  309 + margin: 0;
  310 +
  311 + .search-wrapper {
  312 + margin-bottom: 20px;
  313 +
  314 + .el-row {
  315 + margin-bottom: 20px;
  316 + }
  317 +
  318 + .mt-20 {
  319 + margin-top: 20px;
  320 + }
  321 + }
  322 +
  323 + .list-wrapper {
  324 + .el-pagination {
  325 + margin-top: 20px;
  326 + text-align: right;
  327 + }
  328 + }
  329 +}
  330 +</style>
0 331 \ No newline at end of file
... ...
src/views/room/roomStructureLang.js 0 → 100644
  1 +export const messages = {
  2 + en: {
  3 + roomStructure: {
  4 + noOwner: 'None',
  5 + owe: 'Debt',
  6 + yuan: 'Yuan',
  7 + searchPlaceholder: 'Please enter room number like 1-1-1',
  8 + building: 'Building',
  9 + unit: 'Unit'
  10 + }
  11 + },
  12 + zh: {
  13 + roomStructure: {
  14 + noOwner: '无',
  15 + owe: '欠费',
  16 + yuan: '元',
  17 + searchPlaceholder: '请输入房屋编号 楼栋-单元-房屋 如1-1-1',
  18 + building: '栋',
  19 + unit: '单元'
  20 + }
  21 + }
  22 +}
0 23 \ No newline at end of file
... ...
src/views/room/roomStructureList.vue 0 → 100644
  1 +<template>
  2 + <div class="room-structure-container">
  3 + <el-row :gutter="20">
  4 + <el-col :span="4">
  5 + <floor-unit-tree ref="floorUnitTree" @switchFloorUnit="switchFloorUnit" />
  6 + </el-col>
  7 + <el-col :span="20">
  8 + <el-card v-if="roomStructureInfo.layerRoomCount < 5" class="room-list-card">
  9 + <el-row :gutter="10">
  10 + <el-col v-for="(room, index) in roomStructureInfo.rooms" :key="index" :xs="12" :sm="8" :md="6" :lg="4"
  11 + :xl="3" class="room-item">
  12 + <div class="room-card" :style="{ 'background-color': getBgColor(room) }"
  13 + @dblclick="toSimplifyAcceptance(room)">
  14 + <div class="room-number">{{ room.floorNum }}-{{ room.unitNum }}-{{ room.roomNum }}</div>
  15 + <div class="room-state">{{ room.stateName }}</div>
  16 + <div class="room-owner">{{ room.ownerName || $t('roomStructure.noOwner') }}</div>
  17 + <div class="room-debt">
  18 + <span>{{ $t('roomStructure.owe') }}</span>:{{ room.oweAmount }}
  19 + <span>{{ $t('roomStructure.yuan') }}</span>
  20 + </div>
  21 + </div>
  22 + </el-col>
  23 + </el-row>
  24 + </el-card>
  25 +
  26 + <el-card v-else class="room-list-card">
  27 + <div v-for="(val, key, index) in roomStructureInfo.parkRooms" :key="index">
  28 + <div class="floor-title">{{ key }}F</div>
  29 + <el-row :gutter="10" class="floor-row">
  30 + <el-col v-for="(room, index) in val" :key="index" :xs="12" :sm="8" :md="6" :lg="4" :xl="3"
  31 + class="room-item">
  32 + <div class="room-card" :style="{ 'background-color': getBgColor(room) }"
  33 + @dblclick="toSimplifyAcceptance(room)">
  34 + <div class="room-number">{{ room.floorNum }}-{{ room.unitNum }}-{{ room.roomNum }}</div>
  35 + <div class="room-state">{{ room.stateName }}</div>
  36 + <div class="room-owner">{{ room.ownerName || $t('roomStructure.noOwner') }}</div>
  37 + <div class="room-debt">
  38 + <span>{{ $t('roomStructure.owe') }}</span>:{{ room.oweAmount }}
  39 + <span>{{ $t('roomStructure.yuan') }}</span>
  40 + </div>
  41 + </div>
  42 + </el-col>
  43 + </el-row>
  44 + </div>
  45 + </el-card>
  46 + </el-col>
  47 + </el-row>
  48 + </div>
  49 +</template>
  50 +
  51 +<script>
  52 +import floorUnitTree from '@/components/room/floorUnitTree'
  53 +import { listRoomStructure } from '@/api/room/roomStructureApi'
  54 +import { getCommunityId } from '@/api/community/communityApi'
  55 +
  56 +export default {
  57 + name: 'RoomStructureList',
  58 + components: {
  59 + floorUnitTree
  60 + },
  61 + data() {
  62 + return {
  63 + roomStructureInfo: {
  64 + rooms: [],
  65 + parkRooms: {},
  66 + layerRoomCount: 4
  67 + },
  68 + communityId: ''
  69 + }
  70 + },
  71 + created() {
  72 + this.communityId = getCommunityId()
  73 + },
  74 + methods: {
  75 + handleSwitchUnit(params) {
  76 + this.loadRooms(params.unitId)
  77 + },
  78 + switchFloorUnit(data) {
  79 + if (!data.unitId) {
  80 + return
  81 + }
  82 + this.loadRooms(data.unitId)
  83 + },
  84 + async loadRooms(unitId) {
  85 + this.roomStructureInfo.rooms = []
  86 + this.roomStructureInfo.parkRooms = {}
  87 +
  88 + try {
  89 + const params = {
  90 + page: 1,
  91 + row: 100,
  92 + unitId: unitId,
  93 + communityId: this.communityId
  94 + }
  95 +
  96 + const { data } = await listRoomStructure(params)
  97 + this.roomStructureInfo.rooms = data
  98 + this.supportPark()
  99 + } catch (error) {
  100 + console.error('Failed to load rooms:', error)
  101 + }
  102 + },
  103 + getBgColor(room) {
  104 + if (room.oweAmount > 0) {
  105 + return "#DC3545"
  106 + }
  107 + if (!room.ownerName) {
  108 + return "#1AB394"
  109 + }
  110 + if (room.state === '2001') {
  111 + return '#1296db'
  112 + }
  113 + if (room.state === '2003') {
  114 + return '#4C8CDE'
  115 + }
  116 + if (room.state === '2005') {
  117 + return '#085DC9'
  118 + }
  119 + if (room.state === '2004') {
  120 + return '#9DBFEA'
  121 + }
  122 + if (room.state === '2006') {
  123 + return '#365A87'
  124 + }
  125 + if (room.state === '2007') {
  126 + return '#1053A8'
  127 + }
  128 + if (room.state === '2008') {
  129 + return '#4E79AF'
  130 + }
  131 + if (room.state === '2009') {
  132 + return '#5B81B1'
  133 + }
  134 + return "#1296db"
  135 + },
  136 + toSimplifyAcceptance(room) {
  137 + const date = new Date()
  138 + this.$store.dispatch('app/saveData', {
  139 + key: "JAVA110_IS_BACK",
  140 + value: date.getTime()
  141 + })
  142 + this.$store.dispatch('app/saveData', {
  143 + key: 'simplifyAcceptanceSearch',
  144 + value: {
  145 + searchType: '1',
  146 + searchValue: `${room.floorNum}-${room.unitNum}-${room.roomNum}`,
  147 + searchPlaceholder: this.$t('roomStructure.searchPlaceholder')
  148 + }
  149 + })
  150 + this.$router.push('/pages/property/simplifyAcceptance?tab=业务受理')
  151 + },
  152 + supportPark() {
  153 + const parkRooms = this.roomStructureInfo.parkRooms
  154 + if (!this.roomStructureInfo.rooms || this.roomStructureInfo.rooms.length < 1) {
  155 + return
  156 + }
  157 +
  158 + this.roomStructureInfo.rooms.forEach(item => {
  159 + if (!parkRooms[item.layer]) {
  160 + parkRooms[item.layer] = []
  161 + }
  162 + parkRooms[item.layer].push(item)
  163 + })
  164 +
  165 + this.roomStructureInfo.parkRooms = parkRooms
  166 + if (Object.keys(parkRooms).length > 0) {
  167 + this.roomStructureInfo.layerRoomCount = parkRooms[Object.keys(parkRooms)[0]].length
  168 + }
  169 + }
  170 + }
  171 +}
  172 +</script>
  173 +
  174 +<style lang="scss" scoped>
  175 +.room-structure-container {
  176 + padding: 20px;
  177 + height: 100%;
  178 +
  179 + .room-list-card {
  180 + margin-bottom: 20px;
  181 + min-height: calc(100vh - 120px);
  182 +
  183 + .floor-title {
  184 + font-size: 16px;
  185 + font-weight: bold;
  186 + padding: 10px 0;
  187 + }
  188 +
  189 + .floor-row {
  190 + margin-bottom: 20px;
  191 + }
  192 +
  193 + .room-item {
  194 + margin-bottom: 10px;
  195 + }
  196 +
  197 + .room-card {
  198 + color: #fff;
  199 + border-radius: 5px;
  200 + padding: 10px;
  201 + cursor: pointer;
  202 + transition: all 0.3s;
  203 + height: 100%;
  204 +
  205 + &:hover {
  206 + transform: translateY(-3px);
  207 + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  208 + }
  209 +
  210 + .room-number {
  211 + font-weight: bold;
  212 + margin-bottom: 5px;
  213 + }
  214 +
  215 + .room-state,
  216 + .room-owner,
  217 + .room-debt {
  218 + margin-bottom: 3px;
  219 + font-size: 12px;
  220 + }
  221 + }
  222 + }
  223 +}
  224 +</style>
0 225 \ No newline at end of file
... ...
src/views/system/assetImportLogDetailLang.js 0 → 100644
  1 +export const messages = {
  2 + en: {
  3 + assetImportLogDetail: {
  4 + title: 'Import Details',
  5 + importTime: 'Import Time',
  6 + status: 'Status',
  7 + description: 'Description',
  8 + all: 'All',
  9 + waitingImport: 'Waiting Import',
  10 + success: 'Success',
  11 + failed: 'Failed',
  12 + refresh: 'Refresh',
  13 + back: 'Back'
  14 + }
  15 + },
  16 + zh: {
  17 + assetImportLogDetail: {
  18 + title: '导入详情',
  19 + importTime: '导入时间',
  20 + status: '状态',
  21 + description: '描述',
  22 + all: '全部',
  23 + waitingImport: '待导入',
  24 + success: '成功',
  25 + failed: '失败',
  26 + refresh: '刷新',
  27 + back: '返回'
  28 + }
  29 + }
  30 +}
0 31 \ No newline at end of file
... ...
src/views/system/assetImportLogDetailList.vue 0 → 100644
  1 +<template>
  2 + <div class="animated fadeInRight ecommerce">
  3 + <el-row>
  4 + <el-col :span="4" class="padding-r-0">
  5 + <div class="border-radius">
  6 + <div class="margin-xs-r treeview attendance-staff">
  7 + <ul class="list-group text-center border-radius">
  8 + <li class="list-group-item node-orgTree" v-for="(item, index) in assetImportLogDetailInfo.states"
  9 + :key="index" @click="swatchDetailState(item)"
  10 + :class="{ 'vc-node-selected': assetImportLogDetailInfo.state === item.value }">
  11 + {{ item.name }}
  12 + </li>
  13 + </ul>
  14 + </div>
  15 + </div>
  16 + </el-col>
  17 + <el-col :span="20">
  18 + <el-card>
  19 + <div slot="header" class="clearfix">
  20 + <span>{{ $t('assetImportLogDetail.title') }}</span>
  21 + <div class="ibox-tools" style="float: right;">
  22 + <el-button type="primary" size="small" @click="queryAssetImportLogDetail()">
  23 + {{ $t('common.refresh') }}
  24 + </el-button>
  25 + <el-button size="small" @click="_goBack()">
  26 + {{ $t('common.back') }}
  27 + </el-button>
  28 + </div>
  29 + </div>
  30 + <el-table :data="assetImportLogDetailInfo.logs" border style="width: 100%" v-loading="loading">
  31 + <el-table-column v-for="(item, index) in assetImportLogDetailInfo.logTypes" :key="index"
  32 + :label="item.logColumn" align="center">
  33 + <template slot-scope="scope">
  34 + <div class="textAuto" style="max-width: 200px;">
  35 + {{ scope.row[item.logProperty] }}
  36 + </div>
  37 + </template>
  38 + </el-table-column>
  39 + <el-table-column :label="$t('assetImportLogDetail.importTime')" align="center">
  40 + <template slot-scope="scope">
  41 + {{ scope.row.createTime }}
  42 + </template>
  43 + </el-table-column>
  44 + <el-table-column :label="$t('assetImportLogDetail.status')" align="center">
  45 + <template slot-scope="scope">
  46 + <span v-if="scope.row.state === 'W'">{{ $t('assetImportLogDetail.waitingImport') }}</span>
  47 + <span v-else-if="scope.row.state === 'C'">{{ $t('assetImportLogDetail.success') }}</span>
  48 + <span v-else>{{ $t('assetImportLogDetail.failed') }}</span>
  49 + </template>
  50 + </el-table-column>
  51 + <el-table-column :label="$t('assetImportLogDetail.description')" align="center">
  52 + <template slot-scope="scope">
  53 + <div class="textAuto" style="max-width: 200px;">
  54 + {{ scope.row.message }}
  55 + </div>
  56 + </template>
  57 + </el-table-column>
  58 + </el-table>
  59 + <el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange"
  60 + :current-page="page.current" :page-sizes="[10, 20, 30, 50]" :page-size="page.size"
  61 + layout="total, sizes, prev, pager, next, jumper" :total="assetImportLogDetailInfo.total"
  62 + class="pagination"></el-pagination>
  63 + </el-card>
  64 + </el-col>
  65 + </el-row>
  66 + </div>
  67 +</template>
  68 +
  69 +<script>
  70 +import { queryAssetImportLogDetail, queryAssetImportLogType } from '@/api/system/assetImportLogDetailApi'
  71 +import { getCommunityId } from '@/api/community/communityApi'
  72 +
  73 +export default {
  74 + name: 'AssetImportLogDetailList',
  75 + data() {
  76 + return {
  77 + loading: false,
  78 + assetImportLogDetailInfo: {
  79 + logs: [],
  80 + logTypes: [],
  81 + states: [
  82 + { name: this.$t('assetImportLogDetail.all'), value: '' },
  83 + { name: this.$t('assetImportLogDetail.waitingImport'), value: 'W' },
  84 + { name: this.$t('assetImportLogDetail.success'), value: 'C' },
  85 + { name: this.$t('assetImportLogDetail.failed'), value: 'F' }
  86 + ],
  87 + total: 0,
  88 + records: 1,
  89 + logId: '',
  90 + logType: '',
  91 + state: ''
  92 + },
  93 + page: {
  94 + current: 1,
  95 + size: 10
  96 + },
  97 + communityId: ''
  98 + }
  99 + },
  100 + created() {
  101 + this.communityId = getCommunityId()
  102 + },
  103 + mounted() {
  104 + this.assetImportLogDetailInfo.logId = this.$route.query.logId
  105 + this.assetImportLogDetailInfo.logType = this.$route.query.logType
  106 + this.queryAssetImportLogType()
  107 + },
  108 + methods: {
  109 + async _listAssetImportLogDetails(page, size) {
  110 + try {
  111 + this.loading = true
  112 + const params = {
  113 + page: page,
  114 + row: size,
  115 + logId: this.assetImportLogDetailInfo.logId,
  116 + communityId: this.communityId,
  117 + state: this.assetImportLogDetailInfo.state
  118 + }
  119 + const { data, total } = await queryAssetImportLogDetail(params)
  120 + this.assetImportLogDetailInfo.logs = data
  121 + this.assetImportLogDetailInfo.total = total
  122 + } catch (error) {
  123 + console.error('请求失败:', error)
  124 + } finally {
  125 + this.loading = false
  126 + }
  127 + },
  128 + async queryAssetImportLogType() {
  129 + try {
  130 + const params = {
  131 + logType: this.assetImportLogDetailInfo.logType
  132 + }
  133 + const { data } = await queryAssetImportLogType(params)
  134 + this.assetImportLogDetailInfo.logTypes = data
  135 + this._listAssetImportLogDetails(this.page.current, this.page.size)
  136 + } catch (error) {
  137 + console.error('请求失败:', error)
  138 + }
  139 + },
  140 + _goBack() {
  141 + this.$router.go(-1)
  142 + },
  143 + queryAssetImportLogDetail() {
  144 + this._listAssetImportLogDetails(this.page.current, this.page.size)
  145 + },
  146 + swatchDetailState(item) {
  147 + this.assetImportLogDetailInfo.state = item.value
  148 + this._listAssetImportLogDetails(this.page.current, this.page.size)
  149 + },
  150 + handleSizeChange(val) {
  151 + this.page.size = val
  152 + this._listAssetImportLogDetails(this.page.current, val)
  153 + },
  154 + handleCurrentChange(val) {
  155 + this.page.current = val
  156 + this._listAssetImportLogDetails(val, this.page.size)
  157 + }
  158 + }
  159 +}
  160 +</script>
  161 +
  162 +<style scoped>
  163 +.animated {
  164 + padding: 15px;
  165 +}
  166 +
  167 +.padding-r-0 {
  168 + padding-right: 0;
  169 +}
  170 +
  171 +.border-radius {
  172 + border-radius: 4px;
  173 +}
  174 +
  175 +.margin-xs-r {
  176 + margin-right: 5px;
  177 +}
  178 +
  179 +.list-group {
  180 + padding-left: 0;
  181 + margin-bottom: 0;
  182 +}
  183 +
  184 +.list-group-item {
  185 + position: relative;
  186 + display: block;
  187 + padding: 10px 15px;
  188 + margin-bottom: -1px;
  189 + background-color: #fff;
  190 + border: 1px solid #ddd;
  191 + cursor: pointer;
  192 +}
  193 +
  194 +.list-group-item:hover {
  195 + background-color: #f5f5f5;
  196 +}
  197 +
  198 +.vc-node-selected {
  199 + background-color: #409EFF;
  200 + color: #fff;
  201 +}
  202 +
  203 +.textAuto {
  204 + white-space: nowrap;
  205 + overflow: hidden;
  206 + text-overflow: ellipsis;
  207 +}
  208 +
  209 +.pagination {
  210 + margin-top: 15px;
  211 + text-align: right;
  212 +}
  213 +</style>
0 214 \ No newline at end of file
... ...
src/views/system/assetImportLogLang.js 0 → 100644
  1 +export const messages = {
  2 + en: {
  3 + assetImportLog: {
  4 + title: 'Batch Import Log',
  5 + logId: 'Import ID',
  6 + logTypeName: 'Import Type',
  7 + waitCount: 'Pending Count',
  8 + successCount: 'Success Count',
  9 + errorCount: 'Failed Count',
  10 + state: 'Status',
  11 + stateW: 'Pending',
  12 + stateC: 'Completed',
  13 + stateP: 'Processing',
  14 + createTime: 'Import Time',
  15 + remark: 'Remark',
  16 + fetchError: 'Failed to fetch import logs'
  17 + }
  18 + },
  19 + zh: {
  20 + assetImportLog: {
  21 + title: '批量导入日志',
  22 + logId: '导入编号',
  23 + logTypeName: '导入类型',
  24 + waitCount: '待导入数',
  25 + successCount: '成功数量',
  26 + errorCount: '失败数量',
  27 + state: '状态',
  28 + stateW: '待导入',
  29 + stateC: '已完成',
  30 + stateP: '处理中',
  31 + createTime: '导入时间',
  32 + remark: '备注',
  33 + fetchError: '获取导入日志失败'
  34 + }
  35 + }
  36 +}
0 37 \ No newline at end of file
... ...
src/views/system/assetImportLogList.vue 0 → 100644
  1 +<template>
  2 + <div class="asset-import-log-container animated fadeInRight">
  3 + <el-card class="box-card">
  4 + <div slot="header" class="flex justify-between">
  5 + <span>{{ $t('assetImportLog.title') }}</span>
  6 + <div class="header-tools">
  7 + <el-button type="primary" size="small" @click="_queryData">
  8 + <i class="el-icon-refresh"></i>{{ $t('common.refresh') }}
  9 + </el-button>
  10 + </div>
  11 + </div>
  12 +
  13 + <el-row :gutter="20">
  14 + <el-col :span="24">
  15 + <el-table v-loading="loading" :data="assetImportLogInfo.logs" border style="width: 100%">
  16 + <el-table-column prop="logId" :label="$t('assetImportLog.logId')" align="center" />
  17 + <el-table-column prop="logTypeName" :label="$t('assetImportLog.logTypeName')" align="center" />
  18 + <el-table-column prop="waitCount" :label="$t('assetImportLog.waitCount')" align="center" />
  19 + <el-table-column prop="successCount" :label="$t('assetImportLog.successCount')" align="center" />
  20 + <el-table-column prop="errorCount" :label="$t('assetImportLog.errorCount')" align="center" />
  21 + <el-table-column prop="state" :label="$t('assetImportLog.state')" align="center">
  22 + <template slot-scope="scope">
  23 + <span v-if="scope.row.state === 'W'">{{ $t('assetImportLog.stateW') }}</span>
  24 + <span v-else-if="scope.row.state === 'C'">{{ $t('assetImportLog.stateC') }}</span>
  25 + <span v-else>{{ $t('assetImportLog.stateP') }}</span>
  26 + </template>
  27 + </el-table-column>
  28 + <el-table-column prop="createTime" :label="$t('assetImportLog.createTime')" align="center" />
  29 + <el-table-column prop="remark" :label="$t('assetImportLog.remark')" align="center" />
  30 + <el-table-column :label="$t('common.operation')" align="center" width="150">
  31 + <template slot-scope="scope">
  32 + <el-button size="mini" @click="_openDetail(scope.row)">{{ $t('common.detail') }}</el-button>
  33 + </template>
  34 + </el-table-column>
  35 + </el-table>
  36 +
  37 + <el-pagination :current-page.sync="page.current" :page-sizes="[10, 20, 30, 50]" :page-size="page.size"
  38 + :total="page.total" layout="total, sizes, prev, pager, next, jumper" @size-change="handleSizeChange"
  39 + @current-change="handleCurrentChange" />
  40 + </el-col>
  41 + </el-row>
  42 + </el-card>
  43 + </div>
  44 +</template>
  45 +
  46 +<script>
  47 +import { queryAssetImportLog } from '@/api/system/assetImportLogApi'
  48 +import { getCommunityId } from '@/api/community/communityApi'
  49 +
  50 +export default {
  51 + name: 'AssetImportLogList',
  52 + data() {
  53 + return {
  54 + loading: false,
  55 + assetImportLogInfo: {
  56 + logs: [],
  57 + total: 0
  58 + },
  59 + page: {
  60 + current: 1,
  61 + size: 10,
  62 + total: 0
  63 + },
  64 + communityId: ''
  65 + }
  66 + },
  67 + created() {
  68 + this.communityId = getCommunityId()
  69 + this._listAssetImportLogs(this.page.current, this.page.size)
  70 + },
  71 + methods: {
  72 + async _listAssetImportLogs(page, size) {
  73 + try {
  74 + this.loading = true
  75 + const params = {
  76 + page,
  77 + row: size,
  78 + communityId: this.communityId
  79 + }
  80 + const { data, total } = await queryAssetImportLog(params)
  81 + this.assetImportLogInfo.logs = data
  82 + this.page.total = total
  83 + } catch (error) {
  84 + this.$message.error(this.$t('assetImportLog.fetchError'))
  85 + } finally {
  86 + this.loading = false
  87 + }
  88 + },
  89 + _queryData() {
  90 + this.page.current = 1
  91 + this._listAssetImportLogs(this.page.current, this.page.size)
  92 + },
  93 + _openDetail(log) {
  94 + this.$router.push({
  95 + path: '/views/system/assetImportLogDetail',
  96 + query: {
  97 + logId: log.logId,
  98 + logType: log.logType
  99 + }
  100 + })
  101 + },
  102 + handleSizeChange(val) {
  103 + this.page.size = val
  104 + this._listAssetImportLogs(this.page.current, this.page.size)
  105 + },
  106 + handleCurrentChange(val) {
  107 + this.page.current = val
  108 + this._listAssetImportLogs(this.page.current, this.page.size)
  109 + }
  110 + }
  111 +}
  112 +</script>
  113 +
  114 +<style lang="scss" scoped>
  115 +.asset-import-log-container {
  116 + padding: 20px;
  117 +
  118 + .box-card {
  119 + margin-bottom: 20px;
  120 + }
  121 +
  122 + .header-tools {
  123 + float: right;
  124 + }
  125 +
  126 + .el-pagination {
  127 + margin-top: 20px;
  128 + text-align: right;
  129 + }
  130 +}
  131 +</style>
0 132 \ No newline at end of file
... ...
src/views/system/downloadTempFileLang.js 0 → 100644
  1 +export const messages = {
  2 + en: {
  3 + downloadTempFile: {
  4 + title: 'Download Center',
  5 + refresh: 'Refresh',
  6 + name: 'Name',
  7 + fileType: 'File Type',
  8 + downloadUser: 'Downloader',
  9 + downloadTime: 'Download Time',
  10 + status: 'Status',
  11 + remark: 'Remark',
  12 + operation: 'Operation',
  13 + download: 'Download',
  14 + delete: 'Delete',
  15 + fetchError: 'Failed to fetch file list'
  16 + },
  17 + deleteDownloadTempFile: {
  18 + title: 'Please confirm your operation',
  19 + confirmMessage: 'Are you sure to delete this file?',
  20 + cancel: 'Cancel',
  21 + confirm: 'Confirm Delete',
  22 + deleteSuccess: 'File deleted successfully',
  23 + deleteFailed: 'Failed to delete file'
  24 + }
  25 + },
  26 + zh: {
  27 + downloadTempFile: {
  28 + title: '下载中心',
  29 + refresh: '刷新',
  30 + name: '名称',
  31 + fileType: '文件类型',
  32 + downloadUser: '下载人',
  33 + downloadTime: '下载时间',
  34 + status: '状态',
  35 + remark: '备注',
  36 + operation: '操作',
  37 + download: '下载',
  38 + delete: '删除',
  39 + fetchError: '获取文件列表失败'
  40 + },
  41 + deleteDownloadTempFile: {
  42 + title: '请确认您的操作',
  43 + confirmMessage: '确定删除该文件吗?',
  44 + cancel: '取消',
  45 + confirm: '确认删除',
  46 + deleteSuccess: '文件删除成功',
  47 + deleteFailed: '文件删除失败'
  48 + }
  49 + }
  50 +}
0 51 \ No newline at end of file
... ...
src/views/system/downloadTempFileList.vue 0 → 100644
  1 +<template>
  2 + <div class="download-temp-file-container">
  3 + <el-card class="box-card">
  4 + <div slot="header" class="flex justify-between">
  5 + <span>{{ $t('downloadTempFile.title') }}</span>
  6 + <el-button type="primary" size="mini" style="float: right;" @click="_queryDownloadTempFileMethod">
  7 + {{ $t('downloadTempFile.refresh') }}
  8 + </el-button>
  9 + </div>
  10 +
  11 + <el-table v-loading="loading" :data="downloadTempFileInfo.files" border style="width: 100%">
  12 + <el-table-column prop="name" :label="$t('downloadTempFile.name')" align="center" />
  13 + <el-table-column prop="fileTypeName" :label="$t('downloadTempFile.fileType')" align="center" />
  14 + <el-table-column prop="downloadUserName" :label="$t('downloadTempFile.downloadUser')" align="center" />
  15 + <el-table-column prop="createTime" :label="$t('downloadTempFile.downloadTime')" align="center" />
  16 + <el-table-column prop="stateName" :label="$t('downloadTempFile.status')" align="center" />
  17 + <el-table-column prop="remark" :label="$t('downloadTempFile.remark')" align="center" />
  18 + <el-table-column :label="$t('downloadTempFile.operation')" align="center" width="200">
  19 + <template slot-scope="scope">
  20 + <el-button v-if="scope.row.state === '3003'" size="mini" type="primary" @click="_downLoadFile(scope.row)">
  21 + {{ $t('downloadTempFile.download') }}
  22 + </el-button>
  23 + <el-button v-if="scope.row.state === '3003' || scope.row.state === '4004'" size="mini" type="danger"
  24 + @click="_openDeleteFileModel(scope.row)">
  25 + {{ $t('downloadTempFile.delete') }}
  26 + </el-button>
  27 + </template>
  28 + </el-table-column>
  29 + </el-table>
  30 +
  31 + <el-pagination :current-page.sync="page.current" :page-sizes="[10, 20, 30, 50]" :page-size="page.size"
  32 + :total="page.total" layout="total, sizes, prev, pager, next, jumper" @size-change="handleSizeChange"
  33 + @current-change="handleCurrentChange" />
  34 + </el-card>
  35 +
  36 + <delete-download-temp-file ref="deleteDialog" @success="handleSuccess" />
  37 + </div>
  38 +</template>
  39 +
  40 +<script>
  41 +import { listUserDownloadFile } from '@/api/system/downloadTempFileApi'
  42 +import DeleteDownloadTempFile from '@/components/system/deleteDownloadTempFile'
  43 +import { getCommunityId } from '@/api/community/communityApi'
  44 +
  45 +export default {
  46 + name: 'DownloadTempFileList',
  47 + components: {
  48 + DeleteDownloadTempFile
  49 + },
  50 + data() {
  51 + return {
  52 + loading: false,
  53 + downloadTempFileInfo: {
  54 + files: [],
  55 + conditions: {
  56 + communityId: '',
  57 + page: 1,
  58 + row: 10
  59 + }
  60 + },
  61 + page: {
  62 + current: 1,
  63 + size: 10,
  64 + total: 0
  65 + }
  66 + }
  67 + },
  68 + created() {
  69 + this.downloadTempFileInfo.conditions.communityId = getCommunityId()
  70 + this._listFiles(this.page.current, this.page.size)
  71 + },
  72 + methods: {
  73 + async _listFiles(page, size) {
  74 + try {
  75 + this.loading = true
  76 + this.downloadTempFileInfo.conditions.page = page
  77 + this.downloadTempFileInfo.conditions.row = size
  78 +
  79 + const { data, total } = await listUserDownloadFile(this.downloadTempFileInfo.conditions)
  80 + this.downloadTempFileInfo.files = data
  81 + this.page.total = total
  82 + } catch (error) {
  83 + this.$message.error(this.$t('downloadTempFile.fetchError'))
  84 + } finally {
  85 + this.loading = false
  86 + }
  87 + },
  88 + _downLoadFile(file) {
  89 + if (!file.tempUrl) {
  90 + this.$message.error(this.$t('downloadTempFile.downloadFailed'))
  91 + return
  92 + }
  93 + window.open(file.downloadUrl, '_blank')
  94 + },
  95 + _openDeleteFileModel(file) {
  96 + this.$refs.deleteDialog.open(file)
  97 + },
  98 + _queryDownloadTempFileMethod() {
  99 + this.page.current = 1
  100 + this._listFiles(this.page.current, this.page.size)
  101 + },
  102 + handleSuccess() {
  103 + this._listFiles(this.page.current, this.page.size)
  104 + },
  105 + handleSizeChange(val) {
  106 + this.page.size = val
  107 + this._listFiles(this.page.current, val)
  108 + },
  109 + handleCurrentChange(val) {
  110 + this.page.current = val
  111 + this._listFiles(val, this.page.size)
  112 + }
  113 + }
  114 +}
  115 +</script>
  116 +
  117 +<style lang="scss" scoped>
  118 +.download-temp-file-container {
  119 + padding: 20px;
  120 +
  121 + .box-card {
  122 + margin-bottom: 20px;
  123 + }
  124 +
  125 + .el-pagination {
  126 + margin-top: 20px;
  127 + text-align: right;
  128 + }
  129 +}
  130 +</style>
0 131 \ No newline at end of file
... ...
src/views/system/feePrintPageManageLang.js 0 → 100644
  1 +export const messages = {
  2 + en: {
  3 + feePrintPageManage: {
  4 + search: {
  5 + title: 'Search Conditions',
  6 + pageId: 'Receipt ID',
  7 + pageName: 'Name',
  8 + state: 'Status',
  9 + all: 'All',
  10 + enabled: 'Enabled',
  11 + disabled: 'Disabled'
  12 + },
  13 + list: {
  14 + title: 'Receipt Template'
  15 + },
  16 + table: {
  17 + pageId: 'Receipt ID',
  18 + pageName: 'Name',
  19 + communityId: 'Community ID',
  20 + templateName: 'Receipt Page',
  21 + state: 'Status',
  22 + enabled: 'Enabled',
  23 + disabled: 'Disabled',
  24 + operation: 'Operation'
  25 + },
  26 + add: {
  27 + title: 'Add Receipt Template',
  28 + pageName: 'Name',
  29 + pageNamePlaceholder: 'Please enter name',
  30 + template: 'Receipt Page',
  31 + templatePlaceholder: 'Please select receipt page',
  32 + success: 'Add successfully'
  33 + },
  34 + edit: {
  35 + title: 'Edit Receipt Template',
  36 + pageName: 'Name',
  37 + pageNamePlaceholder: 'Please enter name',
  38 + template: 'Receipt Page',
  39 + templatePlaceholder: 'Please select receipt page',
  40 + success: 'Update successfully'
  41 + },
  42 + delete: {
  43 + title: 'Delete Confirmation',
  44 + confirmText: 'Are you sure to delete this receipt template?',
  45 + success: 'Delete successfully'
  46 + },
  47 + validate: {
  48 + pageNameRequired: 'Name is required',
  49 + pageNameMaxLength: 'Name cannot exceed 128 characters',
  50 + templateRequired: 'Receipt page is required',
  51 + pageIdRequired: 'Receipt ID is required'
  52 + },
  53 + fetchError: 'Failed to fetch data'
  54 + }
  55 + },
  56 + zh: {
  57 + feePrintPageManage: {
  58 + search: {
  59 + title: '查询条件',
  60 + pageId: '收据ID',
  61 + pageName: '名称',
  62 + state: '状态',
  63 + all: '全部',
  64 + enabled: '启用',
  65 + disabled: '停用'
  66 + },
  67 + list: {
  68 + title: '收据模板'
  69 + },
  70 + table: {
  71 + pageId: '收据ID',
  72 + pageName: '名称',
  73 + communityId: '小区ID',
  74 + templateName: '收据页面',
  75 + state: '状态',
  76 + enabled: '启用',
  77 + disabled: '停用',
  78 + operation: '操作'
  79 + },
  80 + add: {
  81 + title: '添加收据模板',
  82 + pageName: '名称',
  83 + pageNamePlaceholder: '请输入名称',
  84 + template: '收据页面',
  85 + templatePlaceholder: '请选择收据页面',
  86 + success: '添加成功'
  87 + },
  88 + edit: {
  89 + title: '修改收据模板',
  90 + pageName: '名称',
  91 + pageNamePlaceholder: '请输入名称',
  92 + template: '收据页面',
  93 + templatePlaceholder: '请选择收据页面',
  94 + success: '修改成功'
  95 + },
  96 + delete: {
  97 + title: '删除确认',
  98 + confirmText: '确定删除该收据模板吗?',
  99 + success: '删除成功'
  100 + },
  101 + validate: {
  102 + pageNameRequired: '名称不能为空',
  103 + pageNameMaxLength: '名称不能超过128个字符',
  104 + templateRequired: '收据页面不能为空',
  105 + pageIdRequired: '收据ID不能为空'
  106 + },
  107 + fetchError: '获取数据失败'
  108 + }
  109 + }
  110 +}
0 111 \ No newline at end of file
... ...
src/views/system/feePrintPageManageList.vue 0 → 100644
  1 +<template>
  2 + <div class="fee-print-page-manage-container">
  3 + <!-- 查询条件 -->
  4 + <el-card class="search-wrapper">
  5 + <div slot="header" class="flex justify-between">
  6 + <span>{{ $t('feePrintPageManage.search.title') }}</span>
  7 + </div>
  8 + <el-row :gutter="20">
  9 + <el-col :span="6">
  10 + <el-input v-model="searchForm.pageId" :placeholder="$t('feePrintPageManage.search.pageId')" clearable />
  11 + </el-col>
  12 + <el-col :span="6">
  13 + <el-input v-model="searchForm.pageName" :placeholder="$t('feePrintPageManage.search.pageName')" clearable />
  14 + </el-col>
  15 + <el-col :span="6">
  16 + <el-select v-model="searchForm.state" :placeholder="$t('feePrintPageManage.search.state')" style="width:100%">
  17 + <el-option :label="$t('feePrintPageManage.search.all')" value="" />
  18 + <el-option :label="$t('feePrintPageManage.search.enabled')" value="T" />
  19 + <el-option :label="$t('feePrintPageManage.search.disabled')" value="F" />
  20 + </el-select>
  21 + </el-col>
  22 + <el-col :span="6">
  23 + <el-button type="primary" @click="handleSearch">
  24 + {{ $t('common.search') }}
  25 + </el-button>
  26 + <el-button @click="handleReset">
  27 + {{ $t('common.reset') }}
  28 + </el-button>
  29 + </el-col>
  30 + </el-row>
  31 + </el-card>
  32 +
  33 + <!-- 收据模板列表 -->
  34 + <el-card class="list-wrapper">
  35 + <div slot="header" class="flex justify-between">
  36 + <span>{{ $t('feePrintPageManage.list.title') }}</span>
  37 + <el-button type="primary" size="small" style="float:right" @click="handleAdd">
  38 + {{ $t('common.add') }}
  39 + </el-button>
  40 + </div>
  41 +
  42 + <el-table v-loading="loading" :data="tableData" border style="width:100%">
  43 + <el-table-column prop="pageId" :label="$t('feePrintPageManage.table.pageId')" align="center" />
  44 + <el-table-column prop="pageName" :label="$t('feePrintPageManage.table.pageName')" align="center" />
  45 + <el-table-column prop="communityId" :label="$t('feePrintPageManage.table.communityId')" align="center" />
  46 + <el-table-column prop="templateName" :label="$t('feePrintPageManage.table.templateName')" align="center" />
  47 + <el-table-column :label="$t('feePrintPageManage.table.state')" align="center">
  48 + <template slot-scope="scope">
  49 + {{ scope.row.state === 'T' ? $t('feePrintPageManage.table.enabled') :
  50 + $t('feePrintPageManage.table.disabled') }}
  51 + </template>
  52 + </el-table-column>
  53 + <el-table-column :label="$t('common.operation')" align="center" width="200">
  54 + <template slot-scope="scope">
  55 + <el-button size="mini" type="primary" @click="handleEdit(scope.row)">
  56 + {{ $t('common.edit') }}
  57 + </el-button>
  58 + <el-button v-if="scope.row.state === 'F'" size="mini" type="success" @click="handleEnable(scope.row)">
  59 + {{ $t('common.enabled') }}
  60 + </el-button>
  61 + <el-button size="mini" type="danger" @click="handleDelete(scope.row)">
  62 + {{ $t('common.delete') }}
  63 + </el-button>
  64 + </template>
  65 + </el-table-column>
  66 + </el-table>
  67 +
  68 + <el-pagination :current-page="pagination.current" :page-sizes="[10, 20, 30, 50]" :page-size="pagination.size"
  69 + :total="pagination.total" layout="total, sizes, prev, pager, next, jumper" @size-change="handleSizeChange"
  70 + @current-change="handleCurrentChange" />
  71 + </el-card>
  72 +
  73 + <!-- 添加弹窗 -->
  74 + <add-fee-print-page ref="addDialog" @success="handleSuccess" />
  75 +
  76 + <!-- 编辑弹窗 -->
  77 + <edit-fee-print-page ref="editDialog" @success="handleSuccess" />
  78 +
  79 + <!-- 删除弹窗 -->
  80 + <delete-fee-print-page ref="deleteDialog" @success="handleSuccess" />
  81 + </div>
  82 +</template>
  83 +
  84 +<script>
  85 +import { getCommunityId } from '@/api/community/communityApi'
  86 +import { listFeePrintPage, updateFeePrintPageState } from '@/api/system/feePrintPageManageApi'
  87 +import AddFeePrintPage from '@/components/system/addFeePrintPage'
  88 +import EditFeePrintPage from '@/components/system/editFeePrintPage'
  89 +import DeleteFeePrintPage from '@/components/system/deleteFeePrintPage'
  90 +
  91 +export default {
  92 + name: 'FeePrintPageManageList',
  93 + components: {
  94 + AddFeePrintPage,
  95 + EditFeePrintPage,
  96 + DeleteFeePrintPage
  97 + },
  98 + data() {
  99 + return {
  100 + loading: false,
  101 + communityId: '',
  102 + searchForm: {
  103 + pageId: '',
  104 + pageName: '',
  105 + state: ''
  106 + },
  107 + tableData: [],
  108 + pagination: {
  109 + current: 1,
  110 + size: 10,
  111 + total: 0
  112 + }
  113 + }
  114 + },
  115 + created() {
  116 + this.communityId = getCommunityId()
  117 + this.getList()
  118 + },
  119 + methods: {
  120 + async getList() {
  121 + try {
  122 + this.loading = true
  123 + const params = {
  124 + page: this.pagination.current,
  125 + row: this.pagination.size,
  126 + communityId: this.communityId,
  127 + ...this.searchForm
  128 + }
  129 + const { data, total } = await listFeePrintPage(params)
  130 + this.tableData = data
  131 + this.pagination.total = total
  132 + } catch (error) {
  133 + this.$message.error(this.$t('feePrintPageManage.fetchError'))
  134 + } finally {
  135 + this.loading = false
  136 + }
  137 + },
  138 + handleSearch() {
  139 + this.pagination.current = 1
  140 + this.getList()
  141 + },
  142 + handleReset() {
  143 + this.searchForm = {
  144 + pageId: '',
  145 + pageName: '',
  146 + state: ''
  147 + }
  148 + this.handleSearch()
  149 + },
  150 + handleAdd() {
  151 + this.$refs.addDialog.open()
  152 + },
  153 + handleEdit(row) {
  154 + this.$refs.editDialog.open(row)
  155 + },
  156 + handleEnable(row) {
  157 + // TODO: 启用逻辑
  158 + console.log(row)
  159 + updateFeePrintPageState({
  160 + pageId: row.pageId,
  161 + state: 'T'
  162 + }).then(() => {
  163 + this.$message.success(this.$t('feePrintPageManage.enableSuccess'))
  164 + this.getList()
  165 + })
  166 + },
  167 + handleDelete(row) {
  168 + this.$refs.deleteDialog.open(row)
  169 + },
  170 + handleSuccess() {
  171 + this.getList()
  172 + },
  173 + handleSizeChange(val) {
  174 + this.pagination.size = val
  175 + this.getList()
  176 + },
  177 + handleCurrentChange(val) {
  178 + this.pagination.current = val
  179 + this.getList()
  180 + }
  181 + }
  182 +}
  183 +</script>
  184 +
  185 +<style lang="scss" scoped>
  186 +.fee-print-page-manage-container {
  187 + padding: 20px;
  188 +
  189 + .search-wrapper {
  190 + margin-bottom: 20px;
  191 +
  192 + .el-row {
  193 + margin-bottom: 20px;
  194 + }
  195 + }
  196 +
  197 + .list-wrapper {
  198 + margin-bottom: 20px;
  199 + }
  200 +
  201 + .el-pagination {
  202 + margin-top: 20px;
  203 + text-align: right;
  204 + }
  205 +}
  206 +</style>
0 207 \ No newline at end of file
... ...