Ver código fonte

修改视频播放、文档阅读的学习记录

余思翰 5 meses atrás
pai
commit
a8f493d0a0

+ 1
- 1
oa-back/ruoyi-system/src/main/resources/mapper/oa/CmcStudyMapper.xml Ver arquivo

47
             <if test="resourceId != null "> and s.resource_id = #{resourceId}</if>
47
             <if test="resourceId != null "> and s.resource_id = #{resourceId}</if>
48
             <if test="userId != null "> and s.user_id = #{userId}</if>
48
             <if test="userId != null "> and s.user_id = #{userId}</if>
49
             <if test="lastPoint != null  and lastPoint != ''"> and s.last_point = #{lastPoint}</if>
49
             <if test="lastPoint != null  and lastPoint != ''"> and s.last_point = #{lastPoint}</if>
50
-            <if test="lastTime != null "> and s.last_time = #{lastTime}</if>
50
+            <if test="lastTime != null "> and YEAR(s.last_time) = YEAR(#{lastTime})</if>
51
             <if test="getHours != null "> and s.get_hours = #{getHours}</if>
51
             <if test="getHours != null "> and s.get_hours = #{getHours}</if>
52
         </where>
52
         </where>
53
     </select>
53
     </select>

+ 1
- 0
oa-ui/package.json Ver arquivo

61
     "jsencrypt": "3.0.0-rc.1",
61
     "jsencrypt": "3.0.0-rc.1",
62
     "nprogress": "0.2.0",
62
     "nprogress": "0.2.0",
63
     "ol": "^7.1.0",
63
     "ol": "^7.1.0",
64
+    "pdfjs-dist": "^2.0.943",
64
     "quill": "1.3.7",
65
     "quill": "1.3.7",
65
     "screenfull": "5.0.2",
66
     "screenfull": "5.0.2",
66
     "sortablejs": "1.10.2",
67
     "sortablejs": "1.10.2",

+ 254
- 0
oa-ui/src/views/oa/study/components/pdfStudy.vue Ver arquivo

1
+<!--
2
+ * @Author: ysh
3
+ * @Date: 2025-03-06 14:38:35
4
+ * @LastEditors: Please set LastEditors
5
+ * @LastEditTime: 2025-03-11 16:16:33
6
+-->
7
+<template>
8
+  <div>
9
+    <div class="return-btn">
10
+      <div>
11
+        <el-button type="primary" size="mini" icon="el-icon-d-arrow-left"
12
+          @click="$router.push({ path: '/oa/study/myStudy' })">返回我的学习</el-button>
13
+      </div>
14
+      <div class="mt20">
15
+        学习进度:
16
+        <el-progress :text-inside="true" :status="formatStatus(percentage)" text-color="#fff" :stroke-width="26"
17
+          :percentage="percentage"></el-progress>
18
+      </div>
19
+      <div class="mt20" style="color:#E23D28;font-size:12px;line-height:30px;">
20
+        tips:鼠标滚动过快将导致进度计算失败,请隔2秒滚动一次页面。若一直未到100%,请返回我的学习重新进入课程。
21
+      </div>
22
+    </div>
23
+    <div id="pdf-container">
24
+
25
+      <div id="viewer" class="pdfViewer"></div>
26
+    </div>
27
+  </div>
28
+</template>
29
+
30
+<script>
31
+import { listStudy, getStudy, delStudy, addStudy, updateStudy } from "@/api/oa/study/myStudy";
32
+import { getDocument, GlobalWorkerOptions } from 'pdfjs-dist/build/pdf'
33
+import { PDFViewer } from 'pdfjs-dist/web/pdf_viewer'
34
+// 设置 PDF.js 的 Web Worker 路径
35
+GlobalWorkerOptions.workerSrc = require('pdfjs-dist/build/pdf.worker.min');
36
+export default {
37
+  async mounted() {
38
+    await this.getData();
39
+    this.initPDFViewer()
40
+  },
41
+  data() {
42
+    return {
43
+      baseUrl: process.env.VUE_APP_BASE_API,
44
+      studyId: '',
45
+      pdfPath: '',
46
+      pdfViewer: null,
47
+      resource: {},
48
+      totalPages: 0, //总页数
49
+      readPages: [], //已阅读页数
50
+      lastPoint: 0,
51
+      debounceTimer: null,
52
+      observers: new Map(),
53
+      isComplete: false,
54
+      percentage: 0,
55
+    }
56
+  },
57
+  methods: {
58
+    async getData() {
59
+      this.studyId = this.$route.params.id;
60
+      let resData = await getStudy(this.studyId);
61
+      let data = resData.data;
62
+      this.pdfPath = data.resource.sourcePath;
63
+      this.lastPoint = parseFloat(data.lastPoint);
64
+      this.resource = data.resource;
65
+      if (Number(this.lastPoint) == 100) {
66
+        this.isComplete = true;
67
+        this.$message.success('文档已阅读完毕')
68
+      }
69
+    },
70
+    // 初始化查看器
71
+    async initPDFViewer() {
72
+      this.pdfViewer = new PDFViewer({
73
+        container: document.getElementById('pdf-container'),
74
+        enhanceTextSelection: true,
75
+        textLayerMode: 1
76
+      })
77
+      // 先绑定监听页面变化事件
78
+      this.setupViewerEvents();
79
+      // 加载文档
80
+      const loadingTask = getDocument({
81
+        url: `/dev-api/profile/upload/${this.pdfPath}`,
82
+        cMapPacked: true
83
+      })
84
+      try {
85
+        const pdfDocument = await loadingTask.promise
86
+        this.pdfViewer.setDocument(pdfDocument);
87
+        this.totalPages = pdfDocument.numPages;
88
+        this.readPages = new Array(this.totalPages).fill(false);
89
+        let curProgress = Math.round((this.lastPoint / 100) * this.totalPages);
90
+        this.percentage = Math.round(curProgress / this.totalPages) * 100;
91
+        for (let i = 0; i < curProgress; i++) {
92
+          this.readPages[i] = true
93
+        }
94
+        this.$nextTick(() => {
95
+          this.setupPageObservers();
96
+        });
97
+      } catch (err) {
98
+        console.error('PDF加载失败:', err)
99
+      }
100
+      // 添加窗口缩放监听
101
+      window.addEventListener("resize", this.handleResize);
102
+    },
103
+    handleResize() {
104
+      if (this.pdfViewer) {
105
+        this.pdfViewer.update();
106
+      }
107
+    },
108
+    setupViewerEvents() {
109
+      const eventBus = this.pdfViewer.eventBus;
110
+      // 新增 pagesloaded 事件监听
111
+      eventBus.on('pagesloaded', () => {
112
+        this.scrollToLastPosition();
113
+        this.setupPageObservers()
114
+      });
115
+    },
116
+    scrollToLastPosition() {
117
+      if (!this.lastPoint || !this.totalPages) return
118
+      // 计算目标页码(向上取整)
119
+      const pageNumber = Math.min(
120
+        Math.ceil((this.lastPoint / 100) * this.totalPages),
121
+        this.totalPages
122
+      )
123
+      // 查找对应的页面元素
124
+      const pageElement = document.querySelector(
125
+        `.page[data-page-number="${pageNumber}"]`
126
+      )
127
+      if (pageElement) {
128
+        // 获取页面元素的垂直位置
129
+        const pageTop = pageElement.offsetTop
130
+        // 设置滚动位置(带20px顶部留白)
131
+        document.getElementById('pdf-container').scrollTo({
132
+          top: pageTop - 20,
133
+          behavior: 'auto'
134
+        })
135
+      }
136
+    },
137
+    setupPageObservers() {
138
+      // 清理旧观察者
139
+      this.observers.forEach((observer, page) => {
140
+        observer.disconnect();
141
+        delete page.dataset.timer;
142
+      });
143
+      this.observers.clear();
144
+      const pages = document.querySelectorAll('.page');
145
+      pages.forEach(page => {
146
+        const observer = new IntersectionObserver((entries) => {
147
+          entries.forEach(entry => {
148
+            const pageNumber = parseInt(page.dataset.pageNumber);
149
+            if (entry.isIntersecting) {
150
+              page.dataset.enterTime = Date.now();
151
+              page.dataset.timer = setTimeout(() => {
152
+                if (!this.readPages[pageNumber - 1]) {
153
+                  this.$set(this.readPages, pageNumber - 1, true);
154
+                  this.updateProgress();
155
+                }
156
+              }, 2000);
157
+            } else {
158
+              clearTimeout(page.dataset.timer);
159
+              delete page.dataset.enterTime;
160
+            }
161
+          });
162
+        }, { threshold: 0.5, root: this.pdfViewer.container });
163
+
164
+        this.observers.set(page, observer);
165
+        observer.observe(page);
166
+      });
167
+    },
168
+    updateProgress() {
169
+      if (!this.isComplete) {
170
+        const readCount = this.readPages.filter(Boolean).length;
171
+        const progress = Math.round(readCount / this.totalPages * 100);
172
+        this.percentage = progress;
173
+        // 防抖保存
174
+        if (!this.debounceTimer) {
175
+          this.debounceTimer = setTimeout(() => {
176
+            this.saveProgress(progress);
177
+            this.debounceTimer = null;
178
+          }, 2000);
179
+        }
180
+      }
181
+    },
182
+    async saveProgress(progress) {
183
+      try {
184
+        if (parseFloat(progress) == 100) {
185
+          this.$message.success('文档已阅读完毕!学时+' + this.resource.hours)
186
+          progress = 100
187
+        }
188
+        await updateStudy(
189
+          {
190
+            studyId: this.studyId,
191
+            lastPoint: progress,
192
+            getHours: Math.round((this.resource.hours * progress) / 100),
193
+            lastTime: this.parseTime(new Date(), '{y}-{m}-{d}')
194
+          });
195
+      } catch (error) {
196
+        console.error('保存进度失败:', error);
197
+      }
198
+    },
199
+    formatStatus(row) {
200
+      if (!row) {
201
+        row = 0
202
+        return 'exception'
203
+      }
204
+      if (row <= 20) {
205
+        return 'exception'
206
+      } else if (row > 20 && row <= 50) {
207
+        return 'warning'
208
+      } else if (row > 50 && row <= 80) {
209
+        return null
210
+      } else {
211
+        return 'success'
212
+      }
213
+    },
214
+  },
215
+  beforeDestroy() {
216
+    // 清理观察者和定时器
217
+    this.observers.forEach((observer, page) => {
218
+      observer.disconnect();
219
+      clearTimeout(page.dataset.timer);
220
+    });
221
+    window.removeEventListener("resize", this.handleResize);
222
+  }
223
+
224
+}
225
+</script>
226
+
227
+<style lang="scss" scoped>
228
+@import '~pdfjs-dist/web/pdf_viewer.css';
229
+
230
+/* 修改后 */
231
+#pdf-container {
232
+  position: relative;
233
+  /* 改为相对定位,避免脱离文档流 */
234
+  width: 100%;
235
+  height: calc(100vh - 88px);
236
+  /* 根据实际布局调整高度 */
237
+  overflow: auto;
238
+  background-color: #cdcdcd;
239
+}
240
+
241
+.pdfViewer .page {
242
+  margin: 1em auto;
243
+  border: 1px solid #ddd;
244
+  box-shadow: 0 10px 10px rgba(0, 0, 0, 0.1);
245
+}
246
+
247
+.return-btn {
248
+  position: absolute;
249
+  left: 20px;
250
+  top: 100px;
251
+  z-index: 9999;
252
+  width: 230px;
253
+}
254
+</style>

