refactor: 合并 biz_support_unit/biz_execution_unit/biz_service_org 为 biz_org, 删除 2 张孤儿表, 改 biz_person.work_unit → org_id FK
主要改动: - SQL: biz_support_unit → biz_org, 加 org_type, 加 business_nature; 删 biz_execution_unit, biz_service_org - SQL: biz_project.support_unit_id/name + service_org_id/name → org_id/name/type - SQL: biz_meeting.support_unit_name → org_name - SQL: biz_person.work_unit → org_id (FK) - 后端: 新 BizOrg entity/mapper/service/controller - 后端: BizProject/BizMeeting 字段重命名 - 后端: 删 12 个 BizSupportUnit/BizExecutionUnit/BizServiceOrg Java 文件 - 后端: BizPersonImportVO.workUnit → orgName (导入时查 biz_org 取 org_id) - 后端: BizAuthController.registerExecutor 完整实现 (原 registerSupplier stub) - 前端: 新 admin/Orgs.vue + manager/Orgs.vue (原 SupportUnits) - 前端: RegisterExecutor.vue (原 RegisterSupplier, 单页 2 步) - 前端: sponsor/executor/manager 多个文件 workUnit → orgId/orgName 重命名 - 前端: 统一 supplier → executor, 业务命名 sponsor(赞助方) / executor(执行方=供应商) - 前端: 全工程 execution → executor (company type / role / person unit_type)
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div class="manager-accounts">
|
||||
<div class="page-card">
|
||||
<div class="desc">查看当前登录账号、修改个人信息与密码</div>
|
||||
<el-descriptions :column="2" border style="margin-bottom: 24px">
|
||||
<el-descriptions-item label="账号">{{ profile.userName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="姓名">{{ profile.nickName || profile.userName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="角色">{{ (profile.roleGroup || '').split(',')[0] || '合规人员' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">{{ profile.status === '0' ? '正常' : '停用' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="手机">{{ profile.phonenumber || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="邮箱">{{ profile.email || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间" :span="2">{{ profile.createTime || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-form :model="form" label-width="120px" style="max-width:600px" :rules="rules" ref="formRef">
|
||||
<div class="section-title">修改个人信息</div>
|
||||
<el-form-item label="姓名" prop="nickName"><el-input v-model="form.nickName" placeholder="请输入姓名" /></el-form-item>
|
||||
<el-form-item label="手机号" prop="phonenumber"><el-input v-model="form.phonenumber" placeholder="请输入手机号" /></el-form-item>
|
||||
<el-form-item label="邮箱" prop="email"><el-input v-model="form.email" placeholder="请输入邮箱" /></el-form-item>
|
||||
|
||||
<div class="section-title">修改密码</div>
|
||||
<el-form-item label="原密码" prop="oldPassword"><el-input v-model="form.oldPassword" type="password" show-password placeholder="请输入原密码" /></el-form-item>
|
||||
<el-form-item label="新密码" prop="newPassword"><el-input v-model="form.newPassword" type="password" show-password placeholder="请输入新密码" /></el-form-item>
|
||||
<el-form-item label="确认密码" prop="confirmPassword"><el-input v-model="form.confirmPassword" type="password" show-password placeholder="请再次输入新密码" /></el-form-item>
|
||||
<div class="form-tip">* 密码长度 6-20 位,支持数字、字母、特殊字符;留空表示不修改密码</div>
|
||||
<el-form-item><el-button type="primary" :loading="saving" @click="onSave">保存</el-button><el-button @click="onCancel">取消</el-button></el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const form = reactive({ nickName: '', phonenumber: '', email: '', oldPassword: '', newPassword: '', confirmPassword: '' })
|
||||
const profile = ref({})
|
||||
const saving = ref(false)
|
||||
const formRef = ref(null)
|
||||
|
||||
const rules = {
|
||||
nickName: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
|
||||
phonenumber: [{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }],
|
||||
email: [{ type: 'email', message: '邮箱格式不正确', trigger: 'blur' }],
|
||||
oldPassword: [{ required: false }],
|
||||
newPassword: [{ min: 6, max: 20, message: '密码长度 6-20 位', trigger: 'blur' }],
|
||||
confirmPassword: [{
|
||||
validator: (rule, val, cb) => {
|
||||
if (form.newPassword && val !== form.newPassword) return cb(new Error('两次输入密码不一致'))
|
||||
cb()
|
||||
}, trigger: 'blur'
|
||||
}],
|
||||
}
|
||||
|
||||
async function loadProfile() {
|
||||
try {
|
||||
const { data } = await request({ url: '/system/user/profile', method: 'get' })
|
||||
profile.value = data || {}
|
||||
form.nickName = profile.value.nickName || ''
|
||||
form.phonenumber = profile.value.phonenumber || ''
|
||||
form.email = profile.value.email || ''
|
||||
} catch { ElMessage.error('获取个人信息失败') }
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
if (form.newPassword && form.newPassword !== form.confirmPassword) {
|
||||
ElMessage.error('两次输入密码不一致'); return
|
||||
}
|
||||
await formRef.value.validate()
|
||||
saving.value = true
|
||||
try {
|
||||
await request({ url: '/system/user/profile', method: 'put', data: { nickName: form.nickName, phonenumber: form.phonenumber, email: form.email, sex: profile.value.sex } })
|
||||
if (form.newPassword) {
|
||||
await request({ url: '/system/user/profile/updatePwd', method: 'put', data: { oldPassword: form.oldPassword, newPassword: form.newPassword } })
|
||||
}
|
||||
ElMessage.success('保存成功')
|
||||
form.oldPassword = ''; form.newPassword = ''; form.confirmPassword = ''
|
||||
loadProfile()
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.msg || '保存失败')
|
||||
} finally { saving.value = false }
|
||||
}
|
||||
function onCancel() {
|
||||
form.nickName = profile.value.nickName || ''
|
||||
form.phonenumber = profile.value.phonenumber || ''
|
||||
form.email = profile.value.email || ''
|
||||
form.oldPassword = ''; form.newPassword = ''; form.confirmPassword = ''
|
||||
}
|
||||
|
||||
onMounted(loadProfile)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.manager-accounts { padding: 16px; }
|
||||
.page-card { background: #fff; border-radius: 6px; padding: 24px; border: 1px solid #ebeef5; }
|
||||
.desc { color: #909399; font-size: 13px; margin-bottom: 16px; }
|
||||
.section-title { font-weight: 600; margin: 16px 0 12px; padding-left: 8px; border-left: 3px solid var(--brand-primary); }
|
||||
.form-tip { color: #909399; font-size: 12px; margin-bottom: 12px; }
|
||||
</style>
|
||||
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<div class="manager-exec-intent">
|
||||
<div class="page-card">
|
||||
<div class="desc">查看专家对项目的执行意向(含待签约/已签约/已拒绝/已退出)</div>
|
||||
<el-form inline :model="q">
|
||||
<el-form-item label="项目编号:"><el-input v-model="q.projectNo" clearable /></el-form-item>
|
||||
<el-form-item label="项目名称:"><el-input v-model="q.projectName" clearable /></el-form-item>
|
||||
<el-form-item label="姓名:"><el-input v-model="q.name" clearable /></el-form-item>
|
||||
<el-form-item label="工作单位:"><el-input v-model="q.workUnit" clearable /></el-form-item>
|
||||
<el-form-item label="手机号:"><el-input v-model="q.phone" clearable /></el-form-item>
|
||||
<el-form-item label="签约状态:">
|
||||
<el-select v-model="q.onboardStatus" clearable>
|
||||
<el-option label="待签约" value="待签约" /><el-option label="已签约" value="已签约" />
|
||||
<el-option label="已拒绝" value="已拒绝" /><el-option label="已退出" value="已退出" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item><el-button type="primary" @click="load">查找</el-button><el-button @click="reset">重置</el-button></el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="page-card">
|
||||
<el-table :data="rows" v-loading="loading" stripe border>
|
||||
<el-table-column type="index" label="#" width="50" />
|
||||
<el-table-column prop="projectNo" label="意向项目编号" width="140" />
|
||||
<el-table-column prop="projectName" label="意向项目名称" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="name" label="姓名" width="100" />
|
||||
<el-table-column prop="workUnit" label="工作单位" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="department" label="部门" width="120" />
|
||||
<el-table-column prop="position" label="职务" width="100" />
|
||||
<el-table-column prop="phone" label="手机号" width="130" />
|
||||
<el-table-column label="签约状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.onboardStatus==='已签约' ? 'success' : row.onboardStatus==='已拒绝' ? 'danger' : row.onboardStatus==='已退出' ? 'info' : 'warning'">{{ row.onboardStatus }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" width="170" />
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize" :total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper" @current-change="load" @size-change="load" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { bizList } from '@/api/public'
|
||||
|
||||
const q = ref({ projectNo: '', projectName: '', name: '', workUnit: '', phone: '', onboardStatus: '' })
|
||||
const rows = ref([])
|
||||
const loading = ref(false)
|
||||
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const { data } = await bizList('executionIntent', { ...q.value, pageNum: page.pageNum, pageSize: page.pageSize })
|
||||
rows.value = data?.rows || []; page.total = data?.total || 0
|
||||
} catch { rows.value = []; page.total = 0 }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
function reset() { q.value = { projectNo:'', projectName:'', name:'', workUnit:'', phone:'', onboardStatus:'' }; page.pageNum = 1; load() }
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.manager-exec-intent { padding: 16px; }
|
||||
.page-card { background: #fff; border-radius: 6px; padding: 16px; border: 1px solid #ebeef5; margin-bottom: 12px; }
|
||||
.page-title { font-size: 20px; font-weight: 600; margin-bottom: 4px; }
|
||||
.desc { color: #909399; font-size: 13px; margin-bottom: 12px; }
|
||||
</style>
|
||||
@@ -0,0 +1,446 @@
|
||||
<template>
|
||||
<div class="page-card">
|
||||
<div class="desc">审核专家注册信息</div>
|
||||
|
||||
<!-- ========== 筛选区 (按原型 top=174: 姓名/手机号/工作单位/审核状态) ========== -->
|
||||
<el-form inline :model="q" class="filter-form">
|
||||
<el-form-item label="姓名">
|
||||
<el-input v-model="q.name" placeholder="" clearable style="width:160px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号">
|
||||
<el-input v-model="q.phone" placeholder="" clearable style="width:160px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="工作单位">
|
||||
<el-input v-model="q.workUnit" placeholder="" clearable style="width:200px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="审核状态">
|
||||
<el-select v-model="q.auditStatus" placeholder="请选择" clearable style="width:140px">
|
||||
<el-option label="待审核" value="0" />
|
||||
<el-option label="通过" value="1" />
|
||||
<el-option label="退回" value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="load">查找</el-button>
|
||||
<el-button @click="reset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- ========== 批量按钮区 (按原型 top=226: 新建专家/批量导入/批量审核/批量导入执业证书/批量导入职称证明) ========== -->
|
||||
<div class="page-card">
|
||||
<div class="batch-bar">
|
||||
<el-button type="primary" @click="openAdd">新建专家</el-button>
|
||||
<el-button @click="openImport">批量导入</el-button>
|
||||
<el-button @click="onBatchAudit" :disabled="!selection.length">批量审核</el-button>
|
||||
<el-button @click="openImportPracticeCert">批量导入执业证书/证明</el-button>
|
||||
<el-button @click="openImportTitleCert">批量导入职称证明</el-button>
|
||||
</div>
|
||||
|
||||
<!-- ========== 表格 (按原型 11 列, 按 left 顺序) ========== -->
|
||||
<el-table :data="rows" v-loading="loading" stripe border @selection-change="onSel">
|
||||
<el-table-column type="selection" width="44" />
|
||||
<el-table-column prop="name" label="姓名" width="100" fixed />
|
||||
<el-table-column prop="phone" label="手机号" width="130" />
|
||||
<el-table-column prop="workUnit" label="工作单位" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="department" label="科室" width="100" />
|
||||
<el-table-column prop="title" label="职称" width="100" />
|
||||
<el-table-column label="执业证书/证明" width="180">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.practiceCertUrl">
|
||||
<el-button link type="primary" @click="onViewCert(row.practiceCertUrl)">查看</el-button>
|
||||
<el-button link type="primary" @click="onDownloadCert(row.practiceCertUrl, row.phone)">下载</el-button>
|
||||
</template>
|
||||
<span v-else style="color:#c0c4cc">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="职称证明" width="180">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.titleCertUrl">
|
||||
<el-button link type="primary" @click="onViewCert(row.titleCertUrl)">查看</el-button>
|
||||
<el-button link type="primary" @click="onDownloadCert(row.titleCertUrl, row.phone)">下载</el-button>
|
||||
</template>
|
||||
<span v-else style="color:#c0c4cc">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="审核状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span :class="['stage-tag', auditStatusClass(row.auditStatus)]">{{ auditStatusText(row.auditStatus) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="auditTime" label="审批时间" width="160" align="center">
|
||||
<template #default="{ row }">{{ fmtTime(row.auditTime) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="auditBy" label="审批人" width="120" align="center" />
|
||||
<el-table-column label="操作" width="280" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="onAudit(row)">审核</el-button>
|
||||
<el-button link type="primary" @click="onEdit(row)">修改</el-button>
|
||||
<el-button link type="primary" @click="onView(row)">查看</el-button>
|
||||
<el-button link :type="row.status === '1' ? 'danger' : 'success'" @click="row.status === '1' ? onDisable(row) : onEnable(row)">
|
||||
{{ row.status === '1' ? '禁用' : '恢复' }}
|
||||
</el-button>
|
||||
<el-button link type="info" v-if="row.auditStatus === '2'" @click="onViewAuditOpinion(row)">已退回</el-button>
|
||||
<el-button link type="primary" v-if="row.practiceCertUrl || row.titleCertUrl || row.resumeUrl" @click="onDownloadAll(row)">下载</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize" :total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper" @current-change="load" @size-change="load" />
|
||||
</div>
|
||||
|
||||
<!-- ========== Dialog: 新建/编辑专家 (按原型 top=239 确定/取消) ========== -->
|
||||
<el-dialog v-model="editOpen" :title="editForm.expertId ? '编辑专家' : '新建专家'" width="640px">
|
||||
<el-form :model="editForm" label-width="120px" :rules="editRules" ref="editFormRef">
|
||||
<el-form-item label="姓名" prop="name"><el-input v-model="editForm.name" /></el-form-item>
|
||||
<el-form-item label="手机号" prop="phone"><el-input v-model="editForm.phone" maxlength="11" /></el-form-item>
|
||||
<el-form-item label="工作单位" prop="workUnit"><el-input v-model="editForm.workUnit" /></el-form-item>
|
||||
<el-form-item label="科室" prop="department"><el-input v-model="editForm.department" placeholder="如:呼吸科" /></el-form-item>
|
||||
<el-form-item label="职称" prop="title">
|
||||
<el-select v-model="editForm.title" placeholder="请选择" style="width:100%">
|
||||
<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>
|
||||
</el-form-item>
|
||||
<el-form-item label="执业证书 URL"><el-input v-model="editForm.practiceCertUrl" /></el-form-item>
|
||||
<el-form-item label="职称证明 URL"><el-input v-model="editForm.titleCertUrl" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="editOpen=false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submitEdit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- ========== Dialog: 审核 (原型 top=404 确认/取消) ========== -->
|
||||
<el-dialog v-model="auditOpen" title="审核" width="520px">
|
||||
<p style="margin-bottom:12px">专家: <b>{{ auditForm.name }}</b> ({{ auditForm.phone }})</p>
|
||||
<el-form :model="auditForm" label-width="100px">
|
||||
<el-form-item label="审核结果">
|
||||
<el-radio-group v-model="auditForm.result">
|
||||
<el-radio value="1">通过</el-radio>
|
||||
<el-radio value="2">退回</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="审核意见">
|
||||
<el-input v-model="auditForm.opinion" type="textarea" :rows="4" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="auditOpen=false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submitAudit">确认</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- ========== Dialog: 批量导入 (原型 top=239 确定/取消, 含 radio 通过/退回) ========== -->
|
||||
<el-dialog v-model="importOpen" title="批量导入专家" width="520px">
|
||||
<div style="margin-bottom:12px;font-size:13px">
|
||||
<el-link type="primary" @click="downloadImportTpl">批量导入模板下载</el-link>
|
||||
</div>
|
||||
<p style="color:#606266;margin-bottom:8px">*批量导入模板 xls 与列表表项一致</p>
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="审核结果">
|
||||
<el-radio-group v-model="importAuditResult">
|
||||
<el-radio value="1">通过</el-radio>
|
||||
<el-radio value="2">退回</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="审核意见">
|
||||
<el-input v-model="importAuditOpinion" type="textarea" :rows="3" placeholder="*新建和批量导入审核状态直接为审核通过" />
|
||||
</el-form-item>
|
||||
<el-form-item label="选择文件">
|
||||
<el-input v-model="importFileUrl" placeholder="上传文件 URL 或选择文件" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="importOpen=false">取消</el-button>
|
||||
<el-button type="primary" :loading="importing" @click="submitImport">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- ========== Dialog: 批量导入执业证书/证明 (原型 u6124) ========== -->
|
||||
<el-dialog v-model="importCertOpen" title="批量导入执业证书/证明" width="520px">
|
||||
<p style="color:#606266;margin-bottom:8px">*退回后,如专家重新提交,则审核状态变为待审核</p>
|
||||
<p style="color:#606266;margin-bottom:8px">提示:需将证明文件名称修改为手机号进行导入</p>
|
||||
<el-input v-model="importCertUrl" placeholder="上传文件 URL 或选择文件" />
|
||||
<template #footer>
|
||||
<el-button @click="importCertOpen=false">取消</el-button>
|
||||
<el-button type="primary" :loading="importing" @click="submitImportCert">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- ========== Dialog: 批量导入职称证明 (原型 u6126) ========== -->
|
||||
<el-dialog v-model="importTitleCertOpen" title="批量导入职称证明" width="520px">
|
||||
<p style="color:#606266;margin-bottom:8px">提示:需将证明文件名称修改为手机号进行导入</p>
|
||||
<el-input v-model="importTitleCertUrl" placeholder="上传文件 URL 或选择文件" />
|
||||
<template #footer>
|
||||
<el-button @click="importTitleCertOpen=false">取消</el-button>
|
||||
<el-button type="primary" :loading="importing" @click="submitImportTitleCert">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- ========== Dialog: 查看专家详情 (原型 操作列 "查看" 按钮) ========== -->
|
||||
<el-dialog v-model="viewOpen" title="专家详情" width="640px">
|
||||
<el-descriptions :column="2" border v-if="currentRow">
|
||||
<el-descriptions-item label="姓名">{{ currentRow.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="手机号">{{ currentRow.phone }}</el-descriptions-item>
|
||||
<el-descriptions-item label="工作单位">{{ currentRow.workUnit }}</el-descriptions-item>
|
||||
<el-descriptions-item label="科室">{{ currentRow.department }}</el-descriptions-item>
|
||||
<el-descriptions-item label="职称">{{ currentRow.title }}</el-descriptions-item>
|
||||
<el-descriptions-item label="审核状态">{{ auditStatusText(currentRow.auditStatus) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="执业证书">
|
||||
<el-link v-if="currentRow.practiceCertUrl" type="primary" @click="onViewCert(currentRow.practiceCertUrl)">查看证书</el-link>
|
||||
<span v-else>-</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="职称证明">
|
||||
<el-link v-if="currentRow.titleCertUrl" type="primary" @click="onViewCert(currentRow.titleCertUrl)">查看证书</el-link>
|
||||
<span v-else>-</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="审批时间">{{ fmtTime(currentRow.auditTime) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="审批人">{{ currentRow.auditBy }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { bizList, bizAdd, bizUpdate } from '@/api/public'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const q = ref({ name: '', phone: '', workUnit: '', auditStatus: '' })
|
||||
const rows = ref([])
|
||||
const selection = ref([])
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const { data } = await bizList('expert', { ...q.value, pageNum: page.pageNum, pageSize: page.pageSize })
|
||||
rows.value = data?.rows || []
|
||||
page.total = data?.total || 0
|
||||
} catch { rows.value = []; page.total = 0 }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
function reset() {
|
||||
q.value = { name: '', phone: '', workUnit: '', auditStatus: '' }
|
||||
page.pageNum = 1
|
||||
load()
|
||||
}
|
||||
function onSel(rs) { selection.value = rs }
|
||||
|
||||
// ========== 工具: 审核状态显示/颜色 ==========
|
||||
function auditStatusText(s) {
|
||||
if (s === '1') return '通过'
|
||||
if (s === '2') return '退回'
|
||||
return '待审核'
|
||||
}
|
||||
function auditStatusClass(s) {
|
||||
if (s === '1') return 'stage-done'
|
||||
if (s === '2') return 'stage-reject'
|
||||
return 'stage-reviewing'
|
||||
}
|
||||
function fmtTime(d) {
|
||||
if (!d) return ''
|
||||
const dt = new Date(d)
|
||||
const pad = n => String(n).padStart(2, '0')
|
||||
return `${dt.getFullYear()}.${pad(dt.getMonth() + 1)}.${pad(dt.getDate())} ${pad(dt.getHours())}:${pad(dt.getMinutes())}`
|
||||
}
|
||||
|
||||
// ========== 新建/编辑 dialog ==========
|
||||
const editOpen = ref(false)
|
||||
const editFormRef = ref(null)
|
||||
const editForm = reactive({
|
||||
expertId: '', name: '', phone: '', workUnit: '', department: '', title: '',
|
||||
practiceCertUrl: '', titleCertUrl: ''
|
||||
})
|
||||
const editRules = {
|
||||
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
|
||||
phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }],
|
||||
workUnit: [{ required: true, message: '请输入工作单位', trigger: 'blur' }],
|
||||
department: [{ required: true, message: '请输入科室', trigger: 'blur' }],
|
||||
title: [{ required: true, message: '请选择职称', trigger: 'change' }]
|
||||
}
|
||||
function openAdd() {
|
||||
Object.assign(editForm, { expertId: '', name: '', phone: '', workUnit: '', department: '', title: '', practiceCertUrl: '', titleCertUrl: '' })
|
||||
editOpen.value = true
|
||||
}
|
||||
function onEdit(row) {
|
||||
Object.assign(editForm, {
|
||||
expertId: row.expertId, name: row.name, phone: row.phone, workUnit: row.workUnit,
|
||||
department: row.department, title: row.title,
|
||||
practiceCertUrl: row.practiceCertUrl || '', titleCertUrl: row.titleCertUrl || ''
|
||||
})
|
||||
editOpen.value = true
|
||||
}
|
||||
async function submitEdit() {
|
||||
await editFormRef.value.validate()
|
||||
submitting.value = true
|
||||
try {
|
||||
if (editForm.expertId) {
|
||||
await bizUpdate('expert', { ...editForm })
|
||||
ElMessage.success('修改成功')
|
||||
} else {
|
||||
await bizAdd('expert', { ...editForm, auditStatus: '1', auditBy: 'admin', auditTime: new Date().toISOString().slice(0, 19).replace('T', ' ') })
|
||||
ElMessage.success('新建成功')
|
||||
}
|
||||
editOpen.value = false
|
||||
load()
|
||||
} catch (e) { ElMessage.error(e?.msg || '保存失败') }
|
||||
finally { submitting.value = false }
|
||||
}
|
||||
|
||||
// ========== 审核 dialog ==========
|
||||
const auditOpen = ref(false)
|
||||
const auditForm = reactive({ expertId: '', name: '', phone: '', result: '1', opinion: '' })
|
||||
function onAudit(row) {
|
||||
auditForm.expertId = row.expertId
|
||||
auditForm.name = row.name
|
||||
auditForm.phone = row.phone
|
||||
auditForm.result = '1'
|
||||
auditForm.opinion = ''
|
||||
auditOpen.value = true
|
||||
}
|
||||
async function submitAudit() {
|
||||
submitting.value = true
|
||||
try {
|
||||
await bizUpdate('expert', {
|
||||
expertId: auditForm.expertId,
|
||||
auditStatus: auditForm.result,
|
||||
auditOpinion: auditForm.opinion,
|
||||
auditBy: 'admin', // 后端应取登录用户
|
||||
auditTime: new Date().toISOString().slice(0, 19).replace('T', ' ')
|
||||
})
|
||||
ElMessage.success(auditForm.result === '1' ? '审核通过' : '已退回')
|
||||
auditOpen.value = false
|
||||
load()
|
||||
} catch { ElMessage.error('审核失败') }
|
||||
finally { submitting.value = false }
|
||||
}
|
||||
|
||||
// ========== 批量审核 ==========
|
||||
async function onBatchAudit() {
|
||||
if (!selection.value.length) return ElMessage.warning('请先勾选行')
|
||||
try { await ElMessage.confirm(`确定批量审核选中的 ${selection.value.length} 位专家吗?`, '批量审核', { type: 'warning' }).catch(() => { throw new Error('cancel') }) } catch { return }
|
||||
submitting.value = true
|
||||
let ok = 0
|
||||
for (const r of selection.value) {
|
||||
try { await bizUpdate('expert', { expertId: r.expertId, auditStatus: '1', auditOpinion: '批量通过', auditBy: 'admin', auditTime: new Date().toISOString().slice(0, 19).replace('T', ' ') }); ok++ } catch {}
|
||||
}
|
||||
submitting.value = false
|
||||
ElMessage.success(`批量审核通过 ${ok}/${selection.value.length} 条`)
|
||||
selection.value = []
|
||||
load()
|
||||
}
|
||||
|
||||
// ========== 禁用/恢复 ==========
|
||||
async function onDisable(row) {
|
||||
try { await bizUpdate('expert', { expertId: row.expertId, status: '0' }); ElMessage.success('已禁用'); load() } catch { ElMessage.error('禁用失败') }
|
||||
}
|
||||
async function onEnable(row) {
|
||||
try { await bizUpdate('expert', { expertId: row.expertId, status: '1' }); ElMessage.success('已恢复'); load() } catch { ElMessage.error('恢复失败') }
|
||||
}
|
||||
|
||||
// ========== 查看详情 ==========
|
||||
const viewOpen = ref(false)
|
||||
const currentRow = ref(null)
|
||||
function onView(row) { currentRow.value = row; viewOpen.value = true }
|
||||
|
||||
// ========== 已退回: 查看退回意见 ==========
|
||||
function onViewAuditOpinion(row) {
|
||||
ElMessageBox.alert(
|
||||
`退回时间: ${fmtTime(row.auditTime) || '-'}\n审批人: ${row.auditBy || '-'}\n退回意见: ${row.auditOpinion || '(无)'}`,
|
||||
`专家 ${row.name} 审核已退回`,
|
||||
{ type: 'warning', confirmButtonText: '知道了' }
|
||||
).catch(() => {})
|
||||
}
|
||||
|
||||
// ========== 证书查看/下载 ==========
|
||||
function onViewCert(url) {
|
||||
if (!url) return
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
function onDownloadCert(url, phone) {
|
||||
if (!url) return
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `${phone || 'cert'}_${Date.now()}`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
}
|
||||
// 操作列 下载:按 执业证书 > 职称证明 > 简历 优先级下载第一个可用附件
|
||||
function onDownloadAll(row) {
|
||||
const url = row.practiceCertUrl || row.titleCertUrl || row.resumeUrl
|
||||
if (!url) return ElMessage.warning('暂无可下载的附件')
|
||||
onDownloadCert(url, row.phone)
|
||||
}
|
||||
|
||||
// ========== 批量导入 dialog ==========
|
||||
const importOpen = ref(false)
|
||||
const importAuditResult = ref('1')
|
||||
const importAuditOpinion = ref('')
|
||||
const importFileUrl = ref('')
|
||||
const importing = ref(false)
|
||||
function openImport() {
|
||||
importAuditResult.value = '1'
|
||||
importAuditOpinion.value = ''
|
||||
importFileUrl.value = ''
|
||||
importOpen.value = true
|
||||
}
|
||||
function downloadImportTpl() {
|
||||
ElMessage.info('批量导入模板下载 — 待后端提供静态文件')
|
||||
}
|
||||
async function submitImport() {
|
||||
if (!importFileUrl.value) return ElMessage.warning('请选择文件')
|
||||
importing.value = true
|
||||
await new Promise(r => setTimeout(r, 500))
|
||||
importing.value = false
|
||||
ElMessage.info(`已提交批量导入任务 (审核结果: ${importAuditResult.value === '1' ? '通过' : '退回'}) — 待后端实现`)
|
||||
importOpen.value = false
|
||||
}
|
||||
|
||||
// ========== 批量导入执业证书/证明 ==========
|
||||
const importCertOpen = ref(false)
|
||||
const importCertUrl = ref('')
|
||||
function openImportPracticeCert() { importCertUrl.value = ''; importCertOpen.value = true }
|
||||
async function submitImportCert() {
|
||||
if (!importCertUrl.value) return ElMessage.warning('请选择文件')
|
||||
importing.value = true
|
||||
await new Promise(r => setTimeout(r, 500))
|
||||
importing.value = false
|
||||
ElMessage.info('已提交批量导入执业证书任务 — 待后端实现')
|
||||
importCertOpen.value = false
|
||||
}
|
||||
|
||||
// ========== 批量导入职称证明 ==========
|
||||
const importTitleCertOpen = ref(false)
|
||||
const importTitleCertUrl = ref('')
|
||||
function openImportTitleCert() { importTitleCertUrl.value = ''; importTitleCertOpen.value = true }
|
||||
async function submitImportTitleCert() {
|
||||
if (!importTitleCertUrl.value) return ElMessage.warning('请选择文件')
|
||||
importing.value = true
|
||||
await new Promise(r => setTimeout(r, 500))
|
||||
importing.value = false
|
||||
ElMessage.info('已提交批量导入职称证明任务 — 待后端实现')
|
||||
importTitleCertOpen.value = false
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-card { background: #fff; border: 1px solid #ebeef5; border-radius: 6px; padding: 16px; margin-bottom: 12px; }
|
||||
.desc { color: #606266; font-size: 13px; margin-bottom: 12px; }
|
||||
.filter-form .el-form-item { margin-right: 12px; margin-bottom: 8px; }
|
||||
.batch-bar { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
|
||||
.stage-tag { display: inline-block; padding: 2px 10px; border-radius: 3px; font-size: 12px; border: 1px solid; }
|
||||
.stage-done { color: #67c23a; border-color: #c2e7b0; background: #f0f9eb; }
|
||||
.stage-reviewing { color: #e6a23c; border-color: #f5dab1; background: #fdf6ec; }
|
||||
.stage-reject { color: #f56c6c; border-color: #fbc4c4; background: #fef0f0; }
|
||||
</style>
|
||||
@@ -0,0 +1,439 @@
|
||||
<template>
|
||||
<div class="page-card">
|
||||
<!-- 面包屑 (与列表页一致) -->
|
||||
<div class="breadcrumb">
|
||||
首页 / 项目管理 / <span class="current">项目详情</span>
|
||||
</div>
|
||||
|
||||
<!-- ================= 1. 项目信息 (11 字段, 一列竖排) ================= -->
|
||||
<el-form v-if="row && row.projectId" label-position="left" class="info-form">
|
||||
<el-form-item label="项目编号">
|
||||
<span class="info-value">{{ row.projectNo || '-' }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="项目名称">
|
||||
<span class="info-value">{{ row.projectName || '-' }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="总场次/总期数">
|
||||
<span class="info-value">{{ row.totalSessions || 0 }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="总金额">
|
||||
<span class="info-value money">¥ {{ fmtMoney(row.totalAmount) }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="管理费及税金">
|
||||
<span class="info-value money">¥ {{ fmtMoney(row.manageFee) }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="项目开始时间">
|
||||
<span class="info-value">{{ fmtDate(row.startTime) }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="项目结束时间">
|
||||
<span class="info-value">{{ fmtDate(row.endTime) }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="赞助公司">
|
||||
<span class="info-value">{{ row.orgName || '-' }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="项目形式">
|
||||
<span class="info-value">{{ row.projectForm || '-' }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="项目评价">
|
||||
<a class="op-link" href="javascript:void(0)" @click="openScoreDialog">{{ fmtScore(row.ratingScore) }}</a>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- ================= 2. 角色劳务设置 ================= -->
|
||||
<div class="section-title">角色劳务设置</div>
|
||||
<p class="hint-red">*角色劳务设置针对该项目所有会议生效</p>
|
||||
<el-table :data="form.roleRows" border>
|
||||
<el-table-column prop="role" label="角色" width="120" />
|
||||
<el-table-column label="劳务金额" align="right">
|
||||
<template #default="{ row }">¥ {{ fmtMoney(row.amount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="80" align="center">
|
||||
<template #default="{ $index }">
|
||||
<el-button link type="danger" :disabled="form.roleRows.length<=1" @click="removeRoleRowAt($index)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div style="margin-top:8px">
|
||||
<el-button link type="primary" @click="addRoleRow">+ 增加角色</el-button>
|
||||
</div>
|
||||
|
||||
<!-- ================= 3. 支持单位监督员 ================= -->
|
||||
<div class="section-title">支持单位监督员</div>
|
||||
<p class="hint-red">*增加只显示已点支持意向的</p>
|
||||
<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.userName || m.name || '-' }}</span>
|
||||
<el-button link type="danger" @click="removeMonitorAt(idx)">删除</el-button>
|
||||
</div>
|
||||
<div v-if="!monitors.length" class="empty-hint">暂无监督员</div>
|
||||
</div>
|
||||
<div style="margin-top:8px">
|
||||
<el-button link type="primary" @click="openAddMonitor">+ 增加监督员</el-button>
|
||||
</div>
|
||||
|
||||
<!-- ================= 4. 已发布公告 ================= -->
|
||||
<div class="section-title">已发布公告</div>
|
||||
<div class="toolbar">
|
||||
<el-button type="primary" @click="openUpload">发布公告</el-button>
|
||||
<el-button @click="loadAnnouncements">刷新</el-button>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="announcements" border>
|
||||
<el-table-column prop="title" label="公告标题" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="createTime" label="发布时间" width="160" align="center" :formatter="(r)=>fmtDate(r.createTime)" />
|
||||
<el-table-column prop="creatorName" label="发布人" width="120" align="center" />
|
||||
<el-table-column label="操作" width="140" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openPreview(row.fileUrl || row.url, row.title)">查看</el-button>
|
||||
<el-button link type="danger" @click="deleteAnnouncement(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- ================= dialogs ================= -->
|
||||
<!-- 发布公告 dialog -->
|
||||
<el-dialog v-model="uploadOpen" title="发布公告" width="560px" @closed="resetUploadForm">
|
||||
<el-form :model="uploadForm" label-width="100px">
|
||||
<el-form-item label="公告类型">
|
||||
<el-select v-model="uploadForm.announceType" placeholder="请选择" style="width:100%">
|
||||
<el-option v-for="t in announceTypes" :key="t.key" :label="t.label" :value="t.key" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="公告标题">
|
||||
<el-input v-model="uploadForm.title" placeholder="请输入公告标题" maxlength="80" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="uploadForm.remark" type="textarea" :rows="2" placeholder="选填" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="uploadOpen=false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submitAnnouncement">发布</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 评分 dialog -->
|
||||
<el-dialog v-model="scoreOpen" title="项目评分" width="560px">
|
||||
<el-form :model="scoreForm" label-width="120px">
|
||||
<el-form-item v-for="d in scoreDims" :key="d.key" :label="d.label">
|
||||
<el-rate v-model="scoreForm[d.key]" :max="5" show-score />
|
||||
</el-form-item>
|
||||
<el-form-item label="总分">
|
||||
<span class="score-num">{{ totalScore }}</span> / 20
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="scoreOpen=false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submitScore">保存评分</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 增加监督员 dialog -->
|
||||
<el-dialog v-model="addMonitorOpen" title="增加监督员" width="420px">
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="监督员">
|
||||
<el-select v-model="newMonitor.userId" placeholder="请选择监督员" filterable style="width:100%">
|
||||
<el-option v-for="u in monitorCandidates" :key="u.userId"
|
||||
:label="`${u.userName}${u.nickName ? ' (' + u.nickName + ')' : ''}`" :value="u.userId" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="addMonitorOpen=false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmAddMonitor">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 公告预览 -->
|
||||
<Preview v-model="previewOpen" :url="previewUrl" :title="previewTitle" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { bizGet, bizList, bizAdd, bizUpdate, bizDelete } from '@/api/public'
|
||||
import Preview from '@/components/Preview.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// ===================== 状态 =====================
|
||||
const projectId = ref('')
|
||||
const row = ref({})
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
|
||||
const announceTypes = [
|
||||
{ key: 'invitation', label: '邀请函' },
|
||||
{ key: 'support', label: '支持函' },
|
||||
{ key: 'notice', label: '通知' },
|
||||
{ key: 'agenda', label: '日程' }
|
||||
]
|
||||
|
||||
const form = reactive({
|
||||
roleRows: [
|
||||
{ role: '主席', customName: '', amount: 5000 },
|
||||
{ role: '主持', customName: '', amount: 3000 }
|
||||
]
|
||||
})
|
||||
const monitors = ref([])
|
||||
const addMonitorOpen = ref(false)
|
||||
const newMonitor = reactive({ userId: null })
|
||||
const monitorCandidates = ref([])
|
||||
|
||||
const announcements = ref([])
|
||||
const uploadOpen = ref(false)
|
||||
const uploadForm = reactive({ announceType: 'notice', title: '', remark: '' })
|
||||
|
||||
const scoreOpen = ref(false)
|
||||
const scoreForm = reactive({ scoreQuality: 0, scoreSpeed: 0, scoreCoordination: 0, scoreCompliance: 0 })
|
||||
const scoreDims = [
|
||||
{ key: 'scoreQuality', label: '履约质量' },
|
||||
{ key: 'scoreSpeed', label: '时效响应' },
|
||||
{ key: 'scoreCoordination', label: '配合度' },
|
||||
{ key: 'scoreCompliance', label: '合规安全' }
|
||||
]
|
||||
const totalScore = computed(() =>
|
||||
scoreForm.scoreQuality + scoreForm.scoreSpeed + scoreForm.scoreCoordination + scoreForm.scoreCompliance
|
||||
)
|
||||
|
||||
const previewOpen = ref(false)
|
||||
const previewUrl = ref('')
|
||||
const previewTitle = ref('公告预览')
|
||||
|
||||
// ===================== 工具 =====================
|
||||
function numOrZero(v) {
|
||||
const n = Number(v)
|
||||
return isNaN(n) ? 0 : n
|
||||
}
|
||||
function fmtScore(v) {
|
||||
if (v == null || v === '') return '-'
|
||||
return Number(v).toFixed(1)
|
||||
}
|
||||
function fmtMoney(v) {
|
||||
if (v == null || v === '' || v === 0) return '0.00'
|
||||
return Number(v).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
}
|
||||
function fmtDate(v) {
|
||||
if (!v) return '-'
|
||||
const s = String(v)
|
||||
const m = s.match(/^(\d{4})-(\d{2})-(\d{2})/)
|
||||
return m ? `${m[1]}.${m[2]}.${m[3]}` : s
|
||||
}
|
||||
|
||||
// ===================== 加载 =====================
|
||||
async function loadProject() {
|
||||
projectId.value = route.params.projectId
|
||||
if (!projectId.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const resp = await bizGet('project', projectId.value)
|
||||
// 后端 success(BizProject) 返回 {code, msg, data: BizProject}
|
||||
const data = (resp && resp.data) || resp || {}
|
||||
row.value = data || {}
|
||||
scoreForm.scoreQuality = numOrZero(row.value.scoreQuality)
|
||||
scoreForm.scoreSpeed = numOrZero(row.value.scoreSpeed)
|
||||
scoreForm.scoreCoordination = numOrZero(row.value.scoreCoordination)
|
||||
scoreForm.scoreCompliance = numOrZero(row.value.scoreCompliance)
|
||||
// 角色劳务 (JSON 字段, 缺则用默认)
|
||||
try {
|
||||
const rl = JSON.parse(row.value.roleLabor || '[]')
|
||||
if (Array.isArray(rl) && rl.length) form.roleRows = rl
|
||||
} catch (e) { /* keep default */ }
|
||||
// 监督员 (JSON 字段)
|
||||
try {
|
||||
const mn = JSON.parse(row.value.monitors || '[]')
|
||||
if (Array.isArray(mn)) monitors.value = mn
|
||||
} catch (e) { /* keep empty */ }
|
||||
} catch (e) {
|
||||
console.error('[project-detail] load failed', e)
|
||||
row.value = {}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAnnouncements() {
|
||||
try {
|
||||
const resp = await bizList('publicity', { projectId: projectId.value })
|
||||
const arr = (resp && resp.rows) || ((resp && resp.data) && (resp && resp.data).rows) || (resp && resp.data) || []
|
||||
announcements.value = Array.isArray(arr) ? arr : []
|
||||
} catch (e) {
|
||||
console.error('[project-detail] announcements load failed', e)
|
||||
announcements.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 角色劳务 =====================
|
||||
function addRoleRow() {
|
||||
if (form.roleRows.length >= 20) { ElMessage.warning('最多 20 行角色'); return }
|
||||
form.roleRows.push({ role: '', customName: '', amount: 0 })
|
||||
}
|
||||
function removeRoleRowAt(idx) {
|
||||
if (form.roleRows.length <= 1) { ElMessage.warning('至少保留 1 行角色'); return }
|
||||
form.roleRows.splice(idx, 1)
|
||||
}
|
||||
|
||||
// ===================== 监督员 =====================
|
||||
async function openAddMonitor() {
|
||||
addMonitorOpen.value = true
|
||||
if (!monitorCandidates.value.length) {
|
||||
try {
|
||||
const resp = await bizList('person', { pageNum: 1, pageSize: 100, role: '监察员', unitType: 'sponsor' })
|
||||
monitorCandidates.value = (resp && resp.rows) || ((resp && resp.data) && (resp && resp.data).rows) || (resp && resp.data) || []
|
||||
} catch (e) { monitorCandidates.value = [] }
|
||||
}
|
||||
}
|
||||
function confirmAddMonitor() {
|
||||
if (!newMonitor.userId) { ElMessage.warning('请选择监督员'); return }
|
||||
const u = monitorCandidates.value.find(x => x.userId === newMonitor.userId)
|
||||
if (u) monitors.value.push({ userId: u.userId, userName: u.userName, name: u.nickName || u.userName })
|
||||
addMonitorOpen.value = false
|
||||
newMonitor.userId = null
|
||||
}
|
||||
function removeMonitorAt(idx) {
|
||||
if (monitors.value.length <= 1) { ElMessage.warning('至少保留 1 行监督员'); return }
|
||||
monitors.value.splice(idx, 1)
|
||||
}
|
||||
|
||||
// ===================== 公告 =====================
|
||||
function openUpload() {
|
||||
resetUploadForm()
|
||||
uploadOpen.value = true
|
||||
}
|
||||
function resetUploadForm() {
|
||||
uploadForm.announceType = 'notice'
|
||||
uploadForm.title = ''
|
||||
uploadForm.remark = ''
|
||||
}
|
||||
async function submitAnnouncement() {
|
||||
if (!uploadForm.title) { ElMessage.warning('请输入公告标题'); return }
|
||||
submitting.value = true
|
||||
try {
|
||||
await bizAdd('publicity', {
|
||||
projectId: projectId.value,
|
||||
announceType: uploadForm.announceType,
|
||||
title: uploadForm.title,
|
||||
remark: uploadForm.remark,
|
||||
status: '1'
|
||||
})
|
||||
ElMessage.success('已发布')
|
||||
uploadOpen.value = false
|
||||
await loadAnnouncements()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
ElMessage.error('发布失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
async function deleteAnnouncement(a) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除公告【${a.title || '未命名'}】?`, '提示', { type: 'warning' })
|
||||
await bizDelete('publicity', a.id || a.publicityId)
|
||||
ElMessage.success('已删除')
|
||||
await loadAnnouncements()
|
||||
} catch (e) {
|
||||
if (e !== 'cancel') console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 评分 =====================
|
||||
function openScoreDialog() {
|
||||
scoreOpen.value = true
|
||||
}
|
||||
async function submitScore() {
|
||||
submitting.value = true
|
||||
try {
|
||||
await bizUpdate('project', {
|
||||
...row.value,
|
||||
scoreQuality: scoreForm.scoreQuality,
|
||||
scoreSpeed: scoreForm.scoreSpeed,
|
||||
scoreCoordination: scoreForm.scoreCoordination,
|
||||
scoreCompliance: scoreForm.scoreCompliance,
|
||||
ratingScore: totalScore.value
|
||||
})
|
||||
ElMessage.success('评分已保存')
|
||||
scoreOpen.value = false
|
||||
await loadProject()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
ElMessage.error('保存失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 导航 =====================
|
||||
function goBack() { router.push('/manager/projects') }
|
||||
function goEdit() { router.push(`/manager/projects/edit/${projectId.value}`) }
|
||||
|
||||
// ===================== 预览 =====================
|
||||
function openPreview(url, title) {
|
||||
if (!url) { ElMessage.warning('暂无附件'); return }
|
||||
previewUrl.value = url
|
||||
previewTitle.value = title || '公告预览'
|
||||
previewOpen.value = true
|
||||
}
|
||||
|
||||
// ===================== 初始化 =====================
|
||||
onMounted(async () => {
|
||||
await loadProject()
|
||||
await loadAnnouncements()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 1:1 抄列表页 (manager/Projects.vue) 风格 */
|
||||
.page-card { background: #fff; padding: 16px 20px; border-radius: 6px; border: 1px solid #f0f0f0; }
|
||||
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
|
||||
.breadcrumb .current { color: #262626; font-weight: 500; }
|
||||
|
||||
.section-title {
|
||||
font-size: 14px; font-weight: 600; color: #262626; margin: 16px 0 12px;
|
||||
padding-left: 8px; border-left: 3px solid var(--brand-primary, #1890ff);
|
||||
}
|
||||
|
||||
/* 项目信息: 11 字段, 一列竖排 (el-form + 文本, 无分隔线) */
|
||||
.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: #595959; width: 130px; padding-right: 16px;
|
||||
}
|
||||
.info-value { font-size: 15px; color: #1a1a1a; font-weight: 500; }
|
||||
.info-value.money {
|
||||
font-family: ui-monospace, "Courier New", monospace;
|
||||
}
|
||||
.op-link {
|
||||
color: var(--brand-primary, #1890ff); text-decoration: none;
|
||||
font-weight: 600; cursor: pointer; font-size: 15px;
|
||||
}
|
||||
.op-link:hover { text-decoration: underline; }
|
||||
|
||||
/* 监督员 */
|
||||
.monitor-list { padding: 4px 0; }
|
||||
.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: #8c8c8c; font-size: 13px; }
|
||||
.monitor-name { flex: 1; font-size: 14px; color: #1a1a1a; }
|
||||
.empty-hint { text-align: center; color: #c0c4cc; padding: 12px 0; font-size: 13px; }
|
||||
|
||||
/* 角色劳务表格 */
|
||||
.info-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||
.info-table th { background: #fafafa; padding: 10px 12px; text-align: left; color: #1a1a1a; font-weight: 600; border-bottom: 1px solid #f0f0f0; white-space: nowrap; }
|
||||
.info-table td { padding: 10px 12px; border-bottom: 1px solid #f5f5f5; color: #595959; vertical-align: middle; }
|
||||
|
||||
/* 红字提示 */
|
||||
.hint-red { font-size: 12px; color: #ff0000; line-height: 1.7; margin: 4px 0 16px; }
|
||||
|
||||
/* 工具栏 */
|
||||
.toolbar { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
|
||||
|
||||
/* 评分 */
|
||||
.score-num { font-size: 18px; color: var(--brand-primary, #1890ff); font-weight: 700; margin-right: 6px; }
|
||||
</style>
|
||||
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div class="page-card manager-meeting-new">
|
||||
<h2 style="margin-bottom:16px">新建会议</h2>
|
||||
<p class="page-sub" v-if="preselectProjectId">已选择项目: {{ preselectProjectId }}</p>
|
||||
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="120px" style="max-width:700px">
|
||||
<el-form-item label="项目编号" prop="projectNo">
|
||||
<el-input v-model="form.projectNo" :disabled="!!preselectProjectId" placeholder="项目编号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="会议名称" prop="meetingName">
|
||||
<el-input v-model="form.meetingName" placeholder="会议名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="会议ID" prop="meetingId">
|
||||
<el-input v-model="form.meetingId" placeholder="会议ID" />
|
||||
</el-form-item>
|
||||
<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-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-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-form-item label="总期数" prop="totalPeriods">
|
||||
<el-input-number v-model="form.totalPeriods" :min="1" style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="期数" prop="periodNo">
|
||||
<el-input-number v-model="form.periodNo" :min="1" style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="当前阶段" prop="currentStage">
|
||||
<el-select v-model="form.currentStage" 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-form-item label="备注">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="3" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="onSave">保存</el-button>
|
||||
<el-button @click="onCancel">取消</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, bizGet } from '@/api/public'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const preselectProjectId = route.query.projectId || ''
|
||||
const preselectProjectNo = route.query.projectNo || ''
|
||||
const formRef = ref()
|
||||
const form = reactive({
|
||||
projectNo: preselectProjectNo || '',
|
||||
meetingName: '',
|
||||
meetingId: '',
|
||||
projectForm: '',
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
totalPeriods: 1,
|
||||
periodNo: 1,
|
||||
currentStage: '未执行',
|
||||
remark: ''
|
||||
})
|
||||
const rules = {
|
||||
projectNo: [{ required: true, message: '请输入项目编号', trigger: 'blur' }],
|
||||
meetingName: [{ required: true, message: '请输入会议名称', trigger: 'blur' }],
|
||||
meetingId: [{ required: true, message: '请输入会议ID', trigger: 'blur' }],
|
||||
startTime: [{ required: true, message: '请选择会议开始时间', trigger: 'change' }],
|
||||
endTime: [{ required: true, message: '请选择会议结束时间', trigger: 'change' }]
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
try { await formRef.value.validate() } catch (e) { return }
|
||||
try {
|
||||
await bizAdd('meeting', { ...form })
|
||||
ElMessage.success('保存成功')
|
||||
router.push('/manager/meetings')
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.msg || '保存失败')
|
||||
}
|
||||
}
|
||||
|
||||
function onCancel() { router.back() }
|
||||
|
||||
onMounted(async () => {
|
||||
if (preselectProjectId) {
|
||||
try {
|
||||
const r = await bizGet('project', preselectProjectId)
|
||||
form.projectNo = r.data?.projectNo || ''
|
||||
} catch (e) {}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.manager-meeting-new { padding: 16px; }
|
||||
.page-sub { color: #909399; font-size: 13px; margin-bottom: 12px; }
|
||||
</style>
|
||||
@@ -0,0 +1,319 @@
|
||||
<template>
|
||||
<div class="manager-meetings">
|
||||
<!-- ========== 筛选区 (严格按 manager 角色原型 会议管理_1.html — 第一行 6 项 + 第二行 3 项) ========== -->
|
||||
<div class="page-card">
|
||||
<el-form inline :model="q" class="filter-form">
|
||||
<el-form-item label="项目编号">
|
||||
<el-input v-model="q.projectNo" placeholder="" clearable style="width:140px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="会议ID">
|
||||
<el-input v-model="q.meetingId" placeholder="" clearable style="width:140px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="会议名称">
|
||||
<el-input v-model="q.meetingName" placeholder="" clearable style="width:160px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="第?期">
|
||||
<el-input v-model="q.periodNo" placeholder="" clearable style="width:80px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="会议时间">
|
||||
<el-date-picker v-model="q.startTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width:170px" />
|
||||
<span style="margin:0 4px">至</span>
|
||||
<el-date-picker v-model="q.endTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width:170px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="项目形式">
|
||||
<el-select v-model="q.projectForm" placeholder="请选择" clearable style="width:140px">
|
||||
<el-option label="线上" value="线上" />
|
||||
<el-option label="线下" value="线下" />
|
||||
<el-option label="线上+线下" value="线上+线下" />
|
||||
<el-option label="其他" value="其他" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="当前阶段">
|
||||
<el-select v-model="q.currentStage" placeholder="请选择" clearable style="width:140px">
|
||||
<el-option label="未执行" value="未执行" />
|
||||
<el-option label="执行中" value="执行中" />
|
||||
<el-option label="已执行" value="已执行" />
|
||||
<el-option label="已完结" value="已完结" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="q.remark" placeholder="" clearable style="width:140px" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="load">查找</el-button>
|
||||
<el-button @click="reset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- ========== 表格区 (按 manager 角色原型 会议管理_1.html: 操作列 4 按钮 — 查看/修改/提交/复制) ========== -->
|
||||
<div class="page-card">
|
||||
<!-- 批量提交按钮 (manager 角色才有) -->
|
||||
<div class="batch-bar" v-if="selectedIds.length">
|
||||
<el-button type="primary" plain @click="onBatchSubmit" :disabled="!selectedIds.length">批量提交</el-button>
|
||||
<span class="selected-tip">已选 {{ selectedIds.length }} 条</span>
|
||||
</div>
|
||||
<el-table :data="rows" v-loading="loading" stripe border @selection-change="onSelectionChange">
|
||||
<el-table-column type="selection" width="44" />
|
||||
<el-table-column prop="projectNo" label="项目编号" width="130" fixed />
|
||||
<el-table-column prop="meetingId" label="会议ID" width="120" align="center" />
|
||||
<el-table-column prop="projectForm" label="项目形式" width="100" align="center" />
|
||||
<el-table-column prop="meetingName" label="会议名称" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="会议开始时间" width="160" align="center">
|
||||
<template #default="{ row }">{{ fmtTime(row.startTime) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="会议结束时间" width="160" align="center">
|
||||
<template #default="{ row }">{{ fmtTime(row.endTime) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="totalPeriods" label="总期数" width="80" align="center" />
|
||||
<el-table-column label="期数" width="80" align="center">
|
||||
<template #default="{ row }">第 {{ row.periodNo || 0 }} 期</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="currentStage" label="当前阶段" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span :class="['stage-tag', 'stage-' + stageClass(row.currentStage)]">{{ row.currentStage }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="240" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="onView(row)">查看</el-button>
|
||||
<el-button link type="primary" @click="onEdit(row)">修改</el-button>
|
||||
<el-button link type="primary" @click="onSubmit(row)">提交</el-button>
|
||||
<el-button link type="primary" @click="onCopy(row)">复制</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
v-model:current-page="page.pageNum"
|
||||
v-model:page-size="page.pageSize"
|
||||
:total="page.total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="load"
|
||||
@size-change="load"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 会议详情 dialog -->
|
||||
<el-dialog v-model="detailOpen" title="会议详情" width="640px">
|
||||
<el-descriptions :column="3" border v-if="currentRow">
|
||||
<el-descriptions-item label="项目编号">{{ currentRow.projectNo }}</el-descriptions-item>
|
||||
<el-descriptions-item label="会议ID">{{ currentRow.meetingId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="项目形式">{{ currentRow.projectForm }}</el-descriptions-item>
|
||||
<el-descriptions-item label="会议名称" :span="3">{{ currentRow.meetingName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="会议开始时间">{{ fmtTime(currentRow.startTime) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="会议结束时间">{{ fmtTime(currentRow.endTime) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="提交剩余时间">{{ calcRemain(currentRow) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="总期数">{{ currentRow.totalPeriods }}</el-descriptions-item>
|
||||
<el-descriptions-item label="期数">第 {{ currentRow.periodNo || 0 }} 期</el-descriptions-item>
|
||||
<el-descriptions-item label="当前阶段">{{ currentRow.currentStage }}</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="3">{{ currentRow.remark || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 修改 dialog (基本字段编辑) -->
|
||||
<el-dialog v-model="editOpen" title="修改会议" width="640px">
|
||||
<el-form :model="editForm" label-width="100px">
|
||||
<el-form-item label="会议名称"><el-input v-model="editForm.meetingName" /></el-form-item>
|
||||
<el-form-item label="项目形式">
|
||||
<el-select v-model="editForm.projectForm">
|
||||
<el-option label="线上" value="线上" />
|
||||
<el-option label="线下" value="线下" />
|
||||
<el-option label="线上+线下" value="线上+线下" />
|
||||
<el-option label="其他" value="其他" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="开始时间">
|
||||
<el-date-picker v-model="editForm.startTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="结束时间">
|
||||
<el-date-picker v-model="editForm.endTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="总期数"><el-input-number v-model="editForm.totalPeriods" :min="1" /></el-form-item>
|
||||
<el-form-item label="备注"><el-input v-model="editForm.remark" type="textarea" :rows="2" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="editOpen = false">取消</el-button>
|
||||
<el-button type="primary" :loading="editSubmitting" @click="submitEdit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { bizList, bizUpdate } from '@/api/public'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
// ========== 筛选 (严格按原型 2 行 8 项: 项目编号/会议ID/会议名称/第几期/会议时间/项目形式/当前阶段/备注) ==========
|
||||
const q = ref({
|
||||
projectNo: '', meetingId: '', meetingName: '', periodNo: '',
|
||||
projectForm: '', currentStage: '', remark: '',
|
||||
startTime: '', endTime: ''
|
||||
})
|
||||
const rows = ref([])
|
||||
const loading = ref(false)
|
||||
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
|
||||
|
||||
// ========== 表格多选 ==========
|
||||
const selectedIds = ref([])
|
||||
function onSelectionChange(sel) {
|
||||
selectedIds.value = sel.map(r => r.meetingId)
|
||||
}
|
||||
|
||||
// ========== 工具: 时间格式 (yyyy.MM.dd HH:mm) ==========
|
||||
function fmtTime(d) {
|
||||
if (!d) return ''
|
||||
const dt = new Date(d)
|
||||
const pad = n => String(n).padStart(2, '0')
|
||||
return `${dt.getFullYear()}.${pad(dt.getMonth() + 1)}.${pad(dt.getDate())} ${pad(dt.getHours())}:${pad(dt.getMinutes())}`
|
||||
}
|
||||
|
||||
// ========== 工具: 当前阶段颜色 class ==========
|
||||
function stageClass(s) {
|
||||
if (s === '未执行') return 'pending'
|
||||
if (s === '执行中') return 'running'
|
||||
if (s === '已完结' || s === '已执行') return 'done'
|
||||
if (s === '待审核') return 'reviewing'
|
||||
if (s === '待结算') return 'waiting'
|
||||
if (s === '冻结中') return 'frozen'
|
||||
return 'default'
|
||||
}
|
||||
|
||||
// ========== 工具: 提交剩余时间 (deadlineDays - 已过天数, 单位天/小时) ==========
|
||||
function calcRemain(row) {
|
||||
if (!row) return ''
|
||||
if (!row.endTime) return ''
|
||||
// 已结束的会议: 冻结中/已完结 等阶段, 不算剩余
|
||||
if (['已完结', '已执行', '已结题'].includes(row.currentStage)) return '-'
|
||||
const end = new Date(row.endTime).getTime()
|
||||
const now = Date.now()
|
||||
if (now > end && row.currentStage !== '冻结中') return '已超时'
|
||||
// 剩余时间 = 提交截止天数 - (now - end) / day
|
||||
// 提交截止天数默认 30 天, 实际从 project.submit_deadline_days 读取
|
||||
const days = Number(row.submitDeadlineDays || 30)
|
||||
const remainMs = end + days * 86400000 - now
|
||||
if (remainMs <= 0) return '0小时'
|
||||
const remDays = Math.floor(remainMs / 86400000)
|
||||
const remHours = Math.floor((remainMs % 86400000) / 3600000)
|
||||
return `${remDays}天${remHours}小时`
|
||||
}
|
||||
|
||||
// ========== 加载 ==========
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const { data } = await bizList('meeting', { ...q.value, pageNum: page.pageNum, pageSize: page.pageSize })
|
||||
rows.value = data?.rows || []
|
||||
page.total = data?.total || 0
|
||||
} catch { rows.value = []; page.total = 0 }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
function reset() {
|
||||
q.value = {
|
||||
projectNo:'', meetingId:'', meetingName:'', periodNo:'',
|
||||
projectForm:'', currentStage:'', remark:'',
|
||||
startTime:'', endTime:''
|
||||
}
|
||||
page.pageNum = 1
|
||||
load()
|
||||
}
|
||||
|
||||
// ========== 查看 ==========
|
||||
const detailOpen = ref(false)
|
||||
const currentRow = ref(null)
|
||||
function onView(row) { currentRow.value = row; detailOpen.value = true }
|
||||
|
||||
// ========== 修改 ==========
|
||||
const editOpen = ref(false)
|
||||
const editSubmitting = ref(false)
|
||||
const editForm = reactive({ meetingId: '', meetingName: '', projectForm: '', startTime: '', endTime: '', totalPeriods: 1, remark: '' })
|
||||
function onEdit(row) {
|
||||
Object.assign(editForm, {
|
||||
meetingId: row.meetingId, meetingName: row.meetingName || '',
|
||||
projectForm: row.projectForm || '', startTime: row.startTime || '',
|
||||
endTime: row.endTime || '', totalPeriods: row.totalPeriods || 1,
|
||||
remark: row.remark || ''
|
||||
})
|
||||
editOpen.value = true
|
||||
}
|
||||
async function submitEdit() {
|
||||
editSubmitting.value = true
|
||||
try {
|
||||
await bizUpdate('meeting', { ...editForm })
|
||||
ElMessage.success('修改成功')
|
||||
editOpen.value = false
|
||||
load()
|
||||
} catch (e) { ElMessage.error('修改失败') }
|
||||
finally { editSubmitting.value = false }
|
||||
}
|
||||
|
||||
// ========== 提交 (单条) ==========
|
||||
async function onSubmit(row) {
|
||||
try { await ElMessageBox.confirm(`确定提交会议「${row.meetingName}」吗?`, '提交确认', { type: 'warning' }) } catch { return }
|
||||
try {
|
||||
await bizUpdate('meeting', { meetingId: row.meetingId, currentStage: '执行中' })
|
||||
ElMessage.success('提交成功')
|
||||
load()
|
||||
} catch { ElMessage.error('提交失败') }
|
||||
}
|
||||
|
||||
// ========== 复制 (单条) ==========
|
||||
function onCopy(row) {
|
||||
// 复制行到会议名 + ' 副本' 打开新建/编辑表单
|
||||
Object.assign(editForm, {
|
||||
meetingId: '', meetingName: (row.meetingName || '') + ' 副本',
|
||||
projectForm: row.projectForm || '', startTime: row.startTime || '',
|
||||
endTime: row.endTime || '', totalPeriods: row.totalPeriods || 1,
|
||||
remark: row.remark || ''
|
||||
})
|
||||
editOpen.value = true
|
||||
}
|
||||
|
||||
// ========== 批量提交 ==========
|
||||
async function onBatchSubmit() {
|
||||
if (!selectedIds.value.length) return
|
||||
try { await ElMessageBox.confirm(`确定对选中的 ${selectedIds.value.length} 个会议执行批量提交吗?`, '批量提交', { type: 'warning' }) } catch { return }
|
||||
try {
|
||||
// 逐条更新 (无批量接口)
|
||||
for (const id of selectedIds.value) {
|
||||
await bizUpdate('meeting', { meetingId: id, currentStage: '执行中' })
|
||||
}
|
||||
ElMessage.success('批量提交成功')
|
||||
load()
|
||||
} catch { ElMessage.error('批量提交失败') }
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.manager-meetings { padding: 16px; }
|
||||
.page-card {
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
border: 1px solid #ebeef5;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.filter-form .el-form-item { margin-right: 12px; margin-bottom: 12px; }
|
||||
.batch-bar { margin-bottom: 12px; display: flex; align-items: center; gap: 8px; }
|
||||
.batch-bar .selected-tip { color: #909399; font-size: 13px; margin-left: 8px; }
|
||||
|
||||
/* 当前阶段 tag 颜色 */
|
||||
.stage-tag {
|
||||
display: inline-block;
|
||||
padding: 2px 10px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
border: 1px solid;
|
||||
}
|
||||
.stage-pending { color: #909399; border-color: #dcdfe6; background: #f5f7fa; }
|
||||
.stage-running { color: #409eff; border-color: #b3d8ff; background: #ecf5ff; }
|
||||
.stage-done { color: #67c23a; border-color: #c2e7b0; background: #f0f9eb; }
|
||||
.stage-reviewing { color: #e6a23c; border-color: #f5dab1; background: #fdf6ec; }
|
||||
.stage-waiting { color: #f56c6c; border-color: #fbc4c4; background: #fef0f0; }
|
||||
.stage-frozen { color: #909399; border-color: #dcdfe6; background: #f5f7fa; opacity: 0.6; }
|
||||
.stage-default { color: #606266; border-color: #dcdfe6; background: #fafafa; }
|
||||
</style>
|
||||
@@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<!--
|
||||
角色: 🟨 合规 公司管理 (manager/orgs)
|
||||
重构说明: 原 manager/SupportUnits.vue (仅看支持单位) 改为可看所有公司
|
||||
接口: bizList('org', q) / bizUpdate
|
||||
-->
|
||||
<div class="manager-orgs">
|
||||
<div class="page-card">
|
||||
<div class="page-title">公司查看</div>
|
||||
<div class="desc">公司管理 > 公司查看 (赞助方 + 执行方)</div>
|
||||
<el-form inline :model="q">
|
||||
<el-form-item label="公司类型:">
|
||||
<el-select v-model="q.orgType" placeholder="全部" clearable style="width:140px">
|
||||
<el-option label="赞助方" value="sponsor" />
|
||||
<el-option label="执行方" value="executor" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="公司名称:">
|
||||
<el-input v-model="q.orgName" placeholder="输入公司名称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="税号/统一社会信用代码:">
|
||||
<el-input v-model="q.taxNo" placeholder="输入税号" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="合作状态:">
|
||||
<el-select v-model="q.status" placeholder="请选择" clearable>
|
||||
<el-option label="正常/合作中" value="0" />
|
||||
<el-option label="合作中" value="合作中" />
|
||||
<el-option label="禁用" value="禁用" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button class="action-btn" @click="load">查找</el-button>
|
||||
<el-button class="action-btn secondary" @click="reset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="page-card">
|
||||
<div class="page-toolbar">
|
||||
<el-button class="action-btn secondary" @click="goBack">返回</el-button>
|
||||
</div>
|
||||
<el-table :data="rows" v-loading="loading" stripe border>
|
||||
<el-table-column type="index" label="#" width="50" />
|
||||
<el-table-column label="类型" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.orgType === 'sponsor' ? 'success' : 'primary'" disable-transitions>
|
||||
{{ row.orgType === 'sponsor' ? '赞助方' : '执行方' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="orgName" label="公司名称" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="address" label="公司地址" min-width="220" show-overflow-tooltip />
|
||||
<el-table-column prop="taxNo" label="税号/统一社会信用代码" min-width="180" />
|
||||
<el-table-column label="合作状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusTagType(row.status)" disable-transitions>{{ row.status }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建日期" width="120" align="center">
|
||||
<template #default="{ row }">{{ row.createTime ? String(row.createTime).slice(0,10) : '' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="contactName" label="联系人" width="100" align="center" />
|
||||
<el-table-column prop="contactPhone" label="联系电话" width="130" align="center" />
|
||||
<el-table-column label="操作" width="160" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="view(row)">查看</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize" :total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper" @current-change="load" @size-change="load" />
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="detailOpen" title="公司详情" width="640px" v-if="currentRow">
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="公司类型">
|
||||
<el-tag :type="currentRow.orgType === 'sponsor' ? 'success' : 'primary'">
|
||||
{{ currentRow.orgType === 'sponsor' ? '赞助方' : '执行方' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="公司名称">{{ currentRow.orgName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="公司地址">{{ currentRow.address }}</el-descriptions-item>
|
||||
<el-descriptions-item label="税号/统一社会信用代码">{{ currentRow.taxNo }}</el-descriptions-item>
|
||||
<el-descriptions-item label="合作状态">
|
||||
<el-tag :type="statusTagType(currentRow.status)">{{ currentRow.status }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建日期">{{ currentRow.createTime ? String(currentRow.createTime).slice(0,10) : '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="联系人">{{ currentRow.contactName || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="联系电话">{{ currentRow.contactPhone || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<template #footer>
|
||||
<el-button @click="detailOpen=false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { bizList } from '@/api/public'
|
||||
|
||||
const router = useRouter()
|
||||
const q = ref({ orgType: '', orgName: '', taxNo: '', status: '' })
|
||||
const rows = ref([])
|
||||
const loading = ref(false)
|
||||
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
|
||||
const currentRow = ref(null)
|
||||
const detailOpen = ref(false)
|
||||
|
||||
function statusTagType(s) {
|
||||
if (s === '正常' || s === '0' || s === '合作中') return 'success'
|
||||
if (s === '禁用' || s === '1') return 'danger'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const { data } = await bizList('org', { ...q.value, pageNum: page.pageNum, pageSize: page.pageSize })
|
||||
rows.value = data?.rows || []; page.total = data?.total || 0
|
||||
} catch { rows.value = []; page.total = 0 }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
function reset() { q.value = { orgType:'', orgName:'', taxNo:'', status:'' }; page.pageNum = 1; load() }
|
||||
function view(row) { currentRow.value = row; detailOpen.value = true }
|
||||
function goBack() { router.back() }
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.manager-orgs { padding: 16px; }
|
||||
.page-card { background: #fff; border-radius: 6px; padding: 16px; border: 1px solid #ebeef5; margin-bottom: 12px; }
|
||||
.page-title { font-size: 20px; font-weight: 600; margin-bottom: 4px; padding-left: 10px; border-left: 3px solid var(--brand-primary); }
|
||||
.desc { color: #909399; font-size: 13px; margin-bottom: 12px; }
|
||||
.page-toolbar { display: flex; gap: 12px; margin-bottom: 12px; flex-wrap: wrap; align-items: center; }
|
||||
.page-toolbar :deep(.action-btn) {
|
||||
height: 36px; padding: 0 20px; font-size: 14px; border-radius: 4px;
|
||||
background: var(--brand-primary); color: #fff; border: 1px solid var(--brand-primary);
|
||||
display: inline-flex; align-items: center; justify-content: center; white-space: nowrap;
|
||||
transition: opacity .15s; margin-left: 0;
|
||||
}
|
||||
.page-toolbar :deep(.action-btn:hover) { opacity: .85; }
|
||||
.page-toolbar :deep(.action-btn.secondary) {
|
||||
background: #fff; color: var(--brand-primary); border: 1px solid var(--brand-primary);
|
||||
}
|
||||
.page-toolbar :deep(.action-btn.secondary:hover) { background: rgba(30,58,138,.06); }
|
||||
</style>
|
||||
@@ -0,0 +1,312 @@
|
||||
<template>
|
||||
<div class="manager-plans">
|
||||
<div class="page-card">
|
||||
<div class="page-title">项目策划方案管理</div>
|
||||
<div class="desc">审核 / 修改 / 结算 项目策划方案</div>
|
||||
<el-form inline :model="q">
|
||||
<!-- 按原型 top=171-178 的筛选区: 项目编号(52) / 项目编号(141) / 策划方案名称(254) / 项目方向(519) / 策划方案名称(591) / 状态(939) / 备注(1138) / 备注(1177) -->
|
||||
<el-form-item label="项目编号:"><el-input v-model="q.projectNo" placeholder="输入项目编号" clearable /></el-form-item>
|
||||
<el-form-item label="策划方案名称:"><el-input v-model="q.planName" placeholder="输入策划方案名称" clearable /></el-form-item>
|
||||
<el-form-item label="项目方向:">
|
||||
<el-select v-model="q.planDirection" placeholder="请选择" clearable>
|
||||
<el-option label="专项计划一" value="专项计划一" /><el-option label="专项计划二" value="专项计划二" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="项目类别:">
|
||||
<el-select v-model="q.planCategory" placeholder="请选择" clearable>
|
||||
<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>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态:">
|
||||
<el-select v-model="q.status" placeholder="请选择" clearable>
|
||||
<el-option label="待审核" value="0" /><el-option label="审核通过" value="1" /><el-option label="已退回" value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否结算:">
|
||||
<el-select v-model="q.isSettled" placeholder="请选择" clearable>
|
||||
<el-option label="已结算" value="1" /><el-option label="未结算" value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注:"><el-input v-model="q.remark" placeholder="输入备注" clearable /></el-form-item>
|
||||
<el-form-item>
|
||||
<el-button class="action-btn" @click="load">查找</el-button>
|
||||
<el-button class="action-btn secondary" @click="reset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="filter-tip">*批量审核:策划案名称,投稿ID,审核结果,审核意见,只能审核不通过</div>
|
||||
</div>
|
||||
|
||||
<div class="page-card">
|
||||
<!-- 批量按钮 (按原型 left: 批量审核 266 / 批量结算 432) -->
|
||||
<div class="page-toolbar">
|
||||
<el-button class="action-btn" :disabled="!selection.length" @click="batchAudit">
|
||||
批量审核<span v-if="selection.length" class="badge">{{ selection.length }}</span>
|
||||
</el-button>
|
||||
<el-button class="action-btn secondary" :disabled="!selection.length" @click="batchSettle">
|
||||
批量结算<span v-if="selection.length" class="badge">{{ selection.length }}</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<!-- 列头顺序严格按原型 top=294-300 的 left: 策划方案名称 342 / 项目方向 569 / 项目类别 739 / 项目形式 864 / 设计文件 999 / 状态 1133 / 是否结算 1246 / 项目编号 1348 / 备注 1492 / 操作 1640 -->
|
||||
<el-table :data="rows" v-loading="loading" stripe border @selection-change="onSel">
|
||||
<el-table-column type="selection" width="44" />
|
||||
<el-table-column prop="planName" label="策划方案名称" min-width="200" fixed show-overflow-tooltip />
|
||||
<el-table-column prop="planDirection" label="项目方向" min-width="220" show-overflow-tooltip />
|
||||
<el-table-column prop="planCategory" label="项目类别" width="100" align="center" />
|
||||
<el-table-column prop="projectForm" label="项目形式" width="100" align="center" />
|
||||
<el-table-column label="设计文件" width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.designFileUrl">
|
||||
<el-button link type="primary" @click="onFile(row)">查看</el-button>
|
||||
<el-button link type="primary" @click="downloadFile(row)">下载</el-button>
|
||||
</template>
|
||||
<span v-else style="color:#c0c4cc">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="row.status==='1' ? 'success' : row.status==='2' ? 'danger' : 'warning'">
|
||||
{{ row.status==='1' ? '审核通过' : row.status==='2' ? '已退回' : '待审核' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="是否结算" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="row.isSettled==='1' ? 'success' : 'warning'">
|
||||
{{ row.isSettled==='1' ? '已结算' : '未结算' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="projectNo" label="项目编号" width="140" align="center" />
|
||||
<el-table-column prop="remark" label="备注" min-width="160" show-overflow-tooltip />
|
||||
<!-- 操作列按原型 left 顺序: 审核 1586 / 修改 1640 / 结算 1693 -->
|
||||
<el-table-column label="操作" width="240" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="onAudit(row)">审核</el-button>
|
||||
<el-button link type="primary" @click="onEdit(row)">修改</el-button>
|
||||
<el-button link type="primary" @click="onSettle(row)" v-if="row.isSettled!=='1'">结算</el-button>
|
||||
<el-button link type="primary" v-if="row.designFileUrl" @click="downloadFile(row)">下载</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize" :total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper" @current-change="load" @size-change="load" />
|
||||
</div>
|
||||
|
||||
<!-- 审核 dialog (按原型: 通过/退回 radio + 项目编号通过时必填 + 审核意见 + 确认/取消) -->
|
||||
<el-dialog v-model="auditOpen" title="审核策划方案" width="500px">
|
||||
<el-form :model="auditForm" :rules="auditRules" ref="auditFormRef" label-width="100px">
|
||||
<el-form-item label="方案名称"><span>{{ auditForm.planName }}</span></el-form-item>
|
||||
<el-form-item label="审核结果">
|
||||
<el-radio-group v-model="auditForm.result">
|
||||
<el-radio label="1">通过</el-radio><el-radio label="2">退回</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="auditForm.result==='1'" label="项目编号" prop="projectNo">
|
||||
<el-input v-model="auditForm.projectNo" placeholder="通过时项目编号为必填" />
|
||||
</el-form-item>
|
||||
<el-form-item label="审核意见"><el-input v-model="auditForm.opinion" type="textarea" rows="3" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="auditOpen=false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submitAudit">确认</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 编辑 dialog -->
|
||||
<el-dialog v-model="editOpen" title="修改策划方案" width="640px" @closed="resetEditForm">
|
||||
<el-form :model="editForm" label-width="120px" :rules="editRules" ref="editFormRef">
|
||||
<el-form-item label="方案名称" prop="planName"><el-input v-model="editForm.planName" /></el-form-item>
|
||||
<el-form-item label="项目编号" prop="projectNo"><el-input v-model="editForm.projectNo" /></el-form-item>
|
||||
<el-form-item label="项目方向">
|
||||
<el-select v-model="editForm.planDirection" placeholder="请选择" style="width:100%">
|
||||
<el-option label="专项计划一" value="专项计划一" /><el-option label="专项计划二" value="专项计划二" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="项目类别">
|
||||
<el-select v-model="editForm.planCategory" placeholder="请选择" style="width:100%">
|
||||
<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>
|
||||
</el-form-item>
|
||||
<el-form-item label="项目形式">
|
||||
<el-select v-model="editForm.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-form-item label="设计方案">
|
||||
<el-input v-model="editForm.designFileUrl" placeholder="文件 URL(暂以文本输入)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注"><el-input v-model="editForm.remark" type="textarea" rows="2" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="editOpen=false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submitEdit">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 结算 dialog (按原型: 仅文字 + 确定/取消) -->
|
||||
<el-dialog v-model="settleOpen" title="结算策划方案" width="420px" :show-close="false">
|
||||
<p>您确定要结算吗?</p>
|
||||
<template #footer>
|
||||
<el-button @click="settleOpen=false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submitSettle">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<Preview v-model="previewOpen" :url="previewUrl" :title="previewTitle" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { bizList, bizUpdate } from '@/api/public'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import Preview from '@/components/Preview.vue'
|
||||
|
||||
const q = ref({ planName: '', projectNo: '', planDirection: '', planCategory: '', status: '', isSettled: '', remark: '' })
|
||||
const rows = ref([]); const selection = ref([])
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const { data } = await bizList('projectPlan', { ...q.value, pageNum: page.pageNum, pageSize: page.pageSize })
|
||||
rows.value = data?.rows || []
|
||||
page.total = data?.total || 0
|
||||
} catch { rows.value = []; page.total = 0 }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
function reset() { q.value = { planName:'', projectNo:'', planDirection:'', planCategory:'', status:'', isSettled:'', remark:'' }; page.pageNum = 1; load() }
|
||||
function onSel(rs) { selection.value = rs }
|
||||
|
||||
async function batchAudit() {
|
||||
if (!selection.value.length) return ElMessage.warning('请先勾选行')
|
||||
let ok = 0
|
||||
submitting.value = true
|
||||
for (const r of selection.value) {
|
||||
try { await bizUpdate('projectPlan', { planId: r.planId, status: '2', auditOpinion: '批量审核不通过' }); ok++ } catch {}
|
||||
}
|
||||
submitting.value = false
|
||||
ElMessage.success(`批量审核 ${ok}/${selection.value.length} 条`)
|
||||
selection.value = []
|
||||
load()
|
||||
}
|
||||
|
||||
async function batchSettle() {
|
||||
if (!selection.value.length) return ElMessage.warning('请先勾选行')
|
||||
submitting.value = true
|
||||
let ok = 0
|
||||
for (const r of selection.value) {
|
||||
try { await bizUpdate('projectPlan', { planId: r.planId, isSettled: '1' }); ok++ } catch {}
|
||||
}
|
||||
submitting.value = false
|
||||
ElMessage.success(`批量结算 ${ok}/${selection.value.length} 条`)
|
||||
selection.value = []
|
||||
load()
|
||||
}
|
||||
|
||||
const auditOpen = ref(false)
|
||||
const auditFormRef = ref(null)
|
||||
const auditForm = reactive({ planId: '', planName: '', projectNo: '', result: '1', opinion: '' })
|
||||
const auditRules = {
|
||||
projectNo: [{ required: true, message: '通过时项目编号为必填', trigger: 'blur' }]
|
||||
}
|
||||
function onAudit(row) { auditForm.planId = row.planId; auditForm.planName = row.planName; auditForm.projectNo = row.projectNo || ''; auditForm.result = '1'; auditForm.opinion = ''; auditOpen.value = true }
|
||||
async function submitAudit() {
|
||||
try {
|
||||
await auditFormRef.value.validate()
|
||||
submitting.value = true
|
||||
await bizUpdate('projectPlan', { planId: auditForm.planId, status: auditForm.result, auditOpinion: auditForm.opinion, projectNo: auditForm.result === '1' ? auditForm.projectNo : undefined })
|
||||
ElMessage.success('审核已确认')
|
||||
auditOpen.value = false
|
||||
load()
|
||||
} catch (e) { if (e?.message) ElMessage.error(e.message) }
|
||||
finally { submitting.value = false }
|
||||
}
|
||||
|
||||
const editOpen = ref(false)
|
||||
const editFormRef = ref(null)
|
||||
const editForm = reactive({ planId:'', planName:'', projectNo:'', planDirection:'', planCategory:'', projectForm:'', designFileUrl:'', remark:'' })
|
||||
const editRules = { planName:[{required:true,message:'请输入方案名称',trigger:'blur'}] }
|
||||
function onEdit(row) {
|
||||
Object.assign(editForm, {
|
||||
planId: row.planId, planName: row.planName, projectNo: row.projectNo,
|
||||
planDirection: row.planDirection, planCategory: row.planCategory,
|
||||
projectForm: row.projectForm, designFileUrl: row.designFileUrl || '', remark: row.remark || ''
|
||||
})
|
||||
editOpen.value = true
|
||||
}
|
||||
function resetEditForm() { Object.assign(editForm, { planId:'', planName:'', projectNo:'', planDirection:'', planCategory:'', projectForm:'', designFileUrl:'', remark:'' }) }
|
||||
async function submitEdit() {
|
||||
await editFormRef.value.validate()
|
||||
try {
|
||||
submitting.value = true
|
||||
await bizUpdate('projectPlan', { ...editForm })
|
||||
ElMessage.success('已保存')
|
||||
editOpen.value = false
|
||||
load()
|
||||
} catch (e) { ElMessage.error(e?.msg || '保存失败') }
|
||||
finally { submitting.value = false }
|
||||
}
|
||||
|
||||
const settleOpen = ref(false)
|
||||
const settleForm = reactive({ planId:'', planName:'' })
|
||||
function onSettle(row) { settleForm.planId = row.planId; settleForm.planName = row.planName; settleOpen.value = true }
|
||||
async function submitSettle() {
|
||||
try {
|
||||
submitting.value = true
|
||||
await bizUpdate('projectPlan', { planId: settleForm.planId, isSettled: '1' })
|
||||
ElMessage.success('已结算')
|
||||
settleOpen.value = false
|
||||
load()
|
||||
} catch { ElMessage.error('结算失败') }
|
||||
finally { submitting.value = false }
|
||||
}
|
||||
|
||||
const previewOpen = ref(false); const previewUrl = ref(''); const previewTitle = ref('策划方案预览')
|
||||
function onFile(row) { previewUrl.value = row.designFileUrl; previewTitle.value = `策划方案 - ${row.planName || ''}`; previewOpen.value = true }
|
||||
function downloadFile(row) {
|
||||
if (!row.designFileUrl) return
|
||||
const a = document.createElement('a')
|
||||
a.href = row.designFileUrl
|
||||
a.download = row.planName || '设计文件'
|
||||
a.target = '_blank'
|
||||
a.click()
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.manager-plans { padding: 16px; }
|
||||
.page-card { background: #fff; border-radius: 6px; padding: 16px; border: 1px solid #ebeef5; margin-bottom: 12px; }
|
||||
.page-title { font-size: 20px; font-weight: 600; margin-bottom: 4px; padding-left: 10px; border-left: 3px solid var(--brand-primary); }
|
||||
.desc { color: #909399; font-size: 13px; margin-bottom: 12px; }
|
||||
.filter-tip { color: #ff4d4f; font-size: 12px; margin-top: 8px; }
|
||||
.page-toolbar { display: flex; gap: 12px; margin-bottom: 12px; flex-wrap: wrap; align-items: center; }
|
||||
.page-toolbar :deep(.action-btn) {
|
||||
height: 36px; padding: 0 20px; font-size: 14px; border-radius: 4px;
|
||||
background: var(--brand-primary); color: #fff; border: 1px solid var(--brand-primary);
|
||||
display: inline-flex; align-items: center; justify-content: center; white-space: nowrap;
|
||||
transition: opacity .15s; margin-left: 0;
|
||||
}
|
||||
.page-toolbar :deep(.action-btn:hover) { opacity: .85; }
|
||||
.page-toolbar :deep(.action-btn.secondary) {
|
||||
background: #fff; color: var(--brand-primary); border: 1px solid var(--brand-primary);
|
||||
}
|
||||
.page-toolbar :deep(.action-btn.secondary:hover) { background: rgba(30,58,138,.06); }
|
||||
.page-toolbar :deep(.action-btn.is-disabled),
|
||||
.page-toolbar :deep(.action-btn:disabled) { background: #f5f7fa; color: #c0c4cc; border-color: #e4e7ed; opacity: 1; cursor: not-allowed; }
|
||||
.page-toolbar :deep(.action-btn.secondary.is-disabled),
|
||||
.page-toolbar :deep(.action-btn.secondary:disabled) { background: #fff; color: #c0c4cc; border-color: #e4e7ed; }
|
||||
.page-toolbar :deep(.action-btn .badge) {
|
||||
display: inline-block; margin-left: 6px; padding: 0 6px; min-width: 18px; height: 18px;
|
||||
line-height: 18px; font-size: 11px; text-align: center;
|
||||
background: #fff; color: var(--brand-primary); border-radius: 9px;
|
||||
}
|
||||
.page-toolbar :deep(.action-btn.secondary .badge) { background: var(--brand-primary); color: #fff; }
|
||||
</style>
|
||||
@@ -0,0 +1,834 @@
|
||||
<template>
|
||||
<div class="page-card">
|
||||
<div class="breadcrumb">首页 / 项目管理</div>
|
||||
|
||||
<el-form inline :model="q" class="filter-form">
|
||||
<el-form-item label="赞助公司:"><el-input v-model="q.orgName" placeholder="输入赞助公司" clearable /></el-form-item>
|
||||
<el-form-item label="公司类型:">
|
||||
<el-select v-model="q.orgType" placeholder="全部" clearable style="width:120px">
|
||||
<el-option label="赞助方" value="sponsor" />
|
||||
<el-option label="执行方" value="executor" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="项目编号:"><el-input v-model="q.projectNo" placeholder="输入项目编号" clearable /></el-form-item>
|
||||
<el-form-item label="项目名称:"><el-input v-model="q.projectName" placeholder="输入项目名称" clearable /></el-form-item>
|
||||
<el-form-item label="项目形式:">
|
||||
<el-select v-model="q.projectForm" placeholder="请选择" clearable>
|
||||
<el-option label="线上" value="线上" /><el-option label="线下" value="下线" />
|
||||
<el-option label="线上+线下" value="线上+线下" /><el-option label="其他" value="其他" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="项目时间:">
|
||||
<el-date-picker v-model="q.startTime" type="date" placeholder="开始日期" value-format="YYYY-MM-DD HH:mm:ss" />
|
||||
<span style="margin:0 4px;">至</span>
|
||||
<el-date-picker v-model="q.endTime" type="date" placeholder="结束日期" value-format="YYYY-MM-DD HH:mm:ss" />
|
||||
</el-form-item>
|
||||
<el-form-item label="是否结题:">
|
||||
<el-select v-model="q.isFinished" placeholder="请选择" clearable>
|
||||
<el-option label="已结题" value="1" /><el-option label="未结题" value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否结算:">
|
||||
<el-select v-model="q.isSettled" placeholder="请选择" clearable>
|
||||
<el-option label="已结算" value="1" /><el-option label="未结算" value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="评价得分:"><el-input v-model="q.ratingScore" placeholder="输入评分" clearable /></el-form-item>
|
||||
<el-form-item><el-button type="primary" @click="load">查找</el-button><el-button @click="reset">重置</el-button></el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="toolbar">
|
||||
<el-button class="action-btn" @click="openNewProject">新建项目</el-button>
|
||||
<el-button class="action-btn secondary" :disabled="!selection.length" @click="openAssign">
|
||||
项目分配<span v-if="selection.length" class="badge">{{ selection.length }}</span>
|
||||
</el-button>
|
||||
<el-button class="action-btn secondary" :disabled="!selection.length" @click="openBatch('score')">
|
||||
批量评分<span v-if="selection.length" class="badge">{{ selection.length }}</span>
|
||||
</el-button>
|
||||
<el-button class="action-btn secondary" :disabled="!selection.length" @click="openBatch('close')">
|
||||
批量结题<span v-if="selection.length" class="badge">{{ selection.length }}</span>
|
||||
</el-button>
|
||||
<el-button class="action-btn secondary" :disabled="!selection.length" @click="openBatch('activate')">
|
||||
批量开通<span v-if="selection.length" class="badge">{{ selection.length }}</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table :data="rows" v-loading="loading" stripe border @selection-change="onSel">
|
||||
<el-table-column type="selection" width="44" />
|
||||
<el-table-column prop="projectNo" label="项目编号" width="140" fixed="left" />
|
||||
<el-table-column prop="projectName" label="项目名称" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="totalSessions" label="总场次/总期数" width="100" align="center" />
|
||||
<el-table-column prop="doneSessions" label="已执行" width="80" align="center" />
|
||||
<el-table-column prop="todoSessions" label="未执行" width="80" align="center" />
|
||||
<el-table-column prop="totalAmount" label="总金额" width="130" align="right" :formatter="fmtMoney" />
|
||||
<el-table-column prop="availableAmount" label="可用金额" width="130" align="right" :formatter="fmtMoney" />
|
||||
<el-table-column prop="paidLaborAmount" label="已支付劳务费" width="140" align="right" :formatter="fmtMoney" />
|
||||
<el-table-column prop="paidMeetingAmount" label="已支付会务费" width="140" align="right" :formatter="fmtMoney" />
|
||||
<el-table-column prop="complianceScore" label="执行单位得分(合规)" width="140" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.ratingScore != null">{{ row.ratingScore }}</span>
|
||||
<span v-else style="color:#c0c4cc">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="orgName" label="赞助公司" width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="orgType" label="公司类型" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.orgType === 'sponsor' ? 'success' : 'primary'" disable-transitions>
|
||||
{{ row.orgType === 'sponsor' ? '赞助方' : '执行方' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="projectForm" label="项目形式" width="100" align="center" />
|
||||
<el-table-column prop="isFinished" label="是否结题" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.isFinished === '1' || row.isFinished === 1" type="success">已结题</el-tag>
|
||||
<el-tag v-else type="warning">未结题</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="startTime" label="项目开始时间" width="120" align="center" :formatter="fmtDate" />
|
||||
<el-table-column prop="endTime" label="项目结束时间" width="120" align="center" :formatter="fmtDate" />
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div class="op-cell">
|
||||
<el-button link type="primary" @click="viewDetail(row)">查看</el-button>
|
||||
<el-button link type="primary" @click="doEdit(row)">编辑</el-button>
|
||||
<el-dropdown trigger="click" @command="(cmd) => onAction(cmd, row)">
|
||||
<el-button link type="primary" class="op-dropdown">
|
||||
更多<el-icon class="op-caret"><ArrowDown /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="createMeeting">建会</el-dropdown-item>
|
||||
<el-dropdown-item v-if="row.isFinished !== '1' && row.isFinished !== 1" command="close">结题</el-dropdown-item>
|
||||
<el-dropdown-item command="assign">项目分配</el-dropdown-item>
|
||||
<el-dropdown-item command="rate">执行单位评分</el-dropdown-item>
|
||||
<el-dropdown-item v-if="row.isPublished === '1' || row.publishUrl" command="activate" divided>开通</el-dropdown-item>
|
||||
<el-dropdown-item v-if="row.isPublished === '1' || row.publishUrl" command="delAnn" divided>删除公告</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize" :total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper" @current-change="load" @size-change="load" />
|
||||
</div>
|
||||
|
||||
<!-- 项目详情已迁到独立页面 /manager/projects/detail/:projectId -->
|
||||
|
||||
<Preview v-model="previewOpen" :url="previewUrl" :title="previewTitle" />
|
||||
|
||||
<!-- ============= 新建项目 dialog(已迁移到独立页面 /manager/projects/new) ============= -->
|
||||
|
||||
<!-- ============= 项目分配 dialog(按原型 project-assign.html 1:1) ============= -->
|
||||
<el-dialog v-model="assignOpen" :title="assignBatchMode ? `批量项目分配 (${assignBatchProjects.length} 个项目)` : '项目分配'" width="780px" @closed="resetAssignForm">
|
||||
<el-form :model="assignForm" label-width="120px">
|
||||
<!-- 单选模式: 选项目 -->
|
||||
<el-form-item v-if="!assignBatchMode" label="项目">
|
||||
<el-select v-model="assignForm.projectId" placeholder="请选择项目" filterable style="width:100%" @change="onAssignProjectChange">
|
||||
<el-option v-for="r in rows" :key="r.projectId" :label="`${r.projectNo} - ${r.projectName}`" :value="r.projectId" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!-- 批量模式: 显示已选项目数 -->
|
||||
<el-form-item v-else label="已选项目">
|
||||
<div style="color:#606266;font-size:13px">共 <b>{{ assignBatchProjects.length }}</b> 个项目</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="支持方">
|
||||
<el-select v-model="assignForm.orgId" placeholder="请选择赞助公司" filterable :filter-method="searchSupporters" clearable style="width:100%" @change="onSupporterPick">
|
||||
<el-option v-for="u in supporterOptions" :key="u.userId" :label="`${u.userName} (${u.nickName || ''})`" :value="u.userId" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-divider>执行方分配</el-divider>
|
||||
<el-table :data="assignForm.execRows" border>
|
||||
<el-table-column type="index" label="#" width="50" />
|
||||
<el-table-column label="执行方名称" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<el-select v-model="row.execUserId" filterable
|
||||
:filter-method="q => searchExecutors(q, row)"
|
||||
:loading="row._loading"
|
||||
placeholder="搜索执行方用户"
|
||||
style="width:100%"
|
||||
@change="v => onExecUserPick(row, v)">
|
||||
<el-option v-for="u in (row._options || [])" :key="u.userId"
|
||||
:label="`${u.userName}${u.nickName ? ' (' + u.nickName + ')' : ''}`" :value="u.userId" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="场次" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-input-number v-model="row.sessions" :min="0" controls-position="right" style="width:100%" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额" width="140">
|
||||
<template #default="{ row }">
|
||||
<el-input-number v-model="row.amount" :min="0" :precision="2" controls-position="right" style="width:100%" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="80">
|
||||
<template #default="{ $index }">
|
||||
<el-button link type="danger" @click="assignForm.execRows.splice($index, 1)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div style="margin-top:8px">
|
||||
<el-button type="primary" link @click="addExecRow">+ 增加执行方</el-button>
|
||||
</div>
|
||||
<el-form-item label="提交截止(天)" style="margin-top:12px">
|
||||
<el-input-number v-model="assignForm.deadlineDays" :min="0" controls-position="right" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="assignOpen=false">取消</el-button>
|
||||
<el-button type="primary" :loading="assignSubmitting" @click="submitAssign">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- ============= 批量评分 dialog(按原型 4 维度评分) ============= -->
|
||||
<el-dialog v-model="batchOpen" :title="batchMap[batchKind]" width="626px" :show-close="false">
|
||||
<!-- 评分专用字段: 共享一份评分 (与 singleScoreForm 同样的三列 table 布局) -->
|
||||
<template v-if="batchKind==='score'">
|
||||
<p style="color:#606266;margin-bottom:8px">将对以下 <b>{{ selection.length }}</b> 个项目共用同一份评分:</p>
|
||||
<table class="score-table" style="width:100%;border-collapse:collapse;font-size:13px">
|
||||
<thead><tr style="background:#fafafa">
|
||||
<th style="width:110px;padding:10px 12px;text-align:left">评价维度</th>
|
||||
<th style="padding:10px 12px;text-align:left">评价内容(简洁版)</th>
|
||||
<th style="width:130px;padding:10px 12px;text-align:center">评分标准(1~5 分)</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<tr><td style="padding:10px 12px;border-top:1px solid #f5f5f5">履约质量</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5">服务/活动效果达标度</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5;text-align:center"><el-input-number v-model="batchScoreForm.q1" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></td></tr>
|
||||
<tr><td style="padding:10px 12px;border-top:1px solid #f5f5f5">时效响应</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5">执行 & 售后响应速度</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5;text-align:center"><el-input-number v-model="batchScoreForm.q2" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></td></tr>
|
||||
<tr><td style="padding:10px 12px;border-top:1px solid #f5f5f5">配合度</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5">沟通配合 & 问题处理</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5;text-align:center"><el-input-number v-model="batchScoreForm.q3" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></td></tr>
|
||||
<tr><td style="padding:10px 12px;border-top:1px solid #f5f5f5">合规安全</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5">流程合规 & 无安全事故</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5;text-align:center"><el-input-number v-model="batchScoreForm.q4" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
<!-- 结题专用字段 -->
|
||||
<template v-else-if="batchKind==='close'">
|
||||
<p>已选择 <b>{{ selection.length }}</b> 个项目。</p>
|
||||
<p style="color:#606266;margin-top:8px">确定要将这些项目标记为结题吗?</p>
|
||||
</template>
|
||||
<!-- 开通专用字段 -->
|
||||
<template v-else-if="batchKind==='activate'">
|
||||
<p>已选择 <b>{{ selection.length }}</b> 个项目。</p>
|
||||
<el-form-item label="开通截止时间"><el-date-picker v-model="activateDeadline" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width:100%" /></el-form-item>
|
||||
</template>
|
||||
<template #footer>
|
||||
<el-button @click="batchOpen=false">取消</el-button>
|
||||
<el-button type="primary" :loading="batchSubmitting" @click="confirmBatch">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- ============= 单行结题 modal ============= -->
|
||||
<el-dialog v-model="closeModalOpen" title="您确定要结题吗?" width="420px" :show-close="false">
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="confirmClose">确定</el-button>
|
||||
<el-button @click="closeModalOpen=false">取消</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- ============= 单行开通 modal ============= -->
|
||||
<el-dialog v-model="activateModalOpen" title="您确定要开通吗?" width="420px" :show-close="false">
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="confirmActivate">确定</el-button>
|
||||
<el-button @click="activateModalOpen=false">取消</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- ============= 单行删除公告 modal ============= -->
|
||||
<el-dialog v-model="delAnnModalOpen" title="您确定要删除公告吗?删除后无法恢复" width="420px" :show-close="false">
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="confirmDeleteAnn">确定</el-button>
|
||||
<el-button @click="delAnnModalOpen=false">取消</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- ============= 单行执行单位评分 modal(4 维度) ============= -->
|
||||
<el-dialog v-model="singleScoreModalOpen" title="执行单位评分" width="626px" :show-close="false">
|
||||
<table class="score-table" style="width:100%;border-collapse:collapse;font-size:13px">
|
||||
<thead><tr style="background:#fafafa">
|
||||
<th style="width:110px;padding:10px 12px;text-align:left">评价维度</th>
|
||||
<th style="padding:10px 12px;text-align:left">评价内容(简洁版)</th>
|
||||
<th style="width:130px;padding:10px 12px;text-align:center">评分标准(1~5 分)</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<tr><td style="padding:10px 12px;border-top:1px solid #f5f5f5">履约质量</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5">服务/活动效果达标度</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5;text-align:center"><el-input-number v-model="singleScoreForm.q1" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></td></tr>
|
||||
<tr><td style="padding:10px 12px;border-top:1px solid #f5f5f5">时效响应</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5">执行 & 售后响应速度</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5;text-align:center"><el-input-number v-model="singleScoreForm.q2" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></td></tr>
|
||||
<tr><td style="padding:10px 12px;border-top:1px solid #f5f5f5">配合度</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5">沟通配合 & 问题处理</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5;text-align:center"><el-input-number v-model="singleScoreForm.q3" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></td></tr>
|
||||
<tr><td style="padding:10px 12px;border-top:1px solid #f5f5f5">合规安全</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5">流程合规 & 无安全事故</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5;text-align:center"><el-input-number v-model="singleScoreForm.q4" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="confirmSingleScore">确定</el-button>
|
||||
<el-button @click="singleScoreModalOpen=false">取消</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, watch } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { bizList, bizAdd, bizUpdate, bizDelete } from '@/api/public'
|
||||
import { listExecutor, listSupporters } from '@/api/system'
|
||||
import request from '@/utils/request'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ArrowDown } from '@element-plus/icons-vue'
|
||||
import Preview from '@/components/Preview.vue'
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const q = ref({ orgName: '', orgType: '', projectNo: '', projectName: '', projectForm: '', startTime: '', endTime: '', isFinished: '', isSettled: '', ratingScore: '' })
|
||||
const rows = ref([])
|
||||
const loading = ref(false)
|
||||
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
console.info('[manager-projects.load] START, q=', JSON.stringify(q.value), 'page=', JSON.stringify(page))
|
||||
// axios 拦截器已将 TableDataInfo 归一化为 {code, msg, data: {total, rows}}
|
||||
const { data } = await bizList('project', { ...q.value, pageNum: page.pageNum, pageSize: page.pageSize })
|
||||
console.info('[manager-projects.load] OK, total=', data?.total, 'rows.length=', data?.rows?.length, 'firstRow=', JSON.stringify(data?.rows?.[0] || null))
|
||||
rows.value = data?.rows || []
|
||||
page.total = data?.total || 0
|
||||
} catch (e) {
|
||||
console.error('[manager-projects.load] FAILED', e)
|
||||
rows.value = []; page.total = 0
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
// 操作列按钮 handlers
|
||||
function onAction(cmd, row) {
|
||||
if (cmd === 'createMeeting') doCreateMeeting(row)
|
||||
else if (cmd === 'close') openClose(row)
|
||||
else if (cmd === 'assign') doAssign(row)
|
||||
else if (cmd === 'rate') openSingleScore(row)
|
||||
else if (cmd === 'activate') openActivate(row)
|
||||
else if (cmd === 'delAnn') openDeleteAnnouncement(row)
|
||||
}
|
||||
function doCreateMeeting(row) { router.push(`/manager/meetings/new?projectId=${row.projectId}`) }
|
||||
function doAssign(row) {
|
||||
resetAssignForm()
|
||||
assignForm.projectId = row.projectId
|
||||
// 先打开 dialog, 再异步加载 (loadAssigns 是 async)
|
||||
assignOpen.value = true
|
||||
onAssignProjectChange(row.projectId)
|
||||
// 预加载 sponsor 候选
|
||||
setTimeout(() => loadSupporters(''), 100)
|
||||
}
|
||||
function doEdit(row) { router.push(`/manager/projects/edit/${row.projectId}`) }
|
||||
|
||||
// 删除项目(按需补充,原型无但需要)
|
||||
async function doDelete(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除项目"${row.projectName}"?\n项目编号: ${row.projectNo}\n\n删除后无法恢复!`, '删除确认', { type: 'error', confirmButtonText: '确认删除', cancelButtonText: '取消' })
|
||||
await bizDelete('project', row.projectId)
|
||||
ElMessage.success('已删除')
|
||||
load()
|
||||
} catch (e) {
|
||||
if (e !== 'cancel' && e !== 'close') ElMessage.error(e?.msg || '删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
function reset() { q.value = { orgName:'', orgType:'', projectNo:'', projectName:'', projectForm:'', startTime:'', endTime:'', isFinished:'', isSettled:'', ratingScore:'' }; page.pageNum = 1; load() }
|
||||
|
||||
// 导出
|
||||
function exportProjects() { ElMessage.info('导出项目功能开发中') }
|
||||
function exportEval() { ElMessage.info('项目评价导出功能开发中') }
|
||||
function exportExperts() { ElMessage.info('已报名专家信息导出功能开发中') }
|
||||
|
||||
// ========== 单行操作按钮 handlers(弹 5 个独立 dialog) ==========
|
||||
function openClose(row) {
|
||||
if (row.isFinished === '1' || row.isFinished === 1) {
|
||||
return ElMessage.warning('该项目已结题,无需重复操作')
|
||||
}
|
||||
closeTargetRow.value = row
|
||||
closeModalOpen.value = true
|
||||
}
|
||||
function confirmClose() {
|
||||
if (!closeTargetRow.value) return
|
||||
bizUpdate('project', { projectId: closeTargetRow.value.projectId, isFinished: '1' })
|
||||
.then(() => { ElMessage.success('结题成功'); closeModalOpen.value = false; load() })
|
||||
.catch(() => ElMessage.error('结题失败'))
|
||||
}
|
||||
|
||||
function openActivate(row) {
|
||||
activateTargetRow.value = row
|
||||
activateModalOpen.value = true
|
||||
}
|
||||
function confirmActivate() {
|
||||
if (!activateTargetRow.value) return
|
||||
bizUpdate('project', { projectId: activateTargetRow.value.projectId, isFinished: '0' })
|
||||
.then(() => { ElMessage.success('已开通'); activateModalOpen.value = false; load() })
|
||||
.catch(() => ElMessage.error('开通失败'))
|
||||
}
|
||||
|
||||
function openDeleteAnnouncement(row) {
|
||||
delAnnTargetRow.value = row
|
||||
delAnnModalOpen.value = true
|
||||
}
|
||||
function confirmDeleteAnn() {
|
||||
if (!delAnnTargetRow.value) return
|
||||
// 清空全部公示 URL + 取消发布状态
|
||||
// (mapper.updateByPrimaryKey 的 trim prefix="SET" 必须有字段, 不能只传空字符串)
|
||||
bizUpdate('project', {
|
||||
projectId: delAnnTargetRow.value.projectId,
|
||||
publishUrl: '',
|
||||
invitationUrl: '',
|
||||
supportLetterUrl: '',
|
||||
noticeUrl: '',
|
||||
scheduleUrl: '',
|
||||
isPublished: '0'
|
||||
})
|
||||
.then(() => { ElMessage.success(`已删除项目 ${delAnnTargetRow.value.projectNo} 的公告`); delAnnModalOpen.value = false; load() })
|
||||
.catch(() => ElMessage.error('删除失败'))
|
||||
}
|
||||
|
||||
function openSingleScore(row) {
|
||||
singleScoreTargetRow.value = row
|
||||
// 反显各维度原值 (从后端 ratingQ1~Q4 字段); 无值才归零
|
||||
singleScoreForm.q1 = row.ratingQ1 != null ? Number(row.ratingQ1) : 0
|
||||
singleScoreForm.q2 = row.ratingQ2 != null ? Number(row.ratingQ2) : 0
|
||||
singleScoreForm.q3 = row.ratingQ3 != null ? Number(row.ratingQ3) : 0
|
||||
singleScoreForm.q4 = row.ratingQ4 != null ? Number(row.ratingQ4) : 0
|
||||
singleScoreModalOpen.value = true
|
||||
}
|
||||
function confirmSingleScore() {
|
||||
if (!singleScoreTargetRow.value) return
|
||||
const dims = [singleScoreForm.q1, singleScoreForm.q2, singleScoreForm.q3, singleScoreForm.q4]
|
||||
const labels = ['履约质量', '时效响应', '配合度', '合规安全']
|
||||
// 4 个维度全部必填
|
||||
for (let i = 0; i < dims.length; i++) {
|
||||
if (dims[i] === 0 || dims[i] === '' || dims[i] == null) {
|
||||
return ElMessage.error(`请填写「${labels[i]}」评分`)
|
||||
}
|
||||
const v = Number(dims[i])
|
||||
if (isNaN(v) || v < 1 || v > 5) return ElMessage.error(`${labels[i]}必须是 1~5 的数字`)
|
||||
}
|
||||
// 校验通过, 计算平均分
|
||||
const scores = dims.map(v => Number(v))
|
||||
const avg = (scores.reduce((s, v) => s + v, 0) / scores.length).toFixed(1)
|
||||
// 构造 payload: 平均分 + 4 个维度分
|
||||
const payload = {
|
||||
projectId: singleScoreTargetRow.value.projectId,
|
||||
ratingScore: Number(avg),
|
||||
ratingQ1: scores[0],
|
||||
ratingQ2: scores[1],
|
||||
ratingQ3: scores[2],
|
||||
ratingQ4: scores[3]
|
||||
}
|
||||
bizUpdate('project', payload)
|
||||
.then(() => { ElMessage.success(`评分完成,平均分:${avg}`); singleScoreModalOpen.value = false; load() })
|
||||
.catch(() => ElMessage.error('评分失败'))
|
||||
}
|
||||
function viewDetail(row) { router.push(`/manager/projects/detail/${row.projectId}`) }
|
||||
|
||||
// 金额格式化: 1234567.5 -> "1,234,567.50"
|
||||
function fmtMoney(row, col, v) {
|
||||
if (v === null || v === undefined || v === '') return ''
|
||||
const n = Number(v)
|
||||
if (isNaN(n)) return String(v)
|
||||
return n.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 2 })
|
||||
}
|
||||
// 日期格式化: "2026-08-09" 或 "2026-08-09 10:30:00" -> "2026.08.09"
|
||||
function fmtDate(row, col, v) {
|
||||
if (!v) return ''
|
||||
const s = String(v)
|
||||
const m = s.match(/^(\d{4})-(\d{2})-(\d{2})/)
|
||||
return m ? `${m[1]}.${m[2]}.${m[3]}` : s
|
||||
}
|
||||
|
||||
const selection = ref([])
|
||||
function onSel(rs) { selection.value = rs }
|
||||
const batchOpen = ref(false)
|
||||
const batchKind = ref('')
|
||||
const batchSubmitting = ref(false)
|
||||
const batchMap = { score: '批量评分', close: '批量结题', activate: '批量开通' }
|
||||
const batchScoreRows = ref([]) // 保留兼容旧引用 (实际不再使用)
|
||||
const batchScoreForm = reactive({ q1: 5, q2: 5, q3: 5, q4: 5 }) // 批量评分: 共享一份 4 维度
|
||||
const activateDeadline = ref('')
|
||||
|
||||
// 5 个单行 modal 状态
|
||||
const closeModalOpen = ref(false)
|
||||
const closeTargetRow = ref(null)
|
||||
const activateModalOpen = ref(false)
|
||||
const activateTargetRow = ref(null)
|
||||
const delAnnModalOpen = ref(false)
|
||||
const delAnnTargetRow = ref(null)
|
||||
const singleScoreModalOpen = ref(false)
|
||||
const singleScoreTargetRow = ref(null)
|
||||
const singleScoreForm = reactive({ q1: 0, q2: 0, q3: 0, q4: 0 })
|
||||
|
||||
// 新建项目已迁到独立页面 /manager/projects/new (ManagerProjectsNew.vue)
|
||||
function openNewProject() {
|
||||
router.push('/manager/projects/new')
|
||||
}
|
||||
function openAssign() {
|
||||
if (!selection.value.length) {
|
||||
ElMessage.warning('请先勾选项目')
|
||||
return
|
||||
}
|
||||
resetAssignForm()
|
||||
if (selection.value.length >= 2) {
|
||||
assignBatchMode.value = true
|
||||
assignBatchProjects.value = selection.value.map(r => ({ projectId: r.projectId, projectNo: r.projectNo, projectName: r.projectName }))
|
||||
} else {
|
||||
assignBatchMode.value = false
|
||||
assignBatchProjects.value = []
|
||||
assignForm.projectId = selection.value[0].projectId
|
||||
onAssignProjectChange(assignForm.projectId)
|
||||
}
|
||||
assignOpen.value = true
|
||||
setTimeout(() => {
|
||||
assignForm.execRows.forEach(r => searchExecutors('', r))
|
||||
loadSupporters('')
|
||||
}, 100)
|
||||
}
|
||||
function openBatch(kind) {
|
||||
if (!selection.value.length) return ElMessage.warning('请先勾选项目')
|
||||
batchKind.value = kind
|
||||
if (kind === 'score') {
|
||||
// 共享评分: 重置 form 为默认值 (5 分)
|
||||
Object.assign(batchScoreForm, { q1: 5, q2: 5, q3: 5, q4: 5 })
|
||||
}
|
||||
if (kind === 'activate') {
|
||||
activateDeadline.value = ''
|
||||
}
|
||||
batchOpen.value = true
|
||||
}
|
||||
|
||||
async function confirmBatch() {
|
||||
if (batchKind.value === 'close') {
|
||||
// 过滤掉已结题项目, 避免无效请求
|
||||
const targets = selection.value.filter(r => r.isFinished !== '1' && r.isFinished !== 1)
|
||||
const skipCount = selection.value.length - targets.length
|
||||
try {
|
||||
batchSubmitting.value = true
|
||||
await Promise.all(targets.map(r => bizUpdate('project', { projectId: r.projectId, isFinished: '1' })))
|
||||
ElMessage.success(skipCount > 0
|
||||
? `已批量结题 ${targets.length} 个项目 (跳过 ${skipCount} 个已结题)`
|
||||
: `已批量结题 ${targets.length} 个项目`)
|
||||
batchOpen.value = false
|
||||
selection.value = []
|
||||
load()
|
||||
} catch { ElMessage.error('批量结题失败') }
|
||||
finally { batchSubmitting.value = false }
|
||||
} else if (batchKind.value === 'activate') {
|
||||
if (!activateDeadline.value) return ElMessage.warning('请选择开通截止时间')
|
||||
try {
|
||||
batchSubmitting.value = true
|
||||
await Promise.all(selection.value.map(r => bizUpdate('project', { projectId: r.projectId, endTime: activateDeadline.value })))
|
||||
ElMessage.success(`已批量开通 ${selection.value.length} 个项目`)
|
||||
batchOpen.value = false
|
||||
selection.value = []
|
||||
load()
|
||||
} catch { ElMessage.error('批量开通失败') }
|
||||
finally { batchSubmitting.value = false }
|
||||
} else if (batchKind.value === 'score') {
|
||||
const q1 = Number(batchScoreForm.q1 || 0)
|
||||
const q2 = Number(batchScoreForm.q2 || 0)
|
||||
const q3 = Number(batchScoreForm.q3 || 0)
|
||||
const q4 = Number(batchScoreForm.q4 || 0)
|
||||
const total = ((q1 + q2 + q3 + q4) / 4).toFixed(1)
|
||||
try {
|
||||
batchSubmitting.value = true
|
||||
let ok = 0
|
||||
// 共享评分: 所有项目用同一组 q1..q4
|
||||
for (const r of selection.value) {
|
||||
try {
|
||||
await bizUpdate('project', {
|
||||
projectId: r.projectId,
|
||||
ratingScore: Number(total),
|
||||
ratingQ1: q1,
|
||||
ratingQ2: q2,
|
||||
ratingQ3: q3,
|
||||
ratingQ4: q4
|
||||
})
|
||||
ok++
|
||||
} catch {}
|
||||
}
|
||||
ElMessage.success(`已批量评分 ${ok}/${selection.value.length} 个项目 (共用评分 ${total})`)
|
||||
batchOpen.value = false
|
||||
selection.value = []
|
||||
load()
|
||||
} finally { batchSubmitting.value = false }
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 新建项目 ==========
|
||||
const newOpen = ref(false)
|
||||
// 新建项目独立页面: /manager/projects/new (ManagerProjectsNew.vue)
|
||||
|
||||
// ========== 项目分配 ==========
|
||||
const assignOpen = ref(false)
|
||||
const assignBatchMode = ref(false) // 项目分配批量模式开关
|
||||
const assignBatchProjects = ref([]) // 批量模式下的项目列表 [{projectId, projectNo, projectName}]
|
||||
const assignSubmitting = ref(false)
|
||||
const assignForm = reactive({
|
||||
projectId: '',
|
||||
orgId: null,
|
||||
orgName: '',
|
||||
execRows: [], // 动态加
|
||||
deadlineDays: 30
|
||||
})
|
||||
const supporterOptions = ref([])
|
||||
let _supporterTimer = null
|
||||
|
||||
function makeEmptyExecRow() {
|
||||
return { _loading: false, _timer: null, _options: [], execUserId: null, execUserName: '', execNickName: '', execOrg: '', sessions: 0, amount: 0, remark: '' }
|
||||
}
|
||||
|
||||
function resetAssignForm() {
|
||||
Object.assign(assignForm, { projectId:'', orgId:null, orgName:'', execRows:[makeEmptyExecRow()], deadlineDays:30 })
|
||||
supporterOptions.value = []
|
||||
assignBatchMode.value = false
|
||||
assignBatchProjects.value = []
|
||||
}
|
||||
|
||||
function addExecRow() {
|
||||
const row = makeEmptyExecRow()
|
||||
assignForm.execRows.push(row)
|
||||
// 新行立即加载默认列表
|
||||
searchExecutors('', row)
|
||||
}
|
||||
|
||||
function onAssignProjectChange(v) {
|
||||
const proj = rows.value.find(r => r.projectId === v)
|
||||
if (proj) {
|
||||
assignForm.orgId = proj.orgId || null
|
||||
assignForm.orgName = proj.orgName || ''
|
||||
}
|
||||
// 切换项目时重新拉已分配记录 (回显)
|
||||
loadAssigns(v)
|
||||
// 重新拉支持方候选
|
||||
loadSupporters('')
|
||||
}
|
||||
|
||||
// 加载支持方 (sponsor 角色)
|
||||
async function loadSupporters(query) {
|
||||
if (_supporterTimer) clearTimeout(_supporterTimer)
|
||||
_supporterTimer = setTimeout(async () => {
|
||||
try {
|
||||
const params = { pageNum: 1, pageSize: 50 }
|
||||
if (query) params.userName = query
|
||||
const r = await listSupporters(params)
|
||||
supporterOptions.value = r.data || []
|
||||
// 已选项不在结果里时单独拉取并加首位
|
||||
if (assignForm.orgId && !supporterOptions.value.find(u => u.userId === assignForm.orgId)) {
|
||||
const r2 = await listSupporters({ userId: assignForm.orgId })
|
||||
const sel = (r2.data || []).find(u => u.userId === assignForm.orgId)
|
||||
if (sel) supporterOptions.value = [sel, ...supporterOptions.value]
|
||||
}
|
||||
} catch (e) {
|
||||
supporterOptions.value = []
|
||||
}
|
||||
}, 200)
|
||||
}
|
||||
|
||||
function searchSupporters(q) { loadSupporters(q) }
|
||||
|
||||
// 选中支持方后回填名称
|
||||
function onSupporterPick(userId) {
|
||||
const u = supporterOptions.value.find(x => x.userId === userId)
|
||||
if (u) assignForm.orgName = u.nickName || u.userName
|
||||
}
|
||||
|
||||
// 拉取项目已分配的执行方
|
||||
async function loadAssigns(projectId) {
|
||||
if (!projectId) { assignForm.execRows = [makeEmptyExecRow()]; return }
|
||||
try {
|
||||
const r = await request({ url: `/business/project/${projectId}/assigns`, method: 'get' })
|
||||
const list = r.data || []
|
||||
if (list.length) {
|
||||
assignForm.execRows = list.map(a => ({
|
||||
...makeEmptyExecRow(),
|
||||
execUserId: a.execUserId,
|
||||
execUserName: a.execUserName,
|
||||
execNickName: a.execNickName,
|
||||
execOrg: a.execOrg,
|
||||
sessions: a.sessions || 0,
|
||||
amount: Number(a.amount || 0),
|
||||
remark: a.remark || ''
|
||||
}))
|
||||
} else {
|
||||
assignForm.execRows = [makeEmptyExecRow()]
|
||||
}
|
||||
// 每一行立即触发远程搜索, 让 el-select 拿到 _options 才能正确显示已选 label
|
||||
assignForm.execRows.forEach(r => searchExecutors('', r))
|
||||
} catch (e) {
|
||||
console.error('加载已分配执行方失败', e)
|
||||
assignForm.execRows = [makeEmptyExecRow()]
|
||||
assignForm.execRows.forEach(r => searchExecutors('', r))
|
||||
}
|
||||
}
|
||||
|
||||
// 加载执行方 (row 级 debounce, 每个 row 独立 timer)
|
||||
async function searchExecutors(query, row) {
|
||||
if (row._timer) clearTimeout(row._timer)
|
||||
row._loading = true
|
||||
row._timer = setTimeout(async () => {
|
||||
try {
|
||||
const params = { pageNum: 1, pageSize: 20 }
|
||||
if (query) params.userName = query
|
||||
const r = await listExecutor(params)
|
||||
row._options = r.data || []
|
||||
// 选中的项如果不在结果里, 加到首位
|
||||
if (row.execUserId && !row._options.find(u => u.userId === row.execUserId)) {
|
||||
const r2 = await listExecutor({ userId: row.execUserId })
|
||||
const sel = (r2.data || []).find(u => u.userId === row.execUserId)
|
||||
if (sel) row._options = [sel, ...row._options]
|
||||
}
|
||||
} catch (e) {
|
||||
row._options = []
|
||||
} finally {
|
||||
row._loading = false
|
||||
}
|
||||
}, 300)
|
||||
}
|
||||
|
||||
// 选中执行方后回填
|
||||
function onExecUserPick(row, userId) {
|
||||
const u = (row._options || []).find(x => x.userId === userId)
|
||||
if (u) {
|
||||
row.execUserName = u.userName
|
||||
row.execNickName = u.nickName || ''
|
||||
row.execOrg = u.dept?.deptName || ''
|
||||
}
|
||||
}
|
||||
|
||||
async function submitAssign() {
|
||||
// 批量模式不需要选项目, 单选模式必选
|
||||
if (!assignBatchMode.value && !assignForm.projectId) return ElMessage.warning('请选择项目')
|
||||
const valid = assignForm.execRows.filter(r => r.execUserId && Number(r.sessions) > 0)
|
||||
if (!valid.length) return ElMessage.warning('请至少选择 1 个执行方并填写场次')
|
||||
const amount = valid.reduce((s, r) => s + Number(r.amount || 0), 0)
|
||||
const sessions = valid.reduce((s, r) => s + Number(r.sessions || 0), 0)
|
||||
const assignBody = valid.map(r => ({
|
||||
execUserId: r.execUserId,
|
||||
execUserName: r.execUserName,
|
||||
execNickName: r.execNickName,
|
||||
execOrg: r.execOrg,
|
||||
sessions: r.sessions,
|
||||
amount: r.amount,
|
||||
remark: r.remark
|
||||
}))
|
||||
assignSubmitting.value = true
|
||||
try {
|
||||
if (assignBatchMode.value) {
|
||||
// 批量: 每个项目单独更新 + 单独写分配 (后端先删后插策略)
|
||||
let ok = 0, errs = []
|
||||
for (const p of assignBatchProjects.value) {
|
||||
try {
|
||||
await bizUpdate('project', {
|
||||
projectId: p.projectId,
|
||||
orgId: assignForm.orgId,
|
||||
orgName: assignForm.orgName,
|
||||
totalAmount: amount,
|
||||
totalSessions: sessions,
|
||||
submitDeadlineDays: assignForm.deadlineDays,
|
||||
})
|
||||
await request({
|
||||
url: `/business/project/${p.projectId}/assigns`,
|
||||
method: 'post',
|
||||
data: assignBody
|
||||
})
|
||||
ok++
|
||||
} catch (e) {
|
||||
errs.push(`${p.projectNo}: ${e?.msg || e.message}`)
|
||||
}
|
||||
}
|
||||
if (errs.length) ElMessageBox.alert(`批量分配: 成功 ${ok}/${assignBatchProjects.value.length}\n\n失败:\n${errs.join('\n')}`, '分配结果', { type: 'warning' })
|
||||
else ElMessage.success(`批量分配成功 (${ok}/${assignBatchProjects.value.length})`)
|
||||
} else {
|
||||
// 单条模式
|
||||
await bizUpdate('project', {
|
||||
projectId: assignForm.projectId,
|
||||
orgId: assignForm.orgId,
|
||||
orgName: assignForm.orgName,
|
||||
totalAmount: amount,
|
||||
totalSessions: sessions,
|
||||
submitDeadlineDays: assignForm.deadlineDays,
|
||||
})
|
||||
await request({
|
||||
url: `/business/project/${assignForm.projectId}/assigns`,
|
||||
method: 'post',
|
||||
data: assignBody
|
||||
})
|
||||
ElMessage.success('分配成功')
|
||||
}
|
||||
assignOpen.value = false
|
||||
load()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
ElMessage.error(e?.msg || '分配失败')
|
||||
}
|
||||
finally { assignSubmitting.value = false }
|
||||
}
|
||||
|
||||
const previewOpen = ref(false)
|
||||
const previewUrl = ref('')
|
||||
const previewTitle = ref('附件预览')
|
||||
function openPreview(url, title) { previewUrl.value = url; previewTitle.value = title || '附件预览'; previewOpen.value = true }
|
||||
|
||||
onMounted(load)
|
||||
// 监听路由变化 (从 /manager/projects/new 跳回时强制重载)
|
||||
watch(() => route.fullPath, (newPath, oldPath) => {
|
||||
if (newPath === '/manager/projects') {
|
||||
console.info('[manager-projects] route watch reload, from=', oldPath, 'to=', newPath)
|
||||
load()
|
||||
}
|
||||
})
|
||||
// 兜底: 即便路由完全相同,从列表跳到详情再回来也触发
|
||||
watch(() => route.path + (route.query._t || ''), () => load())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 1:1 抄 sponsor/SponsorProjects.vue */
|
||||
.page-card { background: #fff; padding: 16px 20px; border-radius: 6px; border: 1px solid #f0f0f0; }
|
||||
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
|
||||
.filter-form { display: flex; flex-wrap: wrap; gap: 0 16px; padding-bottom: 12px; border-bottom: 1px solid #f0f0f0; margin-bottom: 12px; }
|
||||
.toolbar { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; }
|
||||
|
||||
/* 操作列下拉 */
|
||||
.op-cell { display: inline-flex; align-items: center; gap: 12px; }
|
||||
.op-cell :deep(.el-button) { margin-left: 0 !important; padding: 0 4px !important; }
|
||||
.op-dropdown { padding-right: 0; }
|
||||
.op-caret { margin-left: 2px; font-size: 10px; }
|
||||
|
||||
/* 工具按钮 (与 sponsor 一致) */
|
||||
.toolbar :deep(.action-btn) {
|
||||
height: 32px; padding: 0 16px; font-size: 13px; border-radius: 4px;
|
||||
background: var(--brand-primary); color: #fff; border: 1px solid var(--brand-primary);
|
||||
display: inline-flex; align-items: center; justify-content: center; white-space: nowrap;
|
||||
transition: opacity .15s;
|
||||
}
|
||||
.toolbar :deep(.action-btn:hover) { opacity: .85; }
|
||||
.toolbar :deep(.action-btn.secondary) {
|
||||
background: #fff; color: var(--brand-primary); border: 1px solid var(--brand-primary);
|
||||
}
|
||||
.toolbar :deep(.action-btn:disabled) {
|
||||
background: #f5f7fa; color: #c0c4cc; border-color: #e4e7ed; cursor: not-allowed;
|
||||
}
|
||||
.toolbar :deep(.badge) {
|
||||
margin-left: 4px; padding: 0 6px; background: var(--brand-primary); color: #fff;
|
||||
border-radius: 8px; font-size: 11px; line-height: 16px;
|
||||
}
|
||||
.toolbar :deep(.action-btn.secondary .badge) { background: var(--brand-primary); color: #fff; }
|
||||
.page-toolbar :deep(.action-btn.secondary:hover) { background: rgba(30,58,138,.06); }
|
||||
.page-toolbar :deep(.action-btn.is-disabled),
|
||||
.page-toolbar :deep(.action-btn:disabled) { background: #f5f7fa; color: #c0c4cc; border-color: #e4e7ed; opacity: 1; cursor: not-allowed; }
|
||||
.page-toolbar :deep(.action-btn.secondary.is-disabled),
|
||||
.page-toolbar :deep(.action-btn.secondary:disabled) { background: #fff; color: #c0c4cc; border-color: #e4e7ed; }
|
||||
.page-toolbar :deep(.action-btn .badge) {
|
||||
display: inline-block; margin-left: 6px; padding: 0 6px; min-width: 18px; height: 18px;
|
||||
line-height: 18px; font-size: 11px; text-align: center;
|
||||
background: #fff; color: var(--brand-primary); border-radius: 9px;
|
||||
}
|
||||
.page-toolbar :deep(.action-btn.secondary .badge) { background: var(--brand-primary); color: #fff; }
|
||||
.score-row { padding: 8px 0; border-bottom: 1px solid #f0f0f0; }
|
||||
.score-row:last-child { border-bottom: none; }
|
||||
.score-title { font-weight: 600; color: #303133; margin-bottom: 6px; }
|
||||
.score-no { color: #909399; font-weight: normal; font-size: 12px; }
|
||||
.score-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
||||
.score-item { display: flex; align-items: center; gap: 8px; }
|
||||
.score-item span { width: 70px; color: #606266; font-size: 13px; }
|
||||
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,504 @@
|
||||
<template>
|
||||
<div class="page-card">
|
||||
<!-- 面包屑 (与 sponsor 端一致: 首页 / XXX / 当前页) -->
|
||||
<div class="breadcrumb">
|
||||
首页 / 项目管理 / <span class="current">{{ isEdit ? '编辑项目' : '新建项目' }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 第一区块:项目信息 -->
|
||||
<el-card shadow="never" class="new-card">
|
||||
<div class="new-card-title">项目信息</div>
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="项目名称" prop="projectName">
|
||||
<el-input v-model="form.projectName" placeholder="请输入项目名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="项目编号" prop="projectNo">
|
||||
<el-input v-model="form.projectNo" placeholder="请输入项目编号" />
|
||||
</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">
|
||||
<el-input-number v-model="form.totalSessions" :min="1" style="width:100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="总金额(元)" prop="totalAmount">
|
||||
<el-input-number v-model="form.totalAmount" :min="0" :precision="2" style="width:100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="管理费(元)">
|
||||
<el-input-number v-model="form.manageFee" :min="0" :precision="2" style="width:100%" />
|
||||
</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>
|
||||
<!-- 支持单位 -->
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="提交截止(天)">
|
||||
<el-input-number v-model="form.submitDeadlineDays" :min="0" controls-position="right" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="支持单位">
|
||||
<el-select v-model="form.orgId" placeholder="请选择赞助公司" filterable clearable style="width:100%" @change="onSupporterPick">
|
||||
<el-option v-for="u in supporterOptions" :key="u.userId"
|
||||
:label="`${u.userName}${u.nickName ? ' (' + u.nickName + ')' : ''}`"
|
||||
:value="u.userId" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 第二区块:角色劳务设置 -->
|
||||
<el-card shadow="never" class="new-card">
|
||||
<div class="new-card-title">角色劳务设置</div>
|
||||
<div class="hint-text">
|
||||
<p>*角色劳务设置针对该项目所有会议生效</p>
|
||||
<p>*专家劳务明细当会议劳务金额超过角色劳务设置时,该劳务金额为税后金额,则系统进行提示</p>
|
||||
</div>
|
||||
<div v-for="(r, idx) in form.roleRows" :key="idx" class="role-row">
|
||||
<el-select v-model="r.role" placeholder="请选择" style="width:160px" @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>
|
||||
<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-button link type="primary" @click="addRoleRow">增加角色</el-button>
|
||||
<el-button link type="danger" :disabled="form.roleRows.length<=1" @click="removeRoleRowAt(idx)">删除角色</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint-text">*当选择其他时,需手动填写角色名称,显示填写框</p>
|
||||
</el-card>
|
||||
|
||||
<!-- 第三区块:公告文件 -->
|
||||
<el-card shadow="never" class="new-card">
|
||||
<div class="new-card-title">公告文件</div>
|
||||
<p class="hint-text">*勾选公告类型后,上传对应文件;发布公告时可同时发布多个类型</p>
|
||||
<div class="notice-list">
|
||||
<div v-for="(n, idx) in form.notices" :key="idx" class="notice-row">
|
||||
<el-checkbox v-model="n.enabled" :label="n.key" class="notice-check">
|
||||
{{ n.label }}
|
||||
</el-checkbox>
|
||||
<el-upload
|
||||
v-if="n.enabled"
|
||||
class="notice-uploader"
|
||||
:show-file-list="true"
|
||||
:file-list="getFileList(n)"
|
||||
:before-upload="(file) => beforeUpload(file, n)"
|
||||
:http-request="(opts) => httpUpload(opts, n, idx)"
|
||||
:on-remove="(file) => onRemove(n, file)"
|
||||
:on-preview="(file) => onPreview(file)"
|
||||
accept=".pdf,.doc,.docx"
|
||||
drag
|
||||
action="#"
|
||||
>
|
||||
<i class="el-icon-upload"></i>
|
||||
<div class="el-upload__text">
|
||||
将文件拖到此处,或<em>点击上传</em>
|
||||
</div>
|
||||
<div class="el-upload__tip" slot="tip">
|
||||
支持 PDF / Word,单文件 ≤ 20MB
|
||||
</div>
|
||||
</el-upload>
|
||||
<div v-else class="notice-disabled">勾选「{{ n.label }}」后可上传文件</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint-text">*名称规则:公告文件类型+取项目编号的后两段+项目名称</p>
|
||||
</el-card>
|
||||
|
||||
<!-- 底部按钮 (与 sponsor 工具栏风格一致) -->
|
||||
<div 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>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { bizAdd, bizUpdate, bizGet } from '@/api/public'
|
||||
import { listExecutor, listSupporters } from '@/api/system'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { uploadToOss } from '@/utils/oss'
|
||||
import request from '@/utils/request'
|
||||
|
||||
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 uploadingIdx = ref(-1)
|
||||
const currentUploadN = ref(null)
|
||||
const fileInputRef = ref(null)
|
||||
|
||||
function defaultRoleRow() { return { role: '', customName: '', amount: 0 } }
|
||||
function defaultNotice(key, label) { return { key, label, url: '', enabled: false, name: '' } }
|
||||
|
||||
// 支持方候选 (sponsor 角色)
|
||||
const supporterOptions = ref([])
|
||||
|
||||
const form = reactive({
|
||||
projectName: '', projectNo: '', projectForm: '',
|
||||
totalSessions: 0, totalAmount: 0, manageFee: 0,
|
||||
startTime: '', endTime: '',
|
||||
// 赞助公司 (TODO: 下拉源应改为 biz_org,orgType='sponsor', 当前是 sys_user)
|
||||
orgId: null,
|
||||
orgName: '',
|
||||
// 提交截止(天)
|
||||
submitDeadlineDays: 30,
|
||||
roleRows: Array(6).fill(0).map(() => defaultRoleRow()),
|
||||
// 公告附件(每个 notice 含 enabled/type/label/url/name)
|
||||
// enabled: 是否启用该公告类型(对应原型 checkbox 邀请函/支持函/通知/日程)
|
||||
notices: [
|
||||
{ key: 'invitation', label: '邀请函', enabled: false, url: '', name: '' },
|
||||
{ key: 'supportLetter', label: '支持函', enabled: false, url: '', name: '' },
|
||||
{ key: 'notice', label: '通知', enabled: false, url: '', name: '' },
|
||||
{ key: 'schedule', label: '日程', enabled: false, 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('同步数据:从外部数据源拉取最新项目信息 (后端支持时启用)')
|
||||
}
|
||||
|
||||
// 编辑模式:加载已有项目数据并反显到表单
|
||||
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 || ''
|
||||
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.orgId = p.orgId || null
|
||||
form.orgName = p.orgName || ''
|
||||
form.submitDeadlineDays = p.submitDeadlineDays || 30
|
||||
// 公告文件反显: 优先 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 || ''; n.enabled = !!m.url }
|
||||
}
|
||||
} else {
|
||||
// 兼容老数据
|
||||
const noticeMap = {
|
||||
invitation: p.invitationUrl,
|
||||
supportLetter: p.supportLetterUrl,
|
||||
notice: p.noticeUrl,
|
||||
schedule: p.scheduleUrl,
|
||||
publish: p.publishUrl
|
||||
}
|
||||
for (const n of form.notices) {
|
||||
n.url = noticeMap[n.key] || ''
|
||||
n.name = n.url ? (n.url.split('/').pop() || '') : ''
|
||||
n.enabled = !!n.url
|
||||
}
|
||||
}
|
||||
// 加载已有执行方分配 (暂不实现, 留 hook)
|
||||
// await loadAssigns(projectId)
|
||||
} catch (e) {
|
||||
ElMessage.error('加载项目数据失败: ' + (e?.msg || e?.message || e))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function loadSupporters(query = '') {
|
||||
try {
|
||||
const params = { roleType: 'sponsor', pageNum: 1, pageSize: 50 }
|
||||
if (query) params.userName = query
|
||||
const r = await listSupporters(params)
|
||||
supporterOptions.value = r.rows || r.data || []
|
||||
} catch (e) { supporterOptions.value = [] }
|
||||
}
|
||||
|
||||
function onSupporterPick(userId) {
|
||||
const u = supporterOptions.value.find(x => x.userId === userId)
|
||||
if (u) form.orgName = u.userName || ''
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
loadProject()
|
||||
loadSupporters('')
|
||||
})
|
||||
function beforeUpload(file, n) {
|
||||
// 文件大小校验: 限制 20MB
|
||||
const maxSize = 20 * 1024 * 1024
|
||||
if (file.size > maxSize) {
|
||||
ElMessage.error('文件大小不能超过 20MB')
|
||||
return false
|
||||
}
|
||||
// 文件类型校验
|
||||
const accept = ['.pdf', '.doc', '.docx', '.png', '.jpg', '.jpeg']
|
||||
const ext = '.' + file.name.split('.').pop().toLowerCase()
|
||||
if (!accept.includes(ext)) {
|
||||
ElMessage.error('仅支持 PDF/Word/图片格式')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
async function httpUpload(opts, n, idx) {
|
||||
const file = opts.file
|
||||
uploadingIdx.value = idx
|
||||
currentUploadN.value = n
|
||||
const dir = 'ry8080/project/' + n.key + '/'
|
||||
try {
|
||||
const url = await uploadToOss(file, dir)
|
||||
n.url = url
|
||||
n.name = file.name
|
||||
n.enabled = true
|
||||
ElMessage.success(`${n.label} 上传成功`)
|
||||
} catch (err) {
|
||||
ElMessage.error(`上传失败: ${err.message || err}`)
|
||||
n.url = ''
|
||||
n.name = ''
|
||||
} finally {
|
||||
uploadingIdx.value = -1
|
||||
currentUploadN.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// el-upload 内置 file-list 需要从 n.url 转换成 {name, url} 对象
|
||||
function getFileList(n) {
|
||||
if (!n.url) return []
|
||||
// 从 url 截取文件名
|
||||
const url = n.url
|
||||
const name = url.split('/').pop() || `${n.label}.pdf`
|
||||
return [{ name, url }]
|
||||
}
|
||||
|
||||
function onPreview(file) {
|
||||
if (file.url) window.open(file.url, '_blank')
|
||||
}
|
||||
|
||||
function onRemove(n, file) {
|
||||
n.url = ''
|
||||
}
|
||||
function previewFile(n) {
|
||||
if (n.url) window.open(n.url, '_blank')
|
||||
}
|
||||
function downloadFile(n) {
|
||||
if (!n.url) return
|
||||
// 触发浏览器下载
|
||||
const a = document.createElement('a')
|
||||
a.href = n.url
|
||||
a.download = n.name || (n.label + '.pdf')
|
||||
a.target = '_blank'
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
}
|
||||
function removeNotice(idx) {
|
||||
const n = form.notices[idx]
|
||||
if (!n) return
|
||||
ElMessageBox.confirm(`确定删除"${n.label}"附件?`, '提示', { type: 'warning' })
|
||||
.then(() => { n.url = ''; ElMessage.success('已删除') })
|
||||
.catch(() => {})
|
||||
}
|
||||
function goBack() {
|
||||
// 带 _t 强制 Projects.vue watch 触发 reload, 即便路由一样也重载
|
||||
router.push({ path: '/manager/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,
|
||||
orgId: form.orgId,
|
||||
orgName: form.orgName,
|
||||
submitDeadlineDays: form.submitDeadlineDays
|
||||
}
|
||||
// 公告文件以 publicityFiles JSON 数组存储 (替代旧 invitationUrl/supportLetterUrl/noticeUrl/scheduleUrl)
|
||||
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.noticeUrl = n.url
|
||||
else if (n.key === 'schedule') payload.scheduleUrl = n.url
|
||||
}
|
||||
// 发布时设置 is_published='1' + publish_time (同步后端字段)
|
||||
if (mode === 'publish') {
|
||||
payload.isPublished = '1'
|
||||
payload.publishTime = new Date().toISOString().slice(0, 19).replace('T', ' ')
|
||||
// announcement_type 取第一个已上传的文件类型
|
||||
const firstUploaded = uploaded[0]
|
||||
if (firstUploaded) {
|
||||
payload.announcementType = firstUploaded.key
|
||||
}
|
||||
}
|
||||
// 多执行方分配已移至"项目分配"页面单独管理
|
||||
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 roleText = form.roleRows.filter(r => r.role).map(r => `${r.role === '其他' ? (r.customName || '其他') : r.role}=${r.amount}`).join(', ')
|
||||
if (roleText) console.info('[new-project.roleLabor]', form.projectNo, roleText)
|
||||
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>
|
||||
/* 1:1 抄 sponsor/SponsorProjects.vue */
|
||||
.page-card { background: #fff; padding: 16px 20px; border-radius: 6px; border: 1px solid #f0f0f0; }
|
||||
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
|
||||
.breadcrumb .current { color: #262626; font-weight: 500; }
|
||||
|
||||
/* 卡片分块 (与 sponsor 一致: 左边竖线标识 + 标题) */
|
||||
.new-card { margin-bottom: 14px; border: 1px solid #f0f0f0; border-radius: 4px; }
|
||||
.new-card :deep(.el-card__body) { padding: 16px 20px; }
|
||||
.new-card-title {
|
||||
font-size: 14px; font-weight: 600; color: #262626; margin-bottom: 12px;
|
||||
padding-left: 8px; border-left: 3px solid var(--brand-primary, #1890ff); line-height: 1;
|
||||
}
|
||||
|
||||
.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-custom { flex: 1; max-width: 180px; }
|
||||
.role-amount { width: 180px; }
|
||||
.row-actions { display: flex; gap: 8px; }
|
||||
.notice-list { display: flex; flex-direction: column; gap: 12px; }
|
||||
.notice-row { display: flex; align-items: stretch; gap: 16px; margin-bottom: 16px; }
|
||||
.notice-label {
|
||||
width: 100px;
|
||||
display: flex; align-items: center; justify-content: flex-end;
|
||||
color: #606266; font-size: 14px;
|
||||
}
|
||||
.notice-uploader { flex: 1; }
|
||||
.notice-uploader :deep(.el-upload) { width: 100%; }
|
||||
.notice-uploader :deep(.el-upload-dragger) {
|
||||
padding: 12px 16px;
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
}
|
||||
.notice-uploader :deep(.el-upload-dragger .el-upload__text) {
|
||||
font-size: 13px; color: #606266;
|
||||
}
|
||||
.notice-uploader :deep(.el-upload-dragger .el-upload__tip) {
|
||||
font-size: 12px; color: #909399; margin-top: 2px;
|
||||
}
|
||||
.notice-uploader :deep(.el-upload__tip) { margin-top: 0; }
|
||||
.notice-uploader :deep(.el-upload-list) { margin: 0; }
|
||||
.notice-actions { display: flex; flex-direction: column; justify-content: center; gap: 4px; min-width: 70px; }
|
||||
.notice-actions .el-button { padding: 2px 6px; font-size: 12px; }
|
||||
.notice-disabled { flex: 1; color: #c0c4cc; font-size: 13px; padding: 8px 0; }
|
||||
|
||||
/* 底部按钮 (居中, 与 sponsor 风格一致) */
|
||||
.form-actions { display: flex; justify-content: center; gap: 16px; padding: 20px 0 4px; border-top: 1px solid #f0f0f0; margin-top: 8px; }
|
||||
</style>
|
||||
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<div class="manager-support-intent">
|
||||
<div class="page-card">
|
||||
<div class="page-title">支持意向</div>
|
||||
<!-- 筛选区严格按原型 top=169-217 的 left 顺序: 意向项目名称(276) / ZH-2026-658(371) / 姓名(541) / 工作单位(580) / 工作单位(750) / 工作单位(817) / 项目编号(996) / 提示(1113) -->
|
||||
<el-form inline :model="q">
|
||||
<el-form-item label="项目编号:"><el-input v-model="q.projectNo" placeholder="如 ZH-2026-658" clearable /></el-form-item>
|
||||
<el-form-item label="意向项目名称:"><el-input v-model="q.projectName" placeholder="输入意向项目名称" clearable /></el-form-item>
|
||||
<el-form-item label="姓名:"><el-input v-model="q.name" placeholder="输入姓名" clearable /></el-form-item>
|
||||
<el-form-item label="工作单位:"><el-input v-model="q.workUnit" placeholder="输入工作单位" clearable /></el-form-item>
|
||||
<el-form-item>
|
||||
<el-button class="action-btn" @click="load">查找</el-button>
|
||||
<el-button class="action-btn secondary" @click="reset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<!-- 提示语严格按原型 top=217-243 -->
|
||||
<div class="filter-tip">*项目公示中点击支持意向后,显示在此</div>
|
||||
<div class="filter-tip">*支持意向不需要注册,但是如果已经注册账号,则显示账号状态</div>
|
||||
<div class="filter-tip">*根据手机号与系统账号进行匹配</div>
|
||||
</div>
|
||||
|
||||
<div class="page-card">
|
||||
<!-- 列头严格按原型 top=296 left 顺序: 支持意向项目编号 272 / 支持意向项目名称 437 / 姓名 610 / 工作单位名称 685 / 部门 815 / 职务 925 / 手机号 1054 / 账号状态 1164 -->
|
||||
<el-table :data="rows" v-loading="loading" stripe border>
|
||||
<el-table-column type="index" label="#" width="50" />
|
||||
<el-table-column prop="projectNo" label="支持意向项目编号" width="140" align="center" />
|
||||
<el-table-column prop="projectName" label="支持意向项目名称" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="name" label="姓名" width="80" align="center" />
|
||||
<el-table-column prop="workUnit" label="工作单位名称" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="department" label="部门" width="120" align="center" />
|
||||
<el-table-column prop="position" label="职务" width="120" align="center" />
|
||||
<el-table-column prop="phone" label="手机号" width="130" align="center" />
|
||||
<el-table-column label="账号状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.accountStatus==='存在' ? 'success' : 'warning'">{{ row.accountStatus || '不存在' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize" :total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper" @current-change="load" @size-change="load" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { bizList } from '@/api/public'
|
||||
|
||||
const q = ref({ projectNo: '', projectName: '', name: '', workUnit: '' })
|
||||
const rows = ref([])
|
||||
const loading = ref(false)
|
||||
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const { data } = await bizList('supportIntent', { ...q.value, pageNum: page.pageNum, pageSize: page.pageSize })
|
||||
rows.value = data?.rows || []; page.total = data?.total || 0
|
||||
} catch { rows.value = []; page.total = 0 }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
function reset() { q.value = { projectNo:'', projectName:'', name:'', workUnit:'' }; page.pageNum = 1; load() }
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.manager-support-intent { padding: 16px; }
|
||||
.page-card { background: #fff; border-radius: 6px; padding: 16px; border: 1px solid #ebeef5; margin-bottom: 12px; }
|
||||
.page-title { font-size: 20px; font-weight: 600; margin-bottom: 4px; padding-left: 10px; border-left: 3px solid var(--brand-primary); }
|
||||
.filter-tip { color: #909399; font-size: 12px; line-height: 1.8; }
|
||||
.page-toolbar { display: flex; gap: 12px; margin-bottom: 12px; flex-wrap: wrap; align-items: center; }
|
||||
.page-toolbar :deep(.action-btn) {
|
||||
height: 36px; padding: 0 20px; font-size: 14px; border-radius: 4px;
|
||||
background: var(--brand-primary); color: #fff; border: 1px solid var(--brand-primary);
|
||||
display: inline-flex; align-items: center; justify-content: center; white-space: nowrap;
|
||||
transition: opacity .15s; margin-left: 0;
|
||||
}
|
||||
.page-toolbar :deep(.action-btn:hover) { opacity: .85; }
|
||||
.page-toolbar :deep(.action-btn.secondary) {
|
||||
background: #fff; color: var(--brand-primary); border: 1px solid var(--brand-primary);
|
||||
}
|
||||
.page-toolbar :deep(.action-btn.secondary:hover) { background: rgba(30,58,138,.06); }
|
||||
</style>
|
||||
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<div class="manager-workbench">
|
||||
|
||||
<!-- 我的项目策划方案 标题 (原 top=142) -->
|
||||
<h2 class="page-title">我的项目策划方案</h2>
|
||||
|
||||
<!-- 5 个 KPI 卡 (原 top=165 已结算/已结题 + top=458 已执行/未执行) -->
|
||||
<div class="stats-grid">
|
||||
<router-link class="stat-card" to="/manager/projects">
|
||||
<div class="stat-label">项目总数量</div>
|
||||
<div class="stat-value">{{ stats.totalProjects }}</div>
|
||||
</router-link>
|
||||
<router-link class="stat-card" to="/manager/meetings">
|
||||
<div class="stat-label">已结算数量</div>
|
||||
<div class="stat-value">{{ stats.settledProjects }}</div>
|
||||
</router-link>
|
||||
<router-link class="stat-card" to="/manager/projects">
|
||||
<div class="stat-label">已结题数量</div>
|
||||
<div class="stat-value">{{ stats.completedProjects }}</div>
|
||||
</router-link>
|
||||
<router-link class="stat-card" to="/manager/meetings">
|
||||
<div class="stat-label">已执行会议</div>
|
||||
<div class="stat-value">{{ stats.executedMeetings }}</div>
|
||||
</router-link>
|
||||
<router-link class="stat-card" to="/manager/meetings">
|
||||
<div class="stat-label">未执行会议</div>
|
||||
<div class="stat-value">{{ stats.pendingMeetings }}</div>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<p class="hint-text">*点击数字,跳转到相应的列表页</p>
|
||||
|
||||
<!-- 消息通知 (新增) -->
|
||||
<section class="section">
|
||||
<h2 class="section-title">消息通知<a class="more" href="javascript:void(0)">更多 →</a></h2>
|
||||
<ul class="notice-list">
|
||||
<li class="notice-item" v-for="n in notices" :key="n.id" :class="{ read: n.read }">
|
||||
<div class="dot"></div>
|
||||
<div class="body">
|
||||
<div class="title">{{ n.title }}</div>
|
||||
<div class="text">{{ n.text }}</div>
|
||||
</div>
|
||||
<span class="time">{{ n.time }}</span>
|
||||
</li>
|
||||
<li v-if="notices.length === 0" class="empty">暂无消息</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { bizList, listMyMessages } from '@/api/public'
|
||||
|
||||
const stats = ref({
|
||||
totalProjects: 0, settledProjects: 0, completedProjects: 0,
|
||||
executedMeetings: 0, pendingMeetings: 0
|
||||
})
|
||||
const notices = ref([])
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
// 项目总数量
|
||||
const ps = await bizList('project', { pageNum: 1, pageSize: 1 })
|
||||
stats.value.totalProjects = ps.total || ps.data?.total || 0
|
||||
// 已结题项目
|
||||
const cp = await bizList('project', { pageNum: 1, pageSize: 1, isFinished: '1' })
|
||||
stats.value.completedProjects = cp.total || cp.data?.total || 0
|
||||
// 会议总数量
|
||||
const ms = await bizList('meeting', { pageNum: 1, pageSize: 1 })
|
||||
const meetingTotal = ms.total || ms.data?.total || 0
|
||||
// 已执行会议
|
||||
const em = await bizList('meeting', { pageNum: 1, pageSize: 1, status: '已执行' })
|
||||
stats.value.executedMeetings = em.total || em.data?.total || 0
|
||||
stats.value.pendingMeetings = meetingTotal - stats.value.executedMeetings
|
||||
// 已结算项目
|
||||
const sp = await bizList('project', { pageNum: 1, pageSize: 1, isSettled: '1' })
|
||||
stats.value.settledProjects = sp.total || sp.data?.total || 0
|
||||
} catch (e) {
|
||||
console.warn('loadStats failed', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadNotices() {
|
||||
try {
|
||||
const r = await listMyMessages({ limit: 50 }); const list = r.rows
|
||||
notices.value = list.map(n => ({
|
||||
id: n.id,
|
||||
title: n.title,
|
||||
text: n.text,
|
||||
time: n.time,
|
||||
read: n.read
|
||||
}))
|
||||
} catch (e) {
|
||||
console.warn('loadNotices failed', e)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => { loadStats(); loadNotices() })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.manager-workbench { padding: 16px; }
|
||||
.page-title { font-size: 20px; font-weight: 600; color: #1a1a1a; margin-bottom: 16px; padding-left: 10px; border-left: 3px solid var(--brand-primary); }
|
||||
.stats-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 16px; margin-bottom: 12px; }
|
||||
.stat-card {
|
||||
background: #fff; border-radius: 4px; padding: 24px;
|
||||
border: 1px solid #ebeef5; cursor: pointer; text-decoration: none; color: inherit;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
transition: opacity .15s, border-color .15s;
|
||||
min-height: 120px;
|
||||
}
|
||||
.stat-card:hover { border-color: var(--brand-primary); opacity: .9; }
|
||||
.stat-label { font-size: 14px; color: #606266; margin-bottom: 16px; }
|
||||
.stat-value { font-size: 32px; font-weight: 600; color: var(--brand-primary); line-height: 1.2; }
|
||||
.hint-text { font-size: 12px; color: #ff4d4f; margin: 12px 0 20px; text-align: center; }
|
||||
.section { background: #fff; border-radius: 4px; padding: 20px 24px; border: 1px solid #ebeef5; }
|
||||
.section-title { font-size: 15px; font-weight: 600; color: #1a1a1a; margin-bottom: 16px; padding-left: 10px; border-left: 3px solid var(--brand-primary); display: flex; justify-content: space-between; }
|
||||
.more { font-size: 12px; color: var(--brand-primary); text-decoration: none; }
|
||||
.notice-list { list-style: none; border-top: 1px solid #f0f0f0; }
|
||||
.notice-item { padding: 12px 0; border-bottom: 1px solid #f0f0f0; display: flex; align-items: center; gap: 12px; }
|
||||
.notice-item:last-child { border-bottom: none; }
|
||||
.notice-item .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--brand-primary); flex-shrink: 0; }
|
||||
.notice-item.read .dot { background: transparent; border: 1px solid #d9d9d9; }
|
||||
.notice-item .body { flex: 1; min-width: 0; }
|
||||
.notice-item .title { font-size: 13px; color: #1a1a1a; }
|
||||
.notice-item.read .title { color: #8c8c8c; }
|
||||
.notice-item .text { font-size: 12px; color: #595959; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-top: 2px; }
|
||||
.notice-item .time { font-size: 12px; color: #bfbfbf; flex-shrink: 0; }
|
||||
.empty { padding: 24px; text-align: center; color: #909399; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user