zhangjie преди 2 години
родител
ревизия
186a731ad7

+ 13 - 0
src/api/examwork-activity.js

@@ -56,3 +56,16 @@ export function saveActivities(ary) {
 export function toggleEnableActivity({ examId, id, enable }) {
   return httpApp.post("/api/admin/activity/save", [{ examId, id, enable }]);
 }
+
+export function searchActivityAudios(ary) {
+  return httpApp.post("/api/admin/activity/audio/query", ary);
+}
+
+export function saveActivityAudio(datas) {
+  return httpApp.post("/api/admin/activity/audio/save", datas);
+}
+export function toggleEnableActivityAudio({ audioId, enable }) {
+  return httpApp.post("/api/admin/activity/audio/enable", [
+    { audioId, enable },
+  ]);
+}

+ 211 - 0
src/components/UploadFileView.vue

@@ -0,0 +1,211 @@
+<template>
+  <div class="upload-file-view">
+    <el-input
+      :style="{ width: inputWidth }"
+      v-model.trim="attachmentName"
+      placeholder="文件名称"
+      readonly
+    ></el-input>
+    <el-upload
+      :action="uploadUrl"
+      :headers="headers"
+      :max-size="maxSize"
+      :format="format"
+      :data="uploadDataDict"
+      :on-change="handleFileChange"
+      :before-upload="handleBeforeUpload"
+      :on-error="handleError"
+      :on-success="handleSuccess"
+      :on-progress="handleProgress"
+      :http-request="upload"
+      :show-file-list="false"
+      :disabled="disabled"
+      style="display: inline-block; margin: 0 10px;"
+      ref="UploadComp"
+    >
+      <el-button type="primary" :disabled="disabled" :loading="loading">
+        <span v-if="loading">{{ percent }}%</span>
+        <span v-else>选择</span>
+      </el-button>
+    </el-upload>
+    <el-button
+      type="primary"
+      @click="startUpload"
+      v-if="canUpload && !autoUpload"
+      :loading="loading"
+      style="margin-right: 10px;"
+      >开始上传</el-button
+    >
+  </div>
+</template>
+
+<script>
+import { httpApp } from "@/plugins/axiosIndex";
+import { getMd5FromBlob } from "@/utils/utils";
+
+export default {
+  name: "upload-file-view",
+  props: {
+    inputWidth: {
+      type: String,
+      default: "200px",
+    },
+    format: {
+      type: Array,
+      default() {
+        return ["xls", "xlsx"];
+      },
+    },
+    uploadUrl: {
+      type: String,
+      required: true,
+    },
+    uploadData: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+    maxSize: {
+      type: Number,
+      default: 20 * 1024 * 1024,
+    },
+    addFilenameParam: {
+      type: String,
+      default: "filename",
+    },
+    autoUpload: {
+      type: Boolean,
+      default: true,
+    },
+    disabled: {
+      type: Boolean,
+      default: false,
+    },
+  },
+  data() {
+    return {
+      attachmentName: "",
+      canUpload: false,
+      loading: false,
+      uploadDataDict: {},
+      headers: {
+        md5: "",
+      },
+      percent: 0,
+      res: {},
+    };
+  },
+  methods: {
+    startUpload() {
+      this.loading = true;
+      this.$refs.UploadComp.submit();
+    },
+    checkFileFormat(fileType) {
+      const _file_format = fileType.split(".").pop().toLocaleLowerCase();
+      return this.format.some(
+        (item) => item.toLocaleLowerCase() === _file_format
+      );
+    },
+    handleFileChange(file) {
+      this.attachmentName = file.name;
+      this.canUpload = file.status === "ready";
+    },
+    async handleBeforeUpload(file) {
+      this.uploadDataDict = {
+        ...this.uploadData,
+      };
+      this.uploadDataDict[this.addFilenameParam] = file.name;
+
+      if (file.size > this.maxSize) {
+        this.handleExceededSize();
+        return Promise.reject();
+      }
+
+      if (!this.checkFileFormat(file.name)) {
+        this.handleFormatError();
+        return Promise.reject();
+      }
+
+      const md5 = await getMd5FromBlob(file);
+      this.headers["md5"] = md5;
+
+      if (this.autoUpload) this.loading = true;
+
+      return this.autoUpload;
+    },
+    upload(options) {
+      let formData = new FormData();
+      Object.entries(options.data).forEach(([k, v]) => {
+        formData.append(k, v);
+      });
+      formData.append("file", options.file);
+      this.$emit("uploading");
+
+      return httpApp.post(options.action, formData, {
+        headers: options.headers,
+      });
+    },
+    handleError(error) {
+      this.canUpload = false;
+      this.loading = false;
+      this.percent = 0;
+      this.res = {
+        success: false,
+        message: error.message,
+      };
+      this.$emit("upload-error", error);
+    },
+    handleSuccess(responseData) {
+      this.canUpload = false;
+      this.loading = false;
+      this.percent = 0;
+      this.res = {
+        success: true,
+        message: "导入成功!",
+      };
+      this.$emit("upload-success", {
+        ...responseData,
+        filename: this.uploadDataDict[this.addFilenameParam],
+      });
+    },
+    handleProgress(progressEvent) {
+      this.loading = true;
+      if (progressEvent.total > 0 && progressEvent.loaded) {
+        this.percent = (
+          (progressEvent.loaded / progressEvent.total) *
+          100
+        ).toFixed(0);
+      }
+    },
+    handleFormatError() {
+      const content = "只支持文件格式为" + this.format.join("/");
+      this.res = {
+        success: false,
+        message: content,
+      };
+      this.loading = false;
+      this.$emit("valid-error", this.res);
+    },
+    handleExceededSize() {
+      const content =
+        "文件大小不能超过" + Math.floor(this.maxSize / 1024) + "M";
+      this.res = {
+        success: false,
+        message: content,
+      };
+      this.loading = false;
+      this.$emit("valid-error", this.res);
+    },
+    setAttachmentName(name) {
+      this.attachmentName = name;
+    },
+  },
+};
+</script>
+
+<style scoped>
+.upload-file-view {
+  display: inline-block;
+}
+</style>

+ 7 - 1
src/constant/constants.js

@@ -149,6 +149,11 @@ export const IMPORT_EXPORT_TASKS = [
   { code: "EXPORT_MARK_RESULT_STANDARD", name: "导出成绩标准版" },
 ];
 
+export const ACTIVITY_AUDIO_TYPE = {
+  BEFORE: "开考前语音",
+  AFTER: "考试结束前语音",
+};
+
 let domain;
 if (process.env.VUE_APP_SELF_DEFINE_DOMAIN === "true") {
   domain = window.localStorage.getItem("domain_in_url");
@@ -159,7 +164,8 @@ export const ORG_CODE = domain;
 // 注意:组件命名按照驼峰形式,其他方式命名会导致缓存失败
 // [routeNameShouldKeepAlive, [whenReturedFromThisRoute]]
 export const keepAliveRoutesPairs = [
-  ["ExamManagement", ["ExamEdit"]],
+  ["ExamManagement", ["ExamEdit", "ActivityManagement"]],
+  ["ActivityManagement", ["ActivityAudioManagement"]],
   ["InvigilationDetail", ["WarningDetail", "InvigilationWarningDetail"]],
   ["WarningManage", ["WarningDetail", "InvigilationWarningDetail"]],
   ["OnlinePatrol", ["PatrolExamDetail", "PatrolWarningDetail"]],

+ 179 - 0
src/features/examwork/ActivityManagement/ActivityAudioDialog.vue

@@ -0,0 +1,179 @@
+<template>
+  <el-dialog
+    ref="dialog"
+    :title="(isEdit ? '编辑' : '新增') + '语音'"
+    width="540px"
+    :visible.sync="visible"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    append-to-body
+    @open="visibleChange"
+  >
+    <el-form
+      ref="modalFormComp"
+      :model="form"
+      :rules="rules"
+      label-width="100px"
+    >
+      <el-form-item label="语音文件" prop="attachmentId">
+        <upload-file-view
+          :upload-data="uploadData"
+          :upload-url="uploadUrl"
+          :format="format"
+          input-width="300px"
+          @valid-error="validError"
+          @upload-success="uploadSuccess"
+          ref="UploadFileView"
+        ></upload-file-view>
+      </el-form-item>
+      <el-form-item label="内容描述" prop="content">
+        <el-input
+          type="textarea"
+          v-model.trim="form.content"
+          placeholder="请输入"
+          :maxlength="1000"
+          :rows="5"
+          show-word-limit
+          clearable
+        ></el-input>
+      </el-form-item>
+      <el-form-item label="播报时间" prop="type">
+        <el-radio-group v-model="form.type">
+          <el-radio
+            v-for="(val, key) in ACTIVITY_AUDIO_TYPE"
+            :key="key"
+            :label="key"
+          >
+            <span>{{ val }}</span>
+            <el-input-number
+              v-model.trim="form.playTime"
+              :min="1"
+              :step="1"
+              step-strictly
+              style="width: 50px; margin: 0 5px;"
+              :controls="false"
+            >
+            </el-input-number>
+            <span>分钟</span>
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+    </el-form>
+    <div slot="footer" class="d-flex justify-content-center">
+      <el-button type="primary" @click="submitForm" :loading="loading"
+        >保 存</el-button
+      >
+      <el-button @click="closeDialog">取 消</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import { saveActivityAudio } from "@/api/examwork-activity";
+import { ACTIVITY_AUDIO_TYPE } from "@/constant/constants";
+
+const initForm = {
+  id: null,
+  audioDefault: "CUSTOM",
+  attachmentId: null,
+  orgId: null,
+  type: "BEFORE",
+  playTime: 1,
+  activityId: null,
+  content: "",
+};
+
+export default {
+  name: "ActivityAudioDialog",
+  components: {},
+  props: {
+    activity: Object,
+  },
+  computed: {
+    isEdit() {
+      return this.activity.id;
+    },
+  },
+  data() {
+    return {
+      visible: false,
+      ACTIVITY_AUDIO_TYPE,
+      form: {
+        ...initForm,
+      },
+      rules: {
+        attachmentId: [
+          { required: true, message: "请上传语音文件", trigger: "change" },
+        ],
+        content: [
+          { required: true, message: "请输入内容描述", trigger: "blur" },
+        ],
+        type: [
+          {
+            validator: (value, rule, callback) => {
+              if (!this.form.type) {
+                return callback(new Error("请选择播报时间类型"));
+              }
+              if (!this.form.playTime) {
+                return callback(new Error("请输入播报时间"));
+              }
+
+              callback();
+            },
+            trigger: "change",
+          },
+        ],
+      },
+      loading: false,
+      // upload
+      uploadUrl: "/api/admin/sys/file/upload",
+      uploadData: {
+        type: "upload",
+      },
+      format: ["wmv"],
+    };
+  },
+  methods: {
+    visibleChange() {
+      if (this.activity.id) {
+        this.modalForm = Object.assign({}, initForm, this.activity);
+      } else {
+        this.modalForm = { ...initForm };
+      }
+
+      this.$nextTick(() => {
+        this.$refs.modalFormComp.clearValidate();
+        this.$refs.UploadFileView.setAttachmentName("");
+      });
+    },
+    openDialog() {
+      this.visible = true;
+    },
+    closeDialog() {
+      this.visible = false;
+    },
+    validError(errorData) {
+      this.$message.error(errorData.message);
+    },
+    uploadSuccess(data) {
+      this.$message.success("上传成功!");
+
+      this.form.attachmentId = data.id;
+      this.$refs.modalFormComp.validateField("attachmentId");
+    },
+    async submitForm() {
+      const valid = await this.$refs.modalFormComp.validate().catch(() => {});
+      if (!valid) return;
+
+      if (this.loading) return;
+      this.loading = true;
+      const res = await saveActivityAudio({ ...this.form }).catch(() => {});
+      this.loading = false;
+      if (!res) return;
+      this.$emit("reload");
+      this.$notify({ title: "保存成功", type: "success" });
+      this.closeDialog();
+    },
+  },
+};
+</script>

+ 162 - 0
src/features/examwork/ActivityManagement/ActivityAudioManagement.vue

@@ -0,0 +1,162 @@
+<template>
+  <div class="activity-audio-management">
+    <div class="part-box-head">
+      <div class="part-box-head-left">
+        <h1>
+          语音播报
+          <span v-if="examInfo"
+            >-- {{ examInfo.examName }}({{ examInfo.examCode }}) --
+            场次代码({{ examInfo.activityCode }})</span
+          >
+        </h1>
+      </div>
+      <div class="part-box-head-right">
+        <el-button type="primary" @click="handleCurrentChange(0)"
+          >查询</el-button
+        >
+        <el-button type="primary" icon="icon icon-add" @click="add"
+          >新增</el-button
+        >
+        <el-button type="primary" icon="el-icon-back" @click="$router.back()"
+          >返回</el-button
+        >
+      </div>
+    </div>
+
+    <el-table :data="tableData">
+      <el-table-column width="100" prop="id" label="ID"> </el-table-column>
+      <el-table-column label="语音内容">
+        <span slot-scope="scope">{{ scope.row.content }}</span>
+      </el-table-column>
+      <el-table-column label="播报时间">
+        <span slot-scope="scope">{{ scope.row.type }}</span>
+      </el-table-column>
+      <el-table-column width="80" label="状态">
+        <span slot-scope="scope">{{
+          scope.row.enable | zeroOneEnableDisableFilter
+        }}</span>
+      </el-table-column>
+      <el-table-column width="100" label="语音">
+        <template slot-scope="scope">
+          <audio :src="scope.row.attachmentPath"></audio>
+        </template>
+      </el-table-column>
+      <el-table-column width="120" label="操作人">
+        <span slot-scope="scope">{{ scope.row.updateName }}</span>
+      </el-table-column>
+      <el-table-column sortable width="170" label="更新时间">
+        <span slot-scope="scope">{{
+          scope.row.updateTime | datetimeFilter
+        }}</span>
+      </el-table-column>
+      <el-table-column :context="_self" label="操作" width="210" fixed="right">
+        <div slot-scope="scope">
+          <el-button size="mini" type="primary" plain @click="edit(scope.row)">
+            编辑
+          </el-button>
+          <el-button
+            size="mini"
+            type="primary"
+            plain
+            @click="enableAudio(scope.row)"
+          >
+            {{ !scope.row.enable | booleanEnableDisableFilter }}
+          </el-button>
+        </div>
+      </el-table-column>
+    </el-table>
+    <div class="part-page">
+      <el-pagination
+        background
+        @current-change="handleCurrentChange"
+        :current-page="currentPage"
+        :page-size="pageSize"
+        :page-sizes="[10, 20, 50, 100, 200, 300]"
+        @size-change="handleSizeChange"
+        layout="total, sizes, prev, pager, next, jumper"
+        :total="total"
+      />
+    </div>
+
+    <ActivityAudioDialog
+      ref="ActivityAudioDialog"
+      :activity="selectedAudio"
+      @reload="searchForm"
+    />
+  </div>
+</template>
+
+<script>
+import {
+  searchActivityAudios,
+  toggleEnableActivityAudio,
+} from "@/api/examwork-activity";
+import ActivityAudioDialog from "./ActivityAudioDialog";
+
+export default {
+  name: "ActivityAudioManagement",
+  components: {
+    ActivityAudioDialog,
+  },
+  computed: {
+    activityId() {
+      return this.$route.params.activityId;
+    },
+  },
+  data() {
+    return {
+      examInfo: null,
+      tableData: [],
+      currentPage: 1,
+      pageSize: 10,
+      total: 10,
+      selectedAudio: {},
+    };
+  },
+  async created() {
+    const examInfo = window.sessionStorage.getItem("examInfo");
+    this.examInfo = examInfo ? JSON.parse(examInfo) : null;
+    this.searchForm();
+  },
+  methods: {
+    async searchForm() {
+      const res = await searchActivityAudios({
+        activityId: this.activityId,
+        pageNumber: this.currentPage,
+        pageSize: this.pageSize,
+      });
+      this.tableData = res.data.data.records;
+      this.total = res.data.data.total;
+      if (this.total > 0 && this.tableData.length === 0) {
+        this.handleCurrentChange(this.currentPage - 1);
+      }
+    },
+    handleCurrentChange(val) {
+      this.currentPage = val;
+      this.searchForm();
+    },
+    handleSizeChange(val) {
+      this.pageSize = val;
+      this.currentPage = 1;
+      this.searchForm();
+    },
+    add() {
+      this.selectedAudio = {
+        activityId: this.activityId,
+      };
+      this.$refs.ActivityAudioDialog.openDialog();
+    },
+    edit(row) {
+      this.selectedAudio = row;
+      this.$refs.ActivityAudioDialog.openDialog();
+    },
+    async enableAudio(row) {
+      await toggleEnableActivityAudio({
+        audioId: row.id,
+        enable: row.enable === 0 ? 1 : 0,
+      });
+      this.searchForm();
+    },
+  },
+};
+</script>

+ 37 - 2
src/features/examwork/ActivityManagement/ActivityManagement.vue

@@ -1,7 +1,19 @@
 <template>
   <div class="activity-management">
     <div class="part-box-head">
-      <div class="part-box-head-left"><h1>场次设置</h1></div>
+      <div class="part-box-head-left">
+        <h1>
+          场次设置
+          <span v-if="examInfo"
+            >-- {{ examInfo.examName }}({{ examInfo.examCode }})</span
+          >
+        </h1>
+      </div>
+      <div class="part-box-head-right">
+        <el-button type="primary" icon="el-icon-back" @click="$router.back()"
+          >返回</el-button
+        >
+      </div>
     </div>
     <div class="part-filter">
       <div class="part-filter-form">
@@ -56,7 +68,7 @@
           scope.row.updateTime | datetimeFilter
         }}</span>
       </el-table-column>
-      <el-table-column :context="_self" label="操作" width="210" fixed="right">
+      <el-table-column :context="_self" label="操作" width="260" fixed="right">
         <div slot-scope="scope">
           <el-button size="mini" type="primary" plain @click="edit(scope.row)">
             编辑
@@ -69,6 +81,14 @@
           >
             {{ !scope.row.enable | booleanEnableDisableFilter }}
           </el-button>
+          <el-button
+            size="mini"
+            type="primary"
+            plain
+            @click="editAudio(scope.row)"
+          >
+            语音播报
+          </el-button>
         </div>
       </el-table-column>
     </el-table>
@@ -117,6 +137,7 @@ export default {
         code: "",
         enableState: null,
       },
+      examInfo: null,
       tableData: [],
       currentPage: 1,
       pageSize: 10,
@@ -125,6 +146,8 @@ export default {
     };
   },
   async created() {
+    const examInfo = window.sessionStorage.getItem("examInfo");
+    this.examInfo = examInfo ? JSON.parse(examInfo) : null;
     this.searchForm();
   },
   methods: {
@@ -166,6 +189,18 @@ export default {
       //   params: { examId: this.examId, activityId: activity.id },
       // });
     },
+    editAudio(row) {
+      let examInfo = {};
+      if (this.examInfo) examInfo = { ...this.examInfo };
+      examInfo = { ...examInfo, activityCode: row.code };
+      window.sessionStorage.setItem("examInfo", JSON.stringify(examInfo));
+      this.$router.push({
+        name: "ActivityAudioManagement",
+        params: {
+          activityId: row.id,
+        },
+      });
+    },
     async toggleEnableActivity(activity) {
       await toggleEnableActivity({
         examId: this.examId,

+ 4 - 0
src/features/examwork/ExamManagement/ExamManagement.vue

@@ -241,6 +241,10 @@ export default {
       this.$refs.theDialog.openDialog();
     },
     editActivities(exam) {
+      window.sessionStorage.setItem(
+        "examInfo",
+        JSON.stringify({ examName: exam.name, examCode: exam.code })
+      );
       this.$router.push({
         name: "ActivityManagement",
         params: { examId: exam.id },

+ 1 - 3
src/features/examwork/StudentManagement/StudentManagementDialog.vue

@@ -34,7 +34,7 @@
         <el-table-column width="100" label="操作">
           <template slot-scope="scope">
             <el-button
-              v-if="scope.row.monitorRecord && scope.row.monitorRecord.length"
+              v-if="scope.row.videoCount"
               size="mini"
               type="primary"
               plain
@@ -134,5 +134,3 @@ export default {
   },
 };
 </script>
-
-<style></style>

+ 11 - 0
src/router/index.js

@@ -120,6 +120,17 @@ const routes = [
           relate: "ExamManagement",
         },
       },
+      {
+        path: "activity/:activityId/audio",
+        name: "ActivityAudioManagement",
+        component: () =>
+          import(
+            /* webpackChunkName: "exam" */ "../features/examwork/ActivityManagement/ActivityAudioManagement.vue"
+          ),
+        meta: {
+          relate: "ExamManagement",
+        },
+      },
       {
         path: "examstudent",
         name: "ExamStudentManagement",

+ 4 - 3
src/styles/icons.scss

@@ -6,7 +6,7 @@
   height: 16px;
   background-repeat: no-repeat;
   background-size: 100% 100%;
-  
+
   &-base {
     background-image: url(../assets/icon-base.png);
   }
@@ -102,6 +102,8 @@
   }
   &-add {
     background-image: url(../assets/icon-add.png);
+    width: 12px;
+    height: 12px;
   }
   &-copy {
     background-image: url(../assets/icon-copy.png);
@@ -295,9 +297,8 @@
   &-full {
     background-image: url(../assets/icon-full.png);
   }
-  
+
   &-waring {
     background-image: url(../assets/icon-waring.png);
   }
-
 }