+ 19
- 2
oa-ui/src/views/oa/study/components/studyHead.vue Ver arquivo

2
  * @Author: ysh
2
  * @Author: ysh
3
  * @Date: 2025-03-05 14:19:02
3
  * @Date: 2025-03-05 14:19:02
4
  * @LastEditors: Please set LastEditors
4
  * @LastEditors: Please set LastEditors
5
- * @LastEditTime: 2025-03-06 14:26:32
5
+ * @LastEditTime: 2025-03-11 17:00:41
6
 -->
6
 -->
7
 <template>
7
 <template>
8
   <div class="head-bg">
8
   <div class="head-bg">
31
 </template>
31
 </template>
32
 
32
 
33
 <script>
33
 <script>
34
+import { listStudy, getStudy, delStudy, addStudy, updateStudy } from "@/api/oa/study/myStudy";
35
+
34
 export default {
36
 export default {
35
   data() {
37
   data() {
36
     return {
38
     return {
37
       hours: 0,
39
       hours: 0,
38
     }
40
     }
39
   },
41
   },
42
+  created() {
43
+    this.getYearHours();
44
+  },
45
+  methods: {
46
+    getYearHours() {
47
+      let lastTime = new Date().getFullYear() + '-01-01'
48
+      listStudy({ userId: this.$store.getters.userId, lastTime: lastTime }).then(res => {
49
+        console.log(res);
50
+        if (res.total > 0) {
51
+          this.hours = res.rows.reduce((sum, item) => sum + Number(item.getHours), 0)
52
+        }
53
+
54
+      })
55
+    }
56
+  },
40
 }
57
 }
