index.vue 49 KB

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