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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 13 \ No newline at end of file
... ...
src/i18n/index.js
... ... @@ -146,6 +146,7 @@ import { messages as userI18n } from &#39;./userI18n&#39;
146 146 import { messages as systemI18n } from './systemI18n'
147 147 import { messages as communityI18n } from './communityI18n'
148 148 import { messages as workI18n } from './workI18n'
  149 +import { messages as feeI18n } from './feeI18n'
149 150  
150 151 Vue.use(VueI18n)
151 152  
... ... @@ -290,6 +291,7 @@ const messages = {
290 291 ...systemI18n.en,
291 292 ...communityI18n.en,
292 293 ...workI18n.en,
  294 + ...feeI18n.en,
293 295 },
294 296 zh: {
295 297 ...loginMessages.zh,
... ... @@ -428,6 +430,7 @@ const messages = {
428 430 ...systemI18n.zh,
429 431 ...communityI18n.zh,
430 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 13 \ No newline at end of file
... ...
src/router/index.js
... ... @@ -16,6 +16,7 @@ import userRouter from &#39;./userRouter&#39;
16 16 import systemRouter from './systemRouter'
17 17 import communityRouter from './communityRouter'
18 18 import workRouter from './workRouter'
  19 +import feeRouter from './feeRouter'
19 20  
20 21 Vue.use(VueRouter)
21 22  
... ... @@ -640,6 +641,7 @@ const routes = [
640 641 ...systemRouter,
641 642 ...communityRouter,
642 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 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 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 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 248 \ No newline at end of file
... ...