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,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>
|
||||
Reference in New Issue
Block a user