Lijy 4 дней назад
Родитель
Сommit
aacfed6052

+ 75 - 0
fhKeeper/formulahousekeeper/course-pc/src/api.js

@@ -0,0 +1,75 @@
+import http from './http' // 引入你封装的 http.js
+import { Message } from 'element-ui';
+
+// 通用 POST 请求封装
+export function post(url, param) {
+  return new Promise((resolve, reject) => {
+    http.post(
+      url,
+      { ...param },
+      res => {
+        if (res.code === 'ok') {
+          resolve(res)
+        } else {
+          Message.error(res.msg)
+          reject(res)
+        }
+      },
+      err => {
+        Message.error(err)
+        reject(err)
+      }
+    )
+  })
+}
+
+// 通用 GET 请求封装
+export function getData(url) {
+  return new Promise((resolve, reject) => {
+    http.get(
+      url,
+      res => {
+        if (res.code === 'ok') {
+          resolve(res)
+        } else {
+          Message.error(res.msg)
+          reject(res)
+        }
+      },
+      err => {
+        reject(err)
+      }
+    )
+  })
+}
+
+// 上传文件(如果使用 FormData 格式)
+export function uploadFile(url, formData) {
+  return new Promise((resolve, reject) => {
+    http.uploadFileFormData(
+      url,
+      formData,
+      res => {
+        if (res.code === 'ok') {
+          resolve(res)
+        } else {
+          reject(res)
+        }
+      },
+      err => {
+        reject(err)
+      }
+    )
+  })
+}
+
+export function checkAndAddUpload(str) {
+  if(!str) {
+      return '';
+  }
+  if (str.includes('/upload/')) {
+    return str;
+  } else {
+    return '/upload/' + str;
+  }
+}

+ 11 - 0
fhKeeper/formulahousekeeper/course-pc/src/routes.js

@@ -10,6 +10,7 @@ import PdfView from './views/pdf/pdfview';
 import courselist from './views/coursemanagement/list';
 import lecturerList from './views/lecturerManagement/index.vue'
 import offlineTraining from './views/offlineTraining/offlineTraining.vue'
+import examCertification from './views/examCertification/examCertification.vue'
 
 Vue.use(Router)
 
@@ -58,6 +59,16 @@ export const allRouters = [
         name: '',
         hidden: true
     },
