feat: 生产部署 + 会议全链路 (执行方分配/费用结算/签到脱敏/H5相机)

- 生产 nginx 三路径: ry-vue3 /hg/, ry-h5 /camera/, API /hg-api, .env 分环境
- 签约链接/摄像头 base-url 走 yml, 不再硬编码 /hg 与 localhost
- 新增 biz_project_executor_assign (镜像 sponsor_assign) 执行方项目级分配
- 会议费用后台汇总 FeeCalcScheduler + 结算回写项目金额 + 状态流转调度
- 签到表拍照高斯模糊脱敏 extra_oss_url, 新增 PosterService/StageDeriver
- 清理 biz_support_intent 旧表 (6 Java/XML 删除)
- ry-h5 相机黑屏修复: 显式 video.play() + 动态 apiBase 上传
- 资源: simhei 字体 / logo.png / qrcode_1.png / favicon.ico

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
郭庆泰
2026-08-24 01:00:09 +08:00
co-authored by Claude
parent 80a276e661
commit 904e28710f
123 changed files with 6008 additions and 1706 deletions
@@ -1,8 +1,12 @@
<template>
<div class="page-card">
<!-- 面包屑 -->
<!-- 面包屑 (按角色显示: sponsor 我的项目; manager/admin 项目管理) -->
<div class="breadcrumb">
首页 / 项目管理 / <span class="current">项目详情</span>
首页 /
<span v-if="userStore?.user?.roleType === 'sponsor'">我的项目</span>
<span v-else-if="userStore?.user?.roleType === 'executor'">项目列表</span>
<span v-else>项目管理</span>
/ <span class="current">项目详情</span>
</div>
<!-- ================= 1. 项目信息 (11 字段, 两列, 只读) ================= -->
@@ -32,11 +36,27 @@
<el-col :span="12"><el-form-item label="支持公司"><span class="info-value">{{ row.sponsorAdminUserName || '-' }}</span></el-form-item></el-col>
<el-col :span="12"><el-form-item label="项目评价"><span class="info-value">{{ fmtScore(row.managerScore) }}</span></el-form-item></el-col>
</el-row>
<el-row :gutter="12">
<el-col :span="24">
<el-form-item label="服务公司">
<el-table :data="assigns" border size="small" class="assign-table-el">
<el-table-column type="index" label="#" width="48" align="center" />
<el-table-column prop="orgName" label="名称" min-width="160" show-overflow-tooltip />
<el-table-column prop="sessions" label="场次" width="90" align="right" />
<el-table-column label="金额" width="130" align="right">
<template #default="{ row }">¥ {{ fmtMoney(row.amount) }}</template>
</el-table-column>
<template #empty>
<span style="color:#c0c4cc">暂无分配 ( 项目管理 / 项目分配 添加)</span>
</template>
</el-table>
</el-form-item>
</el-col>
</el-row>
</el-form>
<!-- ================= 2. 角色劳务设置 (只读) ================= -->
<div class="new-card-title">角色劳务设置</div>
<p class="hint-text">*角色劳务设置针对该项目所有会议生效</p>
<table class="info-table role-labor-table">
<colgroup>
<col style="width:120px">
@@ -59,7 +79,6 @@
<!-- ================= 3. 支持单位监督员 (只读) ================= -->
<div class="new-card-title">支持单位监督员</div>
<p class="hint-text">*增加只显示已点支持意向的</p>
<div class="monitor-list">
<div v-for="(m, idx) in monitors" :key="idx" class="monitor-row">
<span class="monitor-num">{{ idx + 1 }}.</span>
@@ -68,6 +87,8 @@
<div v-if="!monitors.length" class="empty-hint">暂无监督员</div>
</div>
<!-- (已分配执行方 已折叠进上方"项目信息"section, 此处不再重复) -->
<!-- ================= 4. 已发布公告 (只读, 仅查看) ================= -->
<div class="new-card-title">已发布公告</div>
<el-table v-loading="loading" :data="announcements" border>
@@ -90,16 +111,23 @@
import { ref, reactive, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { bizGet, bizList } from '@/api/public'
import { listExecutorOrgs } from '@/api/system'
import request from '@/utils/request'
import Preview from '@/components/Preview.vue'
import { useUserStore } from '@/store/user'
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
// ===================== 状态 =====================
const projectId = ref('')
const row = ref({})
const loading = ref(false)
// 已分配执行方 (服务公司) — 数据源: GET /business/project/{id}/assigns + listExecutorOrgs 补 org_name
const assigns = ref([])
// 角色劳务 (从 biz_project.role_labor JSON 字段读; 空数组 → 模板 "暂无角色" 行兜底)
const form = reactive({
roleRows: []
@@ -149,11 +177,7 @@ async function loadProject() {
const rl = JSON.parse(row.value.roleLabor || '[]')
if (Array.isArray(rl) && rl.length) form.roleRows = rl
} catch (e) { /* keep empty */ }
// 监督员 (JSON 字段)
try {
const mn = JSON.parse(row.value.monitors || '[]')
if (Array.isArray(mn)) monitors.value = mn
} catch (e) { /* keep empty */ }
// 监督员 (走 /sponsorAssigns 端点, 不在 loadProject 里)
} catch (e) {
console.error('[project-detail] load failed', e)
row.value = {}
@@ -173,8 +197,68 @@ async function loadAnnouncements() {
}
}
// ===================== 已分配执行方 (服务公司) =====================
/**
* 数据源同 manager/projects/edit/{id} 的 loadAssigns:
* 1) GET /business/project/{id}/assigns 拿原始分配行 (assignId, execUserId, sessions, amount)
* 2) 对每个 execUserId 调 listExecutorOrgs({ userId }) 补 org_name
* (后端 SQL 已限定 parent_user_id IS NULL, 自动取 MAIN 账号对应的执行公司)
* N+1 但项目分配数一般 < 10, 简单可控; 真要优化再加聚合 JOIN 接口
*/
async function loadAssigns() {
try {
const resp = await request({ url: `/business/project/${projectId.value}/assigns`, method: 'get' })
const list = (resp && resp.data) || []
if (!Array.isArray(list) || !list.length) { assigns.value = []; return }
// 并发查每个执行方公司名
const enriched = await Promise.all(list.map(async (a) => {
let orgName = '-'
try {
const opts = await listExecutorOrgs({ userId: a.execUserId })
const org = (opts && opts.data && opts.data[0]) || null
if (org && org.orgName) orgName = org.orgName
} catch (e) { /* 单条失败不影响其它, 默认显示 '-' */ }
return {
assignId: a.assignId,
execUserId: a.execUserId,
orgName,
sessions: a.sessions || 0,
amount: a.amount || 0
}
}))
assigns.value = enriched
} catch (e) {
console.error('[project-detail] assigns load failed', e)
assigns.value = []
}
}
// ===================== 监督员 (sponsor 视角的监察员) =====================
// 数据源: GET /business/project/{id}/sponsorAssigns (返回 [{monitorUserId, monitorUserName, ...}])
// 注: 不再读 biz_project.monitors — 该字段在 biz_project 表不存在 (历史 bug)
async function loadMonitors() {
if (!projectId.value) return
try {
const resp = await request({ url: `/business/project/${projectId.value}/sponsorAssigns`, method: 'get' })
const list = (resp && resp.data) || []
monitors.value = (Array.isArray(list) ? list : []).map(x => ({
userId: x.monitorUserId,
userName: x.monitorUserName || ('(userId=' + x.monitorUserId + ')')
}))
} catch (e) {
console.error('[project-detail] monitors load failed', e)
monitors.value = []
}
}
// ===================== 导航 =====================
function goBack() { router.push('/manager/projects') }
// 按角色回退: sponsor → 我的项目, manager → 项目管理, admin → 项目管理
function goBack() {
const role = userStore?.role || userStore?.user?.roleType
if (role === 'sponsor') router.push('/sponsor/my-projects')
else if (role === 'executor') router.push('/executor/projects')
else router.push('/manager/projects')
}
function goEdit() { router.push(`/manager/projects/edit/${projectId.value}`) }
// ===================== 预览 =====================
@@ -188,6 +272,7 @@ function openPreview(url, title) {
// ===================== 初始化 =====================
onMounted(async () => {
await loadProject()
await Promise.all([loadAssigns(), loadMonitors()])
await loadAnnouncements()
})
</script>
@@ -235,6 +320,9 @@ onMounted(async () => {
.role-labor-table td { border-right: 1px solid #ebeef5; }
.role-labor-table th:last-child,
.role-labor-table td:last-child { border-right: none; }
/* 服务公司表格 (el-table in form): 紧凑, 不要撑满整张卡片 */
.assign-table-el { width: auto; max-width: 560px; }
.assign-table-el :deep(.el-table__empty-block) { min-height: 48px; }
.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; }
.empty-row { text-align: center; color: #c0c4cc; }