Bladeren bron

1、课程/blocklu视频课程配置问题时间,变成时分秒配置
2、新增课程大纲,加入引入历史课程(等一些列逻辑)

liyanbo 4 maanden geleden
bovenliggende
commit
a5f550c0e9

+ 41 - 11
src/views/bjdx/courseconfig/CourseConfigForm.vue

@@ -15,14 +15,23 @@
       </el-form-item>
 
       <el-row>
-        <el-col :span="12">
-          <el-form-item label="课程暂停时长" prop="ccTime" required>
-            <el-input-number v-model="formData.ccTime" :min="1" :step="1" step-strictly>
-              <template #suffix><span>秒</span></template>
-            </el-input-number>
+        <el-col :span="16">
+          <el-form-item label="课程暂停时长" required>
+            <div style="display: flex; align-items: center; gap: 10px;">
+              <el-input-number v-model="formData.ccHours" :min="0" :step="1" step-strictly :precision="0" style="width: 140px;">
+                <template #suffix><span>时</span></template>
+              </el-input-number>
+              <el-input-number v-model="formData.ccMinutes" :min="0" :max="59" :step="1" step-strictly :precision="0" style="width: 140px;">
+                <template #suffix><span>分</span></template>
+              </el-input-number>
+              <el-input-number v-model="formData.ccSeconds" :min="0" :max="59" :step="1" step-strictly :precision="0" style="width: 140px;">
+                <template #suffix><span>秒</span></template>
+              </el-input-number>
+            </div>
           </el-form-item>
         </el-col>
-        <el-col :span="12">
+
+        <el-col :span="8">
           <el-form-item label="是否显示答案" prop="ccAnswerJudge" >
             <el-radio-group v-model="formData.ccAnswerJudge">
               <el-radio
@@ -34,8 +43,8 @@
             </el-radio-group>
           </el-form-item>
         </el-col>
-        <el-col :span="24">
-          <br/>
+
+        <el-col :span="12">
           <el-form-item label="问题呈现类型" prop="ccQuestSource" required>
             <el-segmented v-model="formData.ccQuestSource" :options="getStrDictOptions(DICT_TYPE.COURSE_QUEST_SHOW_TYPE)" />
           </el-form-item>
