OA ecology: - BizEcologyController + EcologyProjectService: 项目编号搜索/详情回填, 负责人静默建档 - ComplianceLoginService: 合规/负责人登录校验 + 视图 role 匹配 - application-prod/test.yml: ecology 只读库数据源 会议邀请函/分享: - InvitationPdfService + WxShareService: 邀请函 PDF 生成 + 微信分享 - PdfViewer.vue (pdfjs-dist) + MeetingInvitation.vue: PDF 预览 + 邀请函页 参会人/执行意向: - BizMeetingAttendee*: 参会人模块增强 - BizExecutionIntent*: 执行意向增强 签到/劳务/导入: - BizSignServiceImpl + SignFill.vue: 签到劳务 - FieldMismatch/ImportMismatch: 导入校验 VO 公示/会议详情: - PublicityDetail.vue / MeetingDetail.vue 等前端增强 配置: - test 短信 mock + 供应商/招标接口切生产域名
530 lines
22 KiB
Vue
530 lines
22 KiB
Vue
<template>
|
|
<div class="page-card">
|
|
<!-- 面包屑 (按角色显示: sponsor → 我的项目; manager/admin → 项目管理) -->
|
|
<div class="breadcrumb">
|
|
首页 /
|
|
<span v-if="roleType === 'sponsor'">我的项目</span>
|
|
<span v-else-if="roleType === 'executor'">项目列表</span>
|
|
<span v-else>项目管理</span>
|
|
/ <span class="current">项目详情</span>
|
|
</div>
|
|
|
|
<!-- ================= 1. 项目信息 (11 字段, 两列, 只读) ================= -->
|
|
<div class="new-card-title">项目信息</div>
|
|
<el-form v-if="row && row.projectId" label-width="120px" class="info-form">
|
|
<el-row :gutter="12">
|
|
<el-col :span="12"><el-form-item label="项目编号"><span class="info-value">{{ row.projectNo || '-' }}</span></el-form-item></el-col>
|
|
<el-col :span="12"><el-form-item label="项目名称"><span class="info-value">{{ row.projectName || '-' }}</span></el-form-item></el-col>
|
|
</el-row>
|
|
<el-row :gutter="12">
|
|
<el-col :span="12"><el-form-item label="总场次/总期数"><span class="info-value">{{ row.totalSessions || 0 }}</span></el-form-item></el-col>
|
|
<el-col :span="12"><el-form-item label="总金额"><span class="info-value money">¥ {{ fmtMoney(row.totalAmount) }}</span></el-form-item></el-col>
|
|
</el-row>
|
|
<el-row :gutter="12">
|
|
<el-col :span="12"><el-form-item label="管理费及税金"><span class="info-value money">¥ {{ fmtMoney(row.manageFee) }}</span></el-form-item></el-col>
|
|
<el-col :span="12"><el-form-item label="项目形式"><span class="info-value">{{ row.projectForm || '-' }}</span></el-form-item></el-col>
|
|
</el-row>
|
|
<el-row :gutter="12">
|
|
<el-col :span="12"><el-form-item label="项目开始时间"><span class="info-value">{{ fmtDateTime(row.startTime) }}</span></el-form-item></el-col>
|
|
<el-col :span="12"><el-form-item label="项目结束时间"><span class="info-value">{{ fmtDateTime(row.endTime) }}</span></el-form-item></el-col>
|
|
</el-row>
|
|
<el-row :gutter="12">
|
|
<el-col :span="12"><el-form-item label="是否招标项目"><span class="info-value">{{ row.isBidProject === 'Y' ? '是' : '否' }}</span></el-form-item></el-col>
|
|
<el-col :span="12"><el-form-item label="项目负责人"><span class="info-value">{{ row.leadUserName || '-' }}</span></el-form-item></el-col>
|
|
</el-row>
|
|
<el-row :gutter="12">
|
|
<el-col :span="12"><el-form-item label="支持单位"><span class="info-value">{{ row.sponsorOrgName || '-' }}</span></el-form-item></el-col>
|
|
</el-row>
|
|
<el-row :gutter="12">
|
|
<el-col :span="24">
|
|
<el-form-item label="执行单位">
|
|
<table class="info-table role-labor-table">
|
|
<colgroup>
|
|
<col style="width:160px">
|
|
<col style="width:90px">
|
|
<col style="width:130px">
|
|
</colgroup>
|
|
<thead>
|
|
<tr>
|
|
<th>名称</th>
|
|
<th style="text-align:right">场次</th>
|
|
<th style="text-align:right">金额</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="(a, idx) in assigns" :key="idx">
|
|
<td>{{ a.orgName || '-' }}</td>
|
|
<td class="money" style="text-align:right">{{ a.sessions || 0 }}</td>
|
|
<td class="money" style="text-align:right">¥ {{ fmtMoney(a.amount) }}</td>
|
|
</tr>
|
|
<tr v-if="!assigns.length"><td colspan="3" class="empty-row">到 项目管理/分配 添加</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</el-form-item>
|
|
</el-col>
|
|
</el-row>
|
|
</el-form>
|
|
|
|
<!-- ================= 2. 角色劳务设置 (只读) ================= -->
|
|
<div class="new-card-title">角色劳务设置</div>
|
|
<table class="info-table role-labor-table">
|
|
<colgroup>
|
|
<col style="width:120px">
|
|
<col style="width:180px">
|
|
</colgroup>
|
|
<thead>
|
|
<tr>
|
|
<th>角色</th>
|
|
<th style="text-align:right">劳务金额</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="(r, idx) in form.roleRows" :key="idx">
|
|
<td>{{ r.role || '-' }}</td>
|
|
<td class="money" style="text-align:right">¥ {{ fmtMoney(r.amount) }}</td>
|
|
</tr>
|
|
<tr v-if="!form.roleRows.length"><td colspan="2" class="empty-row">暂无角色</td></tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<!-- ================= 3. 支持单位监督员 (只读) ================= -->
|
|
<div class="new-card-title monitor-title">
|
|
<span>支持单位监督员</span>
|
|
<el-link v-if="roleType === 'manager' && row.sponsorOrgId" :underline="false" type="primary" @click="openMonitorAssign">分配</el-link>
|
|
</div>
|
|
<div class="monitor-list">
|
|
<div v-for="(m, idx) in monitors" :key="idx" class="monitor-row">
|
|
<span class="monitor-num">{{ idx + 1 }}.</span>
|
|
<span class="monitor-name">{{ m.name || '-' }}</span>
|
|
</div>
|
|
<div v-if="!monitors.length" class="empty-hint">暂无监督员</div>
|
|
</div>
|
|
|
|
<!-- (已分配执行方 已折叠进上方"项目信息"section, 此处不再重复) -->
|
|
|
|
<!-- ================= 4. 公告文件 (只读, 仅查看: 邀请函/支持函/通知/日程) ================= -->
|
|
<div class="new-card-title">公告文件</div>
|
|
<div class="notice-list">
|
|
<div v-for="(n, idx) in notices" :key="idx" class="notice-row">
|
|
<span class="notice-label">{{ n.label }}</span>
|
|
<span v-if="n.url" class="notice-file">
|
|
<span class="file-name">{{ n.name || '已上传' }}</span>
|
|
<el-link :underline="false" type="primary" @click="openPreview(n.url, n.label)">查看</el-link>
|
|
</span>
|
|
<span v-else class="file-empty">未上传</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 公告预览 (只读) -->
|
|
<Preview v-model="previewOpen" :url="previewUrl" :title="previewTitle" />
|
|
|
|
<!-- ================= 监督员分配 dialog (仅 manager, 需已分配支持方) ================= -->
|
|
<el-dialog v-model="monitorAssignVisible" title="分配监督员" width="560px" append-to-body destroy-on-close>
|
|
<div class="assign-project-info">
|
|
<div><span class="assign-lbl">项目编号</span><span>{{ row.projectNo || '-' }}</span></div>
|
|
<div><span class="assign-lbl">项目名称</span><span>{{ row.projectName || '-' }}</span></div>
|
|
</div>
|
|
<el-form :model="monitorAssignForm" label-width="100px">
|
|
<el-form-item label="分配给监督员">
|
|
<el-select v-model="monitorAssignForm.monitorUserIds" multiple collapse-tags collapse-tags-tooltip filterable
|
|
placeholder="请选择监督员 (姓名 / 手机号, 可多选)" style="width: 100%">
|
|
<el-option v-for="m in monitorCandidates" :key="m.userId" :label="m.name + ' (' + m.phone + ')'" :value="m.userId" />
|
|
</el-select>
|
|
</el-form-item>
|
|
</el-form>
|
|
<template #footer>
|
|
<el-button @click="monitorAssignVisible = false">取消</el-button>
|
|
<el-button type="primary" :loading="monitorAssignSubmitting" :disabled="!monitorAssignForm.monitorUserIds.length" @click="saveMonitorAssign">确定</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
|
|
<!-- 底部操作: 返回 (按角色回退) -->
|
|
<div class="form-actions">
|
|
<el-button @click="goBack">返回</el-button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, reactive, computed, onMounted } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { bizGet, bizList } from '@/api/public'
|
|
import { listExecutorOrgs } from '@/api/system'
|
|
import { sponsorAssignProject } from '@/api/business/project'
|
|
import request from '@/utils/request'
|
|
import { ElMessage } from 'element-plus'
|
|
import Preview from '@/components/Preview.vue'
|
|
import { useUserStore } from '@/store/user'
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const userStore = useUserStore()
|
|
|
|
// 角色 (role_type 单一可信源: store.role = user.role = /getInfo 返回的 roleType)
|
|
const roleType = computed(() => userStore.role)
|
|
|
|
// ===================== 状态 =====================
|
|
const projectId = ref('')
|
|
const row = ref({})
|
|
const loading = ref(false)
|
|
|
|
// 已分配执行方 (服务公司) — 数据源: GET /business/project/{id}/assigns + listExecutorOrgs 补 org_name
|
|
const assigns = ref([])
|
|
|
|
// 角色劳务 (从 biz_project.role_labor JSON 字段读; 空数组 → 模板 "暂无角色" 行兜底)
|
|
const form = reactive({
|
|
roleRows: []
|
|
})
|
|
// 监督员 (从 biz_project.monitors JSON 字段读)
|
|
const monitors = ref([])
|
|
|
|
// 公告文件 (邀请函/支持函/通知/日程) — 数据源 biz_project 4 个独立列, 与 ProjectsNew 反显逻辑一致
|
|
// 注: 后端无 publicityFiles 字段 (前端传了但不落库), 实际落库是 invitation_url/support_letter_url/publish_url/schedule_url
|
|
const notices = computed(() => {
|
|
const r = row.value || {}
|
|
const items = [
|
|
{ label: '邀请函', url: r.invitationUrl },
|
|
{ label: '支持函', url: r.supportLetterUrl },
|
|
{ label: '通知', url: r.publishUrl },
|
|
{ label: '日程', url: r.scheduleUrl }
|
|
]
|
|
return items.map(m => ({
|
|
label: m.label,
|
|
url: m.url,
|
|
name: fileNameFromUrl(m.url)
|
|
}))
|
|
})
|
|
|
|
// 公告预览
|
|
const previewOpen = ref(false)
|
|
const previewUrl = ref('')
|
|
const previewTitle = ref('公告预览')
|
|
|
|
// ===================== 工具 =====================
|
|
function numOrZero(v) {
|
|
const n = Number(v)
|
|
return isNaN(n) ? 0 : n
|
|
}
|
|
function fmtMoney(v) {
|
|
if (v == null || v === '' || v === 0) return '0.00'
|
|
return Number(v).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
|
}
|
|
function fmtDateTime(v) {
|
|
if (!v) return '-'
|
|
return String(v)
|
|
}
|
|
// 从 OSS URL 提取并解码文件名 (中文文件名是 URL 编码的, 需 decodeURIComponent 还原)
|
|
function fileNameFromUrl(url) {
|
|
if (!url) return ''
|
|
let last
|
|
try {
|
|
const path = String(url).split('?')[0]
|
|
last = decodeURIComponent(path.split('/').pop() || '')
|
|
} catch (e) {
|
|
// 非标准编码导致解码失败 → 退回原始最后一段
|
|
last = String(url).split('?')[0].split('/').pop() || ''
|
|
}
|
|
// 去掉 OSS 上传附加的随机串: 原名_时间戳(13位)_随机串(6位).扩展名 → 原名.扩展名
|
|
const m = last.match(/^(.+)_\d{13}_[a-z0-9]{6}(\.[^.]+)?$/)
|
|
if (m) return m[1] + (m[2] || '')
|
|
return last
|
|
}
|
|
|
|
// ===================== 加载 =====================
|
|
async function loadProject() {
|
|
projectId.value = route.params.projectId
|
|
if (!projectId.value) return
|
|
loading.value = true
|
|
try {
|
|
const resp = await bizGet('project', projectId.value)
|
|
const data = (resp && resp.data) || resp || {}
|
|
row.value = data || {}
|
|
// 角色劳务 (JSON 字段, 缺/空 → form.roleRows 保持空数组, 模板"暂无角色"兜底)
|
|
try {
|
|
const rl = JSON.parse(row.value.roleLabor || '[]')
|
|
if (Array.isArray(rl) && rl.length) form.roleRows = rl
|
|
} catch (e) { /* keep empty */ }
|
|
// 监督员 (走 /sponsorAssigns 端点, 不在 loadProject 里)
|
|
} catch (e) {
|
|
console.error('[project-detail] load failed', e)
|
|
row.value = {}
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
// ===================== 已分配执行方 (服务公司) =====================
|
|
/**
|
|
* 数据源同 manager/projects/edit/{id} 的 loadAssigns:
|
|
* 1) GET /business/project/{id}/assigns 拿原始分配行 (assignId, executionUnitId, sessions, amount)
|
|
* 2) 对每个 executionUnitId 调 listExecutorOrgs({ orgId }) 补 org_name
|
|
* N+1 但项目分配数一般 < 10, 简单可控; 真要优化再加聚合 JOIN 接口
|
|
*/
|
|
async function loadAssigns() {
|
|
try {
|
|
const resp = await request({ url: `/business/project/${projectId.value}/assigns`, method: 'get' })
|
|
const list = (resp && resp.data) || []
|
|
if (!Array.isArray(list) || !list.length) { assigns.value = []; return }
|
|
// 并发查每个执行方公司名
|
|
const enriched = await Promise.all(list.map(async (a) => {
|
|
let orgName = '-'
|
|
try {
|
|
const opts = await listExecutorOrgs({ orgId: a.executionUnitId })
|
|
const org = (opts && opts.data && opts.data[0]) || null
|
|
if (org && org.orgName) orgName = org.orgName
|
|
} catch (e) { /* 单条失败不影响其它, 默认显示 '-' */ }
|
|
return {
|
|
assignId: a.assignId,
|
|
executionUnitId: a.executionUnitId,
|
|
orgName,
|
|
sessions: a.sessions || 0,
|
|
amount: a.amount || 0
|
|
}
|
|
}))
|
|
assigns.value = enriched
|
|
} catch (e) {
|
|
console.error('[project-detail] assigns load failed', e)
|
|
assigns.value = []
|
|
}
|
|
}
|
|
|
|
// ===================== 监督员 (sponsor 视角的监察员) =====================
|
|
// 数据源: GET /business/project/{id}/sponsorAssigns (返回 [{monitorUserId, monitorUserName, ...}])
|
|
// 注: 不再读 biz_project.monitors — 该字段在 biz_project 表不存在 (历史 bug)
|
|
async function loadMonitors() {
|
|
if (!projectId.value) return
|
|
try {
|
|
const resp = await request({ url: `/business/project/${projectId.value}/sponsorAssigns`, method: 'get' })
|
|
const list = (resp && resp.data) || []
|
|
monitors.value = (Array.isArray(list) ? list : []).map(x => ({
|
|
userId: x.monitorUserId,
|
|
name: x.monitorUserName || ('(userId=' + x.monitorUserId + ')')
|
|
}))
|
|
} catch (e) {
|
|
console.error('[project-detail] monitors load failed', e)
|
|
monitors.value = []
|
|
}
|
|
}
|
|
|
|
// ===================== 监督员分配 (仅 manager, 需已分配支持方) =====================
|
|
const monitorAssignVisible = ref(false)
|
|
const monitorAssignSubmitting = ref(false)
|
|
const monitorCandidates = ref([])
|
|
const monitorAssignForm = reactive({ monitorUserIds: [] })
|
|
|
|
// 候选: 本项目已分配支持方的员工 (按 orgId 圈定, 过滤出子账号=监督员)
|
|
async function loadMonitorCandidates() {
|
|
const sponsorOrgId = row.value && row.value.sponsorOrgId
|
|
if (!sponsorOrgId) { monitorCandidates.value = []; return }
|
|
try {
|
|
const { data } = await bizList('person', { orgId: sponsorOrgId, status: '0', pageNum: 1, pageSize: 100 })
|
|
const rows = (data && data.rows) || []
|
|
monitorCandidates.value = rows
|
|
.filter(p => p.accountType === 'SUB')
|
|
.map(p => ({ userId: p.userId, name: p.name, phone: p.phone }))
|
|
} catch (e) {
|
|
monitorCandidates.value = []
|
|
}
|
|
}
|
|
|
|
function openMonitorAssign() {
|
|
monitorAssignForm.monitorUserIds = []
|
|
monitorAssignVisible.value = true
|
|
loadMonitorCandidates()
|
|
// 回显当前已分配监督员 (等候选加载完, 让 el-select 找得到 option)
|
|
request({ url: `/business/project/${projectId.value}/sponsorAssigns`, method: 'get' })
|
|
.then(resp => {
|
|
const list = (resp && resp.data) || []
|
|
monitorAssignForm.monitorUserIds = (Array.isArray(list) ? list : []).map(x => x.monitorUserId).filter(Boolean)
|
|
})
|
|
.catch(() => { monitorAssignForm.monitorUserIds = [] })
|
|
}
|
|
|
|
async function saveMonitorAssign() {
|
|
if (!monitorAssignForm.monitorUserIds.length) return ElMessage.warning('请至少选择一位监督员')
|
|
monitorAssignSubmitting.value = true
|
|
try {
|
|
await sponsorAssignProject({
|
|
projectId: projectId.value,
|
|
monitorUserIds: [...monitorAssignForm.monitorUserIds]
|
|
})
|
|
ElMessage.success('分配成功')
|
|
monitorAssignVisible.value = false
|
|
await loadMonitors()
|
|
} catch (e) {
|
|
ElMessage.error(e?.msg || e?.message || '分配失败')
|
|
} finally {
|
|
monitorAssignSubmitting.value = false
|
|
}
|
|
}
|
|
|
|
// ===================== 导航 =====================
|
|
// 按角色回退: sponsor → 我的项目, manager → 项目管理, admin → 项目管理
|
|
function goBack() {
|
|
const role = roleType.value
|
|
if (role === 'sponsor') router.push('/sponsor/my-projects')
|
|
else if (role === 'executor') router.push('/executor/projects')
|
|
else if (role === 'admin') router.push('/admin/projects')
|
|
else if (role === 'leader') router.push('/leader/projects')
|
|
else router.push('/manager/projects')
|
|
}
|
|
function goEdit() { router.push(`/${roleType.value}/projects/edit/${projectId.value}`) }
|
|
|
|
// ===================== 预览 =====================
|
|
function openPreview(url, title) {
|
|
if (!url) return
|
|
previewUrl.value = url
|
|
previewTitle.value = title || '公告预览'
|
|
previewOpen.value = true
|
|
}
|
|
|
|
// ===================== 初始化 =====================
|
|
onMounted(async () => {
|
|
await loadProject()
|
|
await Promise.all([loadAssigns(), loadMonitors()])
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* 项目详情 — 统一排版: 正文 14px, 章节标题/表头 600, 金额等宽, 三级灰 strong/regular/muted */
|
|
.page-card { background: #fff; padding: 16px 20px; border-radius: 6px; border: 1px solid #f0f0f0; max-width: 1200px; }
|
|
.breadcrumb { font-size: 13px; color: #909399; margin-bottom: 12px; }
|
|
.breadcrumb .current { color: #262626; font-weight: 500; }
|
|
|
|
/* 章节标题 (与编辑页 ProjectsNew.vue 保持一致) */
|
|
.new-card-title {
|
|
font-size: 14px; font-weight: 600; color: #262626;
|
|
margin: 16px 0 12px; padding-left: 8px;
|
|
border-left: 3px solid var(--brand-primary); line-height: 1;
|
|
}
|
|
.new-card-title:first-child { margin-top: 0; }
|
|
|
|
/* 底部操作按钮 (返回) */
|
|
.form-actions { margin-top: 24px; display: flex; gap: 8px; }
|
|
|
|
/* 项目信息: 只读 el-form, 标签 14px/regular, 值 14px/strong */
|
|
.info-form { padding-top: 4px; }
|
|
.info-form :deep(.el-form-item) { margin-bottom: 8px; }
|
|
.info-form :deep(.el-form-item__label) {
|
|
font-size: 14px; color: #606266; width: 130px; padding-right: 16px;
|
|
}
|
|
.info-value { font-size: 14px; color: #262626; font-weight: 400; }
|
|
|
|
/* 金额: 统一等宽字体 + tabular 数字对齐 (表单值 + 两处表格通用) */
|
|
.money {
|
|
font-family: ui-monospace, "SF Mono", "JetBrains Mono", Consolas, "Courier New", monospace;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
/* 监督员 */
|
|
.monitor-list { padding: 4px 0; max-width: 400px; }
|
|
.monitor-row {
|
|
display: flex; align-items: center; gap: 8px;
|
|
padding: 8px 12px; background: #fafafa; border-radius: 4px;
|
|
margin-bottom: 6px;
|
|
}
|
|
.monitor-num { width: 24px; color: #909399; font-size: 13px; }
|
|
.monitor-name { flex: 1; font-size: 14px; color: #262626; }
|
|
.empty-hint { text-align: center; color: #909399; padding: 12px 0; font-size: 13px; }
|
|
.monitor-title { display: flex; align-items: center; justify-content: space-between; max-width: 400px; }
|
|
|
|
/* 监督员分配 dialog 项目信息 */
|
|
.assign-project-info { display: flex; flex-direction: column; gap: 8px; margin-bottom: 12px; font-size: 14px; color: #262626; }
|
|
.assign-lbl { display: inline-block; min-width: 70px; color: #909399; }
|
|
|
|
/* 角色劳务表格 + 服务公司表格: 统一 14px, 表头 600/strong, 单元格 strong */
|
|
.info-table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
|
.role-labor-table { width: auto; border: 1px solid #ebeef5; } /* 按内容紧凑显示, 不撑满卡片 */
|
|
.role-labor-table th,
|
|
.role-labor-table td { border-right: 1px solid #ebeef5; }
|
|
.role-labor-table th:last-child,
|
|
.role-labor-table td:last-child { border-right: none; }
|
|
.info-table th { background: #fafafa; padding: 10px 12px; text-align: left; color: #262626; font-weight: 600; border-bottom: 1px solid #f0f0f0; white-space: nowrap; }
|
|
.info-table td { padding: 10px 12px; border-bottom: 1px solid #f5f5f5; color: #262626; vertical-align: middle; }
|
|
.empty-row { text-align: center; color: #909399; }
|
|
|
|
/* 公告文件 (只读 4 行: 邀请函/支持函/通知/日程) */
|
|
.notice-list { display: flex; flex-direction: column; gap: 6px; }
|
|
.notice-row { display: flex; align-items: center; gap: 12px; padding: 6px 0; }
|
|
.notice-label { width: 90px; flex-shrink: 0; color: #606266; font-size: 14px; }
|
|
.notice-file { flex: 1; display: flex; align-items: center; gap: 12px; min-width: 0; }
|
|
.file-name { color: #262626; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
.file-empty { color: #909399; font-size: 13px; }
|
|
|
|
/* 提示 (灰色, 跟 ProjectsNew 一致) */
|
|
.hint-text { font-size: 12px; color: #909399; margin: 8px 0; line-height: 1.6; }
|
|
.hint-text p { margin-bottom: 4px; }
|
|
.hint-text p:last-child { margin-bottom: 0; }
|
|
|
|
/* ========================================
|
|
移动端适配 (≤768px)
|
|
- 卡片 padding 收窄
|
|
- 两列 12 跨度 → 单列 24
|
|
- label 上方对齐
|
|
- 服务公司表格 + 角色劳务表 横向滚动
|
|
- 底部按钮全宽
|
|
======================================== */
|
|
@media (max-width: 768px) {
|
|
.page-card { padding: 12px !important; border-radius: 4px !important; }
|
|
|
|
/* 面包屑 */
|
|
.breadcrumb { font-size: 12px !important; margin-bottom: 8px !important; }
|
|
|
|
/* 章节标题 */
|
|
.new-card-title { font-size: 13px !important; margin: 12px 0 10px !important; padding-left: 6px !important; }
|
|
|
|
/* 项目信息表: 两列改单列, label 上方对齐 */
|
|
.info-form { padding-top: 0 !important; }
|
|
.info-form :deep(.el-form-item) {
|
|
display: block !important;
|
|
margin-bottom: 10px !important;
|
|
}
|
|
.info-form :deep(.el-form-item__label) {
|
|
float: none !important;
|
|
width: auto !important;
|
|
text-align: left !important;
|
|
padding: 0 0 4px !important;
|
|
font-size: 13px !important;
|
|
color: var(--el-text-color-secondary) !important;
|
|
}
|
|
.info-form :deep(.el-form-item__content) {
|
|
margin-left: 0 !important;
|
|
line-height: 1.6 !important;
|
|
}
|
|
.info-value { font-size: 14px !important; }
|
|
.info-form :deep(.el-row) { margin: 0 !important; }
|
|
.info-form :deep(.el-col) { padding: 0 !important; margin-bottom: 0 !important; }
|
|
|
|
/* 角色劳务表 + 服务公司表 (共用 info-table 原生表格) */
|
|
.role-labor-table { width: 100% !important; font-size: 13px !important; }
|
|
.role-labor-table th,
|
|
.role-labor-table td { padding: 8px 10px !important; font-size: 13px !important; }
|
|
|
|
/* 监督员 */
|
|
.monitor-row { padding: 6px 10px !important; }
|
|
.monitor-name { font-size: 13px !important; }
|
|
|
|
/* 公告文件: label 固定宽度自适应 */
|
|
.notice-row {
|
|
flex-direction: column !important;
|
|
align-items: flex-start !important;
|
|
gap: 4px !important;
|
|
padding: 8px 0 !important;
|
|
border-bottom: 1px dashed var(--brand-slate-100);
|
|
}
|
|
.notice-label { width: auto !important; font-size: 12px !important; color: var(--el-text-color-secondary) !important; }
|
|
.notice-file { width: 100% !important; }
|
|
.file-name { font-size: 13px !important; }
|
|
|
|
/* 底部按钮: 全宽 */
|
|
.form-actions {
|
|
margin-top: 16px !important;
|
|
flex-direction: column !important;
|
|
gap: 8px !important;
|
|
}
|
|
.form-actions .el-button { width: 100% !important; }
|
|
|
|
/* 提示 */
|
|
.hint-text { font-size: 11px !important; }
|
|
}
|
|
</style> |