41
 </script>
58
 </script>
42
 
59
 
93
       font-family: 'puhuiti';
110
       font-family: 'puhuiti';
94
       line-height: 28px;
111
       line-height: 28px;
95
       margin-top: 52px;
112
       margin-top: 52px;
96
-      margin-left:20px;
113
+      margin-left: 20px;
97
     }
114
     }
98
 
115
 
99
     .user-study {
116
     .user-study {

+ 8
- 3
oa-ui/src/views/oa/study/components/studyLeft.vue Ver arquivo

2
  * @Author: ysh
2
  * @Author: ysh
3
  * @Date: 2025-03-05 15:01:52
3
  * @Date: 2025-03-05 15:01:52
4
  * @LastEditors: Please set LastEditors
4
  * @LastEditors: Please set LastEditors
5
- * @LastEditTime: 2025-03-07 15:03:55
5
+ * @LastEditTime: 2025-03-11 16:47:24
6
 -->
6
 -->
7
 <template>
7
 <template>
8
   <div>
8
   <div>
30
         </template>
30
         </template>
31
       </el-table-column>
31
       </el-table-column>
32
     </el-table>
32
     </el-table>
33
+    <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum"
34
+      :layout="'total, prev, pager, next,jumper'" :limit.sync="queryParams.pageSize" :autoScroll="false"
35
+      @pagination="getRecordsList" />
33
   </div>
36
   </div>
34
 </template>
37
 </template>
35
 
38
 
42
       loading: true,
45
       loading: true,
43
       studyList: [],
46
       studyList: [],
44
       total: 0,
47
       total: 0,
45
-      queryParams: {}
48
+      queryParams: {
49
+        pageNum: 1,
50
+        pageSize: 10
51
+      }
46
     }
52
     }
