Commit cd8d442fe0e5ef5a96873e9c9534d81c1bacbdf9

Authored by wuxw
1 parent 5760f7b9

开始处理水电抄表功能

public/js/jessibuca/jessibuca.d.ts deleted
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 deleted
No preview for this file type
public/js/jessibuca/renderer.js deleted
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,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';  
121 - var playBigBase64 = 'data:image/png;base64,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';  
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/fee/contractCreateFeeApi.js 0 → 100644
  1 +import request from '@/utils/request'
  2 +import { getCommunityId } from '@/api/community/communityApi'
  3 +
  4 +// 查询合同列表
  5 +export function queryContract(params) {
  6 + return new Promise((resolve, reject) => {
  7 + request({
  8 + url: '/contract/queryContract',
  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 saveContractCreateFee(data) {
  25 + return new Promise((resolve, reject) => {
  26 + request({
  27 + url: '/fee.saveContractCreateFee',
  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 +}
  41 +
  42 +// 获取费用配置列表
  43 +export function listFeeConfigs(params) {
  44 + return new Promise((resolve, reject) => {
  45 + request({
  46 + url: '/feeConfig.listFeeConfigs',
  47 + method: 'get',
  48 + params: {
  49 + ...params,
  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 getDict(dictType, dictName) {
  63 + return new Promise((resolve, reject) => {
  64 + request({
  65 + url: '/dict/getDict',
  66 + method: 'get',
  67 + params: {
  68 + dictType,
  69 + dictName,
  70 + communityId: getCommunityId()
  71 + }
  72 + }).then(response => {
  73 + const res = response.data
  74 + resolve(res)
  75 + }).catch(error => {
  76 + reject(error)
  77 + })
  78 + })
  79 +}
0 \ No newline at end of file 80 \ No newline at end of file
src/api/fee/meterWaterManageApi.js 0 → 100644
  1 +import request from '@/utils/request'
  2 +import { getCommunityId } from '@/api/community/communityApi'
  3 +
  4 +// 查询抄表记录列表
  5 +export function listMeterWaters(params) {
  6 + return new Promise((resolve, reject) => {
  7 + params.communityId = getCommunityId()
  8 + request({
  9 + url: '/meterWater.listMeterWaters',
  10 + method: 'get',
  11 + params
  12 + }).then(response => {
  13 + const res = response.data
  14 + resolve({
  15 + data: res.data,
  16 + total: res.total,
  17 + records: res.records
  18 + })
  19 + }).catch(error => {
  20 + reject(error)
  21 + })
  22 + })
  23 +}
  24 +
  25 +// 查询抄表类型列表
  26 +export function listMeterTypes(params) {
  27 + return new Promise((resolve, reject) => {
  28 + params.communityId = getCommunityId()
  29 + request({
  30 + url: '/meterType.listMeterType',
  31 + method: 'get',
  32 + params
  33 + }).then(response => {
  34 + const res = response.data
  35 + resolve({
  36 + data: res.data,
  37 + total: res.total
  38 + })
  39 + }).catch(error => {
  40 + reject(error)
  41 + })
  42 + })
  43 +}
  44 +
  45 +// 查询楼栋列表
  46 +export function queryFloors(params) {
  47 + return new Promise((resolve, reject) => {
  48 + params.communityId = getCommunityId()
  49 + request({
  50 + url: '/floor.queryFloors',
  51 + method: 'get',
  52 + params
  53 + }).then(response => {
  54 + const res = response.data
  55 + resolve({
  56 + data: res.apiFloorDataVoList,
  57 + total: res.total
  58 + })
  59 + }).catch(error => {
  60 + reject(error)
  61 + })
  62 + })
  63 +}
  64 +
  65 +// 查询单元列表
  66 +export function queryUnits(params) {
  67 + return new Promise((resolve, reject) => {
  68 + params.communityId = getCommunityId()
  69 + request({
  70 + url: '/unit.queryUnits',
  71 + method: 'get',
  72 + params
  73 + }).then(response => {
  74 + const res = response.data
  75 + resolve(res)
  76 + }).catch(error => {
  77 + reject(error)
  78 + })
  79 + })
  80 +}
  81 +
  82 +// 查询房屋列表
  83 +export function queryRooms(params) {
  84 + return new Promise((resolve, reject) => {
  85 + params.communityId = getCommunityId()
  86 + request({
  87 + url: '/room.queryRooms',
  88 + method: 'get',
  89 + params
  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 queryRoomsTree(params) {
  101 + return new Promise((resolve, reject) => {
  102 + params.communityId = getCommunityId()
  103 + request({
  104 + url: '/room.queryRoomsTree',
  105 + method: 'get',
  106 + params
  107 + }).then(response => {
  108 + const res = response.data
  109 + resolve(res)
  110 + }).catch(error => {
  111 + reject(error)
  112 + })
  113 + })
  114 +}
  115 +
  116 +// 查询上期抄表记录
  117 +export function queryPreMeterWater(params) {
  118 + return new Promise((resolve, reject) => {
  119 + params.communityId = getCommunityId()
  120 + request({
  121 + url: '/meterWater/queryPreMeterWater',
  122 + method: 'get',
  123 + params
  124 + }).then(response => {
  125 + const res = response.data
  126 + resolve(res)
  127 + }).catch(error => {
  128 + reject(error)
  129 + })
  130 + })
  131 +}
  132 +
  133 +// 添加抄表记录
  134 +export function saveMeterWater(data) {
  135 + return new Promise((resolve, reject) => {
  136 + data.communityId = getCommunityId()
  137 + request({
  138 + url: '/meterWater.saveMeterWater',
  139 + method: 'post',
  140 + data
  141 + }).then(response => {
  142 + const res = response.data
  143 + resolve(res)
  144 + }).catch(error => {
  145 + reject(error)
  146 + })
  147 + })
  148 +}
  149 +
  150 +// 修改抄表记录
  151 +export function updateMeterWater(data) {
  152 + return new Promise((resolve, reject) => {
  153 + data.communityId = getCommunityId()
  154 + request({
  155 + url: '/meterWater.updateMeterWater',
  156 + method: 'post',
  157 + data
  158 + }).then(response => {
  159 + const res = response.data
  160 + resolve(res)
  161 + }).catch(error => {
  162 + reject(error)
  163 + })
  164 + })
  165 +}
  166 +
  167 +// 删除抄表记录
  168 +export function deleteMeterWater(data) {
  169 + return new Promise((resolve, reject) => {
  170 + data.communityId = getCommunityId()
  171 + request({
  172 + url: '/meterWater.deleteMeterWater',
  173 + method: 'post',
  174 + data
  175 + }).then(response => {
  176 + const res = response.data
  177 + resolve(res)
  178 + }).catch(error => {
  179 + reject(error)
  180 + })
  181 + })
  182 +}
  183 +
  184 +// 查询收费项目列表
  185 +export function listFeeConfigs(params) {
  186 + return new Promise((resolve, reject) => {
  187 + params.communityId = getCommunityId()
  188 + request({
  189 + url: '/feeConfig.listFeeConfigs',
  190 + method: 'get',
  191 + params
  192 + }).then(response => {
  193 + const res = response.data
  194 + resolve({
  195 + data: res.feeConfigs,
  196 + total: res.total
  197 + })
  198 + }).catch(error => {
  199 + reject(error)
  200 + })
  201 + })
  202 +}
  203 +
  204 +// 导入抄表数据
  205 +export function importMeterWaterData(data) {
  206 + return new Promise((resolve, reject) => {
  207 + data.append('communityId', getCommunityId())
  208 + request({
  209 + url: '/assetImport/importData',
  210 + method: 'post',
  211 + data,
  212 + headers: {
  213 + 'Content-Type': 'multipart/form-data'
  214 + }
  215 + }).then(response => {
  216 + const res = response.data
  217 + resolve(res)
  218 + }).catch(error => {
  219 + reject(error)
  220 + })
  221 + })
  222 +}
  223 +
  224 +// 导出抄表模板
  225 +export function exportMeterWaterTemplate(params) {
  226 + return new Promise((resolve, reject) => {
  227 + params.communityId = getCommunityId()
  228 + request({
  229 + url: '/export.exportData',
  230 + method: 'get',
  231 + params
  232 + }).then(response => {
  233 + const res = response.data
  234 + resolve(res)
  235 + }).catch(error => {
  236 + reject(error)
  237 + })
  238 + })
  239 +}
0 \ No newline at end of file 240 \ No newline at end of file
src/components/car/floorUnitTree.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="expandedKeys"
  9 + :highlight-current="true"
  10 + @node-click="handleNodeClick"
  11 + >
  12 + <template #default="{ node, data }">
  13 + <span class="custom-tree-node">
  14 + <img :src="data.icon" class="tree-icon" v-if="data.icon">
  15 + <span>{{ node.label }}</span>
  16 + </span>
  17 + </template>
  18 + </el-tree>
  19 + <div v-if="!treeData || treeData.length === 0" class="no-data">
  20 + {{ $t('floorUnitTree.noBuilding') }}
  21 + </div>
  22 + </el-card>
  23 +</template>
  24 +
  25 +<script>
  26 +import { queryFloorAndUnits } from '@/api/car/carStructureApi'
  27 +import { getCommunityId } from '@/api/community/communityApi'
  28 +
  29 +export default {
  30 + name: 'FloorUnitTree',
  31 + props: {
  32 + floorId: {
  33 + type: String,
  34 + default: ''
  35 + }
  36 + },
  37 + data() {
  38 + return {
  39 + treeData: [],
  40 + defaultProps: {
  41 + children: 'children',
  42 + label: 'text'
  43 + },
  44 + expandedKeys: [],
  45 + communityId: ''
  46 + }
  47 + },
  48 + watch: {
  49 + floorId(newVal) {
  50 + this.$nextTick(() => {
  51 + if (newVal) {
  52 + const node = this.$refs.tree.getNode('f_' + newVal)
  53 + if (node) {
  54 + this.$refs.tree.setCurrentKey(node.key)
  55 + }
  56 + }
  57 + })
  58 + }
  59 + },
  60 + created() {
  61 + this.communityId = getCommunityId()
  62 + this.loadFloorAndUnits()
  63 + },
  64 + methods: {
  65 + async loadFloorAndUnits() {
  66 + try {
  67 + const params = {
  68 + communityId: this.communityId
  69 + }
  70 + const data = await queryFloorAndUnits(params)
  71 + this.treeData = this.formatTreeData(data)
  72 + this.setDefaultExpanded()
  73 + } catch (error) {
  74 + this.$message.error(this.$t('floorUnitTree.fetchError'))
  75 + }
  76 + },
  77 + formatTreeData(data) {
  78 + const formattedData = []
  79 + const floorMap = {}
  80 +
  81 + // First pass: create floor nodes
  82 + data.forEach(item => {
  83 + if (!floorMap[item.floorId]) {
  84 + floorMap[item.floorId] = {
  85 + id: 'f_' + item.floorId,
  86 + floorId: item.floorId,
  87 + floorNum: item.floorNum,
  88 + icon: require('@/assets/img/floor.png'),
  89 + text: `${item.floorNum}${this.$t('floorUnitTree.building')}(${item.floorName})`,
  90 + children: []
  91 + }
  92 + formattedData.push(floorMap[item.floorId])
  93 + }
  94 +
  95 + // Add unit if it exists and not '0'
  96 + if (item.unitId && item.unitNum !== '0') {
  97 + floorMap[item.floorId].children.push({
  98 + id: 'u_' + item.unitId,
  99 + unitId: item.unitId,
  100 + text: `${item.unitNum}${this.$t('floorUnitTree.unit')}`,
  101 + icon: require('@/assets/img/unit.png')
  102 + })
  103 + }
  104 + })
  105 +
  106 + return formattedData
  107 + },
  108 + setDefaultExpanded() {
  109 + if (this.treeData.length > 0) {
  110 + this.expandedKeys = [this.treeData[0].id]
  111 + }
  112 + },
  113 + handleNodeClick(data) {
  114 + if (data.id.startsWith('f_')) {
  115 + this.$emit('switchFloor', { floorId: data.floorId })
  116 + } else if (data.id.startsWith('u_')) {
  117 + this.$emit('switchUnit', { unitId: data.unitId })
  118 + }
  119 + },
  120 + refreshTree(params) {
  121 + if (params && params.floorId) {
  122 + this.floorId = params.floorId
  123 + }
  124 + this.loadFloorAndUnits()
  125 + }
  126 + }
  127 +}
  128 +</script>
  129 +
  130 +<style lang="scss" scoped>
  131 +.tree-card {
  132 + height: 100%;
  133 +
  134 + .custom-tree-node {
  135 + display: flex;
  136 + align-items: center;
  137 + }
  138 +
  139 + .tree-icon {
  140 + width: 16px;
  141 + height: 16px;
  142 + margin-right: 5px;
  143 + }
  144 +
  145 + .no-data {
  146 + padding: 10px;
  147 + text-align: center;
  148 + color: #909399;
  149 + }
  150 +}
  151 +</style>
0 \ No newline at end of file 152 \ No newline at end of file
src/components/fee/addMeterWater.vue 0 → 100644
  1 +<template>
  2 + <el-dialog :title="$t('meterWater.addMeterReading')" :visible.sync="dialogVisible" width="50%" @close="handleClose">
  3 + <el-form ref="form" :model="form" :rules="rules" label-width="120px" label-position="right">
  4 + <el-form-item :label="$t('meterWater.feeType')" prop="feeTypeCd">
  5 + <el-select v-model="form.feeTypeCd" :placeholder="$t('meterWater.selectFeeType')" style="width: 100%"
  6 + @change="handleFeeTypeChange">
  7 + <el-option v-for="item in feeTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
  8 + </el-select>
  9 + </el-form-item>
  10 +
  11 + <el-form-item :label="$t('meterWater.feeItem')" prop="configId">
  12 + <el-select v-model="form.configId" :placeholder="$t('meterWater.selectFeeItem')" style="width: 100%"
  13 + @change="handleConfigChange">
  14 + <el-option v-for="item in feeConfigs" :key="item.configId" :label="item.feeName" :value="item.configId" />
  15 + </el-select>
  16 + <div class="form-tip">
  17 + {{ $t('meterWater.feeItemTip') }}
  18 + </div>
  19 + </el-form-item>
  20 +
  21 + <el-form-item :label="$t('meterWater.meterType')" prop="meterType">
  22 + <el-select v-model="form.meterType" :placeholder="$t('meterWater.selectMeterType')" style="width: 100%"
  23 + @change="handleMeterTypeChange">
  24 + <el-option v-for="item in meterTypes" :key="item.typeId" :label="item.typeName" :value="item.typeId" />
  25 + </el-select>
  26 + </el-form-item>
  27 +
  28 + <template v-if="!form.hasRoom">
  29 + <el-form-item :label="$t('meterWater.building')">
  30 + <floor-select2 ref="floorSelect" @change="handleFloorChange" />
  31 + </el-form-item>
  32 +
  33 + <el-form-item :label="$t('meterWater.unit')">
  34 + <unit-select2 ref="unitSelect" @change="handleUnitChange" />
  35 + </el-form-item>
  36 +
  37 + <el-form-item :label="$t('meterWater.room')">
  38 + <room-select2 ref="roomSelect" @change="handleRoomChange" />
  39 + </el-form-item>
  40 + </template>
  41 +
  42 + <el-form-item v-else :label="$t('meterWater.feeObject')">
  43 + <el-input v-model="form.ownerName" :placeholder="$t('meterWater.inputRoom')" disabled />
  44 + </el-form-item>
  45 +
  46 + <el-form-item :label="$t('meterWater.preDegrees')" prop="preDegrees">
  47 + <el-input v-model="form.preDegrees" :placeholder="$t('meterWater.inputPreDegrees')" />
  48 + </el-form-item>
  49 +
  50 + <el-form-item :label="$t('meterWater.curDegrees')" prop="curDegrees">
  51 + <el-input v-model="form.curDegrees" :placeholder="$t('meterWater.inputCurDegrees')"
  52 + @change="handleDegreesChange" />
  53 + </el-form-item>
  54 +
  55 + <el-form-item :label="$t('meterWater.preReadingTime')" prop="preReadingTime">
  56 + <el-date-picker v-model="form.preReadingTime" type="datetime"
  57 + :placeholder="$t('meterWater.selectPreReadingTime')" value-format="yyyy-MM-dd HH:mm:ss" style="width: 100%" />
  58 + </el-form-item>
  59 +
  60 + <el-form-item :label="$t('meterWater.curReadingTime')" prop="curReadingTime">
  61 + <el-date-picker v-model="form.curReadingTime" type="datetime"
  62 + :placeholder="$t('meterWater.selectCurReadingTime')" value-format="yyyy-MM-dd HH:mm:ss" style="width: 100%" />
  63 + </el-form-item>
  64 +
  65 + <el-form-item v-if="form.computingFormula === '9009'" :label="$t('meterWater.price')" prop="price">
  66 + <el-input v-model="form.price" :placeholder="$t('meterWater.inputPrice')" />
  67 + </el-form-item>
  68 +
  69 + <el-form-item :label="$t('meterWater.remark')" prop="remark">
  70 + <el-input v-model="form.remark" type="textarea" :placeholder="$t('meterWater.inputRemark')" :rows="2" />
  71 + </el-form-item>
  72 +
  73 + <el-form-item>
  74 + <div class="form-tip">
  75 + {{ $t('meterWater.unitTip') }}
  76 + </div>
  77 + </el-form-item>
  78 + </el-form>
  79 +
  80 + <span slot="footer" class="dialog-footer">
  81 + <el-button @click="dialogVisible = false">{{ $t('common.cancel') }}</el-button>
  82 + <el-button type="primary" @click="handleSubmit">{{ $t('common.save') }}</el-button>
  83 + </span>
  84 + </el-dialog>
  85 +</template>
  86 +
  87 +<script>
  88 +import { saveMeterWater, queryPreMeterWater } from '@/api/fee/meterWaterManageApi'
  89 +import { listFeeConfigs, listMeterTypes } from '@/api/fee/meterWaterManageApi'
  90 +import { getDict } from '@/api/community/communityApi'
  91 +import FloorSelect2 from '@/components/fee/floorSelect2'
  92 +import UnitSelect2 from '@/components/fee/unitSelect2'
  93 +import RoomSelect2 from '@/components/fee/roomSelect2'
  94 +import { getCommunityId } from '@/api/community/communityApi'
  95 +
  96 +export default {
  97 + name: 'AddMeterWater',
  98 + components: {
  99 + FloorSelect2,
  100 + UnitSelect2,
  101 + RoomSelect2
  102 + },
  103 + data() {
  104 + return {
  105 + dialogVisible: false,
  106 + form: {
  107 + waterId: '',
  108 + meterType: '',
  109 + preDegrees: '',
  110 + curDegrees: '',
  111 + preReadingTime: '',
  112 + curReadingTime: '',
  113 + remark: '',
  114 + roomId: '',
  115 + objId: '',
  116 + objName: '',
  117 + feeTypeCd: '',
  118 + feeConfigs: [],
  119 + configId: '',
  120 + objType: '3333',
  121 + hasRoom: false,
  122 + ownerName: '',
  123 + meterTypes: [],
  124 + computingFormula: '',
  125 + price: 0,
  126 + communityId: ''
  127 + },
  128 + rules: {
  129 + feeTypeCd: [
  130 + { required: true, message: this.$t('meterWater.feeTypeRequired'), trigger: 'blur' }
  131 + ],
  132 + configId: [
  133 + { required: true, message: this.$t('meterWater.feeItemRequired'), trigger: 'blur' }
  134 + ],
  135 + meterType: [
  136 + { required: true, message: this.$t('meterWater.meterTypeRequired'), trigger: 'blur' }
  137 + ],
  138 + preDegrees: [
  139 + { required: true, message: this.$t('meterWater.preDegreesRequired'), trigger: 'blur' },
  140 + { pattern: /^\d+(\.\d{1,2})?$/, message: this.$t('meterWater.degreesFormatError') }
  141 + ],
  142 + curDegrees: [
  143 + { required: true, message: this.$t('meterWater.curDegreesRequired'), trigger: 'blur' },
  144 + { pattern: /^\d+(\.\d{1,2})?$/, message: this.$t('meterWater.degreesFormatError') }
  145 + ],
  146 + preReadingTime: [
  147 + { required: true, message: this.$t('meterWater.preReadingTimeRequired'), trigger: 'blur' }
  148 + ],
  149 + curReadingTime: [
  150 + { required: true, message: this.$t('meterWater.curReadingTimeRequired'), trigger: 'blur' }
  151 + ],
  152 + remark: [
  153 + { max: 500, message: this.$t('meterWater.remarkMaxLength'), trigger: 'blur' }
  154 + ]
  155 + },
  156 + feeTypeOptions: [],
  157 + feeConfigs: [],
  158 + meterTypes: []
  159 + }
  160 + },
  161 + created() {
  162 + this.form.communityId = getCommunityId()
  163 + this.loadFeeTypes()
  164 + this.loadMeterTypes()
  165 + },
  166 + methods: {
  167 + open(params) {
  168 + this.resetForm()
  169 + if (params) {
  170 + this.form.hasRoom = Object.prototype.hasOwnProperty.call(params, 'roomId')
  171 + if (params.roomId) {
  172 + this.form.roomId = params.roomId
  173 + this.form.objId = params.roomId
  174 + this.form.objName = params.roomName
  175 + this.form.ownerName = params.ownerName
  176 + ? `${params.roomName}(${params.ownerName})`
  177 + : params.roomName
  178 + }
  179 + }
  180 + this.dialogVisible = true
  181 + },
  182 + resetForm() {
  183 + this.form = {
  184 + waterId: '',
  185 + meterType: '',
  186 + preDegrees: '',
  187 + curDegrees: '',
  188 + preReadingTime: '',
  189 + curReadingTime: '',
  190 + remark: '',
  191 + roomId: '',
  192 + objId: '',
  193 + objName: '',
  194 + feeTypeCd: '',
  195 + feeConfigs: [],
  196 + configId: '',
  197 + objType: '3333',
  198 + hasRoom: false,
  199 + ownerName: '',
  200 + meterTypes: [],
  201 + computingFormula: '',
  202 + price: 0,
  203 + communityId: getCommunityId()
  204 + }
  205 + this.$refs.form && this.$refs.form.resetFields()
  206 + },
  207 + handleClose() {
  208 + this.resetForm()
  209 + },
  210 + async loadFeeTypes() {
  211 + try {
  212 + const data = await getDict('pay_fee_config', 'fee_type_cd')
  213 + this.feeTypeOptions = data.map(item => ({
  214 + value: item.value,
  215 + label: item.label
  216 + }))
  217 + } catch (error) {
  218 + console.error('Failed to load fee types:', error)
  219 + }
  220 + },
  221 + async loadMeterTypes() {
  222 + try {
  223 + const { data } = await listMeterTypes({
  224 + communityId: this.form.communityId,
  225 + page: 1,
  226 + row: 50
  227 + })
  228 + this.meterTypes = data
  229 + } catch (error) {
  230 + console.error('Failed to load meter types:', error)
  231 + }
  232 + },
  233 + async handleFeeTypeChange(feeTypeCd) {
  234 + try {
  235 + const { data } = await listFeeConfigs({
  236 + communityId: this.form.communityId,
  237 + feeTypeCd,
  238 + isDefault: 'F',
  239 + valid: '1',
  240 + page: 1,
  241 + row: 20
  242 + })
  243 + this.feeConfigs = data
  244 + this.form.configId = ''
  245 + } catch (error) {
  246 + console.error('Failed to load fee configs:', error)
  247 + }
  248 + },
  249 + handleConfigChange(configId) {
  250 + const config = this.feeConfigs.find(item => item.configId === configId)
  251 + if (config) {
  252 + this.form.computingFormula = config.computingFormula
  253 + }
  254 + if (this.form.roomId) {
  255 + this.queryPreMeterWater()
  256 + }
  257 + },
  258 + handleMeterTypeChange() {
  259 + if (this.form.roomId) {
  260 + this.queryPreMeterWater()
  261 + }
  262 + },
  263 + async queryPreMeterWater() {
  264 + try {
  265 + const { data } = await queryPreMeterWater({
  266 + communityId: this.form.communityId,
  267 + objId: this.form.roomId,
  268 + objType: this.form.objType,
  269 + meterType: this.form.meterType
  270 + })
  271 +
  272 + if (data && data.length > 0) {
  273 + this.form.preDegrees = data[0].curDegrees
  274 + this.form.preReadingTime = data[0].curReadingTime
  275 + if (this.form.computingFormula === '9009') {
  276 + this.form.price = data[0].price
  277 + }
  278 + } else {
  279 + this.form.preDegrees = '0'
  280 + this.form.preReadingTime = new Date().toISOString().slice(0, 19).replace('T', ' ')
  281 + }
  282 + } catch (error) {
  283 + console.error('Failed to query pre meter water:', error)
  284 + }
  285 + },
  286 + handleDegreesChange() {
  287 + const pre = parseFloat(this.form.preDegrees) || 0
  288 + const cur = parseFloat(this.form.curDegrees) || 0
  289 + if (pre > cur) {
  290 + this.$message.warning(this.$t('meterWater.degreesCompareError'))
  291 + this.form.curDegrees = ''
  292 + }
  293 + },
  294 + handleFloorChange(floor) {
  295 + this.form.floorId = floor.floorId
  296 + this.form.floorNum = floor.floorNum
  297 + },
  298 + handleUnitChange(unit) {
  299 + this.form.unitId = unit.unitId
  300 + this.form.unitNum = unit.unitNum
  301 + },
  302 + handleRoomChange(room) {
  303 + this.form.roomId = room.roomId
  304 + this.form.objId = room.roomId
  305 + this.form.objName = room.name
  306 + this.form.ownerName = room.link
  307 + this.queryPreMeterWater()
  308 + },
  309 + handleSubmit() {
  310 + this.$refs.form.validate(async valid => {
  311 + if (!valid) return
  312 +
  313 + try {
  314 + await saveMeterWater(this.form)
  315 + this.$message.success(this.$t('common.saveSuccess'))
  316 + this.dialogVisible = false
  317 + this.$emit('success')
  318 + } catch (error) {
  319 + console.error('Failed to save meter water:', error)
  320 + this.$message.error(error.message || this.$t('common.saveFailed'))
  321 + }
  322 + })
  323 + }
  324 + }
  325 +}
  326 +</script>
  327 +
  328 +<style lang="scss" scoped>
  329 +.form-tip {
  330 + font-size: 12px;
  331 + color: #999;
  332 + margin-top: 5px;
  333 +}
  334 +</style>
0 \ No newline at end of file 335 \ No newline at end of file
src/components/fee/contractCreateFeeAdd.vue 0 → 100644
  1 +<template>
  2 + <el-dialog :title="$t('contractCreateFeeAdd.title')" :visible.sync="visible" width="800px" @close="handleClose">
  3 + <el-form ref="form" :model="formData" :rules="rules" label-width="120px" label-position="right">
  4 + <el-form-item :label="$t('contractCreateFeeAdd.feeTypeCd')" prop="feeTypeCd">
  5 + <el-select v-model="formData.feeTypeCd" :placeholder="$t('contractCreateFeeAdd.selectFeeType')"
  6 + style="width: 100%" @change="handleFeeTypeChange">
  7 + <el-option v-for="item in feeTypeOptions" :key="item.statusCd" :label="item.name" :value="item.statusCd"
  8 + :disabled="item.statusCd === '888800010008' ||
  9 + item.statusCd === '888800010017'
  10 + " />
  11 + </el-select>
  12 + </el-form-item>
  13 +
  14 + <el-form-item :label="$t('contractCreateFeeAdd.configId')" prop="configId">
  15 + <el-select v-model="formData.configId" :placeholder="$t('contractCreateFeeAdd.selectFeeConfig')"
  16 + style="width: 100%" @change="handleConfigChange">
  17 + <el-option v-for="item in feeConfigOptions" :key="item.configId" :label="item.feeName"
  18 + :value="item.configId" />
  19 + </el-select>
  20 + </el-form-item>
  21 +
  22 + <el-form-item v-if="formData.computingFormula === '4004'" :label="$t('contractCreateFeeAdd.amount')"
  23 + prop="amount">
  24 + <el-input v-model.trim="formData.amount" :placeholder="$t('contractCreateFeeAdd.inputAmount')" />
  25 + </el-form-item>
  26 +
  27 + <el-form-item v-if="isMore" :label="$t('contractCreateFeeAdd.contractState')" prop="contractState">
  28 + <el-checkbox-group v-model="formData.contractState">
  29 + <el-checkbox label="2001">
  30 + {{ $t('contractCreateFeeAdd.state2001') }}
  31 + </el-checkbox>
  32 + <el-checkbox label="2003">
  33 + {{ $t('contractCreateFeeAdd.state2003') }}
  34 + </el-checkbox>
  35 + <el-checkbox label="2005">
  36 + {{ $t('contractCreateFeeAdd.state2005') }}
  37 + </el-checkbox>
  38 + </el-checkbox-group>
  39 + </el-form-item>
  40 +
  41 + <el-form-item :label="$t('contractCreateFeeAdd.startTime')" prop="startTime">
  42 + <el-date-picker v-model="formData.startTime" type="datetime"
  43 + :placeholder="$t('contractCreateFeeAdd.selectStartTime')" value-format="yyyy-MM-dd HH:mm:ss"
  44 + style="width: 100%" @change="validateStartTime" />
  45 + </el-form-item>
  46 +
  47 + <el-form-item :label="$t('contractCreateFeeAdd.endTime')" prop="endTime" :rules="formData.feeFlag === '2006012'
  48 + ? [
  49 + {
  50 + required: true,
  51 + message: $t('contractCreateFeeAdd.endTimeRequired'),
  52 + trigger: 'blur'
  53 + }
  54 + ]
  55 + : []
  56 + ">
  57 + <el-date-picker v-model="formData.endTime" type="datetime"
  58 + :placeholder="$t('contractCreateFeeAdd.selectEndTime')" value-format="yyyy-MM-dd HH:mm:ss" style="width: 100%"
  59 + :disabled="!formData.feeFlag" @change="validateEndTime" />
  60 + </el-form-item>
  61 + </el-form>
  62 +
  63 + <div slot="footer" class="dialog-footer">
  64 + <el-button @click="visible = false">
  65 + {{ $t('common.cancel') }}
  66 + </el-button>
  67 + <el-button type="primary" @click="handleSubmit">
  68 + {{ $t('common.submit') }}
  69 + </el-button>
  70 + </div>
  71 + </el-dialog>
  72 +</template>
  73 +
  74 +<script>
  75 +import { saveContractCreateFee,listFeeConfigs } from '@/api/fee/contractCreateFeeApi'
  76 +import { getCommunityId,getDict } from '@/api/community/communityApi'
  77 +
  78 +export default {
  79 + name: 'ContractCreateFeeAdd',
  80 + data() {
  81 + return {
  82 + visible: false,
  83 + isMore: false,
  84 + formData: {
  85 + feeTypeCds: [],
  86 + feeConfigs: [],
  87 + contractId: '',
  88 + feeTypeCd: '',
  89 + configId: '',
  90 + contractState: ['2001'],
  91 + startTime: '',
  92 + feeFlag: '',
  93 + endTime: '',
  94 + computingFormula: '',
  95 + amount: ''
  96 + },
  97 + feeTypeOptions: [],
  98 + feeConfigOptions: [],
  99 + rules: {
  100 + feeTypeCd: [
  101 + {
  102 + required: true,
  103 + message: this.$t('contractCreateFeeAdd.feeTypeRequired'),
  104 + trigger: 'change'
  105 + }
  106 + ],
  107 + configId: [
  108 + {
  109 + required: true,
  110 + message: this.$t('contractCreateFeeAdd.configRequired'),
  111 + trigger: 'change'
  112 + }
  113 + ],
  114 + startTime: [
  115 + {
  116 + required: true,
  117 + message: this.$t('contractCreateFeeAdd.startTimeRequired'),
  118 + trigger: 'change'
  119 + }
  120 + ],
  121 + amount: [
  122 + {
  123 + required: true,
  124 + message: this.$t('contractCreateFeeAdd.amountRequired'),
  125 + trigger: 'blur'
  126 + }
  127 + ]
  128 + }
  129 + }
  130 + },
  131 + methods: {
  132 + open(contract, isMore) {
  133 + this.isMore = isMore
  134 + if (contract) {
  135 + this.formData.contractId = contract.contractId
  136 + }
  137 + this.visible = true
  138 + this.loadFeeTypes()
  139 + },
  140 + async loadFeeTypes() {
  141 + try {
  142 + const data = await getDict('pay_fee_config', 'fee_type_cd')
  143 + this.feeTypeOptions = data.filter(
  144 + item =>
  145 + item.statusCd !== '888800010015' && item.statusCd !== '888800010016'
  146 + )
  147 + } catch (error) {
  148 + console.error('Failed to load fee types:', error)
  149 + }
  150 + },
  151 + async handleFeeTypeChange(value) {
  152 + this.formData.configId = ''
  153 + try {
  154 + const params = {
  155 + page: 1,
  156 + row: 50,
  157 + communityId: getCommunityId(),
  158 + feeTypeCd: value,
  159 + isDefault: 'F',
  160 + valid: '1'
  161 + }
  162 + const { feeConfigs } = await listFeeConfigs(params)
  163 + this.feeConfigOptions = feeConfigs
  164 + } catch (error) {
  165 + console.error('Failed to load fee configs:', error)
  166 + }
  167 + },
  168 + handleConfigChange(value) {
  169 + this.formData.endTime = ''
  170 + const config = this.feeConfigOptions.find(item => item.configId === value)
  171 + if (config) {
  172 + this.formData.feeFlag = config.feeFlag
  173 + this.formData.computingFormula = config.computingFormula
  174 + }
  175 + },
  176 + validateStartTime(value) {
  177 + if (value && this.formData.endTime) {
  178 + const start = new Date(value).getTime()
  179 + const end = new Date(this.formData.endTime).getTime()
  180 + if (start >= end) {
  181 + this.$message.error(this.$t('contractCreateFeeAdd.timeError'))
  182 + this.formData.startTime = ''
  183 + }
  184 + }
  185 + },
  186 + validateEndTime(value) {
  187 + if (value && this.formData.startTime) {
  188 + const start = new Date(this.formData.startTime).getTime()
  189 + const end = new Date(value).getTime()
  190 + if (start >= end) {
  191 + this.$message.error(this.$t('contractCreateFeeAdd.timeError'))
  192 + this.formData.endTime = ''
  193 + }
  194 + }
  195 + },
  196 + async handleSubmit() {
  197 + try {
  198 + await this.$refs.form.validate()
  199 + const params = {
  200 + ...this.formData,
  201 + communityId: getCommunityId(),
  202 + contractState: this.formData.contractState.join(',')
  203 + }
  204 + const { data } = await saveContractCreateFee(params)
  205 + this.$message.success(
  206 + this.$t('contractCreateFeeAdd.success', {
  207 + total: data.totalRoom,
  208 + success: data.successRoom,
  209 + error: data.errorRoom
  210 + })
  211 + )
  212 + this.$emit('success')
  213 + this.visible = false
  214 + } catch (error) {
  215 + console.error('Submit failed:', error)
  216 + }
  217 + },
  218 + handleClose() {
  219 + this.$refs.form.resetFields()
  220 + this.formData = {
  221 + feeTypeCds: [],
  222 + feeConfigs: [],
  223 + contractId: '',
  224 + feeTypeCd: '',
  225 + configId: '',
  226 + contractState: ['2001'],
  227 + startTime: '',
  228 + feeFlag: '',
  229 + endTime: '',
  230 + computingFormula: '',
  231 + amount: ''
  232 + }
  233 + }
  234 + }
  235 +}
  236 +</script>
0 \ No newline at end of file 237 \ No newline at end of file
src/components/fee/deleteMeterWater.vue 0 → 100644
  1 +<template>
  2 + <el-dialog
  3 + :title="$t('meterWater.confirmOperation')"
  4 + :visible.sync="dialogVisible"
  5 + width="30%"
  6 + @close="handleClose"
  7 + >
  8 + <div class="confirm-content">
  9 + <p>{{ $t('meterWater.confirmDeleteMeterReading') }}</p>
  10 + </div>
  11 + <span slot="footer" class="dialog-footer">
  12 + <el-button @click="dialogVisible = false">{{ $t('common.cancel') }}</el-button>
  13 + <el-button type="primary" @click="handleConfirm">{{ $t('common.confirm') }}</el-button>
  14 + </span>
  15 + </el-dialog>
  16 +</template>
  17 +
  18 +<script>
  19 +import { deleteMeterWater } from '@/api/fee/meterWaterManageApi'
  20 +import { getCommunityId } from '@/api/community/communityApi'
  21 +
  22 +export default {
  23 + name: 'DeleteMeterWater',
  24 + data() {
  25 + return {
  26 + dialogVisible: false,
  27 + waterId: '',
  28 + communityId: ''
  29 + }
  30 + },
  31 + created() {
  32 + this.communityId = getCommunityId()
  33 + },
  34 + methods: {
  35 + open(data) {
  36 + this.waterId = data.waterId
  37 + this.dialogVisible = true
  38 + },
  39 + handleClose() {
  40 + this.waterId = ''
  41 + },
  42 + async handleConfirm() {
  43 + try {
  44 + await deleteMeterWater({
  45 + waterId: this.waterId,
  46 + communityId: this.communityId
  47 + })
  48 + this.$message.success(this.$t('common.deleteSuccess'))
  49 + this.dialogVisible = false
  50 + this.$emit('success')
  51 + } catch (error) {
  52 + console.error('Failed to delete meter water:', error)
  53 + this.$message.error(error.message || this.$t('common.deleteFailed'))
  54 + }
  55 + }
  56 + }
  57 +}
  58 +</script>
  59 +
  60 +<style scoped>
  61 +.confirm-content {
  62 + text-align: center;
  63 + font-size: 16px;
  64 + padding: 20px 0;
  65 +}
  66 +</style>
0 \ No newline at end of file 67 \ No newline at end of file
src/components/fee/editMeterWater.vue 0 → 100644
  1 +<template>
  2 + <el-dialog
  3 + :title="$t('meterWater.editMeterReading')"
  4 + :visible.sync="dialogVisible"
  5 + width="50%"
  6 + @close="handleClose"
  7 + >
  8 + <el-form
  9 + ref="form"
  10 + :model="form"
  11 + :rules="rules"
  12 + label-width="120px"
  13 + label-position="right"
  14 + >
  15 + <el-form-item :label="$t('meterWater.preDegrees')">
  16 + <el-input
  17 + v-model="form.preDegrees"
  18 + :placeholder="$t('meterWater.inputPreDegrees')"
  19 + disabled
  20 + />
  21 + </el-form-item>
  22 +
  23 + <el-form-item :label="$t('meterWater.curDegrees')" prop="curDegrees">
  24 + <el-input
  25 + v-model="form.curDegrees"
  26 + :placeholder="$t('meterWater.inputCurDegrees')"
  27 + @change="handleDegreesChange"
  28 + />
  29 + </el-form-item>
  30 +
  31 + <el-form-item :label="$t('meterWater.preReadingTime')">
  32 + <el-input
  33 + v-model="form.preReadingTime"
  34 + :placeholder="$t('meterWater.selectPreReadingTime')"
  35 + disabled
  36 + />
  37 + </el-form-item>
  38 +
  39 + <el-form-item :label="$t('meterWater.curReadingTime')" prop="curReadingTime">
  40 + <el-date-picker
  41 + v-model="form.curReadingTime"
  42 + type="datetime"
  43 + :placeholder="$t('meterWater.selectCurReadingTime')"
  44 + value-format="yyyy-MM-dd HH:mm:ss"
  45 + style="width: 100%"
  46 + />
  47 + </el-form-item>
  48 +
  49 + <el-form-item :label="$t('meterWater.remark')" prop="remark">
  50 + <el-input
  51 + v-model="form.remark"
  52 + type="textarea"
  53 + :placeholder="$t('meterWater.inputRemark')"
  54 + :rows="2"
  55 + />
  56 + </el-form-item>
  57 + </el-form>
  58 +
  59 + <span slot="footer" class="dialog-footer">
  60 + <el-button @click="dialogVisible = false">{{ $t('common.cancel') }}</el-button>
  61 + <el-button type="primary" @click="handleSubmit">{{ $t('common.save') }}</el-button>
  62 + </span>
  63 + </el-dialog>
  64 +</template>
  65 +
  66 +<script>
  67 +import { updateMeterWater } from '@/api/fee/meterWaterManageApi'
  68 +import { getCommunityId } from '@/api/community/communityApi'
  69 +
  70 +export default {
  71 + name: 'EditMeterWater',
  72 + data() {
  73 + return {
  74 + dialogVisible: false,
  75 + form: {
  76 + waterId: '',
  77 + curDegrees: '',
  78 + curReadingTime: '',
  79 + remark: '',
  80 + communityId: '',
  81 + preDegrees: '',
  82 + preReadingTime: ''
  83 + },
  84 + rules: {
  85 + curDegrees: [
  86 + { required: true, message: this.$t('meterWater.curDegreesRequired'), trigger: 'blur' },
  87 + { pattern: /^\d+(\.\d{1,2})?$/, message: this.$t('meterWater.degreesFormatError') }
  88 + ],
  89 + curReadingTime: [
  90 + { required: true, message: this.$t('meterWater.curReadingTimeRequired'), trigger: 'blur' }
  91 + ],
  92 + remark: [
  93 + { max: 500, message: this.$t('meterWater.remarkMaxLength'), trigger: 'blur' }
  94 + ]
  95 + }
  96 + }
  97 + },
  98 + created() {
  99 + this.form.communityId = getCommunityId()
  100 + },
  101 + methods: {
  102 + open(data) {
  103 + this.form = {
  104 + waterId: data.waterId,
  105 + curDegrees: data.curDegrees,
  106 + curReadingTime: data.curReadingTime,
  107 + remark: data.remark,
  108 + communityId: this.form.communityId,
  109 + preDegrees: data.preDegrees,
  110 + preReadingTime: data.preReadingTime
  111 + }
  112 + this.dialogVisible = true
  113 + },
  114 + handleClose() {
  115 + this.$refs.form.resetFields()
  116 + },
  117 + handleDegreesChange() {
  118 + const pre = parseFloat(this.form.preDegrees) || 0
  119 + const cur = parseFloat(this.form.curDegrees) || 0
  120 + if (pre > cur) {
  121 + this.$message.warning(this.$t('meterWater.degreesCompareError'))
  122 + this.form.curDegrees = ''
  123 + }
  124 + },
  125 + handleSubmit() {
  126 + this.$refs.form.validate(async valid => {
  127 + if (!valid) return
  128 +
  129 + try {
  130 + await updateMeterWater(this.form)
  131 + this.$message.success(this.$t('common.saveSuccess'))
  132 + this.dialogVisible = false
  133 + this.$emit('success')
  134 + } catch (error) {
  135 + console.error('Failed to update meter water:', error)
  136 + this.$message.error(error.message || this.$t('common.saveFailed'))
  137 + }
  138 + })
  139 + }
  140 + }
  141 +}
  142 +</script>
0 \ No newline at end of file 143 \ No newline at end of file
src/components/fee/floorSelect2.vue 0 → 100644
  1 +<template>
  2 + <el-select
  3 + v-model="selectedFloor"
  4 + :placeholder="$t('meterWater.selectBuilding')"
  5 + filterable
  6 + clearable
  7 + style="width: 100%"
  8 + @change="handleChange"
  9 + >
  10 + <el-option
  11 + v-for="item in floors"
  12 + :key="item.floorId"
  13 + :label="`${item.floorNum}栋`"
  14 + :value="item.floorId"
  15 + />
  16 + </el-select>
  17 +</template>
  18 +
  19 +<script>
  20 +import { queryFloors } from '@/api/fee/meterWaterManageApi'
  21 +import { getCommunityId } from '@/api/community/communityApi'
  22 +
  23 +export default {
  24 + name: 'FloorSelect2',
  25 + props: {
  26 + value: {
  27 + type: [String, Number],
  28 + default: ''
  29 + }
  30 + },
  31 + data() {
  32 + return {
  33 + floors: [],
  34 + selectedFloor: this.value,
  35 + communityId: ''
  36 + }
  37 + },
  38 + created() {
  39 + this.communityId = getCommunityId()
  40 + this.loadFloors()
  41 + },
  42 + methods: {
  43 + async loadFloors() {
  44 + try {
  45 + const { data } = await queryFloors({
  46 + communityId: this.communityId,
  47 + page: 1,
  48 + row: 500
  49 + })
  50 + this.floors = data
  51 + } catch (error) {
  52 + console.error('Failed to load floors:', error)
  53 + }
  54 + },
  55 + handleChange(value) {
  56 + const floor = this.floors.find(item => item.floorId === value)
  57 + this.$emit('change', {
  58 + floorId: value,
  59 + floorNum: floor ? floor.floorNum : ''
  60 + })
  61 + }
  62 + },
  63 + watch: {
  64 + value(newVal) {
  65 + this.selectedFloor = newVal
  66 + }
  67 + }
  68 +}
  69 +</script>
0 \ No newline at end of file 70 \ No newline at end of file
src/components/fee/importMeterWaterFee.vue 0 → 100644
  1 +<template>
  2 + <el-dialog
  3 + :title="$t('meterWater.meterReadingImport')"
  4 + :visible.sync="dialogVisible"
  5 + width="50%"
  6 + @close="handleClose"
  7 + >
  8 + <el-form
  9 + ref="form"
  10 + :model="form"
  11 + :rules="rules"
  12 + label-width="120px"
  13 + label-position="right"
  14 + >
  15 + <el-form-item :label="$t('meterWater.feeType')" prop="feeTypeCd">
  16 + <el-select
  17 + v-model="form.feeTypeCd"
  18 + :placeholder="$t('meterWater.selectFeeType')"
  19 + style="width: 100%"
  20 + @change="handleFeeTypeChange"
  21 + >
  22 + <el-option
  23 + v-for="item in feeTypeOptions"
  24 + :key="item.value"
  25 + :label="item.label"
  26 + :value="item.value"
  27 + />
  28 + </el-select>
  29 + </el-form-item>
  30 +
  31 + <el-form-item :label="$t('meterWater.feeItem')" prop="configId">
  32 + <el-select
  33 + v-model="form.configId"
  34 + :placeholder="$t('meterWater.selectFeeItem')"
  35 + style="width: 100%"
  36 + >
  37 + <el-option
  38 + v-for="item in feeConfigs"
  39 + :key="item.configId"
  40 + :label="item.feeName"
  41 + :value="item.configId"
  42 + />
  43 + </el-select>
  44 + </el-form-item>
  45 +
  46 + <el-form-item :label="$t('meterWater.meterType')" prop="meterType">
  47 + <el-select
  48 + v-model="form.meterType"
  49 + :placeholder="$t('meterWater.selectMeterType')"
  50 + style="width: 100%"
  51 + >
  52 + <el-option
  53 + v-for="item in meterTypes"
  54 + :key="item.typeId"
  55 + :label="item.typeName"
  56 + :value="item.typeId"
  57 + />
  58 + </el-select>
  59 + </el-form-item>
  60 +
  61 + <el-form-item :label="$t('meterWater.selectFile')" prop="file">
  62 + <el-upload
  63 + ref="upload"
  64 + :auto-upload="false"
  65 + :limit="1"
  66 + :on-change="handleFileChange"
  67 + :file-list="fileList"
  68 + accept=".xls,.xlsx"
  69 + >
  70 + <el-button size="small" type="primary">{{ $t('meterWater.selectFile') }}</el-button>
  71 + <div slot="tip" class="el-upload__tip">
  72 + {{ fileList.length > 0 ? fileList[0].name : $t('meterWater.fileRequired') }}
  73 + </div>
  74 + </el-upload>
  75 + </el-form-item>
  76 +
  77 + <el-form-item :label="$t('meterWater.downloadTemplate')">
  78 + <div>
  79 + {{ $t('meterWater.downloadTip') }}
  80 + <el-link type="primary" @click="handleDownloadTemplate">
  81 + {{ $t('meterWater.importTemplate') }}
  82 + </el-link>
  83 + {{ $t('meterWater.prepareData') }}
  84 + </div>
  85 + </el-form-item>
  86 + </el-form>
  87 +
  88 + <span slot="footer" class="dialog-footer">
  89 + <el-button @click="dialogVisible = false">{{ $t('common.cancel') }}</el-button>
  90 + <el-button type="primary" @click="handleImport">{{ $t('common.import') }}</el-button>
  91 + </span>
  92 + </el-dialog>
  93 +</template>
  94 +
  95 +<script>
  96 +import { importMeterWaterData, exportMeterWaterTemplate } from '@/api/fee/meterWaterManageApi'
  97 +import { listFeeConfigs, listMeterTypes } from '@/api/fee/meterWaterManageApi'
  98 +import { getDict } from '@/api/community/communityApi'
  99 +import { getCommunityId } from '@/api/community/communityApi'
  100 +
  101 +export default {
  102 + name: 'ImportMeterWaterFee',
  103 + data() {
  104 + return {
  105 + dialogVisible: false,
  106 + form: {
  107 + feeTypeCd: '',
  108 + configId: '',
  109 + meterType: '',
  110 + file: null,
  111 + communityId: ''
  112 + },
  113 + rules: {
  114 + feeTypeCd: [
  115 + { required: true, message: this.$t('meterWater.feeTypeRequired'), trigger: 'blur' }
  116 + ],
  117 + configId: [
  118 + { required: true, message: this.$t('meterWater.feeItemRequired'), trigger: 'blur' }
  119 + ],
  120 + meterType: [
  121 + { required: true, message: this.$t('meterWater.meterTypeRequired'), trigger: 'blur' }
  122 + ],
  123 + file: [
  124 + { required: true, message: this.$t('meterWater.fileRequired'), trigger: 'blur' }
  125 + ]
  126 + },
  127 + feeTypeOptions: [],
  128 + feeConfigs: [],
  129 + meterTypes: [],
  130 + fileList: []
  131 + }
  132 + },
  133 + created() {
  134 + this.form.communityId = getCommunityId()
  135 + this.loadFeeTypes()
  136 + this.loadMeterTypes()
  137 + },
  138 + methods: {
  139 + open() {
  140 + this.dialogVisible = true
  141 + },
  142 + handleClose() {
  143 + this.$refs.form.resetFields()
  144 + this.fileList = []
  145 + },
  146 + async loadFeeTypes() {
  147 + try {
  148 + const data = await getDict('pay_fee_config', 'fee_type_cd')
  149 + this.feeTypeOptions = data.map(item => ({
  150 + value: item.value,
  151 + label: item.label
  152 + }))
  153 + } catch (error) {
  154 + console.error('Failed to load fee types:', error)
  155 + }
  156 + },
  157 + async loadMeterTypes() {
  158 + try {
  159 + const { data } = await listMeterTypes({
  160 + communityId: this.form.communityId,
  161 + page: 1,
  162 + row: 50
  163 + })
  164 + this.meterTypes = data
  165 + } catch (error) {
  166 + console.error('Failed to load meter types:', error)
  167 + }
  168 + },
  169 + async handleFeeTypeChange(feeTypeCd) {
  170 + try {
  171 + const { data } = await listFeeConfigs({
  172 + communityId: this.form.communityId,
  173 + feeTypeCd,
  174 + isDefault: 'F',
  175 + valid: '1',
  176 + page: 1,
  177 + row: 20
  178 + })
  179 + this.feeConfigs = data
  180 + this.form.configId = ''
  181 + } catch (error) {
  182 + console.error('Failed to load fee configs:', error)
  183 + }
  184 + },
  185 + handleFileChange(file) {
  186 + this.form.file = file.raw
  187 + this.fileList = [file]
  188 + },
  189 + async handleDownloadTemplate() {
  190 + if (!this.form.meterType) {
  191 + this.$message.warning(this.$t('meterWater.selectMeterTypeFirst'))
  192 + return
  193 + }
  194 +
  195 + try {
  196 + await exportMeterWaterTemplate({
  197 + communityId: this.form.communityId,
  198 + meterType: this.form.meterType
  199 + })
  200 + this.$message.success(this.$t('meterWater.downloadStarted'))
  201 + } catch (error) {
  202 + console.error('Failed to download template:', error)
  203 + this.$message.error(error.message || this.$t('common.downloadFailed'))
  204 + }
  205 + },
  206 + async handleImport() {
  207 + this.$refs.form.validate(async valid => {
  208 + if (!valid) return
  209 +
  210 + if (!this.form.file) {
  211 + this.$message.warning(this.$t('meterWater.fileRequired'))
  212 + return
  213 + }
  214 +
  215 + const formData = new FormData()
  216 + formData.append('uploadFile', this.form.file)
  217 + formData.append('communityId', this.form.communityId)
  218 + formData.append('feeTypeCd', this.form.feeTypeCd)
  219 + formData.append('configId', this.form.configId)
  220 + formData.append('meterType', this.form.meterType)
  221 + formData.append('importAdapt', 'importMeterWaterFee')
  222 +
  223 + try {
  224 + const { data } = await importMeterWaterData(formData)
  225 + this.$message.success(this.$t('meterWater.importSuccess'))
  226 + this.dialogVisible = false
  227 + this.$emit('success', data.logId)
  228 + } catch (error) {
  229 + console.error('Failed to import meter water data:', error)
  230 + this.$message.error(error.message || this.$t('common.importFailed'))
  231 + }
  232 + })
  233 + }
  234 + }
  235 +}
  236 +</script>
0 \ No newline at end of file 237 \ No newline at end of file
src/components/fee/importMeterWaterFee2.vue 0 → 100644
  1 +<template>
  2 + <el-dialog
  3 + :title="$t('meterWater.meterReadingImport2')"
  4 + :visible.sync="dialogVisible"
  5 + width="50%"
  6 + @close="handleClose"
  7 + >
  8 + <el-form
  9 + ref="form"
  10 + :model="form"
  11 + :rules="rules"
  12 + label-width="120px"
  13 + label-position="right"
  14 + >
  15 + <el-form-item :label="$t('meterWater.feeType')" prop="feeTypeCd">
  16 + <el-select
  17 + v-model="form.feeTypeCd"
  18 + :placeholder="$t('meterWater.selectFeeType')"
  19 + style="width: 100%"
  20 + @change="handleFeeTypeChange"
  21 + >
  22 + <el-option
  23 + v-for="item in feeTypeOptions"
  24 + :key="item.value"
  25 + :label="item.label"
  26 + :value="item.value"
  27 + />
  28 + </el-select>
  29 + </el-form-item>
  30 +
  31 + <el-form-item :label="$t('meterWater.feeItem')" prop="configId">
  32 + <el-select
  33 + v-model="form.configId"
  34 + :placeholder="$t('meterWater.selectFeeItem')"
  35 + style="width: 100%"
  36 + >
  37 + <el-option
  38 + v-for="item in feeConfigs"
  39 + :key="item.configId"
  40 + :label="item.feeName"
  41 + :value="item.configId"
  42 + />
  43 + </el-select>
  44 + </el-form-item>
  45 +
  46 + <el-form-item :label="$t('meterWater.meterType')" prop="meterType">
  47 + <el-select
  48 + v-model="form.meterType"
  49 + :placeholder="$t('meterWater.selectMeterType')"
  50 + style="width: 100%"
  51 + >
  52 + <el-option
  53 + v-for="item in meterTypes"
  54 + :key="item.typeId"
  55 + :label="item.typeName"
  56 + :value="item.typeId"
  57 + />
  58 + </el-select>
  59 + </el-form-item>
  60 +
  61 + <el-form-item :label="$t('meterWater.selectFile')" prop="file">
  62 + <el-upload
  63 + ref="upload"
  64 + :auto-upload="false"
  65 + :limit="1"
  66 + :on-change="handleFileChange"
  67 + :file-list="fileList"
  68 + accept=".xls,.xlsx"
  69 + >
  70 + <el-button size="small" type="primary">{{ $t('meterWater.selectFile') }}</el-button>
  71 + <div slot="tip" class="el-upload__tip">
  72 + {{ fileList.length > 0 ? fileList[0].name : $t('meterWater.fileRequired') }}
  73 + </div>
  74 + </el-upload>
  75 + </el-form-item>
  76 +
  77 + <el-form-item :label="$t('meterWater.downloadTemplate')">
  78 + <div>
  79 + {{ $t('meterWater.downloadTip') }}
  80 + <el-link type="primary" @click="handleDownloadTemplate">
  81 + {{ $t('meterWater.importTemplate') }}
  82 + </el-link>
  83 + {{ $t('meterWater.prepareData') }}
  84 + {{ $t('meterWater.dynamicFeeTip') }}
  85 + </div>
  86 + </el-form-item>
  87 + </el-form>
  88 +
  89 + <span slot="footer" class="dialog-footer">
  90 + <el-button @click="dialogVisible = false">{{ $t('common.cancel') }}</el-button>
  91 + <el-button type="primary" @click="handleImport">{{ $t('common.import') }}</el-button>
  92 + </span>
  93 + </el-dialog>
  94 +</template>
  95 +
  96 +<script>
  97 +import { importMeterWaterData, exportMeterWaterTemplate } from '@/api/fee/meterWaterManageApi'
  98 +import { listFeeConfigs, listMeterTypes } from '@/api/fee/meterWaterManageApi'
  99 +import { getDict } from '@/api/community/communityApi'
  100 +import { getCommunityId } from '@/api/community/communityApi'
  101 +
  102 +export default {
  103 + name: 'ImportMeterWaterFee2',
  104 + data() {
  105 + return {
  106 + dialogVisible: false,
  107 + form: {
  108 + feeTypeCd: '',
  109 + configId: '',
  110 + meterType: '',
  111 + file: null,
  112 + communityId: ''
  113 + },
  114 + rules: {
  115 + feeTypeCd: [
  116 + { required: true, message: this.$t('meterWater.feeTypeRequired'), trigger: 'blur' }
  117 + ],
  118 + configId: [
  119 + { required: true, message: this.$t('meterWater.feeItemRequired'), trigger: 'blur' }
  120 + ],
  121 + meterType: [
  122 + { required: true, message: this.$t('meterWater.meterTypeRequired'), trigger: 'blur' }
  123 + ],
  124 + file: [
  125 + { required: true, message: this.$t('meterWater.fileRequired'), trigger: 'blur' }
  126 + ]
  127 + },
  128 + feeTypeOptions: [],
  129 + feeConfigs: [],
  130 + meterTypes: [],
  131 + fileList: []
  132 + }
  133 + },
  134 + created() {
  135 + this.form.communityId = getCommunityId()
  136 + this.loadFeeTypes()
  137 + this.loadMeterTypes()
  138 + },
  139 + methods: {
  140 + open() {
  141 + this.dialogVisible = true
  142 + },
  143 + handleClose() {
  144 + this.$refs.form.resetFields()
  145 + this.fileList = []
  146 + },
  147 + async loadFeeTypes() {
  148 + try {
  149 + const data = await getDict('pay_fee_config', 'fee_type_cd')
  150 + this.feeTypeOptions = data.map(item => ({
  151 + value: item.value,
  152 + label: item.label
  153 + }))
  154 + } catch (error) {
  155 + console.error('Failed to load fee types:', error)
  156 + }
  157 + },
  158 + async loadMeterTypes() {
  159 + try {
  160 + const { data } = await listMeterTypes({
  161 + communityId: this.form.communityId,
  162 + page: 1,
  163 + row: 50
  164 + })
  165 + this.meterTypes = data
  166 + } catch (error) {
  167 + console.error('Failed to load meter types:', error)
  168 + }
  169 + },
  170 + async handleFeeTypeChange(feeTypeCd) {
  171 + try {
  172 + const { data } = await listFeeConfigs({
  173 + communityId: this.form.communityId,
  174 + feeTypeCd,
  175 + isDefault: 'F',
  176 + valid: '1',
  177 + page: 1,
  178 + row: 20
  179 + })
  180 + this.feeConfigs = data
  181 + this.form.configId = ''
  182 + } catch (error) {
  183 + console.error('Failed to load fee configs:', error)
  184 + }
  185 + },
  186 + handleFileChange(file) {
  187 + this.form.file = file.raw
  188 + this.fileList = [file]
  189 + },
  190 + async handleDownloadTemplate() {
  191 + if (!this.form.meterType) {
  192 + this.$message.warning(this.$t('meterWater.selectMeterTypeFirst'))
  193 + return
  194 + }
  195 +
  196 + const meterType = this.meterTypes.find(item => item.typeId === this.form.meterType)
  197 + const feeName = meterType ? meterType.typeName : ''
  198 +
  199 + try {
  200 + await exportMeterWaterTemplate({
  201 + communityId: this.form.communityId,
  202 + meterType: this.form.meterType,
  203 + feeName,
  204 + pagePath: 'exportMeterWater2'
  205 + })
  206 + this.$message.success(this.$t('meterWater.downloadStarted'))
  207 + } catch (error) {
  208 + console.error('Failed to download template:', error)
  209 + this.$message.error(error.message || this.$t('common.downloadFailed'))
  210 + }
  211 + },
  212 + async handleImport() {
  213 + this.$refs.form.validate(async valid => {
  214 + if (!valid) return
  215 +
  216 + if (!this.form.file) {
  217 + this.$message.warning(this.$t('meterWater.fileRequired'))
  218 + return
  219 + }
  220 +
  221 + const formData = new FormData()
  222 + formData.append('uploadFile', this.form.file)
  223 + formData.append('communityId', this.form.communityId)
  224 + formData.append('feeTypeCd', this.form.feeTypeCd)
  225 + formData.append('configId', this.form.configId)
  226 + formData.append('meterType', this.form.meterType)
  227 + formData.append('importAdapt', 'importMeterWaterFee')
  228 + formData.append('importMeterDynamic', 'true')
  229 +
  230 + try {
  231 + const { data } = await importMeterWaterData(formData)
  232 + this.$message.success(this.$t('meterWater.importSuccess'))
  233 + this.dialogVisible = false
  234 + this.$emit('success', data.logId)
  235 + } catch (error) {
  236 + console.error('Failed to import meter water data:', error)
  237 + this.$message.error(error.message || this.$t('common.importFailed'))
  238 + }
  239 + })
  240 + }
  241 + }
  242 +}
  243 +</script>
0 \ No newline at end of file 244 \ No newline at end of file
src/components/fee/roomMeterQrcode.vue 0 → 100644
  1 +<template>
  2 + <el-dialog
  3 + :title="$t('meterWater.meterQRCode')"
  4 + :visible.sync="dialogVisible"
  5 + width="50%"
  6 + @close="handleClose"
  7 + >
  8 + <el-form
  9 + ref="form"
  10 + :model="form"
  11 + :rules="rules"
  12 + label-width="120px"
  13 + label-position="right"
  14 + >
  15 + <el-form-item :label="$t('meterWater.feeType')" prop="feeTypeCd">
  16 + <el-select
  17 + v-model="form.feeTypeCd"
  18 + :placeholder="$t('meterWater.selectFeeType')"
  19 + style="width: 100%"
  20 + @change="handleFeeTypeChange"
  21 + >
  22 + <el-option
  23 + v-for="item in feeTypeOptions"
  24 + :key="item.value"
  25 + :label="item.label"
  26 + :value="item.value"
  27 + />
  28 + </el-select>
  29 + </el-form-item>
  30 +
  31 + <el-form-item :label="$t('meterWater.feeItem')" prop="configId">
  32 + <el-select
  33 + v-model="form.configId"
  34 + :placeholder="$t('meterWater.selectFeeItem')"
  35 + style="width: 100%"
  36 + @change="generateQRCode"
  37 + >
  38 + <el-option
  39 + v-for="item in feeConfigs"
  40 + :key="item.configId"
  41 + :label="item.feeName"
  42 + :value="item.configId"
  43 + />
  44 + </el-select>
  45 + <div class="form-tip">
  46 + {{ $t('meterWater.feeItemTip') }}
  47 + </div>
  48 + </el-form-item>
  49 +
  50 + <el-form-item :label="$t('meterWater.meterType')" prop="meterType">
  51 + <el-select
  52 + v-model="form.meterType"
  53 + :placeholder="$t('meterWater.selectMeterType')"
  54 + style="width: 100%"
  55 + @change="generateQRCode"
  56 + >
  57 + <el-option
  58 + v-for="item in meterTypes"
  59 + :key="item.typeId"
  60 + :label="item.typeName"
  61 + :value="item.typeId"
  62 + />
  63 + </el-select>
  64 + </el-form-item>
  65 +
  66 + <el-form-item :label="$t('meterWater.room')">
  67 + <el-input
  68 + v-model="form.ownerName"
  69 + :placeholder="$t('meterWater.inputRoom')"
  70 + disabled
  71 + />
  72 + </el-form-item>
  73 +
  74 + <el-form-item v-if="showQRCode" :label="$t('meterWater.qrCode')">
  75 + <div class="qr-code-container">
  76 + <div id="qrCodeCanvas" class="qr-code-canvas"></div>
  77 + <div class="qr-code-title">
  78 + {{ form.ownerName }}-{{ form.typeName }}
  79 + </div>
  80 + <div class="qr-code-tip">
  81 + {{ $t('meterWater.qrCodeTip') }}
  82 + </div>
  83 + </div>
  84 + </el-form-item>
  85 + </el-form>
  86 +
  87 + <span slot="footer" class="dialog-footer">
  88 + <el-button @click="dialogVisible = false">{{ $t('common.cancel') }}</el-button>
  89 + </span>
  90 + </el-dialog>
  91 +</template>
  92 +
  93 +<script>
  94 +import QRCode from 'qrcodejs2'
  95 +import { listFeeConfigs, listMeterTypes } from '@/api/fee/meterWaterManageApi'
  96 +import { getDict } from '@/api/community/communityApi'
  97 +import { getCommunityId } from '@/api/community/communityApi'
  98 +
  99 +export default {
  100 + name: 'RoomMeterQrcode',
  101 + data() {
  102 + return {
  103 + dialogVisible: false,
  104 + form: {
  105 + feeTypeCd: '',
  106 + configId: '',
  107 + meterType: '',
  108 + roomId: '',
  109 + ownerName: '',
  110 + typeName: '',
  111 + communityId: ''
  112 + },
  113 + rules: {
  114 + feeTypeCd: [
  115 + { required: true, message: this.$t('meterWater.feeTypeRequired'), trigger: 'blur' }
  116 + ],
  117 + configId: [
  118 + { required: true, message: this.$t('meterWater.feeItemRequired'), trigger: 'blur' }
  119 + ],
  120 + meterType: [
  121 + { required: true, message: this.$t('meterWater.meterTypeRequired'), trigger: 'blur' }
  122 + ]
  123 + },
  124 + feeTypeOptions: [],
  125 + feeConfigs: [],
  126 + meterTypes: [],
  127 + showQRCode: false,
  128 + qrCode: null
  129 + }
  130 + },
  131 + created() {
  132 + this.form.communityId = getCommunityId()
  133 + this.loadFeeTypes()
  134 + this.loadMeterTypes()
  135 + },
  136 + methods: {
  137 + open(params) {
  138 + this.resetForm()
  139 + if (params) {
  140 + this.form.roomId = params.roomId
  141 + this.form.ownerName = params.ownerName
  142 + ? `${params.roomName}(${params.ownerName})`
  143 + : params.roomName
  144 + }
  145 + this.dialogVisible = true
  146 + },
  147 + resetForm() {
  148 + this.form = {
  149 + feeTypeCd: '',
  150 + configId: '',
  151 + meterType: '',
  152 + roomId: '',
  153 + ownerName: '',
  154 + typeName: '',
  155 + communityId: getCommunityId()
  156 + }
  157 + this.showQRCode = false
  158 + this.$refs.form && this.$refs.form.resetFields()
  159 + this.clearQRCode()
  160 + },
  161 + handleClose() {
  162 + this.resetForm()
  163 + },
  164 + clearQRCode() {
  165 + if (this.qrCode) {
  166 + document.getElementById('qrCodeCanvas').innerHTML = ''
  167 + this.qrCode = null
  168 + }
  169 + },
  170 + async loadFeeTypes() {
  171 + try {
  172 + const data = await getDict('pay_fee_config', 'fee_type_cd')
  173 + this.feeTypeOptions = data.map(item => ({
  174 + value: item.value,
  175 + label: item.label
  176 + }))
  177 + } catch (error) {
  178 + console.error('Failed to load fee types:', error)
  179 + }
  180 + },
  181 + async loadMeterTypes() {
  182 + try {
  183 + const { data } = await listMeterTypes({
  184 + communityId: this.form.communityId,
  185 + page: 1,
  186 + row: 50
  187 + })
  188 + this.meterTypes = data
  189 + } catch (error) {
  190 + console.error('Failed to load meter types:', error)
  191 + }
  192 + },
  193 + async handleFeeTypeChange(feeTypeCd) {
  194 + try {
  195 + const { data } = await listFeeConfigs({
  196 + communityId: this.form.communityId,
  197 + feeTypeCd,
  198 + isDefault: 'F',
  199 + valid: '1',
  200 + page: 1,
  201 + row: 20
  202 + })
  203 + this.feeConfigs = data
  204 + this.form.configId = ''
  205 + } catch (error) {
  206 + console.error('Failed to load fee configs:', error)
  207 + }
  208 + },
  209 + generateQRCode() {
  210 + if (!this.form.configId || !this.form.meterType || !this.form.roomId) {
  211 + return
  212 + }
  213 +
  214 + const meterType = this.meterTypes.find(item => item.typeId === this.form.meterType)
  215 + this.form.typeName = meterType ? meterType.typeName : ''
  216 +
  217 + this.clearQRCode()
  218 +
  219 + const propertyUrl = this.$store.getters.systemInfo.propertyUrl
  220 + const qrCodeUrl = `${propertyUrl}pages/meter/qrcodeMeter?configId=${this.form.configId}&meterType=${this.form.meterType}&roomId=${this.form.roomId}&communityId=${this.form.communityId}`
  221 +
  222 + this.qrCode = new QRCode(document.getElementById('qrCodeCanvas'), {
  223 + text: qrCodeUrl,
  224 + width: 200,
  225 + height: 200,
  226 + colorDark: '#000000',
  227 + colorLight: '#ffffff',
  228 + correctLevel: QRCode.CorrectLevel.L
  229 + })
  230 +
  231 + this.showQRCode = true
  232 + }
  233 + }
  234 +}
  235 +</script>
  236 +
  237 +<style lang="scss" scoped>
  238 +.qr-code-container {
  239 + display: flex;
  240 + flex-direction: column;
  241 + align-items: center;
  242 + padding: 20px;
  243 +
  244 + .qr-code-canvas {
  245 + margin-bottom: 10px;
  246 + }
  247 +
  248 + .qr-code-title {
  249 + font-size: 18px;
  250 + font-weight: bold;
  251 + margin-bottom: 10px;
  252 + text-align: center;
  253 + }
  254 +
  255 + .qr-code-tip {
  256 + font-size: 14px;
  257 + color: #666;
  258 + text-align: center;
  259 + max-width: 300px;
  260 + }
  261 +}
  262 +
  263 +.form-tip {
  264 + font-size: 12px;
  265 + color: #999;
  266 + margin-top: 5px;
  267 +}
  268 +</style>
0 \ No newline at end of file 269 \ No newline at end of file
src/components/fee/roomSelect2.vue 0 → 100644
  1 +<template>
  2 + <el-select
  3 + v-model="selectedRoom"
  4 + :placeholder="$t('meterWater.selectRoom')"
  5 + filterable
  6 + clearable
  7 + style="width: 100%"
  8 + :disabled="!unitId"
  9 + @change="handleChange"
  10 + >
  11 + <el-option
  12 + v-for="item in rooms"
  13 + :key="item.roomId"
  14 + :label="getRoomLabel(item)"
  15 + :value="item.roomId"
  16 + />
  17 + </el-select>
  18 +</template>
  19 +
  20 +<script>
  21 +import { queryRooms } from '@/api/fee/meterWaterManageApi'
  22 +import { getCommunityId } from '@/api/community/communityApi'
  23 +
  24 +export default {
  25 + name: 'RoomSelect2',
  26 + props: {
  27 + value: {
  28 + type: [String, Number],
  29 + default: ''
  30 + },
  31 + unitId: {
  32 + type: [String, Number],
  33 + default: ''
  34 + }
  35 + },
  36 + data() {
  37 + return {
  38 + rooms: [],
  39 + selectedRoom: this.value,
  40 + communityId: ''
  41 + }
  42 + },
  43 + created() {
  44 + this.communityId = getCommunityId()
  45 + },
  46 + watch: {
  47 + unitId: {
  48 + immediate: true,
  49 + handler(newVal) {
  50 + if (newVal) {
  51 + this.loadRooms(newVal)
  52 + } else {
  53 + this.rooms = []
  54 + this.selectedRoom = ''
  55 + }
  56 + }
  57 + },
  58 + value(newVal) {
  59 + this.selectedRoom = newVal
  60 + }
  61 + },
  62 + methods: {
  63 + async loadRooms(unitId) {
  64 + try {
  65 + const { data } = await queryRooms({
  66 + unitId,
  67 + communityId: this.communityId,
  68 + page: 1,
  69 + row: 200
  70 + })
  71 + this.rooms = data.rooms || []
  72 + } catch (error) {
  73 + console.error('Failed to load rooms:', error)
  74 + }
  75 + },
  76 + getRoomLabel(room) {
  77 + return room.ownerName
  78 + ? `${room.roomNum}室(${room.ownerName})`
  79 + : `${room.roomNum}室`
  80 + },
  81 + handleChange(value) {
  82 + const room = this.rooms.find(item => item.roomId === value)
  83 + if (room) {
  84 + this.$emit('change', {
  85 + roomId: value,
  86 + name: `${room.floorNum}-${room.unitNum}-${room.roomNum}`,
  87 + link: this.getRoomLabel(room)
  88 + })
  89 + } else {
  90 + this.$emit('change', null)
  91 + }
  92 + }
  93 + }
  94 +}
  95 +</script>
0 \ No newline at end of file 96 \ No newline at end of file
src/components/fee/roomTreeDiv.vue 0 → 100644
  1 +<template>
  2 + <div class="room-tree-container">
  3 + <el-tree ref="tree" :data="treeData" node-key="id" :props="defaultProps" :highlight-current="true"
  4 + :expand-on-click-node="false" @node-click="handleNodeClick">
  5 + <span slot-scope="{ node, data }" class="custom-tree-node">
  6 + <span>
  7 + <i :class="data.icon" style="margin-right: 5px"></i>
  8 + {{ node.label }}
  9 + </span>
  10 + </span>
  11 + </el-tree>
  12 + </div>
  13 +</template>
  14 +
  15 +<script>
  16 +import { queryUnits, queryRoomsTree } from '@/api/fee/meterWaterManageApi'
  17 +import { getCommunityId } from '@/api/community/communityApi'
  18 +
  19 +export default {
  20 + name: 'RoomTreeDiv',
  21 + data() {
  22 + return {
  23 + treeData: [],
  24 + defaultProps: {
  25 + children: 'children',
  26 + label: 'text'
  27 + },
  28 + communityId: ''
  29 + }
  30 + },
  31 + created() {
  32 + this.communityId = getCommunityId()
  33 + this.loadTreeData()
  34 + },
  35 + methods: {
  36 + async loadTreeData() {
  37 + try {
  38 + const units = await queryUnits({
  39 + communityId: this.communityId
  40 + })
  41 + this.buildTreeData(units)
  42 + } catch (error) {
  43 + console.error('Failed to load tree data:', error)
  44 + }
  45 + },
  46 + buildTreeData(units) {
  47 + const treeMap = new Map()
  48 +
  49 + // Build floor nodes
  50 + units.forEach(unit => {
  51 + if (!treeMap.has(unit.floorId)) {
  52 + treeMap.set(unit.floorId, {
  53 + id: `f_${unit.floorId}`,
  54 + floorId: unit.floorId,
  55 + floorNum: unit.floorNum,
  56 + icon: 'el-icon-office-building',
  57 + text: `${unit.floorNum}栋`,
  58 + children: []
  59 + })
  60 + }
  61 + })
  62 +
  63 + // Build unit nodes
  64 + units.forEach(unit => {
  65 + const floorNode = treeMap.get(unit.floorId)
  66 + if (floorNode) {
  67 + floorNode.children.push({
  68 + id: `u_${unit.unitId}`,
  69 + unitId: unit.unitId,
  70 + unitNum: unit.unitNum,
  71 + icon: 'el-icon-connection',
  72 + text: `${unit.unitNum}单元`,
  73 + children: []
  74 + })
  75 + }
  76 + })
  77 +
  78 + this.treeData = Array.from(treeMap.values())
  79 + },
  80 + async handleNodeClick(data) {
  81 + if (data.id.startsWith('u_')) {
  82 + await this.loadRooms(data)
  83 + } else if (data.id.startsWith('r_')) {
  84 + this.$emit('selectRoom', {
  85 + roomId: data.roomId,
  86 + roomName: data.roomName
  87 + })
  88 + }
  89 + },
  90 + async loadRooms(node) {
  91 + try {
  92 + const {rooms} = await queryRoomsTree({
  93 + unitId: node.unitId,
  94 + communityId: this.communityId,
  95 + page: 1,
  96 + row: 1000
  97 + })
  98 +
  99 + const roomNodes = rooms.map(room => ({
  100 + id: `r_${room.roomId}`,
  101 + roomId: room.roomId,
  102 + roomName: `${room.floorNum}-${room.unitNum}-${room.roomNum}`,
  103 + icon: 'el-icon-house',
  104 + text: room.ownerName
  105 + ? `${room.roomNum}室(${room.ownerName})`
  106 + : `${room.roomNum}室`
  107 + }))
  108 +
  109 + // Update tree data
  110 + const floorNode = this.treeData.find(
  111 + item => item.id === `f_${node.floorId}`
  112 + )
  113 + if (floorNode) {
  114 + const unitNode = floorNode.children.find(
  115 + item => item.id === `u_${node.unitId}`
  116 + )
  117 + if (unitNode) {
  118 + this.$set(unitNode, 'children', roomNodes)
  119 + this.$refs.tree.updateKeyChildren(unitNode.id, roomNodes)
  120 +
  121 + // Auto select first room
  122 + if (roomNodes.length > 0) {
  123 + this.$emit('selectRoom', {
  124 + roomId: roomNodes[0].roomId,
  125 + roomName: roomNodes[0].roomName
  126 + })
  127 + }
  128 + }
  129 + }
  130 + } catch (error) {
  131 + console.error('Failed to load rooms:', error)
  132 + }
  133 + }
  134 + }
  135 +}
  136 +</script>
  137 +
  138 +<style lang="scss" scoped>
  139 +.room-tree-container {
  140 + height: 100%;
  141 + overflow-y: auto;
  142 + padding: 10px;
  143 +
  144 + .custom-tree-node {
  145 + flex: 1;
  146 + display: flex;
  147 + align-items: center;
  148 + font-size: 14px;
  149 + padding: 5px 0;
  150 + }
  151 +}
  152 +</style>
0 \ No newline at end of file 153 \ No newline at end of file
src/components/fee/unitSelect2.vue 0 → 100644
  1 +<template>
  2 + <el-select
  3 + v-model="selectedUnit"
  4 + :placeholder="$t('meterWater.selectUnit')"
  5 + filterable
  6 + clearable
  7 + style="width: 100%"
  8 + :disabled="!floorId"
  9 + @change="handleChange"
  10 + >
  11 + <el-option
  12 + v-for="item in units"
  13 + :key="item.unitId"
  14 + :label="`${item.unitNum}单元`"
  15 + :value="item.unitId"
  16 + />
  17 + </el-select>
  18 +</template>
  19 +
  20 +<script>
  21 +import { queryUnits } from '@/api/fee/meterWaterManageApi'
  22 +import { getCommunityId } from '@/api/community/communityApi'
  23 +
  24 +export default {
  25 + name: 'UnitSelect2',
  26 + props: {
  27 + value: {
  28 + type: [String, Number],
  29 + default: ''
  30 + },
  31 + floorId: {
  32 + type: [String, Number],
  33 + default: ''
  34 + }
  35 + },
  36 + data() {
  37 + return {
  38 + units: [],
  39 + selectedUnit: this.value,
  40 + communityId: ''
  41 + }
  42 + },
  43 + created() {
  44 + this.communityId = getCommunityId()
  45 + },
  46 + watch: {
  47 + floorId: {
  48 + immediate: true,
  49 + handler(newVal) {
  50 + if (newVal) {
  51 + this.loadUnits(newVal)
  52 + } else {
  53 + this.units = []
  54 + this.selectedUnit = ''
  55 + }
  56 + }
  57 + },
  58 + value(newVal) {
  59 + this.selectedUnit = newVal
  60 + }
  61 + },
  62 + methods: {
  63 + async loadUnits(floorId) {
  64 + try {
  65 + const { data } = await queryUnits({
  66 + floorId,
  67 + communityId: this.communityId,
  68 + page: 1,
  69 + row: 50
  70 + })
  71 + this.units = data
  72 + } catch (error) {
  73 + console.error('Failed to load units:', error)
  74 + }
  75 + },
  76 + handleChange(value) {
  77 + const unit = this.units.find(item => item.unitId === value)
  78 + this.$emit('change', {
  79 + unitId: value,
  80 + unitNum: unit ? unit.unitNum : ''
  81 + })
  82 + }
  83 + }
  84 +}
  85 +</script>
0 \ No newline at end of file 86 \ No newline at end of file
src/i18n/feeI18n.js 0 → 100644
  1 +import { messages as contractCreateFeeMessages } from '../views/fee/contractCreateFeeLang'
  2 +import { messages as meterWaterManageMessages } from '../views/fee/meterWaterManageLang'
  3 +export const messages = {
  4 + en: {
  5 + ...contractCreateFeeMessages.en,
  6 + ...meterWaterManageMessages.en,
  7 + },
  8 + zh: {
  9 + ...contractCreateFeeMessages.zh,
  10 + ...meterWaterManageMessages.zh,
  11 + }
  12 +}
0 \ No newline at end of file 13 \ No newline at end of file
src/i18n/index.js
@@ -146,6 +146,7 @@ import { messages as userI18n } from &#39;./userI18n&#39; @@ -146,6 +146,7 @@ import { messages as userI18n } from &#39;./userI18n&#39;
146 import { messages as systemI18n } from './systemI18n' 146 import { messages as systemI18n } from './systemI18n'
147 import { messages as communityI18n } from './communityI18n' 147 import { messages as communityI18n } from './communityI18n'
148 import { messages as workI18n } from './workI18n' 148 import { messages as workI18n } from './workI18n'
  149 +import { messages as feeI18n } from './feeI18n'
149 150
150 Vue.use(VueI18n) 151 Vue.use(VueI18n)
151 152
@@ -290,6 +291,7 @@ const messages = { @@ -290,6 +291,7 @@ const messages = {
290 ...systemI18n.en, 291 ...systemI18n.en,
291 ...communityI18n.en, 292 ...communityI18n.en,
292 ...workI18n.en, 293 ...workI18n.en,
  294 + ...feeI18n.en,
293 }, 295 },
294 zh: { 296 zh: {
295 ...loginMessages.zh, 297 ...loginMessages.zh,
@@ -428,6 +430,7 @@ const messages = { @@ -428,6 +430,7 @@ const messages = {
428 ...systemI18n.zh, 430 ...systemI18n.zh,
429 ...communityI18n.zh, 431 ...communityI18n.zh,
430 ...workI18n.zh, 432 ...workI18n.zh,
  433 + ...feeI18n.zh,
431 } 434 }
432 } 435 }
433 436
src/router/feeRouter.js 0 → 100644
  1 +export default [
  2 + {
  3 + path:'/pages/property/contractCreateFee',
  4 + name:'/pages/property/contractCreateFee',
  5 + component: () => import('@/views/fee/contractCreateFeeList.vue')
  6 + },
  7 + {
  8 + path:'/pages/property/meterWaterManage',
  9 + name:'/pages/property/meterWaterManage',
  10 + component: () => import('@/views/fee/meterWaterManageList.vue')
  11 + },
  12 +]
0 \ No newline at end of file 13 \ No newline at end of file
src/router/index.js
@@ -16,6 +16,7 @@ import userRouter from &#39;./userRouter&#39; @@ -16,6 +16,7 @@ import userRouter from &#39;./userRouter&#39;
16 import systemRouter from './systemRouter' 16 import systemRouter from './systemRouter'
17 import communityRouter from './communityRouter' 17 import communityRouter from './communityRouter'
18 import workRouter from './workRouter' 18 import workRouter from './workRouter'
  19 +import feeRouter from './feeRouter'
19 20
20 Vue.use(VueRouter) 21 Vue.use(VueRouter)
21 22
@@ -640,6 +641,7 @@ const routes = [ @@ -640,6 +641,7 @@ const routes = [
640 ...systemRouter, 641 ...systemRouter,
641 ...communityRouter, 642 ...communityRouter,
642 ...workRouter, 643 ...workRouter,
  644 + ...feeRouter,
643 // 其他子路由可以在这里添加 645 // 其他子路由可以在这里添加
644 ] 646 ]
645 }, 647 },
src/views/fee/contractCreateFeeLang.js 0 → 100644
  1 +export const messages = {
  2 + en: {
  3 + contractCreateFee: {
  4 + search: {
  5 + title: 'Search Conditions',
  6 + contractCode: 'Contract Code',
  7 + contractNameLike: 'Contract Name',
  8 + contractType: 'Contract Type'
  9 + },
  10 + list: {
  11 + title: 'Contract Information',
  12 + batchCreate: 'Batch Create'
  13 + },
  14 + table: {
  15 + contractCode: 'Contract Code',
  16 + parentContractCode: 'Parent Contract Code',
  17 + contractName: 'Contract Name',
  18 + contractType: 'Contract Type',
  19 + partyB: 'Party B',
  20 + amount: 'Amount',
  21 + startTime: 'Start Time',
  22 + endTime: 'End Time',
  23 + operation: 'Operation',
  24 + payFee: 'Pay Fee',
  25 + viewDetail: 'Detail',
  26 + viewFee: 'View Fee'
  27 + },
  28 + fetchError: 'Failed to fetch contract data'
  29 + },
  30 + contractCreateFeeAdd: {
  31 + title: 'Create Fee',
  32 + feeTypeCd: 'Fee Type',
  33 + configId: 'Fee Item',
  34 + amount: 'Amount',
  35 + contractState: 'Contract State',
  36 + startTime: 'Start Time',
  37 + endTime: 'End Time',
  38 + selectFeeType: 'Please select fee type',
  39 + selectFeeConfig: 'Please select fee item',
  40 + inputAmount: 'Please input amount',
  41 + selectStartTime: 'Select start time',
  42 + selectEndTime: 'Select end time',
  43 + state2001: 'Pending Review',
  44 + state2003: 'Under Review',
  45 + state2005: 'Review Completed',
  46 + feeTypeRequired: 'Fee type is required',
  47 + configRequired: 'Fee item is required',
  48 + amountRequired: 'Amount is required',
  49 + startTimeRequired: 'Start time is required',
  50 + endTimeRequired: 'End time is required',
  51 + timeError: 'Start time must be earlier than end time',
  52 + success: 'Successfully created fees. Total: {total}, Success: {success}, Failed: {error}',
  53 + note: 'Note: Batch created fees will be displayed in the contract fee view'
  54 + }
  55 + },
  56 + zh: {
  57 + contractCreateFee: {
  58 + search: {
  59 + title: '查询条件',
  60 + contractCode: '合同编号',
  61 + contractNameLike: '合同名称',
  62 + contractType: '合同类型'
  63 + },
  64 + list: {
  65 + title: '合同信息',
  66 + batchCreate: '批量创建'
  67 + },
  68 + table: {
  69 + contractCode: '合同编号',
  70 + parentContractCode: '父合同编号',
  71 + contractName: '合同名称',
  72 + contractType: '合同类型',
  73 + partyB: '乙方',
  74 + amount: '合同金额',
  75 + startTime: '开始时间',
  76 + endTime: '结束时间',
  77 + operation: '操作',
  78 + payFee: '欠费缴费',
  79 + viewDetail: '详情',
  80 + viewFee: '查看费用'
  81 + },
  82 + fetchError: '获取合同数据失败'
  83 + },
  84 + contractCreateFeeAdd: {
  85 + title: '创建费用',
  86 + feeTypeCd: '费用类型',
  87 + configId: '收费项目',
  88 + amount: '收费金额',
  89 + contractState: '合同状态',
  90 + startTime: '计费起始时间',
  91 + endTime: '计费结束时间',
  92 + selectFeeType: '请选择费用类型',
  93 + selectFeeConfig: '请选择收费项目',
  94 + inputAmount: '请填写收费金额',
  95 + selectStartTime: '请填写计费起始时间',
  96 + selectEndTime: '请填写计费结束时间',
  97 + state2001: '待审核',
  98 + state2003: '审核中',
  99 + state2005: '审核完成',
  100 + feeTypeRequired: '费用类型不能为空',
  101 + configRequired: '费用项目不能为空',
  102 + amountRequired: '收费金额不能为空',
  103 + startTimeRequired: '计费起始时间不能为空',
  104 + endTimeRequired: '计费结束时间不能为空',
  105 + timeError: '计费起始时间必须小于计费终止时间',
  106 + success: '创建收费成功,总共[{total}]合同,成功[{success}],失败[{error}]',
  107 + note: '注:批量创建的费用在合同收费的查看费用中显示'
  108 + }
  109 + }
  110 +}
0 \ No newline at end of file 111 \ No newline at end of file
src/views/fee/contractCreateFeeList.vue 0 → 100644
  1 +<template>
  2 + <div class="contract-create-fee-container">
  3 + <!-- 查询条件 -->
  4 + <el-card class="search-wrapper">
  5 + <div slot="header" class="flex justify-between">
  6 + <span>{{ $t('contractCreateFee.search.title') }}</span>
  7 + </div>
  8 + <el-row :gutter="20">
  9 + <el-col :span="6">
  10 + <el-input
  11 + v-model.trim="searchForm.contractCode"
  12 + :placeholder="$t('contractCreateFee.search.contractCode')"
  13 + clearable
  14 + />
  15 + </el-col>
  16 + <el-col :span="6">
  17 + <el-input
  18 + v-model.trim="searchForm.contractNameLike"
  19 + :placeholder="$t('contractCreateFee.search.contractNameLike')"
  20 + clearable
  21 + />
  22 + </el-col>
  23 + <el-col :span="6">
  24 + <el-input
  25 + v-model.trim="searchForm.contractType"
  26 + :placeholder="$t('contractCreateFee.search.contractType')"
  27 + clearable
  28 + />
  29 + </el-col>
  30 + <el-col :span="6">
  31 + <el-button type="primary" @click="handleSearch">
  32 + <i class="el-icon-search"></i>
  33 + {{ $t('common.search') }}
  34 + </el-button>
  35 + <el-button @click="handleReset">
  36 + <i class="el-icon-refresh"></i>
  37 + {{ $t('common.reset') }}
  38 + </el-button>
  39 + </el-col>
  40 + </el-row>
  41 + </el-card>
  42 +
  43 + <!-- 合同信息 -->
  44 + <el-card class="list-wrapper">
  45 + <div slot="header" class="flex justify-between">
  46 + <span>{{ $t('contractCreateFee.list.title') }}</span>
  47 + <el-button
  48 + type="primary"
  49 + size="small"
  50 + @click="handleBatchCreate"
  51 + >
  52 + <i class="el-icon-plus"></i>
  53 + {{ $t('contractCreateFee.list.batchCreate') }}
  54 + </el-button>
  55 + </div>
  56 +
  57 + <el-table
  58 + v-loading="loading"
  59 + :data="tableData"
  60 + border
  61 + style="width: 100%"
  62 + >
  63 + <el-table-column
  64 + prop="contractCode"
  65 + :label="$t('contractCreateFee.table.contractCode')"
  66 + align="center"
  67 + />
  68 + <el-table-column
  69 + prop="parentContractCode"
  70 + :label="$t('contractCreateFee.table.parentContractCode')"
  71 + align="center"
  72 + >
  73 + <template slot-scope="scope">
  74 + {{ scope.row.parentContractCode || '-' }}
  75 + </template>
  76 + </el-table-column>
  77 + <el-table-column
  78 + prop="contractName"
  79 + :label="$t('contractCreateFee.table.contractName')"
  80 + align="center"
  81 + />
  82 + <el-table-column
  83 + prop="contractTypeName"
  84 + :label="$t('contractCreateFee.table.contractType')"
  85 + align="center"
  86 + />
  87 + <el-table-column
  88 + prop="bContacts"
  89 + :label="$t('contractCreateFee.table.partyB')"
  90 + align="center"
  91 + />
  92 + <el-table-column
  93 + prop="amount"
  94 + :label="$t('contractCreateFee.table.amount')"
  95 + align="center"
  96 + />
  97 + <el-table-column
  98 + prop="startTime"
  99 + :label="$t('contractCreateFee.table.startTime')"
  100 + align="center"
  101 + />
  102 + <el-table-column
  103 + prop="endTime"
  104 + :label="$t('contractCreateFee.table.endTime')"
  105 + align="center"
  106 + />
  107 + <el-table-column
  108 + :label="$t('common.operation')"
  109 + align="center"
  110 + width="220"
  111 + >
  112 + <template slot-scope="scope">
  113 + <el-button
  114 + v-if="scope.row.state !== '2002'"
  115 + size="mini"
  116 + @click="handlePayFee(scope.row)"
  117 + >
  118 + {{ $t('contractCreateFee.table.payFee') }}
  119 + </el-button>
  120 + <el-button size="mini" @click="handleViewDetail(scope.row)">
  121 + {{ $t('contractCreateFee.table.viewDetail') }}
  122 + </el-button>
  123 + <el-button size="mini" @click="handleViewFee(scope.row)">
  124 + {{ $t('contractCreateFee.table.viewFee') }}
  125 + </el-button>
  126 + </template>
  127 + </el-table-column>
  128 + </el-table>
  129 +
  130 + <el-pagination
  131 + :current-page.sync="pagination.current"
  132 + :page-sizes="[10, 20, 30, 50]"
  133 + :page-size="pagination.size"
  134 + :total="pagination.total"
  135 + layout="total, sizes, prev, pager, next, jumper"
  136 + @size-change="handleSizeChange"
  137 + @current-change="handleCurrentChange"
  138 + />
  139 + </el-card>
  140 +
  141 + <!-- 创建费用弹窗 -->
  142 + <contract-create-fee-add ref="createFeeAdd" @success="handleSuccess" />
  143 + </div>
  144 +</template>
  145 +
  146 +<script>
  147 +import { queryContract } from '@/api/fee/contractCreateFeeApi'
  148 +import ContractCreateFeeAdd from '@/components/fee/contractCreateFeeAdd'
  149 +import { getCommunityId } from '@/api/community/communityApi'
  150 +
  151 +export default {
  152 + name: 'ContractCreateFeeList',
  153 + components: {
  154 + ContractCreateFeeAdd
  155 + },
  156 + data() {
  157 + return {
  158 + loading: false,
  159 + searchForm: {
  160 + contractCode: '',
  161 + contractNameLike: '',
  162 + contractType: ''
  163 + },
  164 + tableData: [],
  165 + pagination: {
  166 + current: 1,
  167 + size: 10,
  168 + total: 0
  169 + }
  170 + }
  171 + },
  172 + created() {
  173 + this.getList()
  174 + },
  175 + methods: {
  176 + async getList() {
  177 + try {
  178 + this.loading = true
  179 + const params = {
  180 + page: this.pagination.current,
  181 + row: this.pagination.size,
  182 + ...this.searchForm,
  183 + communityId: getCommunityId()
  184 + }
  185 + const { data, total } = await queryContract(params)
  186 + this.tableData = data
  187 + this.pagination.total = total
  188 + } catch (error) {
  189 + this.$message.error(this.$t('contractCreateFee.fetchError'))
  190 + } finally {
  191 + this.loading = false
  192 + }
  193 + },
  194 + handleSearch() {
  195 + this.pagination.current = 1
  196 + this.getList()
  197 + },
  198 + handleReset() {
  199 + this.searchForm = {
  200 + contractCode: '',
  201 + contractNameLike: '',
  202 + contractType: ''
  203 + }
  204 + this.handleSearch()
  205 + },
  206 + handleSizeChange(val) {
  207 + this.pagination.size = val
  208 + this.getList()
  209 + },
  210 + handleCurrentChange(val) {
  211 + this.pagination.current = val
  212 + this.getList()
  213 + },
  214 + handleBatchCreate() {
  215 + this.$refs.createFeeAdd.open(null, true)
  216 + },
  217 + handlePayFee(row) {
  218 + this.$router.push({
  219 + path: '/property/owePayFeeOrder',
  220 + query: {
  221 + payObjId: row.contractId,
  222 + payObjType: '7777',
  223 + contractName: row.contractName
  224 + }
  225 + })
  226 + },
  227 + handleViewDetail(row) {
  228 + this.$router.push({
  229 + path: '/common/contractApplyDetail',
  230 + query: { contractId: row.contractId }
  231 + })
  232 + },
  233 + handleViewFee(row) {
  234 + this.$router.push({
  235 + path: '/contract/contractDetail',
  236 + query: {
  237 + contractId: row.contractId,
  238 + contractCode: row.contractCode
  239 + }
  240 + })
  241 + },
  242 + handleSuccess() {
  243 + this.getList()
  244 + }
  245 + }
  246 +}
  247 +</script>
  248 +
  249 +<style lang="scss" scoped>
  250 +.contract-create-fee-container {
  251 + padding: 20px;
  252 +
  253 + .search-wrapper {
  254 + margin-bottom: 20px;
  255 +
  256 + .el-row {
  257 + margin-bottom: -20px;
  258 + }
  259 +
  260 + .el-col {
  261 + margin-bottom: 20px;
  262 + }
  263 + }
  264 +
  265 + .list-wrapper {
  266 + .el-pagination {
  267 + margin-top: 20px;
  268 + text-align: right;
  269 + }
  270 + }
  271 +}
  272 +</style>
0 \ No newline at end of file 273 \ No newline at end of file
src/views/fee/meterWaterManageLang.js 0 → 100644
  1 +export const messages = {
  2 + en:{
  3 + meterWater: {
  4 + queryConditions: 'Query Conditions',
  5 + meterReadingInfo: 'Meter Reading Info',
  6 + meterId: 'Meter ID',
  7 + meterType: 'Meter Type',
  8 + objectName: 'Object Name',
  9 + preDegrees: 'Previous Degrees',
  10 + curDegrees: 'Current Degrees',
  11 + preReadingTime: 'Previous Reading Time',
  12 + curReadingTime: 'Current Reading Time',
  13 + createTime: 'Create Time',
  14 + operation: 'Operation',
  15 + readMeter: 'Read Meter',
  16 + qrCodeMeter: 'QR Code Meter',
  17 + import1: 'Import 1',
  18 + import2: 'Import 2',
  19 + selectMeterType: 'Please select meter type',
  20 + inputMeterId: 'Please input meter ID',
  21 + query: 'Query',
  22 + reset: 'Reset',
  23 + addMeterReading: 'Add Meter Reading',
  24 + editMeterReading: 'Edit Meter Reading',
  25 + confirmOperation: 'Please confirm your operation',
  26 + confirmDeleteMeterReading: 'Confirm to delete meter reading',
  27 + cancel: 'Cancel',
  28 + confirmDelete: 'Confirm Delete',
  29 + meterReadingImport: 'Meter Reading Import',
  30 + meterReadingImport2: 'Meter Reading Import 2',
  31 + feeType: 'Fee Type',
  32 + selectFeeType: 'Please select fee type',
  33 + feeItem: 'Fee Item',
  34 + selectFeeItem: 'Please select fee item',
  35 + feeItemTip: 'Note: Fee items with formula [(Current Degrees - Previous Degrees) * Price + Additional Fee]',
  36 + meterTypeRequired: 'Please select meter type',
  37 + building: 'Building',
  38 + unit: 'Unit',
  39 + room: 'Room',
  40 + feeObject: 'Fee Object',
  41 + inputRoom: 'Please input room',
  42 + inputPreDegrees: 'Please input previous degrees',
  43 + inputCurDegrees: 'Please input current degrees',
  44 + selectPreReadingTime: 'Please select previous reading time',
  45 + selectCurReadingTime: 'Please select current reading time',
  46 + price: 'Price',
  47 + inputPrice: 'Please input price',
  48 + remark: 'Remark',
  49 + inputRemark: 'Please input remark',
  50 + unitTip: 'Note: Unit 0 means shop',
  51 + save: 'Save',
  52 + selectFile: 'Select File',
  53 + fileRequired: 'Please select data file',
  54 + downloadTemplate: 'Download Template',
  55 + downloadTip: 'Please download',
  56 + importTemplate: 'Import Template',
  57 + prepareData: 'prepare data first, then upload to import',
  58 + dynamicFeeTip: 'Used when fee item is dynamic fee',
  59 + import: 'Import',
  60 + meterQRCode: 'Meter QR Code',
  61 + qrCode: 'QR Code',
  62 + qrCodeTip: 'Please screenshot and print, paste it to the meter, meter reader can scan quickly',
  63 + selectBuilding: 'Please select building',
  64 + selectUnit: 'Please select unit',
  65 + selectRoom: 'Please select room',
  66 + feeTypeRequired: 'Please select fee type',
  67 + feeItemRequired: 'Please select fee item',
  68 + preDegreesRequired: 'Please input previous degrees',
  69 + curDegreesRequired: 'Please input current degrees',
  70 + preReadingTimeRequired: 'Please select previous reading time',
  71 + curReadingTimeRequired: 'Please select current reading time',
  72 + degreesFormatError: 'Degrees format error',
  73 + degreesCompareError: 'Current degrees cannot be less than previous degrees',
  74 + remarkMaxLength: 'Remark cannot exceed 500 characters',
  75 + selectMeterTypeFirst: 'Please select meter type first',
  76 + downloadStarted: 'Download started',
  77 + importSuccess: 'Import success'
  78 + }
  79 + },
  80 + zh:{
  81 + meterWater: {
  82 + queryConditions: '查询条件',
  83 + meterReadingInfo: '抄表信息',
  84 + meterId: '表ID',
  85 + meterType: '表类型',
  86 + objectName: '对象名称',
  87 + preDegrees: '上期度数',
  88 + curDegrees: '本期度数',
  89 + preReadingTime: '上期读表时间',
  90 + curReadingTime: '本期读表时间',
  91 + createTime: '创建时间',
  92 + operation: '操作',
  93 + readMeter: '抄表',
  94 + qrCodeMeter: '二维码抄表',
  95 + import1: '抄表导入1',
  96 + import2: '抄表导入2',
  97 + selectMeterType: '请选择表类型',
  98 + inputMeterId: '请输入表ID',
  99 + query: '查询',
  100 + reset: '重置',
  101 + addMeterReading: '添加抄表',
  102 + editMeterReading: '修改抄表',
  103 + confirmOperation: '请确认您的操作',
  104 + confirmDeleteMeterReading: '确定删除抄表',
  105 + cancel: '点错了',
  106 + confirmDelete: '确认删除',
  107 + meterReadingImport: '抄表导入',
  108 + meterReadingImport2: '抄表导入2',
  109 + feeType: '费用类型',
  110 + selectFeeType: '请选择费用类型',
  111 + feeItem: '收费项目',
  112 + selectFeeItem: '请选择收费项目',
  113 + feeItemTip: '说明:显示公式为【(本期度数-上期度数)*单价+附加费】的费用项',
  114 + meterTypeRequired: '请选择抄表类型',
  115 + building: '楼栋',
  116 + unit: '单元',
  117 + room: '房屋',
  118 + feeObject: '收费对象',
  119 + inputRoom: '请填写房屋',
  120 + inputPreDegrees: '请输入上期度数',
  121 + inputCurDegrees: '请输入本期度数',
  122 + selectPreReadingTime: '请选择上期读表时间',
  123 + selectCurReadingTime: '请选择本期读表时间',
  124 + price: '单价',
  125 + inputPrice: '请输入单价',
  126 + remark: '备注',
  127 + inputRemark: '请输入备注',
  128 + unitTip: '注:单元选择为0表示为商铺',
  129 + save: '保存',
  130 + selectFile: '选择文件',
  131 + fileRequired: '请选择数据文件',
  132 + downloadTemplate: '下载模板',
  133 + downloadTip: '请先下载',
  134 + importTemplate: '导入模板',
  135 + prepareData: '准备数据后,上传导入',
  136 + dynamicFeeTip: '当收费项目为动态费用时使用',
  137 + import: '导入',
  138 + meterQRCode: '抄表二维码',
  139 + qrCode: '二维码',
  140 + qrCodeTip: '请截图打印后,粘贴到电表处,抄表人员扫码快速抄表',
  141 + selectBuilding: '请选择楼栋',
  142 + selectUnit: '请选择单元',
  143 + selectRoom: '请选择房屋',
  144 + feeTypeRequired: '请选择费用类型',
  145 + feeItemRequired: '请选择收费项目',
  146 + preDegreesRequired: '请输入上期度数',
  147 + curDegreesRequired: '请输入本期度数',
  148 + preReadingTimeRequired: '请选择上期读表时间',
  149 + curReadingTimeRequired: '请选择本期读表时间',
  150 + degreesFormatError: '度数格式错误',
  151 + degreesCompareError: '本期度数不能小于上期度数',
  152 + remarkMaxLength: '备注不能超过500个字符',
  153 + selectMeterTypeFirst: '请先选择抄表类型',
  154 + downloadStarted: '下载已开始',
  155 + importSuccess: '导入成功'
  156 + }
  157 + }
  158 +
  159 +}
0 \ No newline at end of file 160 \ No newline at end of file
src/views/fee/meterWaterManageList.vue 0 → 100644
  1 +<template>
  2 + <div class="meter-water-manage-container">
  3 + <el-row :gutter="20">
  4 + <el-col :span="4" class="tree-container">
  5 + <room-tree-div ref="roomTree" @selectRoom="handleSelectRoom" />
  6 + </el-col>
  7 + <el-col :span="20">
  8 + <el-card class="box-card">
  9 + <div slot="header" class="clearfix">
  10 + <span>{{ $t('meterWater.queryConditions') }}</span>
  11 + </div>
  12 + <el-form :inline="true" :model="conditions" class="demo-form-inline">
  13 + <el-form-item :label="$t('meterWater.meterType')">
  14 + <el-select v-model="conditions.meterType" :placeholder="$t('meterWater.selectMeterType')" clearable
  15 + style="width: 100%">
  16 + <el-option v-for="item in meterTypes" :key="item.typeId" :label="item.typeName" :value="item.typeId" />
  17 + </el-select>
  18 + </el-form-item>
  19 + <el-form-item :label="$t('meterWater.meterId')">
  20 + <el-input v-model="conditions.waterId" :placeholder="$t('meterWater.inputMeterId')" clearable />
  21 + </el-form-item>
  22 + <el-form-item>
  23 + <el-button type="primary" @click="handleQuery">{{
  24 + $t('common.query')
  25 + }}</el-button>
  26 + <el-button @click="handleReset">{{ $t('common.reset') }}</el-button>
  27 + </el-form-item>
  28 + </el-form>
  29 + </el-card>
  30 +
  31 + <el-card class="box-card margin-top">
  32 + <div slot="header" class="clearfix">
  33 + <span>{{ $t('meterWater.meterReadingInfo') }}</span>
  34 + <div style="float: right">
  35 + <el-button v-if="conditions.objId" type="primary" size="small" @click="handleOpenAddMeter">
  36 + {{ $t('meterWater.readMeter') }}
  37 + </el-button>
  38 + <el-button type="text" size="small" @click="handleOpenMeterType">
  39 + {{ $t('meterWater.meterType') }}
  40 + </el-button>
  41 + <el-button v-if="conditions.objId" type="text" size="small" @click="handleOpenQrCode">
  42 + {{ $t('meterWater.qrCodeMeter') }}
  43 + </el-button>
  44 + <el-button type="text" size="small" style="margin-left: 10px" @click="handleOpenImport1">
  45 + <i class="el-icon-plus"></i>{{ $t('meterWater.import1') }}
  46 + </el-button>
  47 + <el-button type="text" size="small" style="margin-left: 10px" @click="handleOpenImport2">
  48 + <i class="el-icon-plus"></i>{{ $t('meterWater.import2') }}
  49 + </el-button>
  50 + </div>
  51 + </div>
  52 +
  53 + <el-table :data="meterWaters" border style="width: 100%">
  54 + <el-table-column prop="waterId" :label="$t('meterWater.meterId')" align="center" />
  55 + <el-table-column prop="meterTypeName" :label="$t('meterWater.meterType')" align="center" />
  56 + <el-table-column prop="objName" :label="$t('meterWater.objectName')" align="center" />
  57 + <el-table-column prop="preDegrees" :label="$t('meterWater.preDegrees')" align="center" />
  58 + <el-table-column prop="curDegrees" :label="$t('meterWater.curDegrees')" align="center" />
  59 + <el-table-column prop="preReadingTime" :label="$t('meterWater.preReadingTime')" align="center" />
  60 + <el-table-column prop="curReadingTime" :label="$t('meterWater.curReadingTime')" align="center" />
  61 + <el-table-column prop="createTime" :label="$t('meterWater.createTime')" align="center" />
  62 + <el-table-column :label="$t('common.operation')" align="center" width="180">
  63 + <template slot-scope="scope">
  64 + <el-button type="text" size="small" @click="handleEdit(scope.row)">
  65 + {{ $t('common.edit') }}
  66 + </el-button>
  67 + <el-button type="text" size="small" @click="handleDelete(scope.row)">
  68 + {{ $t('common.delete') }}
  69 + </el-button>
  70 + </template>
  71 + </el-table-column>
  72 + </el-table>
  73 +
  74 + <el-pagination :current-page="pagination.current" :page-sizes="[10, 20, 30, 50]" :page-size="pagination.size"
  75 + :total="pagination.total" layout="total, sizes, prev, pager, next, jumper" @size-change="handleSizeChange"
  76 + @current-change="handleCurrentChange" />
  77 + </el-card>
  78 + </el-col>
  79 + </el-row>
  80 +
  81 + <!-- 组件 -->
  82 + <add-meter-water ref="addMeterWater" @success="handleSuccess" />
  83 + <edit-meter-water ref="editMeterWater" @success="handleSuccess" />
  84 + <delete-meter-water ref="deleteMeterWater" @success="handleSuccess" />
  85 + <import-meter-water-fee ref="importMeterWaterFee" />
  86 + <import-meter-water-fee2 ref="importMeterWaterFee2" />
  87 + <room-meter-qrcode ref="roomMeterQrcode" />
  88 + </div>
  89 +</template>
  90 +
  91 +<script>
  92 +import {
  93 + listMeterWaters,
  94 + listMeterTypes
  95 +} from '@/api/fee/meterWaterManageApi'
  96 +import { getCommunityId } from '@/api/community/communityApi'
  97 +import RoomTreeDiv from '@/components/fee/roomTreeDiv'
  98 +import AddMeterWater from '@/components/fee/addMeterWater'
  99 +import EditMeterWater from '@/components/fee/editMeterWater'
  100 +import DeleteMeterWater from '@/components/fee/deleteMeterWater'
  101 +import ImportMeterWaterFee from '@/components/fee/importMeterWaterFee'
  102 +import ImportMeterWaterFee2 from '@/components/fee/importMeterWaterFee2'
  103 +import RoomMeterQrcode from '@/components/fee/roomMeterQrcode'
  104 +
  105 +export default {
  106 + name: 'MeterWaterManageList',
  107 + components: {
  108 + RoomTreeDiv,
  109 + AddMeterWater,
  110 + EditMeterWater,
  111 + DeleteMeterWater,
  112 + ImportMeterWaterFee,
  113 + ImportMeterWaterFee2,
  114 + RoomMeterQrcode
  115 + },
  116 + data() {
  117 + return {
  118 + conditions: {
  119 + waterId: '',
  120 + meterType: '',
  121 + roomNum: '',
  122 + objId: '',
  123 + page: 1,
  124 + row: 10,
  125 + communityId: ''
  126 + },
  127 + meterWaters: [],
  128 + meterTypes: [],
  129 + pagination: {
  130 + current: 1,
  131 + size: 10,
  132 + total: 0
  133 + },
  134 + roomName: ''
  135 + }
  136 + },
  137 + created() {
  138 + this.conditions.communityId = getCommunityId()
  139 + this.loadData()
  140 + this.loadMeterTypes()
  141 + },
  142 + methods: {
  143 + async loadData() {
  144 + try {
  145 + const { data, total } = await listMeterWaters(this.conditions)
  146 + this.meterWaters = data
  147 + this.pagination.total = total
  148 + } catch (error) {
  149 + console.error('Failed to load meter waters:', error)
  150 + }
  151 + },
  152 + async loadMeterTypes() {
  153 + try {
  154 + const { data } = await listMeterTypes({
  155 + communityId: this.conditions.communityId,
  156 + page: 1,
  157 + row: 100
  158 + })
  159 + this.meterTypes = data
  160 + } catch (error) {
  161 + console.error('Failed to load meter types:', error)
  162 + }
  163 + },
  164 + handleQuery() {
  165 + this.conditions.page = 1
  166 + this.loadData()
  167 + },
  168 + handleReset() {
  169 + this.conditions.waterId = ''
  170 + this.conditions.meterType = ''
  171 + this.conditions.roomNum = ''
  172 + this.loadData()
  173 + },
  174 + handleSizeChange(val) {
  175 + this.conditions.row = val
  176 + this.loadData()
  177 + },
  178 + handleCurrentChange(val) {
  179 + this.conditions.page = val
  180 + this.loadData()
  181 + },
  182 + handleSelectRoom({ roomId, roomName }) {
  183 + this.conditions.objId = roomId
  184 + this.roomName = roomName
  185 + this.loadData()
  186 + },
  187 + handleOpenAddMeter() {
  188 + this.$refs.addMeterWater.open({
  189 + roomId: this.conditions.objId,
  190 + roomName: this.roomName
  191 + })
  192 + },
  193 + handleEdit(row) {
  194 + this.$refs.editMeterWater.open(row)
  195 + },
  196 + handleDelete(row) {
  197 + this.$refs.deleteMeterWater.open(row)
  198 + },
  199 + handleOpenImport1() {
  200 + this.$refs.importMeterWaterFee.open()
  201 + },
  202 + handleOpenImport2() {
  203 + this.$refs.importMeterWaterFee2.open()
  204 + },
  205 + handleOpenQrCode() {
  206 + this.$refs.roomMeterQrcode.open({
  207 + roomId: this.conditions.objId,
  208 + roomName: this.roomName
  209 + })
  210 + },
  211 + handleOpenMeterType() {
  212 + this.$router.push('/fee/meterTypeManage')
  213 + },
  214 + handleSuccess() {
  215 + this.loadData()
  216 + }
  217 + }
  218 +}
  219 +</script>
  220 +
  221 +<style lang="scss" scoped>
  222 +.meter-water-manage-container {
  223 + padding: 20px;
  224 +
  225 + .tree-container {
  226 + height: calc(100vh - 120px);
  227 + overflow-y: auto;
  228 + background: #fff;
  229 + padding: 10px;
  230 + border-radius: 4px;
  231 + }
  232 +
  233 + .margin-top {
  234 + margin-top: 20px;
  235 + }
  236 +
  237 + .clearfix:before,
  238 + .clearfix:after {
  239 + display: table;
  240 + content: '';
  241 + }
  242 +
  243 + .clearfix:after {
  244 + clear: both;
  245 + }
  246 +}
  247 +</style>
0 \ No newline at end of file 248 \ No newline at end of file