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:
@@ -8,95 +8,82 @@
|
||||
<div class="page-card sponsor-home">
|
||||
<div class="breadcrumb">首页</div>
|
||||
|
||||
<!-- KPI: 5 个数字面板, 1 行展示 -->
|
||||
<!-- KPI: 5 个数字面板, 1 行展示 (跳转 + 自动应用筛选, 字段名跟 Projects.vue / Meetings.vue q 一致) -->
|
||||
<div class="stats-grid stats-grid-5">
|
||||
<router-link class="stat-card" to="/sponsor/projects">
|
||||
<div class="stat-label">项目总数量</div>
|
||||
<div class="stat-value">{{ stats.totalProjects }}</div>
|
||||
<div class="stat-desc">查看全部项目</div>
|
||||
</router-link>
|
||||
<router-link class="stat-card" to="/sponsor/meetings?isSettled=Y">
|
||||
<div class="stat-label">已结算的会议</div>
|
||||
<div class="stat-value">{{ stats.settledMeetings }}</div>
|
||||
<div class="stat-desc">查看已结算会议</div>
|
||||
<router-link class="stat-card" to="/sponsor/projects?isSettled=Y">
|
||||
<div class="stat-label">已结算的项目</div>
|
||||
<div class="stat-value">{{ stats.settledProjects }}</div>
|
||||
<div class="stat-desc">查看已结算项目</div>
|
||||
</router-link>
|
||||
<router-link class="stat-card" to="/sponsor/projects?isFinished=1">
|
||||
<div class="stat-label">已结题的项目</div>
|
||||
<div class="stat-value">{{ stats.completedProjects }}</div>
|
||||
<div class="stat-desc">查看已结题项目</div>
|
||||
</router-link>
|
||||
<router-link class="stat-card" to="/sponsor/meetings?currentStage=已执行">
|
||||
<router-link class="stat-card" to="/sponsor/meetings?currentStage=RUNNING">
|
||||
<div class="stat-label">已执行会议</div>
|
||||
<div class="stat-value">{{ stats.executedMeetings }}</div>
|
||||
<div class="stat-desc">查看已执行会议</div>
|
||||
</router-link>
|
||||
<router-link class="stat-card" to="/sponsor/meetings?currentStage=未执行">
|
||||
<router-link class="stat-card" to="/sponsor/meetings?currentStage=NOT_STARTED">
|
||||
<div class="stat-label">未执行会议</div>
|
||||
<div class="stat-value">{{ stats.pendingMeetings }}</div>
|
||||
<div class="stat-desc">查看未执行会议</div>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- 消息通知 -->
|
||||
<!-- 消息通知 (NoticeList 组件内部已含 SSE 实时刷新 + 详情 dialog + 全部已读) -->
|
||||
<section class="section">
|
||||
<h2 class="section-title">消息通知<router-link class="more" to="/sponsor/messages">更多 →</router-link></h2>
|
||||
<ul class="notice-list">
|
||||
<li class="notice-item" v-for="n in notices" :key="n.title" :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>
|
||||
<NoticeList :limit="50" />
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { bizList, listMyMessages } from '@/api/public'
|
||||
import { bizList } from '@/api/public'
|
||||
import NoticeList from '@/components/NoticeList.vue'
|
||||
|
||||
// 字段名跟 Projects.vue / Meetings.vue q 一致, 之前 3/5 卡 stats 不更新或用错字段名 (status 后端不认, settledMeetings 误指项目级 isSettled)
|
||||
const stats = ref({
|
||||
totalProjects: 0,
|
||||
settledMeetings: 0,
|
||||
completedProjects: 0,
|
||||
executedMeetings: 0,
|
||||
pendingMeetings: 0
|
||||
settledProjects: 0, // 项目级 isSettled=Y
|
||||
completedProjects: 0, // 项目级 isFinished=1
|
||||
executedMeetings: 0, // 会议 currentStage=RUNNING (字段名跟 Meetings.vue q 一致, 之前误用 status= 后端不认, 数字一直是错的)
|
||||
pendingMeetings: 0 // 总会议 - 已执行
|
||||
})
|
||||
const notices = ref([])
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
// 项目总数量 (跟 manager/executor 一样调普通 list, 后续若需要按 sponsor 隔离再换 sponsorList 接口)
|
||||
const ps = await bizList('project', { pageNum: 1, pageSize: 1 })
|
||||
stats.value.totalProjects = ps.total || 0
|
||||
stats.value.totalProjects = ps.total || ps.data?.total || 0
|
||||
// 已结算项目
|
||||
const sp = await bizList('project', { pageNum: 1, pageSize: 1, isSettled: 'Y' })
|
||||
stats.value.settledProjects = sp.total || sp.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 })
|
||||
stats.value.executedMeetings = ms.total || 0
|
||||
const meetingTotal = ms.total || ms.data?.total || 0
|
||||
// 已执行会议 = currentStage='RUNNING' (阶段已过开始时间, 执行方未提交)
|
||||
const em = await bizList('meeting', { pageNum: 1, pageSize: 1, currentStage: 'RUNNING' })
|
||||
stats.value.executedMeetings = em.total || em.data?.total || 0
|
||||
stats.value.pendingMeetings = Math.max(0, meetingTotal - stats.value.executedMeetings)
|
||||
} 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>
|
||||
|
||||
@@ -146,25 +133,8 @@ onMounted(() => {
|
||||
line-height: 1;
|
||||
display: flex; justify-content: space-between;
|
||||
}
|
||||
.more { font-size: 12px; color: var(--brand-primary); text-decoration: none; }
|
||||
.more { font-size: 12px; color: var(--brand-primary); text-decoration: none; cursor: pointer; }
|
||||
.more:hover { text-decoration: underline; }
|
||||
.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; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.stats-grid-3 { grid-template-columns: repeat(2, 1fr); }
|
||||
|
||||
Reference in New Issue
Block a user