@@ -182,7 +191,10 @@ const formData = ref({
   id: undefined,
   ccCourseId: undefined, // 课程id(隐藏字段)
   courseName: undefined, // 课程名称(显示用)
-  ccTime: undefined, // 暂停时长(必填)
+  ccTime: undefined, // 暂停时长(秒数,用于提交)
+  ccHours: 0, // 暂停时长-小时
+  ccMinutes: 0, // 暂停时长-分钟
+  ccSeconds: 0, // 暂停时长-秒
   ccAnswerJudge: undefined, // 是否显示答案(必填)
   ccQuestId: undefined, // 选择的试题id
   ccQuestSource: '0', // 问题呈现类型
@@ -194,7 +206,9 @@ const formData = ref({
   tenantId: undefined,
 })
 const formRules = reactive({
-  ccTime: [{ required: true, message: t('common.required'), trigger: 'blur' }],
+  ccHours: [{ required: true, message: t('common.required'), trigger: 'blur' }],
+  ccMinutes: [{ required: true, message: t('common.required'), trigger: 'blur' }],
+  ccSeconds: [{ required: true, message: t('common.required'), trigger: 'blur' }],
   ccQuestContent: [{ required: true, message: t('common.required'), trigger: 'change' }]
 })
 const formRef = ref()
@@ -219,7 +233,15 @@ const open = async (type: string, id?: number, courseId?: number, courseName?: s
   if (id) {
     formLoading.value = true
     try {
-      formData.value = await CourseConfigApi.getCourseConfig(id)
+      const data = await CourseConfigApi.getCourseConfig(id)
+      formData.value = { ...data }
+      
+      // 将秒数转换为时分秒格式
+      if (data.ccTime) {
+        formData.value.ccHours = Math.floor(data.ccTime / 3600)
+        formData.value.ccMinutes = Math.floor((data.ccTime % 3600) / 60)
+        formData.value.ccSeconds = data.ccTime % 60
+      }
     } finally {
       formLoading.value = false
     }
@@ -275,6 +297,11 @@ const emit = defineEmits(['success'])
 const submitForm = async () => {
   // 校验表单
   await formRef.value.validate()
+  
+  // 将时分秒转换为总秒数
+  const { ccHours, ccMinutes, ccSeconds } = formData.value
+  formData.value.ccTime = (ccHours || 0) * 3600 + (ccMinutes || 0) * 60 + (ccSeconds || 0)
+  
   // 提交请求
   formLoading.value = true
   try {
@@ -301,6 +328,9 @@ const resetForm = () => {
     ccCourseId: undefined,
     courseName: undefined,
     ccTime: undefined,
+    ccHours: 0,
+    ccMinutes: 0,
+    ccSeconds: 0,
     ccAnswerJudge: undefined,
     ccQuestId: undefined, // 选择的试题id
     ccQuestSource: '0', // 问题呈现类型

+ 20 - 1
src/views/bjdx/courseconfig/index.vue

@@ -81,7 +81,11 @@
     <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
 <!--      <el-table-column label="课程配置id" align="center" prop="id" />-->
       <el-table-column label="课程名称" align="center" prop="courseName" />
-      <el-table-column label="课程暂停时长" align="center" prop="ccTime" />
+      <el-table-column label="课程暂停时长" align="center">
+        <template #default="scope">
+          {{ formatDuration(scope.row.ccTime) }}
+        </template>
+      </el-table-column>
       <el-table-column label="暂停类型" align="center" prop="ccQuestSource" >
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.COURSE_QUEST_SHOW_TYPE" :value="scope.row.ccQuestSource" />
@@ -169,6 +173,21 @@ const exportLoading = ref(false) // 导出的加载中
 const route = useRoute()
 const tenantId = ref()
 
+/** 格式化时长为中文小时分钟秒格式 */
+const formatDuration = (seconds: number | undefined): string => {
+  if (!seconds || seconds < 0) return '0秒'
+  const hours = Math.floor(seconds / 3600)
+  const minutes = Math.floor((seconds % 3600) / 60)
+  const secs = seconds % 60
+  
+  let result = ''
+  if (hours > 0) result += `${hours}小时`
+  if (minutes > 0) result += `${minutes}分钟`
+  if (secs > 0 || result === '') result += `${secs}秒`
+  
+  return result
+}
+
 /** 查询列表 */
 const getList = async () => {
   loading.value = true

+ 207 - 0
src/views/bjdx/coursetype/CourseTypeForm.vue

@@ -22,6 +22,22 @@
           @change="handleParentChange"
         />
       </el-form-item>
+      <el-form-item label="引入历史课程" prop="historyCourseTypeId">
+        <el-button type="primary" size="small" style="margin-right: 10px"
+                   title="仅年级类型可以引入历史课程"
+                   :disabled="formData.ctParentId === 0"
+                   @click="openHistoryCourseDialog">
+          引用历史课程</el-button>
+        <el-switch
+          v-if="formData.ctParentId !== 0 && formData.historyCourseTypeId"
+          v-model="isCopyAllSections"
+          inactive-text="复制课程下所有小节:"
+          size="small"
+        />
+        <div v-if="importedCourseTypeInfo" class="imported-info">
+          引入历史课程:{{ importedCourseTypeInfo }},{{ isCopyAllSections ? '(包括该课程下的所有课程小节)' : '(不包括课程小节)' }}
+        </div>
+      </el-form-item>
       <el-form-item label="课程类型节点" prop="ctTypeNode">
         <el-segmented 
           v-model="formData.ctTypeNode" 
@@ -51,10 +67,89 @@
       <el-button @click="dialogVisible = false">取 消</el-button>
     </template>
   </Dialog>
+
+  <!-- 引用历史课程弹窗 -->
+  <Dialog title="引用历史课程类型" v-model="historyCourseDialogVisible" width="1000px">
+    <!-- 搜索工作栏 -->
+    <el-form
+      :model="historyCourseFormData"
+      inline
+      label-width="100px"
+      style="margin-bottom: 20px"
+    >
+      <el-form-item label="课程类型名称" prop="searchName">
+        <el-input
+          v-model="historyCourseFormData.searchName"
+          placeholder="请输入课程类型名称"
+          clearable
+          @keyup.enter="searchHistoryCourseTypes"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="课程类型节点" prop="searchTypeNode">
+        <el-select
+          v-model="historyCourseFormData.searchTypeNode"
+          placeholder="请选择类型节点类型"
+          clearable
+          class="!w-240px"
+          @change="searchHistoryCourseTypes"
+        >
+          <el-option label="ai通识课" value="1" />
+          <el-option label="ai实操课" value="2" />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="searchHistoryCourseTypes"><el-icon><Search /></el-icon> 搜索</el-button>
+        <el-button @click="resetHistoryCourseSearch"><el-icon><Refresh /></el-icon> 重置</el-button>
+      </el-form-item>
+    </el-form>
+    
+    <el-table
+      :data="historyCourseTypes"
+      style="width: 100%; height: 400px; max-height: 500px; overflow: auto"
+      border
+      @row-click="selectHistoryCourseType"
+      :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
+      :stripe="true"
+      :highlight-current-row="true"
+      :show-overflow-tooltip="true"
+      :default-expand-all="false"
+      row-key="id"
+      current-row-key="id"
+    >
+      <el-table-column label="课程类型名称" align="center" prop="ctType"/>
+      <el-table-column label="节点类型" align="center" prop="ctTypeNode">
+        <template #default="scope">
+          <el-tag type="info" v-if="scope.row.ctTypeNode === '0'">年级</el-tag>
+          <el-tag type="warning" v-else-if="scope.row.ctTypeNode === '1'">ai通识课</el-tag>
+          <el-tag type="warning" v-else>ai实操课</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="封面" align="center" prop="ctTypeImage">
+        <template #default="scope">
+          <el-image
+            v-if="scope.row.ctTypeImage"
+            :src="scope.row.ctTypeImage"
+            :preview-src-list="[scope.row.ctTypeImage]"
+            style="width: 100px; height: 80px; object-fit: cover"
+          />
+        </template>
+      </el-table-column>
+      <el-table-column label="排序" align="center" prop="ctTypeSort" width="80" />
+    </el-table>
+
+    <template #footer>
+      <span v-if="selectedHistoryCourseType" style="margin-right: 20px">引入历史课程:{{selectedHistoryCourseType?.ctType}}</span>
+      <el-button @click="confirmHistoryCourseType" type="primary" :disabled="!selectedHistoryCourseType">确 定</el-button>
+      <el-button @click="historyCourseDialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
 </template>
 <script setup lang="ts">
 import { CourseTypeApi, CourseTypeVO } from '@/api/bjdx/coursetype'
 import { defaultProps, handleTree } from '@/utils/tree'
+import { Search, Refresh } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
 
 /** 课程-类型 表单 */
 defineOptions({ name: 'CourseTypeForm' })
@@ -67,6 +162,7 @@ const dialogTitle = ref('') // 弹窗的标题
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const formType = ref('') // 表单的类型:create - 新增;update - 修改
 const formData = ref({
+  historyCourseTypeId: undefined,
   id: undefined,
   ctType: undefined,
   ctTypeImage: undefined,
@@ -84,6 +180,42 @@ const formRules = reactive({
 })
 const formRef = ref() // 表单 Ref
 const courseTypeTree = ref() // 树形结构
+const historyCourseDialogVisible = ref(false) // 历史课程弹窗可见性
+const historyCourseTypes = ref<CourseTypeVO[]>([]) // 历史课程类型列表
+const historyCourseFormData = reactive({
+  searchName: '', // 搜索名称
+  searchTypeNode: undefined // 搜索节点类型
+}) // 历史课程表单数据
+const selectedHistoryCourseType = ref<CourseTypeVO>() // 选中的历史课程类型
+const importedCourseTypeInfo = ref('') // 引入的课程类型信息
+
+// 开关状态变量
+const switchValue = ref(false)
+// 计算属性:控制是否复制所有小节的开关状态
+const isCopyAllSections = computed({
+  get() {
+    // 有historyCourseTypeId值则开启开关
+    return switchValue.value
+  },
+  set(value) {
+    if (value) {
+      // 当尝试打开开关但没有引入课程时,显示提示
+      if (!formData.value.historyCourseTypeId) {
+        ElMessage.warning('需要重新引入课程')
+        return
+      }
+    } else {
+      // 当开关关闭时,清除historyCourseTypeId字段和导入信息
+      formData.value.historyCourseTypeId = undefined
+      importedCourseTypeInfo.value = ''
+      switchValue.value = false
+    }
+    // 只有在有效的情况下才更新开关状态
+    if (formData.value.historyCourseTypeId || !value) {
+      switchValue.value = value
+    }
+  }
+})
 
 // 计算属性:判断父级ID是否为根节点(0)
 const isRootParent = computed(() => {
@@ -104,6 +236,12 @@ const handleParentChange = () => {
   if (isRootParent.value) {
     // 当父级是根节点时,强制设置为年级类型
     formData.value.ctTypeNode = '0'
+    // 清除历史课程ID
+    formData.value.historyCourseTypeId = null
+    // 关闭复制小节开关
+    switchValue.value = false
+    // 清除导入信息
+    importedCourseTypeInfo.value = ''
   } else {
     // 当父级不是根节点时,如果当前类型是年级,则改为通识课
     if (formData.value.ctTypeNode === '0' || formData.value.ctTypeNode === 0) {
@@ -149,6 +287,9 @@ defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 /** 提交表单 */
 const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
 const submitForm = async () => {
+
+  console.log('submitForm', formData.value)
+  return;
   // 校验表单
   await formRef.value.validate()
   // 提交请求
@@ -194,4 +335,70 @@ const getCourseTypeTree = async () => {
   root.children = handleTree(filteredData, 'id', 'ctParentId')
   courseTypeTree.value.push(root)
 }
+
+/** 打开历史课程弹窗 */
+const openHistoryCourseDialog = async () => {
+  historyCourseDialogVisible.value = true
+  await getHistoryCourseTypes()
+}
+
+/** 获取历史课程类型数据 */
+const getHistoryCourseTypes = async () => {
+  const params = {
+    ctType: historyCourseFormData.searchName,
+    ctTypeNode: historyCourseFormData.searchTypeNode
+  }
+  const result = await CourseTypeApi.getCourseTypeList(params)
+  historyCourseTypes.value = handleTree(result, 'id', 'ctParentId')
+}
+
+/** 搜索历史课程类型 */
+const searchHistoryCourseTypes = () => {
+  getHistoryCourseTypes()
+}
+
+/** 重置搜索条件 */
+const resetHistoryCourseSearch = () => {
+  historyCourseFormData.searchName = ''
+  historyCourseFormData.searchTypeNode = undefined
+  getHistoryCourseTypes()
+}
+
+/** 选择历史课程类型 */
+const selectHistoryCourseType = (courseType: CourseTypeVO) => {
+  if (courseType.ctTypeNode !== '0') {
+    selectedHistoryCourseType.value = courseType
+  }
+}
+
+/** 确认选择历史课程类型 */
+const confirmHistoryCourseType = () => {
+  if (selectedHistoryCourseType.value) {
+    // 根据历史课程类型数据填充表单
+    formData.value.historyCourseTypeId = selectedHistoryCourseType.value.id || ''
+    formData.value.ctType = selectedHistoryCourseType.value.ctType || ''
+    formData.value.ctTypeImage = selectedHistoryCourseType.value.ctTypeImage || ''
+    formData.value.ctTypeNode = selectedHistoryCourseType.value.ctTypeNode || '0'
+    formData.value.ctTypeSort = selectedHistoryCourseType.value.ctTypeSort || 0
+    formData.value.ctTypeDescribe = selectedHistoryCourseType.value.ctTypeDescribe || ''
+
+    // 更新引入信息
+    importedCourseTypeInfo.value = selectedHistoryCourseType.value.ctType
+    // 设置historyCourseTypeId
+    formData.value.historyCourseTypeId = selectedHistoryCourseType.value.id
+    // 自动打开开关
+    switchValue.value = true
+
+    historyCourseDialogVisible.value = false
+  }
+}
 </script>
+
+<style scoped>
+.imported-info {
+  margin-top: 8px;
+  color: #606266;
+  font-size: 12px;
+  line-height: 1.5;
+}
+</style>

+ 62 - 13
src/views/blockly/blocklyConfig/BlocklyConfigForm.vue

@@ -15,14 +15,44 @@
       </el-form-item>
 
       <el-row>
-        <el-col :span="12">
-          <el-form-item label="课程暂停时长" prop="ccTime" required>
-            <el-input-number v-model="formData.ccTime" :min="1" :step="1" step-strictly>
-              <template #suffix><span>秒</span></template>
-            </el-input-number>
+        <el-col :span="16">
+          <el-form-item label="课程暂停时长" required>
+            <el-row :gutter="10">
+              <el-col :span="7">
+                <el-input-number
+                  v-model="formData.ccHours"
+                  :min="0"
+                  :step="1"
+                  step-strictly
+                  placeholder="时"
+                />
+              </el-col>
+              <el-col :span="1" class="flex items-center justify-center">:</el-col>
+              <el-col :span="7">
+                <el-input-number
+                  v-model="formData.ccMinutes"
+                  :min="0"
+                  :max="59"
+                  :step="1"
+                  step-strictly
+                  placeholder="分"
+                />
+              </el-col>
+              <el-col :span="1" class="flex items-center justify-center">:</el-col>
+              <el-col :span="7">
+                <el-input-number
+                  v-model="formData.ccSeconds"
+                  :min="0"
+                  :max="59"
+                  :step="1"
+                  step-strictly
+                  placeholder="秒"
+                />
+              </el-col>
+            </el-row>
           </el-form-item>
         </el-col>
-        <el-col :span="12">
+        <el-col :span="8">
           <el-form-item label="是否显示答案" prop="ccAnswerJudge" >
             <el-radio-group v-model="formData.ccAnswerJudge">
               <el-radio
@@ -168,6 +198,7 @@ import { BlocklyConfigApi, BlocklyConfigVO} from '@/api/blockly/blocklyConfig'
 import { CourseQuestionApi} from '@/api/bjdx/coursequestion'
 
 
+
 /** 合并后的课程配置与试题选择表单 */
 defineOptions({ name: 'BlocklyConfigWithQuestionForm' })
 
@@ -183,7 +214,10 @@ const formData = ref({
   id: undefined,
   ccCourseId: undefined, // 课程id(隐藏字段)
   courseName: undefined, // 课程名称(显示用)
-  ccTime: undefined, // 暂停时长(必填)
+  ccTime: 0, // 暂停时长总秒数(用于提交)
+  ccHours: 0, // 小时
+  ccMinutes: 0, // 分钟
+  ccSeconds: 0, // 秒
   ccAnswerJudge: undefined, // 是否显示答案(必填)
   ccQuestId: undefined, // 选择的试题id
   ccQuestSource: '0', // 问题呈现类型
@@ -195,12 +229,14 @@ const formData = ref({
   tenantId: undefined,
 })
 const formRules = reactive({
-  ccTime: [{ required: true, message: t('common.required'), trigger: 'blur' }],
+  ccHours: [{ required: true, message: t('common.required'), trigger: 'blur' }],
+  ccMinutes: [{ required: true, message: t('common.required'), trigger: 'blur' }],
+  ccSeconds: [{ required: true, message: t('common.required'), trigger: 'blur' }],
   ccQuestContent: [{ required: true, message: t('common.required'), trigger: 'change' }]
 })
 const formRef = ref()
 
-// 新增:试题列表相关状态
+// 试题列表相关状态
 const questionLoading = ref(false)
 const questionList = ref<any[]>([])
 const queryParams = reactive({
@@ -220,7 +256,13 @@ const open = async (type: string, id?: number, courseId?: number, courseName?: s
   if (id) {
     formLoading.value = true
     try {
-      formData.value = await BlocklyConfigApi.getCourseConfig(id)
+      const data = await BlocklyConfigApi.getCourseConfig(id)
+      formData.value = data
+      // 转换秒数为时分秒
+      const totalSeconds = data.ccTime || 0
+      formData.value.ccHours = Math.floor(totalSeconds / 3600)
+      formData.value.ccMinutes = Math.floor((totalSeconds % 3600) / 60)
+      formData.value.ccSeconds = totalSeconds % 60
     } finally {
       formLoading.value = false
     }
@@ -250,7 +292,7 @@ const questQueryParams = reactive({
 const loadQuestionList = async () => {
   questionLoading.value = true
   try {
-    const res = await BlocklyQuestionApi.getCourseQuestionPage(questQueryParams)
+    const res = await CourseQuestionApi.getCourseQuestionPage(questQueryParams)
     questionList.value = res.list
     questionTotal.value = res.total // 更新总条数
   } finally {
@@ -279,6 +321,10 @@ const submitForm = async () => {
   // 提交请求
   formLoading.value = true
   try {
+    // 计算总秒数
+    const ccTime = formData.value.ccHours * 3600 + formData.value.ccMinutes * 60 + formData.value.ccSeconds
+    formData.value.ccTime = ccTime
+    
     const data = formData.value as unknown as BlocklyConfigVO
     if (formType.value === 'create') {
       await BlocklyConfigApi.createCourseConfig(data)
@@ -295,13 +341,16 @@ const submitForm = async () => {
   }
 }
 
-/** 重置表单(新增:重置试题相关状态) */
+/** 重置表单(重置试题相关状态) */
 const resetForm = () => {
   formData.value = {
     id: undefined,
     ccCourseId: undefined,
     courseName: undefined,
-    ccTime: undefined,
+    ccTime: 0,
+    ccHours: 0,
+    ccMinutes: 0,
+    ccSeconds: 0,
     ccAnswerJudge: undefined,
     ccQuestId: undefined, // 选择的试题id
     ccQuestSource: '0', // 问题呈现类型

+ 31 - 5
src/views/blockly/blocklyConfig/index.vue

@@ -59,7 +59,7 @@
           plain
           v-if="tenantId == getTenantId()"
           @click="openForm('create', null, queryParams.ccCourseId, queryParams.courseName)"
-          v-hasPermi="['blockly:course-config:create']"
+          v-hasPermi="['blockly:blockly-config:create']"
         >
           <Icon icon="ep:plus" class="mr-5px" /> 新增
         </el-button>
@@ -68,7 +68,7 @@
           plain
           @click="handleExport"
           :loading="exportLoading"
-          v-hasPermi="['blockly:course-config:export']"
+          v-hasPermi="['blockly:blockly-config:export']"
         >
           <Icon icon="ep:download" class="mr-5px" /> 导出
         </el-button>
@@ -81,7 +81,11 @@
     <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
       <!--      <el-table-column label="课程配置id" align="center" prop="id" />-->
       <el-table-column label="课程名称" align="center" prop="courseName" />
-      <el-table-column label="课程暂停时长" align="center" prop="ccTime" />
+      <el-table-column label="课程暂停时长" align="center">
+        <template #default="scope">
+          {{ formatDuration(scope.row.ccTime) }}
+        </template>
+      </el-table-column>
       <el-table-column label="暂停类型" align="center" prop="ccQuestSource">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.COURSE_QUEST_SHOW_TYPE" :value="scope.row.ccQuestSource" />
@@ -107,7 +111,7 @@
             type="primary"
             v-if="scope.row.tenantId == getTenantId()"
             @click="openForm('update', scope.row.id, queryParams.ccCourseId, queryParams.courseName)"
-            v-hasPermi="['blockly:course-config:update']"
+            v-hasPermi="['blockly:blockly-config:update']"
           >
             编辑
           </el-button>
@@ -116,7 +120,7 @@
             type="danger"
             v-if="scope.row.tenantId == getTenantId()"
             @click="handleDelete(scope.row.id)"
-            v-hasPermi="['blockly:course-config:delete']"
+            v-hasPermi="['blockly:blockly-config:delete']"
           >
             删除
           </el-button>
@@ -170,6 +174,28 @@ const exportLoading = ref(false) // 导出的加载中
 const route = useRoute()
 const tenantId = ref()
 
+/** 格式化时长为中文格式 */
+const formatDuration = (seconds: number) => {
+  if (!seconds || seconds < 0) {
+    return '0秒'
+  }
+  const hours = Math.floor(seconds / 3600)
+  const minutes = Math.floor((seconds % 3600) / 60)
+  const secs = seconds % 60
+  
+  let result = ''
+  if (hours > 0) {
+    result += `${hours}小时`
+  }
+  if (minutes > 0) {
+    result += `${minutes}分钟`
+  }
+  if (secs > 0 || result === '') {
+    result += `${secs}秒`
+  }
+  return result
+}
+
 /** 查询列表 */
 const getList = async () => {
   loading.value = true