document.vue 48 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505
  1. <template>
  2. <div>
  3. <el-card shadow="never">
  4. <template #header>
  5. <div class="flex justify-between items-center">
  6. <span class="text-lg font-bold">{{ t('document.document.header.title') }}</span>
  7. <el-button type="primary" @click="handleBack">{{ t('document.document.header.backToList') }}</el-button>
  8. </div>
  9. </template>
  10. <div class="content-wrapper">
  11. <div class="tree-container">
  12. <div class="tree-header">
  13. <el-button v-hasPermi="['document:folder:add']" type="primary" icon="Plus" size="small"
  14. @click="handleAddFolder">{{ t('document.document.button.newFolder') }}</el-button>
  15. </div>
  16. <el-scrollbar class="tree-scrollbar">
  17. <el-tree v-loading="loading" :data="treeData" :props="treeProps" node-key="id" default-expand-all
  18. :expand-on-click-node="false">
  19. <template #default="{ node, data }">
  20. <span class="custom-tree-node">
  21. <el-icon>
  22. <Folder v-if="data.type === 0" />
  23. <Location v-else-if="data.type === 1" />
  24. <OfficeBuilding v-else-if="data.type === 2" />
  25. <Document v-else />
  26. </el-icon>
  27. <span class="node-label" @click="handleFolderClick(data)">{{ node.label }}</span>
  28. <span class="node-actions">
  29. <span class="menu-trigger" @click="toggleMenu($event, data)">
  30. <el-icon>
  31. <MoreFilled />
  32. </el-icon>
  33. </span>
  34. </span>
  35. </span>
  36. </template>
  37. </el-tree>
  38. </el-scrollbar>
  39. </div>
  40. <!-- 一级菜单 -->
  41. <ul class="primary-menu" v-if="activeMenu !== null" :style="primaryMenuStyle">
  42. <li class="menu-item has-submenu" v-hasPermi="['document:folder:add']" @click.stop="toggleSubmenu($event)">
  43. <span>{{ t('document.document.menu.add') }}</span>
  44. <el-icon class="arrow-icon">
  45. <ArrowRight />
  46. </el-icon>
  47. </li>
  48. <li class="menu-item" v-hasPermi="['document:folder:edit']"
  49. @click="handleMenuItemClick('edit', currentMenuData)">
  50. <span>{{ t('document.document.menu.edit') }}</span>
  51. </li>
  52. <li class="menu-item" v-hasPermi="['document:folder:remove']"
  53. @click="handleMenuItemClick('delete', currentMenuData)">
  54. <span>{{ t('document.document.menu.delete') }}</span>
  55. </li>
  56. </ul>
  57. <!-- 二级菜单 -->
  58. <ul class="secondary-menu" v-if="showSecondaryMenu" :style="secondaryMenuStyle">
  59. <!-- 国家或中心:显示中心和文件夹 -->
  60. <template v-if="currentMenuData && (currentMenuData.type === 1 || currentMenuData.type === 2)">
  61. <li class="menu-item" @click="handleMenuItemClick('add:2', currentMenuData)">
  62. <span>{{ t('document.document.menu.center') }}</span>
  63. </li>
  64. <li class="menu-item" @click="handleMenuItemClick('add:0', currentMenuData)">
  65. <span>{{ t('document.document.menu.folder') }}</span>
  66. </li>
  67. </template>
  68. <!-- 文件夹:只显示文件夹和文档 -->
  69. <template v-else-if="currentMenuData && currentMenuData.type === 0">
  70. <li class="menu-item" @click="handleMenuItemClick('add:0', currentMenuData)">
  71. <span>{{ t('document.document.menu.folder') }}</span>
  72. </li>
  73. <li class="menu-item" v-hasPermi="['document:document:add']"
  74. @click="handleMenuItemClick('add:document', currentMenuData)">
  75. <span>{{ t('document.document.menu.document') }}</span>
  76. </li>
  77. </template>
  78. </ul>
  79. <div class="content-container">
  80. <!-- 文档列表展示区域 -->
  81. <div v-if="selectedFolder" class="document-list-container">
  82. <!-- 搜索栏 -->
  83. <el-form :model="documentQueryParams" :inline="true" class="search-form">
  84. <el-form-item :label="t('document.document.documentList.fileName')">
  85. <el-input v-model="documentQueryParams.name"
  86. :placeholder="t('document.document.documentList.fileNamePlaceholder')" clearable style="width: 240px"
  87. @keyup.enter="handleDocumentQuery" />
  88. </el-form-item>
  89. <el-form-item>
  90. <el-button type="primary" icon="Search" @click="handleDocumentQuery">{{
  91. t('document.document.button.search')
  92. }}</el-button>
  93. <el-button icon="Refresh" @click="handleDocumentReset">{{ t('document.document.button.reset')
  94. }}</el-button>
  95. </el-form-item>
  96. </el-form>
  97. <!-- 文档列表 -->
  98. <el-table v-loading="documentLoading" :data="documentList" border style="margin-top: 10px">
  99. <el-table-column type="index" width="55" align="center"
  100. :label="t('document.document.documentList.index')" />
  101. <el-table-column prop="name" :label="t('document.document.documentList.name')" min-width="150"
  102. show-overflow-tooltip />
  103. <el-table-column prop="specification" :label="t('document.document.documentList.specification')"
  104. min-width="120" show-overflow-tooltip />
  105. <el-table-column prop="planDocumentType" :label="t('document.document.documentList.planDocumentType')"
  106. width="120" align="center">
  107. <template #default="scope">
  108. <dict-tag v-if="scope.row.planDocumentType" :options="plan_document_type"
  109. :value="scope.row.planDocumentType" />
  110. <span v-else>-</span>
  111. </template>
  112. </el-table-column>
  113. <el-table-column prop="submitter" :label="t('document.document.documentList.submitter')" width="120"
  114. align="center" />
  115. <el-table-column prop="submitDeadline" :label="t('document.document.documentList.submitDeadline')"
  116. width="110" align="center">
  117. <template #default="scope">
  118. <span v-if="scope.row.submitDeadline">{{ parseTime(scope.row.submitDeadline, '{y}-{m}-{d}') }}</span>
  119. <span v-else>-</span>
  120. </template>
  121. </el-table-column>
  122. <el-table-column prop="submitTime" :label="t('document.document.documentList.submitTime')" width="160"
  123. align="center">
  124. <template #default="scope">
  125. <span v-if="scope.row.submitTime">{{ parseTime(scope.row.submitTime) }}</span>
  126. <span v-else>-</span>
  127. </template>
  128. </el-table-column>
  129. <el-table-column prop="url" :label="t('document.document.documentList.url')" min-width="200">
  130. <template #default="scope">
  131. <div v-if="scope.row.fileName" class="file-name-cell">
  132. <el-icon :size="18" class="file-icon">
  133. <Document v-if="isWordFile(scope.row.fileName)" style="color: #2b579a;" />
  134. <Grid v-else-if="isExcelFile(scope.row.fileName)" style="color: #217346;" />
  135. <Monitor v-else-if="isPPTFile(scope.row.fileName)" style="color: #d24726;" />
  136. <Reading v-else-if="isPDFFile(scope.row.fileName)" style="color: #e74c3c;" />
  137. <Document v-else style="color: #606266;" />
  138. </el-icon>
  139. <span class="file-name-text">{{ scope.row.fileName }}</span>
  140. <!-- 下载按钮已注释 -->
  141. <!-- <el-link v-if="scope.row.url" type="primary" :href="scope.row.url" :download="scope.row.fileName" target="_blank" :underline="false" class="download-btn">
  142. <el-icon><Download /></el-icon>
  143. </el-link> -->
  144. </div>
  145. <span v-else>-</span>
  146. </template>
  147. </el-table-column>
  148. <el-table-column prop="note" :label="t('document.document.documentList.note')" min-width="150"
  149. show-overflow-tooltip />
  150. <el-table-column prop="createTime" :label="t('document.document.documentList.createTime')" width="160"
  151. align="center">
  152. <template #default="scope">
  153. <span v-if="scope.row.createTime">{{ parseTime(scope.row.createTime) }}</span>
  154. <span v-else>-</span>
  155. </template>
  156. </el-table-column>
  157. <el-table-column prop="updateTime" :label="t('document.document.documentList.updateTime')" width="160"
  158. align="center">
  159. <template #default="scope">
  160. <span v-if="scope.row.updateTime">{{ parseTime(scope.row.updateTime) }}</span>
  161. <span v-else>-</span>
  162. </template>
  163. </el-table-column>
  164. <el-table-column :label="t('document.document.documentList.action')" width="150" align="center"
  165. fixed="right">
  166. <template #default="scope">
  167. <el-button v-hasPermi="['document:document:audit']" type="primary" link :icon="Select"
  168. @click="handleAudit(scope.row)" :title="t('document.document.button.audit')" />
  169. <el-button v-hasPermi="['document:document:mark']" type="primary" link icon="Flag"
  170. @click="handleMark(scope.row)" :title="t('document.document.button.mark')" />
  171. <el-button type="primary" link icon="Download" @click="handleDownload(scope.row)"
  172. :title="t('document.document.button.download')" :disabled="!scope.row.url" />
  173. </template>
  174. </el-table-column>
  175. </el-table>
  176. <!-- 分页 -->
  177. <pagination v-show="documentTotal > 0" v-model:page="documentQueryParams.pageNum"
  178. v-model:limit="documentQueryParams.pageSize" :total="documentTotal" @pagination="getDocumentList" />
  179. </div>
  180. <!-- 空状态 -->
  181. <el-empty v-else :description="t('document.document.empty.description')">
  182. </el-empty>
  183. </div>
  184. </div>
  185. </el-card>
  186. <!-- 添加文件夹对话框 -->
  187. <el-dialog v-model="dialog.visible" :title="dialog.title" width="600px" append-to-body>
  188. <el-form ref="folderFormRef" :model="form" :rules="rules" label-width="140px">
  189. <el-form-item :label="t('document.document.form.name')" prop="name">
  190. <el-input v-model="form.name" :placeholder="t('document.document.form.namePlaceholder')" clearable />
  191. </el-form-item>
  192. <el-form-item :label="t('document.document.form.restrictionLevel')" prop="restrictionLevel">
  193. <el-radio-group v-model="isRestricted" @change="handleRestrictionChange">
  194. <el-radio :label="false">{{ t('document.document.form.noRestriction') }}</el-radio>
  195. <el-radio :label="true">{{ t('document.document.form.restricted') }}</el-radio>
  196. </el-radio-group>
  197. <el-input-number v-if="isRestricted" v-model="restrictionLevelValue" :min="0" :max="10000"
  198. style="width: 100%; margin-top: 10px;"
  199. :placeholder="t('document.document.form.restrictionLevelPlaceholder')" />
  200. </el-form-item>
  201. <el-form-item :label="t('document.document.form.note')" prop="note">
  202. <el-input v-model="form.note" type="textarea" :rows="4"
  203. :placeholder="t('document.document.form.notePlaceholder')" />
  204. </el-form-item>
  205. </el-form>
  206. <template #footer>
  207. <div class="dialog-footer">
  208. <el-button :loading="buttonLoading" type="primary" @click="submitForm">{{ t('document.document.button.submit')
  209. }}</el-button>
  210. <el-button @click="cancel">{{ t('document.document.button.cancel') }}</el-button>
  211. </div>
  212. </template>
  213. </el-dialog>
  214. <!-- 添加文档对话框 -->
  215. <el-dialog v-model="documentDialog.visible" :title="documentDialog.title" width="700px" append-to-body>
  216. <el-form ref="documentFormRef" :model="documentForm" :rules="documentRules" label-width="140px">
  217. <el-form-item
  218. :label="documentForm.type === 1 ? t('document.document.documentForm.planName') : t('document.document.documentForm.name')"
  219. prop="name">
  220. <el-input v-model="documentForm.name" :placeholder="t('document.document.documentForm.namePlaceholder')"
  221. clearable />
  222. </el-form-item>
  223. <el-form-item :label="t('document.document.documentForm.type')" prop="type" v-if="hasAddPlanPermission">
  224. <el-radio-group v-model="documentForm.type" @change="handleDocumentTypeChange">
  225. <el-radio :label="0">{{ t('document.document.documentForm.normalDocument') }}</el-radio>
  226. <el-radio :label="1">{{ t('document.document.documentForm.planDocument') }}</el-radio>
  227. </el-radio-group>
  228. </el-form-item>
  229. <el-form-item :label="t('document.document.documentForm.submitter')" prop="submitterId">
  230. <template v-if="documentForm.type === 0">
  231. <el-input v-model="currentUserName" disabled />
  232. </template>
  233. <template v-else>
  234. <el-select v-model="documentForm.submitterId" filterable remote reserve-keyword
  235. :placeholder="t('document.document.documentForm.submitterPlaceholder')" :remote-method="searchSubmitters"
  236. :loading="submitterSearchLoading" style="width: 100%">
  237. <el-option v-for="submitter in submitterOptions" :key="submitter.id"
  238. :label="`${submitter.name} / ${submitter.dept} --- ${submitter.phoneNumber}`" :value="submitter.id" />
  239. </el-select>
  240. </template>
  241. </el-form-item>
  242. <el-form-item v-if="documentForm.type === 1" :label="t('document.document.documentForm.submitDeadline')"
  243. prop="submitDeadline">
  244. <el-date-picker v-model="documentForm.submitDeadline" type="date" value-format="YYYY-MM-DD"
  245. :placeholder="t('document.document.documentForm.submitDeadlinePlaceholder')" style="width: 100%" />
  246. </el-form-item>
  247. <el-form-item v-if="documentForm.type === 1" :label="t('document.document.documentForm.planType')"
  248. prop="planType">
  249. <el-select v-model="documentForm.planType"
  250. :placeholder="t('document.document.documentForm.planTypePlaceholder')" clearable style="width: 100%">
  251. <el-option v-for="dict in plan_document_type" :key="dict.value" :label="dict.label" :value="dict.value" />
  252. </el-select>
  253. </el-form-item>
  254. <el-form-item :label="t('document.document.documentForm.file')" :prop="documentForm.type === 0 ? 'ossId' : ''">
  255. <fileUpload v-model="uploadedFileId" :limit="1" />
  256. </el-form-item>
  257. <el-form-item v-if="documentForm.submitTime" :label="t('document.document.documentForm.submitTime')">
  258. <el-input v-model="documentForm.submitTime" disabled />
  259. </el-form-item>
  260. <el-form-item :label="t('document.document.documentForm.note')" prop="note">
  261. <el-input v-model="documentForm.note" type="textarea" :rows="4"
  262. :placeholder="t('document.document.documentForm.notePlaceholder')" />
  263. </el-form-item>
  264. </el-form>
  265. <template #footer>
  266. <div class="dialog-footer">
  267. <el-button :loading="documentButtonLoading" type="primary" @click="submitDocumentForm">{{
  268. t('document.document.button.submit') }}</el-button>
  269. <el-button @click="cancelDocument">{{ t('document.document.button.cancel') }}</el-button>
  270. </div>
  271. </template>
  272. </el-dialog>
  273. <!-- 标识文档对话框 -->
  274. <el-dialog v-model="markDialog.visible" :title="markDialog.title" width="500px" append-to-body>
  275. <el-form ref="markFormRef" :model="markForm" :rules="markRules" label-width="120px">
  276. <el-form-item :label="t('document.document.markForm.specification')" prop="type">
  277. <el-select v-model="markForm.type" :placeholder="t('document.document.markForm.specificationPlaceholder')"
  278. clearable style="width: 100%">
  279. <el-option v-for="dict in specificationDict" :key="dict.value" :label="dict.label" :value="dict.value" />
  280. </el-select>
  281. </el-form-item>
  282. </el-form>
  283. <template #footer>
  284. <div class="dialog-footer">
  285. <el-button :loading="markButtonLoading" type="primary" @click="submitMarkForm">{{
  286. t('document.document.button.submit') }}</el-button>
  287. <el-button @click="cancelMark">{{ t('document.document.button.cancel') }}</el-button>
  288. </div>
  289. </template>
  290. </el-dialog>
  291. <!-- 审核文档对话框 -->
  292. <el-dialog v-model="auditDialog.visible" :title="auditDialog.title" width="500px" append-to-body>
  293. <el-form ref="auditFormRef" :model="auditForm" :rules="auditRules" label-width="120px">
  294. <el-form-item :label="t('document.document.auditForm.result')" prop="result">
  295. <el-radio-group v-model="auditForm.result">
  296. <el-radio label="0">{{ t('document.document.auditForm.pass') }}</el-radio>
  297. <el-radio label="1">{{ t('document.document.auditForm.reject') }}</el-radio>
  298. </el-radio-group>
  299. </el-form-item>
  300. <el-form-item v-if="auditForm.result === '1'" :label="t('document.document.auditForm.reason')" prop="reason">
  301. <el-input v-model="auditForm.reason" type="textarea" :rows="4"
  302. placeholder="{{ t('document.document.auditForm.reasonPlaceholder') }}" />
  303. </el-form-item>
  304. </el-form>
  305. <template #footer>
  306. <div class="dialog-footer">
  307. <el-button :loading="auditButtonLoading" type="primary" @click="submitAuditForm">{{
  308. t('document.document.button.submit') }}</el-button>
  309. <el-button @click="cancelAudit">{{ t('document.document.button.cancel') }}</el-button>
  310. </div>
  311. </template>
  312. </el-dialog>
  313. </div>
  314. </template>
  315. <script setup lang="ts">
  316. import { ref, reactive, onMounted, onUnmounted, nextTick, getCurrentInstance, watch, computed } from 'vue';
  317. import { useI18n } from 'vue-i18n';
  318. import { listFolder, addFolder, delFolder, getFolder, updateFolder } from '@/api/document/folder';
  319. import { FolderListVO, FolderForm } from '@/api/document/folder/types';
  320. import { addDocument, listDocument, markDocument } from '@/api/document/document';
  321. import { DocumentForm, DocumentQuery, DocumentVO, DocumentMarkForm } from '@/api/document/document/types';
  322. import { queryMemberNotInCenter } from '@/api/project/management';
  323. import { MemberNotInCenterVO, MemberNotInCenterQuery } from '@/api/project/management/types';
  324. import { Folder, Document, Edit, Delete, Plus, MoreFilled, Location, OfficeBuilding, ArrowRight, Download, Select, Grid, Monitor, Reading, Flag } from '@element-plus/icons-vue';
  325. import { ElMessage, ElMessageBox } from 'element-plus';
  326. import type { FormInstance } from 'element-plus';
  327. import type { ComponentInternalInstance } from 'vue';
  328. import { useUserStore } from '@/store/modules/user';
  329. import { checkPermi } from '@/utils/permission';
  330. import fileUpload from '@/components/FileUpload/index.vue';
  331. interface Props {
  332. projectId?: number | string;
  333. }
  334. const props = defineProps<Props>();
  335. const emit = defineEmits<{
  336. back: [];
  337. }>();
  338. const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  339. const { t } = useI18n();
  340. const userStore = useUserStore();
  341. const { plan_document_type, center_file_specification, project_file_specification } = toRefs<any>(proxy?.useDict('plan_document_type', 'center_file_specification', 'project_file_specification'));
  342. // 数据定义
  343. const loading = ref(false);
  344. const buttonLoading = ref(false);
  345. const treeData = ref<FolderListVO[]>([]);
  346. const folderFormRef = ref<FormInstance>();
  347. const documentFormRef = ref<FormInstance>();
  348. const documentButtonLoading = ref(false);
  349. // 检查是否有计划文档添加权限
  350. const hasAddPlanPermission = computed(() => checkPermi(['document:document:addPlan']));
  351. // 当前用户信息
  352. const currentUserName = ref(userStore.nickname || '');
  353. // 对话框
  354. const dialog = reactive({
  355. visible: false,
  356. title: '',
  357. isEdit: false
  358. });
  359. // 文档对话框
  360. const documentDialog = reactive({
  361. visible: false,
  362. title: ''
  363. });
  364. // 标识文档对话框
  365. const markDialog = reactive({
  366. visible: false,
  367. title: ''
  368. });
  369. // 标识表单ref
  370. const markFormRef = ref<FormInstance>();
  371. const markButtonLoading = ref(false);
  372. // 审核文档对话框
  373. const auditDialog = reactive({
  374. visible: false,
  375. title: ''
  376. });
  377. // 审核表单ref
  378. const auditFormRef = ref<FormInstance>();
  379. const auditButtonLoading = ref(false);
  380. // 当前操作的节点
  381. const currentNode = ref<FolderListVO | null>(null);
  382. // 限制层级相关状态
  383. const isRestricted = ref(false); // 是否限制
  384. const restrictionLevelValue = ref(0); // 限制层级值
  385. // 表单初始数据
  386. const initFormData: FolderForm = {
  387. id: undefined,
  388. projectId: undefined,
  389. parentId: undefined,
  390. type: 0,
  391. name: '',
  392. status: 0,
  393. note: '',
  394. restrictionLevel: -1
  395. };
  396. // 表单数据
  397. const form = ref<FolderForm>({ ...initFormData });
  398. // 文档表单初始数据
  399. const initDocumentFormData: DocumentForm = {
  400. id: undefined,
  401. name: '',
  402. type: 0,
  403. submitterId: undefined,
  404. folderId: undefined,
  405. submitDeadline: undefined,
  406. planType: undefined,
  407. ossId: undefined,
  408. submitTime: undefined,
  409. note: ''
  410. };
  411. // 文件上传的ossId(字符串格式)
  412. const uploadedFileId = ref<string>('');
  413. // 文档表单数据
  414. const documentForm = ref<DocumentForm>({ ...initDocumentFormData });
  415. // 标识表单数据
  416. const markForm = ref<DocumentMarkForm>({
  417. id: 0,
  418. type: ''
  419. });
  420. // 审核表单数据
  421. interface AuditForm {
  422. id: number;
  423. result: string; // 0: 通过, 1: 驳回
  424. reason: string; // 驳回理由
  425. }
  426. const auditForm = ref<AuditForm>({
  427. id: 0,
  428. result: '0', // 默认通过
  429. reason: ''
  430. });
  431. // 审核表单验证规则
  432. const auditRules = reactive({
  433. result: [
  434. {
  435. required: true,
  436. message: t('document.document.auditRule.resultRequired'),
  437. trigger: 'change'
  438. }
  439. ],
  440. reason: [
  441. {
  442. required: true,
  443. message: t('document.document.auditRule.reasonRequired'),
  444. trigger: 'blur'
  445. }
  446. ]
  447. });
  448. // 当前选中的文档
  449. const currentDocument = ref<DocumentVO | null>(null);
  450. // 递交人搜索相关
  451. const submitterSearchLoading = ref(false);
  452. const submitterOptions = ref<MemberNotInCenterVO[]>([]);
  453. let submitterSearchTimer: NodeJS.Timeout | null = null;
  454. // ========== 文档列表相关 ==========
  455. // 文档列表数据
  456. const documentList = ref<DocumentVO[]>([]);
  457. const documentLoading = ref(false);
  458. const documentTotal = ref(0);
  459. // 当前选中的文件夹
  460. const selectedFolder = ref<FolderListVO | null>(null);
  461. // 文档查询参数
  462. const documentQueryParams = reactive<DocumentQuery>({
  463. pageNum: 1,
  464. pageSize: 10,
  465. name: '',
  466. folderId: undefined
  467. });
  468. // 表单验证规则
  469. const rules = {
  470. name: [
  471. { required: true, message: t('document.document.rule.nameRequired'), trigger: 'blur' }
  472. ],
  473. type: [
  474. { required: true, message: t('document.document.rule.typeRequired'), trigger: 'change' }
  475. ]
  476. };
  477. // 文档表单验证规则
  478. const documentRules = {
  479. name: [
  480. { required: true, message: t('document.document.documentRule.nameRequired'), trigger: 'blur' }
  481. ],
  482. submitterId: [
  483. { required: true, message: t('document.document.documentRule.submitterRequired'), trigger: 'change' }
  484. ],
  485. ossId: [
  486. { required: true, message: t('document.document.documentRule.fileRequired'), trigger: 'change' }
  487. ]
  488. };
  489. // 标识表单验证规则
  490. const markRules = {
  491. type: [
  492. { required: true, message: t('document.document.markRule.typeRequired'), trigger: 'change' }
  493. ]
  494. };
  495. // 树形组件配置
  496. const treeProps = {
  497. children: 'children',
  498. label: 'name'
  499. };
  500. // 根据当前文件夹获取对应的字典
  501. const specificationDict = computed(() => {
  502. if (!selectedFolder.value) return [];
  503. // 判断是否在中心底下
  504. const isUnderCenter = checkIfUnderCenter(selectedFolder.value.id);
  505. if (isUnderCenter) {
  506. return center_file_specification?.value || [];
  507. } else {
  508. return project_file_specification?.value || [];
  509. }
  510. });
  511. // 检查文件夹是否在中心底下
  512. const checkIfUnderCenter = (folderId: string | number): boolean => {
  513. // 从当前文件夹往上遍历,如果中间存在着中心类型的文件夹,那么他就是中心层级文件
  514. // 递归查找目标节点并收集从根到目标的路径
  515. const findPathToNode = (tree: FolderListVO[], targetId: string | number, path: FolderListVO[] = []): FolderListVO[] | null => {
  516. for (const node of tree) {
  517. const currentPath = [...path, node];
  518. if (node.id === targetId) {
  519. // 找到目标节点,返回路径
  520. return currentPath;
  521. }
  522. // 在子节点中递归查找
  523. if (node.children && node.children.length > 0) {
  524. const result = findPathToNode(node.children, targetId, currentPath);
  525. if (result) {
  526. return result;
  527. }
  528. }
  529. }
  530. return null;
  531. };
  532. // 获取从根到目标文件夹的路径
  533. const path = findPathToNode(treeData.value, folderId);
  534. if (!path) {
  535. return false;
  536. }
  537. // 检查路径中是否存在type为1或2的节点(国家或中心)
  538. return path.some(node => node.type === 1 || node.type === 2);
  539. };
  540. // 获取文件夹列表
  541. const getList = async () => {
  542. if (!props.projectId) {
  543. ElMessage.warning(t('document.document.message.projectIdNotExist'));
  544. return;
  545. }
  546. loading.value = true;
  547. try {
  548. const res = await listFolder({ projectId: props.projectId } as any);
  549. treeData.value = res.data || [];
  550. } catch (error) {
  551. ElMessage.error(t('document.document.message.getFolderListFailed'));
  552. console.error(error);
  553. } finally {
  554. loading.value = false;
  555. }
  556. };
  557. // 返回项目列表
  558. const handleBack = () => {
  559. emit('back');
  560. };
  561. // 表单重置
  562. const reset = () => {
  563. form.value = { ...initFormData };
  564. isRestricted.value = false;
  565. restrictionLevelValue.value = 0;
  566. folderFormRef.value?.resetFields();
  567. };
  568. // 处理限制状态变化
  569. const handleRestrictionChange = (value: boolean) => {
  570. if (value) {
  571. // 选择限制,使用restrictionLevelValue的值
  572. form.value.restrictionLevel = restrictionLevelValue.value;
  573. } else {
  574. // 选择不限制,设置为-1
  575. form.value.restrictionLevel = -1;
  576. }
  577. };
  578. // 监听restrictionLevelValue变化,同步更新form.restrictionLevel
  579. watch(restrictionLevelValue, (newValue) => {
  580. if (isRestricted.value) {
  581. form.value.restrictionLevel = newValue;
  582. }
  583. });
  584. // 取消按钮
  585. const cancel = () => {
  586. reset();
  587. dialog.visible = false;
  588. };
  589. // 新建文件夹(顶级)
  590. const handleAddFolder = () => {
  591. reset();
  592. currentNode.value = null;
  593. form.value.projectId = props.projectId;
  594. form.value.parentId = undefined;
  595. dialog.visible = true;
  596. dialog.title = t('document.document.dialog.addFolder');
  597. dialog.isEdit = false;
  598. };
  599. // 新增子节点
  600. const handleAddChild = (data: FolderListVO) => {
  601. reset();
  602. currentNode.value = data;
  603. form.value.projectId = props.projectId;
  604. form.value.parentId = data.id;
  605. dialog.visible = true;
  606. dialog.title = t('document.document.dialog.addChild');
  607. dialog.isEdit = false;
  608. };
  609. // 获取可选类型(根据父节点类型限制)
  610. const getAvailableTypes = () => {
  611. if (!currentNode.value) {
  612. // 顶级节点,可以选择所有类型
  613. return [
  614. { label: t('document.document.type.folder'), value: 0 },
  615. { label: t('document.document.type.country'), value: 1 },
  616. { label: t('document.document.type.center'), value: 2 }
  617. ];
  618. }
  619. const parentType = currentNode.value.type;
  620. if (parentType === 1 || parentType === 2) {
  621. // 父节点是国家或中心,子节点只能是中心或文件夹
  622. return [
  623. { label: t('document.document.type.folder'), value: 0 },
  624. { label: t('document.document.type.center'), value: 2 }
  625. ];
  626. } else {
  627. // 父节点是文件夹,子节点只能是文件夹
  628. return [
  629. { label: t('document.document.type.folder'), value: 0 }
  630. ];
  631. }
  632. };
  633. // 下拉菜单命令处理
  634. const handleCommand = (command: string, data: FolderListVO) => {
  635. if (command.startsWith('add:')) {
  636. const cmdPart = command.split(':')[1];
  637. if (cmdPart === 'document') {
  638. handleAddDocument(data);
  639. } else {
  640. const type = parseInt(cmdPart);
  641. handleAddChildWithType(data, type);
  642. }
  643. } else if (command === 'edit') {
  644. handleEdit(data);
  645. } else if (command === 'delete') {
  646. handleDelete(data);
  647. }
  648. };
  649. // 新增子节点(指定类型)
  650. const handleAddChildWithType = (data: FolderListVO, type: number) => {
  651. reset();
  652. currentNode.value = data;
  653. form.value.projectId = props.projectId;
  654. form.value.parentId = data.id;
  655. form.value.type = type;
  656. dialog.visible = true;
  657. const typeLabel = type === 0 ? t('document.document.type.folder') : type === 1 ? t('document.document.type.country') : t('document.document.type.center');
  658. dialog.title = type === 0 ? t('document.document.dialog.addFolder') : type === 1 ? t('document.document.dialog.addCountry') : t('document.document.dialog.addCenter');
  659. dialog.isEdit = false;
  660. };
  661. // 提交表单
  662. const submitForm = () => {
  663. folderFormRef.value?.validate(async (valid: boolean) => {
  664. if (valid) {
  665. // 如果是编辑,显示确认对话框
  666. if (dialog.isEdit) {
  667. try {
  668. const confirmMessage = `
  669. <div style="text-align: left;">
  670. <p><strong>${t('document.document.confirm.nameLabel')}</strong>${form.value.name}</p>
  671. <p><strong>${t('document.document.confirm.restrictionLevelLabel')}</strong>${form.value.restrictionLevel}</p>
  672. <p><strong>${t('document.document.confirm.noteLabel')}</strong>${form.value.note || t('document.document.confirm.noNote')}</p>
  673. </div>
  674. `;
  675. await ElMessageBox.confirm(confirmMessage, t('document.document.dialog.confirmEdit'), {
  676. confirmButtonText: t('document.document.message.confirmButton'),
  677. cancelButtonText: t('document.document.message.cancelButton'),
  678. type: 'warning',
  679. dangerouslyUseHTMLString: true
  680. });
  681. } catch {
  682. return; // 用户取消
  683. }
  684. }
  685. buttonLoading.value = true;
  686. try {
  687. if (dialog.isEdit) {
  688. await updateFolder(form.value);
  689. proxy?.$modal.msgSuccess(t('document.document.message.editSuccess'));
  690. } else {
  691. await addFolder(form.value);
  692. proxy?.$modal.msgSuccess(t('document.document.message.addSuccess'));
  693. }
  694. dialog.visible = false;
  695. await getList();
  696. } catch (error) {
  697. console.error(dialog.isEdit ? t('document.document.message.editFailed') : t('document.document.message.addFailed'), error);
  698. } finally {
  699. buttonLoading.value = false;
  700. }
  701. }
  702. });
  703. };
  704. // 编辑
  705. const handleEdit = async (data: FolderListVO) => {
  706. reset();
  707. loading.value = true;
  708. try {
  709. const res = await getFolder(data.id);
  710. Object.assign(form.value, res.data);
  711. // 设置限制层级状态
  712. if (form.value.restrictionLevel === -1) {
  713. isRestricted.value = false;
  714. restrictionLevelValue.value = 0;
  715. } else {
  716. isRestricted.value = true;
  717. restrictionLevelValue.value = form.value.restrictionLevel;
  718. }
  719. currentNode.value = null; // 编辑时不限制类型
  720. dialog.visible = true;
  721. dialog.title = t('document.document.dialog.editFolder');
  722. dialog.isEdit = true;
  723. } catch (error) {
  724. ElMessage.error(t('document.document.message.getFolderInfoFailed'));
  725. console.error(error);
  726. } finally {
  727. loading.value = false;
  728. }
  729. };
  730. // 删除
  731. const handleDelete = async (data: FolderListVO) => {
  732. // 检查是否有子节点
  733. if (data.children && data.children.length > 0) {
  734. ElMessage.warning(t('document.document.message.hasChildren'));
  735. return;
  736. }
  737. try {
  738. await ElMessageBox.confirm(t('document.document.message.deleteConfirm', { name: data.name }), t('document.document.message.deleteTitle'), {
  739. confirmButtonText: t('document.document.message.confirmButton'),
  740. cancelButtonText: t('document.document.message.cancelButton'),
  741. type: 'warning'
  742. });
  743. loading.value = true;
  744. await delFolder(data.id);
  745. ElMessage.success(t('document.document.message.deleteSuccess'));
  746. await getList();
  747. } catch (error: any) {
  748. // 用户取消删除或删除失败
  749. if (error !== 'cancel') {
  750. console.error(t('document.document.message.deleteFailed'), error);
  751. }
  752. } finally {
  753. loading.value = false;
  754. }
  755. };
  756. // 菜单状态管理
  757. const activeMenu = ref<string | number | null>(null); // 当前激活的一级菜单
  758. const showSecondaryMenu = ref(false); // 是否显示二级菜单
  759. const primaryMenuStyle = ref<any>({}); // 一级菜单的样式(位置)
  760. const secondaryMenuStyle = ref<any>({}); // 二级菜单的样式(位置)
  761. const currentMenuData = ref<FolderListVO | null>(null); // 当前操作的菜单数据
  762. // 切换菜单显示
  763. const toggleMenu = (event: MouseEvent, data: FolderListVO) => {
  764. // 只处理点击事件,忽略其他事件
  765. if (event.type !== 'click') {
  766. return;
  767. }
  768. event.stopPropagation();
  769. event.preventDefault();
  770. const trigger = event.currentTarget as HTMLElement;
  771. const rect = trigger.getBoundingClientRect();
  772. if (activeMenu.value === data.id) {
  773. // 如果点击的是同一个菜单,关闭它
  774. closeAllMenus();
  775. } else {
  776. // 关闭之前的菜单,打开新菜单
  777. // 计算一级菜单位置
  778. primaryMenuStyle.value = {
  779. left: `${rect.left}px`,
  780. top: `${rect.bottom + 2}px`
  781. };
  782. activeMenu.value = data.id;
  783. currentMenuData.value = data;
  784. showSecondaryMenu.value = false;
  785. }
  786. };
  787. // 切换二级菜单显示
  788. const toggleSubmenu = (event: MouseEvent) => {
  789. // 只处理点击事件,忽略其他事件
  790. if (event.type !== 'click') {
  791. return;
  792. }
  793. event.stopPropagation();
  794. event.preventDefault();
  795. const target = event.currentTarget as HTMLElement;
  796. const rect = target.getBoundingClientRect();
  797. if (showSecondaryMenu.value) {
  798. // 如果已经显示,则关闭
  799. showSecondaryMenu.value = false;
  800. } else {
  801. // 先设置位置,再显示(避免位置计算前就显示)
  802. secondaryMenuStyle.value = {
  803. left: `${rect.right + 5}px`,
  804. top: `${rect.top}px`
  805. };
  806. // 使用 nextTick 确保位置设置后再显示
  807. nextTick(() => {
  808. showSecondaryMenu.value = true;
  809. });
  810. }
  811. };
  812. // 处理菜单项点击
  813. const handleMenuItemClick = (command: string, data: FolderListVO | null) => {
  814. if (!data) return;
  815. // 执行命令
  816. handleCommand(command, data);
  817. // 关闭所有菜单
  818. closeAllMenus();
  819. };
  820. // ========== 文档相关函数 ==========
  821. // 重置文档表单
  822. const resetDocumentForm = () => {
  823. documentForm.value = { ...initDocumentFormData };
  824. uploadedFileId.value = '';
  825. // 如果没有计划文档权限,默认为非计划文档
  826. if (!hasAddPlanPermission.value) {
  827. documentForm.value.type = 0;
  828. }
  829. documentForm.value.submitterId = userStore.userId;
  830. currentUserName.value = userStore.nickname || '';
  831. submitterOptions.value = [];
  832. documentFormRef.value?.resetFields();
  833. };
  834. // 添加文档
  835. const handleAddDocument = (data: FolderListVO) => {
  836. resetDocumentForm();
  837. documentForm.value.folderId = data.id;
  838. documentDialog.visible = true;
  839. documentDialog.title = t('document.document.dialog.addDocument');
  840. };
  841. // 处理文档类型变化
  842. const handleDocumentTypeChange = (value: number) => {
  843. if (value === 0) {
  844. // 非计划文档,递交人为当前用户
  845. documentForm.value.submitterId = userStore.userId;
  846. documentForm.value.submitDeadline = undefined;
  847. documentForm.value.planType = undefined;
  848. } else {
  849. // 计划文档,清空递交人
  850. documentForm.value.submitterId = undefined;
  851. }
  852. };
  853. // 搜索递交人
  854. const searchSubmitters = async (query: string) => {
  855. if (!query || query.trim() === '') {
  856. submitterOptions.value = [];
  857. return;
  858. }
  859. // 清除之前的定时器
  860. if (submitterSearchTimer) {
  861. clearTimeout(submitterSearchTimer);
  862. }
  863. // 设置防抖
  864. submitterSearchTimer = setTimeout(async () => {
  865. submitterSearchLoading.value = true;
  866. try {
  867. const queryParams: MemberNotInCenterQuery = {
  868. pageNum: 1,
  869. pageSize: 10,
  870. projectId: props.projectId || 0,
  871. folderId: 0,
  872. name: query
  873. };
  874. const res = await queryMemberNotInCenter(queryParams);
  875. submitterOptions.value = res.rows || [];
  876. } catch (error) {
  877. console.error('Failed to search submitters:', error);
  878. ElMessage.error(t('document.document.message.searchSubmitterFailed'));
  879. } finally {
  880. submitterSearchLoading.value = false;
  881. }
  882. }, 300);
  883. };
  884. // 监听文件上传变化
  885. watch(uploadedFileId, (newVal) => {
  886. if (newVal) {
  887. // 解析文件ID(可能是逗号分隔的字符串)
  888. const ids = newVal.split(',').filter(id => id.trim());
  889. if (ids.length > 0) {
  890. documentForm.value.ossId = parseInt(ids[0]);
  891. // 自动设置递交时间为当前时间
  892. const now = new Date();
  893. documentForm.value.submitTime = proxy?.parseTime(now, '{y}-{m}-{d} {h}:{i}:{s}');
  894. }
  895. } else {
  896. documentForm.value.ossId = undefined;
  897. documentForm.value.submitTime = undefined;
  898. }
  899. });
  900. // 取消文档对话框
  901. const cancelDocument = () => {
  902. resetDocumentForm();
  903. documentDialog.visible = false;
  904. };
  905. // 提交文档表单
  906. const submitDocumentForm = () => {
  907. documentFormRef.value?.validate(async (valid: boolean) => {
  908. if (valid) {
  909. documentButtonLoading.value = true;
  910. try {
  911. // 构建完整的请求数据,确保所有字段都存在(参考文件夹的实现)
  912. const submitData: DocumentForm = {
  913. id: documentForm.value.id || 0,
  914. name: documentForm.value.name || '',
  915. type: documentForm.value.type !== undefined ? documentForm.value.type : 0,
  916. submitterId: documentForm.value.submitterId || 0,
  917. folderId: documentForm.value.folderId || 0,
  918. submitDeadline: documentForm.value.submitDeadline || '',
  919. planType: documentForm.value.planType || '',
  920. ossId: documentForm.value.ossId || null,
  921. submitTime: documentForm.value.submitTime || '',
  922. note: documentForm.value.note || ''
  923. };
  924. await addDocument(submitData);
  925. proxy?.$modal.msgSuccess(t('document.document.message.addDocumentSuccess'));
  926. documentDialog.visible = false;
  927. await getList();
  928. // 刷新文档列表
  929. await getDocumentList();
  930. } catch (error) {
  931. console.error(t('document.document.message.addDocumentFailed'), error);
  932. } finally {
  933. documentButtonLoading.value = false;
  934. }
  935. }
  936. });
  937. };
  938. // ========== 文档列表相关函数 ==========
  939. // 点击文件夹节点
  940. const handleFolderClick = (data: FolderListVO) => {
  941. // 只有文件夹类型(type=0)才显示文档列表
  942. if (data.type === 0) {
  943. selectedFolder.value = data;
  944. // 重置查询参数
  945. documentQueryParams.name = '';
  946. documentQueryParams.pageNum = 1;
  947. // 加载文档列表
  948. getDocumentList();
  949. } else {
  950. selectedFolder.value = null;
  951. documentList.value = [];
  952. }
  953. };
  954. // 查询文档列表
  955. const getDocumentList = async () => {
  956. if (!selectedFolder.value) return;
  957. documentLoading.value = true;
  958. documentQueryParams.folderId = selectedFolder.value.id;
  959. try {
  960. const res = await listDocument(documentQueryParams);
  961. documentList.value = res.rows || [];
  962. documentTotal.value = res.total || 0;
  963. } catch (error) {
  964. console.error('Failed to get document list:', error);
  965. ElMessage.error(t('document.document.message.getDocumentListFailed'));
  966. } finally {
  967. documentLoading.value = false;
  968. }
  969. };
  970. // 搜索按钮
  971. const handleDocumentQuery = () => {
  972. documentQueryParams.pageNum = 1;
  973. getDocumentList();
  974. };
  975. // 重置按钮
  976. const handleDocumentReset = () => {
  977. documentQueryParams.name = '';
  978. documentQueryParams.pageNum = 1;
  979. getDocumentList();
  980. };
  981. // 审核文档
  982. const handleAudit = (row: DocumentVO) => {
  983. currentDocument.value = row;
  984. auditForm.value = {
  985. id: row.id,
  986. result: '0', // 默认通过
  987. reason: ''
  988. };
  989. auditDialog.visible = true;
  990. auditDialog.title = t('document.document.dialog.auditDocument');
  991. // 重置表单验证
  992. nextTick(() => {
  993. auditFormRef.value?.clearValidate();
  994. });
  995. };
  996. // 取消审核
  997. const cancelAudit = () => {
  998. auditDialog.visible = false;
  999. auditForm.value = {
  1000. id: 0,
  1001. result: '0',
  1002. reason: ''
  1003. };
  1004. currentDocument.value = null;
  1005. };
  1006. // 提交审核表单
  1007. const submitAuditForm = () => {
  1008. auditFormRef.value?.validate(async (valid: boolean) => {
  1009. if (valid) {
  1010. auditButtonLoading.value = true;
  1011. try {
  1012. // 暂时不与后端进行交互,仅关闭弹窗
  1013. proxy?.$modal.msgSuccess(t('document.document.message.auditSuccess'));
  1014. auditDialog.visible = false;
  1015. // 刷新文档列表
  1016. await getDocumentList();
  1017. } catch (error) {
  1018. console.error(t('document.document.message.auditFailed'), error);
  1019. } finally {
  1020. auditButtonLoading.value = false;
  1021. }
  1022. }
  1023. });
  1024. };
  1025. // 下载文档
  1026. const handleDownload = (row: DocumentVO) => {
  1027. if (!row.url) {
  1028. ElMessage.warning(t('document.document.message.noFileToDownload'));
  1029. return;
  1030. }
  1031. // 新建a标签下载文件
  1032. const a = document.createElement('a');
  1033. a.href = row.url;
  1034. a.download = row.fileName || row.name || 'document';
  1035. document.body.appendChild(a);
  1036. a.click();
  1037. document.body.removeChild(a);
  1038. };
  1039. // 标识文档
  1040. const handleMark = (row: DocumentVO) => {
  1041. currentDocument.value = row;
  1042. markForm.value = {
  1043. id: row.id,
  1044. type: ''
  1045. };
  1046. markDialog.visible = true;
  1047. markDialog.title = t('document.document.dialog.markDocument');
  1048. // 重置表单验证
  1049. nextTick(() => {
  1050. markFormRef.value?.clearValidate();
  1051. });
  1052. };
  1053. // 取消标识
  1054. const cancelMark = () => {
  1055. markDialog.visible = false;
  1056. markForm.value = {
  1057. id: 0,
  1058. type: ''
  1059. };
  1060. currentDocument.value = null;
  1061. };
  1062. // 提交标识表单
  1063. const submitMarkForm = () => {
  1064. markFormRef.value?.validate(async (valid: boolean) => {
  1065. if (valid) {
  1066. markButtonLoading.value = true;
  1067. try {
  1068. await markDocument(markForm.value);
  1069. proxy?.$modal.msgSuccess(t('document.document.message.markSuccess'));
  1070. markDialog.visible = false;
  1071. // 刷新文档列表
  1072. await getDocumentList();
  1073. } catch (error) {
  1074. console.error(t('document.document.message.markFailed'), error);
  1075. } finally {
  1076. markButtonLoading.value = false;
  1077. }
  1078. }
  1079. });
  1080. };
  1081. // ========== 文件类型判断函数 ==========
  1082. // 判断是否为Word文档
  1083. const isWordFile = (fileName: string): boolean => {
  1084. if (!fileName) return false;
  1085. const lowerFileName = fileName.toLowerCase();
  1086. return lowerFileName.endsWith('.doc') || lowerFileName.endsWith('.docx');
  1087. };
  1088. // 判断是否为Excel文档
  1089. const isExcelFile = (fileName: string): boolean => {
  1090. if (!fileName) return false;
  1091. const lowerFileName = fileName.toLowerCase();
  1092. return lowerFileName.endsWith('.xls') ||
  1093. lowerFileName.endsWith('.xlsx') ||
  1094. lowerFileName.endsWith('.csv');
  1095. };
  1096. // 判断是否为PPT文档
  1097. const isPPTFile = (fileName: string): boolean => {
  1098. if (!fileName) return false;
  1099. const lowerFileName = fileName.toLowerCase();
  1100. return lowerFileName.endsWith('.ppt') || lowerFileName.endsWith('.pptx');
  1101. };
  1102. // 判断是否为PDF文档
  1103. const isPDFFile = (fileName: string): boolean => {
  1104. if (!fileName) return false;
  1105. return fileName.toLowerCase().endsWith('.pdf');
  1106. };
  1107. // 关闭所有菜单
  1108. const closeAllMenus = () => {
  1109. showSecondaryMenu.value = false;
  1110. activeMenu.value = null;
  1111. currentMenuData.value = null;
  1112. primaryMenuStyle.value = {};
  1113. secondaryMenuStyle.value = {};
  1114. };
  1115. // 点击页面其他地方关闭菜单
  1116. const handleClickOutside = (event: Event) => {
  1117. // 如果没有激活的菜单,直接返回
  1118. if (!activeMenu.value && !showSecondaryMenu.value) {
  1119. return;
  1120. }
  1121. const target = event.target as HTMLElement;
  1122. // 检查点击是否在菜单内部或触发器上
  1123. const isClickInsideMenu = target.closest('.primary-menu') ||
  1124. target.closest('.secondary-menu') ||
  1125. target.closest('.menu-trigger');
  1126. // 如果点击在菜单外部,立即关闭所有菜单
  1127. if (!isClickInsideMenu) {
  1128. closeAllMenus();
  1129. }
  1130. };
  1131. // 处理滚动事件,滚动时关闭菜单
  1132. const handleScroll = () => {
  1133. if (activeMenu.value || showSecondaryMenu.value) {
  1134. closeAllMenus();
  1135. }
  1136. };
  1137. // 初始化
  1138. onMounted(() => {
  1139. getList();
  1140. // 添加全局点击监听(捕获阶段)
  1141. document.addEventListener('click', handleClickOutside, true);
  1142. // 添加滚动监听
  1143. document.addEventListener('scroll', handleScroll, true);
  1144. });
  1145. // 清理
  1146. onUnmounted(() => {
  1147. // 移除全局点击监听
  1148. document.removeEventListener('click', handleClickOutside, true);
  1149. // 移除滚动监听
  1150. document.removeEventListener('scroll', handleScroll, true);
  1151. // 清理菜单状态
  1152. closeAllMenus();
  1153. // 清理搜索定时器
  1154. if (submitterSearchTimer) {
  1155. clearTimeout(submitterSearchTimer);
  1156. submitterSearchTimer = null;
  1157. }
  1158. });
  1159. </script>
  1160. <style scoped lang="scss">
  1161. .flex {
  1162. display: flex;
  1163. }
  1164. .justify-between {
  1165. justify-content: space-between;
  1166. }
  1167. .items-center {
  1168. align-items: center;
  1169. }
  1170. .text-lg {
  1171. font-size: 1.125rem;
  1172. }
  1173. .font-bold {
  1174. font-weight: 700;
  1175. }
  1176. .content-wrapper {
  1177. display: flex;
  1178. height: calc(100vh - 250px);
  1179. min-height: 500px;
  1180. }
  1181. .tree-container {
  1182. width: 300px;
  1183. border-right: 1px solid #e4e7ed;
  1184. display: flex;
  1185. flex-direction: column;
  1186. }
  1187. .tree-header {
  1188. padding: 10px;
  1189. border-bottom: 1px solid #e4e7ed;
  1190. }
  1191. .tree-scrollbar {
  1192. flex: 1;
  1193. overflow: hidden;
  1194. :deep(.el-scrollbar__view) {
  1195. padding: 10px;
  1196. }
  1197. }
  1198. .custom-tree-node {
  1199. flex: 1;
  1200. display: flex;
  1201. align-items: center;
  1202. font-size: 14px;
  1203. padding-right: 8px;
  1204. .el-icon {
  1205. margin-right: 8px;
  1206. font-size: 16px;
  1207. }
  1208. .node-label {
  1209. flex: 1;
  1210. overflow: hidden;
  1211. text-overflow: ellipsis;
  1212. white-space: nowrap;
  1213. cursor: pointer;
  1214. &:hover {
  1215. color: var(--el-color-primary);
  1216. }
  1217. }
  1218. .node-actions {
  1219. display: none;
  1220. position: relative;
  1221. }
  1222. &:hover .node-actions {
  1223. display: inline-flex;
  1224. gap: 4px;
  1225. }
  1226. }
  1227. .menu-trigger {
  1228. cursor: pointer;
  1229. display: flex;
  1230. align-items: center;
  1231. font-size: 16px;
  1232. padding: 4px;
  1233. border-radius: 4px;
  1234. transition: background-color 0.3s, color 0.3s;
  1235. &:hover {
  1236. background-color: #f5f7fa;
  1237. color: var(--el-color-primary);
  1238. }
  1239. }
  1240. .content-container {
  1241. flex: 1;
  1242. padding: 20px;
  1243. overflow: auto;
  1244. }
  1245. .document-list-container {
  1246. width: 100%;
  1247. .search-form {
  1248. margin-bottom: 16px;
  1249. }
  1250. .file-name-cell {
  1251. display: flex;
  1252. align-items: center;
  1253. gap: 8px;
  1254. padding: 0 8px;
  1255. .file-icon {
  1256. flex-shrink: 0;
  1257. }
  1258. .file-name-text {
  1259. flex: 1;
  1260. text-align: left;
  1261. overflow: hidden;
  1262. text-overflow: ellipsis;
  1263. white-space: nowrap;
  1264. }
  1265. .download-btn {
  1266. flex-shrink: 0;
  1267. font-size: 16px;
  1268. &:hover {
  1269. transform: scale(1.1);
  1270. }
  1271. }
  1272. }
  1273. }
  1274. .detail-content {
  1275. max-width: 800px;
  1276. }
  1277. /* 一级菜单样式 */
  1278. .primary-menu {
  1279. position: fixed;
  1280. min-width: 120px;
  1281. background: #fff;
  1282. border: 1px solid #e4e7ed;
  1283. border-radius: 4px;
  1284. box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  1285. padding: 5px 0;
  1286. margin: 0;
  1287. list-style: none;
  1288. z-index: 2000;
  1289. .menu-item {
  1290. padding: 8px 16px;
  1291. cursor: pointer;
  1292. font-size: 14px;
  1293. color: #606266;
  1294. transition: background-color 0.3s, color 0.3s;
  1295. display: flex;
  1296. justify-content: space-between;
  1297. align-items: center;
  1298. white-space: nowrap;
  1299. &:hover {
  1300. background-color: #f5f7fa;
  1301. color: var(--el-color-primary);
  1302. }
  1303. &.has-submenu {
  1304. .arrow-icon {
  1305. margin-left: 8px;
  1306. font-size: 12px;
  1307. }
  1308. }
  1309. }
  1310. }
  1311. /* 二级菜单样式 */
  1312. .secondary-menu {
  1313. position: fixed;
  1314. min-width: 120px;
  1315. background: #fff;
  1316. border: 1px solid #e4e7ed;
  1317. border-radius: 4px;
  1318. box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  1319. padding: 5px 0;
  1320. margin: 0;
  1321. list-style: none;
  1322. z-index: 3000;
  1323. .menu-item {
  1324. padding: 8px 16px;
  1325. cursor: pointer;
  1326. font-size: 14px;
  1327. color: #606266;
  1328. transition: background-color 0.3s, color 0.3s;
  1329. &:hover {
  1330. background-color: #f5f7fa;
  1331. color: var(--el-color-primary);
  1332. }
  1333. }
  1334. }
  1335. </style>