feat(projects-assign): P1+P2+P3 修复 (含服务端超额校验)
P1: - 截止天数 max=100 + min=1 (前端拦超界输入) - 合同文件区实装 (support/execute_contract_url 走 Preview.vue, 单条模式显示) P2: - 可用金额公式实时显示 (总金额×(1-管理费/总金额)-累计劳务-累计会务) - 服务端二次校验: BizProjectAssignServiceImpl.validateSum 防前端绕过 P3: - 命名统一: 支持方→支持单位 / 执行方→执行单位 - 行内删除按钮: 仅剩1 行时禁用第一个 - loading 态: el-descriptions + 批量 info-bar 加 v-loading - 批量模式加载失败提示: failed 数组 + ElMessageBox.alert Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+1
@@ -172,6 +172,7 @@ public class BizProjectController extends BaseController
|
|||||||
public AjaxResult saveAssigns(@PathVariable("projectId") Long projectId, @RequestBody List<BizProjectAssign> assigns)
|
public AjaxResult saveAssigns(@PathVariable("projectId") Long projectId, @RequestBody List<BizProjectAssign> assigns)
|
||||||
{
|
{
|
||||||
if (assigns == null) assigns = new ArrayList<>();
|
if (assigns == null) assigns = new ArrayList<>();
|
||||||
|
bizProjectAssignService.validateSum(projectId, assigns);
|
||||||
bizProjectAssignService.deleteByProjectId(projectId);
|
bizProjectAssignService.deleteByProjectId(projectId);
|
||||||
for (BizProjectAssign a : assigns) {
|
for (BizProjectAssign a : assigns) {
|
||||||
a.setProjectId(projectId);
|
a.setProjectId(projectId);
|
||||||
|
|||||||
+5
@@ -15,4 +15,9 @@ public interface IBizProjectAssignService
|
|||||||
int updateByPrimaryKey(BizProjectAssign entity);
|
int updateByPrimaryKey(BizProjectAssign entity);
|
||||||
int deleteByPrimaryKey(String assignId);
|
int deleteByPrimaryKey(String assignId);
|
||||||
int deleteByProjectId(Long projectId);
|
int deleteByProjectId(Long projectId);
|
||||||
|
/**
|
||||||
|
* 校验多个执行方总金额不超过项目总金额 (服务端二次校验, 防前端绕过)
|
||||||
|
* 超额抛 ServiceException
|
||||||
|
*/
|
||||||
|
void validateSum(Long projectId, List<BizProjectAssign> assigns);
|
||||||
}
|
}
|
||||||
|
|||||||
+28
@@ -1,13 +1,16 @@
|
|||||||
package com.ruoyi.business.service.impl;
|
package com.ruoyi.business.service.impl;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import com.ruoyi.business.domain.BizOrg;
|
import com.ruoyi.business.domain.BizOrg;
|
||||||
|
import com.ruoyi.business.domain.BizProject;
|
||||||
import com.ruoyi.business.domain.BizProjectAssign;
|
import com.ruoyi.business.domain.BizProjectAssign;
|
||||||
import com.ruoyi.business.mapper.BizOrgMapper;
|
import com.ruoyi.business.mapper.BizOrgMapper;
|
||||||
import com.ruoyi.business.mapper.BizProjectAssignMapper;
|
import com.ruoyi.business.mapper.BizProjectAssignMapper;
|
||||||
|
import com.ruoyi.business.mapper.BizProjectMapper;
|
||||||
import com.ruoyi.business.service.IBizProjectAssignService;
|
import com.ruoyi.business.service.IBizProjectAssignService;
|
||||||
import com.ruoyi.common.exception.ServiceException;
|
import com.ruoyi.common.exception.ServiceException;
|
||||||
import com.ruoyi.common.utils.SecurityUtils;
|
import com.ruoyi.common.utils.SecurityUtils;
|
||||||
@@ -20,6 +23,8 @@ public class BizProjectAssignServiceImpl implements IBizProjectAssignService
|
|||||||
private BizProjectAssignMapper bizProjectAssignMapper;
|
private BizProjectAssignMapper bizProjectAssignMapper;
|
||||||
@Autowired
|
@Autowired
|
||||||
private BizOrgMapper bizOrgMapper;
|
private BizOrgMapper bizOrgMapper;
|
||||||
|
@Autowired
|
||||||
|
private BizProjectMapper bizProjectMapper;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public BizProjectAssign getById(String assignId) { return bizProjectAssignMapper.selectByPrimaryKey(assignId); }
|
public BizProjectAssign getById(String assignId) { return bizProjectAssignMapper.selectByPrimaryKey(assignId); }
|
||||||
@@ -84,4 +89,27 @@ public class BizProjectAssignServiceImpl implements IBizProjectAssignService
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int deleteByProjectId(Long projectId) { return bizProjectAssignMapper.deleteByProjectId(projectId); }
|
public int deleteByProjectId(Long projectId) { return bizProjectAssignMapper.deleteByProjectId(projectId); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验多个执行方总金额不超过项目总金额 (服务端二次校验, 防前端绕过)
|
||||||
|
* 超额抛 ServiceException, 由 axios 拦截器统一弹 ElMessage.error
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void validateSum(Long projectId, List<BizProjectAssign> assigns) {
|
||||||
|
if (assigns == null || assigns.isEmpty()) return;
|
||||||
|
BigDecimal sum = BigDecimal.ZERO;
|
||||||
|
for (BizProjectAssign a : assigns) {
|
||||||
|
if (a.getAmount() != null) sum = sum.add(a.getAmount());
|
||||||
|
}
|
||||||
|
if (sum.signum() <= 0) return;
|
||||||
|
BizProject p = bizProjectMapper.selectByPrimaryKey(projectId);
|
||||||
|
if (p == null) throw new ServiceException("项目 " + projectId + " 不存在");
|
||||||
|
BigDecimal total = p.getTotalAmount();
|
||||||
|
if (total == null || total.signum() <= 0) return;
|
||||||
|
if (sum.compareTo(total) > 0) {
|
||||||
|
throw new ServiceException(
|
||||||
|
"多个执行方的总金额(" + sum.toPlainString() + ")超过项目总金额(" + total.toPlainString() + ")"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 已选项目 (单条: 显示编号+名称 / 批量: 显示项目数) -->
|
<!-- 已选项目 (单条: 显示编号+名称 / 批量: 显示项目数) -->
|
||||||
<div v-if="batchMode" class="info-bar">
|
<div v-if="batchMode" v-loading="batchLoading" class="info-bar">
|
||||||
已选 <b>{{ assignBatchProjects.length }}</b> 个项目 (本次分配将按各项目自身的总场次 / 总金额逐项校验)
|
已选 <b>{{ assignBatchProjects.length }}</b> 个项目 (本次分配将按各项目自身的总场次 / 总金额逐项校验)
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="info-bar">
|
<div v-else class="info-bar">
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 项目核心属性 (单条模式: 只读展示, 字段未加载完时显示 -) -->
|
<!-- 项目核心属性 (单条模式: 只读展示, 字段未加载完时显示 -) -->
|
||||||
<el-descriptions v-if="!batchMode" class="project-meta" :column="3" border size="small">
|
<el-descriptions v-if="!batchMode" v-loading="singleLoading" class="project-meta" :column="3" border size="small">
|
||||||
<el-descriptions-item label="项目编号">{{ currentProject?.projectNo || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="项目编号">{{ currentProject?.projectNo || '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="项目名称">{{ currentProject?.projectName || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="项目名称">{{ currentProject?.projectName || '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="项目形式">{{ currentProject?.projectForm || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="项目形式">{{ currentProject?.projectForm || '-' }}</el-descriptions-item>
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
|
|
||||||
<el-form :model="assignForm" label-width="120px">
|
<el-form :model="assignForm" label-width="120px">
|
||||||
<el-form-item label="支持方">
|
<el-form-item label="支持单位">
|
||||||
<el-select v-model="assignForm.sponsorAdminUserId" placeholder="请选择支持单位" filterable
|
<el-select v-model="assignForm.sponsorAdminUserId" placeholder="请选择支持单位" filterable
|
||||||
:filter-method="searchSponsorOrgs" clearable style="width:100%" @change="onSponsorOrgPick">
|
:filter-method="searchSponsorOrgs" clearable style="width:100%" @change="onSponsorOrgPick">
|
||||||
<el-option v-for="u in sponsorOrgOptions" :key="u.userId"
|
<el-option v-for="u in sponsorOrgOptions" :key="u.userId"
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-divider>执行方分配</el-divider>
|
<el-divider>执行单位分配</el-divider>
|
||||||
|
|
||||||
<!-- 单选模式: 实时校验栏 -->
|
<!-- 单选模式: 实时校验栏 -->
|
||||||
<div v-if="!batchMode" class="assign-sessions-bar">
|
<div v-if="!batchMode" class="assign-sessions-bar">
|
||||||
@@ -43,6 +43,9 @@
|
|||||||
<span>总金额(元): <b>{{ assignForm.totalAmount || 0 }}</b></span>
|
<span>总金额(元): <b>{{ assignForm.totalAmount || 0 }}</b></span>
|
||||||
<span :class="{ 'is-over': singleAmountOver }">已分配金额: <b>¥ {{ assignedAmount }}</b></span>
|
<span :class="{ 'is-over': singleAmountOver }">已分配金额: <b>¥ {{ assignedAmount }}</b></span>
|
||||||
<span v-if="singleAmountOver" class="over-warn">⚠ 已超出 ¥ {{ Math.round((assignedAmount - assignForm.totalAmount) * 100) / 100 }}</span>
|
<span v-if="singleAmountOver" class="over-warn">⚠ 已超出 ¥ {{ Math.round((assignedAmount - assignForm.totalAmount) * 100) / 100 }}</span>
|
||||||
|
<span class="sep">|</span>
|
||||||
|
<span>可用金额: <b>¥ {{ availableAmount }}</b></span>
|
||||||
|
<span class="hint">(公式: 总金额×(1-管理费/总金额)-累计劳务-累计会务)</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 批量模式: 提示保存时按各项目自身校验 -->
|
<!-- 批量模式: 提示保存时按各项目自身校验 -->
|
||||||
<div v-else class="assign-sessions-bar">
|
<div v-else class="assign-sessions-bar">
|
||||||
@@ -52,7 +55,7 @@
|
|||||||
|
|
||||||
<el-table :data="assignForm.execRows" border>
|
<el-table :data="assignForm.execRows" border>
|
||||||
<el-table-column type="index" label="#" width="50" />
|
<el-table-column type="index" label="#" width="50" />
|
||||||
<el-table-column label="执行方名称" min-width="220">
|
<el-table-column label="执行单位" min-width="220">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-select v-model="row.execUserId" filterable
|
<el-select v-model="row.execUserId" filterable
|
||||||
:filter-method="q => searchExecutors(q, row)"
|
:filter-method="q => searchExecutors(q, row)"
|
||||||
@@ -77,7 +80,7 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="80">
|
<el-table-column label="操作" width="80">
|
||||||
<template #default="{ $index }">
|
<template #default="{ $index }">
|
||||||
<el-button link type="danger" @click="assignForm.execRows.splice($index, 1)">删除</el-button>
|
<el-button link type="danger" :disabled="$index === 0 && assignForm.execRows.length === 1" @click="assignForm.execRows.splice($index, 1)">删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -86,10 +89,33 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-form-item label="提交截止(天)" style="margin-top:12px">
|
<el-form-item label="提交截止(天)" style="margin-top:12px">
|
||||||
<el-input-number v-model="assignForm.deadlineDays" :min="0" controls-position="right" />
|
<el-input-number v-model="assignForm.deadlineDays" :min="1" :max="100" controls-position="right" />
|
||||||
|
<span class="hint-text" style="margin-left:12px">自然日, 1~100 天 (超出报错)</span>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
|
<!-- 第 5 区块: 合同文件 (单条模式显示, 批量隐藏) -->
|
||||||
|
<div v-if="!batchMode && currentProject" class="contract-section">
|
||||||
|
<div class="new-card-title">合同文件</div>
|
||||||
|
<div class="notice-row">
|
||||||
|
<span class="notice-label">支持合同:</span>
|
||||||
|
<template v-if="currentProject.supportContractUrl">
|
||||||
|
<a class="file-link" @click.prevent="openPreview(currentProject.supportContractUrl, '支持合同')">合同文件</a>
|
||||||
|
</template>
|
||||||
|
<span v-else class="empty-tip">暂无</span>
|
||||||
|
</div>
|
||||||
|
<div class="notice-row">
|
||||||
|
<span class="notice-label">执行合同:</span>
|
||||||
|
<template v-if="currentProject.executeContractUrl">
|
||||||
|
<a class="file-link" @click.prevent="openPreview(currentProject.executeContractUrl, '执行合同')">合同文件</a>
|
||||||
|
</template>
|
||||||
|
<span v-else class="empty-tip">暂无</span>
|
||||||
|
</div>
|
||||||
|
<p class="hint-text">*根据项目编号,同步OA的合同文件, 可预览</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Preview v-model="previewOpen" :url="previewUrl" :title="previewTitle" />
|
||||||
|
|
||||||
<!-- 底部按钮 -->
|
<!-- 底部按钮 -->
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<el-button @click="confirmCancel">取消</el-button>
|
<el-button @click="confirmCancel">取消</el-button>
|
||||||
@@ -105,6 +131,7 @@ import { bizUpdate, bizGet } from '@/api/public'
|
|||||||
import { listSponsorOrgs, listExecutorOrgs } from '@/api/system'
|
import { listSponsorOrgs, listExecutorOrgs } from '@/api/system'
|
||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import Preview from '@/components/Preview.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -127,6 +154,20 @@ const assignForm = reactive({
|
|||||||
})
|
})
|
||||||
const assignSubmitting = ref(false)
|
const assignSubmitting = ref(false)
|
||||||
|
|
||||||
|
// 合同预览 (支持合同 / 执行合同) — 单条模式用
|
||||||
|
const previewOpen = ref(false)
|
||||||
|
const previewUrl = ref('')
|
||||||
|
const previewTitle = ref('')
|
||||||
|
function openPreview(url, title) {
|
||||||
|
previewUrl.value = url
|
||||||
|
previewTitle.value = title || '附件预览'
|
||||||
|
previewOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载状态
|
||||||
|
const singleLoading = ref(false)
|
||||||
|
const batchLoading = ref(false)
|
||||||
|
|
||||||
const sponsorOrgOptions = ref([])
|
const sponsorOrgOptions = ref([])
|
||||||
let _supporterTimer = null
|
let _supporterTimer = null
|
||||||
|
|
||||||
@@ -145,6 +186,17 @@ const assignedAmount = computed(() => {
|
|||||||
const singleAmountOver = computed(() =>
|
const singleAmountOver = computed(() =>
|
||||||
!batchMode && Number(assignForm.totalAmount || 0) > 0 && assignedAmount.value > Number(assignForm.totalAmount)
|
!batchMode && Number(assignForm.totalAmount || 0) > 0 && assignedAmount.value > Number(assignForm.totalAmount)
|
||||||
)
|
)
|
||||||
|
// 可用金额 (公式: 总金额 × (1 - 管理费/总金额) - 累计劳务 - 累计会务)
|
||||||
|
const availableAmount = computed(() => {
|
||||||
|
if (batchMode) return 0
|
||||||
|
const total = Number(assignForm.totalAmount || 0)
|
||||||
|
if (total <= 0) return 0
|
||||||
|
const manageFee = Number(currentProject.value?.manageFee || 0)
|
||||||
|
const paidLabor = Number(currentProject.value?.paidLaborAmount || 0)
|
||||||
|
const paidMeeting = Number(currentProject.value?.paidMeetingAmount || 0)
|
||||||
|
const avail = total * (1 - manageFee / total) - paidLabor - paidMeeting
|
||||||
|
return Math.round(avail * 100) / 100
|
||||||
|
})
|
||||||
|
|
||||||
function makeEmptyExecRow() {
|
function makeEmptyExecRow() {
|
||||||
return { _loading: false, _timer: null, _options: [], execUserId: null, sessions: 0, amount: 0, remark: '' }
|
return { _loading: false, _timer: null, _options: [], execUserId: null, sessions: 0, amount: 0, remark: '' }
|
||||||
@@ -256,6 +308,7 @@ function onExecUserPick(row, userId) {
|
|||||||
// ========== 初始化 ==========
|
// ========== 初始化 ==========
|
||||||
async function loadSingleProject() {
|
async function loadSingleProject() {
|
||||||
if (!singleProjectId) return
|
if (!singleProjectId) return
|
||||||
|
singleLoading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await bizGet('project', singleProjectId)
|
const res = await bizGet('project', singleProjectId)
|
||||||
const p = res?.data?.data || res?.data || res
|
const p = res?.data?.data || res?.data || res
|
||||||
@@ -271,13 +324,17 @@ async function loadSingleProject() {
|
|||||||
loadSponsorOrgs('')
|
loadSponsorOrgs('')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ElMessage.error('加载项目数据失败: ' + (e?.msg || e?.message || e))
|
ElMessage.error('加载项目数据失败: ' + (e?.msg || e?.message || e))
|
||||||
|
} finally {
|
||||||
|
singleLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadBatchProjects() {
|
async function loadBatchProjects() {
|
||||||
if (!batchProjectIds.length) return
|
batchLoading.value = true
|
||||||
|
if (!batchProjectIds.length) { batchLoading.value = false; return }
|
||||||
try {
|
try {
|
||||||
const list = []
|
const list = []
|
||||||
|
const failed = []
|
||||||
for (const pid of batchProjectIds) {
|
for (const pid of batchProjectIds) {
|
||||||
try {
|
try {
|
||||||
const res = await bizGet('project', pid)
|
const res = await bizGet('project', pid)
|
||||||
@@ -287,13 +344,26 @@ async function loadBatchProjects() {
|
|||||||
projectId: p.projectId,
|
projectId: p.projectId,
|
||||||
projectNo: p.projectNo,
|
projectNo: p.projectNo,
|
||||||
projectName: p.projectName,
|
projectName: p.projectName,
|
||||||
|
sponsorAdminUserId: p.sponsorAdminUserId,
|
||||||
|
sponsorAdminUserName: p.sponsorAdminUserName,
|
||||||
totalSessions: Number(p.totalSessions || 0),
|
totalSessions: Number(p.totalSessions || 0),
|
||||||
totalAmount: Number(p.totalAmount || 0)
|
totalAmount: Number(p.totalAmount || 0)
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
failed.push({ projectId: pid, reason: '返回数据为空' })
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
failed.push({ projectId: pid, reason: e?.msg || e?.message || String(e) })
|
||||||
}
|
}
|
||||||
} catch (e) { /* skip failed project */ }
|
|
||||||
}
|
}
|
||||||
assignBatchProjects.value = list
|
assignBatchProjects.value = list
|
||||||
|
if (failed.length) {
|
||||||
|
ElMessageBox.alert(
|
||||||
|
`以下项目加载失败已跳过:\n${failed.map(f => `项目 ${f.projectId}: ${f.reason}`).join('\n')}`,
|
||||||
|
'加载提示',
|
||||||
|
{ type: 'warning' }
|
||||||
|
).catch(() => {})
|
||||||
|
}
|
||||||
// 批量模式: 取第一个项目的支持方作为默认 (用户可改)
|
// 批量模式: 取第一个项目的支持方作为默认 (用户可改)
|
||||||
if (list.length) {
|
if (list.length) {
|
||||||
assignForm.sponsorAdminUserId = list[0].sponsorAdminUserId || null
|
assignForm.sponsorAdminUserId = list[0].sponsorAdminUserId || null
|
||||||
@@ -304,7 +374,9 @@ async function loadBatchProjects() {
|
|||||||
assignForm.execRows = [makeEmptyExecRow()]
|
assignForm.execRows = [makeEmptyExecRow()]
|
||||||
assignForm.execRows.forEach(r => searchExecutors('', r))
|
assignForm.execRows.forEach(r => searchExecutors('', r))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ElMessage.error('加载批量项目失败')
|
ElMessage.error('加载批量项目失败: ' + (e?.msg || e?.message || e))
|
||||||
|
} finally {
|
||||||
|
batchLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -438,4 +510,30 @@ async function submitAssign() {
|
|||||||
.assign-sessions-bar .sep { color: #dcdfe6; margin: 0 4px; }
|
.assign-sessions-bar .sep { color: #dcdfe6; margin: 0 4px; }
|
||||||
|
|
||||||
.form-actions { display: flex; justify-content: center; gap: 16px; padding: 20px 0 4px; border-top: 1px solid #f0f0f0; margin-top: 16px; }
|
.form-actions { display: flex; justify-content: center; gap: 16px; padding: 20px 0 4px; border-top: 1px solid #f0f0f0; margin-top: 16px; }
|
||||||
|
|
||||||
|
/* 章节标题 (复用 ProjectsNew 风格) */
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 合同行 (复用 ProjectsNew 风格) */
|
||||||
|
.contract-section { margin-top: 8px; }
|
||||||
|
.notice-row { display: flex; align-items: stretch; gap: 12px; margin-bottom: 12px; }
|
||||||
|
.notice-label {
|
||||||
|
width: 90px;
|
||||||
|
display: flex; align-items: center; justify-content: flex-end;
|
||||||
|
color: #606266; font-size: 14px;
|
||||||
|
}
|
||||||
|
.contract-section .file-link { color: #1890ff; cursor: pointer; font-size: 13px; display: flex; align-items: center; }
|
||||||
|
.contract-section .file-link:hover { text-decoration: underline; }
|
||||||
|
.contract-section .empty-tip { font-size: 13px; color: #c0c4cc; display: flex; align-items: center; }
|
||||||
|
.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; }
|
||||||
</style>
|
</style>
|
||||||
Reference in New Issue
Block a user