| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811 |
- <template>
- <div class="delivery-project-page">
- <!-- 查询表单 -->
- <div class="query-form-container">
- <div class="query-form-basic">
- <el-form class="query-form-fields" :inline="true" :model="queryForm" size="small">
- <el-form-item label="事件标题">
- <el-input v-model="queryForm.deliveryEventTitle" clearable placeholder="请输入" style="width: 180px" />
- </el-form-item>
- <el-form-item label="事件状态">
- <el-select
- v-model="queryForm.deliveryEventStatus"
- collapse-tags
- multiple
- placeholder="请选择"
- style="width: 140px">
- <el-option
- v-for="dict in deliveryEventStatusOptions"
- :key="dict.key"
- :label="dict.value"
- :value="dict.key" />
- </el-select>
- </el-form-item>
- <el-form-item label="反馈人">
- <el-input v-model="queryForm.feedbackReporter" clearable placeholder="请输入" style="width: 120px" />
- </el-form-item>
- <el-form-item label="负责人">
- <el-select
- v-model="queryForm.opsUserName"
- class="query-select--owner"
- clearable
- collapse-tags
- filterable
- :loading="opsUsersLoading"
- multiple
- placeholder="请选择"
- remote
- :remote-method="remoteFetchOpsUsers"
- reserve-keyword
- @visible-change="handleOpsUserVisibleChange">
- <el-option v-for="u in opsUserOptions" :key="u.value" :label="u.label" :value="u.label" />
- </el-select>
- </el-form-item>
- <el-form-item label="反馈时间">
- <el-date-picker
- v-model="queryForm.feedbackDateRange"
- end-placeholder="结束日期"
- range-separator="至"
- start-placeholder="开始日期"
- style="width: 300px"
- type="daterange"
- value-format="yyyy-MM-dd" />
- </el-form-item>
- </el-form>
- <div class="query-form-actions">
- <el-button icon="el-icon-search" type="primary" @click="handleSearch">查询</el-button>
- <el-button icon="el-icon-refresh-right" @click="handleReset">重置</el-button>
- <el-button type="text" @click="showAdvanced = !showAdvanced">
- {{ showAdvanced ? '收起' : '高级筛选' }}
- <i :class="showAdvanced ? 'el-icon-arrow-up' : 'el-icon-arrow-down'" />
- </el-button>
- <el-divider direction="vertical" />
- <el-button icon="el-icon-plus" type="success" @click="handleAdd">新增</el-button>
- </div>
- </div>
- <!-- 高级查询条件 -->
- <div v-show="showAdvanced" class="query-form-advanced">
- <el-form :inline="true" :model="queryForm" size="small">
- <el-form-item label="事件类型">
- <el-select
- v-model="queryForm.deliveryEventType"
- collapse-tags
- multiple
- placeholder="请选择"
- style="width: 160px">
- <el-option
- v-for="dict in deliveryEventTypeOptions"
- :key="dict.key"
- :label="dict.value"
- :value="dict.key" />
- </el-select>
- </el-form-item>
- <el-form-item label="事件结果">
- <el-select
- v-model="queryForm.deliveryEventResult"
- collapse-tags
- multiple
- placeholder="请选择"
- style="width: 140px">
- <el-option
- v-for="dict in deliveryEventResultOptions"
- :key="dict.key"
- :label="dict.value"
- :value="dict.key" />
- </el-select>
- </el-form-item>
- <el-form-item label="事件描述">
- <el-input v-model="queryForm.deliveryEventDesc" clearable placeholder="请输入" style="width: 180px" />
- </el-form-item>
- <el-form-item label="处理时间">
- <el-date-picker
- v-model="queryForm.completeDateRange"
- end-placeholder="结束日期"
- range-separator="至"
- start-placeholder="开始日期"
- style="width: 300px"
- type="daterange"
- value-format="yyyy-MM-dd" />
- </el-form-item>
- </el-form>
- </div>
- </div>
- <!-- 主体内容 -->
- <div class="main-content">
- <!-- 左侧项目筛选 -->
- <div :class="['project-sidebar', { collapsed: sidebarCollapsed }]">
- <div class="sidebar-header">
- <div v-if="!sidebarCollapsed" class="sidebar-title-group">
- <span class="sidebar-title">项目列表</span>
- <button
- :class="['sidebar-action-btn', { active: showSidebarFilters }]"
- :title="showSidebarFilters ? '隐藏项目筛选' : '显示项目筛选'"
- type="button"
- @click="toggleSidebarFilters">
- <i class="el-icon-search" />
- </button>
- </div>
- <div class="sidebar-actions">
- <button
- class="collapse-trigger"
- :title="sidebarCollapsed ? '展开项目列表' : '折叠项目列表'"
- type="button"
- @click="toggleSidebar">
- <i :class="sidebarCollapsed ? 'el-icon-d-arrow-right' : 'el-icon-d-arrow-left'" />
- </button>
- </div>
- </div>
- <div v-if="sidebarCollapsed" class="sidebar-collapsed-label">项目列表</div>
- <template v-if="!sidebarCollapsed">
- <template v-if="showSidebarFilters">
- <div class="project-status-filter">
- <el-radio-group v-model="projectStatusFilter" size="small" @change="handleProjectStatusChange">
- <el-radio-button label="">全部</el-radio-button>
- <el-radio-button label="pending">待分配</el-radio-button>
- <el-radio-button label="delivering">交付中</el-radio-button>
- <el-radio-button label="delivered">已验收</el-radio-button>
- </el-radio-group>
- </div>
- <el-input
- v-model="projectSearch"
- class="project-search"
- clearable
- placeholder="搜索项目/销售/交付"
- prefix-icon="el-icon-search"
- size="small"
- @input="onProjectSearchDebounced" />
- </template>
- <div ref="projectList" class="project-list">
- <div
- v-if="allProjectOption"
- :class="['project-item', 'project-item--all', { active: selectedProject === allProjectOption.id }]"
- @click="selectProject(allProjectOption.id)">
- <div>
- <div class="project-overview-label">{{ allProjectOption.name }}</div>
- <div class="project-overview-desc">查看全部项目交付进度</div>
- </div>
- </div>
- <div
- v-for="project in projectCards"
- :key="project.id"
- :class="['project-card', { active: selectedProject === project.id }]"
- @click="selectProject(project.id, project)">
- <div class="project-card-top">
- <div class="project-card-tags">
- <span class="project-contract clickable" @click.stop="handleViewProjectInfo(project)">
- {{ project.contractNo || '-' }}
- </span>
- <span class="project-line-tag">{{ getProductLineLabel(project.productLine) }}</span>
- </div>
- <span :class="['project-status-tag', 'project-status-tag--' + project.status]">
- {{ selectDictLabel(projectStatusOptions, project.status) }}
- </span>
- </div>
- <div class="project-card-title" :title="project.name">{{ project.name }}</div>
- <div class="project-card-meta">
- <div class="project-card-meta-item">
- <i class="el-icon-user project-card-meta-icon" title="销售负责人" />
- <span class="project-card-meta-value" :title="project.salesOwner || '-'">
- {{ project.salesOwner || '-' }}
- </span>
- </div>
- <div class="project-card-meta-item">
- <i
- class="el-icon-s-custom project-card-meta-icon project-card-meta-icon--delivery"
- title="交付负责人" />
- <span class="project-card-meta-value" :title="project.deliveryOwner || '-'">
- {{ project.deliveryOwner || '-' }}
- </span>
- <i
- v-if="project.deliveryOwner && canManageDelivery"
- class="el-icon-refresh-right reassign-btn"
- title="改派"
- @click.stop="handleReassign(project)" />
- </div>
- </div>
- <i v-if="selectedProject === project.id" class="el-icon-check project-card-check" />
- </div>
- <div v-if="projectLoading" class="project-list-loading">
- <i class="el-icon-loading" />
- 加载中...
- </div>
- <div v-else-if="!projectHasMore && projects.length > 1" class="project-list-end">没有更多了</div>
- </div>
- </template>
- </div>
- <!-- 右侧数据表格 -->
- <div class="table-container">
- <div class="table-scroll-wrapper">
- <el-table
- v-loading="loading"
- border
- class="delivery-project-table"
- :data="tableData"
- :fit="false"
- height="100%"
- stripe
- style="width: 100%"
- @row-dblclick="handleRowClick">
- <el-table-column align="center" type="index" width="50" />
- <el-table-column label="事件标题" min-width="320" show-overflow-tooltip>
- <template slot-scope="{ row }">
- <span class="event-title-text">
- {{ row.deliveryEventTitle || row.delivery_event_title || '-' }}
- </span>
- </template>
- </el-table-column>
- <el-table-column label="项目名称" min-width="200" show-overflow-tooltip>
- <template slot-scope="{ row }">
- {{ row.projectName || row.project_name || '-' }}
- </template>
- </el-table-column>
- <el-table-column
- label="事件类型"
- :render-header="renderSortableHeader('事件类型', 'deliveryEventType')"
- width="140">
- <template slot-scope="{ row }">
- <el-tag size="small" type="info">
- {{ selectDictLabel(deliveryEventTypeOptions, row.deliveryEventType || row.delivery_event_type) }}
- </el-tag>
- </template>
- </el-table-column>
- <el-table-column
- label="事件状态"
- :render-header="renderSortableHeader('事件状态', 'deliveryEventStatus')"
- width="120">
- <template slot-scope="{ row }">
- <el-tag
- size="small"
- :type="getDeliveryEventStatusTagType(row.deliveryEventStatus || row.delivery_event_status)">
- {{
- selectDictLabel(deliveryEventStatusOptions, row.deliveryEventStatus || row.delivery_event_status)
- }}
- </el-tag>
- </template>
- </el-table-column>
- <el-table-column
- label="事件结果"
- :render-header="renderSortableHeader('事件结果', 'deliveryEventResult')"
- width="120">
- <template slot-scope="{ row }">
- {{ selectDictLabel(deliveryEventResultOptions, row.deliveryEventResult || row.delivery_event_result) }}
- </template>
- </el-table-column>
- <el-table-column
- label="负责人"
- :render-header="renderSortableHeader('负责人', 'opsUserName')"
- show-overflow-tooltip
- width="130">
- <template slot-scope="{ row }">
- {{ row.opsUserName || row.ops_user_name || '-' }}
- </template>
- </el-table-column>
- <el-table-column
- label="处理时间"
- :render-header="renderSortableHeader('处理时间', 'completeTime')"
- width="180">
- <template slot-scope="{ row }">
- {{ formatEventTime(row.completeTime || row.complete_time) }}
- </template>
- </el-table-column>
- <el-table-column label="处理说明" min-width="240" show-overflow-tooltip>
- <template slot-scope="{ row }">
- {{ row.completeDesc || row.complete_desc || '-' }}
- </template>
- </el-table-column>
- <el-table-column label="是否现场" width="90">
- <template slot-scope="{ row }">
- {{ selectDictLabel(onSiteOptions, row.onSite || row.on_site) }}
- </template>
- </el-table-column>
- <el-table-column
- label="反馈人"
- :render-header="renderSortableHeader('反馈人', 'feedbackReporter')"
- show-overflow-tooltip
- width="130">
- <template slot-scope="{ row }">
- {{ row.feedbackReporter || row.feedback_reporter || '-' }}
- </template>
- </el-table-column>
- <el-table-column
- label="反馈时间"
- :render-header="renderSortableHeader('反馈时间', 'feedbackDate')"
- width="180">
- <template slot-scope="{ row }">
- {{ formatEventTime(row.feedbackDate || row.feedback_date) }}
- </template>
- </el-table-column>
- <el-table-column label="反馈来源" width="100">
- <template slot-scope="{ row }">
- {{ selectDictLabel(feedbackSourceOptions, row.feedbackSource || row.feedback_source) }}
- </template>
- </el-table-column>
- <el-table-column fixed="right" header-align="center" label="操作" width="200">
- <template slot-scope="{ row }">
- <el-button
- v-if="String(row.deliveryEventStatus || row.delivery_event_status) !== '30'"
- size="mini"
- type="text"
- @click.stop="handleEdit(row)">
- 编辑
- </el-button>
- <el-button
- v-if="['10', '20'].includes(String(row.deliveryEventStatus || row.delivery_event_status))"
- size="mini"
- type="text"
- @click.stop="handleProcess(row)">
- 处理
- </el-button>
- <el-button v-if="showProgressButton(row)" size="mini" type="text" @click.stop="handleProgress(row)">
- 进展
- </el-button>
- <el-button
- v-if="['10', '20'].includes(String(row.deliveryEventStatus || row.delivery_event_status))"
- size="mini"
- style="color: #f56c6c"
- type="text"
- @click.stop="handleCancelEvent(row)">
- 作废
- </el-button>
- </template>
- </el-table-column>
- </el-table>
- </div>
- <!-- 分页 -->
- <div class="pagination-wrapper">
- <el-pagination
- background
- :current-page.sync="queryForm.pageNum"
- layout="total, sizes, prev, pager, next, jumper"
- :page-size.sync="queryForm.pageSize"
- :page-sizes="[10, 20, 50, 100]"
- :total="total"
- @current-change="handleCurrentChange"
- @size-change="handleSizeChange" />
- </div>
- </div>
- </div>
- <!-- 编辑弹窗 -->
- <delivery-project-event-edit
- :data="editData"
- :project-id="selectedProject"
- :visible.sync="editDialogVisible"
- @refresh="handleRefreshEvent" />
- <!-- 详情弹窗 -->
- <delivery-project-event-detail
- :data="currentRow"
- :process-mode="detailMode === 'process'"
- :read-only="detailMode === 'view'"
- :show-close-event-dialog.sync="showCloseEventDialog"
- :visible.sync="detailVisible"
- @refresh="handleRefreshEvent" />
- <!-- 作废事件弹窗 -->
- <el-dialog :close-on-click-modal="false" title="作废事件" :visible.sync="cancelEventDialogVisible" width="500px">
- <el-form
- ref="cancelEventForm"
- label-width="100px"
- :model="cancelEventForm"
- :rules="cancelEventRules"
- size="small">
- <el-form-item label="作废原因" prop="cancelReason">
- <el-input v-model="cancelEventForm.cancelReason" placeholder="请输入作废原因..." :rows="4" type="textarea" />
- </el-form-item>
- <el-form-item label="附件上传">
- <el-upload
- action=""
- :auto-upload="false"
- :file-list="cancelEventFileList"
- :limit="5"
- :multiple="true"
- :on-change="handleCancelEventFileChange"
- :on-remove="handleCancelEventFileRemove">
- <el-button size="small" type="primary">选择文件</el-button>
- <div slot="tip" class="el-upload__tip">最多上传5个文件,单个文件不超过20MB</div>
- </el-upload>
- </el-form-item>
- </el-form>
- <div slot="footer">
- <el-button size="small" @click="handleCancelEventDialogCancel">取消</el-button>
- <el-button :loading="submitLoading" size="small" type="primary" @click="handleSaveCancelEvent">确定</el-button>
- </div>
- </el-dialog>
- <!-- 项目负责人指派/改派弹窗 -->
- <delivery-project-assign
- :data="assignDialogData"
- :visible.sync="assignDialogVisible"
- @success="handleAssignSuccess" />
- <!-- 项目信息弹窗 -->
- <project-info-dialog :project-id="selectedProjectId" :visible.sync="projectInfoDialogVisible" />
- <!-- 研发任务进展弹窗 -->
- <delivery-project-event-progress :event="progressEventRow" :visible.sync="progressDialogVisible" />
- </div>
- </template>
- <script>
- import deliveryProjectApi from '@/api/devops/deliveryProject'
- import deliveryProjectEventApi from '@/api/devops/deliveryProjectEvent'
- import userApi from '@/api/system/user'
- import { mapGetters } from 'vuex'
- import { DEVOPS_DEV_DEPT_ID } from '@/config/devops.config'
- import DeliveryProjectEventEdit from './components/DeliveryProjectEventEdit'
- import DeliveryProjectEventDetail from './components/DeliveryProjectEventDetail'
- import DeliveryProjectEventProgress from './components/DeliveryProjectEventProgress'
- import DeliveryProjectAssign from './components/DeliveryProjectAssign'
- import ProjectInfoDialog from '../components/ProjectInfoDialog'
- import { parseTime } from '@/utils'
- import debounce from 'lodash/debounce'
- import { uploadFileToRichtextServer } from '@/utils/richtextUpload'
- import { sanitizeHtml } from '@/utils/safeHtml'
- import { deliveryEventStatusTagTypes, getTagType } from '@/config/devopsTagTypes'
- import dictApi from '@/api/system/dict'
- export default {
- name: 'DeliveryProject',
- components: {
- DeliveryProjectEventEdit,
- DeliveryProjectEventDetail,
- DeliveryProjectEventProgress,
- DeliveryProjectAssign,
- ProjectInfoDialog,
- },
- data() {
- return {
- queryForm: {
- pageNum: 1,
- pageSize: 20,
- projectId: '',
- deliveryEventTitle: '',
- deliveryEventDesc: '',
- deliveryEventType: [],
- deliveryEventStatus: [],
- deliveryEventResult: [],
- opsUserName: '',
- feedbackReporter: '',
- feedbackDateRange: [],
- completeDateRange: [],
- sortFields: [],
- },
- showAdvanced: false,
- opsUserOptions: [],
- opsUsersLoading: false,
- tableData: [],
- total: 0,
- loading: false,
- submitLoading: false,
- sidebarCollapsed: false,
- showSidebarFilters: true,
- projectSearch: '',
- projectPageNum: 1,
- projectPageSize: 20,
- projectTotal: 0,
- projectLoading: false,
- projectHasMore: true,
- projectStatusFilter: 'delivering',
- selectedProject: '',
- projects: [{ id: '', name: '全部' }],
- editDialogVisible: false,
- editData: null,
- productLineDict: [],
- projectStatusOptions: [],
- deliveryEventTypeOptions: [],
- deliveryEventStatusOptions: [],
- deliveryEventResultOptions: [],
- feedbackSourceOptions: [],
- onSiteOptions: [],
- detailVisible: false,
- detailMode: 'view',
- currentRow: null,
- showCloseEventDialog: false,
- // 作废事件弹窗相关数据
- cancelEventDialogVisible: false,
- cancelEventForm: {
- cancelReason: '',
- },
- cancelEventRules: {
- cancelReason: [{ required: true, message: '请输入作废原因', trigger: 'blur' }],
- },
- cancelEventFileList: [],
- cancelEventUploadFiles: [],
- // 项目负责人指派/改派弹窗相关数据
- assignDialogVisible: false,
- assignDialogData: null,
- // 项目信息弹窗
- projectInfoDialogVisible: false,
- selectedProjectId: '',
- // 进展弹窗相关数据
- progressDialogVisible: false,
- progressEventRow: null,
- }
- },
- computed: {
- ...mapGetters({
- roleKeys: 'user/roleKeys',
- }),
- canManageDelivery() {
- return (
- this.roleKeys.includes('ResearchAndDevelopmentDirector') ||
- this.roleKeys.includes('ResearchAndDevelopmentSupervisor')
- )
- },
- filteredProjects() {
- const statusFilter = this.projectStatusFilter
- return this.projects.filter((project) => {
- if (project.id === '') return true
- // 状态筛选
- if (statusFilter) {
- let statusList = []
- if (statusFilter === 'pending') {
- statusList = ['10']
- } else if (statusFilter === 'delivering') {
- statusList = ['20', '30', '40']
- } else if (statusFilter === 'delivered') {
- statusList = ['50']
- } else {
- statusList = statusFilter.split(',')
- }
- if (!statusList.includes(project.status)) {
- return false
- }
- } else {
- if (project.status === '90') {
- return false
- }
- }
- return true
- })
- },
- allProjectOption() {
- return this.filteredProjects.find((project) => project.id === '')
- },
- projectCards() {
- return this.filteredProjects.filter((project) => project.id !== '')
- },
- },
- created() {
- this.getOptions()
- this.fetchData()
- this.onProjectSearchDebounced = debounce(() => {
- this.projectPageNum = 1
- this.projects = [{ id: '', name: '全部' }]
- this.projectHasMore = true
- this.fetchProjectList()
- }, 300)
- },
- mounted() {
- this.$nextTick(() => {
- const el = this.$refs.projectList
- if (!el) return
- this._onProjectListScroll = debounce(() => {
- if (!this.projectHasMore || this.projectLoading) return
- const { scrollHeight, scrollTop, clientHeight } = el
- if (scrollHeight - scrollTop - clientHeight <= 80) {
- this.projectPageNum++
- this.fetchProjectList()
- }
- }, 200)
- el.addEventListener('scroll', this._onProjectListScroll)
- })
- },
- beforeDestroy() {
- if (this._onProjectListScroll) {
- this._onProjectListScroll.cancel()
- const el = this.$refs.projectList
- if (el) el.removeEventListener('scroll', this._onProjectListScroll)
- }
- if (this.onProjectSearchDebounced && this.onProjectSearchDebounced.cancel) {
- this.onProjectSearchDebounced.cancel()
- }
- },
- methods: {
- getTagType,
- getDeliveryEventStatusTagType(status) {
- return getTagType(deliveryEventStatusTagTypes, status, 'info')
- },
- parseTime,
- sanitizeHtml,
- getOptions() {
- dictApi
- .getDictDataByTypes([
- 'sys_product_line',
- 'delivery_project_status',
- 'delivery_event_type',
- 'delivery_event_status',
- 'delivery_event_result',
- 'feedback_source',
- 'sys_yes_no',
- ])
- .then((res) => {
- const dicts = res.data || {}
- this.productLineDict = (dicts.sys_product_line && dicts.sys_product_line.values) || []
- this.projectStatusOptions = (dicts.delivery_project_status && dicts.delivery_project_status.values) || []
- this.deliveryEventTypeOptions = (dicts.delivery_event_type && dicts.delivery_event_type.values) || []
- this.deliveryEventStatusOptions = (dicts.delivery_event_status && dicts.delivery_event_status.values) || []
- this.deliveryEventResultOptions = (dicts.delivery_event_result && dicts.delivery_event_result.values) || []
- this.feedbackSourceOptions = (dicts.feedback_source && dicts.feedback_source.values) || []
- this.onSiteOptions = (dicts.sys_yes_no && dicts.sys_yes_no.values) || []
- })
- .catch((err) => console.log(err))
- },
- // 获取项目列表(支持滚动分页)
- async fetchProjectList() {
- if (this.projectLoading) return
- this.projectLoading = true
- try {
- const params = {
- pageNum: this.projectPageNum,
- pageSize: this.projectPageSize,
- productLine: '10,20,30,40,50,60',
- sortField: 'contract_no',
- sortOrder: 'desc',
- attribute9: '10',
- }
- // 状态筛选
- if (this.projectStatusFilter) {
- const statusMap = {
- pending: '10',
- delivering: '20,30,40',
- delivered: '50',
- }
- params.projectStatus = statusMap[this.projectStatusFilter] || this.projectStatusFilter
- } else {
- params.projectStatus = '10,20,30,40,50'
- }
- const keyword = this.projectSearch.trim()
- if (keyword) {
- params.keyWords = keyword
- }
- const res = await deliveryProjectApi.getDelegatedProjectList(params)
- if (res.code === 200 && res.data && res.data.list) {
- const projectList = (res.data?.list || []).map((item) => ({
- id: String(item.id),
- name: item.projectName || item.project_name,
- contractNo: item.contractNo || item.contract_no,
- productLine: item.productLine || item.product_line,
- salesOwner: item.salesUserName || item.sales_user_name,
- deliveryOwner: item.deliveryUserName || item.delivery_user_name,
- deliveryUserId: item.deliveryUserId || item.delivery_user_id,
- status: String(item.projectStatus || item.project_status),
- }))
- const total = res.data?.total || 0
- projectList.sort((a, b) => (b.contractNo || '').localeCompare(a.contractNo || ''))
- if (this.projectPageNum === 1) {
- this.projects = [{ id: '', name: '全部' }, ...projectList]
- } else {
- this.projects = this.projects.concat(projectList)
- }
- this.projectTotal = total
- this.projectHasMore = this.projects.length - 1 < total
- // 如果选中的项目不在当前列表中,清除选中
- if (this.selectedProject && !this.projects.some((project) => project.id === this.selectedProject)) {
- this.selectedProject = ''
- this.queryForm.projectId = ''
- this.queryForm.pageNum = 1
- this.fetchEventData()
- }
- }
- } catch (error) {
- console.error('获取项目列表失败:', error)
- if (this.projectPageNum === 1) {
- this.projects = [{ id: '', name: '全部' }]
- }
- } finally {
- this.projectLoading = false
- // 内容未填满容器时自动加载下一页
- this.$nextTick(() => {
- const el = this.$refs.projectList
- if (el && this.projectHasMore && !this.projectLoading) {
- if (el.scrollHeight <= el.clientHeight) {
- this.projectPageNum++
- this.fetchProjectList()
- }
- }
- })
- }
- },
- // 获取事件列表数据
- async fetchEventData() {
- this.loading = true
- try {
- const params = {
- pageNum: this.queryForm.pageNum,
- pageSize: this.queryForm.pageSize,
- projectId: this.queryForm.projectId ? parseInt(this.queryForm.projectId) : 0,
- deliveryEventTitle: this.queryForm.deliveryEventTitle,
- deliveryEventDesc: this.queryForm.deliveryEventDesc,
- deliveryEventType: this.queryForm.deliveryEventType,
- deliveryEventStatus: this.queryForm.deliveryEventStatus,
- deliveryEventResult: this.queryForm.deliveryEventResult,
- opsUserName: this.queryForm.opsUserName,
- feedbackReporter: this.queryForm.feedbackReporter,
- sortFields: this.queryForm.sortFields,
- }
- // 处理反馈时间范围
- if (this.queryForm.feedbackDateRange && this.queryForm.feedbackDateRange.length === 2) {
- params.feedbackDateStart = this.queryForm.feedbackDateRange[0] + ' 00:00:00'
- params.feedbackDateEnd = this.queryForm.feedbackDateRange[1] + ' 23:59:59'
- }
- // 处理处理时间范围
- if (this.queryForm.completeDateRange && this.queryForm.completeDateRange.length === 2) {
- params.completeTimeStart = this.queryForm.completeDateRange[0] + ' 00:00:00'
- params.completeTimeEnd = this.queryForm.completeDateRange[1] + ' 23:59:59'
- }
- const res = await deliveryProjectEventApi.getList(params)
- if (res.code === 200 && res.data) {
- this.tableData = res.data?.list || []
- this.total = res.data?.total || 0
- } else {
- this.tableData = []
- this.total = 0
- }
- } catch (error) {
- console.error('获取事件数据失败:', error)
- this.tableData = []
- this.total = 0
- this.$message.error('获取事件数据失败')
- } finally {
- this.loading = false
- }
- },
- // 获取列表数据(项目+事件)
- async fetchData() {
- await this.fetchProjectList()
- await this.fetchEventData()
- },
- // 搜索
- handleSearch() {
- this.queryForm.pageNum = 1
- this.fetchEventData()
- },
- // 重置
- handleReset() {
- this.queryForm = {
- pageNum: 1,
- pageSize: 20,
- projectId: this.queryForm.projectId,
- deliveryEventTitle: '',
- deliveryEventDesc: '',
- deliveryEventType: [],
- deliveryEventStatus: [],
- deliveryEventResult: [],
- opsUserName: [],
- feedbackReporter: '',
- feedbackDateRange: [],
- completeDateRange: [],
- sortFields: [],
- }
- this.fetchEventData()
- },
- // 新增
- handleAdd() {
- this.editData = null
- this.editDialogVisible = true
- },
- // 导出
- handleExport() {
- this.$message.info('导出功能开发中...')
- },
- // 编辑
- async handleEdit(row) {
- // 校验事件状态
- const status = row.deliveryEventStatus || row.delivery_event_status
- if (String(status) === '30') {
- this.$message.warning('该事件已关闭,不允许编辑')
- return
- }
- if (String(status) === '90') {
- this.$message.warning('该事件已作废,不允许编辑')
- return
- }
- try {
- const res = await deliveryProjectEventApi.getById(row.id)
- if (res.code === 200 && res.data) {
- const latestStatus = res.data.deliveryEventStatus || res.data.delivery_event_status
- if (String(latestStatus) === '30') {
- this.$message.warning('该事件已关闭,不允许编辑')
- this.fetchEventData()
- return
- }
- if (String(latestStatus) === '90') {
- this.$message.warning('该事件已作废,不允许编辑')
- this.fetchEventData()
- return
- }
- this.editData = res.data
- } else {
- this.editData = row
- }
- } catch (error) {
- console.error('获取事件详情失败:', error)
- this.editData = row
- } finally {
- this.editDialogVisible = true
- }
- },
- // 查看详情
- handleView(row) {
- this.currentRow = row
- this.detailMode = 'view'
- this.detailVisible = true
- },
- // 处理事件 - 打开详情弹窗(处理模式,可以编辑)
- async handleProcess(row) {
- // 校验事件状态
- const status = row.deliveryEventStatus || row.delivery_event_status
- if (String(status) === '30') {
- this.$message.warning('该事件已关闭,无法进行操作')
- return
- }
- if (String(status) === '90') {
- this.$message.warning('该事件已作废,无法进行操作')
- return
- }
- // 获取最新事件详情
- try {
- const res = await deliveryProjectEventApi.getById(row.id)
- if (res.code === 200 && res.data) {
- const latestStatus = res.data.deliveryEventStatus || res.data.delivery_event_status
- if (String(latestStatus) === '30') {
- this.$message.warning('该事件已关闭,无法进行操作')
- this.fetchEventData()
- return
- }
- if (String(latestStatus) === '90') {
- this.$message.warning('该事件已作废,无法进行操作')
- this.fetchEventData()
- return
- }
- this.currentRow = res.data
- } else {
- this.currentRow = row
- }
- } catch (error) {
- console.error('获取事件详情失败:', error)
- this.currentRow = row
- }
- this.detailMode = 'process'
- this.showCloseEventDialog = false
- this.detailVisible = true
- },
- // 是否显示进展按钮(特定事件类型)
- showProgressButton(row) {
- const eventType = String(row.deliveryEventType || row.delivery_event_type)
- return ['31', '32', '33', '35', '38', '40', '41'].includes(eventType)
- },
- // 查看研发任务进展
- handleProgress(row) {
- this.progressEventRow = row
- this.progressDialogVisible = true
- },
- // 作废事件 - 直接弹出作废事件弹窗
- async handleCancelEvent(row) {
- // 校验事件状态
- const status = row.deliveryEventStatus || row.delivery_event_status
- if (String(status) === '30') {
- this.$message.warning('该事件已关闭,无法进行操作')
- return
- }
- if (String(status) === '90') {
- this.$message.warning('该事件已作废,无法进行操作')
- return
- }
- // 获取最新事件详情
- try {
- const res = await deliveryProjectEventApi.getById(row.id)
- if (res.code === 200 && res.data) {
- const latestStatus = res.data.deliveryEventStatus || res.data.delivery_event_status
- if (String(latestStatus) === '30') {
- this.$message.warning('该事件已关闭,无法进行操作')
- this.fetchEventData()
- return
- }
- if (String(latestStatus) === '90') {
- this.$message.warning('该事件已作废,无法进行操作')
- this.fetchEventData()
- return
- }
- this.currentRow = res.data
- } else {
- this.currentRow = row
- }
- } catch (error) {
- console.error('获取事件详情失败:', error)
- this.currentRow = row
- }
- // 直接显示作废事件弹窗
- this.cancelEventDialogVisible = true
- },
- // 删除
- handleDelete(row) {
- this.$confirm('确认删除该记录?', '提示', {
- confirmButtonText: '确定',
- cancelButtonText: '取消',
- type: 'warning',
- })
- .then(async () => {
- try {
- const res = await deliveryProjectEventApi.deleteByIds([row.id])
- if (res.code === 200) {
- this.$message.success('删除成功')
- this.fetchEventData()
- } else {
- this.$message.error(res.msg || '删除失败')
- }
- } catch (error) {
- console.error('删除失败:', error)
- this.$message.error('删除失败')
- }
- })
- .catch(() => {})
- },
- // 行点击
- handleRowClick(row) {
- this.handleView(row)
- },
- // 分页
- handleSizeChange(size) {
- this.queryForm.pageSize = size
- this.fetchData()
- },
- handleCurrentChange(page) {
- this.queryForm.pageNum = page
- this.fetchData()
- },
- // 侧边栏
- toggleSidebar() {
- this.sidebarCollapsed = !this.sidebarCollapsed
- },
- toggleSidebarFilters() {
- this.showSidebarFilters = !this.showSidebarFilters
- },
- selectProject(projectId, project) {
- // 如果点击的是具体项目卡片且状态为待分配(10),且用户有研发总监/研发主管角色,弹出指派弹窗
- if (project && String(project.status) === '10' && this.canManageDelivery) {
- this.handleAssign(project)
- return
- }
- this.selectedProject = projectId
- this.queryForm.projectId = projectId || ''
- this.queryForm.pageNum = 1
- this.fetchEventData()
- },
- // 处理项目负责人指派(待指派状态)
- handleAssign(project) {
- this.assignDialogData = {
- projectId: project.id,
- projectName: project.name,
- isReassign: false,
- currentDeliveryUserId: null,
- currentDeliveryUserName: null,
- }
- this.assignDialogVisible = true
- },
- // 处理项目负责人改派
- handleReassign(project) {
- this.assignDialogData = {
- projectId: project.id,
- projectName: project.name,
- isReassign: true,
- currentDeliveryUserId: project.deliveryUserId,
- currentDeliveryUserName: project.deliveryOwner,
- }
- this.assignDialogVisible = true
- },
- // 指派/改派成功回调
- handleAssignSuccess() {
- this.fetchProjectList()
- },
- // 查看项目信息
- handleViewProjectInfo(project) {
- if (!project || !project.id) return
- this.selectedProjectId = project.id
- this.projectInfoDialogVisible = true
- },
- formatEventTime(time) {
- return time ? parseTime(time, '{y}-{m}-{d} {h}:{i}') : '-'
- },
- // 产品线标签
- getProductLineLabel(productLine) {
- const label = this.selectDictLabel(this.productLineDict, productLine)
- return label || productLine || '-'
- },
- // 状态样式
- getStatusType(status) {
- const map = {
- 10: 'info',
- 20: 'primary',
- 30: 'warning',
- 40: 'success',
- 50: 'success',
- 90: 'danger',
- }
- return map[status] || 'info'
- },
- // 是否可以删除
- canDelete(status) {
- return ['10', '90'].includes(status)
- },
- // 刷新事件列表
- handleRefreshEvent() {
- this.fetchEventData()
- },
- // 远程搜索负责人
- async remoteFetchOpsUsers(search) {
- this.opsUsersLoading = true
- try {
- const payload = { deptId: DEVOPS_DEV_DEPT_ID, pageNum: 1, pageSize: 999 }
- if (search) payload.keyWords = search
- const res = await userApi.getList(payload)
- const list = res.data?.list || []
- this.opsUserOptions = list.map((u) => ({
- value: u.userId ?? u.user_id ?? u.id ?? null,
- label: u.nickName ?? u.nick_name ?? u.name ?? '',
- }))
- } catch (error) {
- console.error('获取负责人列表失败:', error)
- this.opsUserOptions = []
- } finally {
- this.opsUsersLoading = false
- }
- },
- handleOpsUserVisibleChange(visible) {
- if (visible && !this.opsUsersLoading && !this.opsUserOptions.length) {
- this.remoteFetchOpsUsers('')
- }
- },
- // 多列排序:点击表头切换 无→升序→降序→无
- toggleSort(field) {
- const idx = this.queryForm.sortFields.findIndex((s) => s.field === field)
- if (idx === -1) {
- if (this.queryForm.sortFields.length >= 3) return
- this.queryForm.sortFields.push({ field, order: 'asc' })
- } else if (this.queryForm.sortFields[idx].order === 'asc') {
- this.queryForm.sortFields[idx].order = 'desc'
- } else {
- this.queryForm.sortFields.splice(idx, 1)
- }
- this.fetchEventData()
- },
- getSortState(field) {
- const sf = this.queryForm.sortFields.find((s) => s.field === field)
- return sf ? sf.order : ''
- },
- getSortPriority(field) {
- const idx = this.queryForm.sortFields.findIndex((s) => s.field === field)
- return idx === -1 ? '' : String(idx + 1)
- },
- renderSortableHeader(label, field) {
- const vm = this
- return function (h) {
- const state = vm.getSortState(field)
- const prio = vm.getSortPriority(field)
- return h('div', { class: 'sortable-header', on: { click: () => vm.toggleSort(field) } }, [
- h('span', { class: 'sortable-header-label' }, label),
- h('span', { class: 'sort-arrows' }, [
- h('i', { class: ['el-icon-caret-top', { 'sort-active': state === 'asc' }] }),
- h('i', { class: ['el-icon-caret-bottom', { 'sort-active': state === 'desc' }] }),
- ]),
- prio ? h('span', { class: 'sort-priority' }, prio) : null,
- ])
- }
- },
- // 项目状态筛选变化
- handleProjectStatusChange() {
- this.queryForm.pageNum = 1
- this.projectPageNum = 1
- this.projects = [{ id: '', name: '全部' }]
- this.projectHasMore = true
- this.fetchProjectList()
- },
- // 作废事件弹窗 - 文件上传相关
- handleCancelEventFileChange(file, fileList) {
- this.cancelEventFileList = fileList
- this.cancelEventUploadFiles = fileList.filter((f) => f.raw).map((f) => f.raw)
- },
- handleCancelEventFileRemove(file, fileList) {
- this.cancelEventFileList = fileList
- this.cancelEventUploadFiles = fileList.filter((f) => f.raw).map((f) => f.raw)
- },
- async uploadCancelEventAttachments() {
- const uploadedFiles = []
- for (const file of this.cancelEventUploadFiles) {
- try {
- const result = await uploadFileToRichtextServer(file)
- uploadedFiles.push({
- fileName: result.name || file.name,
- fileUrl: result.url,
- fileType: file.type || 'application/octet-stream',
- })
- } catch (err) {
- this.$message.error(`文件 ${file.name} 上传失败`)
- console.error(err)
- throw err
- }
- }
- return uploadedFiles
- },
- // 取消作废事件弹窗
- handleCancelEventDialogCancel() {
- this.cancelEventDialogVisible = false
- this.cancelEventForm = {
- cancelReason: '',
- }
- this.cancelEventFileList = []
- this.cancelEventUploadFiles = []
- },
- // 保存作废事件
- async handleSaveCancelEvent() {
- this.$refs.cancelEventForm.validate(async (valid) => {
- if (!valid) return
- this.submitLoading = true
- try {
- // 上传附件
- const uploadedAttachments = await this.uploadCancelEventAttachments()
- // 调用作废接口
- const cancelData = {
- id: this.currentRow.id,
- cancelReason: this.cancelEventForm.cancelReason,
- attachments: uploadedAttachments,
- }
- const cancelRes = await deliveryProjectEventApi.cancel(cancelData)
- if (cancelRes.code === 200) {
- this.$message.success('事件作废成功')
- this.handleCancelEventDialogCancel()
- this.fetchEventData()
- } else {
- this.$message.error(cancelRes.msg || '作废事件失败')
- }
- } catch (error) {
- console.error('作废事件失败:', error)
- this.$message.error('作废事件失败')
- } finally {
- this.submitLoading = false
- }
- })
- },
- },
- }
- </script>
- <style lang="scss" scoped>
- .delivery-project-page {
- box-sizing: border-box;
- display: flex;
- flex-direction: column;
- min-height: 0;
- padding: 4px;
- height: calc(100vh - 122px);
- background: #f5f7fa;
- overflow: hidden;
- }
- .query-form-container {
- margin-bottom: 4px;
- flex-shrink: 0;
- padding: 8px 12px;
- background: #fff;
- border-radius: 6px;
- box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
- }
- .query-form-basic {
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- gap: 16px;
- .query-form-fields {
- flex: 1;
- min-width: 0;
- }
- .query-form-actions {
- display: flex;
- align-items: center;
- justify-content: flex-end;
- flex-shrink: 0;
- min-height: 32px;
- white-space: nowrap;
- }
- ::v-deep .el-form {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- row-gap: 2px;
- }
- ::v-deep .el-form-item {
- margin-bottom: 2px;
- margin-right: 20px;
- }
- ::v-deep .el-form-item__label {
- padding-right: 8px;
- font-size: 13px;
- color: #606266;
- }
- ::v-deep .el-divider--vertical {
- margin: 0 12px;
- }
- }
- .query-form-advanced {
- margin-top: 8px;
- padding-top: 8px;
- border-top: 1px dashed #e4e7ed;
- ::v-deep .el-form {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- row-gap: 2px;
- }
- ::v-deep .el-form-item {
- margin-bottom: 2px;
- margin-right: 20px;
- }
- ::v-deep .el-form-item__label {
- padding-right: 8px;
- font-size: 13px;
- color: #606266;
- }
- }
- .table-container {
- display: flex;
- flex: 1;
- flex-direction: column;
- min-width: 0;
- min-height: 0;
- overflow: hidden;
- background: #fff;
- border-radius: 4px;
- padding: 6px 8px;
- box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
- }
- .table-scroll-wrapper {
- display: flex;
- flex: 1;
- min-width: 0;
- min-height: 0;
- overflow: hidden;
- }
- .delivery-project-table {
- flex: 1;
- min-width: 0;
- }
- .pagination-wrapper {
- margin-top: auto;
- padding-top: 4px;
- display: flex;
- justify-content: flex-end;
- }
- .pagination-wrapper ::v-deep .el-pagination {
- padding: 0;
- line-height: 32px;
- }
- .event-title-text {
- display: inline-block;
- max-width: 100%;
- color: #303133;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .project-sidebar {
- display: flex;
- flex-direction: column;
- width: 280px;
- min-height: 0;
- background: #fff;
- border-radius: 4px;
- padding: 8px;
- flex-shrink: 0;
- box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
- transition: width 0.2s ease, padding 0.2s ease;
- overflow: hidden;
- &.collapsed {
- width: 44px;
- padding: 8px 4px;
- align-items: center;
- }
- }
- .sort-select,
- .search-type-select {
- width: 120px;
- }
- .search-input {
- width: 200px;
- }
- .project-status-filter {
- margin-bottom: 8px;
- ::v-deep .el-radio-group {
- display: flex;
- width: 100%;
- }
- ::v-deep .el-radio-button {
- flex: 1;
- .el-radio-button__inner {
- width: 100%;
- padding: 8px 0;
- font-size: 12px;
- }
- }
- }
- .project-status-tag {
- flex-shrink: 0;
- padding: 2px 8px;
- font-size: 11px;
- color: #fff;
- background: #909399;
- border-radius: 999px;
- white-space: nowrap;
- &--10 {
- background: #909399;
- }
- &--20 {
- background: #409eff;
- }
- &--30 {
- background: #e6a23c;
- }
- &--40 {
- background: #67c23a;
- }
- &--50 {
- background: #1f9d55;
- }
- &--90 {
- background: #f56c6c;
- }
- }
- .main-content {
- display: flex;
- flex: 1;
- gap: 4px;
- min-height: 0;
- overflow: hidden;
- }
- .sidebar-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 6px;
- }
- .sidebar-title-group {
- display: flex;
- align-items: center;
- gap: 6px;
- }
- .sidebar-actions {
- display: flex;
- align-items: center;
- }
- .project-sidebar.collapsed .sidebar-header {
- justify-content: center;
- margin-bottom: 0;
- width: 100%;
- }
- .project-sidebar.collapsed .project-search,
- .project-sidebar.collapsed .project-list {
- display: none;
- }
- .sidebar-title {
- font-size: 14px;
- font-weight: 500;
- color: #303133;
- }
- .sidebar-collapsed-label {
- display: flex;
- flex: 1;
- align-items: center;
- justify-content: flex-start;
- padding-top: 12px;
- writing-mode: vertical-rl;
- text-orientation: upright;
- white-space: nowrap;
- font-size: 14px;
- font-weight: 500;
- color: #606266;
- letter-spacing: 2px;
- user-select: none;
- }
- .collapse-trigger {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 24px;
- height: 24px;
- padding: 0;
- background: #f5f7fa;
- border: none;
- border-radius: 4px;
- cursor: pointer;
- color: #909399;
- transition: all 0.2s;
- }
- .sidebar-action-btn {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 24px;
- height: 24px;
- padding: 0;
- background: #f5f7fa;
- border: none;
- border-radius: 4px;
- cursor: pointer;
- color: #909399;
- transition: all 0.2s;
- &:hover {
- background: #ecf5ff;
- color: #409eff;
- }
- &.active {
- background: #ecf5ff;
- color: #409eff;
- }
- i {
- font-size: 12px;
- line-height: 1;
- }
- }
- .project-search {
- margin-bottom: 6px;
- }
- .project-list {
- flex: 1;
- min-height: 0;
- overflow-y: auto;
- padding-right: 2px;
- }
- .project-list-loading,
- .project-list-end {
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 12px 0;
- font-size: 13px;
- color: #909399;
- }
- .project-item {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 10px 12px;
- cursor: pointer;
- border-radius: 12px;
- transition: all 0.2s;
- border: 1px solid transparent;
- &:hover {
- background: #f8fbff;
- }
- &.active {
- background: linear-gradient(180deg, #eef6ff 0%, #e6f0ff 100%);
- border-color: #bfd9ff;
- box-shadow: 0 8px 18px rgba(64, 158, 255, 0.12);
- }
- }
- .project-item--all {
- margin-bottom: 8px;
- background: linear-gradient(135deg, #f8fbff 0%, #f3f7fd 100%);
- border-color: #e4edf7;
- }
- .project-overview-label {
- font-size: 15px;
- font-weight: 600;
- color: #303133;
- }
- .project-overview-desc {
- font-size: 12px;
- color: #909399;
- margin-top: 2px;
- }
- .project-card {
- position: relative;
- display: flex;
- flex-direction: column;
- justify-content: center;
- min-height: 98px;
- padding: 8px 10px;
- border-radius: 14px;
- border: 1px solid #ebeef5;
- background: linear-gradient(180deg, #ffffff 0%, #fbfcfe 100%);
- box-shadow: 0 8px 18px rgba(15, 23, 42, 0.05);
- cursor: pointer;
- transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
- &:not(:last-child) {
- margin-bottom: 8px;
- }
- &:hover {
- transform: translateY(-1px);
- border-color: #d5e7ff;
- box-shadow: 0 12px 22px rgba(64, 158, 255, 0.12);
- }
- &.active {
- border-color: #8bb8ff;
- background: linear-gradient(180deg, #eff6ff 0%, #f7fbff 100%);
- box-shadow: 0 14px 26px rgba(64, 158, 255, 0.16);
- }
- }
- .project-card-top {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 8px;
- margin-bottom: 6px;
- }
- .project-card-tags {
- display: flex;
- align-items: center;
- gap: 6px;
- flex: 1;
- min-width: 0;
- }
- .project-contract {
- max-width: 108px;
- padding: 2px 8px;
- font-size: 11px;
- color: #5f6b7a;
- background: #f2f6fc;
- border-radius: 999px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- &.clickable {
- cursor: pointer;
- transition: all 0.2s;
- &:hover {
- color: #409eff;
- background: #ecf5ff;
- }
- }
- }
- .project-line-tag {
- flex-shrink: 0;
- padding: 2px 8px;
- font-size: 11px;
- color: #409eff;
- background: #ecf5ff;
- border-radius: 999px;
- white-space: nowrap;
- }
- .project-card-title {
- margin-bottom: 8px;
- font-size: 14px;
- font-weight: 600;
- line-height: 1.3;
- color: #303133;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .project-card-meta {
- display: flex;
- align-items: center;
- gap: 6px;
- }
- .project-card-meta-item {
- display: flex;
- align-items: center;
- flex: 1;
- gap: 6px;
- min-width: 0;
- padding: 4px 8px;
- background: #f7f9fc;
- border-radius: 10px;
- }
- .project-card-meta-icon {
- flex-shrink: 0;
- font-size: 14px;
- color: #409eff;
- }
- .project-card-meta-icon--delivery {
- color: #67c23a;
- }
- .reassign-btn {
- flex-shrink: 0;
- font-size: 12px;
- color: #409eff;
- cursor: pointer;
- margin-left: 4px;
- padding: 2px;
- border-radius: 50%;
- transition: all 0.2s;
- &:hover {
- background: #ecf5ff;
- transform: rotate(180deg);
- }
- }
- .project-card-meta-value {
- flex: 1;
- min-width: 0;
- font-size: 13px;
- color: #606266;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .project-card-check {
- position: absolute;
- top: 12px;
- right: 12px;
- font-size: 14px;
- color: #409eff;
- }
- .el-icon-check {
- font-size: 12px;
- color: #409eff;
- }
- ::v-deep .sortable-header {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- cursor: pointer;
- user-select: none;
- white-space: nowrap;
- .sort-arrows {
- display: inline-flex;
- flex-direction: column;
- line-height: 1;
- i {
- font-size: 10px;
- color: #c0c4cc;
- transition: color 0.2s;
- &.sort-active {
- color: #409eff;
- }
- }
- }
- .sort-priority {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 16px;
- height: 16px;
- border-radius: 50%;
- background: #409eff;
- color: #fff;
- font-size: 10px;
- font-weight: 700;
- line-height: 1;
- margin-left: 2px;
- }
- }
- .query-select--owner {
- width: 180px;
- }
- </style>
|