47
   },
53
   },
48
   created() {
54
   created() {
60
     },
66
     },
61
     startStudy(record) {
67
     startStudy(record) {
62
       const routeName = record.resource.type === '视频' ? 'VideoStudy' : 'PdfStudy';
68
       const routeName = record.resource.type === '视频' ? 'VideoStudy' : 'PdfStudy';
63
-      console.log(record);
64
       this.$router.push({
69
       this.$router.push({
65
         name: routeName,
70
         name: routeName,
66
         params: { id: record.studyId }
71
         params: { id: record.studyId }

+ 1
- 17
oa-ui/src/views/oa/study/components/studyRight.vue Ver arquivo

2
  * @Author: ysh
2
  * @Author: ysh
3
  * @Date: 2025-03-05 15:36:17
3
  * @Date: 2025-03-05 15:36:17
4
  * @LastEditors: Please set LastEditors
4
  * @LastEditors: Please set LastEditors
5
- * @LastEditTime: 2025-03-06 16:00:55
5
+ * @LastEditTime: 2025-03-11 16:48:55
6
 -->
6
 -->
7
 <template>
7
 <template>
8
   <div class="right-box">
8
   <div class="right-box">
9
-    <!-- <div class="material-list">
10
-      <el-card v-for="item in resourceList" :key="item.id" class="material-item">
11
-        <div class="material-content">
12
-          <i :class="item.type === '视频' ? 'el-icon-video-camera' : 'el-icon-document'"
13
-            style="font-size: 24px;margin-right: 15px" />
14
-          <div class="material-info">
15
-            <h4>名称:{{ item.title }}</h4>
16
-            <span>学时:{{ item.hours }}个学时</span>
17
-          </div>
18
-          <el-button type="primary" size="mini" @click="addToStudy(item)">
19
-            添加
20
-          </el-button>
21
-        </div>
22
-      </el-card>
23
-    </div> -->
24
     <div class="video">
9
     <div class="video">
25
       <el-tabs v-model="videoTab" type="card">
10
       <el-tabs v-model="videoTab" type="card">
26
         <el-tab-pane label="视频" name="video">
11
         <el-tab-pane label="视频" name="video">
103
       });
88
       });
104
     },
89
     },