+    {
+        path: '/exam-certification',
+        component: Home,
+        name: '考试拿证流程介绍',
+        iconCls: 'iconfont firerock-iconkehu',
+        leaf: true,
+        children: [
+            { path: '/exam-certification', component: examCertification, name: '考试拿证流程介绍' }
+        ]
+    },
     {
         path: '/offline-training',
         component: Home,

+ 401 - 0
fhKeeper/formulahousekeeper/course-pc/src/views/examCertification/examCertification.vue

@@ -0,0 +1,401 @@
+<template>
+  <div class="offline-training-container">
+    <el-card class="box-card">
+      <div slot="header" class="clearfix">
+        <span>考试拿证流程介绍</span>
+      </div>
+      
+      <viewer ref="imageWrapper" :images="previewImages" style="opacity: 1;position: absolute;top: -9999px;">
+        <img v-for="(image, index) in previewImages" :src="image" :key="index" />
+      </viewer>
+
+      <el-form ref="form" :model="form" label-width="120px">
+        <div class="form-section">
+          <el-form-item label="分类名称" prop="typeName">
+            <el-input v-model="form.typeName" placeholder="请输入分类名称"></el-input>
+          </el-form-item>
+        </div>
+
+        <div class="form-section">
+          <el-form-item label="分类封面" prop="coverImage">
+            <div class="upload-area">
+              <el-upload
+                class="cover-uploader"
+                action="#"
+                :show-file-list="false"
+                :on-change="handleCoverChange"
+                :auto-upload="false"
+                :before-upload="beforeCoverUpload">
+                <img v-if="form.coverImage" :src="form.coverImage" class="cover-preview">
+                <i v-else class="el-icon-plus cover-uploader-icon"></i>
+                <div v-if="form.coverImage" class="cover-mask">
+                  <el-link icon="el-icon-view" type="success" @click.stop="previewImage(form.coverImage)" :underline="false">查看</el-link>
+                  <el-link icon="el-icon-edit" type="success" :underline="false" style="margin-left: 10px;">修改</el-link>
+                </div>
+              </el-upload>
+              <div class="upload-tip">建议尺寸:800x600px,大小不超过5MB</div>
+            </div>
+          </el-form-item>
+        </div>
+
+        <div class="form-section">
+          <el-form-item label="内容上传" prop="type">
+            <el-radio-group v-model="form.type" @change="handleContentTypeChange">
+              <el-radio label="image">图片</el-radio>
+              <el-radio label="video">视频</el-radio>
+            </el-radio-group>
+            
+            <div class="upload-area" v-if="form.type === 'image'">
+              <el-upload
+                action="#"
+                :show-file-list="false"
+                :on-change="handleImageChange"
+                :auto-upload="false"
+                accept="image/*"
+                :before-upload="beforeImageUpload">
+                <el-button size="small" type="primary">点击上传图片</el-button>
+              </el-upload>
+              <div class="upload-preview" v-if="form.contentImage">
+                <img :src="form.contentImage" class="content-preview" @click="previewImage(form.contentImage)">
+              </div>
+            </div>
+
+            <div class="upload-area" v-else-if="form.type === 'video'">
+              <el-upload
+                action="#"
+                :show-file-list="false"
+                :on-change="handleVideoChange"
+                :auto-upload="false"
+                accept="video/*"
+                :before-upload="beforeVideoUpload">
+                <el-button size="small" type="primary">点击上传视频</el-button>
+              </el-upload>
+              <div class="upload-preview" v-if="form.contentVideo">
+                <video controls class="video-preview">
+                  <source :src="form.contentVideo" type="video/mp4">
+                </video>
+              </div>
+            </div>
+          </el-form-item>
+        </div>
+
+        <div class="action-buttons">
+          <el-button type="primary" @click="submitForm" :loading="submitLoading">提交</el-button>
+          <el-button @click="resetForm" :loading="submitLoading">重置</el-button>
+        </div>
+      </el-form>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import { post, checkAndAddUpload } from '../../api'
+export default {
+  data() {
+    return {
+      form: {
+        typeName: '',
+        coverImage: '',
+        type: 'image',
+        contentImage: '',
+        contentVideo: ''
+      },
+      previewImages: [],
+      submitLoading: false
+    }
+  },
+  methods: {
+    previewImage(url) {
+      this.previewImages = [url];
+      this.$nextTick(() => {
+        if (this.$refs.imageWrapper && this.$refs.imageWrapper.$viewer) {
+          this.$refs.imageWrapper.$viewer.show();
+        }
+      });
+    },
+    
+    checkAndAddUpload(str) {
+      if(!str) return '';
+      return str.includes('/upload/') ? str : '/upload/' + str;
+    },
+
+    handleCoverChange(file) {
+      const isImage = file.raw.type.includes('image');
+      const isLt5M = file.raw.size / 1024 / 1024 < 5;
+      
+      if (!isImage) {
+        this.$message.error('只能上传图片文件!');
+        return;
+      }
+      if (!isLt5M) {
+        this.$message.error('图片大小不能超过5MB!');
+        return;
+      }
+
+      const formData = new FormData();
+      formData.append('multipartFile', file.raw);
+      
+      this.http.uploadFile('/common/uploadFile', formData, res => {
+        if (res.code === "ok") {
+          this.form.coverImage = this.checkAndAddUpload(res.data);
+          this.$message.success('封面图片上传成功');
+        } else {
+          this.$message.error(res.msg || '上传失败');
+        }
+      });
+    },
+    
+    beforeCoverUpload(file) {
+      const isImage = file.type.includes('image');
+      const isLt5M = file.size / 1024 / 1024 < 5;
+      
+      if (!isImage) {
+        this.$message.error('只能上传图片文件!');
+        return false;
+      }
+      if (!isLt5M) {
+        this.$message.error('图片大小不能超过5MB!');
+        return false;
+      }
+      return true;
+    },
+    
+    handleImageChange(file) {
+      const isImage = file.raw.type.includes('image');
+      const isLt5M = file.raw.size / 1024 / 1024 < 5;
+      
+      if (!isImage) {
+        this.$message.error('只能上传图片文件!');
+        return;
+      }
+      if (!isLt5M) {
+        this.$message.error('图片大小不能超过5MB!');
+        return;
+      }
+
+      const formData = new FormData();
+      formData.append('multipartFile', file.raw);
+      
+      this.http.uploadFile('/common/uploadFile', formData, res => {
+        if (res.code === "ok") {
+          this.form.contentImage = this.checkAndAddUpload(res.data);
+          this.$message.success('内容图片上传成功');
+        } else {
+          this.$message.error(res.msg || '上传失败');
+        }
+      });
+    },
+    
+    beforeImageUpload(file) {
+      return this.beforeCoverUpload(file);
+    },
+    
+    handleVideoChange(file) {
+      const isVideo = file.raw.type.includes('video');
+      const isLt50M = file.raw.size / 1024 / 1024 < 50;
+      
+      if (!isVideo) {
+        this.$message.error('只能上传视频文件!');
+        return;
+      }
+      if (!isLt50M) {
+        this.$message.error('视频大小不能超过50MB!');
+        return;
+      }
+
+      const formData = new FormData();
+      formData.append('multipartFile', file.raw);
+      
+      this.http.uploadFile('/common/uploadFile', formData, res => {
+        if (res.code === "ok") {
+          this.form.contentVideo = this.checkAndAddUpload(res.data);
+          this.$message.success('视频上传成功');
+        } else {
+          this.$message.error(res.msg || '上传失败');
+        }
+      });
+    },
+    
+    beforeVideoUpload(file) {
+      const isVideo = file.type.includes('video');
+      const isLt50M = file.size / 1024 / 1024 < 50;
+      
+      if (!isVideo) {
+        this.$message.error('只能上传视频文件!');
+        return false;
+      }
+      if (!isLt50M) {
+        this.$message.error('视频大小不能超过50MB!');
+        return false;
+      }
+      return true;
+    },
+
+    handleContentTypeChange(val) {
+      this.form.contentImage = '';
+      this.form.contentVideo = '';
+    },
+
+    submitForm() {
+      this.$refs.form.validate(valid => {
+        if (valid) {
+          console.log(this.form, '<==== 将要提交的表单')
+          const { id, type, typeName, coverImage, contentImage, contentVideo } = this.form
+          const obj = {
+            typeName,
+            coverImage,
+            type: type == 'video' ? 1 : 0,
+            content: type == 'video' ? contentVideo : contentImage,
+          }
+
+          if(id) {
+            obj.id = id
+          }
+
+          this.submitLoading = true
+          post(`/examination-process/saveOrUpdate`, { ...obj }).then(res => {
+            this.$message({ 
+              type: 'success',
+              message: res.msg || '保存成功',
+            });
+
+            this.obtainOfflineTrainingCourses()
+          }).finally(() => {
+            this.submitLoading = false
+          })
+        }
+      });
+    },
+
+    resetForm() {
+      this.$refs.form.resetFields();
+      this.form.contentImage = '';
+      this.form.contentVideo = '';
+    },
+
+    obtainOfflineTrainingCourses() {
+      post(`/examination-process/getDetail`, {}).then((res) => {
+        this.form = {
+          ...res.data,
+          content: checkAndAddUpload(res.data.content),
+          coverImage: checkAndAddUpload(res.data.coverImage),
+          contentImage: res.data.type != 1 ? checkAndAddUpload(res.data.content) : '',
+          contentVideo: res.data.type == 1 ? checkAndAddUpload(res.data.content) : '',
+          type: res.data.type == 1 ? 'video' : 'image'
+        }
+      })
+    },
+
+    
+  },
+  mounted() {
+    this.obtainOfflineTrainingCourses()
+  }
+}
+</script>
+
+<style scoped>
+.offline-training-container {
+  padding: 20px;
+  max-width: 1000px;
+  margin: 0 auto;
+}
+
+.box-card {
+  border-radius: 8px;
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+}
+
+.form-section {
+  margin-bottom: 24px;
+}
+
+.upload-area {
+  border: 1px dashed #dcdfe6;
+  border-radius: 6px;
+  padding: 20px;
+  margin-bottom: 20px;
+  background-color: #f5f7fa;
+}
+
+.upload-preview {
+  margin-top: 15px;
+  text-align: center;
+}
+
+.cover-uploader {
+  border: 1px dashed #d9d9d9;
+  border-radius: 6px;
+  cursor: pointer;
+  position: relative;
+  overflow: hidden;
+  width: 200px;
+  height: 150px;
+  margin: 0 auto;
+}
+
+.cover-uploader:hover {
+  border-color: #409EFF;
+}
+
+.cover-uploader:hover .cover-mask {
+  opacity: 1;
+}
+
+.cover-mask {
+  width: 100%;
+  height: 100%;
+  background: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  position: absolute;
+  top: 0;
+  left: 0;
+  opacity: 0;
+}
+
+.cover-uploader-icon {
+  font-size: 28px;
+  color: #8c939d;
+  width: 200px;
+  height: 150px;
+  line-height: 150px;
+  text-align: center;
+}
+
+.cover-preview {
+  width: 200px;
+  height: 150px;
+  object-fit: cover;
+  border-radius: 4px;
+  cursor: pointer;
+}
+
+.content-preview {
+  max-width: 100%;
+  max-height: 300px;
+  border-radius: 4px;
+  cursor: pointer;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.video-preview {
+  max-width: 100%;
+  max-height: 300px;
+  border-radius: 4px;
+  background-color: #000;
+}
+
+.upload-tip {
+  color: #909399;
+  font-size: 12px;
+  margin-top: 8px;
+  text-align: center;
+}
+
+.action-buttons {
+  margin-top: 24px;
+  text-align: center;
+}
+</style>

+ 80 - 15
fhKeeper/formulahousekeeper/course-pc/src/views/offlineTraining/offlineTraining.vue

@@ -11,8 +11,8 @@
 
       <el-form ref="form" :model="form" label-width="120px">
         <div class="form-section">
-          <el-form-item label="分类名称" prop="categoryName">
-            <el-input v-model="form.categoryName" placeholder="请输入分类名称"></el-input>
+          <el-form-item label="分类名称" prop="typeName">
+            <el-input v-model="form.typeName" placeholder="请输入分类名称"></el-input>
           </el-form-item>
         </div>
 
@@ -26,8 +26,12 @@
                 :on-change="handleCoverChange"
                 :auto-upload="false"
                 :before-upload="beforeCoverUpload">
-                <img v-if="form.coverImage" :src="form.coverImage" class="cover-preview" @click="previewImage(form.coverImage)">
+                <img v-if="form.coverImage" :src="form.coverImage" class="cover-preview">
                 <i v-else class="el-icon-plus cover-uploader-icon"></i>
+                <div v-if="form.coverImage" class="cover-mask">
+                  <el-link icon="el-icon-view" type="success" @click.stop="previewImage(form.coverImage)" :underline="false">查看</el-link>
+                  <el-link icon="el-icon-edit" type="success" :underline="false" style="margin-left: 10px;">修改</el-link>
+                </div>
               </el-upload>
               <div class="upload-tip">建议尺寸:800x600px,大小不超过5MB</div>
             </div>
@@ -35,18 +39,19 @@
         </div>
 
         <div class="form-section">
-          <el-form-item label="内容上传" prop="contentType">
-            <el-radio-group v-model="form.contentType" @change="handleContentTypeChange">
+          <el-form-item label="内容上传" prop="type">
+            <el-radio-group v-model="form.type" @change="handleContentTypeChange">
               <el-radio label="image">图片</el-radio>
               <el-radio label="video">视频</el-radio>
             </el-radio-group>
             
-            <div class="upload-area" v-if="form.contentType === 'image'">
+            <div class="upload-area" v-if="form.type === 'image'">
               <el-upload
                 action="#"
                 :show-file-list="false"
                 :on-change="handleImageChange"
                 :auto-upload="false"
+                accept="image/*"
                 :before-upload="beforeImageUpload">
                 <el-button size="small" type="primary">点击上传图片</el-button>
               </el-upload>
@@ -55,12 +60,13 @@
               </div>
             </div>
 
-            <div class="upload-area" v-else-if="form.contentType === 'video'">
+            <div class="upload-area" v-else-if="form.type === 'video'">
               <el-upload
                 action="#"
                 :show-file-list="false"
                 :on-change="handleVideoChange"
                 :auto-upload="false"
+                accept="video/*"
                 :before-upload="beforeVideoUpload">
                 <el-button size="small" type="primary">点击上传视频</el-button>
               </el-upload>
@@ -74,8 +80,8 @@
         </div>
 
         <div class="action-buttons">
-          <el-button type="primary" @click="submitForm">提交</el-button>
-          <el-button @click="resetForm">重置</el-button>
+          <el-button type="primary" @click="submitForm" :loading="submitLoading">提交</el-button>
+          <el-button @click="resetForm" :loading="submitLoading">重置</el-button>
         </div>
       </el-form>
     </el-card>
@@ -83,17 +89,19 @@
 </template>
 
 <script>
+import { post, checkAndAddUpload } from '../../api'
 export default {
   data() {
     return {
       form: {
-        categoryName: '',
+        typeName: '',
         coverImage: '',
-        contentType: 'image',
+        type: 'image',
         contentImage: '',
         contentVideo: ''
       },
-      previewImages: []
+      previewImages: [],
+      submitLoading: false
     }
   },
   methods: {
@@ -231,8 +239,30 @@ export default {
     submitForm() {
       this.$refs.form.validate(valid => {
         if (valid) {
-          // 提交表单逻辑
-          this.$message.success('提交成功!');
+          console.log(this.form, '<==== 将要提交的表单')
+          const { id, type, typeName, coverImage, contentImage, contentVideo } = this.form
+          const obj = {
+            typeName,
+            coverImage,
+            type: type == 'video' ? 1 : 0,
+            content: type == 'video' ? contentVideo : contentImage,
+          }
+
+          if(id) {
+            obj.id = id
+          }
+
+          this.submitLoading = true
+          post(`/offline-training-course/saveOrUpdate`, { ...obj }).then(res => {
+            this.$message({ 
+              type: 'success',
+              message: res.msg || '保存成功',
+            });
+
+            this.obtainOfflineTrainingCourses()
+          }).finally(() => {
+            this.submitLoading = false
+          })
         }
       });
     },
@@ -241,7 +271,25 @@ export default {
       this.$refs.form.resetFields();
       this.form.contentImage = '';
       this.form.contentVideo = '';
-    }
+    },
+
+    obtainOfflineTrainingCourses() {
+      post(`/offline-training-course/getDetail`, {}).then((res) => {
+        this.form = {
+          ...res.data,
+          content: checkAndAddUpload(res.data.content),
+          coverImage: checkAndAddUpload(res.data.coverImage),
+          contentImage: res.data.type != 1 ? checkAndAddUpload(res.data.content) : '',
+          contentVideo: res.data.type == 1 ? checkAndAddUpload(res.data.content) : '',
+          type: res.data.type == 1 ? 'video' : 'image'
+        }
+      })
+    },
+
+    
+  },
+  mounted() {
+    this.obtainOfflineTrainingCourses()
   }
 }
 </script>
@@ -290,6 +338,23 @@ export default {
   border-color: #409EFF;
 }
 
+.cover-uploader:hover .cover-mask {
+  opacity: 1;
+}
+
+.cover-mask {
+  width: 100%;
+  height: 100%;
+  background: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  position: absolute;
+  top: 0;
+  left: 0;
+  opacity: 0;
+}
+
 .cover-uploader-icon {
   font-size: 28px;
   color: #8c939d;

+ 4 - 1
fhKeeper/formulahousekeeper/timesheet/src/views/corpreport/list.vue

@@ -1605,6 +1605,7 @@
                 </template>
               </el-table-column>
             </el-table-column>
+            <el-table-column prop="projectWorkingHours" align="center" label="项目工时(h)" width="120px"></el-table-column>
             <el-table-column prop="assistTime" align="center" label="协助工时(h)" width="120px">
               <template slot-scope="scope">
                 <el-link type="primary" :underline="false" @click="adjustWorkingHours(scope.row, '协助工时',scope.row.assistTime,'assistTime')">{{ scope.row.assistTime }}</el-link>
@@ -4990,8 +4991,10 @@ export default {
           ...res.data,
           detailList: (res.data.detailList || []).map(item => {
             const timeFields = ['maintanceTime', 'debugTime', 'waitingTime', 'assistTime', 'publicTime'];
+            const projectWorkingHoursFields = ['bustripTime', 'cleanTime']
             const totalTime = timeFields.reduce((sum, key) => sum + (item[key] || 0), 0);
-            return { ...item, totalTime: (totalTime).toFixed(2) };
+            const projectWorkingHours = projectWorkingHoursFields.reduce((sum, key) => sum + (item[key] || 0), 0)
+            return { ...item, totalTime: (totalTime).toFixed(2), projectWorkingHours };
           })
         };
       }).finally(() => {