index.vue 53 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775
  1. <template>
  2. <fs-page>
  3. <el-row class="document-el-row">
  4. <el-col :span="3">
  5. <div class="document-box document-left-box">
  6. <DocumentTreeCom
  7. ref="documentTreeRef"
  8. :treeData="documentTreeData"
  9. @treeClick="handleTreeClick"
  10. @updateDocument="handleUpdateDocument"
  11. @deleteDocument="handleDeleteDocument"
  12. />
  13. </div>
  14. </el-col>
  15. <el-col :span="21">
  16. <div class="document-box document-right-box">
  17. <fs-crud ref="crudRef" v-bind="crudBinding">
  18. <!-- 行操作按钮插槽 -->
  19. <template #cell-rowHandle-middle="scope">
  20. <!-- 绑定开放问题按钮 -->
  21. <el-button
  22. v-if="scope.row.question_form === 5"
  23. type="text"
  24. @click="bindParentQuestion(scope.row)">
  25. <el-icon><Link /></el-icon>绑定开放问题
  26. </el-button>
  27. <!-- 编辑按钮 -->
  28. </template>
  29. <template #form_file="scope">
  30. <!-- 只在编辑和添加模式下显示上传功能 -->
  31. <template v-if="scope.mode !== 'view'">
  32. <el-upload :action="getBaseURL() + 'api/system/minioupload/'"
  33. :headers="{
  34. Authorization: 'JWT ' + Session.get('token')
  35. }"
  36. :multiple="false"
  37. :on-success="(response: any, file: any) => handleUploadSuccess(response, file, scope)"
  38. :drag="false"
  39. :show-file-list="false">
  40. <el-button type="primary" icon="plus">上传</el-button>
  41. </el-upload>
  42. <div v-if="uploadedFile" class="uploaded-file-info mt-2">
  43. <el-card shadow="hover" class="w-full">
  44. <div class="flex items-center justify-between">
  45. <div class="flex items-center">
  46. <el-icon class="mr-2"><Document /></el-icon>
  47. <span>{{ uploadedFile.name }}</span>
  48. </div>
  49. <div class="flex items-center">
  50. <span class="text-gray-500 text-sm mr-4">{{ formatFileSize(uploadedFile.file_size) }}</span>
  51. <el-button type="danger" link @click="handleRemoveFile(scope)">
  52. <el-icon><Delete /></el-icon>
  53. </el-button>
  54. </div>
  55. </div>
  56. </el-card>
  57. </div>
  58. </template>
  59. <!-- 查看模式下只显示文件名 -->
  60. <template v-else>
  61. <div class="flex items-center justify-between">
  62. <span>{{ scope.form.fileName }}</span>
  63. <el-button v-if="isPreviewable(scope.form.file_type)" type="primary" link @click="previewFile(scope.form)">
  64. <el-icon class="mr-1"><View /></el-icon>预览
  65. </el-button>
  66. </div>
  67. </template>
  68. </template>
  69. </fs-crud>
  70. </div>
  71. </el-col>
  72. </el-row>
  73. <el-drawer v-model="drawerVisible" title="添加分类" direction="rtl" size="500px" :close-on-click-modal="false" :before-close="handleDrawerClose">
  74. <DocumentFormCom
  75. v-if="drawerVisible"
  76. :initFormData="drawerFormData"
  77. :cacheData="documentTreeCacheData"
  78. :treeData="documentTreeData"
  79. @drawerClose="handleDrawerClose"
  80. />
  81. </el-drawer>
  82. <!-- 添加自定义查看详情对话框 -->
  83. <!-- <el-dialog
  84. v-model="detailDialogVisible"
  85. title="文档详情"
  86. width="50%"
  87. destroy-on-close
  88. >
  89. <div ref="printArea">
  90. <el-descriptions :column="2" size="small" border>
  91. <el-descriptions-item label-align="right" label="文档名称">{{ detailData.name || '-' }}</el-descriptions-item>
  92. <el-descriptions-item label-align="right" label="文档类型">
  93. {{ getDocTypeName(detailData.doc_type) || '-' }}
  94. </el-descriptions-item>
  95. <el-descriptions-item label-align="right" label="文件分类">
  96. {{ getCategoryName(detailData.category) || '-' }}
  97. </el-descriptions-item>
  98. <el-descriptions-item label-align="right" label="所属项目">
  99. {{ getProjectName(detailData.project) || '-' }}
  100. </el-descriptions-item>
  101. <el-descriptions-item label-align="right" label="文档版本">{{ detailData.version || '-' }}</el-descriptions-item>
  102. <el-descriptions-item label-align="right" label="文件类型">{{ detailData.file_type || '-' }}</el-descriptions-item>
  103. <el-descriptions-item label-align="right" label="文件大小">{{ formatFileSize(detailData.file_size) || '-' }}</el-descriptions-item>
  104. <el-descriptions-item label-align="right" label="状态">
  105. <el-tag :type="detailData.status ? 'success' : 'info'">
  106. {{ detailData.status ? '启用' : '禁用' }}
  107. </el-tag>
  108. </el-descriptions-item>
  109. <el-descriptions-item label-align="right" label="文档描述" :span="2">{{ detailData.doc_desc || '-' }}</el-descriptions-item>
  110. <el-descriptions-item label-align="right" label="更新时间">{{ detailData.update_datetime || '-' }}</el-descriptions-item>
  111. </el-descriptions>
  112. </div>
  113. <template #footer>
  114. <span class="dialog-footer">
  115. <el-button type="primary" @click="handlePrint">
  116. <el-icon><Printer /></el-icon>打印
  117. </el-button>
  118. <el-button @click="detailDialogVisible = false">关闭</el-button>
  119. </span>
  120. </template>
  121. </el-dialog> -->
  122. <!-- 修改视频上传对话框为表单格式 -->
  123. <el-dialog
  124. v-model="videoUploadDialogVisible"
  125. title="上传数字人视频及字幕"
  126. width="600px"
  127. destroy-on-close
  128. >
  129. <el-form
  130. ref="videoFormRef"
  131. :model="videoForm"
  132. :rules="videoFormRules"
  133. label-width="100px"
  134. label-position="top"
  135. >
  136. <el-form-item label="数字人视频" prop="videoUrl">
  137. <!-- <div class="upload-section"> -->
  138. <el-upload
  139. class="video-uploader"
  140. :action="getBaseURL() + 'api/system/admin_upload/'"
  141. :headers="{
  142. Authorization: 'JWT ' + Session.get('token')
  143. }"
  144. :data="{
  145. tenant_id:1
  146. }"
  147. :multiple="false"
  148. :on-success="handleVideoUploadSuccess"
  149. :on-error="handleUploadError"
  150. :before-upload="beforeVideoUpload"
  151. :show-file-list="false"
  152. ><!-- accept="video/*" -->
  153. <div v-if="videoInfo.url" class="video-preview">
  154. <video :src="videoInfo.url" controls class="video-player"></video>
  155. </div>
  156. <el-button v-else type="primary" icon="Plus">选择视频文件</el-button>
  157. <div v-if="videoInfo.name" class="file-info">
  158. <span>{{ videoInfo.name }}</span>
  159. <span class="file-size">{{ formatFileSize(videoInfo.size) }}</span>
  160. </div>
  161. </el-upload>
  162. <!-- </div> -->
  163. </el-form-item>
  164. <el-form-item label="字幕内容" prop="subtitleContent">
  165. <!-- <div class="upload-section"> -->
  166. <el-input
  167. v-model="videoForm.subtitleContent"
  168. type="textarea"
  169. :rows="4"
  170. placeholder="请输入数字人视频字幕内容"
  171. ></el-input>
  172. <!-- <el-upload
  173. class="subtitle-uploader mt-2"
  174. :action="getBaseURL() + 'api/system/minioupload/'"
  175. :headers="{
  176. Authorization: 'JWT ' + Session.get('token')
  177. }"
  178. :multiple="false"
  179. :on-success="handleSubtitleUploadSuccess"
  180. :on-error="handleUploadError"
  181. :before-upload="beforeSubtitleUpload"
  182. :show-file-list="false"
  183. accept=".srt,.vtt,.ass"
  184. >
  185. <div v-if="subtitleInfo.url" class="file-preview">
  186. <el-icon class="file-icon"><Document /></el-icon>
  187. <div class="file-info">
  188. <span>{{ subtitleInfo.name }}</span>
  189. <span class="file-size">{{ formatFileSize(subtitleInfo.size) }}</span>
  190. </div>
  191. </div>
  192. <el-button v-else type="primary" icon="Plus">选择字幕文件</el-button>
  193. </el-upload> -->
  194. <!-- </div> -->
  195. </el-form-item>
  196. </el-form>
  197. <template #footer>
  198. <span class="dialog-footer">
  199. <el-button @click="videoUploadDialogVisible = false">取消</el-button>
  200. <el-button type="primary" @click="submitVideoForm" :disabled="!videoInfo.url">保存</el-button>
  201. </span>
  202. </template>
  203. </el-dialog>
  204. <!-- 添加排序对话框 -->
  205. <el-dialog
  206. v-model="sortDialogVisible"
  207. title="修改排序"
  208. width="400px"
  209. destroy-on-close
  210. >
  211. <el-form
  212. ref="sortFormRef"
  213. :model="sortForm"
  214. :rules="sortFormRules"
  215. label-width="100px"
  216. >
  217. <el-form-item label="排序值" prop="sequence_number">
  218. <el-input-number
  219. v-model="sortForm.sequence_number"
  220. :min="0"
  221. :max="9999"
  222. style="width: 100%"
  223. />
  224. </el-form-item>
  225. <div class="form-helper">数值越小,排序越靠前</div>
  226. </el-form>
  227. <template #footer>
  228. <span class="dialog-footer">
  229. <el-button @click="sortDialogVisible = false">取消</el-button>
  230. <el-button type="primary" @click="submitSortForm">保存</el-button>
  231. </span>
  232. </template>
  233. </el-dialog>
  234. <!-- 添加职位问题对话框 -->
  235. <el-dialog
  236. v-model="positionQuestionDialogVisible"
  237. title="职位问题"
  238. width="60%"
  239. destroy-on-close
  240. >
  241. <el-form :model="positionQuestionForm" label-width="80px">
  242. <el-form-item label="职位">
  243. <el-select v-model="positionQuestionForm.position" placeholder="请选择" style="width: 25%">
  244. <el-option
  245. v-for="item in positionOptions"
  246. :key="item.value"
  247. :label="item.label"
  248. :value="item.value"
  249. />
  250. </el-select>
  251. </el-form-item>
  252. <el-form-item label="职位问题">
  253. <div class="question-selection-container">
  254. <div class="question-search">
  255. <div class="search-row">
  256. <el-input
  257. style="width: 200px;"
  258. v-model="questionSearchKeyword"
  259. placeholder="搜索问题"
  260. clearable
  261. @input="filterQuestions"
  262. >
  263. <template #prefix>
  264. <el-icon><Search /></el-icon>
  265. </template>
  266. </el-input>
  267. <el-select
  268. v-model="selectedQuestionCategory"
  269. placeholder="问题分类"
  270. clearable
  271. @change="filterQuestions"
  272. style="margin-left: 10px; width: 200px;"
  273. >
  274. <el-option
  275. v-for="item in questionCategoryOptions"
  276. :key="item.value"
  277. :label="item.label"
  278. :value="item.value"
  279. />
  280. </el-select>
  281. </div>
  282. </div>
  283. <div class="question-table-container">
  284. <el-table
  285. v-loading="questionsLoading"
  286. :data="filteredQuestionOptions"
  287. style="width: 100%"
  288. height="300"
  289. @selection-change="handleQuestionSelectionChange"
  290. >
  291. <el-table-column type="selection" width="55" />
  292. <el-table-column prop="label" label="问题" show-overflow-tooltip />
  293. <el-table-column prop="category_id.name" label="分类" />
  294. <el-table-column prop="question_form_name" label="题目形式" show-overflow-tooltip />
  295. <el-table-column prop="chinese_explanation" label="描述" show-overflow-tooltip />
  296. </el-table>
  297. <div class="pagination-container">
  298. <el-pagination
  299. v-model:current-page="questionPagination.currentPage"
  300. v-model:page-size="questionPagination.pageSize"
  301. :page-sizes="[10, 20, 50, 100]"
  302. layout="total, sizes, prev, pager, next, jumper"
  303. :total="questionPagination.total"
  304. @size-change="handleQuestionSizeChange"
  305. @current-change="handleQuestionCurrentChange"
  306. />
  307. </div>
  308. </div>
  309. </div>
  310. </el-form-item>
  311. <el-form-item label="排序">
  312. <el-input style="width: 25%" v-model="positionQuestionForm.sequence" placeholder="请输入排序值" />
  313. </el-form-item>
  314. </el-form>
  315. <!-- <div class="question-list-container">
  316. <h3>已选问题列表</h3>
  317. <el-table :data="selectedQuestions" style="width: 100%">
  318. <el-table-column prop="label" label="问题" show-overflow-tooltip />
  319. <el-table-column prop="question_form_name" label="问题类型" show-overflow-tooltip />
  320. <el-table-column prop="chinese_explanation" label="中文释义" show-overflow-tooltip />
  321. <el-table-column label="操作" width="120">
  322. <template #default="scope">
  323. <el-button type="danger" link @click="removeQuestion(scope.row)">
  324. <el-icon><Delete /></el-icon>
  325. </el-button>
  326. </template>
  327. </el-table-column>
  328. </el-table>
  329. </div> -->
  330. <template #footer>
  331. <span class="dialog-footer">
  332. <el-button @click="positionQuestionDialogVisible = false">取消</el-button>
  333. <el-button type="primary" @click="submitPositionQuestionForm">保存</el-button>
  334. </span>
  335. </template>
  336. </el-dialog>
  337. <!-- 修改绑定开放问题的对话框 -->
  338. <el-dialog
  339. v-model="bindParentQuestionDialogVisible"
  340. title="绑定开放问题"
  341. width="50%"
  342. :before-close="handleBindDialogClose"
  343. >
  344. <div>
  345. <!-- <div class="search-container" style="margin-bottom: 15px;">
  346. <el-input
  347. v-model="openQuestionKeyword"
  348. placeholder="搜索开放问题"
  349. clearable
  350. @input="searchOpenQuestions"
  351. style="width: 100%"
  352. >
  353. <template #prefix>
  354. <el-icon><Search /></el-icon>
  355. </template>
  356. </el-input>
  357. </div> -->
  358. <p>请选择要关联的开放问题:</p>
  359. <el-select
  360. v-model="selectedParentQuestionId"
  361. filterable
  362. clearable
  363. placeholder="请选择开放问题"
  364. style="width: 100%"
  365. :loading="openQuestionsLoading"
  366. >
  367. <el-option
  368. v-for="item in openQuestionsList"
  369. :key="item.id"
  370. :label="item.question"
  371. :value="item.id"
  372. />
  373. </el-select>
  374. </div>
  375. <template #footer>
  376. <span class="dialog-footer">
  377. <el-button @click="handleBindDialogClose">取消</el-button>
  378. <el-button type="primary" @click="confirmBindParentQuestion">确认</el-button>
  379. </span>
  380. </template>
  381. </el-dialog>
  382. </fs-page>
  383. </template>
  384. <script lang="ts" setup name="documentList">
  385. import { ref, onMounted, onBeforeUnmount, computed } from 'vue';
  386. import { useFs } from '@fast-crud/fast-crud';
  387. import XEUtils from 'xe-utils';
  388. import { ElMessageBox, ElMessage } from 'element-plus';
  389. import { createCrudOptions } from './crud';
  390. import { Session } from '/@/utils/storage';
  391. import { getBaseURL } from '/@/utils/baseUrl';
  392. import { Document, Delete, View, Printer, Search, Link } from '@element-plus/icons-vue';
  393. import { successNotification } from '/@/utils/message';
  394. import DocumentTreeCom from './components/DocumentTreeCom/index.vue';
  395. import DocumentFormCom from './components/DocumentFormCom/index.vue';
  396. import { GetDocumentTree, DeleteDocumentCategory, UpdateDocument ,UpdateSequence, GetInterviewQuestions,AddDocument,
  397. GetCategoryList, GetTagList
  398. } from './api';
  399. import { useRouter } from 'vue-router';
  400. /* import { print } from '/@/utils/print'; */
  401. const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
  402. const router = useRouter();
  403. // 添加 uploadedFile 类型定义
  404. interface UploadedFileInfo {
  405. name: string;
  406. file_path: string;
  407. file_type: string;
  408. file_size: number;
  409. doc_type: string;
  410. category: string;
  411. status: boolean;
  412. doc_desc: string;
  413. version: string;
  414. file: string;
  415. }
  416. // 修改 uploadedFile 的类型
  417. const uploadedFile = ref<UploadedFileInfo | null>(null);
  418. // 树形结构相关数据
  419. const documentTreeData = ref([]);
  420. const documentTreeCacheData = ref<any[]>([]);
  421. const drawerVisible = ref(false);
  422. const drawerFormData = ref({});
  423. const documentTreeRef = ref<DocumentTreeRef | null>(null);
  424. // 添加一个响应式变量来存储当前选中的分类ID
  425. const selectedCategoryId = ref('');
  426. // 添加详情对话框相关数据
  427. const detailDialogVisible = ref(false);
  428. const detailData = ref({
  429. id: 0,
  430. name: '',
  431. doc_type: '',
  432. category: '',
  433. project: '',
  434. version: '',
  435. file_type: '',
  436. file_size: 0,
  437. status: true,
  438. doc_desc: '',
  439. update_datetime: '',
  440. file_path: '',
  441. file: ''
  442. });
  443. // 添加打印区域的ref
  444. const printArea = ref(null);
  445. // 视频和字幕上传相关状态
  446. const videoInfo = ref({
  447. url: '',
  448. name: '',
  449. size: 0,
  450. file_type: ''
  451. });
  452. const subtitleInfo = ref({
  453. url: '',
  454. name: '',
  455. size: 0,
  456. file_type: ''
  457. });
  458. // 视频上传对话框状态
  459. const videoUploadDialogVisible = ref(false);
  460. const currentRow = ref<QuestionRow | null>(null);
  461. // 添加字幕内容输入框
  462. const subtitleContent = ref('');
  463. // 添加表单引用
  464. const videoFormRef = ref<{
  465. validate: (callback: (valid: boolean) => void) => void;
  466. } | null>(null);
  467. // 添加表单数据对象
  468. const videoForm = ref({
  469. videoUrl: '',
  470. subtitleContent: ''
  471. });
  472. // 添加表单验证规则
  473. const videoFormRules = {
  474. videoUrl: [
  475. { required: true, message: '请上传数字人视频', trigger: 'change' }
  476. ]
  477. };
  478. // 添加排序对话框相关状态
  479. const sortDialogVisible = ref(false);
  480. const sortForm = ref({
  481. sequence_number: 0
  482. });
  483. const sortFormRules = {
  484. sequence_number: [
  485. { required: true, message: '请输入排序值', trigger: 'blur' },
  486. { type: 'number', message: '排序值必须为数字', trigger: 'blur' }
  487. ]
  488. };
  489. const sortFormRef = ref<any>(null);
  490. // 添加类型定义
  491. interface DocumentTreeRef {
  492. treeRef?: {
  493. currentNode?: {
  494. parent?: {
  495. data: any;
  496. };
  497. };
  498. };
  499. }
  500. // 修改 currentRow 的类型
  501. interface QuestionRow {
  502. id?: number;
  503. question_id?: string | number;
  504. sequence_number?: number;
  505. }
  506. // 职位问题对话框相关状态
  507. const positionQuestionDialogVisible = ref(false);
  508. const positionQuestionForm = ref({
  509. position: '',
  510. questions: [] as string[],
  511. sequence: '0'
  512. });
  513. // 职位选项和问题选项
  514. const positionOptions = ref<{ value: string, label: string }[]>([]);
  515. const questionOptions = ref<{ value: string, label: string, chinese_explanation: string, question_form_name: string ,category_id: string}[]>([]);
  516. // 计算属性:已选问题列表
  517. const selectedQuestions = computed(() => {
  518. // 根据已选ID查找完整的问题对象
  519. return positionQuestionForm.value.questions.map(questionId => {
  520. // 先在当前页面的问题中查找
  521. const question = questionOptions.value.find(q => q.value === questionId);
  522. if (question) return question;
  523. // 如果在当前页面找不到,可能需要额外请求
  524. // 这里简化处理,只返回ID
  525. return { value: questionId, label: `问题 ${questionId}`, chinese_explanation: '' ,question_form_name: '' ,category_id: ''};
  526. });
  527. });
  528. // 问题加载状态
  529. const questionsLoading = ref(false);
  530. // 问题分页参数
  531. const questionPagination = ref({
  532. currentPage: 1,
  533. pageSize: 10,
  534. total: 0
  535. });
  536. // 添加问题分类选项
  537. const questionCategoryOptions = ref<{ value: string, label: string }[]>([]);
  538. const selectedQuestionCategory = ref('');
  539. // 修改 loadQuestionOptions 函数,添加加载分类选项的逻辑
  540. const loadQuestionOptions = async () => {
  541. try {
  542. questionsLoading.value = true;
  543. // 构建请求参数,添加分类筛选
  544. const requestParams: any = {
  545. page: questionPagination.value.currentPage,
  546. limit: questionPagination.value.pageSize,
  547. tenant_id: 1,
  548. keyword: questionSearchKeyword.value
  549. };
  550. // 如果选择了分类,添加到请求参数
  551. if (selectedQuestionCategory.value) {
  552. requestParams.category_id = selectedQuestionCategory.value;
  553. }
  554. const response = await GetInterviewQuestions(requestParams);
  555. if (response && response.code === 2000 && response.data) {
  556. // 转换数据格式
  557. questionOptions.value = response.data.items.map((item: any) => ({
  558. value: item.id.toString(),
  559. label: item.question,
  560. chinese_explanation: item.scoring_reference || '暂无释义',
  561. question_form_name: item.question_form_name || '暂无问题类型',
  562. category_id: item.category || '暂无分类'
  563. }));
  564. // 更新总数
  565. questionPagination.value.total = response.data.total;
  566. // 如果是第一次加载,获取所有分类选项
  567. if (questionCategoryOptions.value.length === 0) {
  568. loadQuestionCategories();
  569. }
  570. } else {
  571. ElMessage.error('获取问题列表失败');
  572. }
  573. } catch (error) {
  574. console.error('加载问题选项失败:', error);
  575. ElMessage.error('加载问题选项失败');
  576. } finally {
  577. questionsLoading.value = false;
  578. }
  579. };
  580. // 添加加载问题分类的方法
  581. const loadQuestionCategories = async () => {
  582. try {
  583. // 调用API获取问题分类列表
  584. const response = await fetch(getBaseURL() + 'api/system/question_category/list?tenant_id=1', {
  585. method: 'GET',
  586. headers: {
  587. 'Authorization': 'JWT ' + Session.get('token'),
  588. 'Content-Type': 'application/json'
  589. }
  590. });
  591. if (!response.ok) {
  592. throw new Error('获取问题分类列表失败');
  593. }
  594. const data = await response.json();
  595. if (data && data.code === 2000 && data.data.items) {
  596. // 转换数据格式
  597. questionCategoryOptions.value = data.data.items.map((item: any) => ({
  598. value: item.id.toString(),
  599. label: item.name
  600. }));
  601. }
  602. } catch (error) {
  603. console.error('加载问题分类选项失败:', error);
  604. }
  605. };
  606. // 修改 filterQuestions 方法
  607. const filterQuestions = () => {
  608. // 重置到第一页
  609. questionPagination.value.currentPage = 1;
  610. // 重新加载数据
  611. loadQuestionOptions();
  612. };
  613. // 修改计算属性:过滤后的问题选项
  614. const filteredQuestionOptions = computed(() => {
  615. // 由于我们已经在API请求中处理了关键词和分类筛选,
  616. // 这里直接返回问题选项即可
  617. return questionOptions.value;
  618. });
  619. // 处理问题选择变化
  620. const handleQuestionSelectionChange = (selection: any[]) => {
  621. positionQuestionForm.value.questions = selection.map(item => item.value);
  622. };
  623. // 处理问题分页大小变化
  624. const handleQuestionSizeChange = (size: number) => {
  625. questionPagination.value.pageSize = size;
  626. loadQuestionOptions();
  627. };
  628. // 处理问题当前页变化
  629. const handleQuestionCurrentChange = (page: number) => {
  630. questionPagination.value.currentPage = page;
  631. loadQuestionOptions();
  632. };
  633. // 修改打开职位问题对话框方法
  634. const openPositionQuestionDialog = () => {
  635. // 加载职位选项
  636. loadPositionOptions();
  637. // 加载问题选项
  638. loadQuestionOptions();
  639. positionQuestionDialogVisible.value = true;
  640. };
  641. // 修改 loadPositionOptions 函数,从实际API获取职位数据
  642. const loadPositionOptions = async () => {
  643. try {
  644. // 调用实际的API获取职位列表
  645. const response = await fetch(getBaseURL() + 'api/system/job/list?tenant_id=1', {
  646. method: 'GET',
  647. headers: {
  648. 'Authorization': 'JWT ' + Session.get('token'),
  649. 'Content-Type': 'application/json'
  650. }
  651. });
  652. if (!response.ok) {
  653. throw new Error('获取职位列表失败');
  654. }
  655. const data = await response.json();
  656. if (data && data.code === 2000 && data.data) {
  657. // 转换数据格式
  658. positionOptions.value = data.data.map((item: any) => ({
  659. value: item.id.toString(),
  660. label: item.title,
  661. // 如果有中文释义字段,可以在这里添加
  662. description: item.description || ''
  663. }));
  664. } else {
  665. ElMessage.error('获取职位列表失败');
  666. }
  667. } catch (error) {
  668. console.error('加载职位选项失败:', error);
  669. ElMessage.error('加载职位选项失败');
  670. }
  671. };
  672. // 添加一个函数来获取职位名称
  673. const getPositionName = (positionId: string | number) => {
  674. if (!positionId) return '';
  675. const position = positionOptions.value.find(p => p.value === positionId.toString());
  676. return position ? position.label : positionId.toString();
  677. };
  678. // 修改 submitPositionQuestionForm 函数,添加中文释义处理
  679. const submitPositionQuestionForm = async () => {
  680. try {
  681. if (!positionQuestionForm.value.position) {
  682. ElMessage.warning('请选择职位');
  683. return;
  684. }
  685. if (positionQuestionForm.value.questions.length === 0) {
  686. ElMessage.warning('请选择至少一个问题');
  687. return;
  688. }
  689. // 获取职位名称,用于显示成功消息
  690. const positionName = getPositionName(positionQuestionForm.value.position);
  691. // 构建API请求数据 - 修改为符合API期望的格式
  692. const requestData = {
  693. position_id: parseInt(positionQuestionForm.value.position),
  694. question_id: {
  695. question_id: positionQuestionForm.value.questions.map(questionId => parseInt(questionId))
  696. },
  697. duration: parseInt(positionQuestionForm.value.sequence) || 60,
  698. tenant_id: 1
  699. };
  700. console.log('提交的数据:', requestData);
  701. // 调用实际的API保存职位问题关联
  702. const response = await AddDocument(requestData);
  703. if (response && response.code === 2000) {
  704. ElMessage.success(`${positionName}职位问题保存成功`);
  705. positionQuestionDialogVisible.value = false;
  706. // 重置表单
  707. positionQuestionForm.value = {
  708. position: '',
  709. questions: [],
  710. sequence: '0'
  711. };
  712. // 刷新列表
  713. crudExpose.doRefresh();
  714. } else {
  715. ElMessage.error(response?.msg || '保存失败');
  716. }
  717. } catch (error) {
  718. console.error('保存职位问题失败:', error);
  719. ElMessage.error('保存失败,请重试');
  720. }
  721. };
  722. // 移除已选问题
  723. const removeQuestion = (question: { value: string, label: string }) => {
  724. positionQuestionForm.value.questions = positionQuestionForm.value.questions.filter(
  725. id => id !== question.value
  726. );
  727. };
  728. // 获取文档树数据
  729. const getTreeData = () => {
  730. GetDocumentTree({}).then((ret: any) => {
  731. console.log('ret', ret);
  732. const responseData = ret.data;
  733. GetCategoryList({}).then((res: any) => {
  734. console.log('res', res);
  735. const categoryList = res.data.items;
  736. GetTagList({}).then((res: any) => {
  737. console.log('res', res);
  738. const tagList = res.data.items;
  739. // 创建根节点
  740. const rootNode = [
  741. {
  742. id: 'root',
  743. title: '职位开放题',
  744. children: responseData.map((item: any) => ({
  745. ...item,
  746. id: `open_${item.id}`, // 为职位开放题添加特定前缀
  747. title: item.name || item.title,
  748. question_type: 'open' // 添加类型标识
  749. }))
  750. },
  751. {
  752. id: 'professional_questions',
  753. title: '专业考察题库',
  754. children: responseData.map((item: any) => ({
  755. ...item,
  756. id: `prof_${item.id}`, // 为专业考察题添加特定前缀
  757. title: item.name || item.title,
  758. question_type: 'professional' // 添加类型标识
  759. }))
  760. },
  761. /* ...categoryList.map((item: any) => ({
  762. id: `category_${item.id}`,
  763. title: item.name,
  764. children: tagList.filter((tag: any) => tag.category_id === item.id).map((tag: any) => ({
  765. id: `tag_${tag.id}`,
  766. title: tag.name
  767. }))
  768. }))
  769. {
  770. id: categoryList.find((item: any) => item.name === '基本常识')?.id || '7',
  771. title: categoryList.find((item: any) => item.name === '基本常识')?.name || '基本常识',
  772. },
  773. {
  774. id: 'psychology_questions',
  775. title: '心理题库',
  776. children: [
  777. {
  778. id: 'psychology_questions_1',
  779. title: 'DVF心理测评',
  780. },
  781. {
  782. id: 'psychology_questions_2',
  783. title: '卡特尔人格因素测验',
  784. },
  785. {
  786. id: 'psychology_questions_3',
  787. title: '明尼苏达多相人格测验',
  788. }
  789. ]
  790. } */
  791. ]
  792. documentTreeData.value = rootNode
  793. });
  794. })
  795. });
  796. };
  797. // 处理树节点点击
  798. const handleTreeClick = (record: any) => {
  799. console.log('record', record);
  800. // 获取当前节点ID并处理前缀
  801. const currentNodeId = record?.currentNode?.id || '';
  802. const originalId = currentNodeId.replace(/^(open_|prof_|category_|tag_)/, '');
  803. // 保存当前选中的分类ID,如果ID不是数字则使用默认值1
  804. selectedCategoryId.value = (!isNaN(Number(originalId)) && originalId !== null) ? originalId : 1;
  805. // 根据不同的问题类型设置question_form
  806. let question_form;
  807. const firstLevelId = record?.firstLevel?.id;
  808. const nodeType = currentNodeId.startsWith('open_') ? 'open' :
  809. currentNodeId.startsWith('prof_') ? 'professional' :
  810. currentNodeId.startsWith('category_') ? 'category' :
  811. currentNodeId.startsWith('tag_') ? 'tag' :
  812. firstLevelId;
  813. if (nodeType === 'open' || firstLevelId === 'root') {
  814. question_form = '0'; // 职位开放题
  815. } else if (firstLevelId === 'psychology_questions') {
  816. question_form = '4'; // 心理题库
  817. } else if (firstLevelId === 'common_questions') {
  818. question_form = '1'; // 常识题库
  819. } else if (nodeType === 'professional' || firstLevelId === 'professional_questions') {
  820. question_form = '1,2,3,4,6'; // 专业考察题库
  821. } /* else {
  822. question_form = '0'; // 默认为职位开放题
  823. } */
  824. // 重置上传文件状态
  825. uploadedFile.value = null;
  826. // 构建搜索参数
  827. const searchParams = {
  828. form: {
  829. limit: 100,
  830. job_id: selectedCategoryId.value,
  831. question_form: question_form,
  832. ...(nodeType === 'category' ? { category: originalId } : {}),
  833. ...(nodeType === 'tag' ? { tag: originalId } : {})
  834. }
  835. };
  836. // 执行搜索
  837. crudExpose.doSearch(searchParams);
  838. };
  839. // 处理文档分类的新增或编辑
  840. const handleUpdateDocument = (type: any, record: any) => {
  841. if (type === 'update' && record) {
  842. const parentData = documentTreeRef.value?.treeRef?.currentNode?.parent?.data || {};
  843. documentTreeCacheData.value = [parentData];
  844. drawerFormData.value = record;
  845. }
  846. drawerVisible.value = true;
  847. };
  848. // 关闭抽屉
  849. const handleDrawerClose = (type: string) => {
  850. if (type === 'submit') {
  851. getTreeData();
  852. }
  853. drawerVisible.value = false;
  854. drawerFormData.value = {};
  855. };
  856. // 删除文档分类
  857. const handleDeleteDocument = (id: any, callback: any) => {
  858. ElMessageBox.confirm('您确认删除该文档分类吗?', '温馨提示', {
  859. confirmButtonText: '确认',
  860. cancelButtonText: '取消',
  861. type: 'warning',
  862. }).then(async () => {
  863. const res = await DeleteDocumentCategory(id);
  864. callback();
  865. if (res?.code === 2000) {
  866. successNotification(res.msg);
  867. getTreeData();
  868. }
  869. });
  870. };
  871. // 处理上传成功
  872. const handleUploadSuccess = (response: any, uploadFile: any, scope: any) => {
  873. if (!response || !response.data) {
  874. console.error('Upload response is invalid:', response);
  875. return;
  876. }
  877. const fileInfo = {
  878. name: uploadFile.name,
  879. file_path: response.data.bucket_url,
  880. file_type: response.data.file_type,
  881. file_size: uploadFile.size,
  882. doc_type: scope.form.doc_type || '',
  883. category: selectedCategoryId.value, // 使用当前选中的分类ID
  884. status: true,
  885. doc_desc: '',
  886. version: '',
  887. /* merchant: Session.get('merchant_info').merchant_id, */
  888. file: response.data.bucket_url
  889. };
  890. // 更新表单数据
  891. if (scope.form) {
  892. Object.assign(scope.form, fileInfo);
  893. } else {
  894. console.error('Form reference is not available');
  895. return;
  896. }
  897. // 更新已上传文件显示
  898. uploadedFile.value = fileInfo;
  899. // 刷新列表时传递category参数
  900. crudExpose.doRefresh();
  901. };
  902. // 处理文件删除
  903. const handleRemoveFile = (scope: any) => {
  904. uploadedFile.value = null;
  905. // 清空表单数据
  906. if (crudRef.value?.form) {
  907. const form = crudRef.value.form;
  908. form.name = '';
  909. form.file_path = '';
  910. form.file_type = '';
  911. form.file_size = '';
  912. form.doc_type = '';
  913. form.category = '';
  914. form.doc_desc = '';
  915. form.version = '';
  916. form.file = '';
  917. scope.value = '';
  918. }
  919. };
  920. // 格式化文件大小
  921. const formatFileSize = (bytes: number) => {
  922. if (bytes === 0) return '0 B';
  923. const k = 1024;
  924. const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
  925. const i = Math.floor(Math.log(bytes) / Math.log(k));
  926. return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
  927. };
  928. // 判断文件是否可预览
  929. const isPreviewable = (fileType: string) => {
  930. if (!fileType) return false;
  931. // 支持的文件类型
  932. const supportedTypes = [
  933. 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx',
  934. 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'
  935. ];
  936. return supportedTypes.includes(fileType.toLowerCase());
  937. };
  938. // 预览文件
  939. const previewFile = (fileData: any) => {
  940. if (!fileData || !fileData.file) {
  941. ElMessageBox.alert('文件链接不存在', '预览失败', { type: 'error' });
  942. return;
  943. }
  944. const fileType = fileData.file_type || '';
  945. const filePath = fileData.file;
  946. const previewUrl = `/#/preview?url=${encodeURIComponent(filePath)}&type=${fileType}`;
  947. window.open(previewUrl, '_blank');
  948. };
  949. // 处理查看详情
  950. const handleViewDetail = async (event: any) => {
  951. const row = event.detail || event;
  952. try {
  953. // 可以选择直接使用传入的行数据,或者重新请求完整数据
  954. // 如果需要重新请求数据,可以添加一个 GetDocument API 方法
  955. // const res = await GetDocument(row.id);
  956. // detailData.value = res.data;
  957. // 这里直接使用行数据
  958. detailData.value = row;
  959. detailDialogVisible.value = true;
  960. } catch (error) {
  961. ElMessage.error('获取文档详情失败');
  962. }
  963. };
  964. // 获取文档类型名称
  965. const getDocTypeName = (docTypeId: number | string) => {
  966. if (!docTypeId) return '';
  967. const docTypeDict = [
  968. { value: 1, label: '合同文档' },
  969. { value: 2, label: '技术文档' },
  970. { value: 3, label: '操作手册' },
  971. { value: 4, label: '维护记录' },
  972. { value: 5, label: '其他文档' }
  973. ];
  974. const docType = docTypeDict.find(item => item.value === docTypeId);
  975. return docType ? docType.label : docTypeId;
  976. };
  977. // 获取分类名称
  978. const getCategoryName = (categoryId: number | string) => {
  979. if (!categoryId) return '';
  980. const categoryDict = (window as any).__categoryDict || [];
  981. const category = categoryDict.find((item: any) => item.id === categoryId);
  982. return category ? category.name : categoryId;
  983. };
  984. // 获取项目名称
  985. const getProjectName = (projectId: number | string) => {
  986. if (!projectId) return '';
  987. const projectDict = (window as any).__projectDict || [];
  988. const project = projectDict.find((item: any) => item.id === projectId);
  989. return project ? project.name : projectId;
  990. };
  991. // 预览详情中的文件
  992. const previewDetailFile = (fileData: any) => {
  993. if (!fileData || !fileData.file_path) {
  994. ElMessage.error('文件路径无效');
  995. return;
  996. }
  997. const filePath = fileData.file_path;
  998. const fileType = fileData.file_type;
  999. // 支持的文件类型列表
  1000. const supportedTypes = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
  1001. if (!supportedTypes.includes(fileType)) {
  1002. ElMessage.warning(`暂不支持预览该文件类型: ${fileType}`);
  1003. return;
  1004. }
  1005. // 使用window.open在新窗口中打开预览页面
  1006. const previewUrl = `/#/preview?url=${encodeURIComponent(filePath)}&type=${fileType}`;
  1007. window.open(previewUrl, '_blank');
  1008. };
  1009. // 处理打印功能
  1010. const handlePrint = () => {
  1011. /* print(printArea.value, {
  1012. title: `${detailData.value.name}文档详情`,
  1013. style: `
  1014. .el-descriptions { margin: 10px 0; }
  1015. .el-descriptions-item__label { font-weight: bold; }
  1016. .el-tag { padding: 2px 6px; }
  1017. @media print {
  1018. .dialog-footer { display: none; }
  1019. }
  1020. `
  1021. }); */
  1022. };
  1023. // 视频上传前的验证
  1024. const beforeVideoUpload = (file: any) => {
  1025. const isVideo = file.type.startsWith('video/');
  1026. const isLt500M = file.size / 1024 / 1024 < 500;
  1027. if (!isVideo) {
  1028. ElMessage.error('请上传视频文件!');
  1029. return false;
  1030. }
  1031. if (!isLt500M) {
  1032. ElMessage.error('视频大小不能超过 500MB!');
  1033. return false;
  1034. }
  1035. return true;
  1036. };
  1037. // 字幕上传前的验证
  1038. const beforeSubtitleUpload = (file: any) => {
  1039. const validExtensions = ['.srt', '.vtt', '.ass'];
  1040. const extension = file.name.substring(file.name.lastIndexOf('.')).toLowerCase();
  1041. const isValidType = validExtensions.includes(extension);
  1042. const isLt2M = file.size / 1024 / 1024 < 2;
  1043. if (!isValidType) {
  1044. ElMessage.error('请上传有效的字幕文件 (.srt, .vtt, .ass)!');
  1045. return false;
  1046. }
  1047. if (!isLt2M) {
  1048. ElMessage.error('字幕文件大小不能超过 2MB!');
  1049. return false;
  1050. }
  1051. return true;
  1052. };
  1053. // 修改视频上传成功处理函数
  1054. const handleVideoUploadSuccess = (response: any, file: any) => {
  1055. console.log('response', response);
  1056. console.log('file', file);
  1057. // 检查响应格式,确保能正确获取URL
  1058. if (response && response.url) {
  1059. // 直接使用响应中的URL
  1060. videoInfo.value = {
  1061. url: response.url,
  1062. name: file.name || response.filename || '',
  1063. size: file.size || 0,
  1064. file_type: file.type || response.mime_type || 'video/mp4'
  1065. };
  1066. // 更新表单数据
  1067. videoForm.value.videoUrl = response.url;
  1068. ElMessage.success('视频上传成功');
  1069. } else if (response && response.data && response.data.url) {
  1070. // 兼容另一种可能的响应格式
  1071. videoInfo.value = {
  1072. url: response.data.url,
  1073. name: file.name || response.data.filename || '',
  1074. size: file.size || 0,
  1075. file_type: file.type || response.data.mime_type || 'video/mp4'
  1076. };
  1077. // 更新表单数据
  1078. videoForm.value.videoUrl = response.data.url;
  1079. ElMessage.success('视频上传成功');
  1080. } else {
  1081. // 尝试从您提供的响应格式中获取URL
  1082. try {
  1083. // 根据您提供的响应示例,URL可能在这个位置
  1084. const url = response.url ||
  1085. (response.response && response.response.url) ||
  1086. (response[0] && response[0].url) ||
  1087. (response.data && response.data.bucket_url);
  1088. if (url) {
  1089. videoInfo.value = {
  1090. url: url,
  1091. name: file.name || response.filename || response.original_filename || '',
  1092. size: file.size || response.size || 0,
  1093. file_type: file.type || response.mime_type || 'video/mp4'
  1094. };
  1095. // 更新表单数据
  1096. videoForm.value.videoUrl = url;
  1097. ElMessage.success('视频上传成功');
  1098. } else {
  1099. throw new Error('无法从响应中获取URL');
  1100. }
  1101. } catch (error) {
  1102. console.error('处理上传响应时出错:', error);
  1103. ElMessage.error('视频上传成功,但无法解析响应数据');
  1104. }
  1105. }
  1106. };
  1107. // 修改字幕上传成功处理函数
  1108. const handleSubtitleUploadSuccess = (response: any, file: any) => {
  1109. if (response && response.data) {
  1110. subtitleInfo.value = {
  1111. url: response.data.bucket_url,
  1112. name: file.name,
  1113. size: file.size,
  1114. file_type: file.name.substring(file.name.lastIndexOf('.') + 1)
  1115. };
  1116. // 尝试读取字幕文件内容
  1117. fetch(response.data.bucket_url)
  1118. .then(res => res.text())
  1119. .then(text => {
  1120. videoForm.value.subtitleContent = text;
  1121. ElMessage.success('字幕文件已上传并内容已加载');
  1122. })
  1123. .catch(err => {
  1124. console.error('无法读取字幕文件内容:', err);
  1125. ElMessage.warning('字幕文件已上传,但无法读取内容');
  1126. });
  1127. } else {
  1128. ElMessage.error('字幕上传失败');
  1129. }
  1130. };
  1131. // 上传错误处理
  1132. const handleUploadError = () => {
  1133. ElMessage.error('上传失败,请重试');
  1134. };
  1135. // 处理打开视频上传对话框
  1136. const handleOpenVideoUploadDialog = (event: any) => {
  1137. const row = event.detail;
  1138. if (row) {
  1139. currentRow.value = row;
  1140. videoUploadDialogVisible.value = true;
  1141. // 重置上传状态
  1142. videoInfo.value = { url: '', name: '', size: 0, file_type: '' };
  1143. subtitleInfo.value = { url: '', name: '', size: 0, file_type: '' };
  1144. // 重置表单数据
  1145. videoForm.value = {
  1146. videoUrl: '',
  1147. subtitleContent: ''
  1148. };
  1149. }
  1150. };
  1151. // 提交表单
  1152. const submitVideoForm = () => {
  1153. if (!videoFormRef.value) return;
  1154. videoFormRef.value.validate(async (valid: boolean) => {
  1155. if (valid) {
  1156. try {
  1157. if (!currentRow.value || !currentRow.value.id) {
  1158. ElMessage.error('当前选中的题目信息无效');
  1159. return;
  1160. }
  1161. // 确保我们有视频URL
  1162. if (!videoForm.value.videoUrl && !videoInfo.value.url) {
  1163. ElMessage.warning('请先上传数字人视频');
  1164. return;
  1165. }
  1166. // 修改为符合API要求的参数格式
  1167. const updateData = {
  1168. question_id: currentRow.value.question_id, // 必填,问题ID
  1169. video_url: videoForm.value.videoUrl || videoInfo.value.url, // 数字人视频URL
  1170. digital_human_video_subtitle: videoForm.value.subtitleContent, // 数字人视频字幕内容
  1171. tenant_id: 1
  1172. };
  1173. console.log('提交的数据:', updateData);
  1174. const response = await UpdateDocument(updateData);
  1175. if (response && response.code === 2000) {
  1176. ElMessage.success('数字人视频和字幕信息保存成功');
  1177. videoUploadDialogVisible.value = false;
  1178. // 重置上传状态
  1179. videoInfo.value = { url: '', name: '', size: 0, file_type: '' };
  1180. subtitleInfo.value = { url: '', name: '', size: 0, file_type: '' };
  1181. // 重置表单数据
  1182. videoForm.value = {
  1183. videoUrl: '',
  1184. subtitleContent: ''
  1185. };
  1186. // 刷新列表
  1187. crudExpose.doRefresh();
  1188. } else {
  1189. ElMessage.error(response?.msg || '保存失败');
  1190. }
  1191. } catch (error) {
  1192. console.error('保存视频和字幕信息时出错:', error);
  1193. ElMessage.error('保存失败,请重试');
  1194. }
  1195. } else {
  1196. ElMessage.warning('请完成必填项');
  1197. return false;
  1198. }
  1199. });
  1200. };
  1201. // 处理打开排序对话框
  1202. const handleOpenSortDialog = (event: any) => {
  1203. const row = event.detail;
  1204. if (row) {
  1205. currentRow.value = row;
  1206. sortForm.value.sequence_number = row.sequence_number || 0;
  1207. sortDialogVisible.value = true;
  1208. }
  1209. };
  1210. // 修改提交排序表单函数
  1211. const submitSortForm = () => {
  1212. if (!sortFormRef.value) return;
  1213. sortFormRef.value.validate(async (valid: boolean) => {
  1214. if (valid) {
  1215. try {
  1216. if (!currentRow.value || !currentRow.value.question_id) {
  1217. ElMessage.error('当前选中的题目信息无效');
  1218. return;
  1219. }
  1220. // 获取当前职位ID
  1221. const positionId = selectedCategoryId.value;
  1222. if (!positionId) {
  1223. ElMessage.error('未选择职位分类');
  1224. return;
  1225. }
  1226. // 构建符合新格式的更新数据
  1227. const updateData = {
  1228. position_id: positionId,
  1229. questions: [
  1230. {
  1231. question_id: currentRow.value.question_id,
  1232. sequence_number: sortForm.value.sequence_number
  1233. }
  1234. ],
  1235. tenant_id: 1
  1236. };
  1237. console.log('提交的排序数据:', updateData);
  1238. const response = await UpdateSequence(updateData);
  1239. if (response && response.code === 2000) {
  1240. ElMessage.success('排序修改成功');
  1241. sortDialogVisible.value = false;
  1242. // 刷新列表
  1243. crudExpose.doRefresh();
  1244. } else {
  1245. ElMessage.error(response?.msg || '保存失败');
  1246. }
  1247. } catch (error) {
  1248. console.error('保存排序信息时出错:', error);
  1249. ElMessage.error('保存失败,请重试');
  1250. }
  1251. } else {
  1252. ElMessage.warning('请输入有效的排序值');
  1253. return false;
  1254. }
  1255. });
  1256. };
  1257. // 页面加载时获取数据
  1258. onMounted(() => {
  1259. getTreeData();
  1260. // 先执行一次刷新,确保组件已完全初始化
  1261. crudExpose.doRefresh();
  1262. // 监听crud组件初始化完成
  1263. setTimeout(() => {
  1264. if (crudRef.value) {
  1265. // 确保搜索表单已初始化
  1266. if (!crudRef.value.searchForm) {
  1267. crudRef.value.searchForm = {};
  1268. }
  1269. // 如果有默认选中的分类,设置搜索参数并执行搜索
  1270. if (selectedCategoryId.value) {
  1271. crudRef.value.searchForm.category = selectedCategoryId.value;
  1272. crudExpose.doSearch({
  1273. form: {
  1274. category: selectedCategoryId.value
  1275. }
  1276. });
  1277. }
  1278. }
  1279. }, 300);
  1280. // 添加全局事件监听器
  1281. window.addEventListener('viewDocumentDetail', handleViewDetail);
  1282. window.addEventListener('openVideoUploadDialog', handleOpenVideoUploadDialog);
  1283. window.addEventListener('openSortDialog', handleOpenSortDialog);
  1284. window.addEventListener('openPositionQuestionDialog', openPositionQuestionDialog);
  1285. // 添加绑定开放问题对话框的事件监听器
  1286. window.addEventListener('openBindParentQuestionDialog', handleOpenBindParentQuestionDialog as EventListener);
  1287. });
  1288. // 添加 onBeforeUnmount 钩子移除事件监听器
  1289. onBeforeUnmount(() => {
  1290. window.removeEventListener('viewDocumentDetail', handleViewDetail);
  1291. window.removeEventListener('openVideoUploadDialog', handleOpenVideoUploadDialog);
  1292. window.removeEventListener('openSortDialog', handleOpenSortDialog);
  1293. window.removeEventListener('openPositionQuestionDialog', openPositionQuestionDialog);
  1294. // 移除绑定开放问题对话框的事件监听器
  1295. window.removeEventListener('openBindParentQuestionDialog', handleOpenBindParentQuestionDialog as EventListener);
  1296. });
  1297. // 新增 questionSearchKeyword
  1298. const questionSearchKeyword = ref('');
  1299. // 绑定开放问题相关变量
  1300. const bindParentQuestionDialogVisible = ref(false);
  1301. const selectedParentQuestionId = ref<number | null>(null);
  1302. const openQuestionsList = ref<any[]>([]);
  1303. const openQuestionsLoading = ref(false);
  1304. const openQuestionKeyword = ref('');
  1305. // 获取开放问题列表
  1306. const fetchOpenQuestions = async () => {
  1307. try {
  1308. openQuestionsLoading.value = true;
  1309. // 构建请求参数,添加关键词搜索
  1310. const params: any = {
  1311. page: 1,
  1312. limit: 200,
  1313. tenant_id: 1,
  1314. question_form: 0, // 开放问题类型
  1315. };
  1316. // 如果有关键词,添加到请求参数
  1317. if (openQuestionKeyword.value) {
  1318. params.keyword = openQuestionKeyword.value;
  1319. }
  1320. const res = await GetInterviewQuestions(params);
  1321. openQuestionsList.value = res.data.items || [];
  1322. } catch (error) {
  1323. console.error('获取开放问题列表失败:', error);
  1324. ElMessage.error('获取开放问题列表失败');
  1325. } finally {
  1326. openQuestionsLoading.value = false;
  1327. }
  1328. };
  1329. // 搜索开放问题
  1330. const searchOpenQuestions = () => {
  1331. // 使用防抖函数,避免频繁请求
  1332. if (searchOpenQuestionsDebounce) {
  1333. clearTimeout(searchOpenQuestionsDebounce);
  1334. }
  1335. searchOpenQuestionsDebounce = setTimeout(() => {
  1336. fetchOpenQuestions();
  1337. }, 300);
  1338. };
  1339. // 防抖变量
  1340. let searchOpenQuestionsDebounce: number | null = null;
  1341. // 监听打开绑定对话框事件
  1342. const handleOpenBindParentQuestionDialog = (event: CustomEvent) => {
  1343. currentRow.value = event.detail;
  1344. // 如果已有关联的开放问题,则预选中
  1345. if (currentRow.value.parent_question_id) {
  1346. selectedParentQuestionId.value = currentRow.value.parent_question_id;
  1347. } else {
  1348. selectedParentQuestionId.value = null;
  1349. }
  1350. // 重置搜索关键词
  1351. openQuestionKeyword.value = '';
  1352. // 获取开放问题列表
  1353. fetchOpenQuestions();
  1354. // 显示对话框
  1355. bindParentQuestionDialogVisible.value = true;
  1356. };
  1357. // 添加处理绑定对话框关闭的函数
  1358. const handleBindDialogClose = () => {
  1359. bindParentQuestionDialogVisible.value = false;
  1360. selectedParentQuestionId.value = null;
  1361. currentRow.value = null;
  1362. };
  1363. // 添加确认绑定开放问题的函数
  1364. const confirmBindParentQuestion = async () => {
  1365. if (!currentRow.value || selectedParentQuestionId.value === null) {
  1366. ElMessage.warning('请选择要关联的开放问题');
  1367. return;
  1368. }
  1369. try {
  1370. await UpdateDocument({
  1371. id: currentRow.value.question_id,
  1372. parent_question_id: selectedParentQuestionId.value,
  1373. tenant_id: '1'
  1374. });
  1375. ElMessage.success('绑定开放问题成功');
  1376. bindParentQuestionDialogVisible.value = false;
  1377. selectedParentQuestionId.value = null;
  1378. // 刷新表格数据
  1379. crudExpose.doRefresh();
  1380. } catch (error) {
  1381. console.error('绑定开放问题失败:', error);
  1382. ElMessage.error('绑定开放问题失败');
  1383. }
  1384. };
  1385. // 添加 bindParentQuestion 方法
  1386. const bindParentQuestion = (row: any) => {
  1387. // 触发自定义事件,让父组件处理弹窗显示
  1388. const event = new CustomEvent('openBindParentQuestionDialog', { detail: row });
  1389. window.dispatchEvent(event);
  1390. };
  1391. </script>
  1392. <style lang="scss" scoped>
  1393. .document-el-row {
  1394. height: 100%;
  1395. overflow: hidden;
  1396. .el-col {
  1397. height: 100%;
  1398. padding: 10px 0;
  1399. box-sizing: border-box;
  1400. }
  1401. }
  1402. .document-box {
  1403. height: 100%;
  1404. padding: 10px;
  1405. background-color: var(--el-fill-color-blank);
  1406. box-sizing: border-box;
  1407. }
  1408. .document-left-box {
  1409. position: relative;
  1410. border-radius: 0 8px 8px 0;
  1411. margin-right: 10px;
  1412. overflow-y: auto;
  1413. }
  1414. .document-right-box {
  1415. border-radius: 8px 0 0 8px;
  1416. }
  1417. .mt-2 {
  1418. margin-top: 0.5rem;
  1419. }
  1420. .w-full {
  1421. width: 100%;
  1422. }
  1423. .mr-2 {
  1424. margin-right: 0.5rem;
  1425. }
  1426. .mr-4 {
  1427. margin-right: 1rem;
  1428. }
  1429. .text-gray-500 {
  1430. color: #6b7280;
  1431. }
  1432. .text-sm {
  1433. font-size: 0.875rem;
  1434. }
  1435. .flex {
  1436. display: flex;
  1437. }
  1438. .items-center {
  1439. align-items: center;
  1440. }
  1441. .justify-between {
  1442. justify-content: space-between;
  1443. }
  1444. /* 添加详情对话框样式 */
  1445. .el-descriptions {
  1446. margin: 10px 0;
  1447. }
  1448. .dialog-footer {
  1449. display: flex;
  1450. justify-content: flex-end;
  1451. }
  1452. .mt-4 {
  1453. margin-top: 1rem;
  1454. }
  1455. ::v-deep .el-upload{
  1456. flex-direction: column;
  1457. }
  1458. /* 表单样式优化 */
  1459. .el-form-item {
  1460. margin-bottom: 20px;
  1461. }
  1462. .upload-section {
  1463. border: 1px solid #ebeef5;
  1464. border-radius: 4px;
  1465. padding: 15px;
  1466. background-color: #f8f9fa;
  1467. }
  1468. .video-uploader, .subtitle-uploader {
  1469. width: 100%;
  1470. }
  1471. .video-preview {
  1472. width: 100%;
  1473. margin-bottom: 10px;
  1474. }
  1475. .video-player {
  1476. width: 100%;
  1477. max-height: 200px;
  1478. object-fit: contain;
  1479. border-radius: 4px;
  1480. }
  1481. .file-preview {
  1482. display: flex;
  1483. align-items: center;
  1484. padding: 10px;
  1485. border: 1px dashed #d9d9d9;
  1486. border-radius: 4px;
  1487. background-color: #fff;
  1488. }
  1489. .file-icon {
  1490. font-size: 24px;
  1491. margin-right: 10px;
  1492. color: #409eff;
  1493. }
  1494. .file-info {
  1495. display: flex;
  1496. flex-direction: column;
  1497. margin-top: 5px;
  1498. }
  1499. .file-size {
  1500. font-size: 12px;
  1501. color: #909399;
  1502. margin-top: 3px;
  1503. }
  1504. .subtitle-tip {
  1505. color: #909399;
  1506. font-size: 12px;
  1507. }
  1508. .mt-2 {
  1509. margin-top: 8px;
  1510. }
  1511. .question-list-container {
  1512. margin-top: 20px;
  1513. border: 1px solid #ebeef5;
  1514. border-radius: 4px;
  1515. padding: 15px;
  1516. background-color: #f8f9fa;
  1517. }
  1518. .question-list-container h3 {
  1519. margin-top: 0;
  1520. margin-bottom: 15px;
  1521. font-size: 16px;
  1522. color: #303133;
  1523. }
  1524. .question-selection-container {
  1525. width: 100%;
  1526. border: 1px solid #ebeef5;
  1527. border-radius: 4px;
  1528. overflow: hidden;
  1529. }
  1530. .question-search {
  1531. padding: 10px;
  1532. background-color: #f5f7fa;
  1533. border-bottom: 1px solid #ebeef5;
  1534. }
  1535. .search-row {
  1536. display: flex;
  1537. align-items: center;
  1538. }
  1539. .question-table-container {
  1540. display: flex;
  1541. flex-direction: column;
  1542. height: 350px;
  1543. }
  1544. .pagination-container {
  1545. margin-top: 10px;
  1546. padding: 5px 0;
  1547. display: flex;
  1548. justify-content: flex-end;
  1549. background-color: #f5f7fa;
  1550. border-top: 1px solid #ebeef5;
  1551. }
  1552. .question-item {
  1553. margin-bottom: 8px;
  1554. padding: 8px;
  1555. border-radius: 4px;
  1556. transition: background-color 0.2s;
  1557. &:hover {
  1558. background-color: #f5f7fa;
  1559. }
  1560. }
  1561. .no-data {
  1562. padding: 20px 0;
  1563. text-align: center;
  1564. }
  1565. .search-container {
  1566. margin-bottom: 15px;
  1567. }
  1568. </style>