105
     addToStudy(row) {
90
     addToStudy(row) {
106
-      console.log(row);
107
       this.$modal.confirm('是否添加资料名称为"' + row.title + '"到我的学习记录?').then(res => {
91
       this.$modal.confirm('是否添加资料名称为"' + row.title + '"到我的学习记录?').then(res => {
108
         let obj = {
92
         let obj = {
109
           resourceId: row.resourceId,
93
           resourceId: row.resourceId,

+ 70
- 22
oa-ui/src/views/oa/study/components/videoStudy.vue Ver arquivo

1
 <template>
1
 <template>
2
   <div class="video-container">
2
   <div class="video-container">
3
-    <el-button type="primary" @click="$router.push({ path: '/oa/study/myStudy' })">返回我的学习</el-button>
4
-    <video ref="videoPlayer" class="video-js vjs-big-play-centered" @play="handlePlay" @pause="handlePause"
5
-      @ended="handleEnd">
3
+    <div class="return-btn">
4
+      <el-button type="primary" size="mini" icon="el-icon-d-arrow-left"
5
+        @click="$router.push({ path: '/oa/study/myStudy' })">返回我的学习</el-button>
6
+      <div class="mt20">
7
+        学习进度:
8
+        <el-progress :text-inside="true" :status="formatStatus(percentage)" text-color="#fff" :stroke-width="26"
9
+          :percentage="percentage"></el-progress>
10
+      </div>
11
+      <div class="mt20" style="color:#E23D28;font-size:12px;line-height:30px;">
12
+        tips:视频不可拖动进度条,每隔30秒会自动保存,暂停也会保存视频进度,若出现学习进度一直不到100%,请退出后重新进入课程。
13
+      </div>
14
+    </div>
15
+    <video ref="videoPlayer" width="1300" height="750" class="custom-video video-js vjs-big-play-centered"
16
+      @play="handlePlay" @pause="handlePause" @ended="handleEnd">
6
     </video>
17
     </video>
7
     <el-alert v-if="showProgressTip" type="info" :closable="false" class="progress-alert">
18
     <el-alert v-if="showProgressTip" type="info" :closable="false" class="progress-alert">
8
       已为您定位到上次播放位置:{{ formatTime(lastPosition) }}
19
       已为您定位到上次播放位置:{{ formatTime(lastPosition) }}
27
       getHours: '', //可获得的学时
38
       getHours: '', //可获得的学时
28
       isComplete: false, //是否已经看完
39
       isComplete: false, //是否已经看完
29
       showProgressTip: false,
40
       showProgressTip: false,
41
+      resource: {},
30
       // 禁用功能配置
42
       // 禁用功能配置
31
       disabledControls: [
43
       disabledControls: [
32
         'progressControl', // 隐藏进度条
44
         'progressControl', // 隐藏进度条
35
         'playbackRateMenuButton'
47
         'playbackRateMenuButton'
36
       ],
48
       ],
37
       videoDuration: undefined,
49
       videoDuration: undefined,
50
+      percentage: 0,
38
     };
51
     };
39
   },
52
   },
40
   watch: {
53
   watch: {
67
   methods: {
80
   methods: {
68
     async getData() {
81
     async getData() {
69
       let studyId = this.$route.params.id;
82
       let studyId = this.$route.params.id;
70
-      let resData = await getStudy(studyId)
83
+      let resData = await getStudy(studyId);
71
       let data = resData.data
84
       let data = resData.data
85
+      this.resource = data.resource;
72
       this.videoPath = data.resource.sourcePath;
86
       this.videoPath = data.resource.sourcePath;
87
+      this.percentage = Number(data.lastPoint);
88
+      if (Number(data.lastPoint) > 0 && Number(data.lastPoint) < 100) {
89
+        this.showProgressTip = true;
90
+      }
73
       if (Number(data.lastPoint) == 100) {
91
       if (Number(data.lastPoint) == 100) {
74
         this.isComplete = true;
92
         this.isComplete = true;
75
         this.getHours = data.resource.hours;
93
         this.getHours = data.resource.hours;
76
-        console.log(data.resource);
77
-
78
         this.$message.warning('视频已学习完毕,若要再次学习,请在学习记录里重新添加该课程。')
94
         this.$message.warning('视频已学习完毕,若要再次学习,请在学习记录里重新添加该课程。')
79
       }
95
       }
80
     },
96
     },
110
         }
126
         }
111
       });
127
       });
112
 
128
 
113
-      // 节流保存(每10秒保存一次)
129
+      // 节流保存(每30秒保存一次)
114
       this.player.on('timeupdate', _.throttle(() => {
130
       this.player.on('timeupdate', _.throttle(() => {
115
         this.saveCurrentPosition();
131
         this.saveCurrentPosition();
116
-      }, 10000));
132
+      }, 30000));
117
     },
133
     },
