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 + 供应商/招标接口切生产域名
587 lines
24 KiB
Vue
587 lines
24 KiB
Vue
<template>
|
||
<div class="page-card manager-projects-new">
|
||
<!-- 面包屑 -->
|
||
<div class="breadcrumb">
|
||
首页 / 项目管理 / <span class="current">{{ isEdit ? '编辑项目' : '新建项目' }}</span>
|
||
</div>
|
||
|
||
<el-form :model="form" :rules="rules" ref="formRef" label-width="120px" class="project-form">
|
||
<!-- ========== 第一区块:项目信息 ========== -->
|
||
<div class="new-card-title">项目信息</div>
|
||
<el-row :gutter="12">
|
||
<el-col :span="12">
|
||
<el-form-item label="项目编号" prop="projectNo">
|
||
<el-select v-model="form.projectNo" placeholder="输入项目编号搜索" filterable allow-create default-first-option
|
||
:filter-method="searchProjectNos" :loading="projectNoLoading" clearable style="width:100%"
|
||
@change="onProjectNoChange">
|
||
<el-option v-for="p in projectNoOptions" :key="p.projectNo"
|
||
:label="p.projectName ? `${p.projectNo}(${p.projectName})` : p.projectNo" :value="p.projectNo" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="项目名称" prop="projectName">
|
||
<el-input v-model="form.projectName" placeholder="请输入项目名称" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="12">
|
||
<el-col :span="12">
|
||
<el-form-item label="项目负责人">
|
||
<el-select v-model="form.leadUserId" placeholder="请选择项目负责人" filterable clearable :filter-method="searchLeaders" style="width:100%">
|
||
<el-option v-for="u in leaderOptions" :key="u.userId"
|
||
:label="`${u.nickName || u.userName}${u.nickName && u.userName ? ' (' + u.userName + ')' : ''}`" :value="u.userId" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="是否招标项目">
|
||
<el-select v-model="form.isBidProject" placeholder="请选择" style="width:100%" clearable>
|
||
<el-option label="是" value="Y" />
|
||
<el-option label="否" value="N" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="12">
|
||
<el-col :span="12">
|
||
<el-form-item label="项目形式" prop="projectForm">
|
||
<el-select v-model="form.projectForm" placeholder="请选择" style="width:100%">
|
||
<el-option label="线上" value="线上" />
|
||
<el-option label="线下" value="线下" />
|
||
<el-option label="线上+线下" value="线上+线下" />
|
||
<el-option label="其他" value="其他" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="总场次/总期数" prop="totalSessions">
|
||
<div class="num-input"><el-input-number v-model="form.totalSessions" :min="1" /></div>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="12">
|
||
<el-col :span="12">
|
||
<el-form-item label="总金额(元)" prop="totalAmount">
|
||
<div class="num-input"><el-input-number v-model="form.totalAmount" :min="0" :precision="2" /></div>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="管理费(元)">
|
||
<div class="num-input"><el-input-number v-model="form.manageFee" :min="0" :precision="2" /></div>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="12">
|
||
<el-col :span="12">
|
||
<el-form-item label="开始时间" prop="startTime">
|
||
<el-date-picker v-model="form.startTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width:100%" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="结束时间" prop="endTime">
|
||
<el-date-picker v-model="form.endTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width:100%" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<!-- ========== 第二区块:角色劳务设置 ========== -->
|
||
<div class="new-card-title">角色劳务设置</div>
|
||
<div v-for="(r, idx) in form.roleRows" :key="idx" class="role-row">
|
||
<div class="role-select">
|
||
<el-select v-model="r.role" placeholder="请选择" @change="onRoleSelectChange(r)">
|
||
<el-option label="主席" value="主席" />
|
||
<el-option label="主持" value="主持" />
|
||
<el-option label="讲者" value="讲者" />
|
||
<el-option label="点评/评审" value="点评/评审" />
|
||
<el-option label="讨论" value="讨论" />
|
||
<el-option label="其他" value="其他" />
|
||
</el-select>
|
||
</div>
|
||
<el-input v-if="r.role === '其他'" v-model="r.customName" placeholder="角色名" class="role-custom" />
|
||
<el-input-number v-model="r.amount" :min="0" :precision="2" placeholder="劳务金额" class="role-amount" />
|
||
<div class="row-actions">
|
||
<el-link :underline="false" type="danger" :disabled="form.roleRows.length<=1" @click="removeRoleRowAt(idx)">删除角色</el-link>
|
||
</div>
|
||
</div>
|
||
<el-link :underline="false" type="primary" @click="addRoleRow" style="margin-top: 8px;">+ 增加角色</el-link>
|
||
|
||
<!-- ========== 第三区块:公告文件 ========== -->
|
||
<div class="new-card-title">公告文件</div>
|
||
<div class="notice-list">
|
||
<div v-for="(n, idx) in form.notices" :key="idx" class="notice-row">
|
||
<span class="notice-label">{{ n.label }}</span>
|
||
<div class="notice-uploader">
|
||
<oss-file-uploader
|
||
v-model="n.url"
|
||
:dir="`ry8080/project/${n.key}/`"
|
||
accept=".pdf,.png,.jpg,.jpeg,.gif,.webp"
|
||
:placeholder="`点击上传${n.label}`"
|
||
hint="支持 PDF / 图片"
|
||
block
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 底部按钮 (skill: 用 el-form-item 包裹, 不另起 div.form-actions) -->
|
||
<el-form-item class="form-actions">
|
||
<el-button @click="confirmCancel">取消</el-button>
|
||
<el-button type="warning" :loading="submitting" @click="submit('publish')">保存并发布公告</el-button>
|
||
<el-button type="primary" :loading="submitting" @click="submit('save')">保存</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, onMounted } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { bizAdd, bizUpdate, bizGet, searchEcologyProjects, pullEcologyProject } from '@/api/public'
|
||
import { listExecutor, listLeaders } from '@/api/system'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import request from '@/utils/request'
|
||
import OssFileUploader from '@/components/OssFileUploader.vue'
|
||
|
||
const router = useRouter()
|
||
const route = useRoute()
|
||
const isEdit = !!route.params.projectId
|
||
const projectId = route.params.projectId || null
|
||
const formRef = ref(null)
|
||
const submitting = ref(false)
|
||
const fileInputRef = ref(null)
|
||
|
||
function defaultRoleRow() { return { role: '', customName: '', amount: 0 } }
|
||
function defaultNotice(key, label) { return { key, label, url: '', name: '' } }
|
||
// 当前本地时间 "yyyy-MM-dd HH:mm:ss" (发布用, 与 el-date-picker 的本地时间口径一致; 不能用 toISOString 的 UTC)
|
||
function nowLocalDatetime() {
|
||
const d = new Date()
|
||
const p = n => String(n).padStart(2, '0')
|
||
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`
|
||
}
|
||
|
||
// 项目负责人候选 (sys_user.role_type='leader')
|
||
const leaderOptions = ref([])
|
||
let _managerTimer = null
|
||
|
||
const form = reactive({
|
||
projectName: '', projectNo: '', projectForm: '',
|
||
// 项目负责人 (sys_user.role_type='leader', 名称由后端 JOIN 查, 不缓存)
|
||
leadUserId: null,
|
||
// 是否招标项目 Y/N (默认 N)
|
||
isBidProject: 'N',
|
||
totalSessions: 0, totalAmount: 0, manageFee: 0,
|
||
startTime: '', endTime: '',
|
||
roleRows: [
|
||
{ role: '主席', customName: '', amount: 0 },
|
||
{ role: '主持', customName: '', amount: 0 },
|
||
{ role: '讲者', customName: '', amount: 0 },
|
||
{ role: '点评/评审', customName: '', amount: 0 },
|
||
{ role: '讨论', customName: '', amount: 0 }
|
||
],
|
||
// 公告附件 (key/label/url/name, 没有 url 即为空, 不参与提交)
|
||
notices: [
|
||
{ key: 'invitation', label: '邀请函', url: '', name: '' },
|
||
{ key: 'supportLetter', label: '支持函', url: '', name: '' },
|
||
{ key: 'notice', label: '通知', url: '', name: '' },
|
||
{ key: 'schedule', label: '日程', url: '', name: '' }
|
||
]
|
||
})
|
||
|
||
const rules = {
|
||
projectName: [{ required: true, message: '请输入项目名称', trigger: 'blur' }],
|
||
projectNo: [{ required: true, message: '请输入项目编号', trigger: 'blur' }],
|
||
projectForm: [{ required: true, message: '请选择项目形式', trigger: 'change' }],
|
||
totalSessions: [{ required: true, message: '请输入总场次', trigger: 'blur' }],
|
||
totalAmount: [{ required: true, message: '请输入总金额', trigger: 'blur' }],
|
||
startTime: [{ required: true, message: '请选择开始时间', trigger: 'change' }],
|
||
endTime: [{ required: true, message: '请选择结束时间', trigger: 'change' }]
|
||
}
|
||
|
||
function onRoleSelectChange(r) {
|
||
if (r.role !== '其他') r.customName = ''
|
||
}
|
||
function addRoleRow() {
|
||
if (form.roleRows.length >= 20) { ElMessage.warning('最多 20 行角色'); return }
|
||
form.roleRows.push(defaultRoleRow())
|
||
}
|
||
function removeRoleRowAt(idx) {
|
||
if (form.roleRows.length <= 1) {
|
||
ElMessage.warning('至少保留 1 行角色')
|
||
return
|
||
}
|
||
form.roleRows.splice(idx, 1)
|
||
}
|
||
function syncProjectData() {
|
||
ElMessage.info('同步数据:从外部数据源拉取最新项目信息 (后端支持时启用)')
|
||
}
|
||
|
||
// ===== ecology 项目编号远程搜索 + 自动拉取 =====
|
||
const projectNoOptions = ref([])
|
||
const projectNoLoading = ref(false)
|
||
let _projectNoTimer = null
|
||
|
||
// 项目编号边输入边搜索 (空关键词清空, 不发请求)
|
||
function searchProjectNos(query) {
|
||
if (_projectNoTimer) clearTimeout(_projectNoTimer)
|
||
_projectNoTimer = setTimeout(async () => {
|
||
const kw = (query || '').trim()
|
||
if (!kw) {
|
||
projectNoOptions.value = []
|
||
projectNoLoading.value = false
|
||
return
|
||
}
|
||
projectNoLoading.value = true
|
||
try {
|
||
const r = await searchEcologyProjects({ xmbh: kw })
|
||
projectNoOptions.value = (r?.data) || []
|
||
} catch (e) {
|
||
projectNoOptions.value = []
|
||
} finally {
|
||
projectNoLoading.value = false
|
||
}
|
||
}, 200)
|
||
}
|
||
|
||
// 选中项目编号 → 拉取完整信息并覆盖表单 (负责人静默建档由后端完成)
|
||
async function onProjectNoChange(val) {
|
||
if (!val) return
|
||
// 仅当所选值来自 ecology 下拉时才拉取 (allow-create 自由输入不拉, 避免无谓请求)
|
||
const matched = projectNoOptions.value.find(p => p.projectNo === val)
|
||
if (!matched) return
|
||
try {
|
||
const res = await pullEcologyProject(val, { __silentError: true })
|
||
const d = res?.data || res
|
||
if (!d) return
|
||
form.projectName = d.projectName || ''
|
||
form.projectForm = d.projectForm || ''
|
||
form.totalAmount = d.totalAmount != null ? Number(d.totalAmount) : 0
|
||
form.manageFee = d.manageFee != null ? Number(d.manageFee) : 0
|
||
form.startTime = d.startTime || ''
|
||
form.endTime = d.endTime || ''
|
||
if (d.leaderUserId != null) {
|
||
form.leadUserId = d.leaderUserId
|
||
// 把自动建档的负责人塞进 leaderOptions, 避免 el-select 选中后 label 空白
|
||
if (!leaderOptions.value.find(u => u.userId === d.leaderUserId)) {
|
||
leaderOptions.value = [{ userId: d.leaderUserId, nickName: d.leaderName, userName: d.leaderPhone }, ...leaderOptions.value]
|
||
}
|
||
}
|
||
} catch (e) {
|
||
// ecology 未找到/不可达: 静默, 保留用户手填内容
|
||
}
|
||
}
|
||
|
||
// 编辑模式:加载已有项目数据并反显到表单
|
||
async function loadProject() {
|
||
if (!isEdit || !projectId) return
|
||
try {
|
||
const res = await bizGet('project', projectId)
|
||
// res 结构: {code:200, msg, data: BizProject} (单条 success 包了一层)
|
||
const p = res?.data?.data || res?.data || res
|
||
if (!p) return
|
||
form.projectName = p.projectName || ''
|
||
form.projectNo = p.projectNo || ''
|
||
// 编辑模式: 项目编号 select 预置当前项, 避免远程搜索 select 无选项时 label 空白
|
||
if (form.projectNo) projectNoOptions.value = [{ projectNo: form.projectNo, projectName: form.projectName }]
|
||
form.projectForm = p.projectForm || ''
|
||
form.totalSessions = p.totalSessions || 0
|
||
form.totalAmount = p.totalAmount || 0
|
||
form.manageFee = p.manageFee || 0
|
||
form.startTime = p.startTime || ''
|
||
form.endTime = p.endTime || ''
|
||
form.leadUserId = p.leadUserId || null
|
||
form.isBidProject = p.isBidProject || 'N'
|
||
// 角色劳务反显: biz_project.role_labor JSON 数组 [{role, customName, amount}]
|
||
// 空数组/解析失败 → 保留 form 默认 5 行 (主席/主持/讲者/点评/评审/讨论)
|
||
try {
|
||
const rl = (typeof p.roleLabor === 'string') ? JSON.parse(p.roleLabor || '[]') : p.roleLabor
|
||
if (Array.isArray(rl) && rl.length) form.roleRows = rl
|
||
} catch (e) { /* keep default */ }
|
||
// 公告文件反显: 优先 publicityFiles JSON 数组, 否则兼容旧字段
|
||
const files = []
|
||
if (Array.isArray(p.publicityFiles)) files.push(...p.publicityFiles)
|
||
else if (typeof p.publicityFiles === 'string' && p.publicityFiles) {
|
||
try { files.push(...JSON.parse(p.publicityFiles)) } catch (e) { /* ignore */ }
|
||
}
|
||
if (files.length) {
|
||
// 按 type 反显到对应 notice 行
|
||
for (const n of form.notices) {
|
||
const m = files.find(f => f.type === n.key)
|
||
if (m) { n.url = m.url || ''; n.name = m.name || '' }
|
||
}
|
||
} else {
|
||
// 兼容老数据 (独立列: invitationUrl/supportLetterUrl/publishUrl/scheduleUrl)
|
||
const noticeMap = {
|
||
invitation: p.invitationUrl,
|
||
supportLetter: p.supportLetterUrl,
|
||
notice: p.publishUrl,
|
||
schedule: p.scheduleUrl
|
||
}
|
||
for (const n of form.notices) {
|
||
n.url = noticeMap[n.key] || ''
|
||
n.name = n.url ? (n.url.split('/').pop() || '') : ''
|
||
}
|
||
}
|
||
// 加载已有执行方分配 (暂不实现, 留 hook)
|
||
// await loadAssigns(projectId)
|
||
} catch (e) {
|
||
ElMessage.error('加载项目数据失败: ' + (e?.msg || e?.message || e))
|
||
}
|
||
}
|
||
|
||
// 加载项目负责人 (sys_user.role_type='leader')
|
||
async function loadLeaders(query) {
|
||
if (_managerTimer) clearTimeout(_managerTimer)
|
||
_managerTimer = setTimeout(async () => {
|
||
try {
|
||
const params = { pageNum: 1, pageSize: 50 }
|
||
if (query) params.userName = query
|
||
const r = await listLeaders(params)
|
||
leaderOptions.value = (r.data && r.data.rows) || r.rows || []
|
||
// 已选项不在结果里时按 userId 单独拉取并加首位, 避免 el-select 选中后 label 空白
|
||
if (form.leadUserId && !leaderOptions.value.find(u => u.userId === form.leadUserId)) {
|
||
const r2 = await listLeaders({ userId: form.leadUserId })
|
||
const sel = (r2.data && r2.data.rows) || r2.rows || []
|
||
if (sel[0]) leaderOptions.value = [sel[0], ...leaderOptions.value]
|
||
}
|
||
} catch (e) { leaderOptions.value = [] }
|
||
}, 200)
|
||
}
|
||
function searchLeaders(q) { loadLeaders(q) }
|
||
|
||
onMounted(() => {
|
||
loadProject()
|
||
loadLeaders('')
|
||
})
|
||
function goBack() {
|
||
// 带 _t 强制 Projects.vue watch 触发 reload, 即便路由一样也重载
|
||
// 本页被 admin/manager 复用, 按当前路由前缀回各自项目列表 (否则 admin 会被角色守卫拦回工作台)
|
||
const base = '/' + route.path.split('/')[1]
|
||
router.push({ path: `${base}/projects`, query: { _t: Date.now() } })
|
||
}
|
||
function confirmCancel() {
|
||
ElMessageBox.confirm(`确定取消${isEdit ? '编辑' : '新建'}项目?未保存的内容将丢失`, '提示', { type: 'warning' })
|
||
.then(() => goBack())
|
||
.catch(() => {})
|
||
}
|
||
async function submit(mode = 'save') {
|
||
await formRef.value.validate()
|
||
const uploaded = form.notices.filter(n => n.url)
|
||
const payload = {
|
||
projectName: form.projectName,
|
||
projectNo: form.projectNo,
|
||
projectForm: form.projectForm,
|
||
totalSessions: form.totalSessions,
|
||
totalAmount: form.totalAmount,
|
||
manageFee: form.manageFee,
|
||
startTime: form.startTime,
|
||
endTime: form.endTime,
|
||
leadUserId: form.leadUserId,
|
||
isBidProject: form.isBidProject
|
||
}
|
||
// 角色劳务: 过滤空 role, 序列化为 JSON 字符串存到 biz_project.role_labor (TEXT 列)
|
||
// "其他" 角色把 customName 作为 role 显示名 (与 ManagerProjectDetail 渲染一致)
|
||
const roleRowsPayload = form.roleRows
|
||
.filter(r => r.role && String(r.role).trim() !== '')
|
||
.map(r => ({
|
||
role: r.role,
|
||
customName: r.role === '其他' ? (r.customName || '') : '',
|
||
amount: Number(r.amount) || 0
|
||
}))
|
||
payload.roleLabor = JSON.stringify(roleRowsPayload)
|
||
// 公告文件以 publicityFiles JSON 数组存储 (替代旧 invitationUrl/supportLetterUrl/publishUrl 独立列)
|
||
payload.publicityFiles = JSON.stringify(uploaded.map(n => ({
|
||
type: n.key,
|
||
label: n.label,
|
||
url: n.url,
|
||
name: n.name || (n.url ? n.url.split('/').pop() || '' : '')
|
||
})))
|
||
// 兼容旧字段: 同步写一份 (保持后端字段兼容, 新逻辑以 publicityFiles 为准)
|
||
for (const n of uploaded) {
|
||
if (n.key === 'invitation') payload.invitationUrl = n.url
|
||
else if (n.key === 'supportLetter') payload.supportLetterUrl = n.url
|
||
else if (n.key === 'notice') payload.publishUrl = n.url
|
||
else if (n.key === 'schedule') payload.scheduleUrl = n.url
|
||
}
|
||
// 发布时设置 is_published='1' + publish_time (同步后端字段)
|
||
if (mode === 'publish') {
|
||
payload.isPublished = '1'
|
||
payload.publishTime = nowLocalDatetime()
|
||
}
|
||
// 多执行方分配已移至"项目分配"页面单独管理
|
||
try {
|
||
submitting.value = true
|
||
let savedProjectId = projectId
|
||
if (isEdit) {
|
||
payload.projectId = projectId
|
||
await bizUpdate('project', payload)
|
||
} else {
|
||
const r = await bizAdd('project', payload)
|
||
// bizAdd 返回 { code, msg, data: { projectId: '...' } } 或 直接 data
|
||
savedProjectId = r?.data?.projectId || r?.projectId || r?.data
|
||
}
|
||
const successText = isEdit
|
||
? (mode === 'publish' ? '更新并发布公告成功' : '更新成功')
|
||
: (mode === 'publish' ? '保存并发布公告成功' : '保存成功')
|
||
ElMessage.success(successText)
|
||
goBack()
|
||
} catch (e) {
|
||
ElMessage.error(e?.msg || (isEdit ? '更新失败' : '新建失败'))
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* 整页面板 (与 ManagerProjectDetail.vue 风格一致: 新建/编辑页 max-width 1200px, 居左, 不居中) */
|
||
.manager-projects-new {
|
||
background: #fff;
|
||
padding: 16px 20px;
|
||
border-radius: 6px;
|
||
border: 1px solid #f0f0f0;
|
||
max-width: 1200px;
|
||
}
|
||
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
|
||
.breadcrumb .current { color: #262626; font-weight: 500; }
|
||
|
||
/* 表单 (整页一个 form) */
|
||
.project-form { max-width: 100%; }
|
||
|
||
/* 章节标题 (替换原 .new-card 容器, 用分隔线分割 3 块) */
|
||
.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; }
|
||
|
||
.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; }
|
||
.role-row { display: flex; align-items: center; gap: 12px; padding: 6px 0; }
|
||
.role-select { width: 160px; flex-shrink: 0; display: flex; align-items: center; }
|
||
.role-custom { flex: 1; max-width: 180px; }
|
||
.role-amount { width: 180px; }
|
||
.row-actions { display: flex; gap: 8px; }
|
||
/* 项目信息数字输入框: 桌面限制宽度, 不撑满双列 */
|
||
.num-input { width: 200px; max-width: 100%; }
|
||
/* 公告文件上传: 限制最大宽度, 上传按钮不要太长 */
|
||
.notice-uploader { flex: 1; min-width: 0; max-width: 480px; }
|
||
.notice-list { display: flex; flex-direction: column; gap: 10px; }
|
||
.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;
|
||
}
|
||
.notice-uploader { flex: 1; }
|
||
/* 紧凑版 (覆盖 OssFileUploader 默认尺寸) */
|
||
.notice-uploader :deep(.ht-file-upload) { padding: 6px 12px; min-height: 40px; gap: 8px; }
|
||
.notice-uploader :deep(.placeholder-text) { font-size: 13px; }
|
||
.notice-uploader :deep(.placeholder-hint) { font-size: 11px; }
|
||
.notice-uploader :deep(.file-name) { font-size: 13px; }
|
||
.notice-uploader :deep(.file-type) { font-size: 11px; }
|
||
.notice-uploader :deep(.file-icon) { font-size: 18px; }
|
||
.notice-uploader :deep(.file-icon svg) { width: 18px; height: 18px; }
|
||
|
||
/* 底部按钮 (skill: 用 el-form-item 包裹, 不用 div.form-actions) */
|
||
.form-actions {
|
||
margin-top: 24px;
|
||
padding-top: 24px;
|
||
border-top: 1px solid #f0f0f0;
|
||
}
|
||
/* 按钮所在 form-item 不显示 label 列 (skill form-actions 包裹的标准做法) */
|
||
.form-actions :deep(.el-form-item__label) {
|
||
display: none;
|
||
}
|
||
.form-actions :deep(.el-form-item__content) {
|
||
margin-left: 0 !important;
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
|
||
|
||
/* ========================================
|
||
移动端适配 (≤768px)
|
||
- 卡片 padding 收窄
|
||
- el-row 双列 → 单列, form-item 横向 (label-width=120px 桌面默认沿用, 让 Element Plus 自然对齐)
|
||
- 角色劳务行 / 公告文件行 / 底部按钮: 紧凑布局
|
||
======================================== */
|
||
@media (max-width: 768px) {
|
||
/* 卡片 padding */
|
||
.manager-projects-new { 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 8px !important; }
|
||
|
||
/* 双列 → 单列: el-row 强制 block, el-col 100% 宽 */
|
||
.project-form :deep(.el-row) { display: block !important; }
|
||
.project-form :deep(.el-col) {
|
||
width: 100% !important;
|
||
max-width: 100% !important;
|
||
flex: 0 0 100% !important;
|
||
display: block !important;
|
||
}
|
||
|
||
/* form-item: 横向 label + content, flex-start 让 error message 占独立行
|
||
不动 __label 的 float/width/height/line-height — Element Plus 自带 label-width=120px
|
||
自然右对齐 + 跟随 content 行高, 不会参差不齐 */
|
||
.project-form :deep(.el-form-item) {
|
||
display: flex !important;
|
||
align-items: flex-start !important;
|
||
margin-right: 0 !important;
|
||
}
|
||
.project-form :deep(.el-form-item__content) {
|
||
flex: 1 !important;
|
||
min-width: 0 !important;
|
||
}
|
||
|
||
/* 表单控件全宽, 强制拉伸 (项目信息 section 的输入控件全宽)
|
||
注意: .role-row 内的控件有 .role-select/.role-amount 容器保护, 不会吃这条规则 */
|
||
.project-form :deep(.el-form-item .el-select),
|
||
.project-form :deep(.el-form-item .el-input),
|
||
.project-form :deep(.el-form-item .el-textarea),
|
||
.project-form :deep(.el-form-item .el-date-editor),
|
||
.project-form :deep(.el-form-item .el-input-number),
|
||
.project-form :deep(.el-form-item .el-cascader) {
|
||
width: 100% !important;
|
||
min-width: 0 !important;
|
||
display: block !important;
|
||
}
|
||
|
||
/* 数字输入框: 移动端占满 (有 .num-input 容器保护, 不会挤压其它数字输入) */
|
||
.project-form .num-input { width: 100% !important; }
|
||
|
||
/* 角色劳务行: 保持桌面原有横向布局 (角色/金额/删除) 一行, 不被表单规则拉宽 */
|
||
.role-row { flex-wrap: wrap !important; gap: 6px !important; }
|
||
.role-row .role-select { width: 110px !important; flex-shrink: 0; display: flex !important; align-items: center !important; }
|
||
.role-row .role-amount { width: 130px !important; flex-shrink: 0; }
|
||
.role-row .row-actions { margin-left: auto; }
|
||
|
||
/* 公告文件行: 横向 (label + uploader), label 顶部对齐 */
|
||
.notice-row { gap: 8px !important; margin-bottom: 10px !important; }
|
||
.notice-label {
|
||
width: 70px !important;
|
||
font-size: 12px !important;
|
||
}
|
||
/* 移动端: uploader 限制固定宽度 280px (不撑满不溢出) */
|
||
.notice-uploader { width: 280px !important; max-width: 100% !important; min-width: 0 !important; flex: none !important; }
|
||
|
||
/* 底部按钮: 等宽并排 */
|
||
.form-actions :deep(.el-form-item__content) {
|
||
display: flex !important;
|
||
flex-wrap: wrap !important;
|
||
gap: 8px !important;
|
||
}
|
||
.form-actions :deep(.el-button) {
|
||
flex: 1 1 0 !important;
|
||
width: 0 !important;
|
||
}
|
||
}
|
||
</style> |