| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290 |
- <template>
- <el-drawer
- v-model="visible"
- direction="rtl"
- size="80%"
- class="platform-detail-drawer"
- :destroy-on-close="true"
- :with-header="false"
- >
- <!-- 自定义头部 -->
- <div class="detail-header">
- <div class="header-left">
- <span class="project-title">{{ drawerForm.projectName }}</span>
- </div>
- <div class="header-right">
- <el-icon class="close-btn" @click="visible = false"><Close /></el-icon>
- </div>
- </div>
- <!-- 详情内容区 -->
- <div class="detail-main-scroll" v-loading="loading">
- <!-- 项目进度 (扁平化设计) -->
- <div class="project-progress-card">
- <div class="section-title">项目进度</div>
- <div class="chevron-steps">
- <div v-for="(step, idx) in stepList" :key="idx" class="chevron-step"
- :class="{
- 'is-finished': (drawerForm.projectStatus || 1) > (idx + 1),
- 'is-active': (drawerForm.projectStatus || 1) === (idx + 1)
- }"
- @click="handleStepClick(idx + 1)">
- <span class="step-text">{{ step }}</span>
- </div>
- </div>
- <div class="progress-footer">
- <span class="sub-label">最新进度</span>
- <span class="all-progress-link">全部进度</span>
- </div>
- </div>
- <div class="detail-content-layout">
- <!-- 左侧核心信息 -->
- <div class="content-left-panel">
- <div class="detail-content-card">
- <el-tabs v-model="drawerActiveTab" class="custom-tabs sticky-tabs">
- <el-tab-pane label="项目信息" name="info">
- <!-- 基本信息 -->
- <div class="info-section">
- <div class="info-section-header">
- <span class="title">基本信息</span>
- <el-button v-if="!isReadOnly" type="primary" size="small" @click="handleEdit">
- <el-icon style="margin-right: 4px;"><Edit /></el-icon>编辑
- </el-button>
- </div>
- <el-descriptions :column="3" class="custom-desc">
- <el-descriptions-item label="项目名称" :span="3">{{ drawerForm.projectName }}</el-descriptions-item>
- <el-descriptions-item label="归属公司">{{ displayCompanyName }}</el-descriptions-item>
- <el-descriptions-item label="客户名称">{{ drawerForm.customName || drawerForm.customerName }}</el-descriptions-item>
- <el-descriptions-item label="行业">{{ displayIndustryName }}</el-descriptions-item>
- <el-descriptions-item label="部门">{{ displayDeptName }}</el-descriptions-item>
- <el-descriptions-item label="项目状态">{{ getStatusLabel(drawerForm.projectStatus) }}</el-descriptions-item>
- <el-descriptions-item label="项目级别">{{ displayProjectLevelName }}</el-descriptions-item>
- <el-descriptions-item label="项目类型">{{ displayProjectTypeName }}</el-descriptions-item>
- <el-descriptions-item label="业务负责人">{{ drawerForm.leaderName || findUserName(drawerForm.leader) }}</el-descriptions-item>
- <el-descriptions-item label="产品支持">{{ drawerForm.productSupportName || findUserName(drawerForm.productSupport) }}</el-descriptions-item>
- <el-descriptions-item label="平台名称" :span="2">{{ drawerForm.platformName }}</el-descriptions-item>
- <el-descriptions-item label="平台链接" :span="3">{{ drawerForm.platformLink }}</el-descriptions-item>
- </el-descriptions>
- </div>
- <!-- 项目情况 -->
- <div class="info-section mt-24">
- <div class="info-section-header">
- <span class="title">项目情况</span>
- </div>
- <el-descriptions :column="3" class="custom-desc">
- <el-descriptions-item label="登记日期">{{ formatDate(drawerForm.createTime) }}</el-descriptions-item>
- <el-descriptions-item label="金额(万)">{{ drawerForm.amount ? Number(drawerForm.amount).toFixed(2) : '' }}</el-descriptions-item>
- <el-descriptions-item label="报名费">{{ drawerForm.entryFee ? Number(drawerForm.entryFee).toFixed(2) : '' }}</el-descriptions-item>
- <el-descriptions-item label="投标保证金">{{ drawerForm.bidBond ? Number(drawerForm.bidBond).toFixed(2) : '' }}</el-descriptions-item>
- <el-descriptions-item label="赢单率(%)">{{ drawerForm.winningRate ? drawerForm.winningRate + '%' : (drawerForm.winRate ? drawerForm.winRate + '%' : '') }}</el-descriptions-item>
- <el-descriptions-item label="报名截止时间">{{ formatDate(drawerForm.signUpDeadline) }}</el-descriptions-item>
- <el-descriptions-item label="投标截止时间">{{ formatDate(drawerForm.tenderDeadline) }}</el-descriptions-item>
- <el-descriptions-item label="标书汇编完成时间">{{ formatDate(drawerForm.docCompileTime) }}</el-descriptions-item>
- <el-descriptions-item label="服务期(年)">{{ drawerForm.standardPeriod }}</el-descriptions-item>
- <el-descriptions-item label="服务时间段" :span="1">{{ drawerForm.serviceTime }}</el-descriptions-item>
- <el-descriptions-item label="入围类型" :span="2">{{ displayShortlistedName }}</el-descriptions-item>
- <el-descriptions-item label="物资类目" :span="3">{{ displayProfessionName }}</el-descriptions-item>
- <el-descriptions-item label="招标代理机构" :span="1">{{ drawerForm.biddingAgency }}</el-descriptions-item>
- <el-descriptions-item label="机构联系方式" :span="2">{{ drawerForm.agencyContact }}</el-descriptions-item>
- <el-descriptions-item label="标期类型">{{ drawerForm.bidPeriodType == 2 ? '周期性框架' : '单项目入围' }}</el-descriptions-item>
- <template v-if="drawerForm.bidPeriodType == 2">
- <el-descriptions-item label="预计下次投标时间">{{ formatDate(drawerForm.nextBiddingTime) }}</el-descriptions-item>
- <el-descriptions-item label="提前提醒天数">{{ drawerForm.noticeAdvanceDays }}</el-descriptions-item>
- </template>
- <template v-else>
- <el-descriptions-item :span="2" label-class-name="hidden-label" class-name="hidden-value"></el-descriptions-item>
- </template>
- <el-descriptions-item label="入围要求" :span="3">{{ drawerForm.shortlistedRequirement }}</el-descriptions-item>
- <el-descriptions-item label="项目描述" :span="3">{{ drawerForm.projectDesc }}</el-descriptions-item>
- </el-descriptions>
- </div>
- </el-tab-pane>
- <el-tab-pane label="项目联系人" name="contact">
- <div class="contact-tab-content">
- <div class="tab-toolbar">
- <el-dropdown v-if="!isReadOnly" @command="handleContactCommand">
- <el-button type="primary">
- <el-icon style="margin-right: 4px;"><Plus /></el-icon>新建联系人<el-icon class="el-icon--right"><arrow-down /></el-icon>
- </el-button>
- <template #dropdown>
- <el-dropdown-menu>
- <el-dropdown-item command="associate">关联客户联系人</el-dropdown-item>
- <el-dropdown-item command="new">新建联系人</el-dropdown-item>
- </el-dropdown-menu>
- </template>
- </el-dropdown>
- </div>
- <el-table :data="contactList" v-loading="contactLoading" class="custom-table" border>
- <el-table-column label="姓名" align="center" width="100">
- <template #default="scope">
- <el-link type="primary" :underline="false" @click="handleEditContact(scope.row)">
- {{ scope.row.contactName || scope.row.name }}
- </el-link>
- </template>
- </el-table-column>
- <el-table-column label="性别" align="center" width="80">
- <template #default="scope">
- {{ (scope.row.gender == '0' || scope.row.sex == '0') ? '男' : (scope.row.gender == '1' || scope.row.sex == '1' ? '女' : '') }}
- </template>
- </el-table-column>
- <el-table-column label="部门" prop="deptName" align="center" show-overflow-tooltip />
- <el-table-column label="职位" align="center" prop="position" show-overflow-tooltip />
- <el-table-column label="项目角色" align="center">
- <template #default="scope">
- {{ getProjectRoleLabel(scope.row.projectRole) }}
- </template>
- </el-table-column>
- <el-table-column label="是否关键人" align="center" width="100">
- <template #default="scope">
- <span>{{ (scope.row.isKeyPerson === 1 || scope.row.keyPerson === 1) ? '是' : '否' }}</span>
- </template>
- </el-table-column>
- <el-table-column label="操作" align="center" width="120" fixed="right">
- <template #default="scope">
- <el-button v-if="!isReadOnly" link type="primary" @click="handleEditContact(scope.row)">编辑</el-button>
- <el-button v-if="!isReadOnly" link type="danger" @click="handleDeleteContact(scope.row)">删除</el-button>
- </template>
- </el-table-column>
- </el-table>
- </div>
- </el-tab-pane>
- <el-tab-pane label="结果分析" name="analysis">
- <div class="analysis-tab-content">
- <div class="analysis-form">
- <div class="analysis-row">
- <span class="form-label">成交结果</span>
- <el-radio-group v-model="analysisForm.resultType" class="result-radio">
- <el-radio value="win">赢单</el-radio>
- <el-radio value="lose">丢单</el-radio>
- </el-radio-group>
- <div style="flex:1"></div>
- <el-button v-if="!isReadOnly" type="primary" icon="CircleCheck" @click="handleSaveAnalysis">保 存</el-button>
- </div>
- <template v-if="analysisForm.resultType === 'win'">
- <div class="summary-title">赢单总结</div>
- <div class="summary-textarea-wrap">
- <el-input v-model="analysisForm.summary" type="textarea" :rows="4" placeholder="请输入赢单总结" />
- </div>
- </template>
- <template v-else>
- <div class="summary-title">丢单原因</div>
- <div class="summary-textarea-wrap">
- <el-input v-model="analysisForm.loseReason" type="textarea" :rows="4" placeholder="请输入丢单原因" />
- </div>
- </template>
- <div class="attachment-section-title">附件</div>
- <div v-if="!isReadOnly" class="upload-area">
- <el-upload
- :action="uploadFileUrl"
- :headers="headers"
- :on-success="handleAnalysisUploadSuccess"
- :show-file-list="false"
- multiple
- >
- <el-button type="primary" icon="Upload">上 传</el-button>
- </el-upload>
- </div>
- <div v-if="analysisFileList.length > 0" class="analysis-file-list" style="margin-top:10px;">
- <el-table :data="analysisFileList" border size="small">
- <el-table-column label="文件名称" prop="name" align="center" show-overflow-tooltip />
- <el-table-column label="操作" width="120" align="center">
- <template #default="scope">
- <el-button link type="primary" @click="downloadFile(scope.row)">下载</el-button>
- <el-button v-if="!isReadOnly" link type="danger" @click="handleAnalysisDeleteFile(scope.$index)">删除</el-button>
- </template>
- </el-table-column>
- </el-table>
- </div>
- </div>
- </div>
- </el-tab-pane>
- <el-tab-pane label="附件" name="files">
- <div class="files-tab-content">
- <div v-if="!isReadOnly" class="tab-toolbar">
- <el-upload
- :action="uploadFileUrl"
- :headers="headers"
- :on-success="handleUploadSuccess"
- :show-file-list="false"
- multiple
- >
- <el-button type="primary" icon="Upload">上传附件</el-button>
- </el-upload>
- </div>
- <el-table :data="detailFileList" border class="custom-table">
- <el-table-column label="文件名称" prop="name" show-overflow-tooltip />
- <el-table-column label="上传时间" prop="uploadTime" width="180" align="center">
- <template #default="scope"><span>{{ formatDate(scope.row.uploadTime) }}</span></template>
- </el-table-column>
- <el-table-column label="操作" width="120" align="center">
- <template #default="scope">
- <el-button link type="primary" @click="downloadFile(scope.row)">下载</el-button>
- <el-button v-if="!isReadOnly" link type="danger" @click="handleDeleteFile(scope.row)">删除</el-button>
- </template>
- </el-table-column>
- </el-table>
- </div>
- </el-tab-pane>
- </el-tabs>
- </div>
- </div>
- <!-- 右侧跟进活动 -->
- <div class="content-right-panel">
- <business-activity
- ref="businessActivityRef"
- :business-id="props.id"
- :dataType="3"
- business-type="platformSelection"
- :info-data="{
- ...drawerForm,
- customerName: drawerForm.customName || drawerForm.customerName,
- deptName: displayDeptName,
- department: displayDeptName,
- profession: displayIndustryName,
- industryName: displayIndustryName,
- /* 显式传入负责人姓名,兼容多种字段名,避免回退到系统创建人 */
- leaderName: drawerForm.leaderName || drawerForm.managerName || drawerForm.staffName || ''
- }"
- @update-readonly="val => isReadOnly = val"
- />
- </div>
- </div>
- </div>
- </el-drawer>
- <!-- 项目联系人抽屉 -->
- <el-drawer
- v-model="projectContactDrawerOpen"
- direction="rtl"
- size="80%"
- destroy-on-close
- :with-header="false"
- >
- <div class="drawer-header-compact" style="height: 40px; padding: 0 16px; border-bottom: 1px solid #e2e8f0; display: flex; justify-content: space-between; align-items: center; background: #fff;">
- <span style="font-size: 15px; font-weight: 600; color: #333;">{{ projectContactForm.id ? '编辑项目联系人' : '新建项目联系人' }}</span>
- <el-icon @click="projectContactDrawerOpen = false" style="cursor: pointer; font-size: 18px; color: #94a3b8;"><Close /></el-icon>
- </div>
- <div class="drawer-body-custom" style="padding: 24px; overflow-y: auto; height: calc(100% - 120px);">
- <el-form :model="projectContactForm" :rules="projectContactRules" ref="projectContactFormRef" label-width="100px" label-position="right" class="no-bold-label">
- <!-- 基本信息 -->
- <div class="form-section-group">
- <div class="section-group-title-bar">基本信息</div>
- <el-row :gutter="20">
- <el-col :span="8">
- <el-form-item label="姓名" prop="contactName"><el-input v-model="projectContactForm.contactName" placeholder="请输入" /></el-form-item>
- </el-col>
- <el-col :span="8">
- <el-form-item label="联系人类型" prop="type">
- <el-radio-group v-model="projectContactForm.type">
- <el-radio value="1">公司职员</el-radio>
- <el-radio value="2">关系资源人</el-radio>
- </el-radio-group>
- </el-form-item>
- </el-col>
- <el-col :span="8">
- <el-form-item label="性别" prop="gender">
- <el-radio-group v-model="projectContactForm.gender">
- <el-radio value="0">男</el-radio><el-radio value="1">女</el-radio>
- </el-radio-group>
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="20">
- <el-col :span="8">
- <el-form-item label="年龄" prop="age"><el-input v-model="projectContactForm.age" type="number" placeholder="请输入" /></el-form-item>
- </el-col>
- <el-col :span="8">
- <el-form-item label="籍贯" prop="nativePlace"><el-input v-model="projectContactForm.nativePlace" placeholder="请输入" /></el-form-item>
- </el-col>
- <el-col :span="8">
- <el-form-item label="生日" prop="birthday">
- <el-date-picker v-model="projectContactForm.birthday" type="date" placeholder="请选择" style="width: 100%" value-format="YYYY-MM-DD" />
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="20">
- <el-col :span="24">
- <el-form-item label="描述" prop="remark"><el-input v-model="projectContactForm.remark" type="textarea" :rows="3" placeholder="请输入" /></el-form-item>
- </el-col>
- </el-row>
- </div>
- <!-- 办公信息 -->
- <div class="form-section-group">
- <div class="section-group-title-bar">办公信息</div>
- <el-row :gutter="20">
- <el-col :span="8">
- <el-form-item label="在职状态" prop="jobStatus">
- <el-radio-group v-model="projectContactForm.jobStatus">
- <el-radio value="1">在职</el-radio>
- <el-radio value="2">离职</el-radio>
- </el-radio-group>
- </el-form-item>
- </el-col>
- <el-col :span="8">
- <el-form-item label="手机号码" prop="phone"><el-input v-model="projectContactForm.phone" placeholder="请输入" /></el-form-item>
- </el-col>
- <el-col :span="8">
- <el-form-item label="部门" prop="deptName"><el-input v-model="projectContactForm.deptName" placeholder="请输入" /></el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="20">
- <el-col :span="8">
- <el-form-item label="职位" prop="position"><el-input v-model="projectContactForm.position" placeholder="请输入" /></el-form-item>
- </el-col>
- <el-col :span="8">
- <el-form-item label="办公座机" prop="officePhone"><el-input v-model="projectContactForm.officePhone" placeholder="请输入" /></el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="20">
- <el-col :span="24">
- <el-form-item label="办公地址" prop="addressDetail">
- <div style="display: flex; gap: 10px;">
- <el-cascader
- v-model="projectContactForm.officeRegion"
- :options="areaOptions"
- :props="{ label: 'areaName', value: 'id', children: 'children' }"
- placeholder="请选择省/市/区"
- style="width: 250px"
- clearable
- />
- <el-input v-model="projectContactForm.addressDetail" placeholder="请输入详细地址" style="flex: 1" />
- </div>
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="20">
- <el-col :span="24">
- <el-form-item label="工作内容" prop="jobContent"><el-input v-model="projectContactForm.jobContent" type="textarea" :rows="3" placeholder="请输入" /></el-form-item>
- </el-col>
- </el-row>
- </div>
- <!-- 项目决策 -->
- <div class="form-section-group">
- <div class="section-group-title-bar">项目决策</div>
- <el-row :gutter="20">
- <el-col :span="8">
- <el-form-item label="项目角色" prop="projectRole">
- <el-select v-model="projectContactForm.projectRole" style="width: 100%" placeholder="请选择" filterable clearable>
- <el-option v-for="item in projectRoleOptions" :key="item.value" :label="item.label" :value="parseInt(item.value)" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :span="8">
- <el-form-item label="是否关键人" prop="isKeyPerson">
- <el-select v-model="projectContactForm.isKeyPerson" style="width: 100%" placeholder="请选择">
- <el-option label="是" :value="1" /><el-option label="否" :value="0" />
- </el-select>
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="20">
- <el-col :span="24">
- <el-form-item label="公关情况" prop="prStatus"><el-input v-model="projectContactForm.prStatus" type="textarea" :rows="3" placeholder="请输入" /></el-form-item>
- </el-col>
- </el-row>
- </div>
- <!-- 家庭信息 -->
- <div class="form-section-group">
- <div class="section-group-title-bar">家庭信息</div>
- <el-row :gutter="20">
- <el-col :span="24">
- <el-form-item label="家庭住址" prop="homeAddressDetail">
- <div style="display: flex; gap: 10px;">
- <el-cascader
- v-model="projectContactForm.homeRegion"
- :options="areaOptions"
- :props="{ label: 'areaName', value: 'id', children: 'children' }"
- placeholder="请选择省/市/区"
- style="width: 250px"
- clearable
- />
- <el-input v-model="projectContactForm.homeAddressDetail" placeholder="请输入详细地址" style="flex: 1" />
- </div>
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="20">
- <el-col :span="8">
- <el-form-item label="家庭情况" prop="familyStatus"><el-input v-model="projectContactForm.familyStatus" placeholder="请输入" /></el-form-item>
- </el-col>
- <el-col :span="8">
- <el-form-item label="爱好" prop="hobby"><el-input v-model="projectContactForm.hobby" placeholder="请输入" /></el-form-item>
- </el-col>
- <el-col :span="8">
- <el-form-item label="性格特征" prop="characterTrait"><el-input v-model="projectContactForm.characterTrait" placeholder="请输入" /></el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="20">
- <el-col :span="8">
- <el-form-item label="是否抽烟" prop="isSmoke">
- <el-radio-group v-model="projectContactForm.isSmoke">
- <el-radio value="1">是</el-radio><el-radio value="0">否</el-radio>
- </el-radio-group>
- </el-form-item>
- </el-col>
- <el-col :span="8">
- <el-form-item label="是否喝酒" prop="isDrink">
- <el-radio-group v-model="projectContactForm.isDrink">
- <el-radio value="1">是</el-radio><el-radio value="0">否</el-radio>
- </el-radio-group>
- </el-form-item>
- </el-col>
- </el-row>
- </div>
- </el-form>
- </div>
- <div class="drawer-footer-standard" style="padding: 16px 24px; border-top: 1px solid #f0f0f0; text-align: right;">
- <el-button @click="projectContactDrawerOpen = false">取 消</el-button>
- <el-button type="primary" @click="submitProjectContact" :loading="contactSubmitting">确 定</el-button>
- </div>
- </el-drawer>
- <!-- 关联联系人对话框 -->
- <el-dialog title="关联客户联系人" v-model="associateVisible" width="900px" append-to-body>
- <el-table :data="associateList" v-loading="associateLoading" border @selection-change="handleSelectionChange" max-height="400px">
- <el-table-column type="selection" width="55" />
- <el-table-column label="姓名" align="center" prop="contactName" width="120" />
- <el-table-column label="客户名称" align="center" prop="customerName" show-overflow-tooltip />
- <el-table-column label="部门" align="center" prop="deptName" show-overflow-tooltip />
- <el-table-column label="职位" align="center" prop="position" width="150" />
- <el-table-column label="手机号码" align="center" prop="phone" width="150" />
- </el-table>
- <template #footer>
- <div class="dialog-footer">
- <el-button @click="associateVisible = false">取 消</el-button>
- <el-button type="primary" @click="confirmAssociate" :disabled="selectedContacts.length === 0">确 认</el-button>
- </div>
- </template>
- </el-dialog>
- </template>
- <script setup>
- import { ref, reactive, computed, watch, getCurrentInstance, toRefs } from 'vue';
- import { getPlatformSelection, updatePlatformSelection } from '@/api/saleManage/platformSelection/index';
- import { listContact, addContact, delContact, updateContact, getContact } from '@/api/customer/crmContact';
- import { getSalesResultAnalyzeByObjectNo, addSalesResultAnalyze, updateSalesResultAnalyze } from '@/api/saleManage/leads/salesResultAnalyze';
- import { listByIds } from "@/api/system/oss/index";
- import { globalHeaders } from '@/utils/request';
- import { listProvinceWithCities } from "@/api/customer/addressArea";
- import { useDebounceFn } from '@vueuse/core';
- import BusinessActivity from '@/views/common/businessActivity.vue';
- import { ElMessageBox, ElMessage } from 'element-plus';
- import { listComStaff } from '@/api/system/comStaff/index';
- import { listCustomerInfo } from "@/api/customer/customerInfo/index";
- const proxy = getCurrentInstance().proxy;
- const businessActivityRef = ref(null);
- const props = defineProps({
- modelValue: Boolean,
- id: [String, Number],
- options: { type: Object, default: () => ({}) }
- });
- const emit = defineEmits(['update:modelValue', 'edit', 'success']);
- const visible = computed({
- get: () => props.modelValue,
- set: (val) => emit('update:modelValue', val)
- });
- const loading = ref(false);
- const isReadOnly = ref(true);
- const drawerForm = ref({ projectStatus: 1, fileList: [], memberList: [] });
- const drawerActiveTab = ref('info');
- const stepList = ['获取信息', '正式立项', '竞价投标', '项目跟进', '结案'];
- /**
- * loadData 后调用:当后端详情接口未返回 leaderName / deptName 文字时,
- * 通过 listComStaff(staffId) 查询 CRM 员工信息(含 staffName 和 deptName)进行补全。
- */
- const enrichFormNames = async () => {
- const data = drawerForm.value;
- const leaderId = data.leader || data.managerId;
- // leaderName 和 deptName 都已有文字,不需要查询
- const hasLeaderName = data.leaderName && isNaN(Number(data.leaderName));
- const hasDeptName = data.deptName && isNaN(Number(data.deptName));
- if (hasLeaderName && hasDeptName) return;
- if (!leaderId) return;
- try {
- const res = await listComStaff({ staffId: leaderId });
- // listComStaff 返回分页结构,取第一条匹配的员工
- const staff = (res.rows || res.data || [])[0];
- if (!staff) return;
- drawerForm.value = {
- ...drawerForm.value,
- // 只在当前字段为空时覆盖
- leaderName: hasLeaderName ? data.leaderName : (staff.staffName || ''),
- deptName: hasDeptName ? data.deptName : (staff.deptName || '')
- };
- } catch (e) {
- // 静默失败,不影响主流程
- }
- };
- const contactList = ref([]);
- const contactLoading = ref(false);
- const contactSubmitting = ref(false);
- const projectContactDrawerOpen = ref(false);
- const projectContactFormRef = ref(null);
- const projectContactForm = reactive({
- id: undefined,
- contactName: '',
- type: '1',
- gender: '0',
- age: '',
- nativePlace: '',
- birthday: '',
- remark: '',
- jobStatus: '1',
- phone: '',
- deptName: '',
- position: '',
- officePhone: '',
- officeRegion: [],
- addressDetail: '',
- jobContent: '',
- projectRole: null,
- isKeyPerson: 0,
- prStatus: '',
- familyStatus: '',
- hobby: '',
- characterTrait: '',
- isSmoke: '0',
- isDrink: '0',
- homeRegion: [],
- homeAddressDetail: ''
- });
- const projectContactRules = {
- contactName: [{ required: true, message: '姓名不能为空', trigger: 'blur' }],
- age: [{ required: true, message: '年龄不能为空', trigger: 'blur' }],
- deptName: [{ required: true, message: '部门不能为空', trigger: 'blur' }],
- phone: [
- { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的11位手机号码', trigger: 'blur' }
- ],
- officePhone: [
- { pattern: /^1[3-9]\d{9}$/, message: '座机号请输入正确的11位手机号码', trigger: 'blur' }
- ]
- };
- const { LXRJE0001: projectRoleOptions } = toRefs(reactive(proxy.useDict("LXRJE0001")));
- const areaOptions = ref([]);
- const associateVisible = ref(false);
- const associateLoading = ref(false);
- const associateList = ref([]);
- const selectedContacts = ref([]);
- // 结果分析逻辑
- const analysisForm = reactive({ id: undefined, resultType: 'win', summary: '', loseReason: '', fileNo: '' });
- const analysisFileList = ref([]);
- // 附件管理
- const detailFileList = ref([]);
- const uploadFileUrl = ref(import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload');
- const headers = ref(globalHeaders());
- watch(() => props.modelValue, (val) => {
- if (val && props.id) {
- loadData();
- }
- });
- const loadData = async () => {
- loading.value = true;
- try {
- const res = await getPlatformSelection(props.id);
- const data = res.data || {};
- drawerForm.value = {
- ...data,
- // 字段兼容映射
- materialCategory: data.materialCategory || data.profession,
- biddingAgency: data.biddingAgency || data.agency,
- nextBiddingTime: data.nextBiddingTime || data.nextBidTime,
- noticeAdvanceDays: data.noticeAdvanceDays ?? data.reminderDays,
- serviceTime: data.serviceTime || data.serviceTimeRange,
- signUpDeadline: data.signUpDeadline || data.entryDeadline,
- tenderDeadline: data.tenderDeadline || data.bidDeadline,
- shortlistedType: data.shortlistedType || data.finalizationType,
- shortlistedRequirement: data.shortlistedRequirement || data.condition || data.requirement,
- industry: data.industry || data.industryName || data.professionName,
- deptName: data.deptName || data.createDeptName || data.dept?.deptName || data.createDept
- };
- // 通过客户编号查询真实的客户 ID
- const cNo = drawerForm.value.customNo || drawerForm.value.companyNo || drawerForm.value.customerNo;
- if (cNo) {
- listCustomerInfo({ customerNo: cNo }).then(cRes => {
- if (cRes.rows && cRes.rows.length > 0) {
- drawerForm.value.realCustomerId = cRes.rows[0].id;
- }
- });
- }
- // 加载完数据后,立即补全负责人和部门的文字名称
- enrichFormNames();
- fetchContactList(props.id);
- loadAnalysisData();
- loadFileList();
- } catch (error) {
- proxy.$modal.msgError("获取数据失败");
- } finally {
- loading.value = false;
- }
- };
- const loadFileList = async () => {
- if (drawerForm.value.fileNo) {
- try {
- const res = await listByIds(drawerForm.value.fileNo);
- detailFileList.value = (res.data || []).map(item => ({
- name: item.originalName || item.fileName || '',
- url: item.url,
- ossId: item.ossId,
- uploadTime: item.createTime
- }));
- } catch (e) {}
- } else {
- detailFileList.value = [];
- }
- };
- const fetchContactList = async (id) => {
- if (!id) return;
- contactLoading.value = true;
- try {
- const res = await listContact({ platformCode: String(id) });
- contactList.value = res.rows || [];
- } catch (e) {} finally { contactLoading.value = false; }
- };
- const loadDicts = () => {
- if (areaOptions.value.length === 0) {
- listProvinceWithCities().then(res => {
- const list = res.rows || [];
- if (list.length > 0) {
- areaOptions.value = handleTree(list, "id", "parentId");
- }
- });
- }
- };
- /** 构造树型结构数据 */
- function handleTree(data, id, parentId, children) {
- let config = {
- id: id || 'id',
- parentId: parentId || 'parentId',
- childrenList: children || 'children'
- };
- var childrenListMap = {};
- var nodeIds = {};
- var tree = [];
- for (let d of data) {
- let pId = d[config.parentId];
- if (childrenListMap[pId] == null) {
- childrenListMap[pId] = [];
- }
- nodeIds[d[config.id]] = d;
- childrenListMap[pId].push(d);
- }
- for (let d of data) {
- let pId = d[config.parentId];
- if (nodeIds[pId] == null) {
- tree.push(d);
- }
- }
- for (let t of tree) {
- adaptToChildrenList(t);
- }
- function adaptToChildrenList(o) {
- if (childrenListMap[o[config.id]] !== null) {
- o[config.childrenList] = childrenListMap[o[config.id]];
- }
- if (o[config.childrenList]) {
- for (let c of o[config.childrenList]) {
- adaptToChildrenList(c);
- }
- }
- }
- return tree;
- }
- loadDicts();
- // --- 项目联系人交互 ---
- const handleContactCommand = (command) => {
- if (command === 'associate') { handleAssociate(); }
- else if (command === 'new') { handleNewProjectContact(); }
- };
- const handleAssociate = () => {
- if (!drawerForm.value.customNo && !drawerForm.value.companyNo && !drawerForm.value.customerNo) {
- return ElMessage.warning("缺少客户信息,无法关联联系人");
- }
- associateVisible.value = true;
- associateLoading.value = true;
- // 统一使用客户编号进行查询
- const cNo = drawerForm.value.customNo || drawerForm.value.companyNo || drawerForm.value.customerNo;
- listContact({ customNo: cNo }).then(res => {
- const currentIds = contactList.value.map(c => c.id);
- associateList.value = (res.rows || []).filter(item => !currentIds.includes(item.id));
- }).finally(() => { associateLoading.value = false; });
- };
- const handleSelectionChange = (selection) => { selectedContacts.value = selection; };
- const confirmAssociate = async () => {
- if (selectedContacts.value.length === 0) return;
- try {
- const promises = selectedContacts.value.map(contact => {
- return updateContact({ ...contact, platformCode: String(props.id) });
- });
- await Promise.all(promises);
- ElMessage.success('关联成功');
- associateVisible.value = false;
- fetchContactList(props.id);
- } catch (e) {}
- };
- const handleNewProjectContact = () => {
- Object.assign(projectContactForm, {
- id: undefined, contactName: '', type: '1', gender: '0', age: '', nativePlace: '', birthday: '', remark: '',
- jobStatus: '1', phone: '', deptName: '', position: '', officePhone: '', officeRegion: [], addressDetail: '', jobContent: '',
- projectRole: null, isKeyPerson: 0, prStatus: '', familyStatus: '', hobby: '', characterTrait: '', isSmoke: '0', isDrink: '0',
- homeRegion: [], homeAddressDetail: ''
- });
- projectContactDrawerOpen.value = true;
- };
- const handleEditContact = (row) => {
- const contactId = row.contactId || row.id;
- if (!contactId) return;
- proxy.$modal.loading("加载中...");
- getContact(contactId).then(res => {
- const data = res.data || {};
- // 防御性合并:优先使用详情接口数据,但如果详情接口返回 null,则保留列表行中的有效数据
- const mergedData = { ...row };
- for (const key in data) {
- if (data[key] !== null && data[key] !== undefined && data[key] !== '') {
- mergedData[key] = data[key];
- }
- }
-
- Object.assign(projectContactForm, {
- id: data.id || row.id,
- contactName: mergedData.contactName,
- type: String(mergedData.type || '1'),
- gender: String(mergedData.gender || '0'),
- age: mergedData.age,
- nativePlace: mergedData.nativePlace,
- birthday: mergedData.birthday,
- remark: mergedData.remark,
- jobStatus: String(mergedData.jobStatus !== null && mergedData.jobStatus !== undefined ? mergedData.jobStatus : '1'),
- phone: mergedData.phone,
- deptName: mergedData.deptName,
- position: mergedData.position,
- officePhone: mergedData.officePhone,
- officeRegion: (mergedData.addressProvince != null && mergedData.addressCity != null && mergedData.addressCounty != null)
- ? [String(mergedData.addressProvince), String(mergedData.addressCity), String(mergedData.addressCounty)] : [],
- addressDetail: mergedData.addressDetail,
- jobContent: mergedData.jobContent,
- projectRole: mergedData.projectRole !== null && mergedData.projectRole !== undefined ? Number(mergedData.projectRole) : null,
- isKeyPerson: mergedData.isKeyPerson !== null && mergedData.isKeyPerson !== undefined ? Number(mergedData.isKeyPerson) : 0,
- prStatus: mergedData.prStatus,
- familyStatus: mergedData.familyStatus || mergedData.familyInfo || '',
- hobby: mergedData.hobby || mergedData.hobbies || '',
- characterTrait: mergedData.characterTrait || mergedData.character || '',
- isSmoke: String(mergedData.isSmoke !== null && mergedData.isSmoke !== undefined ? mergedData.isSmoke : '0'),
- isDrink: String(mergedData.isDrink !== null && mergedData.isDrink !== undefined ? mergedData.isDrink : '0'),
- homeRegion: (mergedData.homeProvinceId != null && mergedData.homeCityId != null && mergedData.homeAreaId != null)
- ? [Number(mergedData.homeProvinceId), Number(mergedData.homeCityId), Number(mergedData.homeAreaId)] : [],
- homeAddressDetail: mergedData.homeAddressDetail
- });
- // 区域级联处理 (适配代码转换)
- const findIdByCode = (code) => {
- if (!code) return null;
- const findInTree = (nodes, targetCode) => {
- for (const node of nodes) {
- if (String(node.areaCode) === String(targetCode)) return node.id;
- if (node.children) {
- const found = findInTree(node.children, targetCode);
- if (found) return found;
- }
- }
- return null;
- };
- return findInTree(areaOptions.value, code);
- };
- if (mergedData.addressProvince || mergedData.addressCity || mergedData.addressCounty) {
- const officeArr = [];
- if (mergedData.addressProvince) {
- const pid = /^\d+$/.test(mergedData.addressProvince) && mergedData.addressProvince.length < 5 ? mergedData.addressProvince : findIdByCode(mergedData.addressProvince);
- if (pid) officeArr.push(Number(pid));
- }
- if (mergedData.addressCity) {
- const cid = /^\d+$/.test(mergedData.addressCity) && mergedData.addressCity.length < 7 ? mergedData.addressCity : findIdByCode(mergedData.addressCity);
- if (cid) officeArr.push(Number(cid));
- }
- if (mergedData.addressCounty) {
- const aid = /^\d+$/.test(mergedData.addressCounty) && mergedData.addressCounty.length < 9 ? mergedData.addressCounty : findIdByCode(mergedData.addressCounty);
- if (aid) officeArr.push(Number(aid));
- }
- projectContactForm.officeRegion = officeArr;
- }
-
- projectContactDrawerOpen.value = true;
- }).finally(() => {
- proxy.$modal.closeLoading();
- });
- };
- const submitProjectContact = () => {
- projectContactFormRef.value.validate(async (valid) => {
- if (valid) {
- contactSubmitting.value = true;
- try {
- // 获取选中的 AreaCode 列表 (用于办公地址转换)
- const findCodeById = (id) => {
- const findInTree = (nodes, targetId) => {
- for (const node of nodes) {
- if (Number(node.id) === Number(targetId)) return node.areaCode;
- if (node.children) {
- const found = findInTree(node.children, targetId);
- if (found) return found;
- }
- }
- return null;
- };
- return findInTree(areaOptions.value, id);
- };
- const payload = {
- ...projectContactForm,
- platformCode: String(props.id),
- // 转换办公地址编码为代码 String
- addressProvince: projectContactForm.officeRegion?.[0] ? findCodeById(projectContactForm.officeRegion[0]) : null,
- addressCity: projectContactForm.officeRegion?.[1] ? findCodeById(projectContactForm.officeRegion[1]) : null,
- addressCounty: projectContactForm.officeRegion?.[2] ? findCodeById(projectContactForm.officeRegion[2]) : null,
- // 转换家庭地址为 ID Long
- homeProvinceId: projectContactForm.homeRegion?.[0] || null,
- homeCityId: projectContactForm.homeRegion?.[1] || null,
- homeAreaId: projectContactForm.homeRegion?.[2] || null,
- // 关联客户信息
- customerName: drawerForm.value.customName || drawerForm.value.customerName || '',
- customNo: drawerForm.value.customNo || drawerForm.value.customerNo || '',
- customerId: drawerForm.value.realCustomerId || null
- };
- if (projectContactForm.id) { await updateContact(payload); }
- else { await addContact(payload); }
- ElMessage.success('保存成功');
- projectContactDrawerOpen.value = false;
- fetchContactList(props.id);
- } catch (e) {} finally { contactSubmitting.value = false; }
- }
- });
- };
- const handleDeleteContact = (row) => {
- ElMessageBox.confirm(`确认删除联系人「${row.name || row.contactName}」吗?`, '提示', { type: 'warning' }).then(async () => {
- await delContact(row.id);
- ElMessage.success('操作成功');
- fetchContactList(props.id);
- });
- };
- // --- 结果分析交互 ---
- const loadAnalysisData = async () => {
- if (!drawerForm.value.projectNo) return;
- try {
- const res = await getSalesResultAnalyzeByObjectNo(drawerForm.value.projectNo);
- if (res.data) {
- Object.assign(analysisForm, {
- id: res.data.id, resultType: res.data.dealResult === 1 ? 'win' : 'lose',
- summary: res.data.winSumUp, loseReason: res.data.loseReason, fileNo: res.data.fileNo
- });
- if (analysisForm.fileNo) {
- const ossRes = await listByIds(analysisForm.fileNo);
- analysisFileList.value = (ossRes.data || []).map(i => ({
- name: i.originalName || i.fileName || '',
- url: i.url,
- ossId: i.ossId
- }));
- }
- }
- } catch (e) {}
- };
- const handleSaveAnalysis = useDebounceFn(async () => {
- const ossIds = analysisFileList.value.map(f => f.ossId).join(',');
- const payload = {
- id: analysisForm.id,
- objectNo: drawerForm.value.projectNo,
- dataType: 4, // 1:线索, 2:商机, 3:项目优选, 4:平台优选
- dealResult: analysisForm.resultType === 'win' ? 1 : 2,
- winSumUp: analysisForm.summary,
- loseReason: analysisForm.loseReason,
- fileNo: ossIds
- };
- try {
- if (analysisForm.id) { await updateSalesResultAnalyze(payload); }
- else { await addSalesResultAnalyze(payload); }
- ElMessage.success("保存成功");
- loadAnalysisData();
- } catch (e) {}
- }, 300);
- const handleAnalysisUploadSuccess = (res) => {
- if (res.code === 200) {
- analysisFileList.value.push({
- name: res.data.originalName || res.data.fileName || '',
- url: res.data.url,
- ossId: res.data.ossId
- });
- ElMessage.success("上传成功");
- }
- };
- const handleAnalysisDeleteFile = (index) => { analysisFileList.value.splice(index, 1); };
- // --- 附件管理交互 ---
- const handleUploadSuccess = async (res) => {
- if (res.code === 200) {
- const ossId = res.data.ossId;
- const currentFileNos = drawerForm.value.fileNo ? drawerForm.value.fileNo.split(',') : [];
- currentFileNos.push(ossId);
- const newFileNo = currentFileNos.join(',');
-
- try {
- await updatePlatformSelection({ id: props.id, fileNo: newFileNo });
- drawerForm.value.fileNo = newFileNo;
- loadFileList();
- ElMessage.success('上传成功');
- } catch (e) {}
- }
- };
- const downloadFile = (file) => {
- if (file.url) {
- if (proxy.$download && proxy.$download.resource) {
- proxy.$download.resource(file.url);
- } else {
- window.open(file.url, '_blank');
- }
- }
- };
- const handleDeleteFile = (row) => {
- ElMessageBox.confirm(`确认删除附件「${row.name}」吗?`, '提示', { type: 'warning' }).then(async () => {
- const currentFileNos = drawerForm.value.fileNo ? drawerForm.value.fileNo.split(',') : [];
- const newFileNos = currentFileNos.filter(id => String(id) !== String(row.ossId));
- const newFileNoStr = newFileNos.join(',');
-
- try {
- await updatePlatformSelection({ id: props.id, fileNo: newFileNoStr });
- drawerForm.value.fileNo = newFileNoStr;
- loadFileList();
- ElMessage.success('删除成功');
- } catch (e) {
- proxy.$modal.msgError("删除失败");
- }
- }).catch(() => {});
- };
- const handleStepClick = async (status) => {
- if (isReadOnly.value) return;
- // 再次点击取消:如果点击已激活步骤则设为 null,否则设为目标步骤
- const newStatus = String(drawerForm.value.projectStatus) === String(status) ? null : status;
-
- try {
- await updatePlatformSelection({ id: props.id, projectStatus: newStatus });
- drawerForm.value.projectStatus = newStatus;
- proxy.$modal.msgSuccess(newStatus ? "进度更新成功" : "进度已取消");
- if (businessActivityRef.value) {
- businessActivityRef.value.fetchDynamicData();
- }
- emit('success');
- } catch (err) {
- console.error('更新进度失败:', err);
- }
- };
- const handleEdit = () => {
- emit('edit', props.id);
- };
- const handleDownload = (row) => {
- if (!row.fileUrl) return proxy.$modal.msgError("文件地址不存在");
- window.open(import.meta.env.VITE_APP_BASE_API + row.fileUrl, '_blank');
- };
- const submitAnalysis = async () => {
- await updatePlatformSelection(drawerForm.value);
- proxy.$modal.msgSuccess("分析结果保存成功");
- };
- const openLink = (link) => {
- if (link) {
- if (!link.startsWith('http')) link = 'http://' + link;
- window.open(link, '_blank');
- }
- };
- const formatDate = (date) => date ? proxy.parseTime(date, '{y}-{m}-{d}') : '';
- const getStatusLabel = (s) => props.options.status?.find(o => String(o.value) === String(s))?.label || (String(s) === '0' || String(s) === '1' ? '跟进中' : '');
- const getProjectRoleLabel = (val) => {
- return projectRoleOptions.value.find(o => String(o.value) === String(val))?.label || val || '';
- };
- const displayProfessionName = computed(() => {
- const val = drawerForm.value.profession || drawerForm.value.materialCategory;
- return props.options.material?.find(o => String(o.value) === String(val))?.label || '';
- });
- const displayShortlistedName = computed(() => {
- const val = drawerForm.value.shortlistedType || drawerForm.value.finalizationType;
- return props.options.shortlisted?.find(o => String(o.value) === String(val))?.label || '';
- });
- const displayCompanyName = computed(() => {
- const val = drawerForm.value.companyNo;
- return props.options.company?.find(c => String(c.companyCode) === String(val) || String(c.id) === String(val))?.companyName || '';
- });
- const displayProjectLevelName = computed(() => props.options.level?.find(o => String(o.value) === String(drawerForm.value.projectLevel))?.label || '');
- const displayProjectTypeName = computed(() => props.options.type?.find(o => String(o.value) === String(drawerForm.value.businessType))?.label || '');
- const displayIndustryName = computed(() => {
- // 优先取确认是文字名称的字段(排除纯数字 ID)
- if (drawerForm.value.industryName && isNaN(Number(drawerForm.value.industryName))) return drawerForm.value.industryName;
- if (drawerForm.value.industryCategoryName && isNaN(Number(drawerForm.value.industryCategoryName))) return drawerForm.value.industryCategoryName;
- if (drawerForm.value.industry && isNaN(Number(drawerForm.value.industry))) return drawerForm.value.industry;
-
- const flatten = (list) => {
- let res = [];
- list.forEach(i => {
- res.push(i);
- if (i.children) res = res.concat(flatten(i.children));
- });
- return res;
- };
- // 否则通过 ID 查找
- const professionId = drawerForm.value.profession || drawerForm.value.industry;
- if (professionId && props.options.industryList) {
- const flatList = flatten(props.options.industryList);
- const found = flatList.find(i => String(i.id) === String(professionId))?.industryCategoryName || '';
- if (found) return found;
- }
- return '';
- });
- const displayDeptName = computed(() => {
- // 第一优先:后端直接返回的文字部门名
- const name = drawerForm.value.deptName || drawerForm.value.createDeptName || '';
- if (name && isNaN(Number(name))) return name;
- // 第二兑底:通过部门 ID 在父组件传入的部门树中反查
- const deptId = drawerForm.value.deptNo || drawerForm.value.deptId;
- if (deptId && props.options.dept) {
- const found = flatten(props.options.dept).find(d => String(d.id) === String(deptId));
- if (found) return found.label || found.deptName || '';
- }
- return '';
- });
- const findUserName = (id) => {
- if (!id) return '';
- const user = props.options.user?.find(u => String(u.userId || u.staffId || u.id) === String(id));
- return user ? (user.nickName || user.staffName || user.name) : id;
- };
- </script>
- <style scoped lang="scss">
- .platform-detail-drawer {
- :deep(.el-drawer__body) { padding: 0; background: #fff; overflow: hidden; display: flex; flex-direction: column; }
- }
- .detail-header {
- padding: 12px 24px;
- background: #fff;
- display: flex;
- justify-content: space-between;
- align-items: center;
- z-index: 10;
- border-bottom: 1px solid #f0f2f5;
- .project-title {
- font-size: 16px;
- color: #333;
- font-weight: normal;
- }
- .header-right {
- display: flex;
- align-items: center;
- gap: 16px;
- .close-btn {
- font-size: 20px;
- color: #86909c;
- cursor: pointer;
- &:hover { color: #333; }
- }
- }
- }
- .detail-main-scroll {
- flex: 1;
- display: flex;
- flex-direction: column;
- overflow: hidden;
- padding: 0;
- }
- .drawer-body-custom {
- &::-webkit-scrollbar { display: none; }
- -ms-overflow-style: none;
- scrollbar-width: none;
- }
- .project-progress-card {
- background: #fff; padding: 15px 24px 20px; margin-bottom: 0;
- flex-shrink: 0;
- .section-title { font-size: 13px; color: #333; margin-bottom: 12px; font-weight: normal; }
- .chevron-steps {
- display: flex; align-items: center; gap: 0; padding: 2px 0;
- .chevron-step {
- flex: 1; height: 32px; display: flex; align-items: center; justify-content: center; background-color: #F2F3F5; color: #86909C; font-size: 13px; position: relative; cursor: pointer; transition: all 0.3s;
- margin-right: -10px; // 负边距实现重叠
- clip-path: polygon(calc(100% - 10px) 0%, 100% 50%, calc(100% - 10px) 100%, 0% 100%, 10px 50%, 0% 0%);
-
- &:first-child {
- border-radius: 4px 0 0 4px;
- clip-path: polygon(calc(100% - 10px) 0%, 100% 50%, calc(100% - 10px) 100%, 0% 100%, 0% 0%);
- }
- &:last-child {
- border-radius: 0 4px 4px 0;
- margin-right: 0;
- clip-path: polygon(100% 0%, 100% 100%, 0% 100%, 10px 50%, 0% 0%);
- }
-
- &.is-active { background-color: #00B881; color: #fff; z-index: 2; font-weight: 500 !important; }
- &.is-finished { background-color: #E6F8F3; color: #00B881; z-index: 1; }
- &:hover:not(.is-active) { background-color: #E5E6EB; z-index: 3; }
- }
- }
- .progress-footer { display: none; }
- }
- .detail-content-layout { display: flex; gap: 0; align-items: stretch; margin-top: 0; min-height: 0; flex: 1; border-top: 1px solid #f0f2f5; }
- .content-left-panel { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
- .content-right-panel { width: 450px; background: #fff; display: flex; flex-direction: column; flex-shrink: 0; border-left: 1px solid #f0f2f5; overflow: hidden; }
- .detail-content-card {
- background: #fff; padding: 0; flex: 1; display: flex; flex-direction: column;
- }
- .custom-tabs {
- flex: 1; display: flex; flex-direction: column; overflow: hidden;
- :deep(.el-tabs__header) {
- order: 1;
- margin: 0 !important;
- padding: 0 16px !important;
- background: #fff !important;
- height: 48px !important;
- box-sizing: border-box !important;
- position: relative !important;
- border-bottom: 1px solid #f2f2f2 !important;
- }
- :deep(.el-tabs__nav-wrap::after) { display: none !important; }
- :deep(.el-tabs__item) {
- height: 48px !important;
- line-height: 48px !important;
- font-size: 14px;
- color: #666;
- &.is-active { color: #409eff; font-weight: 500 !important; }
- }
- :deep(.el-tabs__active-bar) {
- height: 2px !important;
- border-radius: 1px !important;
- bottom: 0 !important;
- z-index: 5;
- }
- :deep(.el-tabs__content) { order: 2; flex: 1; overflow-y: auto; padding: 24px; }
- }
- .content-right-panel {
- :deep(.el-tabs) { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
- :deep(.el-tabs__content) { order: 2; flex: 1; overflow-y: auto; padding: 20px; }
- :deep(.el-tabs__header) {
- order: 1;
- margin: 0 !important;
- padding: 0 16px !important;
- background: #fff !important;
- height: 48px !important;
- box-sizing: border-box !important;
- position: relative !important;
- border-bottom: 1px solid #f2f2f2 !important;
- }
- :deep(.el-tabs__item) {
- height: 48px !important;
- line-height: 48px !important;
- padding: 0 10px !important;
- min-width: auto !important;
- }
- :deep(.el-tabs__nav-wrap::after) { display: none !important; }
- :deep(.el-tabs__active-bar) {
- height: 2px !important;
- bottom: 0 !important;
- z-index: 5;
- }
- /* 隐藏右侧可能出现的滚动箭头 */
- :deep(.el-tabs__nav-prev), :deep(.el-tabs__nav-next) { display: none !important; }
- :deep(.el-tabs__nav-scroll) { overflow: visible !important; }
- }
- .info-section {
- .info-section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; .title { font-size: 15px; color: #409eff; font-weight: normal; } }
- .custom-desc {
- :deep(.el-descriptions__label) { color: #86909c; font-weight: normal; width: auto; background: transparent !important; padding-bottom: 12px; &::after { content: ":"; } }
- :deep(.el-descriptions__content) { color: #333; padding-bottom: 12px; }
- }
- }
- .mt-24 { margin-top: 24px; }
- :deep(.hidden-label) { display: none !important; }
- :deep(.hidden-value) { display: none !important; }
- .no-bold-label {
- :deep(.el-form-item__label) { font-weight: normal !important; color: #666; }
- :deep(.el-form-item) { margin-bottom: 18px !important; }
- }
- // 移除全局强制取消加粗限制
- // :deep(*) { font-weight: normal !important; }
- .mt-20 { margin-top: 20px; }
- .tab-toolbar { margin-bottom: 16px; display: flex; justify-content: flex-end; }
- .contact-tab-content, .analysis-tab-content, .files-tab-content { min-height: 400px; padding: 10px 0; }
- .analysis-form {
- max-width: 900px;
- .analysis-row { display: flex; align-items: center; margin-bottom: 24px; .form-label { width: 80px; color: #4E5969; } .result-radio { margin-left: 12px; } }
- .summary-title { font-size: 14px; color: #4E5969; margin-bottom: 12px; }
- .summary-textarea-wrap { margin-bottom: 24px; }
- .attachment-section-title { font-size: 14px; color: #4E5969; margin-bottom: 12px; }
- .upload-area { margin-bottom: 12px; }
- }
- .empty-tab { color: #86909c; font-size: 13px; text-align: center; padding: 40px; }
- .custom-table { :deep(th.el-table__cell) { background-color: #f7f8fa !important; color: #4e5969; font-weight: normal; } }
- /* 抽屉标准样式 */
- .section-group-title-bar { font-size: 14px; color: #409eff; padding: 8px 16px; background-color: #F8F9FA; margin-bottom: 20px; border-radius: 4px; }
- .form-section-group {
- margin-bottom: 32px;
- :deep(.el-form-item) { margin-bottom: 18px !important; }
- }
- .dialog-footer { display: flex; justify-content: flex-end; gap: 12px; padding-top: 20px; }
- </style>
|