118
     // 加载上次播放位置
134
     // 加载上次播放位置
119
     async loadLastPosition() {
135
     async loadLastPosition() {
155
     },
171
     },
156
 
172
 
157
     // 处理视频结束
173
     // 处理视频结束
158
-    handleEnd() {
159
-      this.saveCurrentPosition(true);
174
+    async handleEnd() {
175
+      this.$message.warning('视频已学习完毕,若要再次学习,请在学习记录里重新添加该课程。')
176
+      await updateStudy({
177
+        studyId: this.videoId,
178
+        lastPoint: 100,
179
+        getHours: this.resource.hours,
180
+        lastTime: this.parseTime(new Date(), '{y}-{m}-{d}')
181
+      });
160
     },
182
     },
161
     // 保存当前进度
183
     // 保存当前进度
162
     async saveCurrentPosition(isEnd = false) {
184
     async saveCurrentPosition(isEnd = false) {
166
       if (isNaN(percentage)) {
188
       if (isNaN(percentage)) {
167
         return
189
         return
168
       }
190
       }
191
+      this.percentage = Number(percentage)
169
       try {
192
       try {
170
-        if (!this.isComplete) {
171
-          await updateStudy({
172
-            studyId: this.videoId,
173
-            lastPoint: percentage
174
-          });
175
-        } else {
176
-          await updateStudy({
177
-            studyId: this.videoId,
178
-            getHours: this.getHours
179
-          });
193
+        if (Number(percentage) == 100) {
194
+          return
180
         }
195
         }
196
+        await updateStudy({
197
+          studyId: this.videoId,
198
+          lastPoint: percentage,
199
+          getHours: Math.round((this.resource.hours * percentage) / 100),
200
+          lastTime: this.parseTime(new Date(), '{y}-{m}-{d}')
201
+        });
181
         // 始终更新 lastPosition(即使播放结束)
202
         // 始终更新 lastPosition(即使播放结束)
182
         this.lastPosition = currentTime;
203
         this.lastPosition = currentTime;
183
       } catch (error) {
204
       } catch (error) {
191
       date.setSeconds(seconds);
212
       date.setSeconds(seconds);
192
       return date.toISOString().substr(11, 8);
213
       return date.toISOString().substr(11, 8);
193
     },
214
     },
215
+    formatStatus(row) {
216
+      if (!row) {
217
+        row = 0
218
+        return 'exception'
219
+      }
220
+      if (row <= 20) {
221
+        return 'exception'
222
+      } else if (row > 20 && row <= 50) {
223
+        return 'warning'
224
+      } else if (row > 50 && row <= 80) {
225
+        return null
226
+      } else {
227
+        return 'success'
228
+      }
229
+    },
194
   },
230
   },
195
   beforeDestroy() {
231
   beforeDestroy() {
196
     if (this.player) {
232
     if (this.player) {
203
 <style lang="scss" scoped>
239
 <style lang="scss" scoped>
204
 .video-container {
240
 .video-container {
205
   position: relative;
241
   position: relative;
206
-  width: 1000px;
207
-  // margin: 0 auto;
242
+
243
+  .return-btn {
244
+    position: absolute;
245
+    left: 20px;
246
+    top: 30px;
247
+    width: 130px;
248
+  }
249
+
250
+  .custom-video {
251
+    position: absolute;
252
+    left: 50%;
253
+    top: 50%;
254
+    transform: translate(-50%, 0%);
255
+  }
208
 
256
 
209
   .progress-alert {
257
   .progress-alert {
210
     position: absolute;
258
     position: absolute;

Carregando…
Cancelar
Salvar