|
|
@@ -0,0 +1,977 @@
|
|
|
+<template>
|
|
|
+ <div class="delivery-project-page">
|
|
|
+ <!-- 工具栏 -->
|
|
|
+ <div class="toolbar">
|
|
|
+ <el-button icon="el-icon-plus" size="small" type="primary" @click="handleAdd">新增</el-button>
|
|
|
+ <div class="toolbar-right">
|
|
|
+ <span class="sort-label">排序</span>
|
|
|
+ <el-select v-model="queryForm.sortBy" class="sort-select" size="small">
|
|
|
+ <el-option label="创建时间" value="createdTime" />
|
|
|
+ <el-option label="项目名称" value="projectName" />
|
|
|
+ </el-select>
|
|
|
+ <el-select v-model="queryForm.searchType" class="search-type-select" size="small">
|
|
|
+ <el-option label="项目名称" value="projectName" />
|
|
|
+ <el-option label="合同编号" value="contractNo" />
|
|
|
+ <el-option label="客户名称" value="custName" />
|
|
|
+ </el-select>
|
|
|
+ <el-input
|
|
|
+ v-model="queryForm.keyWords"
|
|
|
+ class="search-input"
|
|
|
+ clearable
|
|
|
+ placeholder=""
|
|
|
+ prefix-icon="el-icon-search"
|
|
|
+ size="small"
|
|
|
+ @keyup.enter.native="handleSearch" />
|
|
|
+ <el-button icon="el-icon-search" size="small" type="primary" @click="handleSearch">查询</el-button>
|
|
|
+ <el-button icon="el-icon-refresh-right" size="small" @click="handleReset">重置</el-button>
|
|
|
+ <el-button icon="el-icon-download" size="small" type="success" @click="handleExport">导出</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 主体内容 -->
|
|
|
+ <div class="main-content">
|
|
|
+ <!-- 左侧项目筛选 -->
|
|
|
+ <div :class="['project-sidebar', { collapsed: sidebarCollapsed }]">
|
|
|
+ <div class="sidebar-header">
|
|
|
+ <span v-if="!sidebarCollapsed" class="sidebar-title">项目列表</span>
|
|
|
+ <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 v-if="sidebarCollapsed" class="sidebar-collapsed-label">项目列表</div>
|
|
|
+ <template v-if="!sidebarCollapsed">
|
|
|
+ <el-input
|
|
|
+ v-model="projectSearch"
|
|
|
+ class="project-search"
|
|
|
+ clearable
|
|
|
+ placeholder="搜索"
|
|
|
+ prefix-icon="el-icon-search"
|
|
|
+ size="small" />
|
|
|
+ <div 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 class="project-overview-count">
|
|
|
+ <span>{{ allProjectOption.count }}</span>
|
|
|
+ <span class="project-overview-count-label">项</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ v-for="project in projectCards"
|
|
|
+ :key="project.id"
|
|
|
+ :class="['project-card', { active: selectedProject === project.id }]"
|
|
|
+ @click="selectProject(project.id)">
|
|
|
+ <div class="project-card-top">
|
|
|
+ <div class="project-card-tags">
|
|
|
+ <span class="project-contract">{{ project.contractNo || '-' }}</span>
|
|
|
+ <span class="project-line-tag">{{ getProductLineLabel(project.productLine) }}</span>
|
|
|
+ </div>
|
|
|
+ <span class="project-card-count">{{ project.count }}项</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>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <i v-if="selectedProject === project.id" class="el-icon-check project-card-check" />
|
|
|
+ </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-click="handleRowClick">
|
|
|
+ <el-table-column align="center" type="index" width="50" />
|
|
|
+ <el-table-column label="事件标题" min-width="320" show-overflow-tooltip>
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <el-popover
|
|
|
+ placement="top-start"
|
|
|
+ popper-class="delivery-event-desc-popover"
|
|
|
+ trigger="click"
|
|
|
+ width="360">
|
|
|
+ <div class="event-desc-popover">
|
|
|
+ <div class="event-desc-popover__title">事件描述</div>
|
|
|
+ <div class="event-desc-popover__content">
|
|
|
+ {{ row.deliveryEventDesc || row.delivery_event_desc || '暂无事件描述' }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <span slot="reference" class="event-title-trigger" @click.stop>
|
|
|
+ {{ row.deliveryEventTitle || row.delivery_event_title || '-' }}
|
|
|
+ </span>
|
|
|
+ </el-popover>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="事件类型" width="120">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <el-tag size="small" type="info">
|
|
|
+ {{ getDeliveryEventTypeLabel(row.deliveryEventType || row.delivery_event_type) }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="事件状态" width="100">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <el-tag
|
|
|
+ size="small"
|
|
|
+ :type="getDeliveryEventStatusType(row.deliveryEventStatus || row.delivery_event_status)">
|
|
|
+ {{ getDeliveryEventStatusLabel(row.deliveryEventStatus || row.delivery_event_status) }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="事件结果" width="100">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ {{ getDeliveryEventResultLabel(row.deliveryEventResult || row.delivery_event_result) }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="负责人" show-overflow-tooltip width="110">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ {{ row.opsUserName || row.ops_user_name || '-' }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="处理时间" width="160">
|
|
|
+ <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 }">
|
|
|
+ {{ getOnSiteLabel(row.onSite || row.on_site) }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="反馈人" show-overflow-tooltip width="110">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ {{ row.feedbackReporter || row.feedback_reporter || '-' }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="反馈时间" width="160">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ {{ formatEventTime(row.feedbackDate || row.feedback_date) }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="反馈来源" width="100">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ {{ getFeedbackSourceLabel(row.feedbackSource || row.feedback_source) }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column fixed="right" header-align="center" label="操作" width="150">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <el-button size="mini" type="text" @click.stop="handleEdit(row)">编辑</el-button>
|
|
|
+ <el-button size="mini" type="text" @click.stop="handleView(row)">详情</el-button>
|
|
|
+ <el-button
|
|
|
+ v-if="canDelete(row.projectStatus)"
|
|
|
+ danger
|
|
|
+ size="mini"
|
|
|
+ type="text"
|
|
|
+ @click.stop="handleDelete(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>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+ import deliveryProjectApi from '@/api/devops/deliveryProject'
|
|
|
+ import { parseTime } from '@/utils'
|
|
|
+
|
|
|
+ export default {
|
|
|
+ name: 'DeliveryProject',
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ queryForm: {
|
|
|
+ keyWords: '',
|
|
|
+ searchType: 'projectName',
|
|
|
+ sortBy: 'createdTime',
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 20,
|
|
|
+ },
|
|
|
+ tableData: [],
|
|
|
+ total: 0,
|
|
|
+ loading: false,
|
|
|
+ mockTableData: [
|
|
|
+ {
|
|
|
+ id: 100000001,
|
|
|
+ deliveryEventTitle: '测试项目交付计划沟通',
|
|
|
+ deliveryEventDesc: '围绕整体交付里程碑、客户配合事项和项目实施边界进行首次沟通确认。',
|
|
|
+ deliveryEventType: '20',
|
|
|
+ deliveryEventStatus: '20',
|
|
|
+ deliveryEventResult: '10',
|
|
|
+ opsUserName: '李强',
|
|
|
+ completeTime: '2026-04-24 15:30:00',
|
|
|
+ completeDesc: '已完成首次交付计划确认,待客户二次反馈。',
|
|
|
+ onSite: '20',
|
|
|
+ feedbackReporter: '王静',
|
|
|
+ feedbackDate: '2026-04-23 10:20:00',
|
|
|
+ feedbackSource: '20',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 100000002,
|
|
|
+ deliveryEventTitle: '区域医检平台升级项目系统培训',
|
|
|
+ deliveryEventDesc: '针对检验、报告、质控等核心模块安排集中培训,覆盖关键业务流程和常见问题。',
|
|
|
+ deliveryEventType: '30',
|
|
|
+ deliveryEventStatus: '10',
|
|
|
+ deliveryEventResult: '30',
|
|
|
+ opsUserName: '周楠',
|
|
|
+ completeTime: '',
|
|
|
+ completeDesc: '培训议程已确认,等待客户安排现场时间。',
|
|
|
+ onSite: '10',
|
|
|
+ feedbackReporter: '陈晨',
|
|
|
+ feedbackDate: '2026-04-25 09:00:00',
|
|
|
+ feedbackSource: '10',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 100000003,
|
|
|
+ deliveryEventTitle: '生物样本库平台BUG修复',
|
|
|
+ deliveryEventDesc: '修复样本入库页面在批量提交场景下出现的数据校验异常,并完成回归验证。',
|
|
|
+ deliveryEventType: '34',
|
|
|
+ deliveryEventStatus: '30',
|
|
|
+ deliveryEventResult: '10',
|
|
|
+ opsUserName: '赵敏',
|
|
|
+ completeTime: '2026-04-22 18:10:00',
|
|
|
+ completeDesc: '已修复样本入库页面校验异常,并完成回归测试。',
|
|
|
+ onSite: '20',
|
|
|
+ feedbackReporter: '刘畅',
|
|
|
+ feedbackDate: '2026-04-21 14:35:00',
|
|
|
+ feedbackSource: '10',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 100000004,
|
|
|
+ deliveryEventTitle: 'LIMS系统发版准备',
|
|
|
+ deliveryEventDesc: '整理发版清单、确认数据库变更脚本及灰度验证方案,准备周五晚间上线。',
|
|
|
+ deliveryEventType: '36',
|
|
|
+ deliveryEventStatus: '20',
|
|
|
+ deliveryEventResult: '',
|
|
|
+ opsUserName: '孙涛',
|
|
|
+ completeTime: '',
|
|
|
+ completeDesc: '正在整理发版清单,计划本周五晚间发版。',
|
|
|
+ onSite: '20',
|
|
|
+ feedbackReporter: '杨帆',
|
|
|
+ feedbackDate: '2026-04-25 11:15:00',
|
|
|
+ feedbackSource: '30',
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ sidebarCollapsed: false,
|
|
|
+ projectSearch: '',
|
|
|
+ selectedProject: '',
|
|
|
+ projects: [
|
|
|
+ { id: '', name: '全部', count: 9 },
|
|
|
+ {
|
|
|
+ id: '1',
|
|
|
+ name: '测试项目',
|
|
|
+ count: 9,
|
|
|
+ contractNo: 'HT2026040101',
|
|
|
+ productLine: '20',
|
|
|
+ salesOwner: '王静',
|
|
|
+ deliveryOwner: '李强',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: '2',
|
|
|
+ name: '区域医检平台升级项目',
|
|
|
+ count: 6,
|
|
|
+ contractNo: 'HT2026040102',
|
|
|
+ productLine: '10',
|
|
|
+ salesOwner: '陈晨',
|
|
|
+ deliveryOwner: '周楠',
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ }
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ filteredProjects() {
|
|
|
+ const keyword = this.projectSearch.trim().toLowerCase()
|
|
|
+
|
|
|
+ if (!keyword) return this.projects
|
|
|
+
|
|
|
+ return this.projects.filter((project) => {
|
|
|
+ if (project.id === '') return true
|
|
|
+
|
|
|
+ return [project.name, project.contractNo, project.salesOwner, project.deliveryOwner]
|
|
|
+ .filter(Boolean)
|
|
|
+ .some((field) => field.toLowerCase().includes(keyword))
|
|
|
+ })
|
|
|
+ },
|
|
|
+ allProjectOption() {
|
|
|
+ return this.filteredProjects.find((project) => project.id === '')
|
|
|
+ },
|
|
|
+ projectCards() {
|
|
|
+ return this.filteredProjects.filter((project) => project.id !== '')
|
|
|
+ },
|
|
|
+ },
|
|
|
+ created() {
|
|
|
+ this.fetchData()
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ parseTime,
|
|
|
+
|
|
|
+ // 获取列表数据
|
|
|
+ async fetchData() {
|
|
|
+ this.loading = true
|
|
|
+ try {
|
|
|
+ const res = await deliveryProjectApi.getList(this.queryForm)
|
|
|
+ if (res.code === 200 && res.data) {
|
|
|
+ const list = res.data.list || []
|
|
|
+ this.tableData = list.length ? list : this.mockTableData
|
|
|
+ this.total = list.length ? res.data.total || 0 : this.mockTableData.length
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取数据失败:', error)
|
|
|
+ this.tableData = this.mockTableData
|
|
|
+ this.total = this.mockTableData.length
|
|
|
+ this.$message.error('获取数据失败,已展示样例数据')
|
|
|
+ } finally {
|
|
|
+ this.loading = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 搜索
|
|
|
+ handleSearch() {
|
|
|
+ this.queryForm.pageNum = 1
|
|
|
+ this.fetchData()
|
|
|
+ },
|
|
|
+
|
|
|
+ // 重置
|
|
|
+ handleReset() {
|
|
|
+ this.queryForm = {
|
|
|
+ keyWords: '',
|
|
|
+ searchType: 'projectName',
|
|
|
+ sortBy: 'createdTime',
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 20,
|
|
|
+ }
|
|
|
+ this.fetchData()
|
|
|
+ },
|
|
|
+
|
|
|
+ // 新增
|
|
|
+ handleAdd() {
|
|
|
+ this.$message.info('新增功能开发中...')
|
|
|
+ },
|
|
|
+
|
|
|
+ // 导出
|
|
|
+ handleExport() {
|
|
|
+ this.$message.info('导出功能开发中...')
|
|
|
+ },
|
|
|
+
|
|
|
+ // 编辑
|
|
|
+ handleEdit() {
|
|
|
+ this.$message.info('编辑功能开发中...')
|
|
|
+ },
|
|
|
+
|
|
|
+ // 查看详情
|
|
|
+ handleView() {
|
|
|
+ this.$message.info('详情功能开发中...')
|
|
|
+ },
|
|
|
+
|
|
|
+ // 删除
|
|
|
+ handleDelete() {
|
|
|
+ this.$confirm('确认删除该记录?', '提示', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning',
|
|
|
+ })
|
|
|
+ .then(() => {
|
|
|
+ this.$message.success('删除成功')
|
|
|
+ this.fetchData()
|
|
|
+ })
|
|
|
+ .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
|
|
|
+ },
|
|
|
+
|
|
|
+ selectProject(projectId) {
|
|
|
+ this.selectedProject = projectId
|
|
|
+ this.queryForm.projectId = projectId
|
|
|
+ this.queryForm.pageNum = 1
|
|
|
+ this.fetchData()
|
|
|
+ },
|
|
|
+
|
|
|
+ formatEventTime(time) {
|
|
|
+ return time ? parseTime(time, '{y}-{m}-{d} {h}:{i}') : '-'
|
|
|
+ },
|
|
|
+
|
|
|
+ getDeliveryEventTypeLabel(type) {
|
|
|
+ const map = {
|
|
|
+ 10: '内部启动会',
|
|
|
+ 15: '外部启动会',
|
|
|
+ 20: '制定计划',
|
|
|
+ 30: '系统培训',
|
|
|
+ 31: '需求沟通',
|
|
|
+ 32: '功能调整',
|
|
|
+ 33: '二开需求',
|
|
|
+ 34: 'BUG修复',
|
|
|
+ 35: '功能测试',
|
|
|
+ 36: '系统发版',
|
|
|
+ 39: '软件部署',
|
|
|
+ 40: '硬件发货',
|
|
|
+ 41: '硬件安装',
|
|
|
+ 50: '试运行',
|
|
|
+ 60: '验收汇报',
|
|
|
+ }
|
|
|
+ return map[type] || type || '-'
|
|
|
+ },
|
|
|
+
|
|
|
+ getDeliveryEventStatusLabel(status) {
|
|
|
+ const map = {
|
|
|
+ 10: '待处理',
|
|
|
+ 20: '处理中',
|
|
|
+ 30: '已关闭',
|
|
|
+ }
|
|
|
+ return map[status] || status || '-'
|
|
|
+ },
|
|
|
+
|
|
|
+ getDeliveryEventStatusType(status) {
|
|
|
+ const map = {
|
|
|
+ 10: 'info',
|
|
|
+ 20: 'warning',
|
|
|
+ 30: 'success',
|
|
|
+ }
|
|
|
+ return map[status] || 'info'
|
|
|
+ },
|
|
|
+
|
|
|
+ getDeliveryEventResultLabel(result) {
|
|
|
+ const map = {
|
|
|
+ 10: '已解决',
|
|
|
+ 30: '未解决',
|
|
|
+ }
|
|
|
+ return map[result] || result || '-'
|
|
|
+ },
|
|
|
+
|
|
|
+ getFeedbackSourceLabel(source) {
|
|
|
+ const map = {
|
|
|
+ 10: '客户',
|
|
|
+ 20: '销售',
|
|
|
+ 30: '交付',
|
|
|
+ }
|
|
|
+ return map[source] || source || '-'
|
|
|
+ },
|
|
|
+
|
|
|
+ getOnSiteLabel(onSite) {
|
|
|
+ const map = {
|
|
|
+ 10: '是',
|
|
|
+ 20: '否',
|
|
|
+ }
|
|
|
+ return map[onSite] || onSite || '-'
|
|
|
+ },
|
|
|
+
|
|
|
+ // 产品线标签
|
|
|
+ getProductLineLabel(productLine) {
|
|
|
+ const map = {
|
|
|
+ 10: 'Biobank',
|
|
|
+ 20: 'LIMS',
|
|
|
+ 30: 'CellBank',
|
|
|
+ 40: 'MCS',
|
|
|
+ }
|
|
|
+ return map[productLine] || productLine
|
|
|
+ },
|
|
|
+
|
|
|
+ // 状态标签
|
|
|
+ getStatusLabel(status) {
|
|
|
+ const map = {
|
|
|
+ 10: '待交付',
|
|
|
+ 20: '交付中',
|
|
|
+ 30: '暂停',
|
|
|
+ 40: '交付完成',
|
|
|
+ 50: '验收',
|
|
|
+ 90: '作废',
|
|
|
+ }
|
|
|
+ return map[status] || status
|
|
|
+ },
|
|
|
+
|
|
|
+ // 状态样式
|
|
|
+ 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)
|
|
|
+ },
|
|
|
+ },
|
|
|
+ }
|
|
|
+</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;
|
|
|
+ }
|
|
|
+
|
|
|
+ .toolbar {
|
|
|
+ margin-bottom: 4px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ flex-shrink: 0;
|
|
|
+ padding: 8px;
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 6px;
|
|
|
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
|
|
+ }
|
|
|
+
|
|
|
+ .toolbar-right {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .sort-label {
|
|
|
+ font-size: 14px;
|
|
|
+ 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-trigger {
|
|
|
+ display: inline-block;
|
|
|
+ max-width: 100%;
|
|
|
+ color: #409eff;
|
|
|
+ cursor: pointer;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ color: #66b1ff;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .event-desc-popover {
|
|
|
+ &__title {
|
|
|
+ margin-bottom: 8px;
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__content {
|
|
|
+ max-height: 180px;
|
|
|
+ font-size: 13px;
|
|
|
+ line-height: 1.6;
|
|
|
+ color: #606266;
|
|
|
+ white-space: pre-wrap;
|
|
|
+ word-break: break-word;
|
|
|
+ overflow-y: auto;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .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;
|
|
|
+ }
|
|
|
+
|
|
|
+ .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;
|
|
|
+ }
|
|
|
+
|
|
|
+ .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;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ 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-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-overview-count {
|
|
|
+ display: flex;
|
|
|
+ align-items: baseline;
|
|
|
+ gap: 2px;
|
|
|
+ color: #409eff;
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+
|
|
|
+ .project-overview-count-label {
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+
|
|
|
+ .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;
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ .project-line-tag {
|
|
|
+ flex-shrink: 0;
|
|
|
+ padding: 2px 8px;
|
|
|
+ font-size: 11px;
|
|
|
+ color: #409eff;
|
|
|
+ background: #ecf5ff;
|
|
|
+ border-radius: 999px;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+
|
|
|
+ .project-card-count {
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #409eff;
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ .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;
|
|
|
+ }
|
|
|
+</style>
|