| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505 |
- <template>
- <div>
- <el-card shadow="never">
- <template #header>
- <div class="flex justify-between items-center">
- <span class="text-lg font-bold">{{ t('document.document.header.title') }}</span>
- <el-button type="primary" @click="handleBack">{{ t('document.document.header.backToList') }}</el-button>
- </div>
- </template>
- <div class="content-wrapper">
- <div class="tree-container">
- <div class="tree-header">
- <el-button v-hasPermi="['document:folder:add']" type="primary" icon="Plus" size="small"
- @click="handleAddFolder">{{ t('document.document.button.newFolder') }}</el-button>
- </div>
- <el-scrollbar class="tree-scrollbar">
- <el-tree v-loading="loading" :data="treeData" :props="treeProps" node-key="id" default-expand-all
- :expand-on-click-node="false">
- <template #default="{ node, data }">
- <span class="custom-tree-node">
- <el-icon>
- <Folder v-if="data.type === 0" />
- <Location v-else-if="data.type === 1" />
- <OfficeBuilding v-else-if="data.type === 2" />
- <Document v-else />
- </el-icon>
- <span class="node-label" @click="handleFolderClick(data)">{{ node.label }}</span>
- <span class="node-actions">
- <span class="menu-trigger" @click="toggleMenu($event, data)">
- <el-icon>
- <MoreFilled />
- </el-icon>
- </span>
- </span>
- </span>
- </template>
- </el-tree>
- </el-scrollbar>
- </div>
- <!-- 一级菜单 -->
- <ul class="primary-menu" v-if="activeMenu !== null" :style="primaryMenuStyle">
- <li class="menu-item has-submenu" v-hasPermi="['document:folder:add']" @click.stop="toggleSubmenu($event)">
- <span>{{ t('document.document.menu.add') }}</span>
- <el-icon class="arrow-icon">
- <ArrowRight />
- </el-icon>
- </li>
- <li class="menu-item" v-hasPermi="['document:folder:edit']"
- @click="handleMenuItemClick('edit', currentMenuData)">
- <span>{{ t('document.document.menu.edit') }}</span>
- </li>
- <li class="menu-item" v-hasPermi="['document:folder:remove']"
- @click="handleMenuItemClick('delete', currentMenuData)">
- <span>{{ t('document.document.menu.delete') }}</span>
- </li>
- </ul>
- <!-- 二级菜单 -->
- <ul class="secondary-menu" v-if="showSecondaryMenu" :style="secondaryMenuStyle">
- <!-- 国家或中心:显示中心和文件夹 -->
- <template v-if="currentMenuData && (currentMenuData.type === 1 || currentMenuData.type === 2)">
- <li class="menu-item" @click="handleMenuItemClick('add:2', currentMenuData)">
- <span>{{ t('document.document.menu.center') }}</span>
- </li>
- <li class="menu-item" @click="handleMenuItemClick('add:0', currentMenuData)">
- <span>{{ t('document.document.menu.folder') }}</span>
- </li>
- </template>
- <!-- 文件夹:只显示文件夹和文档 -->
- <template v-else-if="currentMenuData && currentMenuData.type === 0">
- <li class="menu-item" @click="handleMenuItemClick('add:0', currentMenuData)">
- <span>{{ t('document.document.menu.folder') }}</span>
- </li>
- <li class="menu-item" v-hasPermi="['document:document:add']"
- @click="handleMenuItemClick('add:document', currentMenuData)">
- <span>{{ t('document.document.menu.document') }}</span>
- </li>
- </template>
- </ul>
- <div class="content-container">
- <!-- 文档列表展示区域 -->
- <div v-if="selectedFolder" class="document-list-container">
- <!-- 搜索栏 -->
- <el-form :model="documentQueryParams" :inline="true" class="search-form">
- <el-form-item :label="t('document.document.documentList.fileName')">
- <el-input v-model="documentQueryParams.name"
- :placeholder="t('document.document.documentList.fileNamePlaceholder')" clearable style="width: 240px"
- @keyup.enter="handleDocumentQuery" />
- </el-form-item>
- <el-form-item>
- <el-button type="primary" icon="Search" @click="handleDocumentQuery">{{
- t('document.document.button.search')
- }}</el-button>
- <el-button icon="Refresh" @click="handleDocumentReset">{{ t('document.document.button.reset')
- }}</el-button>
- </el-form-item>
- </el-form>
- <!-- 文档列表 -->
- <el-table v-loading="documentLoading" :data="documentList" border style="margin-top: 10px">
- <el-table-column type="index" width="55" align="center"
- :label="t('document.document.documentList.index')" />
- <el-table-column prop="name" :label="t('document.document.documentList.name')" min-width="150"
- show-overflow-tooltip />
- <el-table-column prop="specification" :label="t('document.document.documentList.specification')"
- min-width="120" show-overflow-tooltip />
- <el-table-column prop="planDocumentType" :label="t('document.document.documentList.planDocumentType')"
- width="120" align="center">
- <template #default="scope">
- <dict-tag v-if="scope.row.planDocumentType" :options="plan_document_type"
- :value="scope.row.planDocumentType" />
- <span v-else>-</span>
- </template>
- </el-table-column>
- <el-table-column prop="submitter" :label="t('document.document.documentList.submitter')" width="120"
- align="center" />
- <el-table-column prop="submitDeadline" :label="t('document.document.documentList.submitDeadline')"
- width="110" align="center">
- <template #default="scope">
- <span v-if="scope.row.submitDeadline">{{ parseTime(scope.row.submitDeadline, '{y}-{m}-{d}') }}</span>
- <span v-else>-</span>
- </template>
- </el-table-column>
- <el-table-column prop="submitTime" :label="t('document.document.documentList.submitTime')" width="160"
- align="center">
- <template #default="scope">
- <span v-if="scope.row.submitTime">{{ parseTime(scope.row.submitTime) }}</span>
- <span v-else>-</span>
- </template>
- </el-table-column>
- <el-table-column prop="url" :label="t('document.document.documentList.url')" min-width="200">
- <template #default="scope">
- <div v-if="scope.row.fileName" class="file-name-cell">
- <el-icon :size="18" class="file-icon">
- <Document v-if="isWordFile(scope.row.fileName)" style="color: #2b579a;" />
- <Grid v-else-if="isExcelFile(scope.row.fileName)" style="color: #217346;" />
- <Monitor v-else-if="isPPTFile(scope.row.fileName)" style="color: #d24726;" />
- <Reading v-else-if="isPDFFile(scope.row.fileName)" style="color: #e74c3c;" />
- <Document v-else style="color: #606266;" />
- </el-icon>
- <span class="file-name-text">{{ scope.row.fileName }}</span>
- <!-- 下载按钮已注释 -->
- <!-- <el-link v-if="scope.row.url" type="primary" :href="scope.row.url" :download="scope.row.fileName" target="_blank" :underline="false" class="download-btn">
- <el-icon><Download /></el-icon>
- </el-link> -->
- </div>
- <span v-else>-</span>
- </template>
- </el-table-column>
- <el-table-column prop="note" :label="t('document.document.documentList.note')" min-width="150"
- show-overflow-tooltip />
- <el-table-column prop="createTime" :label="t('document.document.documentList.createTime')" width="160"
- align="center">
- <template #default="scope">
- <span v-if="scope.row.createTime">{{ parseTime(scope.row.createTime) }}</span>
- <span v-else>-</span>
- </template>
- </el-table-column>
- <el-table-column prop="updateTime" :label="t('document.document.documentList.updateTime')" width="160"
- align="center">
- <template #default="scope">
- <span v-if="scope.row.updateTime">{{ parseTime(scope.row.updateTime) }}</span>
- <span v-else>-</span>
- </template>
- </el-table-column>
- <el-table-column :label="t('document.document.documentList.action')" width="150" align="center"
- fixed="right">
- <template #default="scope">
- <el-button v-hasPermi="['document:document:audit']" type="primary" link :icon="Select"
- @click="handleAudit(scope.row)" :title="t('document.document.button.audit')" />
- <el-button v-hasPermi="['document:document:mark']" type="primary" link icon="Flag"
- @click="handleMark(scope.row)" :title="t('document.document.button.mark')" />
- <el-button type="primary" link icon="Download" @click="handleDownload(scope.row)"
- :title="t('document.document.button.download')" :disabled="!scope.row.url" />
- </template>
- </el-table-column>
- </el-table>
- <!-- 分页 -->
- <pagination v-show="documentTotal > 0" v-model:page="documentQueryParams.pageNum"
- v-model:limit="documentQueryParams.pageSize" :total="documentTotal" @pagination="getDocumentList" />
- </div>
- <!-- 空状态 -->
- <el-empty v-else :description="t('document.document.empty.description')">
- </el-empty>
- </div>
- </div>
- </el-card>
- <!-- 添加文件夹对话框 -->
- <el-dialog v-model="dialog.visible" :title="dialog.title" width="600px" append-to-body>
- <el-form ref="folderFormRef" :model="form" :rules="rules" label-width="140px">
- <el-form-item :label="t('document.document.form.name')" prop="name">
- <el-input v-model="form.name" :placeholder="t('document.document.form.namePlaceholder')" clearable />
- </el-form-item>
- <el-form-item :label="t('document.document.form.restrictionLevel')" prop="restrictionLevel">
- <el-radio-group v-model="isRestricted" @change="handleRestrictionChange">
- <el-radio :label="false">{{ t('document.document.form.noRestriction') }}</el-radio>
- <el-radio :label="true">{{ t('document.document.form.restricted') }}</el-radio>
- </el-radio-group>
- <el-input-number v-if="isRestricted" v-model="restrictionLevelValue" :min="0" :max="10000"
- style="width: 100%; margin-top: 10px;"
- :placeholder="t('document.document.form.restrictionLevelPlaceholder')" />
- </el-form-item>
- <el-form-item :label="t('document.document.form.note')" prop="note">
- <el-input v-model="form.note" type="textarea" :rows="4"
- :placeholder="t('document.document.form.notePlaceholder')" />
- </el-form-item>
- </el-form>
- <template #footer>
- <div class="dialog-footer">
- <el-button :loading="buttonLoading" type="primary" @click="submitForm">{{ t('document.document.button.submit')
- }}</el-button>
- <el-button @click="cancel">{{ t('document.document.button.cancel') }}</el-button>
- </div>
- </template>
- </el-dialog>
- <!-- 添加文档对话框 -->
- <el-dialog v-model="documentDialog.visible" :title="documentDialog.title" width="700px" append-to-body>
- <el-form ref="documentFormRef" :model="documentForm" :rules="documentRules" label-width="140px">
- <el-form-item
- :label="documentForm.type === 1 ? t('document.document.documentForm.planName') : t('document.document.documentForm.name')"
- prop="name">
- <el-input v-model="documentForm.name" :placeholder="t('document.document.documentForm.namePlaceholder')"
- clearable />
- </el-form-item>
- <el-form-item :label="t('document.document.documentForm.type')" prop="type" v-if="hasAddPlanPermission">
- <el-radio-group v-model="documentForm.type" @change="handleDocumentTypeChange">
- <el-radio :label="0">{{ t('document.document.documentForm.normalDocument') }}</el-radio>
- <el-radio :label="1">{{ t('document.document.documentForm.planDocument') }}</el-radio>
- </el-radio-group>
- </el-form-item>
- <el-form-item :label="t('document.document.documentForm.submitter')" prop="submitterId">
- <template v-if="documentForm.type === 0">
- <el-input v-model="currentUserName" disabled />
- </template>
- <template v-else>
- <el-select v-model="documentForm.submitterId" filterable remote reserve-keyword
- :placeholder="t('document.document.documentForm.submitterPlaceholder')" :remote-method="searchSubmitters"
- :loading="submitterSearchLoading" style="width: 100%">
- <el-option v-for="submitter in submitterOptions" :key="submitter.id"
- :label="`${submitter.name} / ${submitter.dept} --- ${submitter.phoneNumber}`" :value="submitter.id" />
- </el-select>
- </template>
- </el-form-item>
- <el-form-item v-if="documentForm.type === 1" :label="t('document.document.documentForm.submitDeadline')"
- prop="submitDeadline">
- <el-date-picker v-model="documentForm.submitDeadline" type="date" value-format="YYYY-MM-DD"
- :placeholder="t('document.document.documentForm.submitDeadlinePlaceholder')" style="width: 100%" />
- </el-form-item>
- <el-form-item v-if="documentForm.type === 1" :label="t('document.document.documentForm.planType')"
- prop="planType">
- <el-select v-model="documentForm.planType"
- :placeholder="t('document.document.documentForm.planTypePlaceholder')" clearable style="width: 100%">
- <el-option v-for="dict in plan_document_type" :key="dict.value" :label="dict.label" :value="dict.value" />
- </el-select>
- </el-form-item>
- <el-form-item :label="t('document.document.documentForm.file')" :prop="documentForm.type === 0 ? 'ossId' : ''">
- <fileUpload v-model="uploadedFileId" :limit="1" />
- </el-form-item>
- <el-form-item v-if="documentForm.submitTime" :label="t('document.document.documentForm.submitTime')">
- <el-input v-model="documentForm.submitTime" disabled />
- </el-form-item>
- <el-form-item :label="t('document.document.documentForm.note')" prop="note">
- <el-input v-model="documentForm.note" type="textarea" :rows="4"
- :placeholder="t('document.document.documentForm.notePlaceholder')" />
- </el-form-item>
- </el-form>
- <template #footer>
- <div class="dialog-footer">
- <el-button :loading="documentButtonLoading" type="primary" @click="submitDocumentForm">{{
- t('document.document.button.submit') }}</el-button>
- <el-button @click="cancelDocument">{{ t('document.document.button.cancel') }}</el-button>
- </div>
- </template>
- </el-dialog>
- <!-- 标识文档对话框 -->
- <el-dialog v-model="markDialog.visible" :title="markDialog.title" width="500px" append-to-body>
- <el-form ref="markFormRef" :model="markForm" :rules="markRules" label-width="120px">
- <el-form-item :label="t('document.document.markForm.specification')" prop="type">
- <el-select v-model="markForm.type" :placeholder="t('document.document.markForm.specificationPlaceholder')"
- clearable style="width: 100%">
- <el-option v-for="dict in specificationDict" :key="dict.value" :label="dict.label" :value="dict.value" />
- </el-select>
- </el-form-item>
- </el-form>
- <template #footer>
- <div class="dialog-footer">
- <el-button :loading="markButtonLoading" type="primary" @click="submitMarkForm">{{
- t('document.document.button.submit') }}</el-button>
- <el-button @click="cancelMark">{{ t('document.document.button.cancel') }}</el-button>
- </div>
- </template>
- </el-dialog>
- <!-- 审核文档对话框 -->
- <el-dialog v-model="auditDialog.visible" :title="auditDialog.title" width="500px" append-to-body>
- <el-form ref="auditFormRef" :model="auditForm" :rules="auditRules" label-width="120px">
- <el-form-item :label="t('document.document.auditForm.result')" prop="result">
- <el-radio-group v-model="auditForm.result">
- <el-radio label="0">{{ t('document.document.auditForm.pass') }}</el-radio>
- <el-radio label="1">{{ t('document.document.auditForm.reject') }}</el-radio>
- </el-radio-group>
- </el-form-item>
- <el-form-item v-if="auditForm.result === '1'" :label="t('document.document.auditForm.reason')" prop="reason">
- <el-input v-model="auditForm.reason" type="textarea" :rows="4"
- placeholder="{{ t('document.document.auditForm.reasonPlaceholder') }}" />
- </el-form-item>
- </el-form>
- <template #footer>
- <div class="dialog-footer">
- <el-button :loading="auditButtonLoading" type="primary" @click="submitAuditForm">{{
- t('document.document.button.submit') }}</el-button>
- <el-button @click="cancelAudit">{{ t('document.document.button.cancel') }}</el-button>
- </div>
- </template>
- </el-dialog>
- </div>
- </template>
- <script setup lang="ts">
- import { ref, reactive, onMounted, onUnmounted, nextTick, getCurrentInstance, watch, computed } from 'vue';
- import { useI18n } from 'vue-i18n';
- import { listFolder, addFolder, delFolder, getFolder, updateFolder } from '@/api/document/folder';
- import { FolderListVO, FolderForm } from '@/api/document/folder/types';
- import { addDocument, listDocument, markDocument } from '@/api/document/document';
- import { DocumentForm, DocumentQuery, DocumentVO, DocumentMarkForm } from '@/api/document/document/types';
- import { queryMemberNotInCenter } from '@/api/project/management';
- import { MemberNotInCenterVO, MemberNotInCenterQuery } from '@/api/project/management/types';
- import { Folder, Document, Edit, Delete, Plus, MoreFilled, Location, OfficeBuilding, ArrowRight, Download, Select, Grid, Monitor, Reading, Flag } from '@element-plus/icons-vue';
- import { ElMessage, ElMessageBox } from 'element-plus';
- import type { FormInstance } from 'element-plus';
- import type { ComponentInternalInstance } from 'vue';
- import { useUserStore } from '@/store/modules/user';
- import { checkPermi } from '@/utils/permission';
- import fileUpload from '@/components/FileUpload/index.vue';
- interface Props {
- projectId?: number | string;
- }
- const props = defineProps<Props>();
- const emit = defineEmits<{
- back: [];
- }>();
- const { proxy } = getCurrentInstance() as ComponentInternalInstance;
- const { t } = useI18n();
- const userStore = useUserStore();
- const { plan_document_type, center_file_specification, project_file_specification } = toRefs<any>(proxy?.useDict('plan_document_type', 'center_file_specification', 'project_file_specification'));
- // 数据定义
- const loading = ref(false);
- const buttonLoading = ref(false);
- const treeData = ref<FolderListVO[]>([]);
- const folderFormRef = ref<FormInstance>();
- const documentFormRef = ref<FormInstance>();
- const documentButtonLoading = ref(false);
- // 检查是否有计划文档添加权限
- const hasAddPlanPermission = computed(() => checkPermi(['document:document:addPlan']));
- // 当前用户信息
- const currentUserName = ref(userStore.nickname || '');
- // 对话框
- const dialog = reactive({
- visible: false,
- title: '',
- isEdit: false
- });
- // 文档对话框
- const documentDialog = reactive({
- visible: false,
- title: ''
- });
- // 标识文档对话框
- const markDialog = reactive({
- visible: false,
- title: ''
- });
- // 标识表单ref
- const markFormRef = ref<FormInstance>();
- const markButtonLoading = ref(false);
- // 审核文档对话框
- const auditDialog = reactive({
- visible: false,
- title: ''
- });
- // 审核表单ref
- const auditFormRef = ref<FormInstance>();
- const auditButtonLoading = ref(false);
- // 当前操作的节点
- const currentNode = ref<FolderListVO | null>(null);
- // 限制层级相关状态
- const isRestricted = ref(false); // 是否限制
- const restrictionLevelValue = ref(0); // 限制层级值
- // 表单初始数据
- const initFormData: FolderForm = {
- id: undefined,
- projectId: undefined,
- parentId: undefined,
- type: 0,
- name: '',
- status: 0,
- note: '',
- restrictionLevel: -1
- };
- // 表单数据
- const form = ref<FolderForm>({ ...initFormData });
- // 文档表单初始数据
- const initDocumentFormData: DocumentForm = {
- id: undefined,
- name: '',
- type: 0,
- submitterId: undefined,
- folderId: undefined,
- submitDeadline: undefined,
- planType: undefined,
- ossId: undefined,
- submitTime: undefined,
- note: ''
- };
- // 文件上传的ossId(字符串格式)
- const uploadedFileId = ref<string>('');
- // 文档表单数据
- const documentForm = ref<DocumentForm>({ ...initDocumentFormData });
- // 标识表单数据
- const markForm = ref<DocumentMarkForm>({
- id: 0,
- type: ''
- });
- // 审核表单数据
- interface AuditForm {
- id: number;
- result: string; // 0: 通过, 1: 驳回
- reason: string; // 驳回理由
- }
- const auditForm = ref<AuditForm>({
- id: 0,
- result: '0', // 默认通过
- reason: ''
- });
- // 审核表单验证规则
- const auditRules = reactive({
- result: [
- {
- required: true,
- message: t('document.document.auditRule.resultRequired'),
- trigger: 'change'
- }
- ],
- reason: [
- {
- required: true,
- message: t('document.document.auditRule.reasonRequired'),
- trigger: 'blur'
- }
- ]
- });
- // 当前选中的文档
- const currentDocument = ref<DocumentVO | null>(null);
- // 递交人搜索相关
- const submitterSearchLoading = ref(false);
- const submitterOptions = ref<MemberNotInCenterVO[]>([]);
- let submitterSearchTimer: NodeJS.Timeout | null = null;
- // ========== 文档列表相关 ==========
- // 文档列表数据
- const documentList = ref<DocumentVO[]>([]);
- const documentLoading = ref(false);
- const documentTotal = ref(0);
- // 当前选中的文件夹
- const selectedFolder = ref<FolderListVO | null>(null);
- // 文档查询参数
- const documentQueryParams = reactive<DocumentQuery>({
- pageNum: 1,
- pageSize: 10,
- name: '',
- folderId: undefined
- });
- // 表单验证规则
- const rules = {
- name: [
- { required: true, message: t('document.document.rule.nameRequired'), trigger: 'blur' }
- ],
- type: [
- { required: true, message: t('document.document.rule.typeRequired'), trigger: 'change' }
- ]
- };
- // 文档表单验证规则
- const documentRules = {
- name: [
- { required: true, message: t('document.document.documentRule.nameRequired'), trigger: 'blur' }
- ],
- submitterId: [
- { required: true, message: t('document.document.documentRule.submitterRequired'), trigger: 'change' }
- ],
- ossId: [
- { required: true, message: t('document.document.documentRule.fileRequired'), trigger: 'change' }
- ]
- };
- // 标识表单验证规则
- const markRules = {
- type: [
- { required: true, message: t('document.document.markRule.typeRequired'), trigger: 'change' }
- ]
- };
- // 树形组件配置
- const treeProps = {
- children: 'children',
- label: 'name'
- };
- // 根据当前文件夹获取对应的字典
- const specificationDict = computed(() => {
- if (!selectedFolder.value) return [];
- // 判断是否在中心底下
- const isUnderCenter = checkIfUnderCenter(selectedFolder.value.id);
- if (isUnderCenter) {
- return center_file_specification?.value || [];
- } else {
- return project_file_specification?.value || [];
- }
- });
- // 检查文件夹是否在中心底下
- const checkIfUnderCenter = (folderId: string | number): boolean => {
- // 从当前文件夹往上遍历,如果中间存在着中心类型的文件夹,那么他就是中心层级文件
- // 递归查找目标节点并收集从根到目标的路径
- const findPathToNode = (tree: FolderListVO[], targetId: string | number, path: FolderListVO[] = []): FolderListVO[] | null => {
- for (const node of tree) {
- const currentPath = [...path, node];
- if (node.id === targetId) {
- // 找到目标节点,返回路径
- return currentPath;
- }
- // 在子节点中递归查找
- if (node.children && node.children.length > 0) {
- const result = findPathToNode(node.children, targetId, currentPath);
- if (result) {
- return result;
- }
- }
- }
- return null;
- };
- // 获取从根到目标文件夹的路径
- const path = findPathToNode(treeData.value, folderId);
- if (!path) {
- return false;
- }
- // 检查路径中是否存在type为1或2的节点(国家或中心)
- return path.some(node => node.type === 1 || node.type === 2);
- };
- // 获取文件夹列表
- const getList = async () => {
- if (!props.projectId) {
- ElMessage.warning(t('document.document.message.projectIdNotExist'));
- return;
- }
- loading.value = true;
- try {
- const res = await listFolder({ projectId: props.projectId } as any);
- treeData.value = res.data || [];
- } catch (error) {
- ElMessage.error(t('document.document.message.getFolderListFailed'));
- console.error(error);
- } finally {
- loading.value = false;
- }
- };
- // 返回项目列表
- const handleBack = () => {
- emit('back');
- };
- // 表单重置
- const reset = () => {
- form.value = { ...initFormData };
- isRestricted.value = false;
- restrictionLevelValue.value = 0;
- folderFormRef.value?.resetFields();
- };
- // 处理限制状态变化
- const handleRestrictionChange = (value: boolean) => {
- if (value) {
- // 选择限制,使用restrictionLevelValue的值
- form.value.restrictionLevel = restrictionLevelValue.value;
- } else {
- // 选择不限制,设置为-1
- form.value.restrictionLevel = -1;
- }
- };
- // 监听restrictionLevelValue变化,同步更新form.restrictionLevel
- watch(restrictionLevelValue, (newValue) => {
- if (isRestricted.value) {
- form.value.restrictionLevel = newValue;
- }
- });
- // 取消按钮
- const cancel = () => {
- reset();
- dialog.visible = false;
- };
- // 新建文件夹(顶级)
- const handleAddFolder = () => {
- reset();
- currentNode.value = null;
- form.value.projectId = props.projectId;
- form.value.parentId = undefined;
- dialog.visible = true;
- dialog.title = t('document.document.dialog.addFolder');
- dialog.isEdit = false;
- };
- // 新增子节点
- const handleAddChild = (data: FolderListVO) => {
- reset();
- currentNode.value = data;
- form.value.projectId = props.projectId;
- form.value.parentId = data.id;
- dialog.visible = true;
- dialog.title = t('document.document.dialog.addChild');
- dialog.isEdit = false;
- };
- // 获取可选类型(根据父节点类型限制)
- const getAvailableTypes = () => {
- if (!currentNode.value) {
- // 顶级节点,可以选择所有类型
- return [
- { label: t('document.document.type.folder'), value: 0 },
- { label: t('document.document.type.country'), value: 1 },
- { label: t('document.document.type.center'), value: 2 }
- ];
- }
- const parentType = currentNode.value.type;
- if (parentType === 1 || parentType === 2) {
- // 父节点是国家或中心,子节点只能是中心或文件夹
- return [
- { label: t('document.document.type.folder'), value: 0 },
- { label: t('document.document.type.center'), value: 2 }
- ];
- } else {
- // 父节点是文件夹,子节点只能是文件夹
- return [
- { label: t('document.document.type.folder'), value: 0 }
- ];
- }
- };
- // 下拉菜单命令处理
- const handleCommand = (command: string, data: FolderListVO) => {
- if (command.startsWith('add:')) {
- const cmdPart = command.split(':')[1];
- if (cmdPart === 'document') {
- handleAddDocument(data);
- } else {
- const type = parseInt(cmdPart);
- handleAddChildWithType(data, type);
- }
- } else if (command === 'edit') {
- handleEdit(data);
- } else if (command === 'delete') {
- handleDelete(data);
- }
- };
- // 新增子节点(指定类型)
- const handleAddChildWithType = (data: FolderListVO, type: number) => {
- reset();
- currentNode.value = data;
- form.value.projectId = props.projectId;
- form.value.parentId = data.id;
- form.value.type = type;
- dialog.visible = true;
- const typeLabel = type === 0 ? t('document.document.type.folder') : type === 1 ? t('document.document.type.country') : t('document.document.type.center');
- dialog.title = type === 0 ? t('document.document.dialog.addFolder') : type === 1 ? t('document.document.dialog.addCountry') : t('document.document.dialog.addCenter');
- dialog.isEdit = false;
- };
- // 提交表单
- const submitForm = () => {
- folderFormRef.value?.validate(async (valid: boolean) => {
- if (valid) {
- // 如果是编辑,显示确认对话框
- if (dialog.isEdit) {
- try {
- const confirmMessage = `
- <div style="text-align: left;">
- <p><strong>${t('document.document.confirm.nameLabel')}</strong>${form.value.name}</p>
- <p><strong>${t('document.document.confirm.restrictionLevelLabel')}</strong>${form.value.restrictionLevel}</p>
- <p><strong>${t('document.document.confirm.noteLabel')}</strong>${form.value.note || t('document.document.confirm.noNote')}</p>
- </div>
- `;
- await ElMessageBox.confirm(confirmMessage, t('document.document.dialog.confirmEdit'), {
- confirmButtonText: t('document.document.message.confirmButton'),
- cancelButtonText: t('document.document.message.cancelButton'),
- type: 'warning',
- dangerouslyUseHTMLString: true
- });
- } catch {
- return; // 用户取消
- }
- }
- buttonLoading.value = true;
- try {
- if (dialog.isEdit) {
- await updateFolder(form.value);
- proxy?.$modal.msgSuccess(t('document.document.message.editSuccess'));
- } else {
- await addFolder(form.value);
- proxy?.$modal.msgSuccess(t('document.document.message.addSuccess'));
- }
- dialog.visible = false;
- await getList();
- } catch (error) {
- console.error(dialog.isEdit ? t('document.document.message.editFailed') : t('document.document.message.addFailed'), error);
- } finally {
- buttonLoading.value = false;
- }
- }
- });
- };
- // 编辑
- const handleEdit = async (data: FolderListVO) => {
- reset();
- loading.value = true;
- try {
- const res = await getFolder(data.id);
- Object.assign(form.value, res.data);
- // 设置限制层级状态
- if (form.value.restrictionLevel === -1) {
- isRestricted.value = false;
- restrictionLevelValue.value = 0;
- } else {
- isRestricted.value = true;
- restrictionLevelValue.value = form.value.restrictionLevel;
- }
- currentNode.value = null; // 编辑时不限制类型
- dialog.visible = true;
- dialog.title = t('document.document.dialog.editFolder');
- dialog.isEdit = true;
- } catch (error) {
- ElMessage.error(t('document.document.message.getFolderInfoFailed'));
- console.error(error);
- } finally {
- loading.value = false;
- }
- };
- // 删除
- const handleDelete = async (data: FolderListVO) => {
- // 检查是否有子节点
- if (data.children && data.children.length > 0) {
- ElMessage.warning(t('document.document.message.hasChildren'));
- return;
- }
- try {
- await ElMessageBox.confirm(t('document.document.message.deleteConfirm', { name: data.name }), t('document.document.message.deleteTitle'), {
- confirmButtonText: t('document.document.message.confirmButton'),
- cancelButtonText: t('document.document.message.cancelButton'),
- type: 'warning'
- });
- loading.value = true;
- await delFolder(data.id);
- ElMessage.success(t('document.document.message.deleteSuccess'));
- await getList();
- } catch (error: any) {
- // 用户取消删除或删除失败
- if (error !== 'cancel') {
- console.error(t('document.document.message.deleteFailed'), error);
- }
- } finally {
- loading.value = false;
- }
- };
- // 菜单状态管理
- const activeMenu = ref<string | number | null>(null); // 当前激活的一级菜单
- const showSecondaryMenu = ref(false); // 是否显示二级菜单
- const primaryMenuStyle = ref<any>({}); // 一级菜单的样式(位置)
- const secondaryMenuStyle = ref<any>({}); // 二级菜单的样式(位置)
- const currentMenuData = ref<FolderListVO | null>(null); // 当前操作的菜单数据
- // 切换菜单显示
- const toggleMenu = (event: MouseEvent, data: FolderListVO) => {
- // 只处理点击事件,忽略其他事件
- if (event.type !== 'click') {
- return;
- }
- event.stopPropagation();
- event.preventDefault();
- const trigger = event.currentTarget as HTMLElement;
- const rect = trigger.getBoundingClientRect();
- if (activeMenu.value === data.id) {
- // 如果点击的是同一个菜单,关闭它
- closeAllMenus();
- } else {
- // 关闭之前的菜单,打开新菜单
- // 计算一级菜单位置
- primaryMenuStyle.value = {
- left: `${rect.left}px`,
- top: `${rect.bottom + 2}px`
- };
- activeMenu.value = data.id;
- currentMenuData.value = data;
- showSecondaryMenu.value = false;
- }
- };
- // 切换二级菜单显示
- const toggleSubmenu = (event: MouseEvent) => {
- // 只处理点击事件,忽略其他事件
- if (event.type !== 'click') {
- return;
- }
- event.stopPropagation();
- event.preventDefault();
- const target = event.currentTarget as HTMLElement;
- const rect = target.getBoundingClientRect();
- if (showSecondaryMenu.value) {
- // 如果已经显示,则关闭
- showSecondaryMenu.value = false;
- } else {
- // 先设置位置,再显示(避免位置计算前就显示)
- secondaryMenuStyle.value = {
- left: `${rect.right + 5}px`,
- top: `${rect.top}px`
- };
- // 使用 nextTick 确保位置设置后再显示
- nextTick(() => {
- showSecondaryMenu.value = true;
- });
- }
- };
- // 处理菜单项点击
- const handleMenuItemClick = (command: string, data: FolderListVO | null) => {
- if (!data) return;
- // 执行命令
- handleCommand(command, data);
- // 关闭所有菜单
- closeAllMenus();
- };
- // ========== 文档相关函数 ==========
- // 重置文档表单
- const resetDocumentForm = () => {
- documentForm.value = { ...initDocumentFormData };
- uploadedFileId.value = '';
- // 如果没有计划文档权限,默认为非计划文档
- if (!hasAddPlanPermission.value) {
- documentForm.value.type = 0;
- }
- documentForm.value.submitterId = userStore.userId;
- currentUserName.value = userStore.nickname || '';
- submitterOptions.value = [];
- documentFormRef.value?.resetFields();
- };
- // 添加文档
- const handleAddDocument = (data: FolderListVO) => {
- resetDocumentForm();
- documentForm.value.folderId = data.id;
- documentDialog.visible = true;
- documentDialog.title = t('document.document.dialog.addDocument');
- };
- // 处理文档类型变化
- const handleDocumentTypeChange = (value: number) => {
- if (value === 0) {
- // 非计划文档,递交人为当前用户
- documentForm.value.submitterId = userStore.userId;
- documentForm.value.submitDeadline = undefined;
- documentForm.value.planType = undefined;
- } else {
- // 计划文档,清空递交人
- documentForm.value.submitterId = undefined;
- }
- };
- // 搜索递交人
- const searchSubmitters = async (query: string) => {
- if (!query || query.trim() === '') {
- submitterOptions.value = [];
- return;
- }
- // 清除之前的定时器
- if (submitterSearchTimer) {
- clearTimeout(submitterSearchTimer);
- }
- // 设置防抖
- submitterSearchTimer = setTimeout(async () => {
- submitterSearchLoading.value = true;
- try {
- const queryParams: MemberNotInCenterQuery = {
- pageNum: 1,
- pageSize: 10,
- projectId: props.projectId || 0,
- folderId: 0,
- name: query
- };
- const res = await queryMemberNotInCenter(queryParams);
- submitterOptions.value = res.rows || [];
- } catch (error) {
- console.error('Failed to search submitters:', error);
- ElMessage.error(t('document.document.message.searchSubmitterFailed'));
- } finally {
- submitterSearchLoading.value = false;
- }
- }, 300);
- };
- // 监听文件上传变化
- watch(uploadedFileId, (newVal) => {
- if (newVal) {
- // 解析文件ID(可能是逗号分隔的字符串)
- const ids = newVal.split(',').filter(id => id.trim());
- if (ids.length > 0) {
- documentForm.value.ossId = parseInt(ids[0]);
- // 自动设置递交时间为当前时间
- const now = new Date();
- documentForm.value.submitTime = proxy?.parseTime(now, '{y}-{m}-{d} {h}:{i}:{s}');
- }
- } else {
- documentForm.value.ossId = undefined;
- documentForm.value.submitTime = undefined;
- }
- });
- // 取消文档对话框
- const cancelDocument = () => {
- resetDocumentForm();
- documentDialog.visible = false;
- };
- // 提交文档表单
- const submitDocumentForm = () => {
- documentFormRef.value?.validate(async (valid: boolean) => {
- if (valid) {
- documentButtonLoading.value = true;
- try {
- // 构建完整的请求数据,确保所有字段都存在(参考文件夹的实现)
- const submitData: DocumentForm = {
- id: documentForm.value.id || 0,
- name: documentForm.value.name || '',
- type: documentForm.value.type !== undefined ? documentForm.value.type : 0,
- submitterId: documentForm.value.submitterId || 0,
- folderId: documentForm.value.folderId || 0,
- submitDeadline: documentForm.value.submitDeadline || '',
- planType: documentForm.value.planType || '',
- ossId: documentForm.value.ossId || null,
- submitTime: documentForm.value.submitTime || '',
- note: documentForm.value.note || ''
- };
- await addDocument(submitData);
- proxy?.$modal.msgSuccess(t('document.document.message.addDocumentSuccess'));
- documentDialog.visible = false;
- await getList();
- // 刷新文档列表
- await getDocumentList();
- } catch (error) {
- console.error(t('document.document.message.addDocumentFailed'), error);
- } finally {
- documentButtonLoading.value = false;
- }
- }
- });
- };
- // ========== 文档列表相关函数 ==========
- // 点击文件夹节点
- const handleFolderClick = (data: FolderListVO) => {
- // 只有文件夹类型(type=0)才显示文档列表
- if (data.type === 0) {
- selectedFolder.value = data;
- // 重置查询参数
- documentQueryParams.name = '';
- documentQueryParams.pageNum = 1;
- // 加载文档列表
- getDocumentList();
- } else {
- selectedFolder.value = null;
- documentList.value = [];
- }
- };
- // 查询文档列表
- const getDocumentList = async () => {
- if (!selectedFolder.value) return;
- documentLoading.value = true;
- documentQueryParams.folderId = selectedFolder.value.id;
- try {
- const res = await listDocument(documentQueryParams);
- documentList.value = res.rows || [];
- documentTotal.value = res.total || 0;
- } catch (error) {
- console.error('Failed to get document list:', error);
- ElMessage.error(t('document.document.message.getDocumentListFailed'));
- } finally {
- documentLoading.value = false;
- }
- };
- // 搜索按钮
- const handleDocumentQuery = () => {
- documentQueryParams.pageNum = 1;
- getDocumentList();
- };
- // 重置按钮
- const handleDocumentReset = () => {
- documentQueryParams.name = '';
- documentQueryParams.pageNum = 1;
- getDocumentList();
- };
- // 审核文档
- const handleAudit = (row: DocumentVO) => {
- currentDocument.value = row;
- auditForm.value = {
- id: row.id,
- result: '0', // 默认通过
- reason: ''
- };
- auditDialog.visible = true;
- auditDialog.title = t('document.document.dialog.auditDocument');
- // 重置表单验证
- nextTick(() => {
- auditFormRef.value?.clearValidate();
- });
- };
- // 取消审核
- const cancelAudit = () => {
- auditDialog.visible = false;
- auditForm.value = {
- id: 0,
- result: '0',
- reason: ''
- };
- currentDocument.value = null;
- };
- // 提交审核表单
- const submitAuditForm = () => {
- auditFormRef.value?.validate(async (valid: boolean) => {
- if (valid) {
- auditButtonLoading.value = true;
- try {
- // 暂时不与后端进行交互,仅关闭弹窗
- proxy?.$modal.msgSuccess(t('document.document.message.auditSuccess'));
- auditDialog.visible = false;
- // 刷新文档列表
- await getDocumentList();
- } catch (error) {
- console.error(t('document.document.message.auditFailed'), error);
- } finally {
- auditButtonLoading.value = false;
- }
- }
- });
- };
- // 下载文档
- const handleDownload = (row: DocumentVO) => {
- if (!row.url) {
- ElMessage.warning(t('document.document.message.noFileToDownload'));
- return;
- }
- // 新建a标签下载文件
- const a = document.createElement('a');
- a.href = row.url;
- a.download = row.fileName || row.name || 'document';
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- };
- // 标识文档
- const handleMark = (row: DocumentVO) => {
- currentDocument.value = row;
- markForm.value = {
- id: row.id,
- type: ''
- };
- markDialog.visible = true;
- markDialog.title = t('document.document.dialog.markDocument');
- // 重置表单验证
- nextTick(() => {
- markFormRef.value?.clearValidate();
- });
- };
- // 取消标识
- const cancelMark = () => {
- markDialog.visible = false;
- markForm.value = {
- id: 0,
- type: ''
- };
- currentDocument.value = null;
- };
- // 提交标识表单
- const submitMarkForm = () => {
- markFormRef.value?.validate(async (valid: boolean) => {
- if (valid) {
- markButtonLoading.value = true;
- try {
- await markDocument(markForm.value);
- proxy?.$modal.msgSuccess(t('document.document.message.markSuccess'));
- markDialog.visible = false;
- // 刷新文档列表
- await getDocumentList();
- } catch (error) {
- console.error(t('document.document.message.markFailed'), error);
- } finally {
- markButtonLoading.value = false;
- }
- }
- });
- };
- // ========== 文件类型判断函数 ==========
- // 判断是否为Word文档
- const isWordFile = (fileName: string): boolean => {
- if (!fileName) return false;
- const lowerFileName = fileName.toLowerCase();
- return lowerFileName.endsWith('.doc') || lowerFileName.endsWith('.docx');
- };
- // 判断是否为Excel文档
- const isExcelFile = (fileName: string): boolean => {
- if (!fileName) return false;
- const lowerFileName = fileName.toLowerCase();
- return lowerFileName.endsWith('.xls') ||
- lowerFileName.endsWith('.xlsx') ||
- lowerFileName.endsWith('.csv');
- };
- // 判断是否为PPT文档
- const isPPTFile = (fileName: string): boolean => {
- if (!fileName) return false;
- const lowerFileName = fileName.toLowerCase();
- return lowerFileName.endsWith('.ppt') || lowerFileName.endsWith('.pptx');
- };
- // 判断是否为PDF文档
- const isPDFFile = (fileName: string): boolean => {
- if (!fileName) return false;
- return fileName.toLowerCase().endsWith('.pdf');
- };
- // 关闭所有菜单
- const closeAllMenus = () => {
- showSecondaryMenu.value = false;
- activeMenu.value = null;
- currentMenuData.value = null;
- primaryMenuStyle.value = {};
- secondaryMenuStyle.value = {};
- };
- // 点击页面其他地方关闭菜单
- const handleClickOutside = (event: Event) => {
- // 如果没有激活的菜单,直接返回
- if (!activeMenu.value && !showSecondaryMenu.value) {
- return;
- }
- const target = event.target as HTMLElement;
- // 检查点击是否在菜单内部或触发器上
- const isClickInsideMenu = target.closest('.primary-menu') ||
- target.closest('.secondary-menu') ||
- target.closest('.menu-trigger');
- // 如果点击在菜单外部,立即关闭所有菜单
- if (!isClickInsideMenu) {
- closeAllMenus();
- }
- };
- // 处理滚动事件,滚动时关闭菜单
- const handleScroll = () => {
- if (activeMenu.value || showSecondaryMenu.value) {
- closeAllMenus();
- }
- };
- // 初始化
- onMounted(() => {
- getList();
- // 添加全局点击监听(捕获阶段)
- document.addEventListener('click', handleClickOutside, true);
- // 添加滚动监听
- document.addEventListener('scroll', handleScroll, true);
- });
- // 清理
- onUnmounted(() => {
- // 移除全局点击监听
- document.removeEventListener('click', handleClickOutside, true);
- // 移除滚动监听
- document.removeEventListener('scroll', handleScroll, true);
- // 清理菜单状态
- closeAllMenus();
- // 清理搜索定时器
- if (submitterSearchTimer) {
- clearTimeout(submitterSearchTimer);
- submitterSearchTimer = null;
- }
- });
- </script>
- <style scoped lang="scss">
- .flex {
- display: flex;
- }
- .justify-between {
- justify-content: space-between;
- }
- .items-center {
- align-items: center;
- }
- .text-lg {
- font-size: 1.125rem;
- }
- .font-bold {
- font-weight: 700;
- }
- .content-wrapper {
- display: flex;
- height: calc(100vh - 250px);
- min-height: 500px;
- }
- .tree-container {
- width: 300px;
- border-right: 1px solid #e4e7ed;
- display: flex;
- flex-direction: column;
- }
- .tree-header {
- padding: 10px;
- border-bottom: 1px solid #e4e7ed;
- }
- .tree-scrollbar {
- flex: 1;
- overflow: hidden;
- :deep(.el-scrollbar__view) {
- padding: 10px;
- }
- }
- .custom-tree-node {
- flex: 1;
- display: flex;
- align-items: center;
- font-size: 14px;
- padding-right: 8px;
- .el-icon {
- margin-right: 8px;
- font-size: 16px;
- }
- .node-label {
- flex: 1;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- cursor: pointer;
- &:hover {
- color: var(--el-color-primary);
- }
- }
- .node-actions {
- display: none;
- position: relative;
- }
- &:hover .node-actions {
- display: inline-flex;
- gap: 4px;
- }
- }
- .menu-trigger {
- cursor: pointer;
- display: flex;
- align-items: center;
- font-size: 16px;
- padding: 4px;
- border-radius: 4px;
- transition: background-color 0.3s, color 0.3s;
- &:hover {
- background-color: #f5f7fa;
- color: var(--el-color-primary);
- }
- }
- .content-container {
- flex: 1;
- padding: 20px;
- overflow: auto;
- }
- .document-list-container {
- width: 100%;
- .search-form {
- margin-bottom: 16px;
- }
- .file-name-cell {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 0 8px;
- .file-icon {
- flex-shrink: 0;
- }
- .file-name-text {
- flex: 1;
- text-align: left;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .download-btn {
- flex-shrink: 0;
- font-size: 16px;
- &:hover {
- transform: scale(1.1);
- }
- }
- }
- }
- .detail-content {
- max-width: 800px;
- }
- /* 一级菜单样式 */
- .primary-menu {
- position: fixed;
- min-width: 120px;
- background: #fff;
- border: 1px solid #e4e7ed;
- border-radius: 4px;
- box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
- padding: 5px 0;
- margin: 0;
- list-style: none;
- z-index: 2000;
- .menu-item {
- padding: 8px 16px;
- cursor: pointer;
- font-size: 14px;
- color: #606266;
- transition: background-color 0.3s, color 0.3s;
- display: flex;
- justify-content: space-between;
- align-items: center;
- white-space: nowrap;
- &:hover {
- background-color: #f5f7fa;
- color: var(--el-color-primary);
- }
- &.has-submenu {
- .arrow-icon {
- margin-left: 8px;
- font-size: 12px;
- }
- }
- }
- }
- /* 二级菜单样式 */
- .secondary-menu {
- position: fixed;
- min-width: 120px;
- background: #fff;
- border: 1px solid #e4e7ed;
- border-radius: 4px;
- box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
- padding: 5px 0;
- margin: 0;
- list-style: none;
- z-index: 3000;
- .menu-item {
- padding: 8px 16px;
- cursor: pointer;
- font-size: 14px;
- color: #606266;
- transition: background-color 0.3s, color 0.3s;
- &:hover {
- background-color: #f5f7fa;
- color: var(--el-color-primary);
- }
- }
- }
- </style>
|