Binary file not shown.
|
After Width: | Height: | Size: 305 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 114 KiB |
@@ -1,7 +1,7 @@
|
||||
/* =============================================================
|
||||
品牌主题色(Brand Theme)— 低调绿色主调
|
||||
------------------------------------------------------------
|
||||
主色:#15803D 低饱和深绿 (政务/卫健委稳重型, 不刺眼)
|
||||
主色:#42a288 低饱和绿 (政务/卫健委稳重型, 不刺眼)
|
||||
次色:#0E7490 青蓝 (保留原品牌青蓝, 用作次要强调/链接)
|
||||
配色逻辑:
|
||||
- 主色绿 = 品牌色 (按钮/标题/侧栏高亮/KPI)
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
:root {
|
||||
/* —— 主色:低调深绿 (政务/卫健委风格) —— */
|
||||
--brand-primary: #15803D;
|
||||
--brand-primary: #42a288;
|
||||
--brand-primary-deep: #0F5F2E; /* hover / active */
|
||||
--brand-primary-darker: #073D1D; /* 深底 (登录页 banner / 关键装饰) */
|
||||
--brand-primary-text: #16A34A; /* 链接/文字绿 (中等可读) */
|
||||
|
||||
@@ -41,6 +41,19 @@
|
||||
<li v-if="!rows.length" class="empty">{{ emptyText }}</li>
|
||||
</ul>
|
||||
|
||||
<!-- 分页 (pageable=true 时显示; 嵌入式小列表 pageable=false 隐藏) -->
|
||||
<div v-if="pageable && total > 0" class="notice-pager">
|
||||
<el-pagination
|
||||
v-model:current-page="page.pageNum"
|
||||
v-model:page-size="page.pageSize"
|
||||
:total="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="detail.title || '消息详情'"
|
||||
@@ -70,7 +83,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { ref, reactive, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import QRCode from 'qrcode'
|
||||
import { listMyMessages, markMessageRead, markAllMessagesRead } from '@/api/public'
|
||||
@@ -80,10 +93,18 @@ const props = defineProps({
|
||||
limit: { type: Number, default: 5 },
|
||||
showCategory: { type: Boolean, default: false },
|
||||
showHeader: { type: Boolean, default: true },
|
||||
emptyText: { type: String, default: '暂无通知' }
|
||||
emptyText: { type: String, default: '暂无通知' },
|
||||
/**
|
||||
* 是否启用分页
|
||||
* - false (默认): 嵌入式小列表, 拉 props.limit 条, 不显示 el-pagination
|
||||
* - true: 独立页, 显示 el-pagination, 按 page.pageSize 拉, total 读后端真实值
|
||||
*/
|
||||
pageable: { type: Boolean, default: false }
|
||||
})
|
||||
|
||||
const rows = ref([])
|
||||
const total = ref(0)
|
||||
const page = reactive({ pageNum: 1, pageSize: 20 })
|
||||
const detailOpen = ref(false)
|
||||
const detail = ref({})
|
||||
const detailLink = ref('')
|
||||
@@ -94,18 +115,29 @@ let unsubscribeNewMessage = null
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const r = await listMyMessages({ limit: props.limit })
|
||||
// 分页模式: 传 pageNum/pageSize (后端返回真实 total); 非分页模式: 传 limit
|
||||
const params = props.pageable
|
||||
? { pageNum: page.pageNum, pageSize: page.pageSize }
|
||||
: { limit: props.limit }
|
||||
const r = await listMyMessages(params)
|
||||
rows.value = r.rows || []
|
||||
total.value = r.total || 0
|
||||
} catch (e) {
|
||||
rows.value = []
|
||||
total.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
/** SSE 触发的静默重拉 (失败不打扰用户, 下次手动刷新可见) */
|
||||
/** SSE 触发的静默重拉 (失败不打扰用户, 下次手动刷新可见)
|
||||
* 分页模式: 重拉当前页; 非分页模式: 仍按 limit 拉 */
|
||||
async function refresh() {
|
||||
try {
|
||||
const r = await listMyMessages({ limit: props.limit })
|
||||
const params = props.pageable
|
||||
? { pageNum: page.pageNum, pageSize: page.pageSize }
|
||||
: { limit: props.limit }
|
||||
const r = await listMyMessages(params)
|
||||
rows.value = r.rows || []
|
||||
total.value = r.total || 0
|
||||
} catch (e) { /* swallow */ }
|
||||
}
|
||||
|
||||
@@ -260,6 +292,13 @@ onBeforeUnmount(() => {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* 分页 (独立页用, 嵌入式不显示) */
|
||||
.notice-pager {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
/* 详情 dialog */
|
||||
.detail-body .meta {
|
||||
display: flex;
|
||||
|
||||
@@ -64,7 +64,12 @@ function goPublicity() { router.push('/publicity') }
|
||||
|
||||
<style scoped>
|
||||
.container { max-width: 1354px; margin: 0 auto; padding: 0 60px; }
|
||||
.footer { background: var(--brand-primary-darker); color: rgba(255, 255, 255, 0.65); padding: 48px 0 0; }
|
||||
.footer {
|
||||
/* 保持 brand-primary 色调, 叠一层 18% 黑色遮罩让整体变暗, 不改色相 */
|
||||
background: linear-gradient(rgba(0, 0, 0, 0.18), rgba(0, 0, 0, 0.18)), var(--brand-primary);
|
||||
color: #fff;
|
||||
padding: 48px 0 0;
|
||||
}
|
||||
.footer-main { display: grid; grid-template-columns: 1.4fr 1fr 1fr 1.2fr auto; gap: 48px; padding-bottom: 36px; }
|
||||
.footer-brand .brand-row { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
|
||||
.footer-logo-icon {
|
||||
@@ -74,8 +79,8 @@ function goPublicity() { router.push('/publicity') }
|
||||
}
|
||||
.footer-logo-icon img { width: 100%; height: 100%; object-fit: contain; display: block; }
|
||||
.footer-brand-name { font-size: 16px; font-weight: 600; color: #fff; letter-spacing: 1px; }
|
||||
.footer-brand-en { font-size: 10px; color: rgba(255, 255, 255, 0.4); letter-spacing: 0.5px; margin-top: 2px; }
|
||||
.footer-desc { font-size: 13px; line-height: 1.9; color: rgba(255, 255, 255, 0.55); }
|
||||
.footer-brand-en { font-size: 10px; color: #fff; letter-spacing: 0.5px; margin-top: 2px; }
|
||||
.footer-desc { font-size: 13px; line-height: 1.9; color: #fff; }
|
||||
.footer-col h4 {
|
||||
font-size: 14px; font-weight: 600; color: #fff;
|
||||
letter-spacing: 1px; margin-bottom: 16px; padding-bottom: 10px;
|
||||
@@ -86,7 +91,7 @@ function goPublicity() { router.push('/publicity') }
|
||||
width: 24px; height: 2px; background: #93c5fd;
|
||||
}
|
||||
.footer-col a, .footer-col p {
|
||||
display: block; font-size: 13px; color: rgba(255, 255, 255, 0.6);
|
||||
display: block; font-size: 13px; color: #fff;
|
||||
line-height: 2.1; transition: color 0.2s;
|
||||
}
|
||||
.footer-col a { cursor: pointer; }
|
||||
@@ -98,12 +103,12 @@ function goPublicity() { router.push('/publicity') }
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
color: #1f2937;
|
||||
}
|
||||
.qr-label { font-size: 12px; color: rgba(255, 255, 255, 0.5); margin-top: 10px; }
|
||||
.qr-label { font-size: 12px; color: #fff; margin-top: 10px; }
|
||||
.footer-bottom {
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.12);
|
||||
padding: 18px 0;
|
||||
display: flex; justify-content: space-between;
|
||||
font-size: 12px; color: rgba(255, 255, 255, 0.4);
|
||||
font-size: 12px; color: #fff;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<!-- 公开门户共享顶部导航 (首页 / 项目公示 / 公示详情 共用)
|
||||
active 态按当前 route 推导, 无需各页面各自传参 -->
|
||||
<header class="top-nav" :class="topNavClass">
|
||||
<header class="top-nav" :class="[topNavClass, themeClass, isHomeMobileClass]">
|
||||
<a class="logo" title="返回首页" @click.prevent="goHome">
|
||||
<div class="logo-icon"><img src="/logo.png" alt="logo" /></div>
|
||||
<div class="logo-text">
|
||||
@@ -95,17 +95,30 @@ const router = useRouter()
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 主题: '' = 默认首页透明/滚动变白; 'solid-brand' = brand-primary 实色 + 白字 (与 PortalShell 一致)
|
||||
const props = defineProps({
|
||||
theme: { type: String, default: '' }
|
||||
})
|
||||
|
||||
const isScrolled = ref(false)
|
||||
const drawerOpen = ref(false)
|
||||
|
||||
const topNavClass = computed(() => isScrolled.value ? 'is-scrolled' : '')
|
||||
const loggedIn = computed(() => !!userStore.token)
|
||||
const userName = computed(() => userStore.user?.userName || '用户')
|
||||
|
||||
// active 态: 按当前路由推导 (首页=年度项目规划, /publicity* = 项目公示)
|
||||
const isHome = computed(() => route.path === '/')
|
||||
const isPublicity = computed(() => route.path === '/publicity' || route.path.startsWith('/publicity/'))
|
||||
|
||||
// 顶栏风格: theme=solid-brand 时强制走品牌色 (与 PortalShell 一致, 不跟随滚动/路由变白)
|
||||
const themeClass = computed(() => props.theme === 'solid-brand' ? 'is-solid-brand' : '')
|
||||
// mobile 下首页标记: 用于媒体查询里选择性隐藏 logo (避免与 banner.jpg 文字重叠)
|
||||
const isHomeMobileClass = computed(() => isHome.value ? 'is-home-mobile' : '')
|
||||
const topNavClass = computed(() => {
|
||||
if (props.theme === 'solid-brand') return ''
|
||||
if (!isHome.value) return 'is-solid'
|
||||
return isScrolled.value ? 'is-solid' : 'is-transparent'
|
||||
})
|
||||
const loggedIn = computed(() => !!userStore.token)
|
||||
const userName = computed(() => userStore.user?.userName || '用户')
|
||||
|
||||
function handleScroll() { isScrolled.value = window.scrollY > 10 }
|
||||
onMounted(() => window.addEventListener('scroll', handleScroll, { passive: true }))
|
||||
onBeforeUnmount(() => window.removeEventListener('scroll', handleScroll))
|
||||
@@ -120,7 +133,7 @@ async function onUserCmd(cmd) {
|
||||
if (cmd === 'logout') return goLogout()
|
||||
if (cmd === 'account') {
|
||||
const r = (userStore.user?.role || '')
|
||||
const map = { admin: '/admin/workbench', manager: '/manager/workbench', doctor: '/doctor/home', executor: '/executor/meetings', sponsor: '/sponsor/home' }
|
||||
const map = { admin: '/admin/workbench', manager: '/manager/workbench', doctor: '/doctor/home', executor: '/executor/overview', sponsor: '/sponsor/home' }
|
||||
router.push(map[r] || '/admin/workbench')
|
||||
}
|
||||
}
|
||||
@@ -139,23 +152,67 @@ a { color: inherit; text-decoration: none; }
|
||||
|
||||
/* ========== 顶部导航 ========== */
|
||||
.top-nav {
|
||||
position: sticky;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
height: 72px;
|
||||
padding: 0 60px;
|
||||
background: var(--brand-primary);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
align-items: center;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
|
||||
transition: box-shadow 0.3s;
|
||||
transition: background 0.3s, box-shadow 0.3s, border-color 0.3s, color 0.3s;
|
||||
}
|
||||
|
||||
.top-nav.is-scrolled {
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
|
||||
/* 透明态: 首页置顶, 文字白, 背景透明与 banner 融合 */
|
||||
.top-nav.is-transparent {
|
||||
background: transparent;
|
||||
border-bottom: none;
|
||||
box-shadow: none;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 白色态: 滚动后 / 非首页 */
|
||||
.top-nav.is-solid {
|
||||
background: #fff;
|
||||
border-bottom: none;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
||||
color: #1f2937;
|
||||
}
|
||||
/* 一刀切: 白底态下所有未显式覆盖的子元素都用深灰, 避免继承 is-transparent 的 #fff */
|
||||
.top-nav.is-solid,
|
||||
.top-nav.is-solid * {
|
||||
color: #1f2937;
|
||||
}
|
||||
.top-nav.is-solid .nav-item:hover .nav-link,
|
||||
.top-nav.is-solid .nav-link.active {
|
||||
color: var(--brand-primary);
|
||||
}
|
||||
|
||||
/* 品牌实色态: 公示页/公示详情专用, 与 PortalShell 一致 (brand-primary 实色 + 白字) */
|
||||
.top-nav.is-solid-brand {
|
||||
background: var(--brand-primary);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
|
||||
color: #fff;
|
||||
}
|
||||
.top-nav.is-solid-brand,
|
||||
.top-nav.is-solid-brand * {
|
||||
color: #fff;
|
||||
}
|
||||
.top-nav.is-solid-brand .nav-link { color: rgba(255, 255, 255, 0.85); }
|
||||
.top-nav.is-solid-brand .nav-item:hover .nav-link,
|
||||
.top-nav.is-solid-brand .nav-link.active { color: #fff; }
|
||||
.top-nav.is-solid-brand .nav-link::after { background: #fff; }
|
||||
.top-nav.is-solid-brand .logo-title { color: #fff; }
|
||||
.top-nav.is-solid-brand .logo-subtitle { color: rgba(255, 255, 255, 0.6); }
|
||||
.top-nav.is-solid-brand .hamburger { color: #fff; }
|
||||
.top-nav.is-solid-brand .hamburger:hover { background: rgba(255, 255, 255, 0.12); }
|
||||
.top-nav.is-solid-brand .hamburger:active { background: rgba(255, 255, 255, 0.2); }
|
||||
.top-nav.is-solid-brand .user-link { color: rgba(255, 255, 255, 0.85); }
|
||||
.top-nav.is-solid-brand .user-link:hover { background: rgba(255, 255, 255, 0.1); color: #fff; }
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -183,19 +240,25 @@ a { color: inherit; text-decoration: none; }
|
||||
.logo-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
letter-spacing: 0.5px;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
.top-nav.is-transparent .logo-title { color: #fff; }
|
||||
.top-nav.is-solid .logo-title { color: #1f2937; }
|
||||
|
||||
.logo-subtitle {
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin-top: 2px;
|
||||
letter-spacing: 0.3px;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
.top-nav.is-transparent .logo-subtitle { color: rgba(255, 255, 255, 0.6); }
|
||||
.top-nav.is-solid .logo-subtitle { color: #6b7280; }
|
||||
|
||||
.nav-list {
|
||||
flex: 1;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -213,7 +276,6 @@ a { color: inherit; text-decoration: none; }
|
||||
.nav-link {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
cursor: pointer;
|
||||
transition: color 0.25s;
|
||||
position: relative;
|
||||
@@ -221,6 +283,8 @@ a { color: inherit; text-decoration: none; }
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.top-nav.is-transparent .nav-link { color: rgba(255, 255, 255, 0.85); }
|
||||
.top-nav.is-solid .nav-link { color: #4b5563; }
|
||||
|
||||
.nav-link::after {
|
||||
content: '';
|
||||
@@ -229,15 +293,20 @@ a { color: inherit; text-decoration: none; }
|
||||
bottom: 0;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background: #fff;
|
||||
background: currentColor;
|
||||
transform: translateX(-50%);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-item:hover .nav-link,
|
||||
.nav-link.active {
|
||||
color: #fff;
|
||||
color: currentColor;
|
||||
}
|
||||
.top-nav.is-transparent .nav-item:hover .nav-link,
|
||||
.top-nav.is-transparent .nav-link.active { color: #fff; }
|
||||
.top-nav.is-solid .nav-item:hover .nav-link,
|
||||
.top-nav.is-solid .nav-link.active { color: var(--brand-primary); }
|
||||
.top-nav.is-solid .nav-link::after { background: var(--brand-primary); }
|
||||
|
||||
.nav-item:hover .nav-link::after,
|
||||
.nav-link.active::after {
|
||||
@@ -249,38 +318,34 @@ a { color: inherit; text-decoration: none; }
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
padding: 7px 20px;
|
||||
background: #fff;
|
||||
color: var(--brand-primary);
|
||||
font-size: 13px;
|
||||
padding: 6px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
letter-spacing: 1px;
|
||||
background: none;
|
||||
border: none;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.login-btn:hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
.login-btn:hover { opacity: 0.7; }
|
||||
|
||||
.user-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
.user-link:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
}
|
||||
.top-nav.is-transparent .user-link { color: rgba(255, 255, 255, 0.85); }
|
||||
.top-nav.is-transparent .user-link:hover { background: rgba(255, 255, 255, 0.1); color: #fff; }
|
||||
.top-nav.is-solid .user-link { color: #4b5563; }
|
||||
.top-nav.is-solid .user-link:hover { background: #f3f4f6; color: var(--brand-primary); }
|
||||
|
||||
/* ========== 汉堡按钮 (桌面隐藏, 手机显示) ========== */
|
||||
.hamburger {
|
||||
@@ -289,15 +354,18 @@ a { color: inherit; text-decoration: none; }
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #fff;
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
margin-left: auto;
|
||||
grid-column: 3;
|
||||
justify-self: end;
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s;
|
||||
transition: background 0.2s, color 0.3s;
|
||||
color: currentColor;
|
||||
}
|
||||
.hamburger:hover { background: rgba(255, 255, 255, 0.12); }
|
||||
.hamburger:active { background: rgba(255, 255, 255, 0.2); }
|
||||
.top-nav.is-transparent .hamburger:hover { background: rgba(255, 255, 255, 0.12); }
|
||||
.top-nav.is-transparent .hamburger:active { background: rgba(255, 255, 255, 0.2); }
|
||||
.top-nav.is-solid .hamburger:hover { background: #f3f4f6; }
|
||||
.top-nav.is-solid .hamburger:active { background: #e5e7eb; }
|
||||
|
||||
/* ========== 抽屉 (手机端) ========== */
|
||||
.drawer-mask {
|
||||
@@ -400,8 +468,10 @@ a { color: inherit; text-decoration: none; }
|
||||
@media (max-width: 768px) {
|
||||
/* 顶栏: 高度收窄 + padding 减小 */
|
||||
.top-nav { height: 56px !important; padding: 0 16px !important; }
|
||||
.logo-title { font-size: 14px !important; }
|
||||
.logo-subtitle { display: none; }
|
||||
/* mobile: 只有首页隐藏 logo (与 banner.jpg 文字重叠); 公示/公示详情保留 logo */
|
||||
.is-home-mobile .logo { display: none !important; }
|
||||
/* 首页滚动后 navbar 变白底, 此时显示 logo (白色 navbar 上有 logo 图标+标题更明确) */
|
||||
.is-home-mobile.is-solid .logo { display: flex !important; }
|
||||
/* 桌面 nav-list + 顶部 tools 隐藏, 改用汉堡 */
|
||||
.nav-list { display: none !important; }
|
||||
.top-tools { display: none !important; }
|
||||
|
||||
@@ -80,7 +80,7 @@ async function onUserCmd(cmd) {
|
||||
if (cmd === 'logout') return goLogout()
|
||||
if (cmd === 'account') {
|
||||
const r = (userStore.user?.role || '')
|
||||
const map = { admin: '/admin/workbench', manager: '/manager/workbench', doctor: '/doctor/home', executor: '/executor/meetings', sponsor: '/sponsor/home' }
|
||||
const map = { admin: '/admin/workbench', manager: '/manager/workbench', doctor: '/doctor/home', executor: '/executor/overview', sponsor: '/sponsor/home' }
|
||||
router.push(map[r] || '/')
|
||||
}
|
||||
}
|
||||
@@ -98,6 +98,8 @@ onBeforeUnmount(() => window.removeEventListener('scroll', handleScroll))
|
||||
background: #f5f6f8;
|
||||
min-width: 1354px;
|
||||
line-height: 1.6;
|
||||
/* PortalLayout 的 padding-top: 72px 对注册页是冗余的 (PortalShell 自带 sticky 顶栏占位), 上移抵消 (与 Login.vue 同处理) */
|
||||
margin-top: -72px;
|
||||
}
|
||||
a { color: inherit; text-decoration: none; }
|
||||
|
||||
@@ -112,7 +114,7 @@ a { color: inherit; text-decoration: none; }
|
||||
transition: box-shadow 0.3s;
|
||||
}
|
||||
.top-nav.is-scrolled { box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25); }
|
||||
.logo { display: flex; align-items: center; gap: 12px; }
|
||||
.logo { display: flex; align-items: center; gap: 12px; cursor: pointer; }
|
||||
.logo-icon {
|
||||
width: 36px; height: 36px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
@@ -124,7 +126,10 @@ a { color: inherit; text-decoration: none; }
|
||||
.logo-subtitle { font-size: 11px; color: rgba(255, 255, 255, 0.6); margin-top: 2px; letter-spacing: 0.3px; }
|
||||
|
||||
.nav-list {
|
||||
flex: 1; display: flex; align-items: center; justify-content: center;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
gap: 36px; list-style: none;
|
||||
}
|
||||
.nav-item { position: relative; height: 72px; display: flex; align-items: center; }
|
||||
@@ -141,13 +146,19 @@ a { color: inherit; text-decoration: none; }
|
||||
.nav-item:hover .nav-link, .nav-link.active { color: #fff; }
|
||||
.nav-item:hover .nav-link::after, .nav-link.active::after { width: 100%; }
|
||||
|
||||
.top-tools { display: flex; align-items: center; gap: 16px; }
|
||||
.top-tools { display: flex; align-items: center; gap: 16px; margin-left: auto; }
|
||||
.login-btn {
|
||||
padding: 7px 20px; background: #fff; color: var(--brand-primary);
|
||||
font-size: 13px; font-weight: 500;
|
||||
cursor: pointer; transition: background 0.2s; letter-spacing: 1px;
|
||||
padding: 6px 0;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.login-btn:hover { background: #f3f4f6; }
|
||||
.login-btn:hover { opacity: 0.7; }
|
||||
.user-link {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
padding: 6px 10px;
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="account" @click="$router.push('/' + (role || 'admin') + '/account')">账号信息</el-dropdown-item>
|
||||
<el-dropdown-item command="home" @click="goMyHome">我的主页</el-dropdown-item>
|
||||
<el-dropdown-item command="logout" divided>退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
@@ -145,6 +145,18 @@ function goNotice() {
|
||||
router.push(p)
|
||||
}
|
||||
|
||||
/** 头像下拉「我的主页」: 跳到各角色首页 (工作台/首页) */
|
||||
const HOME_PATH = {
|
||||
admin: '/admin/workbench',
|
||||
manager: '/manager/workbench',
|
||||
doctor: '/doctor/home',
|
||||
executor: '/executor/overview',
|
||||
sponsor: '/sponsor/home'
|
||||
}
|
||||
function goMyHome() {
|
||||
router.push(HOME_PATH[role.value] || '/')
|
||||
}
|
||||
|
||||
/** doctor 角色: 拉 biz_expert.auditStatus 同步到 store, 控制侧栏 menu + Home pannel */
|
||||
async function loadExpertAuditStatus() {
|
||||
if (store.role !== 'doctor') return
|
||||
@@ -245,6 +257,7 @@ const MENU = {
|
||||
{ path: '/doctor/account', title: '账号信息', icon: User }
|
||||
],
|
||||
executor: [
|
||||
{ path: '/executor/overview', title: '首页', icon: House },
|
||||
{ path: '/executor/submissions', title: '我的项目策划方案', icon: EditPen },
|
||||
{ path: '/executor/projects', title: '项目列表', icon: Document },
|
||||
{ path: '/executor/meetings', title: '会议列表', icon: Calendar },
|
||||
@@ -254,7 +267,6 @@ const MENU = {
|
||||
],
|
||||
sponsor: [
|
||||
{ path: '/sponsor/home', title: '首页', icon: House },
|
||||
{ path: '/sponsor/submissions', title: '我的项目策划方案', icon: EditPen },
|
||||
{ path: '/sponsor/my-projects', title: '我的项目', icon: Document },
|
||||
{ path: '/sponsor/meetings', title: '会议列表', icon: Calendar },
|
||||
{ path: '/sponsor/people', title: '人员管理', icon: User, requireMain: true },
|
||||
|
||||
@@ -9,5 +9,9 @@
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.portal-layout { min-height: 100vh; background: #fff; }
|
||||
.portal-layout { min-height: 100vh; background: #fff; padding-top: 72px; }
|
||||
@media (max-width: 768px) {
|
||||
/* mobile 下 navbar 收窄到 56px, padding-top 同步收窄, 避免 hero 与 navbar 之间出现 16px 空白 */
|
||||
.portal-layout { padding-top: 56px; }
|
||||
}
|
||||
</style>
|
||||
@@ -26,6 +26,7 @@ const routes = [
|
||||
{ path: 'users', name: 'admin-users', component: () => import('@/views/admin/Users.vue'), meta: { title: '用户管理' } },
|
||||
{ path: 'roles', name: 'admin-roles', component: () => import('@/views/admin/Roles.vue'), meta: { title: '角色管理' } },
|
||||
{ path: 'projects', name: 'admin-projects', component: () => import('@/views/manager/Projects.vue'), meta: { title: '项目管理' } },
|
||||
{ path: 'projects/detail/:projectId', name: 'admin-projects-detail', component: () => import('@/views/manager/ManagerProjectDetail.vue'), meta: { title: '项目详情' } },
|
||||
{ path: 'meetings', name: 'admin-meetings', component: () => import('@/views/meetings/Meetings.vue'), meta: { title: '会议管理' } },
|
||||
{ path: 'meetings/detail/:meetingId', name: 'admin-meetings-detail', component: () => import('@/views/meetings/MeetingDetail.vue'), meta: { title: '会议详情' } },
|
||||
{ path: 'meetings/view/:meetingId', name: 'admin-meetings-view', component: () => import('@/views/meetings/MeetingDetail.vue'), meta: { title: '会议详情', readonly: true } },
|
||||
@@ -49,6 +50,7 @@ const routes = [
|
||||
{ path: 'article', name: 'admin-article', component: () => import('@/views/admin/BizArticleAdmin.vue'), meta: { title: '协议管理' } },
|
||||
{ path: 'article/edit/:id', name: 'admin-article-edit', component: () => import('@/views/admin/BizArticleEdit.vue'), meta: { title: '编辑文章' } },
|
||||
{ path: 'special-plan', name: 'admin-special-plan', component: () => import('@/views/admin/BizSpecialPlanAdmin.vue'), meta: { title: '专项计划管理' } },
|
||||
{ path: 'special-plan/new', name: 'admin-special-plan-new', component: () => import('@/views/admin/BizSpecialPlanEdit.vue'), meta: { title: '新建专项计划' } },
|
||||
{ path: 'special-plan/edit/:id', name: 'admin-special-plan-edit', component: () => import('@/views/admin/BizSpecialPlanEdit.vue'), meta: { title: '编辑专项计划' } },
|
||||
{ path: 'project-category', name: 'admin-project-category', component: () => import('@/views/admin/ProjectCategory.vue'), meta: { title: '项目类别管理' } },
|
||||
{ path: 'labor-protocol', name: 'admin-labor-protocol', component: () => import('@/views/admin/LaborProtocol.vue'), meta: { title: '劳务协议配置' } },
|
||||
@@ -109,7 +111,7 @@ const routes = [
|
||||
]
|
||||
},
|
||||
{ path: '/executor', component: AdminLayout, meta: { role: 'executor' }, children: [
|
||||
{ path: '', redirect: { name: 'executor-submissions' } },
|
||||
{ path: '', redirect: { name: 'executor-overview' } },
|
||||
{ path: 'overview', name: 'executor-overview', component: () => import('@/views/executor/Overview.vue'), meta: { title: '首页' } },
|
||||
{ path: 'submissions', name: 'executor-submissions', component: () => import('@/views/doctor/Submissions.vue'), meta: { title: '我的项目策划方案' } },
|
||||
{ path: 'submission/new', name: 'executor-submission-new', component: () => import('@/views/doctor/SubmissionNew.vue'), meta: { title: '新建项目策划方案' } },
|
||||
@@ -159,6 +161,15 @@ const router = createRouter({
|
||||
routes
|
||||
})
|
||||
|
||||
// 各角色首页映射 (role_type → 首页, 与 Login.vue roleHome 保持一致)
|
||||
const ROLE_HOME = {
|
||||
admin: '/admin/workbench',
|
||||
manager: '/manager/workbench',
|
||||
doctor: '/doctor/home',
|
||||
executor: '/executor/overview',
|
||||
sponsor: '/sponsor/home'
|
||||
}
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
document.title = (to.meta?.title || 'BAHIM') + ' - 合规系统'
|
||||
// 公开路由 (无 role meta) 不拦截
|
||||
@@ -168,7 +179,11 @@ router.beforeEach((to, from, next) => {
|
||||
// 扫码带 token 直登 (签劳务): 放行, 由页面 onMounted 用 token 完成登录
|
||||
if (!user && to.query?.token) return next()
|
||||
if (!user) return next({ name: 'login', query: { redirect: to.fullPath } })
|
||||
// 已登录: role 不匹配由后端 401 拦截, 不在前端强跳 (避免误判让用户卡死)
|
||||
// 已登录: 前端校验角色匹配 (role_type 单一可信源), 不匹配跳回自己角色首页
|
||||
const userRole = user.role
|
||||
if (userRole && to.meta.role !== userRole) {
|
||||
return next(ROLE_HOME[userRole] || '/')
|
||||
}
|
||||
next()
|
||||
})
|
||||
|
||||
|
||||
@@ -76,13 +76,14 @@ function chooseState(role, labor, service) {
|
||||
}
|
||||
|
||||
/** 单轨措辞 (代表轨状态 + 角色 → 展示名). */
|
||||
function render(role, s, executed) {
|
||||
function render(role, s, phase) {
|
||||
switch (s) {
|
||||
case 0: // R 退回
|
||||
return role === 'executor' ? '已退回' : '待整改'
|
||||
case 1: // N 未提交
|
||||
if (!executed) return '未执行'
|
||||
return role === 'executor' ? '执行中' : '已执行未传材料'
|
||||
case 1: // N 未提交 (时间驱动三态: 未执行 → 执行中 → 已执行, 所有角色统一)
|
||||
if (phase === 0) return '未执行'
|
||||
if (phase === 1) return '执行中'
|
||||
return '已执行'
|
||||
case 2: // C0 合规审中
|
||||
if (role === 'sponsor') return '已执行未传材料' // 只读
|
||||
return '待审核'
|
||||
@@ -94,6 +95,21 @@ function render(role, s, executed) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行进度三态: 0=未执行 (now < startTime), 1=执行中 (startTime ≤ now < endTime),
|
||||
* 2=已执行 (now ≥ endTime 或 isExecuted=1). 仅用于材料未提交 (N) 的展示措辞.
|
||||
* isExecuted=1 是 scheduler 在 end_time 到点落库的「已执行」事实, 优先采信.
|
||||
*/
|
||||
function executionPhase(row) {
|
||||
if (isTrue(row.isExecuted)) return 2
|
||||
const now = Date.now()
|
||||
const end = row.endTime ? new Date(row.endTime).getTime() : NaN
|
||||
const start = row.startTime ? new Date(row.startTime).getTime() : NaN
|
||||
if (!Number.isNaN(end) && now >= end) return 2
|
||||
if (!Number.isNaN(start) && now >= start) return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 各角色展示阶段名 (镜像后端 StageDeriver.deriveDisplay).
|
||||
* role ∈ {executor, sponsor, manager, admin, doctor, expert}; 非流程角色回退 admin 中性.
|
||||
@@ -104,7 +120,7 @@ export function deriveStage(role, row) {
|
||||
if (isTrue(row.isFinished)) return '已完结'
|
||||
if (isTrue(row.isSettled)) return '已结算'
|
||||
const chosen = chooseState(role, laborState(row), serviceState(row))
|
||||
return render(role, chosen, isTrue(row.isExecuted))
|
||||
return render(role, chosen, executionPhase(row))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,7 +134,7 @@ export function stageLabel(role, row) {
|
||||
* 展示阶段名 → 颜色映射 (class + el-tag type), 与 render() 措辞一一对应.
|
||||
* 颜色跟随「各角色看到的展示阶段」而非物理阶段, 避免文案与颜色错位
|
||||
* (如 sponsor 看「待审核」却因物理阶段 RECTIFYING 显示红色).
|
||||
* 规则: 待整改/已退回=红, 未执行=灰, 执行中/已执行未传材料=蓝, 待审核=橙, 通过/待结算/完结=绿.
|
||||
* 规则: 待整改/已退回=红, 未执行=灰, 执行中/已执行/已执行未传材料=蓝, 待审核=橙, 通过/待结算/完结=绿.
|
||||
*/
|
||||
const STAGE_STYLE = {
|
||||
'冻结中': { cls: 'frozen', tag: 'info' },
|
||||
@@ -128,6 +144,7 @@ const STAGE_STYLE = {
|
||||
'已退回': { cls: 'waiting', tag: 'danger' },
|
||||
'未执行': { cls: 'pending', tag: 'info' },
|
||||
'执行中': { cls: 'running', tag: 'primary' },
|
||||
'已执行': { cls: 'running', tag: 'primary' },
|
||||
'已执行未传材料': { cls: 'running', tag: 'primary' },
|
||||
'待审核': { cls: 'reviewing', tag: 'warning' },
|
||||
'审核通过': { cls: 'done', tag: 'success' },
|
||||
@@ -154,12 +171,12 @@ export function stageTag(role, row) {
|
||||
*/
|
||||
export const STAGE_OPTIONS = [
|
||||
{ label: '未执行', value: 'NOT_STARTED' },
|
||||
{ label: '执行中', value: 'RUNNING' },
|
||||
{ label: '执行中', value: 'IN_PROGRESS' },
|
||||
{ label: '已执行', value: 'RUNNING' },
|
||||
{ label: '待合规审核', value: 'AWAITING_COMPLIANCE' },
|
||||
{ label: '待支持方审核', value: 'AWAITING_SUPERVISION' },
|
||||
{ label: '待整改', value: 'RECTIFYING' },
|
||||
{ label: '待结算', value: 'AWAITING_SETTLEMENT' },
|
||||
{ label: '已结算', value: 'SETTLED' },
|
||||
{ label: '已完结', value: 'FINISHED' },
|
||||
{ label: '冻结中', value: 'FROZEN' },
|
||||
]
|
||||
|
||||
@@ -25,6 +25,11 @@
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 工具栏 -->
|
||||
<div class="toolbar">
|
||||
<el-button type="primary" @click="$router.push('/admin/special-plan/new')">新增</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 列表 -->
|
||||
<el-table :data="rows" border stripe v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
@@ -39,10 +44,11 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="updateTime" label="更新时间" width="170" />
|
||||
<el-table-column label="操作" width="140" fixed="right">
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="{ row }"><div class="table-actions">
|
||||
<el-link :underline="false" size="small" type="primary" @click="$router.push('/admin/special-plan/edit/' + row.id)">编辑</el-link>
|
||||
<el-link :underline="false" size="small" type="primary" :disabled="row.status !== '0'" @click="preview(row)">预览</el-link>
|
||||
<el-link :underline="false" size="small" type="danger" @click="remove(row)">删除</el-link>
|
||||
</div></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -64,6 +70,7 @@
|
||||
<script setup>
|
||||
import { reactive, ref, onMounted } from 'vue'
|
||||
import request from '@/utils/request'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const TYPE_LABEL = { rich: '富文本', file: '上传文件' }
|
||||
|
||||
@@ -91,12 +98,26 @@ function preview(row) {
|
||||
window.open(`${import.meta.env.BASE_URL}#/special-plan/${row.id}`, '_blank')
|
||||
}
|
||||
|
||||
async function remove(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除「${row.title}」?`, '删除确认', { type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' })
|
||||
} catch { return }
|
||||
try {
|
||||
await request({ url: `/business/specialPlan/${row.id}`, method: 'delete' })
|
||||
ElMessage.success('已删除')
|
||||
load()
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.msg || '删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-plan { padding: 16px; }
|
||||
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
|
||||
.toolbar { margin-bottom: 12px; }
|
||||
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
|
||||
|
||||
|
||||
|
||||
@@ -332,7 +332,9 @@ async function loadOrgOptions() {
|
||||
method: 'get',
|
||||
params: { orgType: createForm.roleType, pageSize: 500 }
|
||||
})
|
||||
orgOptions.value = (r.data && r.data.rows) || r.rows || []
|
||||
const list = (r.data && r.data.rows) || r.rows || []
|
||||
// 子账号须挂到"已有主账号"的单位下: 过滤掉无主账号(userId=null)的单位, 避免 parent_user_id 落空成孤儿
|
||||
orgOptions.value = list.filter(o => o.userId != null)
|
||||
} catch (e) { /* GET 错误拦截器已统一 toast, 这里静默 */ }
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<div class="stat-value">{{ stats.totalProjects }}</div>
|
||||
<div class="stat-extra">查看详情 →</div>
|
||||
</router-link>
|
||||
<router-link class="stat-card" to="/admin/projects">
|
||||
<router-link class="stat-card" to="/admin/projects?isFinished=1">
|
||||
<div class="stat-bar"></div>
|
||||
<div class="stat-label">已结题项目</div>
|
||||
<div class="stat-value">{{ stats.finishedProjects }}</div>
|
||||
@@ -38,19 +38,8 @@
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- 各角色用户数分布 + 快捷入口, 双栏布局 -->
|
||||
<!-- 快捷入口 -->
|
||||
<div class="content-grid">
|
||||
<section class="content-card">
|
||||
<h2 class="section-title">各角色用户数</h2>
|
||||
<div class="role-grid">
|
||||
<div v-for="r in roleStats" :key="r.code" class="role-card">
|
||||
<div class="role-name">{{ r.label }}</div>
|
||||
<div class="role-count">{{ r.count }}</div>
|
||||
<div class="role-bar" :style="{ width: r.pct + '%' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-card">
|
||||
<h2 class="section-title">快捷入口</h2>
|
||||
<div class="quick-grid">
|
||||
@@ -88,25 +77,26 @@
|
||||
</router-link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 消息通知 (NoticeList 组件内部已含 SSE 实时刷新 + 详情 dialog + 全部已读 + 分页) -->
|
||||
<section class="content-card message-card">
|
||||
<h2 class="section-title">消息通知<a class="more" @click.prevent="$router.push('/admin/messages')">更多 →</a></h2>
|
||||
<NoticeList :pageable="true" :show-header="false" />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { listUser, listByRole } from '@/api/system'
|
||||
import { listUser } from '@/api/system'
|
||||
import { bizList } from '@/api/public'
|
||||
import NoticeList from '@/components/NoticeList.vue'
|
||||
import { User, Document, Calendar, UserFilled, Star, OfficeBuilding, Files, Bell } from '@element-plus/icons-vue'
|
||||
|
||||
const stats = ref({
|
||||
totalUsers: 0, roleCount: 0, totalProjects: 0, finishedProjects: 0, totalMeetings: 0
|
||||
})
|
||||
const roleStats = ref([])
|
||||
|
||||
const ROLE_LABEL = {
|
||||
admin: '后台管理员', manager: '合规人员',
|
||||
doctor: '评审专家', executor: '执行人', sponsor: '支持方'
|
||||
}
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
@@ -114,21 +104,8 @@ async function load() {
|
||||
const u = await listUser({ pageNum: 1, pageSize: 1 })
|
||||
stats.value.totalUsers = (u.data && u.data.total) || 0
|
||||
|
||||
// 各角色用户数
|
||||
const all = []
|
||||
const codes = Object.keys(ROLE_LABEL)
|
||||
for (const code of codes) {
|
||||
try {
|
||||
const r = await listByRole({ pageNum: 1, pageSize: 1, roleType: code })
|
||||
all.push({ code, label: ROLE_LABEL[code], count: (r.data && r.data.total) || 0 })
|
||||
} catch (e) {
|
||||
all.push({ code, label: ROLE_LABEL[code], count: 0 })
|
||||
}
|
||||
}
|
||||
// 计算占比 (用于进度条)
|
||||
const max = Math.max(1, ...all.map(r => r.count))
|
||||
roleStats.value = all.map(r => ({ ...r, pct: Math.round((r.count / max) * 100) }))
|
||||
stats.value.roleCount = codes.length
|
||||
// 角色类型数 = 业务角色 4 类 (不含 admin)
|
||||
stats.value.roleCount = 4
|
||||
|
||||
// 项目数
|
||||
const ps = await bizList('project', { pageNum: 1, pageSize: 1 })
|
||||
@@ -204,11 +181,9 @@ onMounted(load)
|
||||
background: var(--brand-slate-50);
|
||||
}
|
||||
|
||||
/* —— 双栏内容区 —— */
|
||||
/* —— 内容区 (单卡片, 快捷入口独占) —— */
|
||||
.content-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 16px;
|
||||
display: block;
|
||||
}
|
||||
.content-card {
|
||||
background: #FFFFFF;
|
||||
@@ -228,43 +203,16 @@ onMounted(load)
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* —— 角色分布: 6 列网格 —— */
|
||||
.role-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
.role-card {
|
||||
background: var(--brand-slate-50);
|
||||
border: 1px solid var(--brand-slate-200);
|
||||
border-radius: 4px;
|
||||
padding: 14px 16px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.role-name {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.role-count {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
line-height: 1.1;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.role-bar {
|
||||
height: 4px;
|
||||
background: var(--brand-primary);
|
||||
border-radius: 2px;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
/* —— 消息通知卡片 —— */
|
||||
.message-card { margin-top: 16px; }
|
||||
.message-card .section-title { display: flex; justify-content: space-between; align-items: center; }
|
||||
.message-card .more { font-size: 12px; color: var(--brand-primary); text-decoration: none; cursor: pointer; font-weight: 400; }
|
||||
.message-card .more:hover { text-decoration: underline; }
|
||||
|
||||
/* —— 快捷入口: 4 列网格, 图标 + 文字 —— */
|
||||
.quick-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
.quick-item {
|
||||
@@ -293,7 +241,6 @@ onMounted(load)
|
||||
/* —— 响应式: 5 卡变 3/2/1 列 —— */
|
||||
@media (max-width: 1200px) {
|
||||
.stats-grid { grid-template-columns: repeat(3, 1fr); }
|
||||
.content-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
/* 容器内边距收窄 (不影响 admin-layout 的 main padding) */
|
||||
@@ -315,21 +262,12 @@ onMounted(load)
|
||||
.stat-value { font-size: 22px !important; padding-bottom: 10px; }
|
||||
.stat-extra { font-size: 11px !important; padding: 8px 12px; }
|
||||
|
||||
/* 双栏内容: 单列堆叠 */
|
||||
.content-grid { grid-template-columns: 1fr; gap: 12px; }
|
||||
|
||||
/* 内容卡: padding 收窄 */
|
||||
.content-card { padding: 14px 16px !important; border-radius: 4px !important; }
|
||||
.section-title { font-size: 14px !important; margin-bottom: 12px !important; }
|
||||
|
||||
/* 角色卡: 2 列 */
|
||||
.role-grid { grid-template-columns: repeat(2, 1fr); gap: 10px; }
|
||||
.role-card { padding: 10px 12px !important; }
|
||||
.role-name { font-size: 11px !important; margin-bottom: 4px; }
|
||||
.role-count { font-size: 18px !important; margin-bottom: 6px; }
|
||||
|
||||
/* 快捷入口: 1 列紧凑 (8 项太多, 1 列更易点) */
|
||||
.quick-grid { grid-template-columns: 1fr; gap: 6px; }
|
||||
/* 快捷入口: 2 列紧凑 */
|
||||
.quick-grid { grid-template-columns: repeat(2, 1fr); gap: 6px; }
|
||||
.quick-item { padding: 10px 12px !important; font-size: 13px; }
|
||||
.quick-icon { font-size: 16px !important; }
|
||||
}
|
||||
@@ -337,7 +275,6 @@ onMounted(load)
|
||||
/* —— 超窄屏 (≤480px): KPI 单列 —— */
|
||||
@media (max-width: 480px) {
|
||||
.stats-grid { grid-template-columns: 1fr; }
|
||||
.role-grid { grid-template-columns: 1fr; }
|
||||
.stat-value { font-size: 20px !important; }
|
||||
.content-card { padding: 12px 14px !important; }
|
||||
}
|
||||
|
||||
@@ -15,16 +15,7 @@
|
||||
<main class="main-content">
|
||||
<!-- 左侧 banner -->
|
||||
<div class="banner-section">
|
||||
<div class="banner-inner">
|
||||
<span class="banner-tag">2025-2030</span>
|
||||
<h2 class="banner-title">协同创新 共建共享<br>整合医学发展新格局</h2>
|
||||
<div class="banner-divider"></div>
|
||||
<p class="banner-desc">
|
||||
聚焦医学整合创新,系统推进七大专项计划,<br>
|
||||
全面构建覆盖诊疗、科研、人才、管理、公益、<br>
|
||||
政学协作与组织建设的协同发展体系。
|
||||
</p>
|
||||
</div>
|
||||
<div class="banner-inner" :style="{ backgroundImage: `url(${loginBg})` }"></div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧登录卡片 -->
|
||||
@@ -175,6 +166,7 @@ import { reactive, ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import request from '@/utils/request'
|
||||
import loginBg from '@/assets/login.jpg'
|
||||
import { login, getInfo, getCaptcha, sendLoginSms, smsLogin } from '@/api/auth'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { useAsyncLock } from '@/utils/useAsyncLock'
|
||||
@@ -279,13 +271,13 @@ async function afterLogin(token, displayName) {
|
||||
ElMessage.success(`欢迎,${displayName}`)
|
||||
// 角色不在角色首页映射里 → 拒绝
|
||||
if (!roleHome[role]) return router.replace({ name: 'login' })
|
||||
// 所有角色统一跳门户首页 '/' (业务方 2026-08-22 要求, 各自角色菜单从导航栏进入)
|
||||
// 带 redirect 回跳 (401/守卫带过来的原页面), 但必须属于当前角色 (否则跳过去被踢回 login)
|
||||
const redirect = route.query.redirect
|
||||
if (redirect && redirectBelongsToRole(String(redirect), role)) {
|
||||
return router.replace(String(redirect))
|
||||
}
|
||||
router.replace('/')
|
||||
// 登录默认跳各自角色工作台首页 (不再跳门户首页 '/')
|
||||
router.replace(roleHome[role])
|
||||
}
|
||||
|
||||
function switchMode(mode) {
|
||||
@@ -351,7 +343,7 @@ const roleHome = {
|
||||
admin: '/admin/workbench',
|
||||
manager: '/manager/workbench',
|
||||
doctor: '/doctor/home',
|
||||
executor: '/executor/submissions',
|
||||
executor: '/executor/overview',
|
||||
sponsor: '/sponsor/home',
|
||||
}
|
||||
|
||||
@@ -489,16 +481,25 @@ onUnmounted(() => {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body { margin: 0; padding: 0; }
|
||||
/* PortalLayout 的 padding-top: 72px 对登录页是冗余的 (登录页有自己的 header 占位), 上移抵消 */
|
||||
.login-page { margin-top: -72px; }
|
||||
|
||||
/* mobile 下 PortalLayout padding-top 收窄到 56px, 这里同步收窄, 让 login header 完整露出不被裁剪 */
|
||||
@media (max-width: 768px) {
|
||||
.login-page { margin-top: -56px; }
|
||||
}
|
||||
|
||||
.login-page{
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
color: #333;
|
||||
background: #f5f6f8;
|
||||
background: linear-gradient(135deg, #f5f7fb 0%, #eef2f7 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 顶部导航 */
|
||||
/* 顶部导航: 背景延展到视口左右边缘, 内容保留 60px 内边距 */
|
||||
.login-page .header{
|
||||
height: 64px;
|
||||
padding: 0 60px;
|
||||
@@ -506,6 +507,7 @@ onUnmounted(() => {
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login-page .logo{
|
||||
@@ -555,93 +557,65 @@ onUnmounted(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 60px;
|
||||
gap: 60px;
|
||||
padding: 60px 80px;
|
||||
gap: 80px;
|
||||
}
|
||||
|
||||
.login-page .banner-section{
|
||||
flex: 1;
|
||||
max-width: 560px;
|
||||
max-width: 640px;
|
||||
}
|
||||
|
||||
.login-page .banner-inner{
|
||||
background: var(--brand-primary);
|
||||
color: #fff;
|
||||
padding: 56px 56px;
|
||||
height: 440px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login-page .banner-tag{
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
font-size: 12px;
|
||||
letter-spacing: 2px;
|
||||
margin-bottom: 24px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.login-page .banner-title{
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 20px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.login-page .banner-divider{
|
||||
width: 48px;
|
||||
height: 2px;
|
||||
background: #fff;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.login-page .banner-desc{
|
||||
font-size: 14px;
|
||||
line-height: 1.9;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
letter-spacing: 0.5px;
|
||||
position: relative;
|
||||
background-color: var(--brand-primary);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
height: 520px;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.18);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 登录卡片 */
|
||||
.login-page .login-section{
|
||||
width: 400px;
|
||||
width: 440px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.login-page .login-card{
|
||||
background: #ffffff;
|
||||
padding: 40px 40px;
|
||||
padding: 48px 44px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.login-page .login-title{
|
||||
font-size: 22px;
|
||||
font-size: 26px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 10px;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.login-page .login-subtitle{
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
margin-bottom: 28px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
/* 登录模式切换 tabs */
|
||||
.login-page .login-tabs{
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
margin-bottom: 24px;
|
||||
gap: 28px;
|
||||
margin-bottom: 28px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
.login-page .login-tabs .tab{
|
||||
padding: 8px 0;
|
||||
font-size: 14px;
|
||||
padding: 10px 0;
|
||||
font-size: 15px;
|
||||
color: #6b7280;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
@@ -657,24 +631,26 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.login-page .form-group{
|
||||
margin-bottom: 18px;
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.login-page .form-input{
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
padding: 0 14px 0 42px;
|
||||
border: 1px solid #d1d5db;
|
||||
height: 50px;
|
||||
padding: 0 16px 0 46px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
font-size: 14px;
|
||||
font-size: 15px;
|
||||
color: #1f2937;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.login-page .form-input:focus{
|
||||
border-color: var(--brand-primary);
|
||||
box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.login-page .form-input::placeholder{
|
||||
@@ -683,14 +659,16 @@ onUnmounted(() => {
|
||||
|
||||
.login-page .input-icon{
|
||||
position: absolute;
|
||||
left: 14px;
|
||||
left: 16px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: #9ca3af;
|
||||
pointer-events: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.login-page .form-group:focus-within .input-icon{ color: var(--brand-primary); }
|
||||
|
||||
.login-page .captcha-group{
|
||||
display: flex;
|
||||
@@ -711,14 +689,15 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.login-page .captcha-image{
|
||||
width: 110px;
|
||||
height: 44px;
|
||||
border: 1px solid #d1d5db;
|
||||
width: 120px;
|
||||
height: 50px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
background: #f3f4f6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 4px;
|
||||
color: var(--brand-primary);
|
||||
@@ -730,8 +709,8 @@ onUnmounted(() => {
|
||||
.login-page .form-bottom-links{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 14px;
|
||||
font-size: 13px;
|
||||
margin-top: 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.login-page .form-bottom-links .bottom-link{
|
||||
@@ -745,20 +724,28 @@ onUnmounted(() => {
|
||||
|
||||
.login-page .login-btn{
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
height: 52px;
|
||||
background: var(--brand-primary);
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 4px;
|
||||
cursor: pointer;
|
||||
margin-top: 8px;
|
||||
transition: background 0.2s;
|
||||
margin-top: 12px;
|
||||
border-radius: 8px;
|
||||
transition: background 0.2s, transform 0.2s, box-shadow 0.2s;
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.login-page .login-btn:hover{
|
||||
background: var(--brand-primary-deep);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.16);
|
||||
}
|
||||
|
||||
.login-page .login-btn:active{
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* 底部 */
|
||||
@@ -792,7 +779,9 @@ onUnmounted(() => {
|
||||
padding: 32px 36px;
|
||||
width: 440px;
|
||||
max-width: 90vw;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e5e7eb;
|
||||
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.login-page .modal-title{
|
||||
@@ -880,12 +869,12 @@ onUnmounted(() => {
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.main-content {
|
||||
flex-direction: column;
|
||||
padding: 30px 30px;
|
||||
padding: 40px 40px;
|
||||
gap: 30px;
|
||||
}
|
||||
.banner-section { width: 100%; max-width: 100%; }
|
||||
.banner-inner { height: auto; padding: 40px 30px; }
|
||||
.login-section { width: 100%; max-width: 400px; }
|
||||
.banner-inner { height: 360px; }
|
||||
.login-section { width: 100%; max-width: 440px; }
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
@@ -895,9 +884,8 @@ onUnmounted(() => {
|
||||
.footer { padding: 16px 20px; }
|
||||
}
|
||||
|
||||
/* === 用户要求: 左侧保持 440px 不动, 右侧登录卡片高度追平左侧, 两栏在 main-content 中居中 === */
|
||||
/* === 用户要求: 两栏在 main-content 中居中, 卡片高度自适应 === */
|
||||
.login-page .main-content { align-items: center; }
|
||||
.login-page .login-card { height: 440px; display: flex; flex-direction: column; justify-content: center; }
|
||||
|
||||
/* === 注册选择类别弹窗 (原型 1:1 抄 /home/john/ry8080/proto/html/登录.html) === */
|
||||
.modal-overlay {
|
||||
@@ -916,6 +904,7 @@ onUnmounted(() => {
|
||||
width: 440px;
|
||||
max-width: 90vw;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
.modal-title { font-size: 18px; font-weight: 600; color: #1a1a1a; margin-bottom: 20px; text-align: center; }
|
||||
@@ -943,6 +932,7 @@ onUnmounted(() => {
|
||||
flex: 1; height: 40px;
|
||||
border: none; cursor: pointer;
|
||||
font-size: 14px; font-weight: 500;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.modal-btn.secondary { background: #f5f7fa; color: #606266; }
|
||||
@@ -968,12 +958,13 @@ onUnmounted(() => {
|
||||
}
|
||||
.sms-btn {
|
||||
flex-shrink: 0;
|
||||
width: 110px;
|
||||
height: 44px;
|
||||
border: 1px solid #d1d5db;
|
||||
width: 120px;
|
||||
height: 50px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
background: #f9fafb;
|
||||
color: var(--brand-primary);
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
@@ -992,6 +983,6 @@ onUnmounted(() => {
|
||||
.login-page .main-content { flex-direction: column !important; padding: 20px !important; gap: 0 !important; }
|
||||
.login-page .banner-section { display: none !important; }
|
||||
.login-page .login-section { width: 100% !important; max-width: 100% !important; padding: 0 !important; flex: 0 0 auto !important; }
|
||||
.login-page .login-card { width: calc(100% - 10px) !important; max-width: none !important; margin: 0 auto !important; height: auto !important; min-height: 0 !important; padding: 24px 20px !important; }
|
||||
.login-page .login-card { width: calc(100% - 10px) !important; max-width: none !important; margin: 0 auto !important; height: auto !important; min-height: 0 !important; padding: 24px 20px !important; box-shadow: none !important; border: none !important; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -37,6 +37,10 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="联系人姓名" prop="realName">
|
||||
<el-input v-model="form.realName" placeholder="请输入联系人姓名" maxlength="30" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="手机号码" prop="phone">
|
||||
<el-input v-model="form.phone" placeholder="请输入手机号码" maxlength="11" />
|
||||
</el-form-item>
|
||||
@@ -110,6 +114,7 @@ const agreed = ref(false)
|
||||
const form = reactive({
|
||||
username: '',
|
||||
orgId: null,
|
||||
realName: '',
|
||||
phone: '',
|
||||
smsCode: '',
|
||||
password: '',
|
||||
@@ -123,6 +128,7 @@ const rules = {
|
||||
{ pattern: /^[A-Za-z0-9_]+$/, message: '只能包含字母/数字/下划线', trigger: 'blur' }
|
||||
],
|
||||
orgId: [{ required: true, message: '请选择企业', trigger: 'change' }],
|
||||
realName: [{ required: true, message: '请输入联系人姓名', trigger: 'blur' }],
|
||||
phone: [
|
||||
{ required: true, message: '请输入手机号码', trigger: 'blur' },
|
||||
{ pattern: /^1\d{10}$/, message: '手机号格式错误', trigger: 'blur' }
|
||||
|
||||
@@ -244,7 +244,7 @@ async function loadUserProfile() {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
form.name = store.user?.userName || ''
|
||||
form.name = store.user?.nickName || ''
|
||||
form.phone = store.user?.phonenumber || ''
|
||||
await Promise.all([loadUserProfile(), loadExpertProfile()])
|
||||
snapshot = ref(JSON.parse(JSON.stringify(form)))
|
||||
|
||||
@@ -14,44 +14,27 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 待参加的会议 + 待签署的协议: 仅审核通过的医生可见 -->
|
||||
<div class="cols-row" v-if="store.expertAuditApproved">
|
||||
<div class="section">
|
||||
<h2 class="section-title">
|
||||
待参加的会议
|
||||
<a class="more" @click.prevent="$router.push('/doctor/meetings')">更多 →</a>
|
||||
</h2>
|
||||
<ul class="simple-list">
|
||||
<li class="simple-item" v-for="m in upcomingMeetings" :key="m.meetingId" @click="$router.push('/doctor/meetings')">
|
||||
<div class="item-main">
|
||||
<span class="item-title">{{ m.meetingName || m.title }}</span>
|
||||
</div>
|
||||
<span class="item-status">{{ formatTime(m.startTime) }}</span>
|
||||
</li>
|
||||
<li v-if="!upcomingMeetings.length" class="empty">暂无待参加会议</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h2 class="section-title">
|
||||
待签署的协议
|
||||
<a class="more" @click.prevent="$router.push('/doctor/submissions')">更多 →</a>
|
||||
</h2>
|
||||
<ul class="simple-list">
|
||||
<li
|
||||
class="simple-item"
|
||||
:class="{ disabled: s.isEsigned !== 1 }"
|
||||
v-for="(s, idx) in pendingAgreements"
|
||||
:key="s.id"
|
||||
@click="s.isEsigned === 1 && showQrcode(s, idx)"
|
||||
>
|
||||
<div class="item-main">
|
||||
<span class="item-title">{{ s.meetingName || ('会议 #' + s.meetingId) }}</span>
|
||||
</div>
|
||||
<span class="item-status">{{ s.isEsigned === 1 ? '待签署' : '未推送' }}</span>
|
||||
</li>
|
||||
<li v-if="!pendingAgreements.length" class="empty">暂无待签协议</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- 待签署的协议: 仅审核通过的医生可见 -->
|
||||
<div class="section" v-if="store.expertAuditApproved">
|
||||
<h2 class="section-title">
|
||||
待签署的协议
|
||||
<a class="more" @click.prevent="$router.push('/doctor/submissions')">更多 →</a>
|
||||
</h2>
|
||||
<ul class="simple-list">
|
||||
<li
|
||||
class="simple-item"
|
||||
:class="{ disabled: s.isEsigned !== 1 }"
|
||||
v-for="(s, idx) in pendingAgreements"
|
||||
:key="s.id"
|
||||
@click="s.isEsigned === 1 && showQrcode(s, idx)"
|
||||
>
|
||||
<div class="item-main">
|
||||
<span class="item-title">{{ s.meetingName || ('会议 #' + s.meetingId) }}</span>
|
||||
</div>
|
||||
<span class="item-status">{{ s.isEsigned === 1 ? '待签署' : '未推送' }}</span>
|
||||
</li>
|
||||
<li v-if="!pendingAgreements.length" class="empty">暂无待签协议</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 通知消息 (NoticeList 组件内部已含 SSE 实时刷新 + 详情 dialog + 全部已读) -->
|
||||
@@ -60,7 +43,7 @@
|
||||
通知消息
|
||||
<a class="more" @click.prevent="$router.push('/doctor/messages')">更多 →</a>
|
||||
</h2>
|
||||
<NoticeList :limit="5" :show-header="false" />
|
||||
<NoticeList :pageable="true" :show-header="false" />
|
||||
</section>
|
||||
|
||||
<!-- 二维码弹窗 -->
|
||||
@@ -87,18 +70,17 @@ import { useUserStore } from '@/store/user'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import QRCode from 'qrcode'
|
||||
import { getMyExpertProfile } from '@/api/business/expert'
|
||||
import { listUnsignedMeetingProtocols, listInvitedMeetings } from '@/api/business/meetingAttendee'
|
||||
import { listUnsignedMeetingProtocols } from '@/api/business/meetingAttendee'
|
||||
import NoticeList from '@/components/NoticeList.vue'
|
||||
|
||||
const store = useUserStore()
|
||||
|
||||
const upcomingMeetings = ref([])
|
||||
const pendingAgreements = ref([])
|
||||
// 专家真实姓名 (从 biz_expert.name 拿, 不显示 sys_user.userName (登录账号/手机号))
|
||||
const expertName = ref('')
|
||||
// 兜底显示名: 优先专家真实姓名 > nickName > userName > '专家'
|
||||
const displayName = computed(() =>
|
||||
expertName.value || store.user?.nickName || store.user?.userName || '专家'
|
||||
expertName.value || store.user?.nickName || '专家'
|
||||
)
|
||||
|
||||
const nowTime = ref('')
|
||||
@@ -154,16 +136,6 @@ async function copyQrcodeUrl() {
|
||||
}
|
||||
}
|
||||
|
||||
function formatTime(t) {
|
||||
if (!t) return ''
|
||||
const d = new Date(t)
|
||||
const today = new Date()
|
||||
const diff = Math.floor((d - today) / 86400000)
|
||||
if (diff === 0) return d.toTimeString().slice(0, 5)
|
||||
if (diff === 1) return '明天'
|
||||
return `${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
function updateClock() {
|
||||
const now = new Date()
|
||||
nowTime.value = now.toTimeString().slice(0, 5)
|
||||
@@ -177,13 +149,8 @@ async function load() {
|
||||
expertName.value = data?.name || ''
|
||||
} catch (e) { expertName.value = '' }
|
||||
|
||||
// 待参加会议 + 待签署协议: 仅审核通过的医生才拉 (未通过时 2 个 pannel 隐藏)
|
||||
// 待签署协议: 仅审核通过的医生才拉 (未通过时 panel 隐藏)
|
||||
if (store.expertAuditApproved) {
|
||||
try {
|
||||
const { data } = await listInvitedMeetings()
|
||||
upcomingMeetings.value = (Array.isArray(data) ? data : []).slice(0, 5)
|
||||
} catch (e) { upcomingMeetings.value = [] }
|
||||
|
||||
try {
|
||||
const { data } = await listUnsignedMeetingProtocols()
|
||||
pendingAgreements.value = (data || []).slice(0, 5).map(s => ({
|
||||
@@ -195,7 +162,6 @@ async function load() {
|
||||
}))
|
||||
} catch (e) { pendingAgreements.value = [] }
|
||||
} else {
|
||||
upcomingMeetings.value = []
|
||||
pendingAgreements.value = []
|
||||
}
|
||||
// 通知列表已抽到 <NoticeList> 组件, 本页不再处理
|
||||
@@ -215,7 +181,7 @@ onBeforeUnmount(() => {
|
||||
.doctor-home { padding: 16px 20px; }
|
||||
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
|
||||
.welcome-bar { background: var(--brand-primary); border-radius: 4px; padding: 20px 24px; color: #fff; display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
|
||||
.welcome-text h2 { font-size: 20px; font-weight: 600; margin-bottom: 6px; }
|
||||
.welcome-text h2 { font-size: 20px; font-weight: 600; margin-bottom: 6px; color: #fff; }
|
||||
.welcome-text p { font-size: 13px; opacity: 0.85; }
|
||||
.welcome-time .now { font-size: 14px; font-weight: 500; }
|
||||
.welcome-time .date { font-size: 12px; opacity: 0.75; margin-top: 4px; }
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<el-option v-for="o in STAGE_OPTIONS" :key="o.value" :label="o.label" :value="o.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-item><el-button type="primary" @click="load">查询</el-button><el-button @click="reset">重置</el-button></el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table :data="rows" v-loading="loading" stripe border>
|
||||
@@ -23,12 +23,20 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="日程" width="130" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-link :underline="false" type="primary" :disabled="!row.scheduleUrl" @click="onPreview(row.scheduleUrl, '日程海报')">查看</el-link>
|
||||
<el-link :underline="false" :disabled="!row.scheduleUrl" @click="onDownload(row.scheduleUrl, `${row.meetingName || '会议'}_日程海报`)">下载</el-link></template></el-table-column>
|
||||
<div class="table-actions">
|
||||
<el-link :underline="false" type="primary" :disabled="!row.scheduleUrl" @click="onPreview(row.scheduleUrl, '日程海报')">查看</el-link>
|
||||
<el-link :underline="false" :disabled="!row.scheduleUrl" @click="onDownload(row.scheduleUrl, `${row.meetingName || '会议'}_日程海报`)">下载</el-link>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="邀请函" width="130" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-link :underline="false" type="primary" :disabled="!row.projectInvitationUrl" @click="onPreview(row.projectInvitationUrl, '邀请函')">查看</el-link>
|
||||
<el-link :underline="false" :disabled="!row.projectInvitationUrl" @click="onDownload(row.projectInvitationUrl, `${row.meetingName || '会议'}_邀请函`)">下载</el-link></template></el-table-column>
|
||||
<div class="table-actions">
|
||||
<el-link :underline="false" type="primary" :disabled="!row.projectInvitationUrl" @click="onPreview(row.projectInvitationUrl, '邀请函')">查看</el-link>
|
||||
<el-link :underline="false" :disabled="!row.projectInvitationUrl" @click="onDownload(row.projectInvitationUrl, `${row.meetingName || '会议'}_邀请函`)">下载</el-link>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="签署状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.attendeeLaborProtocol ? 'success' : 'info'" size="small">{{ row.attendeeLaborProtocol ? '已签署' : '未签署' }}</el-tag>
|
||||
|
||||
@@ -6,13 +6,21 @@
|
||||
<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-input v-model="q.meetingName" 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-item><el-button type="primary" @click="load">查询</el-button><el-button @click="reset">重置</el-button></el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table :data="rows" v-loading="loading" stripe border>
|
||||
<el-table-column prop="projectNo" label="项目编号" width="170" />
|
||||
<el-table-column prop="projectName" label="项目名称" min-width="240" show-overflow-tooltip />
|
||||
<el-table-column prop="meetingName" label="会议名称" min-width="220" show-overflow-tooltip />
|
||||
<el-table-column prop="projectName" label="项目名称" min-width="240" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-link :underline="false" type="primary" @click="onView(row)">{{ row.projectName }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="meetingName" label="会议名称" min-width="220" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-link :underline="false" type="primary" @click="onView(row)">{{ row.meetingName }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<span class="status">{{ row.status || '已报名' }}</span>
|
||||
|
||||
@@ -5,7 +5,20 @@
|
||||
<el-button type="primary" @click="goBack">返回</el-button>
|
||||
</div>
|
||||
<div v-else v-loading="loading">
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-position="top" class="sign-fill-form">
|
||||
<!-- 已签署: 再次打开签署链接, 直接展示劳务协议 PDF (复用 publicity 的 OSS 代理预览) -->
|
||||
<div v-if="signed" class="signed-pdf">
|
||||
<div class="sign-header">
|
||||
<div class="sign-header-title">{{ meetingName || '劳务协议' }}</div>
|
||||
<div v-if="periodDisplay" class="sign-header-period">期数:{{ periodDisplay }}</div>
|
||||
</div>
|
||||
<iframe v-if="laborPdfUrl" :src="proxyUrl(laborPdfUrl)" class="signed-pdf-frame"></iframe>
|
||||
<div v-else class="signed-pdf-empty">协议已签署,暂无可展示的 PDF</div>
|
||||
<div class="signed-pdf-actions">
|
||||
<el-button v-if="laborPdfUrl" type="primary" @click="downloadPdf">下载劳务协议</el-button>
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-form v-else :model="form" :rules="rules" ref="formRef" label-position="top" class="sign-fill-form">
|
||||
<!-- 大标题: 会议名称 + 期数 -->
|
||||
<div class="sign-header">
|
||||
<div class="sign-header-title">{{ meetingName || '劳务协议签署' }}</div>
|
||||
@@ -106,6 +119,9 @@ const notInvited = ref(false)
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const formRef = ref(null)
|
||||
// 已签署状态: 再次打开签署链接时直接展示 PDF
|
||||
const signed = ref(false)
|
||||
const laborPdfUrl = ref('')
|
||||
|
||||
// 劳务形式 (checkboxOther 多选)
|
||||
const laborFormOptions = ref([])
|
||||
@@ -173,7 +189,7 @@ async function ensureLogin() {
|
||||
userStore.setUser({
|
||||
userId: u.userId,
|
||||
userName: u.userName || u.nickName || '',
|
||||
nickName: u.nickName || u.userName || '',
|
||||
nickName: u.nickName || '',
|
||||
phonenumber: u.phonenumber || '',
|
||||
accountType: u.accountType || 'MAIN',
|
||||
parentUserId: u.parentUserId || null,
|
||||
@@ -231,6 +247,21 @@ async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const { data } = await getSignInfo(attendeeId.value)
|
||||
// 已签署: 再次打开签署链接 → 整页跳转到 OSS proxy 的 PDF 地址 (全屏显示, 不用 iframe 内嵌)
|
||||
if (data.signed || (data.laborProtocol && data.laborProtocol.trim())) {
|
||||
const url = data.laborProtocol
|
||||
if (url) {
|
||||
window.location.href = proxyUrl(url)
|
||||
return
|
||||
}
|
||||
// 无 PDF URL 兜底: 仍留在本页显示"已签署但无可展示 PDF"
|
||||
signed.value = true
|
||||
laborPdfUrl.value = ''
|
||||
meetingName.value = data.meetingName || ''
|
||||
periodNo.value = data.periodNo ?? null
|
||||
totalPeriods.value = data.totalPeriods ?? null
|
||||
return
|
||||
}
|
||||
// 预填 (current 优先, 否则用 defaults)
|
||||
const cur = data.current || {}
|
||||
const def = data.defaults || {}
|
||||
@@ -326,6 +357,23 @@ async function onSubmit() {
|
||||
}
|
||||
}
|
||||
|
||||
// OSS 代理预览 (同 publicity / doctor/Meetings.vue): 重写 Content-Disposition 为 inline, 隐藏工具栏撑满宽度
|
||||
function proxyUrl(url) {
|
||||
if (!url) return url
|
||||
if (url.includes('hwtossbamlorgcn.oss-cn-beijing.aliyuncs.com')) {
|
||||
return import.meta.env.VITE_APP_BASE_API + '/common/oss/proxy?url=' + encodeURIComponent(url) + '#toolbar=0&zoom=page-width'
|
||||
}
|
||||
return url
|
||||
}
|
||||
function downloadPdf() {
|
||||
if (!laborPdfUrl.value) return
|
||||
const a = document.createElement('a')
|
||||
a.href = laborPdfUrl.value
|
||||
a.download = `${meetingName.value || '劳务协议'}.pdf`
|
||||
a.target = '_blank'
|
||||
document.body.appendChild(a); a.click(); document.body.removeChild(a)
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
router.push('/doctor/home')
|
||||
}
|
||||
@@ -335,13 +383,20 @@ onMounted(init)
|
||||
|
||||
<style scoped>
|
||||
/* 手机端全屏表单 (无 navbar / 无 page-card) */
|
||||
.sign-fill { padding: 16px; }
|
||||
.sign-fill { padding: 0; }
|
||||
.sign-fill-form { padding: 16px; }
|
||||
|
||||
/* 大标题: 会议名称 + 期数 */
|
||||
.sign-header { margin: 4px 0 20px; padding-bottom: 14px; border-bottom: 1px solid #f0f0f0; }
|
||||
.sign-header-title { font-size: 20px; font-weight: 600; color: #1a1a1a; line-height: 1.4; }
|
||||
.sign-header-period { margin-top: 6px; font-size: 14px; color: #595959; }
|
||||
|
||||
/* 已签署: 直接展示 PDF (宽度 100%, 高度不限制) */
|
||||
.signed-pdf { display: flex; flex-direction: column; }
|
||||
.signed-pdf-frame { width: 100%; height: 424vw; border: 0; }
|
||||
.signed-pdf-empty { padding: 48px 16px; text-align: center; color: #909399; }
|
||||
.signed-pdf-actions { margin-top: 16px; display: flex; gap: 12px; }
|
||||
|
||||
/* 未受邀提示 */
|
||||
.not-invited { padding: 60px 20px; text-align: center; }
|
||||
.not-invited-text { font-size: 16px; color: #606266; margin-bottom: 20px; }
|
||||
|
||||
@@ -115,9 +115,12 @@ onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 整页面板 (详情页标准: max-width 1200px) */
|
||||
.doctor-submission-detail { max-width: 1200px; padding: 16px; }
|
||||
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 20px; }
|
||||
/* 整页面板 (与 Submissions.vue 列表页风格一致: 16px/20px padding, 6px 圆角, 1px 浅灰描边) */
|
||||
.doctor-submission-detail { background: #fff; max-width: 1200px; padding: 16px 20px; border-radius: 6px; border: 1px solid #f0f0f0; }
|
||||
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
|
||||
/* 主按钮品牌色 (与列表页保持一致) */
|
||||
:deep(.el-button--primary) { background: var(--brand-primary); border-color: var(--brand-primary); border-radius: 4px; }
|
||||
:deep(.el-button--primary:hover) { background: var(--brand-primary-deep); border-color: var(--brand-primary-deep); }
|
||||
|
||||
/* 只读表单样式 - 模拟 el-input 视觉, 但只显示文字 */
|
||||
.readonly-form :deep(.el-form-item) { margin-bottom: 22px; }
|
||||
@@ -140,84 +143,71 @@ onMounted(load)
|
||||
|
||||
|
||||
/* ========================================
|
||||
移动端适配 (≤768px)
|
||||
- 卡片 padding 收窄
|
||||
- 工具栏横向滚动
|
||||
- filter-form: label 与输入框横向
|
||||
- 表格字号收紧
|
||||
移动端适配 (≤768px) — 参考 executor/meetings/new MeetingNew.vue
|
||||
不动 el-form-item__label 内部样式 (float/width/height), 只改容器布局,
|
||||
让 Element Plus 自带的 label-width=120px 右对齐 + input 高度自然撑开 label
|
||||
======================================== */
|
||||
@media (max-width: 768px) {
|
||||
/* 卡片 padding */
|
||||
.page-card { padding: 12px !important; border-radius: 4px !important; }
|
||||
/* 卡片 padding (与列表页统一) */
|
||||
.doctor-submission-detail { padding: 12px !important; border-radius: 4px !important; }
|
||||
.breadcrumb { font-size: 12px !important; margin-bottom: 8px !important; }
|
||||
|
||||
/* 工具栏: 横向滚动 */
|
||||
.toolbar {
|
||||
flex-wrap: nowrap !important;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding-bottom: 6px;
|
||||
margin-bottom: 8px !important;
|
||||
scrollbar-width: thin;
|
||||
/* 双列 → 单列:
|
||||
- el-row 强制 block (避免 flex 横排)
|
||||
- el-col 强制 100% 宽
|
||||
间距来源: 桌面样式 .readonly-form :deep(.el-form-item) { margin-bottom: 22px }
|
||||
(第 126 行), 手机端直接继承, 不再额外加 padding 避免双倍间距 */
|
||||
.readonly-form :deep(.el-row) {
|
||||
display: block !important;
|
||||
}
|
||||
.toolbar::-webkit-scrollbar { height: 4px; }
|
||||
.toolbar::-webkit-scrollbar-thumb { background: var(--brand-slate-200); border-radius: 2px; }
|
||||
.toolbar :deep(.action-btn),
|
||||
.toolbar :deep(.el-button) {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px !important;
|
||||
padding: 0 10px !important;
|
||||
height: 30px !important;
|
||||
.readonly-form :deep(.el-col) {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
flex: 0 0 100% !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* filter-form: 横向 (label 左 + input 右) */
|
||||
.filter-form {
|
||||
/* form-item: 横向 label + content, flex-start 让 error message 占独立行
|
||||
保留桌面已有的 22px margin-bottom (不要清零, 那是行间/字段间间距的关键),
|
||||
改 flex-start 避免多行 label 被居中.
|
||||
不动 __label 的 float/width/height/line-height — Element Plus 自带 label-width=120px
|
||||
会自然右对齐 + 跟随 content 行高, 不会参差不齐 */
|
||||
.readonly-form :deep(.el-form-item) {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
align-items: stretch !important;
|
||||
gap: 10px !important;
|
||||
}
|
||||
.filter-form :deep(.el-form-item) {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
align-items: flex-start !important;
|
||||
margin-right: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.filter-form :deep(.el-form-item__label) {
|
||||
float: none !important;
|
||||
width: auto !important;
|
||||
min-width: 80px !important;
|
||||
text-align: right !important;
|
||||
padding: 0 8px 0 0 !important;
|
||||
font-size: 13px !important;
|
||||
color: var(--el-text-color-regular) !important;
|
||||
line-height: 32px !important;
|
||||
height: 32px !important;
|
||||
}
|
||||
.filter-form :deep(.el-form-item__content) {
|
||||
margin-left: 0 !important;
|
||||
line-height: 32px !important;
|
||||
.readonly-form :deep(.el-form-item__content) {
|
||||
flex: 1 !important;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
/* 强制所有控件全宽 */
|
||||
.filter-form :deep(.el-select),
|
||||
.filter-form :deep(.el-input),
|
||||
.filter-form :deep(.el-date-editor),
|
||||
.filter-form :deep(.el-button),
|
||||
.filter-form :deep(.el-cascader) {
|
||||
/* 只读控件全宽, display:block 让 textarea 也按 block 拉伸 */
|
||||
.readonly-form :deep(.el-input),
|
||||
.readonly-form :deep(.el-textarea) {
|
||||
width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
margin-left: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
display: block !important;
|
||||
}
|
||||
.filter-form :deep(.el-button + .el-button) {
|
||||
margin-top: 8px !important;
|
||||
}
|
||||
.readonly-cell { flex: 1; min-width: 0; font-size: 14px; }
|
||||
|
||||
/* 表格字号收紧 */
|
||||
:deep(.el-table) { font-size: 12px !important; }
|
||||
/* 返回按钮 — 左右并排 (与 MeetingNew 的 form-actions 一致) */
|
||||
.detail-actions {
|
||||
display: flex !important;
|
||||
flex-wrap: wrap !important;
|
||||
gap: 0 !important;
|
||||
margin-top: 16px !important;
|
||||
padding-top: 16px !important;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
.detail-actions :deep(.el-button) {
|
||||
flex: 1 1 0 !important;
|
||||
width: 0 !important;
|
||||
margin-left: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
.detail-actions :deep(.el-button + .el-button) {
|
||||
margin-left: 8px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -60,12 +60,13 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="saving" @click="onSave">保存</el-button>
|
||||
<el-button @click="confirmCancel">取消</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 操作按钮 — 从 el-form 提出来, 避免 Element Plus form-item 容器影响按钮布局 -->
|
||||
<div class="detail-actions">
|
||||
<el-button type="primary" :loading="saving" @click="onSave">保存</el-button>
|
||||
<el-button @click="confirmCancel">取消</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -198,90 +199,73 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 整页面板 (与 admin/ExpertNew.vue 一致: max-width 1200px) */
|
||||
.doctor-submission-new { max-width: 1200px; padding: 16px; }
|
||||
/* 整页面板 (与 Submissions.vue 列表页风格一致) */
|
||||
.doctor-submission-new { background: #fff; max-width: 1200px; padding: 16px 20px; border-radius: 6px; border: 1px solid #f0f0f0; }
|
||||
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
|
||||
/* 主按钮品牌色 (与列表页保持一致) */
|
||||
:deep(.el-button--primary) { background: var(--brand-primary); border-color: var(--brand-primary); border-radius: 4px; }
|
||||
:deep(.el-button--primary:hover) { background: var(--brand-primary-deep); border-color: var(--brand-primary-deep); }
|
||||
|
||||
/* 操作按钮区 (从 el-form 提出来, 桌面/移动端一致布局) */
|
||||
.detail-actions { display: flex; gap: 8px; margin-top: 16px; padding-top: 16px; border-top: 1px solid #f0f0f0; }
|
||||
|
||||
|
||||
/* ========================================
|
||||
移动端适配 (≤768px)
|
||||
- 卡片 padding 收窄
|
||||
- 工具栏横向滚动
|
||||
- filter-form: label 与输入框横向
|
||||
- 表格字号收紧
|
||||
移动端适配 (≤768px) — 参考 executor/meetings/new MeetingNew
|
||||
不动 el-form-item__label 内部样式 (float/width/height), 让 Element Plus 自带
|
||||
label-width=120px 右对齐 + input 高度自然撑开 label, 不会参差不齐
|
||||
======================================== */
|
||||
@media (max-width: 768px) {
|
||||
/* 卡片 padding */
|
||||
.page-card { padding: 12px !important; border-radius: 4px !important; }
|
||||
/* 卡片 padding (与列表页统一) */
|
||||
.doctor-submission-new { padding: 12px !important; border-radius: 4px !important; }
|
||||
.breadcrumb { font-size: 12px !important; margin-bottom: 8px !important; }
|
||||
|
||||
/* 工具栏: 横向滚动 */
|
||||
.toolbar {
|
||||
flex-wrap: nowrap !important;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding-bottom: 6px;
|
||||
margin-bottom: 8px !important;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.toolbar::-webkit-scrollbar { height: 4px; }
|
||||
.toolbar::-webkit-scrollbar-thumb { background: var(--brand-slate-200); border-radius: 2px; }
|
||||
.toolbar :deep(.action-btn),
|
||||
.toolbar :deep(.el-button) {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px !important;
|
||||
padding: 0 10px !important;
|
||||
height: 30px !important;
|
||||
/* 双列 → 单列: el-row 强制 block, el-col 100% 宽 */
|
||||
.doctor-submission-new :deep(.el-row) { display: block !important; }
|
||||
.doctor-submission-new :deep(.el-col) {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
flex: 0 0 100% !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* filter-form: 横向 (label 左 + input 右) */
|
||||
.filter-form {
|
||||
/* form-item: 横向 label + content, flex-start 让 error message 占独立行
|
||||
不动 __label 的 float/width/height/line-height,
|
||||
不动 form-item 的 margin-bottom (桌面默认 0, 手机端继承, 间距靠 el-form-item 自带 + el-row 默认布局) */
|
||||
.doctor-submission-new :deep(.el-form-item) {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
align-items: stretch !important;
|
||||
gap: 10px !important;
|
||||
}
|
||||
.filter-form :deep(.el-form-item) {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
align-items: flex-start !important;
|
||||
margin-right: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.filter-form :deep(.el-form-item__label) {
|
||||
float: none !important;
|
||||
width: auto !important;
|
||||
min-width: 80px !important;
|
||||
text-align: right !important;
|
||||
padding: 0 8px 0 0 !important;
|
||||
font-size: 13px !important;
|
||||
color: var(--el-text-color-regular) !important;
|
||||
line-height: 32px !important;
|
||||
height: 32px !important;
|
||||
}
|
||||
.filter-form :deep(.el-form-item__content) {
|
||||
margin-left: 0 !important;
|
||||
line-height: 32px !important;
|
||||
.doctor-submission-new :deep(.el-form-item__content) {
|
||||
flex: 1 !important;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
/* 强制所有控件全宽 */
|
||||
.filter-form :deep(.el-select),
|
||||
.filter-form :deep(.el-input),
|
||||
.filter-form :deep(.el-date-editor),
|
||||
.filter-form :deep(.el-button),
|
||||
.filter-form :deep(.el-cascader) {
|
||||
/* 全部控件全宽, display:block 强制 select/input 也按 block 拉伸 (与列表页一致) */
|
||||
.doctor-submission-new :deep(.el-select),
|
||||
.doctor-submission-new :deep(.el-input),
|
||||
.doctor-submission-new :deep(.el-textarea),
|
||||
.doctor-submission-new :deep(.el-date-editor),
|
||||
.doctor-submission-new :deep(.el-cascader) {
|
||||
width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
margin-left: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
display: block !important;
|
||||
}
|
||||
.filter-form :deep(.el-button + .el-button) {
|
||||
margin-top: 8px !important;
|
||||
}
|
||||
|
||||
/* 表格字号收紧 */
|
||||
:deep(.el-table) { font-size: 12px !important; }
|
||||
/* 保存/取消按钮 — 独立 .detail-actions, flex 横排占满 */
|
||||
.detail-actions {
|
||||
display: flex !important;
|
||||
gap: 8px !important;
|
||||
margin-top: 16px !important;
|
||||
padding-top: 16px !important;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
.detail-actions :deep(.el-button) {
|
||||
flex: 1 1 0 !important;
|
||||
width: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -22,14 +22,14 @@
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="q.status" placeholder="请选择" clearable style="width: 140px">
|
||||
<el-option label="待提交" value="0" />
|
||||
<el-option label="未提交" value="0" />
|
||||
<el-option label="待审核" value="1" />
|
||||
<el-option label="通过" value="2" />
|
||||
<el-option label="拒绝" value="3" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注"><el-input v-model="q.remark" placeholder="输入备注" clearable style="width: 200px" /></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-item><el-button type="primary" @click="load">查询</el-button><el-button @click="reset">重置</el-button></el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="toolbar">
|
||||
@@ -48,8 +48,10 @@
|
||||
<el-table-column label="设计文件" width="140">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.designFileUrl">
|
||||
<el-link :underline="false" type="primary" @click="openPreview(row.designFileUrl, '设计文件')">查看</el-link>
|
||||
<el-link type="primary" :href="row.designFileUrl" target="_blank">下载</el-link>
|
||||
<div class="table-actions">
|
||||
<el-link :underline="false" type="primary" @click="openPreview(row.designFileUrl, '设计文件')">查看</el-link>
|
||||
<el-link type="primary" :href="row.designFileUrl" target="_blank">下载</el-link>
|
||||
</div>
|
||||
</template>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
@@ -59,6 +61,20 @@
|
||||
<audit-status-tag :status="row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="是否结算" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="row.isSettled==='Y' ? 'success' : 'warning'">
|
||||
{{ row.isSettled==='Y' ? '已结算' : '未结算' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="projectNo" label="项目编号" width="140" align="center" />
|
||||
<el-table-column label="创建时间" width="160" align="center">
|
||||
<template #default="{ row }">{{ row.createTime || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="审核意见" min-width="180" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.auditOpinion || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="{ row }"><div class="table-actions">
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
</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="isAdmin ? 320 : 240" fixed="right">
|
||||
<el-table-column label="操作" :width="isAdmin ? 240 : 180" fixed="right">
|
||||
<template #default="{ row }"><div class="table-actions">
|
||||
<!-- 查看: 两角色都有 (manager 原版本有, admin 原版本用 alert, 这里统一用 dialog 更清晰) -->
|
||||
<el-link :underline="false" type="primary" @click="onView(row)">查看</el-link>
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
<el-tag :type="statusTagType(row.status)" disable-transitions>{{ row.status === '1' ? '禁用' : '正常' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="340" fixed="right">
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="{ row }"><div class="table-actions">
|
||||
<!-- 查看 (只读详情): 两角色都有, 跳独立详情页 -->
|
||||
<el-link :underline="false" type="primary" @click="goView(row)">查看</el-link>
|
||||
|
||||
@@ -149,83 +149,33 @@ onMounted(loadDetail)
|
||||
|
||||
/* ========================================
|
||||
移动端适配 (≤768px)
|
||||
- 卡片 padding 收窄
|
||||
- 工具栏横向滚动
|
||||
- filter-form: label 与输入框横向
|
||||
- 表格字号收紧
|
||||
======================================== */
|
||||
@media (max-width: 768px) {
|
||||
/* 卡片 padding */
|
||||
.page-card { padding: 12px !important; border-radius: 4px !important; }
|
||||
.breadcrumb { font-size: 12px !important; margin-bottom: 8px !important; }
|
||||
|
||||
/* 工具栏: 横向滚动 */
|
||||
.toolbar {
|
||||
flex-wrap: nowrap !important;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding-bottom: 6px;
|
||||
margin-bottom: 8px !important;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.toolbar::-webkit-scrollbar { height: 4px; }
|
||||
.toolbar::-webkit-scrollbar-thumb { background: var(--brand-slate-200); border-radius: 2px; }
|
||||
.toolbar :deep(.action-btn),
|
||||
.toolbar :deep(.el-button) {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px !important;
|
||||
padding: 0 10px !important;
|
||||
height: 30px !important;
|
||||
}
|
||||
/* form-card 内部 padding 收窄 */
|
||||
:deep(.form-card .el-card__body) { padding: 12px !important; }
|
||||
|
||||
/* filter-form: 横向 (label 左 + input 右) */
|
||||
.filter-form {
|
||||
/* form-item: 横向 label + content, flex-start 让 error message 占独立行
|
||||
不动 __label — Element Plus 自带 label-width=100px 自然对齐 */
|
||||
:deep(.el-form-item) {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
align-items: stretch !important;
|
||||
gap: 10px !important;
|
||||
}
|
||||
.filter-form :deep(.el-form-item) {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
align-items: flex-start !important;
|
||||
margin-right: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.filter-form :deep(.el-form-item__label) {
|
||||
float: none !important;
|
||||
width: auto !important;
|
||||
min-width: 80px !important;
|
||||
text-align: right !important;
|
||||
padding: 0 8px 0 0 !important;
|
||||
font-size: 13px !important;
|
||||
color: var(--el-text-color-regular) !important;
|
||||
line-height: 32px !important;
|
||||
height: 32px !important;
|
||||
}
|
||||
.filter-form :deep(.el-form-item__content) {
|
||||
margin-left: 0 !important;
|
||||
line-height: 32px !important;
|
||||
:deep(.el-form-item__content) {
|
||||
flex: 1 !important;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
/* 强制所有控件全宽 */
|
||||
.filter-form :deep(.el-select),
|
||||
.filter-form :deep(.el-input),
|
||||
.filter-form :deep(.el-date-editor),
|
||||
.filter-form :deep(.el-button),
|
||||
.filter-form :deep(.el-cascader) {
|
||||
/* 底部按钮: 占满整行 (返回按钮单独占一行) */
|
||||
.form-actions {
|
||||
margin-top: 12px !important;
|
||||
}
|
||||
.form-actions :deep(.el-button) {
|
||||
width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
margin-left: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
display: block !important;
|
||||
}
|
||||
.filter-form :deep(.el-button + .el-button) {
|
||||
margin-top: 8px !important;
|
||||
}
|
||||
|
||||
/* 表格字号收紧 */
|
||||
:deep(.el-table) { font-size: 12px !important; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -185,83 +185,36 @@ onMounted(() => {
|
||||
|
||||
/* ========================================
|
||||
移动端适配 (≤768px)
|
||||
- 卡片 padding 收窄
|
||||
- 工具栏横向滚动
|
||||
- filter-form: label 与输入框横向
|
||||
- 表格字号收紧
|
||||
======================================== */
|
||||
@media (max-width: 768px) {
|
||||
/* 卡片 padding */
|
||||
.page-card { padding: 12px !important; border-radius: 4px !important; }
|
||||
.breadcrumb { font-size: 12px !important; margin-bottom: 8px !important; }
|
||||
|
||||
/* 工具栏: 横向滚动 */
|
||||
.toolbar {
|
||||
flex-wrap: nowrap !important;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding-bottom: 6px;
|
||||
margin-bottom: 8px !important;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.toolbar::-webkit-scrollbar { height: 4px; }
|
||||
.toolbar::-webkit-scrollbar-thumb { background: var(--brand-slate-200); border-radius: 2px; }
|
||||
.toolbar :deep(.action-btn),
|
||||
.toolbar :deep(.el-button) {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px !important;
|
||||
padding: 0 10px !important;
|
||||
height: 30px !important;
|
||||
}
|
||||
/* form-card 内部 padding 收窄 */
|
||||
:deep(.form-card .el-card__body) { padding: 12px !important; }
|
||||
|
||||
/* filter-form: 横向 (label 左 + input 右) */
|
||||
.filter-form {
|
||||
/* form-item: 横向 label + content, flex-start 让 error message 占独立行
|
||||
不动 __label — Element Plus 自带 label-width=100px 自然对齐 */
|
||||
:deep(.el-form-item) {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
align-items: stretch !important;
|
||||
gap: 10px !important;
|
||||
}
|
||||
.filter-form :deep(.el-form-item) {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
align-items: flex-start !important;
|
||||
margin-right: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.filter-form :deep(.el-form-item__label) {
|
||||
float: none !important;
|
||||
width: auto !important;
|
||||
min-width: 80px !important;
|
||||
text-align: right !important;
|
||||
padding: 0 8px 0 0 !important;
|
||||
font-size: 13px !important;
|
||||
color: var(--el-text-color-regular) !important;
|
||||
line-height: 32px !important;
|
||||
height: 32px !important;
|
||||
}
|
||||
.filter-form :deep(.el-form-item__content) {
|
||||
margin-left: 0 !important;
|
||||
line-height: 32px !important;
|
||||
:deep(.el-form-item__content) {
|
||||
flex: 1 !important;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
/* 强制所有控件全宽 */
|
||||
.filter-form :deep(.el-select),
|
||||
.filter-form :deep(.el-input),
|
||||
.filter-form :deep(.el-date-editor),
|
||||
.filter-form :deep(.el-button),
|
||||
.filter-form :deep(.el-cascader) {
|
||||
width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
margin-left: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
display: block !important;
|
||||
/* 底部按钮: 等宽并排 */
|
||||
.form-actions {
|
||||
flex-wrap: wrap !important;
|
||||
gap: 8px !important;
|
||||
margin-top: 12px !important;
|
||||
}
|
||||
.filter-form :deep(.el-button + .el-button) {
|
||||
margin-top: 8px !important;
|
||||
.form-actions :deep(.el-button) {
|
||||
flex: 1 1 0 !important;
|
||||
width: 0 !important;
|
||||
}
|
||||
|
||||
/* 表格字号收紧 */
|
||||
:deep(.el-table) { font-size: 12px !important; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -53,7 +53,7 @@ async function onSave() {
|
||||
await formRef.value.validate()
|
||||
saving.value = true
|
||||
try {
|
||||
await request({ url: '/system/user/profile', method: 'put', data: { nickName: form.nickName, phonenumber: form.phonenumber, sex: profile.value.sex } })
|
||||
await request({ url: '/business/person/profile', method: 'put', data: { name: form.nickName, phone: form.phonenumber } })
|
||||
if (form.newPassword) {
|
||||
await request({ url: '/system/user/profile/updatePwd', method: 'put', data: { oldPassword: form.oldPassword, newPassword: form.newPassword } })
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<el-form-item label="会议名称"><el-input v-model="q.meetingName" clearable placeholder="输入会议名称" style="width:200px" /></el-form-item>
|
||||
<el-form-item label="专家姓名"><el-input v-model="q.expertName" clearable placeholder="输入专家姓名" style="width:160px" /></el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="load">查找</el-button>
|
||||
<el-button type="primary" @click="load">查询</el-button>
|
||||
<el-button @click="reset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
<el-form-item label="会议名称"><el-input v-model="q.meetingName" placeholder="请输入会议名称" clearable style="width:160px" /></el-form-item>
|
||||
<el-form-item label="期数"><el-input-number v-model="q.periodNo" :min="1" placeholder="期数" style="width:140px" /></el-form-item>
|
||||
<el-form-item label="会议时间">
|
||||
<el-date-picker v-model="q.startTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="开始时间" style="width:170px" />
|
||||
<el-date-picker v-model="q.startTime" type="date" value-format="YYYY-MM-DD" placeholder="开始日期" style="width:170px" />
|
||||
<span class="date-sep">至</span>
|
||||
<el-date-picker v-model="q.endTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="结束时间" style="width:170px" />
|
||||
<el-date-picker v-model="q.endTime" type="date" value-format="YYYY-MM-DD" placeholder="结束日期" style="width:170px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="项目形式">
|
||||
<el-select v-model="q.projectForm" placeholder="请选择" clearable style="width:140px">
|
||||
@@ -28,7 +28,7 @@
|
||||
</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 type="primary" @click="load">查询</el-button>
|
||||
<el-button @click="reset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
@@ -40,14 +40,20 @@
|
||||
<el-table-column prop="projectNo" label="项目编号" width="170" 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 prop="meetingName" label="会议名称" min-width="180" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-link :underline="false" type="primary" @click="onView(row)">{{ row.meetingName }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<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.assignedSessions ?? row.totalPeriods ?? '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="期数" width="80" align="center">
|
||||
<template #default="{ row }">{{ row.periodNo ? '第 ' + row.periodNo + ' 期' : '-' }}</template>
|
||||
</el-table-column>
|
||||
@@ -57,11 +63,12 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="200" fixed="right" align="center">
|
||||
<el-table-column label="操作" width="240" fixed="right" align="center">
|
||||
<template #default="{ row }"><div class="table-actions">
|
||||
<el-link :underline="false" type="primary" @click="onView(row)">查看</el-link>
|
||||
<el-link :underline="false" type="primary" @click="onUpload(row)">编辑材料</el-link>
|
||||
<el-link :underline="false" type="primary" @click="onEdit(row)">修改</el-link>
|
||||
<el-link :underline="false" type="primary" @click="onEdit(row)">编辑</el-link>
|
||||
<el-link :underline="false" type="primary" @click="onUpload(row)">上传材料</el-link>
|
||||
<el-link :underline="false" type="primary" @click="onCopy(row)">复制</el-link>
|
||||
</div></template>
|
||||
</el-table-column>
|
||||
</GrTable>
|
||||
@@ -101,7 +108,11 @@ function fmtTime(d) {
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const { data } = await bizList('meeting', { ...q.value, pageNum: page.pageNum, pageSize: page.pageSize })
|
||||
const params = { ...q.value, pageNum: page.pageNum, pageSize: page.pageSize }
|
||||
// 时间范围含两端: 开始日 00:00:00 ~ 结束日 23:59:59
|
||||
if (params.startTime) params.startTime = params.startTime + ' 00:00:00'
|
||||
if (params.endTime) params.endTime = params.endTime + ' 23:59:59'
|
||||
const { data } = await bizList('meeting', params)
|
||||
rows.value = data?.rows || []
|
||||
page.total = data?.total || 0
|
||||
} catch { rows.value = []; page.total = 0 }
|
||||
@@ -135,6 +146,11 @@ function onEdit(row) {
|
||||
router.push({ name: 'executor-meetings-new', query: { meetingId: row.meetingId, projectId: row.projectId || '' } })
|
||||
}
|
||||
|
||||
// 复制会议: 跳 executor-meetings-new (MeetingNew.vue 公共页), mode=copy → 后端 POST /business/meeting
|
||||
function onCopy(row) {
|
||||
router.push({ name: 'executor-meetings-new', query: { meetingId: row.meetingId, projectId: row.projectId || '', mode: 'copy' } })
|
||||
}
|
||||
|
||||
onMounted(() => { readQueryFromRoute(); load() })
|
||||
</script>
|
||||
|
||||
|
||||
@@ -294,82 +294,40 @@ onMounted(() => {
|
||||
/* ========================================
|
||||
移动端适配 (≤768px)
|
||||
- 卡片 padding 收窄
|
||||
- 工具栏横向滚动
|
||||
- filter-form: label 与输入框横向
|
||||
- 表格字号收紧
|
||||
- el-row 双列 → 单列
|
||||
- form-item label 左 + content/content 横向并排 (不压控件内部样式, 留 error 提示行)
|
||||
- 保存/取消按钮左右并排等宽
|
||||
======================================== */
|
||||
@media (max-width: 768px) {
|
||||
/* 卡片 padding */
|
||||
.page-card { padding: 12px !important; border-radius: 4px !important; }
|
||||
.breadcrumb { font-size: 12px !important; margin-bottom: 8px !important; }
|
||||
:deep(.form-card .el-card__body) { padding: 16px 12px !important; }
|
||||
|
||||
/* 工具栏: 横向滚动 */
|
||||
.toolbar {
|
||||
flex-wrap: nowrap !important;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding-bottom: 6px;
|
||||
margin-bottom: 8px !important;
|
||||
scrollbar-width: thin;
|
||||
/* 双列 el-row → 单列 */
|
||||
.new-person :deep(.el-row) { display: block !important; }
|
||||
.new-person :deep(.el-col) {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
flex: 0 0 100% !important;
|
||||
display: block !important;
|
||||
}
|
||||
.toolbar::-webkit-scrollbar { height: 4px; }
|
||||
.toolbar::-webkit-scrollbar-thumb { background: var(--brand-slate-200); border-radius: 2px; }
|
||||
.toolbar :deep(.action-btn),
|
||||
.toolbar :deep(.el-button) {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px !important;
|
||||
padding: 0 10px !important;
|
||||
height: 30px !important;
|
||||
}
|
||||
|
||||
/* filter-form: 横向 (label 左 + input 右) */
|
||||
.filter-form {
|
||||
/* form-item 横向: label 左 + content 右, flex-start 保留 error 提示行 */
|
||||
.new-person :deep(.el-form-item) {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
align-items: stretch !important;
|
||||
gap: 10px !important;
|
||||
}
|
||||
.filter-form :deep(.el-form-item) {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
align-items: flex-start !important;
|
||||
margin-right: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
margin-bottom: 14px !important;
|
||||
}
|
||||
.filter-form :deep(.el-form-item__label) {
|
||||
float: none !important;
|
||||
width: auto !important;
|
||||
min-width: 80px !important;
|
||||
text-align: right !important;
|
||||
padding: 0 8px 0 0 !important;
|
||||
font-size: 13px !important;
|
||||
color: var(--el-text-color-regular) !important;
|
||||
line-height: 32px !important;
|
||||
height: 32px !important;
|
||||
}
|
||||
.filter-form :deep(.el-form-item__content) {
|
||||
margin-left: 0 !important;
|
||||
line-height: 32px !important;
|
||||
.new-person :deep(.el-form-item__content) {
|
||||
flex: 1 !important;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
/* 强制所有控件全宽 */
|
||||
.filter-form :deep(.el-select),
|
||||
.filter-form :deep(.el-input),
|
||||
.filter-form :deep(.el-date-editor),
|
||||
.filter-form :deep(.el-button),
|
||||
.filter-form :deep(.el-cascader) {
|
||||
width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
margin-left: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
display: block !important;
|
||||
/* 保存/取消按钮: 左右并排等宽 */
|
||||
.form-actions { margin-top: 12px !important; }
|
||||
.form-actions :deep(.el-button) {
|
||||
flex: 1 1 0 !important;
|
||||
width: 0 !important;
|
||||
}
|
||||
.filter-form :deep(.el-button + .el-button) {
|
||||
margin-top: 8px !important;
|
||||
}
|
||||
|
||||
/* 表格字号收紧 */
|
||||
:deep(.el-table) { font-size: 12px !important; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -29,13 +29,14 @@
|
||||
<!-- 消息通知 (NoticeList 组件内部已含 SSE 实时刷新 + 详情 dialog + 全部已读) -->
|
||||
<section class="section">
|
||||
<div class="section-title">消息通知<a class="more" @click.prevent="$router.push('/executor/messages')">更多 →</a></div>
|
||||
<NoticeList :limit="50" />
|
||||
<NoticeList :pageable="true" :show-header="false" />
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import request from '@/utils/request'
|
||||
import { bizList } from '@/api/public'
|
||||
import NoticeList from '@/components/NoticeList.vue'
|
||||
|
||||
@@ -46,22 +47,21 @@ const stats = ref({
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
// 项目总数量 (跟 manager 一样调普通 list, 后续若需要按 executor 隔离再换 executorList 接口)
|
||||
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
|
||||
// 会议总数量
|
||||
// 项目统计走 executorList: 后端按当前账号隔离 (MAIN=本执行单位全部, SUB=自己负责的项目)
|
||||
const projectCount = async (extra = {}) => {
|
||||
const { data } = await request.get('/business/project/executorList', { params: { pageNum: 1, pageSize: 1, ...extra } })
|
||||
return data?.total || 0
|
||||
}
|
||||
stats.value.totalProjects = await projectCount()
|
||||
stats.value.settledProjects = await projectCount({ isSettled: 'Y' })
|
||||
stats.value.completedProjects = await projectCount({ isFinished: '1' })
|
||||
// 会议统计走 /business/meeting/list (后端已按 roleType MAIN/SUB 隔离)
|
||||
const ms = await bizList('meeting', { pageNum: 1, pageSize: 1 })
|
||||
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)
|
||||
// 已结算项目
|
||||
const sp = await bizList('project', { pageNum: 1, pageSize: 1, isSettled: 'Y' })
|
||||
stats.value.settledProjects = sp.total || sp.data?.total || 0
|
||||
} catch (e) {
|
||||
console.warn('loadStats failed', e)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<el-form-item label="部门"><el-input v-model="q.department" placeholder="部门" clearable style="width:140px" /></el-form-item>
|
||||
<el-form-item label="职务"><el-input v-model="q.position" placeholder="职务" clearable style="width:140px" /></el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="loadList">查找</el-button>
|
||||
<el-button type="primary" @click="loadList">查询</el-button>
|
||||
<el-button @click="reset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
@@ -43,17 +43,27 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="300" fixed="right">
|
||||
<el-table-column label="操作" width="130" fixed="right">
|
||||
<template #default="{ row }"><div class="table-actions">
|
||||
<el-link :underline="false" size="small" type="primary" @click="onView(row)">查看</el-link>
|
||||
<el-link :underline="false" size="small" type="primary" @click="goEdit(row)">编辑</el-link>
|
||||
<!-- 重置密码: 重置为默认 123456 (与新建人员默认密码一致) -->
|
||||
<el-link :underline="false" v-if="row.userId" size="small" type="primary" @click="onResetPwd(row)">重置密码</el-link>
|
||||
<!-- 本人不显示禁用/恢复按钮 (跟 sponsor 一样, 避免主账号把自己禁用) -->
|
||||
<template v-if="row.userId !== store.user?.userId">
|
||||
<el-link :underline="false" v-if="row.status === '0'" size="small" type="danger" @click="onToggleStatus(row, '禁用')">禁用</el-link>
|
||||
<el-link :underline="false" v-else size="small" type="success" @click="onToggleStatus(row, '恢复')">恢复</el-link>
|
||||
</template>
|
||||
<!-- 次要操作折叠到"更多"下拉, 避免操作列过宽 -->
|
||||
<el-dropdown trigger="hover" @command="(cmd) => onMoreAction(cmd, row)">
|
||||
<el-link :underline="false" size="small" type="primary" class="op-dropdown">
|
||||
更多<el-icon class="op-caret"><ArrowDown /></el-icon>
|
||||
</el-link>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="edit">编辑</el-dropdown-item>
|
||||
<el-dropdown-item v-if="row.userId" command="resetPwd">重置密码</el-dropdown-item>
|
||||
<el-dropdown-item v-if="row.userId !== store.user?.userId && row.status === '0'" command="disable" divided>
|
||||
<span style="color:#DC2626">禁用</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="row.userId !== store.user?.userId && row.status !== '0'" command="enable" divided>
|
||||
<span style="color:#16A34A">恢复</span>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div></template>
|
||||
</el-table-column>
|
||||
</GrTable>
|
||||
@@ -115,6 +125,7 @@ import { bizUpdate, resetPersonPassword } from '@/api/public'
|
||||
import { listExecutorPerson } from '@/api/business/person'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ArrowDown } from '@element-plus/icons-vue'
|
||||
import { accountTypeRoleLabel, accountTypeRoleTagType } from '@/utils/roleMap'
|
||||
import GrTable from '@/components/GrTable.vue'
|
||||
import request from '@/utils/request'
|
||||
@@ -156,6 +167,14 @@ function goEdit(row) { router.push(`/executor/people/edit/${row.personId}`) }
|
||||
|
||||
function onView(row) { router.push(`/executor/people/detail/${row.personId}`) }
|
||||
|
||||
// "更多"下拉分发: 把 cmd 映射到现有 handler (操作列折叠, 避免过宽)
|
||||
function onMoreAction(cmd, row) {
|
||||
if (cmd === 'edit') goEdit(row)
|
||||
else if (cmd === 'resetPwd') onResetPwd(row)
|
||||
else if (cmd === 'disable') onToggleStatus(row, '禁用')
|
||||
else if (cmd === 'enable') onToggleStatus(row, '恢复')
|
||||
}
|
||||
|
||||
async function onResetPwd(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
|
||||
@@ -143,82 +143,40 @@ onMounted(loadDetail)
|
||||
/* ========================================
|
||||
移动端适配 (≤768px)
|
||||
- 卡片 padding 收窄
|
||||
- 工具栏横向滚动
|
||||
- filter-form: label 与输入框横向
|
||||
- 表格字号收紧
|
||||
- el-row 双列 → 单列
|
||||
- form-item label 左 + content/content 横向并排 (不压控件内部样式)
|
||||
- 返回按钮全宽
|
||||
======================================== */
|
||||
@media (max-width: 768px) {
|
||||
/* 卡片 padding */
|
||||
.page-card { padding: 12px !important; border-radius: 4px !important; }
|
||||
.breadcrumb { font-size: 12px !important; margin-bottom: 8px !important; }
|
||||
:deep(.form-card .el-card__body) { padding: 16px 12px !important; }
|
||||
|
||||
/* 工具栏: 横向滚动 */
|
||||
.toolbar {
|
||||
flex-wrap: nowrap !important;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding-bottom: 6px;
|
||||
margin-bottom: 8px !important;
|
||||
scrollbar-width: thin;
|
||||
/* 双列 el-row → 单列 */
|
||||
.readonly-form :deep(.el-row) { display: block !important; }
|
||||
.readonly-form :deep(.el-col) {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
flex: 0 0 100% !important;
|
||||
display: block !important;
|
||||
}
|
||||
.toolbar::-webkit-scrollbar { height: 4px; }
|
||||
.toolbar::-webkit-scrollbar-thumb { background: var(--brand-slate-200); border-radius: 2px; }
|
||||
.toolbar :deep(.action-btn),
|
||||
.toolbar :deep(.el-button) {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px !important;
|
||||
padding: 0 10px !important;
|
||||
height: 30px !important;
|
||||
}
|
||||
|
||||
/* filter-form: 横向 (label 左 + input 右) */
|
||||
.filter-form {
|
||||
/* form-item 横向: label 左 + content 右 (flex-start 保留 error 提示行空间) */
|
||||
.readonly-form :deep(.el-form-item) {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
align-items: stretch !important;
|
||||
gap: 10px !important;
|
||||
}
|
||||
.filter-form :deep(.el-form-item) {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
align-items: flex-start !important;
|
||||
margin-right: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
margin-bottom: 14px !important;
|
||||
}
|
||||
.filter-form :deep(.el-form-item__label) {
|
||||
float: none !important;
|
||||
width: auto !important;
|
||||
min-width: 80px !important;
|
||||
text-align: right !important;
|
||||
padding: 0 8px 0 0 !important;
|
||||
font-size: 13px !important;
|
||||
color: var(--el-text-color-regular) !important;
|
||||
line-height: 32px !important;
|
||||
height: 32px !important;
|
||||
}
|
||||
.filter-form :deep(.el-form-item__content) {
|
||||
margin-left: 0 !important;
|
||||
line-height: 32px !important;
|
||||
.readonly-form :deep(.el-form-item__content) {
|
||||
flex: 1 !important;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
/* 强制所有控件全宽 */
|
||||
.filter-form :deep(.el-select),
|
||||
.filter-form :deep(.el-input),
|
||||
.filter-form :deep(.el-date-editor),
|
||||
.filter-form :deep(.el-button),
|
||||
.filter-form :deep(.el-cascader) {
|
||||
width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
margin-left: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
display: block !important;
|
||||
/* 按钮区: 按钮等宽并排 (只有1 个时仍走同规则, 居中显示) */
|
||||
.form-actions { margin-top: 12px !important; }
|
||||
.form-actions :deep(.el-button) {
|
||||
flex: 1 1 0 !important;
|
||||
width: 0 !important;
|
||||
}
|
||||
.filter-form :deep(.el-button + .el-button) {
|
||||
margin-top: 8px !important;
|
||||
}
|
||||
|
||||
/* 表格字号收紧 */
|
||||
:deep(.el-table) { font-size: 12px !important; }
|
||||
}
|
||||
</style>
|
||||
@@ -7,7 +7,7 @@
|
||||
<el-form-item label="项目编号"><el-input v-model="query.projectNo" clearable placeholder="项目编号" style="width:160px" /></el-form-item>
|
||||
<el-form-item label="项目名称"><el-input v-model="query.projectName" clearable placeholder="项目名称" style="width:200px" /></el-form-item>
|
||||
<el-form-item label="项目时间">
|
||||
<el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" style="width:240px" />
|
||||
<el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD" style="width:240px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="是否结题">
|
||||
<el-select v-model="query.isFinished" clearable placeholder="请选择" style="width:140px">
|
||||
@@ -40,13 +40,17 @@
|
||||
stripe
|
||||
v-loading="loading"
|
||||
:main-cols="['projectNo', 'projectName']"
|
||||
@selection-change="sel=selected=sel"
|
||||
@selection-change="onSelectionChange"
|
||||
>
|
||||
<el-table-column v-if="!isSub" type="selection" width="48" />
|
||||
<el-table-column prop="projectNo" label="项目编号" width="170" />
|
||||
<el-table-column prop="projectName" label="项目名称" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="总场次/总期数" width="110" align="center">
|
||||
<template #default="{ row }">{{ row.assignedSessions || 0 }}/{{ row.assignedSessions || 0 }}</template>
|
||||
<el-table-column prop="projectName" label="项目名称" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-link :underline="false" type="primary" @click="viewDetail(row)">{{ row.projectName }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="总场次/总期数" width="130" align="center">
|
||||
<template #default="{ row }">{{ row.assignedSessions || 0 }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="doneSessions" label="已执行" width="80" align="center" />
|
||||
<el-table-column prop="todoSessions" label="未执行" width="80" align="center" />
|
||||
@@ -62,18 +66,6 @@
|
||||
<el-table-column label="已支付会务费" width="120" align="right">
|
||||
<template #default="{ row }">¥ {{ formatNum(row.paidMeetingAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="managerScore" label="执行单位得分(合规)" width="140" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.managerScore != null">{{ row.managerScore }}</span>
|
||||
<span v-else style="color:#c0c4cc">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sponsorScore" label="执行单位得分(支持)" width="140" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.sponsorScore != null">{{ row.sponsorScore }}</span>
|
||||
<span v-else style="color:#c0c4cc">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="isFinished" label="是否结题" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.isFinished === '1' || row.isFinished === '已结题' ? 'warning' : 'info'" size="small">
|
||||
@@ -81,13 +73,13 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="startTime" label="项目开始时间" width="150" />
|
||||
<el-table-column prop="endTime" label="项目结束时间" width="150" />
|
||||
<el-table-column prop="startTime" label="项目开始时间" width="170" />
|
||||
<el-table-column prop="endTime" label="项目结束时间" width="170" />
|
||||
<el-table-column label="操作" width="250" fixed="right">
|
||||
<template #default="{ row }"><div class="table-actions">
|
||||
<el-link :underline="false" type="primary" :disabled="atCap(row)" :title="atCap(row) ? '已建会议数已达分配场次上限' : ''" @click="onCreateMeeting(row)">建会</el-link>
|
||||
<el-link v-if="!isFinishedRow(row)" :underline="false" type="primary" :disabled="atCap(row)" :title="atCap(row) ? '已建会议数已达分配场次上限' : ''" @click="onCreateMeeting(row)">建会</el-link>
|
||||
<el-link :underline="false" type="primary" @click="viewDetail(row)">查看</el-link>
|
||||
<el-link :underline="false" v-if="!isSub" type="primary" @click="onAssign(row)">分配</el-link>
|
||||
<el-link :underline="false" v-if="!isSub && !isFinishedRow(row)" type="primary" @click="onAssign(row)">分配</el-link>
|
||||
</div></template>
|
||||
</el-table-column>
|
||||
</GrTable>
|
||||
@@ -110,7 +102,7 @@
|
||||
<el-form :model="assignForm" label-width="110px">
|
||||
<el-form-item label="分配给执行人">
|
||||
<el-select v-model="assignForm.staffUserIds" multiple collapse-tags collapse-tags-tooltip filterable placeholder="请选择执行人 (可多选)" style="width:100%">
|
||||
<el-option v-for="s in allStaff" :key="s.userId" :label="s.name + (s.phone ? ' (' + s.phone + ')' : '')" :value="s.userId" />
|
||||
<el-option v-for="s in allStaff" :key="s.userId" :label="s.name + (s.accountType === 'MAIN' ? '(管理员)' : '') + (s.phone ? ' (' + s.phone + ')' : '')" :value="s.userId" :disabled="s.accountType === 'MAIN'" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
@@ -122,6 +114,7 @@
|
||||
|
||||
<!-- 评分弹窗已移除: executor 不可评分 -->
|
||||
<!-- 项目详情已迁到公共页 /executor/projects/detail/:projectId (复用 manager/ManagerProjectDetail.vue) -->
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -170,8 +163,9 @@ async function loadList() {
|
||||
try {
|
||||
const params = { pageNum: page.pageNum, pageSize: page.pageSize, ...query }
|
||||
if (dateRange.value && dateRange.value.length === 2) {
|
||||
params.startTime = dateRange.value[0]
|
||||
params.endTime = dateRange.value[1]
|
||||
// 时间范围含两端: 开始日 00:00:00 ~ 结束日 23:59:59
|
||||
params.startTime = dateRange.value[0] + ' 00:00:00'
|
||||
params.endTime = dateRange.value[1] + ' 23:59:59'
|
||||
}
|
||||
const data = await listExecutorProjects(params)
|
||||
list.value = data.rows || []
|
||||
@@ -203,10 +197,11 @@ function reset() {
|
||||
function viewDetail(row) { router.push(`/executor/projects/detail/${row.projectId}`) }
|
||||
|
||||
async function loadStaff() {
|
||||
// 执行方自己的执行人员 (SUB 子账号), 走 listExecutorPerson (后端强制 unit_type='executor' + 当前主账号隔离)
|
||||
// 本 org 下所有执行方用户 (MAIN 管理员 + SUB 执行人员), 走 listExecutorPerson (后端强制 unit_type='executor' + 当前主账号隔离)
|
||||
// status='0': 只拉启用账号; 管理员 (MAIN) 一并返回但由前端置灰 (不可选)
|
||||
try {
|
||||
const res = await listExecutorPerson({ pageNum: 1, pageSize: 100 })
|
||||
allStaff.value = (res.rows || []).filter(p => p.accountType === 'SUB')
|
||||
const res = await listExecutorPerson({ pageNum: 1, pageSize: 100, status: '0' })
|
||||
allStaff.value = res.rows || []
|
||||
} catch { allStaff.value = [] }
|
||||
}
|
||||
/** 拉项目当前已分配执行人 → 回显 staffUserIds (单选模式) */
|
||||
@@ -217,7 +212,7 @@ async function loadCurrentAssigns(projectId) {
|
||||
assignForm.staffUserIds = (Array.isArray(arr) ? arr : []).map(x => x.staffUserId).filter(Boolean)
|
||||
} catch (e) { assignForm.staffUserIds = [] }
|
||||
}
|
||||
function openAssign(row) {
|
||||
async function openAssign(row) {
|
||||
assignBatchMode.value = false
|
||||
assignBatchRows.value = []
|
||||
assignRow.value = row
|
||||
@@ -226,8 +221,9 @@ function openAssign(row) {
|
||||
assignForm.projectName = row.projectName
|
||||
assignForm.staffUserIds = []
|
||||
assignOpen.value = true
|
||||
loadStaff()
|
||||
loadCurrentAssigns(row.projectId)
|
||||
// 先等人列表加载完, 再回显已分配执行人, 避免回显值先到导致闪裸 user_id (如 205)
|
||||
await loadStaff()
|
||||
await loadCurrentAssigns(row.projectId)
|
||||
}
|
||||
function openAssignBatch(rows) {
|
||||
assignBatchMode.value = true
|
||||
@@ -243,6 +239,7 @@ function onAssign(row) {
|
||||
if (selected.value.length === 1) openAssign(selected.value[0])
|
||||
else openAssignBatch(selected.value)
|
||||
}
|
||||
function onSelectionChange(arr) { selected.value = arr }
|
||||
async function saveAssign() {
|
||||
if (!assignForm.staffUserIds.length) return ElMessage.warning('请至少选择一位执行人')
|
||||
assignSubmitting.value = true
|
||||
@@ -276,6 +273,8 @@ function atCap(row) {
|
||||
const built = row.meetingCount || 0
|
||||
return built >= assigned
|
||||
}
|
||||
// 结题项目不显示建会入口 (与 manager/项目管理 页一致)
|
||||
function isFinishedRow(row) { return row.isFinished === '1' || row.isFinished === '已结题' }
|
||||
|
||||
function onCreateMeeting(row) {
|
||||
router.push(`/executor/meetings/new?projectId=${row.projectId}`)
|
||||
|
||||
@@ -343,82 +343,48 @@ onMounted(() => {
|
||||
/* ========================================
|
||||
移动端适配 (≤768px)
|
||||
- 卡片 padding 收窄
|
||||
- 工具栏横向滚动
|
||||
- filter-form: label 与输入框横向
|
||||
- 表格字号收紧
|
||||
- el-row 双列 → 单列, form-item 横向 (label-width=120px 桌面默认沿用, Element Plus 自然对齐)
|
||||
- form-card 去掉 padding + 底部按钮等宽并排
|
||||
======================================== */
|
||||
@media (max-width: 768px) {
|
||||
/* 卡片 padding */
|
||||
.page-card { padding: 12px !important; border-radius: 4px !important; }
|
||||
.breadcrumb { font-size: 12px !important; margin-bottom: 8px !important; }
|
||||
|
||||
/* 工具栏: 横向滚动 */
|
||||
.toolbar {
|
||||
flex-wrap: nowrap !important;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding-bottom: 6px;
|
||||
margin-bottom: 8px !important;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.toolbar::-webkit-scrollbar { height: 4px; }
|
||||
.toolbar::-webkit-scrollbar-thumb { background: var(--brand-slate-200); border-radius: 2px; }
|
||||
.toolbar :deep(.action-btn),
|
||||
.toolbar :deep(.el-button) {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px !important;
|
||||
padding: 0 10px !important;
|
||||
height: 30px !important;
|
||||
/* form-card 内部 padding 收窄 */
|
||||
:deep(.form-card .el-card__body) { padding: 12px !important; }
|
||||
|
||||
/* 双列 → 单列: el-row 强制 block, el-col 100% 宽 */
|
||||
:deep(.el-row) { display: block !important; }
|
||||
:deep(.el-col) {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
flex: 0 0 100% !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* filter-form: 横向 (label 左 + input 右) */
|
||||
.filter-form {
|
||||
/* form-item: 横向 label + content, flex-start 让 error message 占独立行
|
||||
不动 __label 的 float/width/height/line-height — Element Plus 自带 label-width=120px
|
||||
自然右对齐 + 跟随 content 行高, 不会参差不齐 */
|
||||
:deep(.el-form-item) {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
align-items: stretch !important;
|
||||
gap: 10px !important;
|
||||
}
|
||||
.filter-form :deep(.el-form-item) {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
align-items: flex-start !important;
|
||||
margin-right: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.filter-form :deep(.el-form-item__label) {
|
||||
float: none !important;
|
||||
width: auto !important;
|
||||
min-width: 80px !important;
|
||||
text-align: right !important;
|
||||
padding: 0 8px 0 0 !important;
|
||||
font-size: 13px !important;
|
||||
color: var(--el-text-color-regular) !important;
|
||||
line-height: 32px !important;
|
||||
height: 32px !important;
|
||||
}
|
||||
.filter-form :deep(.el-form-item__content) {
|
||||
margin-left: 0 !important;
|
||||
line-height: 32px !important;
|
||||
:deep(.el-form-item__content) {
|
||||
flex: 1 !important;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
/* 强制所有控件全宽 */
|
||||
.filter-form :deep(.el-select),
|
||||
.filter-form :deep(.el-input),
|
||||
.filter-form :deep(.el-date-editor),
|
||||
.filter-form :deep(.el-button),
|
||||
.filter-form :deep(.el-cascader) {
|
||||
width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
margin-left: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
display: block !important;
|
||||
/* 底部按钮: 等宽并排 */
|
||||
.form-actions {
|
||||
flex-wrap: wrap !important;
|
||||
gap: 8px !important;
|
||||
margin-top: 12px !important;
|
||||
}
|
||||
.filter-form :deep(.el-button + .el-button) {
|
||||
margin-top: 8px !important;
|
||||
.form-actions :deep(.el-button) {
|
||||
flex: 1 1 0 !important;
|
||||
width: 0 !important;
|
||||
}
|
||||
|
||||
/* 表格字号收紧 */
|
||||
:deep(.el-table) { font-size: 12px !important; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="load">{{ isAdmin ? '查询' : '查找' }}</el-button>
|
||||
<el-button type="primary" @click="load">查询</el-button>
|
||||
<el-button @click="reset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
@@ -65,8 +65,10 @@
|
||||
<el-table-column label="执业证书" width="170">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.practiceCertUrl">
|
||||
<el-link :underline="false" type="primary" @click="onPreviewCert(row.practiceCertUrl)">查看</el-link>
|
||||
<el-link :underline="false" type="primary" @click="onDownloadCert(row.practiceCertUrl, row.phone)">下载</el-link>
|
||||
<div class="table-actions">
|
||||
<el-link :underline="false" type="primary" @click="onPreviewCert(row.practiceCertUrl)">查看</el-link>
|
||||
<el-link :underline="false" type="primary" @click="onDownloadCert(row.practiceCertUrl, row.phone)">下载</el-link>
|
||||
</div>
|
||||
</template>
|
||||
<span v-else style="color:#c0c4cc">-</span>
|
||||
</template>
|
||||
@@ -74,8 +76,10 @@
|
||||
<el-table-column label="职称证明" width="170">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.titleCertUrl">
|
||||
<el-link :underline="false" type="primary" @click="onPreviewCert(row.titleCertUrl)">查看</el-link>
|
||||
<el-link :underline="false" type="primary" @click="onDownloadCert(row.titleCertUrl, row.phone)">下载</el-link>
|
||||
<div class="table-actions">
|
||||
<el-link :underline="false" type="primary" @click="onPreviewCert(row.titleCertUrl)">查看</el-link>
|
||||
<el-link :underline="false" type="primary" @click="onDownloadCert(row.titleCertUrl, row.phone)">下载</el-link>
|
||||
</div>
|
||||
</template>
|
||||
<span v-else style="color:#c0c4cc">-</span>
|
||||
</template>
|
||||
|
||||
@@ -9,15 +9,8 @@
|
||||
<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 label="手机号"><el-input v-model="q.phone" placeholder="输入手机号" clearable /></el-form-item>
|
||||
<el-form-item label="审核状态">
|
||||
<el-select v-model="q.intentStatus" clearable style="width: 130px">
|
||||
<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 type="primary" @click="load">查询</el-button>
|
||||
<el-button @click="reset">重置</el-button>
|
||||
<el-button type="success" @click="doExport">导出</el-button>
|
||||
</el-form-item>
|
||||
@@ -72,7 +65,7 @@ import {
|
||||
exportPublicityExecutionIntent
|
||||
} from '@/api/public'
|
||||
|
||||
const q = ref({ projectNo: '', projectName: '', name: '', workUnit: '', phone: '', intentStatus: '' })
|
||||
const q = ref({ projectNo: '', projectName: '', name: '', workUnit: '', phone: '' })
|
||||
const rows = ref([])
|
||||
const loading = ref(false)
|
||||
const page = reactive({ pageNum: 1, pageSize: 20, total: 0 })
|
||||
@@ -91,7 +84,7 @@ async function load() {
|
||||
}
|
||||
|
||||
function reset() {
|
||||
q.value = { projectNo: '', projectName: '', name: '', workUnit: '', phone: '', intentStatus: '' }
|
||||
q.value = { projectNo: '', projectName: '', name: '', workUnit: '', phone: '' }
|
||||
page.pageNum = 1
|
||||
load()
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<!-- 面包屑 (按角色显示: sponsor → 我的项目; manager/admin → 项目管理) -->
|
||||
<div class="breadcrumb">
|
||||
首页 /
|
||||
<span v-if="userStore?.user?.roleType === 'sponsor'">我的项目</span>
|
||||
<span v-else-if="userStore?.user?.roleType === 'executor'">项目列表</span>
|
||||
<span v-if="roleType === 'sponsor'">我的项目</span>
|
||||
<span v-else-if="roleType === 'executor'">项目列表</span>
|
||||
<span v-else>项目管理</span>
|
||||
/ <span class="current">项目详情</span>
|
||||
</div>
|
||||
@@ -25,31 +25,41 @@
|
||||
<el-col :span="12"><el-form-item label="项目形式"><span class="info-value">{{ row.projectForm || '-' }}</span></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="12"><el-form-item label="项目开始时间"><span class="info-value">{{ fmtDate(row.startTime) }}</span></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="项目结束时间"><span class="info-value">{{ fmtDate(row.endTime) }}</span></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="项目开始时间"><span class="info-value">{{ fmtDateTime(row.startTime) }}</span></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="项目结束时间"><span class="info-value">{{ fmtDateTime(row.endTime) }}</span></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="12"><el-form-item label="是否招标项目"><span class="info-value">{{ row.isBidProject === 'Y' ? '是' : '否' }}</span></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="项目负责人"><span class="info-value">{{ row.leadUserName || '-' }}</span></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="12"><el-form-item label="支持公司"><span class="info-value">{{ row.sponsorOrgName || '-' }}</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-col :span="12"><el-form-item label="支持单位"><span class="info-value">{{ row.sponsorOrgName || '-' }}</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 }"><span class="money">¥ {{ fmtMoney(row.amount) }}</span></template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<span style="color:#909399">暂无分配 (到 项目管理 / 项目分配 添加)</span>
|
||||
</template>
|
||||
</el-table>
|
||||
<el-form-item label="执行单位">
|
||||
<table class="info-table role-labor-table">
|
||||
<colgroup>
|
||||
<col style="width:160px">
|
||||
<col style="width:90px">
|
||||
<col style="width:130px">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>名称</th>
|
||||
<th style="text-align:right">场次</th>
|
||||
<th style="text-align:right">金额</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(a, idx) in assigns" :key="idx">
|
||||
<td>{{ a.orgName || '-' }}</td>
|
||||
<td class="money" style="text-align:right">{{ a.sessions || 0 }}</td>
|
||||
<td class="money" style="text-align:right">¥ {{ fmtMoney(a.amount) }}</td>
|
||||
</tr>
|
||||
<tr v-if="!assigns.length"><td colspan="3" class="empty-row">到 项目管理/分配 添加</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@@ -78,11 +88,14 @@
|
||||
</table>
|
||||
|
||||
<!-- ================= 3. 支持单位监督员 (只读) ================= -->
|
||||
<div class="new-card-title">支持单位监督员</div>
|
||||
<div class="new-card-title monitor-title">
|
||||
<span>支持单位监督员</span>
|
||||
<el-link v-if="roleType === 'manager' && row.sponsorOrgId" :underline="false" type="primary" @click="openMonitorAssign">分配</el-link>
|
||||
</div>
|
||||
<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>
|
||||
<span class="monitor-name">{{ m.name || '-' }}</span>
|
||||
</div>
|
||||
<div v-if="!monitors.length" class="empty-hint">暂无监督员</div>
|
||||
</div>
|
||||
@@ -105,6 +118,26 @@
|
||||
<!-- 公告预览 (只读) -->
|
||||
<Preview v-model="previewOpen" :url="previewUrl" :title="previewTitle" />
|
||||
|
||||
<!-- ================= 监督员分配 dialog (仅 manager, 需已分配支持方) ================= -->
|
||||
<el-dialog v-model="monitorAssignVisible" title="分配监督员" width="560px" append-to-body destroy-on-close>
|
||||
<div class="assign-project-info">
|
||||
<div><span class="assign-lbl">项目编号</span><span>{{ row.projectNo || '-' }}</span></div>
|
||||
<div><span class="assign-lbl">项目名称</span><span>{{ row.projectName || '-' }}</span></div>
|
||||
</div>
|
||||
<el-form :model="monitorAssignForm" label-width="100px">
|
||||
<el-form-item label="分配给监督员">
|
||||
<el-select v-model="monitorAssignForm.monitorUserIds" multiple collapse-tags collapse-tags-tooltip filterable
|
||||
placeholder="请选择监督员 (姓名 / 手机号, 可多选)" style="width: 100%">
|
||||
<el-option v-for="m in monitorCandidates" :key="m.userId" :label="m.name + ' (' + m.phone + ')'" :value="m.userId" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="monitorAssignVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="monitorAssignSubmitting" :disabled="!monitorAssignForm.monitorUserIds.length" @click="saveMonitorAssign">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 底部操作: 返回 (按角色回退) -->
|
||||
<div class="form-actions">
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
@@ -115,9 +148,11 @@
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { bizGet } from '@/api/public'
|
||||
import { bizGet, bizList } from '@/api/public'
|
||||
import { listExecutorOrgs } from '@/api/system'
|
||||
import { sponsorAssignProject } from '@/api/business/project'
|
||||
import request from '@/utils/request'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import Preview from '@/components/Preview.vue'
|
||||
import { useUserStore } from '@/store/user'
|
||||
|
||||
@@ -125,6 +160,9 @@ const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 角色 (role_type 单一可信源: store.role = user.role = /getInfo 返回的 roleType)
|
||||
const roleType = computed(() => userStore.role)
|
||||
|
||||
// ===================== 状态 =====================
|
||||
const projectId = ref('')
|
||||
const row = ref({})
|
||||
@@ -167,30 +205,29 @@ 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) {
|
||||
function fmtDateTime(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
|
||||
return String(v)
|
||||
}
|
||||
// 从 OSS URL 提取并解码文件名 (中文文件名是 URL 编码的, 需 decodeURIComponent 还原)
|
||||
function fileNameFromUrl(url) {
|
||||
if (!url) return ''
|
||||
let last
|
||||
try {
|
||||
const path = String(url).split('?')[0]
|
||||
return decodeURIComponent(path.split('/').pop() || '')
|
||||
last = decodeURIComponent(path.split('/').pop() || '')
|
||||
} catch (e) {
|
||||
// 非标准编码导致解码失败 → 退回原始最后一段
|
||||
return String(url).split('?')[0].split('/').pop() || ''
|
||||
last = String(url).split('?')[0].split('/').pop() || ''
|
||||
}
|
||||
// 去掉 OSS 上传附加的随机串: 原名_时间戳(13位)_随机串(6位).扩展名 → 原名.扩展名
|
||||
const m = last.match(/^(.+)_\d{13}_[a-z0-9]{6}(\.[^.]+)?$/)
|
||||
if (m) return m[1] + (m[2] || '')
|
||||
return last
|
||||
}
|
||||
|
||||
// ===================== 加载 =====================
|
||||
@@ -261,7 +298,7 @@ async function loadMonitors() {
|
||||
const list = (resp && resp.data) || []
|
||||
monitors.value = (Array.isArray(list) ? list : []).map(x => ({
|
||||
userId: x.monitorUserId,
|
||||
userName: x.monitorUserName || ('(userId=' + x.monitorUserId + ')')
|
||||
name: x.monitorUserName || ('(userId=' + x.monitorUserId + ')')
|
||||
}))
|
||||
} catch (e) {
|
||||
console.error('[project-detail] monitors load failed', e)
|
||||
@@ -269,12 +306,65 @@ async function loadMonitors() {
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 监督员分配 (仅 manager, 需已分配支持方) =====================
|
||||
const monitorAssignVisible = ref(false)
|
||||
const monitorAssignSubmitting = ref(false)
|
||||
const monitorCandidates = ref([])
|
||||
const monitorAssignForm = reactive({ monitorUserIds: [] })
|
||||
|
||||
// 候选: 本项目已分配支持方的员工 (按 orgId 圈定, 过滤出子账号=监督员)
|
||||
async function loadMonitorCandidates() {
|
||||
const sponsorOrgId = row.value && row.value.sponsorOrgId
|
||||
if (!sponsorOrgId) { monitorCandidates.value = []; return }
|
||||
try {
|
||||
const { data } = await bizList('person', { orgId: sponsorOrgId, status: '0', pageNum: 1, pageSize: 100 })
|
||||
const rows = (data && data.rows) || []
|
||||
monitorCandidates.value = rows
|
||||
.filter(p => p.accountType === 'SUB')
|
||||
.map(p => ({ userId: p.userId, name: p.name, phone: p.phone }))
|
||||
} catch (e) {
|
||||
monitorCandidates.value = []
|
||||
}
|
||||
}
|
||||
|
||||
function openMonitorAssign() {
|
||||
monitorAssignForm.monitorUserIds = []
|
||||
monitorAssignVisible.value = true
|
||||
loadMonitorCandidates()
|
||||
// 回显当前已分配监督员 (等候选加载完, 让 el-select 找得到 option)
|
||||
request({ url: `/business/project/${projectId.value}/sponsorAssigns`, method: 'get' })
|
||||
.then(resp => {
|
||||
const list = (resp && resp.data) || []
|
||||
monitorAssignForm.monitorUserIds = (Array.isArray(list) ? list : []).map(x => x.monitorUserId).filter(Boolean)
|
||||
})
|
||||
.catch(() => { monitorAssignForm.monitorUserIds = [] })
|
||||
}
|
||||
|
||||
async function saveMonitorAssign() {
|
||||
if (!monitorAssignForm.monitorUserIds.length) return ElMessage.warning('请至少选择一位监督员')
|
||||
monitorAssignSubmitting.value = true
|
||||
try {
|
||||
await sponsorAssignProject({
|
||||
projectId: projectId.value,
|
||||
monitorUserIds: [...monitorAssignForm.monitorUserIds]
|
||||
})
|
||||
ElMessage.success('分配成功')
|
||||
monitorAssignVisible.value = false
|
||||
await loadMonitors()
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.msg || e?.message || '分配失败')
|
||||
} finally {
|
||||
monitorAssignSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 导航 =====================
|
||||
// 按角色回退: sponsor → 我的项目, manager → 项目管理, admin → 项目管理
|
||||
function goBack() {
|
||||
const role = userStore?.role || userStore?.user?.roleType
|
||||
const role = roleType.value
|
||||
if (role === 'sponsor') router.push('/sponsor/my-projects')
|
||||
else if (role === 'executor') router.push('/executor/projects')
|
||||
else if (role === 'admin') router.push('/admin/projects')
|
||||
else router.push('/manager/projects')
|
||||
}
|
||||
function goEdit() { router.push(`/manager/projects/edit/${projectId.value}`) }
|
||||
@@ -326,7 +416,7 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
/* 监督员 */
|
||||
.monitor-list { padding: 4px 0; }
|
||||
.monitor-list { padding: 4px 0; max-width: 400px; }
|
||||
.monitor-row {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 8px 12px; background: #fafafa; border-radius: 4px;
|
||||
@@ -335,19 +425,19 @@ onMounted(async () => {
|
||||
.monitor-num { width: 24px; color: #909399; font-size: 13px; }
|
||||
.monitor-name { flex: 1; font-size: 14px; color: #262626; }
|
||||
.empty-hint { text-align: center; color: #909399; padding: 12px 0; font-size: 13px; }
|
||||
.monitor-title { display: flex; align-items: center; justify-content: space-between; max-width: 400px; }
|
||||
|
||||
/* 监督员分配 dialog 项目信息 */
|
||||
.assign-project-info { display: flex; flex-direction: column; gap: 8px; margin-bottom: 12px; font-size: 14px; color: #262626; }
|
||||
.assign-lbl { display: inline-block; min-width: 70px; color: #909399; }
|
||||
|
||||
/* 角色劳务表格 + 服务公司表格: 统一 14px, 表头 600/strong, 单元格 strong */
|
||||
.info-table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
||||
.role-labor-table { width: auto; border: 1px solid #ebeef5; } /* 角色劳务表格按内容紧凑显示, 不撑满卡片 */
|
||||
.role-labor-table { width: auto; border: 1px solid #ebeef5; } /* 按内容紧凑显示, 不撑满卡片 */
|
||||
.role-labor-table th,
|
||||
.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; font-size: 14px; }
|
||||
.assign-table-el :deep(.el-table__empty-block) { min-height: 48px; }
|
||||
.assign-table-el :deep(th.el-table__cell) { color: #262626; font-weight: 600; background: #fafafa; }
|
||||
.assign-table-el :deep(td.el-table__cell) { color: #262626; }
|
||||
.info-table th { background: #fafafa; padding: 10px 12px; text-align: left; color: #262626; font-weight: 600; border-bottom: 1px solid #f0f0f0; white-space: nowrap; }
|
||||
.info-table td { padding: 10px 12px; border-bottom: 1px solid #f5f5f5; color: #262626; vertical-align: middle; }
|
||||
.empty-row { text-align: center; color: #909399; }
|
||||
@@ -404,15 +494,7 @@ onMounted(async () => {
|
||||
.info-form :deep(.el-row) { margin: 0 !important; }
|
||||
.info-form :deep(.el-col) { padding: 0 !important; margin-bottom: 0 !important; }
|
||||
|
||||
/* 服务公司表格: 横向滚动容器 + 字号缩小 */
|
||||
.assign-table-el {
|
||||
max-width: 100% !important;
|
||||
width: 100% !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
.assign-table-el :deep(.el-table__body-wrapper) { overflow-x: auto !important; }
|
||||
|
||||
/* 角色劳务表 */
|
||||
/* 角色劳务表 + 服务公司表 (共用 info-table 原生表格) */
|
||||
.role-labor-table { width: 100% !important; font-size: 13px !important; }
|
||||
.role-labor-table th,
|
||||
.role-labor-table td { padding: 8px 10px !important; font-size: 13px !important; }
|
||||
|
||||
@@ -33,14 +33,6 @@
|
||||
<el-form-item label="总场次/总期数">{{ currentProject?.totalSessions || 0 }}</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="总金额(元)">{{ currentProject?.totalAmount || 0 }}</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="管理费及税金">{{ currentProject?.manageFee || 0 }}</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
@@ -63,6 +55,7 @@
|
||||
<span v-if="singleSessionOver" class="over-warn">⚠ 已超出 {{ assignedSessions - assignForm.totalSessions }} 场</span>
|
||||
<span class="sep">|</span>
|
||||
<span>总金额(元): <b>{{ assignForm.totalAmount || 0 }}</b></span>
|
||||
<span>管理费及税金: <b>¥ {{ manageFeeDisplay }}</b></span>
|
||||
<span :class="{ 'is-over': singleAmountOver }">已分配金额: <b>¥ {{ assignedAmount }}</b></span>
|
||||
<span v-if="singleAmountOver" class="over-warn">⚠ 已超出 ¥ {{ Math.round((assignedAmount - assignForm.totalAmount) * 100) / 100 }}</span>
|
||||
<span class="sep">|</span>
|
||||
@@ -74,38 +67,41 @@
|
||||
<span class="hint">(保存时按各项目自身的总场次和总金额逐项校验)</span>
|
||||
</div>
|
||||
|
||||
<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.executionUnitId" 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 execOptions(row)" :key="u.orgId"
|
||||
:label="u.orgName + (isOrgDisabled(u) ? '(已禁用)' : '')" :value="u.orgId"
|
||||
:disabled="isOrgDisabled(u)" />
|
||||
</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-link :underline="false" type="danger" :disabled="$index === 0 && assignForm.execRows.length === 1" @click="assignForm.execRows.splice($index, 1)">删除</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- 表格: 外层 .table-scroll 提供横向滚动, 表格设置 min-width 总宽避免列被挤成竖条 -->
|
||||
<div class="table-scroll">
|
||||
<el-table :data="assignForm.execRows" border :min-width="640">
|
||||
<el-table-column type="index" label="#" width="50" fixed="left" />
|
||||
<el-table-column label="执行单位" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<el-select v-model="row.executionUnitId" 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 execOptions(row)" :key="u.orgId"
|
||||
:label="u.orgName + (isOrgDisabled(u) ? '(已禁用)' : '')" :value="u.orgId"
|
||||
:disabled="isOrgDisabled(u)" />
|
||||
</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" fixed="right">
|
||||
<template #default="{ $index }">
|
||||
<el-link :underline="false" type="danger" :disabled="$index === 0 && assignForm.execRows.length === 1" @click="assignForm.execRows.splice($index, 1)">删除</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div style="margin-top:8px">
|
||||
<el-link :underline="false" type="primary" @click="addExecRow">+ 增加执行方</el-link>
|
||||
</div>
|
||||
@@ -213,15 +209,13 @@ const assignedAmount = computed(() => {
|
||||
const singleAmountOver = computed(() =>
|
||||
!batchMode && Number(assignForm.totalAmount || 0) > 0 && assignedAmount.value > Number(assignForm.totalAmount)
|
||||
)
|
||||
// 可用金额 (公式: 总金额 × (1 - 管理费/总金额) - 累计劳务 - 累计会务)
|
||||
// 管理费及税金 (只读展示, 来自项目)
|
||||
const manageFeeDisplay = computed(() => Number(currentProject.value?.manageFee || 0))
|
||||
// 可用金额 (纯前端: 总金额 - 管理费及税金 - 已分配金额, 与已结算无关)
|
||||
const availableAmount = computed(() => {
|
||||
if (batchMode) return 0
|
||||
const total = Number(assignForm.totalAmount || 0)
|
||||
if (total <= 0) return 0
|
||||
const manageFee = Number(currentProject.value?.manageFee || 0)
|
||||
const paidLabor = Number(currentProject.value?.paidLaborAmount || 0)
|
||||
const paidMeeting = Number(currentProject.value?.paidMeetingAmount || 0)
|
||||
const avail = total * (1 - manageFee / total) - paidLabor - paidMeeting
|
||||
const avail = total - manageFeeDisplay.value - assignedAmount.value
|
||||
return Math.round(avail * 100) / 100
|
||||
})
|
||||
|
||||
@@ -244,7 +238,6 @@ function resetAssignForm() {
|
||||
function addExecRow() {
|
||||
const row = makeEmptyExecRow()
|
||||
assignForm.execRows.push(row)
|
||||
searchExecutors('', row)
|
||||
}
|
||||
|
||||
// 加载支持方 (sponsor 角色) - 按公司名查, JOIN biz_org + sys_user
|
||||
@@ -292,11 +285,22 @@ async function loadAssigns(projectId) {
|
||||
} else {
|
||||
assignForm.execRows = [makeEmptyExecRow()]
|
||||
}
|
||||
// 每一行立即触发远程搜索, 让 el-select 拿到 _options 才能正确显示已选 label
|
||||
assignForm.execRows.forEach(r => searchExecutors('', r))
|
||||
// 初始化不拉全量: 只对已分配的行按 orgId 拉回 label, 避免下拉闪现无关执行单位
|
||||
assignForm.execRows.forEach(r => loadSelectedExecOption(r))
|
||||
} catch (e) {
|
||||
assignForm.execRows = [makeEmptyExecRow()]
|
||||
assignForm.execRows.forEach(r => searchExecutors('', r))
|
||||
}
|
||||
}
|
||||
|
||||
// 仅按 orgId 拉回已选执行单位的 label (初始化回显用, 不拉全量列表)
|
||||
async function loadSelectedExecOption(row) {
|
||||
if (!row.executionUnitId) return
|
||||
try {
|
||||
const r2 = await listExecutorOrgs({ orgId: row.executionUnitId })
|
||||
const sel = (r2.data || []).find(u => u.orgId === row.executionUnitId)
|
||||
if (sel) row._options = [sel]
|
||||
} catch (e) {
|
||||
row._options = []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -550,6 +554,15 @@ async function submitAssign() {
|
||||
|
||||
.form-actions { display: flex; justify-content: center; gap: 16px; padding: 20px 0 4px; border-top: 1px solid #f0f0f0; margin-top: 16px; }
|
||||
|
||||
/* 表格横向滚动容器: 列宽总和超过容器时滚动, 列不被挤成竖条 */
|
||||
.table-scroll {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.table-scroll::-webkit-scrollbar { height: 6px; }
|
||||
.table-scroll::-webkit-scrollbar-thumb { background: var(--brand-slate-200); border-radius: 3px; }
|
||||
|
||||
/* 章节标题 (复用 ProjectsNew 风格) */
|
||||
.new-card-title {
|
||||
font-size: 14px;
|
||||
@@ -580,82 +593,82 @@ async function submitAssign() {
|
||||
/* ========================================
|
||||
移动端适配 (≤768px)
|
||||
- 卡片 padding 收窄
|
||||
- 工具栏横向滚动
|
||||
- filter-form: label 与输入框横向
|
||||
- 表格字号收紧
|
||||
- 校验栏 / 章节标题 / 按钮区 字号收紧
|
||||
- 表格走 .table-scroll 横向滚动, 不强制压缩列宽
|
||||
======================================== */
|
||||
@media (max-width: 768px) {
|
||||
/* 卡片 padding */
|
||||
.page-card { padding: 12px !important; border-radius: 4px !important; }
|
||||
.breadcrumb { font-size: 12px !important; margin-bottom: 8px !important; }
|
||||
|
||||
/* 工具栏: 横向滚动 */
|
||||
.toolbar {
|
||||
flex-wrap: nowrap !important;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding-bottom: 6px;
|
||||
margin-bottom: 8px !important;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.toolbar::-webkit-scrollbar { height: 4px; }
|
||||
.toolbar::-webkit-scrollbar-thumb { background: var(--brand-slate-200); border-radius: 2px; }
|
||||
.toolbar :deep(.action-btn),
|
||||
.toolbar :deep(.el-button) {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px !important;
|
||||
padding: 0 10px !important;
|
||||
height: 30px !important;
|
||||
}
|
||||
/* info-bar 紧凑 */
|
||||
.info-bar { font-size: 12px !important; padding: 6px 10px !important; }
|
||||
|
||||
/* filter-form: 横向 (label 左 + input 右) */
|
||||
.filter-form {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
align-items: stretch !important;
|
||||
gap: 10px !important;
|
||||
/* 项目核心属性只读区: 单列 + form-item 横向 label + content */
|
||||
.project-meta { padding: 10px 12px !important; }
|
||||
.project-meta :deep(.el-row) { display: block !important; }
|
||||
.project-meta :deep(.el-col) {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
flex: 0 0 100% !important;
|
||||
display: block !important;
|
||||
}
|
||||
.filter-form :deep(.el-form-item) {
|
||||
.project-meta :deep(.el-form-item) {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
align-items: flex-start !important;
|
||||
margin-right: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
margin-bottom: 12px !important;
|
||||
}
|
||||
.filter-form :deep(.el-form-item__label) {
|
||||
float: none !important;
|
||||
width: auto !important;
|
||||
min-width: 80px !important;
|
||||
text-align: right !important;
|
||||
padding: 0 8px 0 0 !important;
|
||||
font-size: 13px !important;
|
||||
color: var(--el-text-color-regular) !important;
|
||||
line-height: 32px !important;
|
||||
height: 32px !important;
|
||||
}
|
||||
.filter-form :deep(.el-form-item__content) {
|
||||
margin-left: 0 !important;
|
||||
line-height: 32px !important;
|
||||
.project-meta :deep(.el-form-item__content) {
|
||||
flex: 1 !important;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
/* 强制所有控件全宽 */
|
||||
.filter-form :deep(.el-select),
|
||||
.filter-form :deep(.el-input),
|
||||
.filter-form :deep(.el-date-editor),
|
||||
.filter-form :deep(.el-button),
|
||||
.filter-form :deep(.el-cascader) {
|
||||
width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
margin-left: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
display: block !important;
|
||||
/* 校验栏: 横向换行, 字号收紧 */
|
||||
.assign-sessions-bar {
|
||||
flex-wrap: wrap !important;
|
||||
gap: 6px 12px !important;
|
||||
padding: 8px 10px !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
.filter-form :deep(.el-button + .el-button) {
|
||||
margin-top: 8px !important;
|
||||
.assign-sessions-bar b { font-size: 13px !important; }
|
||||
.assign-sessions-bar .sep { display: none; }
|
||||
|
||||
/* 章节标题字号收紧 */
|
||||
.new-card-title { font-size: 13px !important; margin: 12px 0 8px !important; }
|
||||
|
||||
/* 表格走横向滚动容器, 列不被挤 */
|
||||
.table-scroll {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: thin;
|
||||
margin: 0 -12px; /* 拉满 page-card 内的可用宽度 */
|
||||
padding: 0 12px;
|
||||
}
|
||||
.table-scroll::-webkit-scrollbar { height: 6px; }
|
||||
.table-scroll::-webkit-scrollbar-thumb { background: var(--brand-slate-200); border-radius: 3px; }
|
||||
:deep(.el-table) { font-size: 12px !important; }
|
||||
|
||||
/* 截止天数 form-item: 横向 (label + content) */
|
||||
:deep(.el-form-item) {
|
||||
display: flex !important;
|
||||
align-items: flex-start !important;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
:deep(.el-form-item__content) {
|
||||
flex: 1 !important;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
/* 表格字号收紧 */
|
||||
:deep(.el-table) { font-size: 12px !important; }
|
||||
/* 底部按钮: 等宽并排 */
|
||||
.form-actions {
|
||||
flex-wrap: wrap !important;
|
||||
gap: 8px !important;
|
||||
padding: 12px 0 4px !important;
|
||||
}
|
||||
.form-actions :deep(.el-button) {
|
||||
flex: 1 1 0 !important;
|
||||
width: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -26,7 +26,7 @@
|
||||
</el-form-item>
|
||||
<el-form-item label="备注"><el-input v-model="q.remark" placeholder="输入备注" clearable /></el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="load">查找</el-button>
|
||||
<el-button type="primary" @click="load">查询</el-button>
|
||||
<el-button @click="reset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
@@ -50,8 +50,10 @@
|
||||
<el-table-column label="设计文件" width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.designFileUrl">
|
||||
<el-link :underline="false" type="primary" @click="openDesign(row)">查看</el-link>
|
||||
<el-link :underline="false" type="primary" @click="downloadFile(row)">下载</el-link>
|
||||
<div class="table-actions">
|
||||
<el-link :underline="false" type="primary" @click="openDesign(row)">查看</el-link>
|
||||
<el-link :underline="false" type="primary" @click="downloadFile(row)">下载</el-link>
|
||||
</div>
|
||||
</template>
|
||||
<span v-else style="color:#c0c4cc">-</span>
|
||||
</template>
|
||||
@@ -69,12 +71,15 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="projectNo" label="项目编号" width="140" align="center" />
|
||||
<el-table-column label="提交时间" width="160" align="center">
|
||||
<template #default="{ row }">{{ row.submitTime || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="160" fixed="right">
|
||||
<template #default="{ row }"><div class="table-actions">
|
||||
<el-link :underline="false" type="primary" @click="goEdit(row)">修改</el-link>
|
||||
<el-link :underline="false" type="primary" @click="onAudit(row)" :class="{ 'is-disabled': row.planStatus !== '0' }">审核</el-link>
|
||||
<el-link v-if="row.isSettled!=='Y'" :underline="false" type="primary" @click="onSettle(row)">结算</el-link>
|
||||
<el-link v-if="row.status === '1'" :underline="false" type="primary" @click="onAudit(row)">审核</el-link>
|
||||
<el-link v-if="row.status === '2' && row.isSettled !== 'Y'" :underline="false" type="primary" @click="onSettle(row)">结算</el-link>
|
||||
</div></template>
|
||||
</el-table-column>
|
||||
</GrTable>
|
||||
@@ -107,27 +112,17 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 批量审核 dialog: 审核结果(通过/退回 radio) + 项目编号(仅通过时必填) + 审核意见(可选), 批量应用于所有选中方案 -->
|
||||
<el-dialog v-model="batchAuditOpen" title="批量审核策划方案" width="500px">
|
||||
<el-form :model="batchAuditForm" :rules="batchAuditRules" ref="batchAuditFormRef" label-width="100px">
|
||||
<el-form-item label="审核结果" prop="result">
|
||||
<el-radio-group v-model="batchAuditForm.result">
|
||||
<el-radio value="2">通过</el-radio>
|
||||
<el-radio value="3">退回</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="batchAuditForm.result === '2'" label="项目编号" prop="projectNo">
|
||||
<el-select v-model="batchAuditForm.projectNo" placeholder="请选择项目编号" filterable style="width:100%">
|
||||
<el-option v-for="opt in projectNoOptions" :key="opt.projectNo" :label="opt.projectNo + (opt.projectName ? ' / ' + opt.projectName : '')" :value="opt.projectNo" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!-- 批量退回 dialog: 只允许退回 (status='3'), 填审核意见, 批量应用于所有选中方案 -->
|
||||
<el-dialog v-model="batchAuditOpen" title="批量退回策划方案" width="500px">
|
||||
<el-form :model="batchAuditForm" label-width="100px">
|
||||
<el-form-item label="审核结果"><span>退回</span></el-form-item>
|
||||
<el-form-item label="审核意见">
|
||||
<el-input v-model="batchAuditForm.opinion" type="textarea" rows="3" placeholder="请填写审核意见 (可选)" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="batchAuditOpen=false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submitBatchAudit">确认</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submitBatchAudit">确认退回</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
@@ -198,13 +193,16 @@ function onSel(rs) { selection.value = rs }
|
||||
|
||||
async function batchSettle() {
|
||||
if (!selection.value.length) return ElMessage.warning('请先勾选行')
|
||||
// 只有通过 (status='2') 且未结算的才可结算
|
||||
const eligible = selection.value.filter(r => r.status === '2' && r.isSettled !== 'Y')
|
||||
if (!eligible.length) return ElMessage.warning('所选方案中没有通过审核且未结算的')
|
||||
submitting.value = true
|
||||
let ok = 0
|
||||
for (const r of selection.value) {
|
||||
for (const r of eligible) {
|
||||
try { await bizUpdate('projectPlan', { planId: r.planId, isSettled: 'Y' }); ok++ } catch {}
|
||||
}
|
||||
submitting.value = false
|
||||
ElMessage.success(`批量结算 ${ok}/${selection.value.length} 条`)
|
||||
ElMessage.success(`批量结算 ${ok}/${eligible.length} 条`)
|
||||
selection.value = []
|
||||
load()
|
||||
}
|
||||
@@ -250,44 +248,29 @@ async function submitAudit() {
|
||||
finally { submitting.value = false }
|
||||
}
|
||||
|
||||
// 批量审核: 与单独审核字段一致 (审核结果/项目编号/审核意见), 批量应用于所有选中方案
|
||||
// 批量退回: 只允许退回 (status='3'), 填审核意见, 批量应用于所有选中方案
|
||||
const batchAuditOpen = ref(false)
|
||||
const batchAuditFormRef = ref(null)
|
||||
const batchAuditForm = reactive({ result: '2', projectNo: '', opinion: '' })
|
||||
const batchAuditRules = reactive({
|
||||
result: [{ required: true, message: '请选择审核结果', trigger: 'change' }],
|
||||
projectNo: [{ required: false }]
|
||||
})
|
||||
// 通过时: projectNo 必填; 退回时: projectNo 非必填且隐藏 (同时清掉校验状态)
|
||||
watch(() => batchAuditForm.result, (val) => {
|
||||
if (val === '2') {
|
||||
batchAuditRules.projectNo = [{ required: true, message: '请填写项目编号', trigger: 'blur' }]
|
||||
} else {
|
||||
batchAuditRules.projectNo = [{ required: false }]
|
||||
batchAuditFormRef.value?.clearValidate(['projectNo'])
|
||||
}
|
||||
})
|
||||
const batchAuditForm = reactive({ opinion: '' })
|
||||
function openBatchAudit() {
|
||||
if (!selection.value.length) return ElMessage.warning('请先勾选行')
|
||||
batchAuditForm.result = '2'
|
||||
batchAuditForm.projectNo = ''
|
||||
// 只对「待审核」(status='1', 即具备「审核」按钮) 的行生效; 其它状态跳过
|
||||
const eligible = selection.value.filter(r => r.status === '1')
|
||||
if (!eligible.length) return ElMessage.warning('所选方案中没有待审核的, 请重新勾选')
|
||||
const skipped = selection.value.length - eligible.length
|
||||
if (skipped > 0) ElMessage.warning(`${skipped} 条不是待审核状态 (无审核按钮), 将被跳过`)
|
||||
batchAuditForm.opinion = ''
|
||||
loadProjectNoOptions()
|
||||
batchAuditOpen.value = true
|
||||
}
|
||||
async function submitBatchAudit() {
|
||||
submitting.value = true
|
||||
try {
|
||||
await batchAuditFormRef.value.validate()
|
||||
submitting.value = true
|
||||
const isPass = batchAuditForm.result === '2'
|
||||
const payload = { status: batchAuditForm.result, auditOpinion: batchAuditForm.opinion }
|
||||
if (isPass) payload.projectNo = batchAuditForm.projectNo
|
||||
const payload = { status: '3', auditOpinion: batchAuditForm.opinion }
|
||||
let ok = 0
|
||||
for (const r of selection.value) {
|
||||
const eligible = selection.value.filter(r => r.status === '1')
|
||||
for (const r of eligible) {
|
||||
try { await bizUpdate('projectPlan', { ...payload, planId: r.planId }); ok++ } catch {}
|
||||
}
|
||||
submitting.value = false
|
||||
ElMessage.success(`批量审核 ${ok}/${selection.value.length} 条`)
|
||||
ElMessage.success(`批量退回 ${ok}/${eligible.length} 条`)
|
||||
batchAuditOpen.value = false
|
||||
selection.value = []
|
||||
load()
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
<el-option label="已结算" value="Y" /><el-option label="未结算" value="N" />
|
||||
</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-item><el-button type="primary" @click="load">查询</el-button><el-button @click="reset">重置</el-button></el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="toolbar">
|
||||
@@ -91,23 +91,27 @@
|
||||
>
|
||||
<el-table-column type="selection" width="44" />
|
||||
<el-table-column prop="projectNo" label="项目编号" width="170" 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="projectName" label="项目名称" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-link :underline="false" type="primary" @click="viewDetail(row)">{{ row.projectName }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="totalSessions" label="总场次/总期数" width="130" 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="managerScore" label="执行单位评分(合规)" width="140" align="center">
|
||||
<el-table-column prop="managerScore" label="执行单位评分(合规)" width="170" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.managerScore != null">{{ row.managerScore }}</span>
|
||||
<el-link v-if="row.managerScore != null" :underline="false" type="primary" @click="openScoreDetail(row, 'manager')">{{ row.managerScore }}</el-link>
|
||||
<span v-else style="color:#c0c4cc">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sponsorScore" label="执行单位评分(支持)" width="140" align="center">
|
||||
<el-table-column prop="sponsorScore" label="执行单位评分(支持)" width="170" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.sponsorScore != null">{{ row.sponsorScore }}</span>
|
||||
<el-link v-if="row.sponsorScore != null" :underline="false" type="primary" @click="openScoreDetail(row, 'sponsor')">{{ row.sponsorScore }}</el-link>
|
||||
<span v-else style="color:#c0c4cc">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -120,12 +124,12 @@
|
||||
<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 prop="startTime" label="项目开始时间" width="170" align="center" :formatter="fmtDateTime" />
|
||||
<el-table-column prop="endTime" label="项目结束时间" width="170" align="center" :formatter="fmtDateTime" />
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="{ row }"><div class="table-actions">
|
||||
<div class="op-cell">
|
||||
<el-link :underline="false" type="primary" @click="doCreateMeeting(row)">建会</el-link>
|
||||
<el-link v-if="!isFinishedRow(row)" :underline="false" type="primary" @click="doCreateMeeting(row)">建会</el-link>
|
||||
<el-link :underline="false" type="primary" :disabled="isFinishedRow(row)" @click="doAssign(row)">分配</el-link>
|
||||
<el-dropdown trigger="hover" @command="(cmd) => onAction(cmd, row)">
|
||||
<el-link :underline="false" type="primary" class="op-dropdown">
|
||||
@@ -194,7 +198,8 @@
|
||||
</el-dialog>
|
||||
|
||||
<!-- ============= 单行结题 modal ============= -->
|
||||
<el-dialog v-model="closeModalOpen" title="您确定要结题吗?" width="420px" :show-close="false">
|
||||
<el-dialog v-model="closeModalOpen" title="结题确认" width="420px" :show-close="false">
|
||||
<p style="color:#606266">你确定要结题吗?</p>
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="confirmClose">确定</el-button>
|
||||
<el-button @click="closeModalOpen=false">取消</el-button>
|
||||
@@ -240,6 +245,29 @@
|
||||
<el-button @click="singleScoreModalOpen=false">取消</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- ============= 评分详情 modal(只读 4 维度明细, 点列表列查看) ============= -->
|
||||
<el-dialog v-model="scoreDetailOpen" :title="scoreDetailTitle" width="520px" :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:110px;padding:10px 12px;text-align:center">平均得分</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">{{ scoreDetail.qualityScore ?? '-' }}</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">{{ scoreDetail.responseScore ?? '-' }}</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">{{ scoreDetail.cooperationScore ?? '-' }}</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">{{ scoreDetail.complianceScore ?? '-' }}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div style="margin-top:12px;text-align:right;font-size:13px;color:#303133">
|
||||
平均分: <b>{{ scoreDetailTotal }}</b>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="scoreDetailOpen=false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -265,8 +293,12 @@ const page = reactive({ pageNum: 1, pageSize: 20, total: 0 })
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = { ...q.value, pageNum: page.pageNum, pageSize: page.pageSize }
|
||||
// 时间范围含两端: 开始日 00:00:00 ~ 结束日 23:59:59
|
||||
if (params.startTime) params.startTime = params.startTime + ' 00:00:00'
|
||||
if (params.endTime) params.endTime = params.endTime + ' 23:59:59'
|
||||
// axios 拦截器已将 TableDataInfo 归一化为 {code, msg, data: {total, rows}}
|
||||
const { data } = await bizList('project', { ...q.value, pageNum: page.pageNum, pageSize: page.pageSize })
|
||||
const { data } = await bizList('project', params)
|
||||
rows.value = data?.rows || []
|
||||
page.total = data?.total || 0
|
||||
} catch (e) {
|
||||
@@ -302,13 +334,9 @@ 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` +
|
||||
`此操作将级联软删除:\n` +
|
||||
`· 项目主表 + 方案 + 执行方分配 + 支持方分配 + 评分 (5 张表)\n` +
|
||||
`· 项目下所有会议 + 会议附件 + 参会人 + 监督员 + 执行人 + 审计日志 (6 张表)\n\n` +
|
||||
`数据保留在库, 不再展示给前台用户, 审计追溯可查。`,
|
||||
`确认删除项目"${row.projectName}"?`,
|
||||
'删除确认',
|
||||
{ type: 'error', confirmButtonText: '确认软删除', cancelButtonText: '取消' }
|
||||
{ type: 'error', confirmButtonText: '确认删除', cancelButtonText: '取消' }
|
||||
)
|
||||
await bizDelete('project', row.projectId)
|
||||
ElMessage.success('已删除')
|
||||
@@ -325,11 +353,7 @@ async function onBatchDelete() {
|
||||
if (!selection.value.length) return ElMessage.warning('请先勾选项目')
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认批量软删除选中的 ${selection.value.length} 个项目?\n\n` +
|
||||
`此操作将级联软删除每个项目下的:\n` +
|
||||
`· 5 张项目表 (主表/方案/分配/支持方/评分)\n` +
|
||||
`· 6 张会议表 (会议本身 + 附件 + 参会人 + 监督员 + 执行人 + 审计日志)\n\n` +
|
||||
`数据保留, 不再展示给前台用户。`,
|
||||
`确认删除选中的 ${selection.value.length} 个项目?`,
|
||||
'批量删除确认',
|
||||
{ type: 'error', confirmButtonText: `确认删除 ${selection.value.length} 个`, cancelButtonText: '取消' }
|
||||
)
|
||||
@@ -337,7 +361,7 @@ async function onBatchDelete() {
|
||||
const ids = selection.value.map(r => r.projectId)
|
||||
try {
|
||||
await bizDelete('project', ids)
|
||||
ElMessage.success(`已批量软删除 ${ids.length} 个项目`)
|
||||
ElMessage.success(`已删除 ${ids.length} 个项目`)
|
||||
selection.value = []
|
||||
load()
|
||||
} catch (e) {
|
||||
@@ -435,9 +459,7 @@ async function confirmSingleScore() {
|
||||
const avg = ((f.qualityScore + f.responseScore + f.cooperationScore + f.complianceScore) / 4).toFixed(1)
|
||||
const projectId = singleScoreTargetRow.value.projectId
|
||||
try {
|
||||
// 1. 写聚合分到 biz_project.manager_score
|
||||
await bizUpdate('project', { projectId, managerScore: Number(avg) })
|
||||
// 2. 写 4 维度明细到 biz_project_rating (rater_role='manager', 后端自动填 raterId)
|
||||
// 写 4 维度明细到 biz_project_rating (rater_role='manager', 后端自动填 raterId + 重算聚合分)
|
||||
await request.post('/business/project/rate', {
|
||||
projectId,
|
||||
raterRole: 'manager',
|
||||
@@ -453,7 +475,32 @@ async function confirmSingleScore() {
|
||||
ElMessage.error(e?.msg || '评分失败')
|
||||
}
|
||||
}
|
||||
function viewDetail(row) { router.push(`/manager/projects/detail/${row.projectId}`) }
|
||||
function viewDetail(row) {
|
||||
// admin 复用本列表页, 详情须跳到 /admin/projects/detail (否则被 beforeEach 角色守卫拦截)
|
||||
const base = userStore.role === 'admin' ? '/admin' : '/manager'
|
||||
router.push(`${base}/projects/detail/${row.projectId}`)
|
||||
}
|
||||
|
||||
// 点击列表「执行单位评分(合规/支持)」列 → 只读显示该角色 4 维度评分明细
|
||||
async function openScoreDetail(row, role) {
|
||||
scoreDetailTitle.value = role === 'sponsor' ? '执行单位评分详情(支持方)' : '执行单位评分详情(合规)'
|
||||
Object.assign(scoreDetail, { qualityScore: null, responseScore: null, cooperationScore: null, complianceScore: null })
|
||||
scoreDetailTotal.value = role === 'sponsor' ? (row.sponsorScore ?? '—') : (row.managerScore ?? '—')
|
||||
scoreDetailOpen.value = true
|
||||
try {
|
||||
const r = await request({ url: '/business/project/ratings', method: 'get', params: { projectId: row.projectId } })
|
||||
const list = (r.data && (Array.isArray(r.data) ? r.data : r.data.rows)) || r.rows || []
|
||||
const items = list.filter(x => String(x.projectId) === String(row.projectId) && x.raterRole === role)
|
||||
if (items.length) {
|
||||
const n = items.length
|
||||
const dimAvg = (key) => (items.reduce((s, x) => s + (Number(x[key]) || 0), 0) / n).toFixed(1)
|
||||
scoreDetail.qualityScore = dimAvg('qualityScore')
|
||||
scoreDetail.responseScore = dimAvg('responseScore')
|
||||
scoreDetail.cooperationScore = dimAvg('cooperationScore')
|
||||
scoreDetail.complianceScore = dimAvg('complianceScore')
|
||||
}
|
||||
} catch (e) { /* 拉取失败保持空, 弹窗显示 - */ }
|
||||
}
|
||||
|
||||
// 金额格式化: 1234567.5 -> "1,234,567.50"
|
||||
function fmtMoney(row, col, v) {
|
||||
@@ -462,12 +509,10 @@ function fmtMoney(row, col, 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) {
|
||||
// 日期时间格式化: 展示完整 yyyy-MM-dd HH:mm:ss (后端 @JsonFormat 已序列化, 直接透传)
|
||||
function fmtDateTime(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
|
||||
return String(v)
|
||||
}
|
||||
|
||||
const selection = ref([])
|
||||
@@ -491,6 +536,12 @@ const singleScoreModalOpen = ref(false)
|
||||
const singleScoreTargetRow = ref(null)
|
||||
const singleScoreForm = reactive({ qualityScore: 0, responseScore: 0, cooperationScore: 0, complianceScore: 0 })
|
||||
|
||||
// ========== 评分详情 (只读 4 维度明细, 点列表 score 列查看) ==========
|
||||
const scoreDetailOpen = ref(false)
|
||||
const scoreDetailTitle = ref('评分详情')
|
||||
const scoreDetail = reactive({ qualityScore: null, responseScore: null, cooperationScore: null, complianceScore: null })
|
||||
const scoreDetailTotal = ref('—')
|
||||
|
||||
// 新建项目已迁到独立页面 /manager/projects/new (ManagerProjectsNew.vue)
|
||||
function openNewProject() {
|
||||
router.push('/manager/projects/new')
|
||||
@@ -568,10 +619,6 @@ async function confirmBatch() {
|
||||
// 共享评分: 所有项目用同一组 4 维度
|
||||
for (const r of selection.value) {
|
||||
try {
|
||||
await bizUpdate('project', {
|
||||
projectId: r.projectId,
|
||||
managerScore: Number(total)
|
||||
})
|
||||
await request.post('/business/project/rate', {
|
||||
projectId: r.projectId,
|
||||
raterRole: 'manager',
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<el-form-item label="项目负责人">
|
||||
<el-select v-model="form.leadUserId" placeholder="请选择合规管理员" filterable clearable :filter-method="searchManagers" style="width:100%">
|
||||
<el-option v-for="u in managerOptions" :key="u.userId"
|
||||
:label="`${u.userName}${u.nickName ? ' (' + u.nickName + ')' : ''}`" :value="u.userId" />
|
||||
:label="`${u.nickName || u.userName}${u.nickName && u.userName ? ' (' + u.userName + ')' : ''}`" :value="u.userId" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -51,19 +51,19 @@
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="总场次/总期数" prop="totalSessions">
|
||||
<el-input-number v-model="form.totalSessions" :min="1" style="width:100%" />
|
||||
<div class="num-input"><el-input-number v-model="form.totalSessions" :min="1" /></div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="总金额(元)" prop="totalAmount">
|
||||
<el-input-number v-model="form.totalAmount" :min="0" :precision="2" style="width:100%" />
|
||||
<div class="num-input"><el-input-number v-model="form.totalAmount" :min="0" :precision="2" /></div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="管理费(元)">
|
||||
<el-input-number v-model="form.manageFee" :min="0" :precision="2" style="width:100%" />
|
||||
<div class="num-input"><el-input-number v-model="form.manageFee" :min="0" :precision="2" /></div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@@ -83,14 +83,16 @@
|
||||
<!-- ========== 第二区块:角色劳务设置 ========== -->
|
||||
<div class="new-card-title">角色劳务设置</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>
|
||||
<div class="role-select">
|
||||
<el-select v-model="r.role" placeholder="请选择" @change="onRoleSelectChange(r)">
|
||||
<el-option label="主席" value="主席" />
|
||||
<el-option label="主持" value="主持" />
|
||||
<el-option label="讲者" value="讲者" />
|
||||
<el-option label="点评/评审" value="点评/评审" />
|
||||
<el-option label="讨论" value="讨论" />
|
||||
<el-option label="其他" value="其他" />
|
||||
</el-select>
|
||||
</div>
|
||||
<el-input v-if="r.role === '其他'" v-model="r.customName" placeholder="角色名" class="role-custom" />
|
||||
<el-input-number v-model="r.amount" :min="0" :precision="2" placeholder="劳务金额" class="role-amount" />
|
||||
<div class="row-actions">
|
||||
@@ -104,15 +106,16 @@
|
||||
<div class="notice-list">
|
||||
<div v-for="(n, idx) in form.notices" :key="idx" class="notice-row">
|
||||
<span class="notice-label">{{ n.label }}</span>
|
||||
<oss-file-uploader
|
||||
class="notice-uploader"
|
||||
v-model="n.url"
|
||||
:dir="`ry8080/project/${n.key}/`"
|
||||
accept=".pdf,.png,.jpg,.jpeg,.gif,.webp"
|
||||
:placeholder="`点击上传${n.label}`"
|
||||
hint="支持 PDF / 图片"
|
||||
block
|
||||
/>
|
||||
<div class="notice-uploader">
|
||||
<oss-file-uploader
|
||||
v-model="n.url"
|
||||
:dir="`ry8080/project/${n.key}/`"
|
||||
accept=".pdf,.png,.jpg,.jpeg,.gif,.webp"
|
||||
:placeholder="`点击上传${n.label}`"
|
||||
hint="支持 PDF / 图片"
|
||||
block
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -397,9 +400,14 @@ async function submit(mode = 'save') {
|
||||
.hint-text p { margin-bottom: 4px; }
|
||||
.hint-text p:last-child { margin-bottom: 0; }
|
||||
.role-row { display: flex; align-items: center; gap: 12px; padding: 6px 0; }
|
||||
.role-select { width: 160px; flex-shrink: 0; display: flex; align-items: center; }
|
||||
.role-custom { flex: 1; max-width: 180px; }
|
||||
.role-amount { width: 180px; }
|
||||
.row-actions { display: flex; gap: 8px; }
|
||||
/* 项目信息数字输入框: 桌面限制宽度, 不撑满双列 */
|
||||
.num-input { width: 200px; max-width: 100%; }
|
||||
/* 公告文件上传: 限制最大宽度, 上传按钮不要太长 */
|
||||
.notice-uploader { flex: 1; min-width: 0; max-width: 480px; }
|
||||
.notice-list { display: flex; flex-direction: column; gap: 10px; }
|
||||
.notice-row { display: flex; align-items: stretch; gap: 12px; margin-bottom: 12px; }
|
||||
.notice-label {
|
||||
@@ -437,82 +445,79 @@ async function submit(mode = 'save') {
|
||||
/* ========================================
|
||||
移动端适配 (≤768px)
|
||||
- 卡片 padding 收窄
|
||||
- 工具栏横向滚动
|
||||
- filter-form: label 与输入框横向
|
||||
- 表格字号收紧
|
||||
- el-row 双列 → 单列, form-item 横向 (label-width=120px 桌面默认沿用, 让 Element Plus 自然对齐)
|
||||
- 角色劳务行 / 公告文件行 / 底部按钮: 紧凑布局
|
||||
======================================== */
|
||||
@media (max-width: 768px) {
|
||||
/* 卡片 padding */
|
||||
.page-card { padding: 12px !important; border-radius: 4px !important; }
|
||||
.manager-projects-new { padding: 12px !important; border-radius: 4px !important; }
|
||||
.breadcrumb { font-size: 12px !important; margin-bottom: 8px !important; }
|
||||
|
||||
/* 工具栏: 横向滚动 */
|
||||
.toolbar {
|
||||
flex-wrap: nowrap !important;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding-bottom: 6px;
|
||||
margin-bottom: 8px !important;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.toolbar::-webkit-scrollbar { height: 4px; }
|
||||
.toolbar::-webkit-scrollbar-thumb { background: var(--brand-slate-200); border-radius: 2px; }
|
||||
.toolbar :deep(.action-btn),
|
||||
.toolbar :deep(.el-button) {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px !important;
|
||||
padding: 0 10px !important;
|
||||
height: 30px !important;
|
||||
/* 章节标题间距收紧 */
|
||||
.new-card-title { font-size: 13px !important; margin: 12px 0 8px !important; }
|
||||
|
||||
/* 双列 → 单列: el-row 强制 block, el-col 100% 宽 */
|
||||
.project-form :deep(.el-row) { display: block !important; }
|
||||
.project-form :deep(.el-col) {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
flex: 0 0 100% !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* filter-form: 横向 (label 左 + input 右) */
|
||||
.filter-form {
|
||||
/* form-item: 横向 label + content, flex-start 让 error message 占独立行
|
||||
不动 __label 的 float/width/height/line-height — Element Plus 自带 label-width=120px
|
||||
自然右对齐 + 跟随 content 行高, 不会参差不齐 */
|
||||
.project-form :deep(.el-form-item) {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
align-items: stretch !important;
|
||||
gap: 10px !important;
|
||||
}
|
||||
.filter-form :deep(.el-form-item) {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
align-items: flex-start !important;
|
||||
margin-right: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.filter-form :deep(.el-form-item__label) {
|
||||
float: none !important;
|
||||
width: auto !important;
|
||||
min-width: 80px !important;
|
||||
text-align: right !important;
|
||||
padding: 0 8px 0 0 !important;
|
||||
font-size: 13px !important;
|
||||
color: var(--el-text-color-regular) !important;
|
||||
line-height: 32px !important;
|
||||
height: 32px !important;
|
||||
}
|
||||
.filter-form :deep(.el-form-item__content) {
|
||||
margin-left: 0 !important;
|
||||
line-height: 32px !important;
|
||||
.project-form :deep(.el-form-item__content) {
|
||||
flex: 1 !important;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
/* 强制所有控件全宽 */
|
||||
.filter-form :deep(.el-select),
|
||||
.filter-form :deep(.el-input),
|
||||
.filter-form :deep(.el-date-editor),
|
||||
.filter-form :deep(.el-button),
|
||||
.filter-form :deep(.el-cascader) {
|
||||
/* 表单控件全宽, 强制拉伸 (项目信息 section 的输入控件全宽)
|
||||
注意: .role-row 内的控件有 .role-select/.role-amount 容器保护, 不会吃这条规则 */
|
||||
.project-form :deep(.el-form-item .el-select),
|
||||
.project-form :deep(.el-form-item .el-input),
|
||||
.project-form :deep(.el-form-item .el-textarea),
|
||||
.project-form :deep(.el-form-item .el-date-editor),
|
||||
.project-form :deep(.el-form-item .el-input-number),
|
||||
.project-form :deep(.el-form-item .el-cascader) {
|
||||
width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
margin-left: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
display: block !important;
|
||||
}
|
||||
.filter-form :deep(.el-button + .el-button) {
|
||||
margin-top: 8px !important;
|
||||
}
|
||||
|
||||
/* 表格字号收紧 */
|
||||
:deep(.el-table) { font-size: 12px !important; }
|
||||
/* 数字输入框: 移动端占满 (有 .num-input 容器保护, 不会挤压其它数字输入) */
|
||||
.project-form .num-input { width: 100% !important; }
|
||||
|
||||
/* 角色劳务行: 保持桌面原有横向布局 (角色/金额/删除) 一行, 不被表单规则拉宽 */
|
||||
.role-row { flex-wrap: wrap !important; gap: 6px !important; }
|
||||
.role-row .role-select { width: 110px !important; flex-shrink: 0; display: flex !important; align-items: center !important; }
|
||||
.role-row .role-amount { width: 130px !important; flex-shrink: 0; }
|
||||
.role-row .row-actions { margin-left: auto; }
|
||||
|
||||
/* 公告文件行: 横向 (label + uploader), label 顶部对齐 */
|
||||
.notice-row { gap: 8px !important; margin-bottom: 10px !important; }
|
||||
.notice-label {
|
||||
width: 70px !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
/* 移动端: uploader 限制固定宽度 280px (不撑满不溢出) */
|
||||
.notice-uploader { width: 280px !important; max-width: 100% !important; min-width: 0 !important; flex: none !important; }
|
||||
|
||||
/* 底部按钮: 等宽并排 */
|
||||
.form-actions :deep(.el-form-item__content) {
|
||||
display: flex !important;
|
||||
flex-wrap: wrap !important;
|
||||
gap: 8px !important;
|
||||
}
|
||||
.form-actions :deep(.el-button) {
|
||||
flex: 1 1 0 !important;
|
||||
width: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -9,15 +9,8 @@
|
||||
<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 label="手机号"><el-input v-model="q.phone" placeholder="输入手机号" clearable /></el-form-item>
|
||||
<el-form-item label="审核状态">
|
||||
<el-select v-model="q.intentStatus" clearable style="width: 130px">
|
||||
<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 type="primary" @click="load">查询</el-button>
|
||||
<el-button @click="reset">重置</el-button>
|
||||
<el-button type="success" @click="doExport">导出</el-button>
|
||||
</el-form-item>
|
||||
@@ -72,7 +65,7 @@ import {
|
||||
exportPublicitySupportIntent
|
||||
} from '@/api/public'
|
||||
|
||||
const q = ref({ projectNo: '', projectName: '', name: '', workUnit: '', phone: '', intentStatus: '' })
|
||||
const q = ref({ projectNo: '', projectName: '', name: '', workUnit: '', phone: '' })
|
||||
const rows = ref([])
|
||||
const loading = ref(false)
|
||||
const page = reactive({ pageNum: 1, pageSize: 20, total: 0 })
|
||||
@@ -91,7 +84,7 @@ async function load() {
|
||||
}
|
||||
|
||||
function reset() {
|
||||
q.value = { projectNo: '', projectName: '', name: '', workUnit: '', phone: '', intentStatus: '' }
|
||||
q.value = { projectNo: '', projectName: '', name: '', workUnit: '', phone: '' }
|
||||
page.pageNum = 1
|
||||
load()
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<div class="stat-label">已结题数量</div>
|
||||
<div class="stat-value">{{ stats.completedProjects }}</div>
|
||||
</router-link>
|
||||
<router-link class="stat-card" to="/manager/meetings?currentStage=RUNNING">
|
||||
<router-link class="stat-card" to="/manager/meetings?currentStageNotIn=NOT_STARTED,IN_PROGRESS">
|
||||
<div class="stat-label">已执行会议</div>
|
||||
<div class="stat-value">{{ stats.executedMeetings }}</div>
|
||||
</router-link>
|
||||
@@ -29,7 +29,7 @@
|
||||
<!-- 消息通知 (NoticeList 组件内部已含 SSE 实时刷新 + 详情 dialog + 全部已读) -->
|
||||
<section class="section">
|
||||
<h2 class="section-title">消息通知<a class="more" @click.prevent="$router.push('/manager/messages')">更多 →</a></h2>
|
||||
<NoticeList :limit="50" />
|
||||
<NoticeList :pageable="true" :show-header="false" />
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
@@ -52,13 +52,11 @@ async function loadStats() {
|
||||
// 已结题项目
|
||||
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
|
||||
// 已执行会议 = currentStage='RUNNING' (阶段已过开始时间, 执行方未提交)
|
||||
const em = await bizList('meeting', { pageNum: 1, pageSize: 1, currentStage: 'RUNNING' })
|
||||
// 会议统计口径 (与 sponsor/Home 对齐): 未执行=NOT_STARTED; 已执行=排除 NOT_STARTED/IN_PROGRESS 的其余 stage (含 FROZEN)
|
||||
const em = await bizList('meeting', { pageNum: 1, pageSize: 1, currentStageNotIn: 'NOT_STARTED,IN_PROGRESS' })
|
||||
stats.value.executedMeetings = em.total || em.data?.total || 0
|
||||
stats.value.pendingMeetings = meetingTotal - stats.value.executedMeetings
|
||||
const pm = await bizList('meeting', { pageNum: 1, pageSize: 1, currentStage: 'NOT_STARTED' })
|
||||
stats.value.pendingMeetings = pm.total || pm.data?.total || 0
|
||||
// 已结算项目
|
||||
const sp = await bizList('project', { pageNum: 1, pageSize: 1, isSettled: 'Y' })
|
||||
stats.value.settledProjects = sp.total || sp.data?.total || 0
|
||||
|
||||
@@ -16,12 +16,13 @@
|
||||
<!-- 1. 会议基本信息 -->
|
||||
<div class="card">
|
||||
<div class="section-title">会议基本信息</div>
|
||||
|
||||
<div class="info-grid" v-loading="loading">
|
||||
<div class="info-row"><span class="info-label">项目编号:</span><span class="info-value code">{{ row.projectNo || '-' }}</span></div>
|
||||
<div class="info-row"><span class="info-label">会议名称:</span><span class="info-value">{{ row.meetingName || '-' }}</span></div>
|
||||
<div class="info-row"><span class="info-label">项目名称:</span><span class="info-value">{{ row.projectName || '-' }}</span></div>
|
||||
<div class="info-row"><span class="info-label">期数:</span><span class="info-value">{{ periodDisplay }}</span></div>
|
||||
<div class="info-row"><span class="info-label">总场次/总期数:</span><span class="info-value">{{ row.totalPeriods ?? '-' }}</span></div>
|
||||
<div class="info-row"><span class="info-label">总场次/总期数:</span><span class="info-value">{{ displayTotalPeriods ?? '-' }}</span></div>
|
||||
<div class="info-row"><span class="info-label">会议开始时间:</span><span class="info-value">{{ fmtDateTime(row.startTime) }}</span></div>
|
||||
<div class="info-row"><span class="info-label">支持单位:</span><span class="info-value">{{ row.orgName || '-' }}</span></div>
|
||||
<div class="info-row"><span class="info-label">会议结束时间:</span><span class="info-value">{{ fmtDateTime(row.endTime) }}</span></div>
|
||||
@@ -81,7 +82,11 @@
|
||||
<el-table :data="attendeeRows" v-loading="attendeeLoading" border size="small" style="width: 100%;" show-summary :summary-method="attendeeSummary" @selection-change="onAttendeeSelectionChange" class="attendee-table">
|
||||
<el-table-column v-if="!isSponsor && !isReadonly" type="selection" width="42" />
|
||||
<el-table-column type="index" label="序号" width="50" align="center" />
|
||||
<el-table-column prop="name" label="医生" min-width="90" />
|
||||
<el-table-column label="医生" min-width="90">
|
||||
<template #default="{ row }">
|
||||
<span :style="(row.hasIntent === 1 || row.hasIntent === '1') ? {} : { color: '#f56c6c' }">{{ row.name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="联系电话" width="120">
|
||||
<template #default="{ row }">{{ isSponsor ? maskPhone(row.phone) : row.phone }}</template>
|
||||
</el-table-column>
|
||||
@@ -125,10 +130,10 @@
|
||||
<span v-else class="text-muted">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="现场照片" min-width="140" show-overflow-tooltip>
|
||||
<el-table-column label="现场照片" min-width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="!row.onSitePhotos" class="text-muted">-</span>
|
||||
<span v-else>{{ row.onSitePhotos.split(',').length }} 张</span>
|
||||
<el-link :underline="false" v-if="onSitePhotoFiles(row).length" type="primary" @click="openOnSitePhotos(row)">查看</el-link>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="签字" width="70" align="center">
|
||||
@@ -299,71 +304,35 @@
|
||||
<div class="timeline-title">会议已执行</div>
|
||||
<div class="timeline-desc">{{ nodeDesc('PRE') }}</div>
|
||||
</div>
|
||||
<!-- 材料审核三步 (提交 → 合规 → 监察) 并入主时间轴, 两轨并列, 保留重交历史 -->
|
||||
<div :class="['timeline-item', nodeStatus('submit')]">
|
||||
<!-- 材料审核: 单条时间轴, 按轮次循环 (提交 → 合规 → 监察 → 提交 → …), 节点上标注 劳务/会务 -->
|
||||
<div v-if="!timelineEvents.length && !pendingNode" class="timeline-item pending">
|
||||
<div class="timeline-title">执行方提交材料</div>
|
||||
<div v-if="!materialTracks.length" class="timeline-desc pending-text">待执行人员提交</div>
|
||||
<div v-for="track in materialTracks" :key="track.key" class="track-block">
|
||||
<div class="track-label-row">
|
||||
<el-tag v-if="track.label" size="small" effect="plain">{{ track.label }}</el-tag>
|
||||
<span v-if="!track.cycles.length" class="pending-text">待提交</span>
|
||||
<div class="timeline-desc pending-text">待执行人员提交</div>
|
||||
</div>
|
||||
<template v-for="(ev, idx) in timelineEvents" :key="'ev' + idx">
|
||||
<div :class="['timeline-item', eventStatus(ev)]">
|
||||
<div class="timeline-title">
|
||||
{{ stepLabel(ev.step) }}
|
||||
<span v-if="trackTag(ev)" class="track-tag">{{ trackTag(ev) }}</span>
|
||||
</div>
|
||||
<div v-for="(c, i) in track.cycles" :key="i" class="timeline-meta">
|
||||
<span v-if="track.cycles.length > 1" class="cycle-no">第{{ i + 1 }}次</span>
|
||||
<span>{{ c.submit.auditor }}</span>
|
||||
<div v-for="(e, i) in ev.entries" :key="i" class="timeline-meta">
|
||||
<span v-if="ev.entries.length > 1" class="track-mini">{{ e.label }}</span>
|
||||
<span>{{ e.auditor || '—' }}</span>
|
||||
<span class="dot">·</span>
|
||||
<span>{{ fmtDateTime(c.submit.auditTime) }}</span>
|
||||
<span>{{ fmtDateTime(e.auditTime) }}</span>
|
||||
<el-tag v-if="e.auditResult" size="small" :type="e.auditResult === 'REJECTED' ? 'danger' : 'success'">{{ e.auditResult === 'REJECTED' ? '拒绝' : '通过' }}</el-tag>
|
||||
</div>
|
||||
<div v-for="(e, i) in ev.entries" :key="'op' + i">
|
||||
<div v-if="e.opinion" :class="['timeline-opinion', e.auditResult === 'REJECTED' ? 'opinion-reject' : 'opinion-approve']">💬 {{ e.opinion }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="['timeline-item', nodeStatus('compliance')]">
|
||||
<div class="timeline-title">合规审核</div>
|
||||
<div v-if="!materialTracks.length" class="timeline-desc pending-text">待合规审核</div>
|
||||
<div v-for="track in materialTracks" :key="track.key" class="track-block">
|
||||
<div class="track-label-row">
|
||||
<el-tag v-if="track.label" size="small" effect="plain">{{ track.label }}</el-tag>
|
||||
<span v-if="!track.cycles.length" class="pending-text">—</span>
|
||||
</div>
|
||||
<template v-for="(c, i) in track.cycles" :key="i">
|
||||
<div class="timeline-meta">
|
||||
<span v-if="track.cycles.length > 1" class="cycle-no">第{{ i + 1 }}次</span>
|
||||
<template v-if="c.compliance">
|
||||
<span>{{ c.compliance.auditor }}</span>
|
||||
<span class="dot">·</span>
|
||||
<span>{{ fmtDateTime(c.compliance.auditTime) }}</span>
|
||||
<el-tag size="small" :type="c.compliance.auditResult === 'REJECTED' ? 'danger' : 'success'">{{ c.compliance.auditResult === 'REJECTED' ? '拒绝' : '通过' }}</el-tag>
|
||||
</template>
|
||||
<span v-else class="pending-text">待审核</span>
|
||||
</div>
|
||||
<div v-if="c.compliance?.opinion" class="timeline-opinion">💬 {{ c.compliance.opinion }}</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="['timeline-item', nodeStatus('supervision')]">
|
||||
<div class="timeline-title">监察意见</div>
|
||||
<div v-if="!materialTracks.length" class="timeline-desc pending-text">待监察审核</div>
|
||||
<div v-for="track in materialTracks" :key="track.key" class="track-block">
|
||||
<div class="track-label-row">
|
||||
<el-tag v-if="track.label" size="small" effect="plain">{{ track.label }}</el-tag>
|
||||
<span v-if="!track.cycles.length" class="pending-text">—</span>
|
||||
</div>
|
||||
<template v-for="(c, i) in track.cycles" :key="i">
|
||||
<div class="timeline-meta">
|
||||
<span v-if="track.cycles.length > 1" class="cycle-no">第{{ i + 1 }}次</span>
|
||||
<template v-if="c.compliance && c.compliance.auditResult === 'REJECTED'">
|
||||
<span class="pending-text">已退回 · 未进入监察</span>
|
||||
</template>
|
||||
<template v-else-if="c.supervision">
|
||||
<span>{{ c.supervision.auditor }}</span>
|
||||
<span class="dot">·</span>
|
||||
<span>{{ fmtDateTime(c.supervision.auditTime) }}</span>
|
||||
<el-tag size="small" :type="c.supervision.auditResult === 'REJECTED' ? 'danger' : 'success'">{{ c.supervision.auditResult === 'REJECTED' ? '拒绝' : '通过' }}</el-tag>
|
||||
</template>
|
||||
<span v-else class="pending-text">待监察审核</span>
|
||||
</div>
|
||||
<div v-if="c.supervision?.opinion" class="timeline-opinion">💬 {{ c.supervision.opinion }}</div>
|
||||
</template>
|
||||
</template>
|
||||
<div v-if="pendingNode" class="timeline-item pending">
|
||||
<div class="timeline-title">
|
||||
{{ stepLabel(pendingNode.step) }}
|
||||
<span v-if="trackTag(pendingNode)" class="track-tag">{{ trackTag(pendingNode) }}</span>
|
||||
</div>
|
||||
<div class="timeline-desc pending-text">{{ pendingDesc(pendingNode.step) }}</div>
|
||||
</div>
|
||||
<!-- 节点 3/4: 结算 / 完结 -->
|
||||
<div :class="['timeline-item', fixedNodeStatus('POST')]">
|
||||
@@ -622,9 +591,16 @@ function fmtDateTime(v) {
|
||||
if (isNaN(dt.getTime())) return v
|
||||
return `${dt.getFullYear()}-${pad(dt.getMonth() + 1)}-${pad(dt.getDate())} ${pad(dt.getHours())}:${pad(dt.getMinutes())}`
|
||||
}
|
||||
// 执行方看到的"总期数"= 分配给本执行方的场次 (assignedSessions), 而非项目总场次 (total_periods); 其他角色仍用项目总场次
|
||||
const displayTotalPeriods = computed(() => {
|
||||
if (currentRole.value === 'executor') {
|
||||
return row.value.assignedSessions != null ? row.value.assignedSessions : row.value.totalPeriods
|
||||
}
|
||||
return row.value.totalPeriods
|
||||
})
|
||||
const periodDisplay = computed(() => {
|
||||
const p = row.value.periodNo
|
||||
const t = row.value.totalPeriods
|
||||
const t = displayTotalPeriods.value
|
||||
if (p == null && t == null) return '-'
|
||||
if (p == null) return `${t}`
|
||||
if (t == null) return `${p}`
|
||||
@@ -664,6 +640,20 @@ function protocolUrl(row) {
|
||||
function idCardFiles(row) {
|
||||
return (row.idCardAttachments || '').split(',').map(s => s.trim()).filter(Boolean)
|
||||
}
|
||||
/** 现场照片 CSV "url1,url2" 拆成非空数组 */
|
||||
function onSitePhotoFiles(row) {
|
||||
return (row.onSitePhotos || '').split(',').map(s => s.trim()).filter(Boolean)
|
||||
}
|
||||
/** OSS 图片压缩参数 (对齐 PosterService.download): 缩宽 + 压质量 + 转 JPEG, 减小预览体积 */
|
||||
function ossThumb(url, width = 1200) {
|
||||
if (!url || url.includes('x-oss-process')) return url
|
||||
const sep = url.includes('?') ? '&' : '?'
|
||||
return `${url}${sep}x-oss-process=image/resize,w_${width}/quality,q_80/format,jpg`
|
||||
}
|
||||
/** 现场照片预览: 每张带 OSS 压缩参数 → Preview 组件平铺多图 */
|
||||
function openOnSitePhotos(row) {
|
||||
openPreview(onSitePhotoFiles(row).map(u => ossThumb(u)), '现场照片')
|
||||
}
|
||||
|
||||
// ===================== 材料管理 4 个 tab =====================
|
||||
const ROW_CONFIG = [
|
||||
@@ -738,27 +728,73 @@ const materialTracks = computed(() => {
|
||||
})
|
||||
|
||||
/**
|
||||
* 审核时间轴内嵌子步骤 (提交/合规/监察) 状态 — 由物理阶段 (两轨取最小) 推导.
|
||||
* - submit: 越过 NOT_STARTED/RUNNING (已提交过) 即 done
|
||||
* - compliance: RECTIFYING=rejected, AWAITING_COMPLIANCE=pending(当前), 之后=done
|
||||
* - supervision: RECTIFYING=rejected, AWAITING_SUPERVISION=pending(当前), 之后=done
|
||||
* 单条材料审核时间轴: 把两轨(劳务/会务)的 cycle 事件拍平成一条按时间升序的节点流.
|
||||
* 节点 = { step: submit|compliance|supervision, entries: [{label, roundNo, auditor, auditTime, auditResult, opinion}] }
|
||||
* - 同一步骤 + 同一 timestamp 的两轨事件合并成一个节点 (提交全部/审核全部会同时写两轨日志)
|
||||
* - entries.length>1 时, 每行前置 track-mini 标注是哪一轨
|
||||
*/
|
||||
function nodeStatus(slot) {
|
||||
const s = derivePhysicalStage(row.value)
|
||||
if (slot === 'submit') return (s !== 'NOT_STARTED' && s !== 'RUNNING') ? 'done' : 'pending'
|
||||
if (slot === 'compliance') {
|
||||
if (s === 'RECTIFYING') return 'rejected'
|
||||
if (s === 'AWAITING_COMPLIANCE') return 'pending'
|
||||
if (['AWAITING_SUPERVISION', 'AWAITING_SETTLEMENT', 'SETTLED', 'FINISHED'].includes(s)) return 'done'
|
||||
return 'pending'
|
||||
const timelineEvents = computed(() => {
|
||||
const raw = []
|
||||
for (const track of materialTracks.value) {
|
||||
const label = track.label
|
||||
track.cycles.forEach((c, i) => {
|
||||
const roundNo = i + 1
|
||||
if (c.submit) raw.push({ step: 'submit', time: c.submit.auditTime, entry: { label, roundNo, auditor: c.submit.auditor, auditTime: c.submit.auditTime } })
|
||||
if (c.compliance) raw.push({ step: 'compliance', time: c.compliance.auditTime, entry: { label, roundNo, auditor: c.compliance.auditor, auditTime: c.compliance.auditTime, auditResult: c.compliance.auditResult, opinion: c.compliance.opinion } })
|
||||
if (c.supervision) raw.push({ step: 'supervision', time: c.supervision.auditTime, entry: { label, roundNo, auditor: c.supervision.auditor, auditTime: c.supervision.auditTime, auditResult: c.supervision.auditResult, opinion: c.supervision.opinion } })
|
||||
})
|
||||
}
|
||||
if (slot === 'supervision') {
|
||||
if (s === 'RECTIFYING') return 'rejected'
|
||||
if (s === 'AWAITING_SUPERVISION') return 'pending'
|
||||
if (['AWAITING_SETTLEMENT', 'SETTLED', 'FINISHED'].includes(s)) return 'done'
|
||||
return 'pending'
|
||||
raw.sort((a, b) => new Date(a.time).getTime() - new Date(b.time).getTime())
|
||||
const nodes = []
|
||||
for (const ev of raw) {
|
||||
const last = nodes[nodes.length - 1]
|
||||
if (last && last.step === ev.step && last.time === ev.time) { last.entries.push(ev.entry); continue }
|
||||
nodes.push({ step: ev.step, time: ev.time, entries: [ev.entry] })
|
||||
}
|
||||
return 'pending'
|
||||
return nodes
|
||||
})
|
||||
|
||||
/** 当前"待办"节点 (时间轴末尾的灰色占位): 依各轨状态推导下一步该谁动. */
|
||||
const pendingNode = computed(() => {
|
||||
if (!executed.value || isOne(row.value.isSettled) || isOne(row.value.isFinished)) return null
|
||||
const c0 = []; if (laborC0.value) c0.push('劳务'); if (serviceC0.value) c0.push('会务')
|
||||
if (c0.length) return { step: 'compliance', entries: c0.map(l => ({ label: l })) }
|
||||
const c1 = []; if (laborC1.value) c1.push('劳务'); if (serviceC1.value) c1.push('会务')
|
||||
if (c1.length) return { step: 'supervision', entries: c1.map(l => ({ label: l })) }
|
||||
const rej = []
|
||||
if (row.value.laborAuditStage === 'REJECTED') rej.push('劳务')
|
||||
if (row.value.serviceAuditStage === 'REJECTED') rej.push('会务')
|
||||
if (rej.length) return { step: 'submit', entries: rej.map(l => ({ label: l })) }
|
||||
return null
|
||||
})
|
||||
|
||||
function stepLabel(step) {
|
||||
if (step === 'submit') return '执行方提交材料'
|
||||
if (step === 'compliance') return '合规审核'
|
||||
return '监察意见'
|
||||
}
|
||||
/** 节点右侧轨标注: 提交节点带轮次 (劳务·第1次), 审核节点只标轨 (劳务·会务). */
|
||||
function trackTag(node) {
|
||||
const labels = node.entries.map(e => e.label).filter(Boolean)
|
||||
if (!labels.length) return ''
|
||||
if (node.step === 'submit') {
|
||||
const withRound = node.entries.filter(e => e.roundNo != null)
|
||||
if (withRound.length) {
|
||||
if (withRound.length === node.entries.length && withRound.every(e => e.roundNo === withRound[0].roundNo)) {
|
||||
return `${labels.join('·')} · 第${withRound[0].roundNo}次`
|
||||
}
|
||||
return node.entries.map(e => (e.roundNo != null ? `${e.label}·第${e.roundNo}次` : e.label)).filter(Boolean).join(' · ')
|
||||
}
|
||||
}
|
||||
return labels.join('·')
|
||||
}
|
||||
function eventStatus(ev) {
|
||||
return ev.entries.some(e => e.auditResult === 'REJECTED') ? 'rejected' : 'done'
|
||||
}
|
||||
function pendingDesc(step) {
|
||||
if (step === 'submit') return '待重新提交'
|
||||
if (step === 'compliance') return '待合规审核'
|
||||
return '待监察审核'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -793,9 +829,9 @@ const isAssignedSupervisor = computed(() => isSponsor.value)
|
||||
const isSponsorMain = computed(() => currentRole.value === 'sponsor' && userStore.isMain)
|
||||
const executed = computed(() => isOne(row.value.isExecuted))
|
||||
const frozen = computed(() => isOne(row.value.isFrozen))
|
||||
/** 单轨是否处于「可提交/可编辑」态 (未提交 或 被驳回) */
|
||||
const laborEditable = computed(() => ['NOT_SUBMITTED', 'REJECTED'].includes(row.value.laborAuditStage))
|
||||
const serviceEditable = computed(() => ['NOT_SUBMITTED', 'REJECTED'].includes(row.value.serviceAuditStage))
|
||||
/** 单轨是否处于「可提交/可编辑」态: 执行方仅未提交/被驳回可编辑; 合规(manager)/管理员 永远可编辑 */
|
||||
const laborEditable = computed(() => isManager.value || isAdmin.value || ['NOT_SUBMITTED', 'REJECTED'].includes(row.value.laborAuditStage))
|
||||
const serviceEditable = computed(() => isManager.value || isAdmin.value || ['NOT_SUBMITTED', 'REJECTED'].includes(row.value.serviceAuditStage))
|
||||
/** 单轨状态判定: C0=已提交待合规审 / C1=已提交待支持方审 */
|
||||
function isC0(stage, compliance) { return stage === 'SUBMITTED' && !isOne(compliance) }
|
||||
function isC1(stage, compliance) { return stage === 'SUBMITTED' && isOne(compliance) }
|
||||
@@ -1065,7 +1101,7 @@ async function pushEsign(ids, loadingId) {
|
||||
try {
|
||||
const resp = await request.post('/business/meetingAttendee/esign', ids, { __silentError: true })
|
||||
const sent = (resp && resp.data != null) ? resp.data : ids.length
|
||||
ElMessage.success(`已推送 ${sent} 份电子签 (短信 + 站内信)`)
|
||||
ElMessage.success(`已推送 ${sent} 份电子签`)
|
||||
selectedAttendees.value = []
|
||||
await loadAttendees()
|
||||
} catch (e) {
|
||||
@@ -1214,6 +1250,7 @@ async function lookupExpertByPhone() {
|
||||
f.title = expert.title || f.title
|
||||
f.idCard = expert.idCard || f.idCard
|
||||
f.bankName = expert.bankName || f.bankName
|
||||
f.bankBranch = expert.bankBranch || f.bankBranch
|
||||
f.bankCard = expert.bankCard || f.bankCard
|
||||
f.bankRegion = expert.bankRegion || f.bankRegion
|
||||
f.bankAddress = expert.bankAddress || f.bankAddress
|
||||
@@ -1669,17 +1706,23 @@ function openPreview(urls, title) {
|
||||
previewOpen.value = true
|
||||
}
|
||||
|
||||
// 保存所有待落库内容 (材料/日程海报/邀请函/付款凭证); 成功返回 OCR 提交数, 失败抛错
|
||||
async function doSave() {
|
||||
const { ocrCount } = await saveMaterials()
|
||||
await saveSchedulePoster()
|
||||
await saveInvitation()
|
||||
if (canUploadVoucher.value) await saveVouchers()
|
||||
// 材料落库 → 会议费用待重算, 刷新状态并轮询到汇总完成
|
||||
refreshFee()
|
||||
return ocrCount
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
if (saving.value) return
|
||||
saving.value = true
|
||||
try {
|
||||
const { saved, ocrCount } = await saveMaterials()
|
||||
await saveSchedulePoster()
|
||||
await saveInvitation()
|
||||
if (canUploadVoucher.value) await saveVouchers()
|
||||
// 材料落库 → 会议费用待重算, 刷新状态并轮询到汇总完成
|
||||
refreshFee()
|
||||
ElMessage.success(`保存成功 (${saved.length} 条)`)
|
||||
const ocrCount = await doSave()
|
||||
ElMessage.success('保存成功')
|
||||
if (ocrCount) ElMessage.info(`已提交 ${ocrCount} 个识别任务 (后台执行)`)
|
||||
} catch (e) {
|
||||
console.error('[meeting-detail] save failed', e)
|
||||
@@ -1844,7 +1887,6 @@ async function confirmAudit() {
|
||||
}
|
||||
|
||||
// ===================== 结算 / 完结 (合规/管理员 手动点击) =====================
|
||||
// 结算前必须先已上传保存付款凭证 (劳务/会务凭证 tab), 后端 settle 校验凭证存在, 缺失则报错.
|
||||
async function onSettle() {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认结算? 结算前请确保已上传劳务/会务付款凭证。', '结算确认', { type: 'warning' })
|
||||
@@ -2028,8 +2070,11 @@ onBeforeUnmount(stopFeePolling)
|
||||
.track-block:last-child { margin-bottom: 0; }
|
||||
.track-label-row { display: flex; align-items: center; gap: 6px; margin-bottom: 2px; }
|
||||
.cycle-no { font-size: 11px; color: #909399; }
|
||||
.timeline-opinion { margin-top: 4px; font-size: 12px; color: #f56c6c; background: #fef0f0; padding: 4px 8px; border-radius: 3px; line-height: 1.5; word-break: break-all; }
|
||||
.timeline-item.done .timeline-opinion { color: #909399; background: #f5f7fa; }
|
||||
.track-tag { display: inline-block; margin-left: 6px; font-size: 11px; font-weight: 400; color: #909399; background: #f5f7fa; border-radius: 3px; padding: 1px 6px; line-height: 1.6; vertical-align: middle; }
|
||||
.track-mini { display: inline-block; min-width: 34px; font-size: 11px; color: #909399; }
|
||||
.timeline-opinion { margin-top: 4px; font-size: 12px; padding: 4px 8px; border-radius: 3px; line-height: 1.5; word-break: break-all; }
|
||||
.timeline-opinion.opinion-reject { color: #f56c6c; background: #fef0f0; }
|
||||
.timeline-opinion.opinion-approve { color: #389e0d; background: #f6ffed; }
|
||||
|
||||
.audit-columns { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; min-width: 0; }
|
||||
.audit-column { margin-bottom: 0; padding: 16px 18px; min-width: 0; }
|
||||
@@ -2261,4 +2306,4 @@ onBeforeUnmount(stopFeePolling)
|
||||
padding: 6px 3px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -13,28 +13,41 @@
|
||||
<el-form-item label="项目名称">
|
||||
<span class="form-value">{{ snap.projectName }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="会议名称">
|
||||
<el-input v-if="canEditName" v-model="form.meetingName" placeholder="请输入会议名称" />
|
||||
<span v-else class="form-value">{{ form.meetingName }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="支持单位">
|
||||
<span class="form-value">{{ snap.sponsorOrgName }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="总场次/总期数">
|
||||
<span class="form-value">{{ snap.totalSessions }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="项目创建人">
|
||||
<span class="form-value">{{ snap.createUserName }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="项目创建时间">
|
||||
<span class="form-value">{{ snap.createTime }}</span>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 待填写 (放下面) -->
|
||||
<div class="form-grid form-grid-inputs">
|
||||
<el-form-item label="会议名称">
|
||||
<el-input v-if="canEditName" v-model="form.meetingName" placeholder="请输入会议名称" />
|
||||
<span v-else class="form-value">{{ form.meetingName }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="期数" prop="periodNo">
|
||||
<!--
|
||||
不用 el-input-number: 移动端其 +/- 按钮 absolute 定位在容器最右,
|
||||
容器 100% 后按钮飘到行尾. 改用 type="number" 原生 input, 浏览器自带步进按钮在 input 内右贴边
|
||||
-->
|
||||
<el-input v-model.number="form.periodNo" type="number" :min="1" :max="totalSessionsNum" placeholder="请输入期数" />
|
||||
</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" placeholder="选择开始时间" />
|
||||
</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" placeholder="选择结束时间" />
|
||||
</el-form-item>
|
||||
<el-form-item label="期数" prop="periodNo">
|
||||
<el-input v-model="form.periodNo" placeholder="请填写第几期" />
|
||||
<el-form-item label="会议地点" prop="address">
|
||||
<el-input v-model="form.address" placeholder="请输入会议地点" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" placeholder="请输入备注" />
|
||||
@@ -95,7 +108,15 @@ const snap = reactive({
|
||||
projectNo: '',
|
||||
projectName: '',
|
||||
totalSessions: '',
|
||||
sponsorOrgName: ''
|
||||
sponsorOrgName: '',
|
||||
createUserName: '',
|
||||
createTime: ''
|
||||
})
|
||||
|
||||
// 期数上限 = 总场次/总期数 (executor 快照 totalSessions 已换成分配给本机构的场次口径)
|
||||
const totalSessionsNum = computed(() => {
|
||||
const n = Number(snap.totalSessions)
|
||||
return Number.isNaN(n) || n <= 0 ? undefined : n
|
||||
})
|
||||
|
||||
// ========== 表单 (右列 5 项: meetingName 只读 + 4 输入) ==========
|
||||
@@ -104,16 +125,18 @@ const form = reactive({
|
||||
projectNo: '',
|
||||
projectName: '',
|
||||
meetingName: '',
|
||||
periodNo: '',
|
||||
periodNo: null,
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
address: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
periodNo: [{ required: true, message: '请填写第几期', trigger: 'blur' }],
|
||||
periodNo: [{ required: true, message: '请填写期数', trigger: 'blur' }],
|
||||
startTime: [{ required: true, message: '请选择会议开始时间', trigger: 'change' }],
|
||||
endTime: [{ required: true, message: '请选择会议结束时间', trigger: 'change' }]
|
||||
endTime: [{ required: true, message: '请选择会议结束时间', trigger: 'change' }],
|
||||
address: [{ required: true, message: '请填写会议地点', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
// ========== 加载项目快照 ==========
|
||||
@@ -126,6 +149,8 @@ async function loadProjectSnapshot(pid) {
|
||||
snap.projectName = d.projectName || ''
|
||||
snap.totalSessions = d.totalSessions ?? ''
|
||||
snap.sponsorOrgName = d.sponsorOrgName || ''
|
||||
snap.createUserName = d.createUserName || ''
|
||||
snap.createTime = d.createTime || ''
|
||||
// executor: "总场次" = 分配给本执行方(公司)的场次, 不是项目总场次
|
||||
if (roleSegment.value === 'executor') {
|
||||
try {
|
||||
@@ -147,9 +172,10 @@ async function loadMeeting(mid) {
|
||||
form.projectNo = d.projectNo || ''
|
||||
form.projectName = d.projectName || ''
|
||||
form.meetingName = d.meetingName || ''
|
||||
form.periodNo = d.periodNo || ''
|
||||
form.periodNo = d.periodNo ?? null
|
||||
form.startTime = d.startTime || ''
|
||||
form.endTime = d.endTime || ''
|
||||
form.address = d.address || ''
|
||||
form.remark = d.remark || ''
|
||||
// 左列快照 projectNo/projectName 用 meeting 冗余字段 (即使 project_id 为 NULL 也能显示)
|
||||
snap.projectNo = d.projectNo || ''
|
||||
@@ -170,14 +196,12 @@ async function onSave() {
|
||||
ElMessage.error('会议开始时间不能晚于会议结束时间')
|
||||
return
|
||||
}
|
||||
// executor: 期数不得超过分配给本机构的场次 (快照 totalSessions 已换成 executor 口径)
|
||||
if (roleSegment.value === 'executor') {
|
||||
const pn = Number(form.periodNo)
|
||||
const total = Number(snap.totalSessions)
|
||||
if (!Number.isNaN(pn) && !Number.isNaN(total) && pn > total) {
|
||||
ElMessage.error(`期数不能超过分配给本机构的场次 (共 ${total} 场)`)
|
||||
return
|
||||
}
|
||||
// 期数不得高于总场次/总期数 (executor 快照 totalSessions 已换成分配给本机构的场次口径)
|
||||
const pn = Number(form.periodNo)
|
||||
const total = Number(snap.totalSessions)
|
||||
if (!Number.isNaN(pn) && !Number.isNaN(total) && pn > total) {
|
||||
ElMessage.error(`期数不能高于总场次/总期数 (共 ${total} 场)`)
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
@@ -189,6 +213,7 @@ async function onSave() {
|
||||
periodNo: form.periodNo,
|
||||
startTime: form.startTime,
|
||||
endTime: form.endTime,
|
||||
address: form.address,
|
||||
remark: form.remark
|
||||
}
|
||||
if (mode.value === 'edit') {
|
||||
@@ -226,10 +251,10 @@ onMounted(async () => {
|
||||
if (mode.value === 'edit') {
|
||||
await loadMeeting(meetingId)
|
||||
} else if (mode.value === 'copy') {
|
||||
// 复制: 加载会议详情, name 加 " 副本", 期数 reset 让用户重填
|
||||
// 复制: 加载会议详情, 会议名用项目名称 (而非 "xx 副本"), 期数 reset 让用户重填
|
||||
await loadMeeting(meetingId)
|
||||
form.meetingName = (form.meetingName || '') + ' 副本'
|
||||
form.periodNo = '' // 复制后让用户重新填第几期
|
||||
form.meetingName = snap.projectName || form.projectName || form.meetingName
|
||||
form.periodNo = null // 复制后让用户重新填第几期
|
||||
form.meetingId = null // 防御: 不带源 meetingId 走 bizAdd
|
||||
} else {
|
||||
// 新建: 加载项目快照, meetingName 默认 = 项目名
|
||||
@@ -303,85 +328,55 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
|
||||
/* ========================================
|
||||
/* ========================================
|
||||
移动端适配 (≤768px)
|
||||
- 卡片 padding 收窄
|
||||
- 工具栏横向滚动
|
||||
- filter-form: label 与输入框横向
|
||||
- 表格字号收紧
|
||||
- 只调整本页面布局 (form-grid / form-actions / form-value + form-item 容器),
|
||||
不强制覆盖 Element Plus 控件内部样式 (.el-input / .el-date-editor / .el-input__wrapper)
|
||||
======================================== */
|
||||
@media (max-width: 768px) {
|
||||
/* 卡片 padding */
|
||||
.page-card { padding: 12px !important; border-radius: 4px !important; }
|
||||
.breadcrumb { font-size: 12px !important; margin-bottom: 8px !important; }
|
||||
.breadcrumb { font-size: 12px !important; margin-bottom: 12px !important; }
|
||||
|
||||
/* 工具栏: 横向滚动 */
|
||||
.toolbar {
|
||||
flex-wrap: nowrap !important;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding-bottom: 6px;
|
||||
margin-bottom: 8px !important;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.toolbar::-webkit-scrollbar { height: 4px; }
|
||||
.toolbar::-webkit-scrollbar-thumb { background: var(--brand-slate-200); border-radius: 2px; }
|
||||
.toolbar :deep(.action-btn),
|
||||
.toolbar :deep(.el-button) {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px !important;
|
||||
padding: 0 10px !important;
|
||||
height: 30px !important;
|
||||
/* 双列 grid → 单列, gap 加大保证 form-item 之间有足够空间显示 error message */
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr !important;
|
||||
gap: 22px !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
.form-grid-inputs { margin-top: 16px !important; padding-top: 16px !important; }
|
||||
|
||||
/* filter-form: 横向 (label 左 + input 右) */
|
||||
.filter-form {
|
||||
/* form-item 容器: 让 label + content 横向并排 (label-width=110px 桌面默认, mobile 仍生效)
|
||||
align-items: flex-start 让 .el-form-item__error (validate 错误提示) 能在 content 下方独立占行,
|
||||
不被垂直居中挤掉 */
|
||||
.manager-meeting-new :deep(.el-form-item) {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
align-items: stretch !important;
|
||||
gap: 10px !important;
|
||||
}
|
||||
.filter-form :deep(.el-form-item) {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
align-items: flex-start !important;
|
||||
margin-right: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.filter-form :deep(.el-form-item__label) {
|
||||
float: none !important;
|
||||
width: auto !important;
|
||||
min-width: 80px !important;
|
||||
text-align: right !important;
|
||||
padding: 0 8px 0 0 !important;
|
||||
font-size: 13px !important;
|
||||
color: var(--el-text-color-regular) !important;
|
||||
line-height: 32px !important;
|
||||
height: 32px !important;
|
||||
}
|
||||
.filter-form :deep(.el-form-item__content) {
|
||||
margin-left: 0 !important;
|
||||
line-height: 32px !important;
|
||||
.manager-meeting-new :deep(.el-form-item__content) {
|
||||
flex: 1 !important;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
/* 强制所有控件全宽 */
|
||||
.filter-form :deep(.el-select),
|
||||
.filter-form :deep(.el-input),
|
||||
.filter-form :deep(.el-date-editor),
|
||||
.filter-form :deep(.el-button),
|
||||
.filter-form :deep(.el-cascader) {
|
||||
width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
/* 只读值容器 (form-value) 也走 flex:1 让其与 input 一致 */
|
||||
.form-value { flex: 1; min-width: 0; line-height: 32px; }
|
||||
|
||||
/* 底部按钮: 左右并排等宽 */
|
||||
.form-actions { margin-top: 16px !important; padding-top: 16px !important; }
|
||||
.form-actions :deep(.el-form-item__content) {
|
||||
display: flex !important;
|
||||
flex-wrap: wrap !important;
|
||||
gap: 0 !important;
|
||||
}
|
||||
.form-actions :deep(.el-button) {
|
||||
flex: 1 1 0 !important;
|
||||
width: 0 !important;
|
||||
margin-left: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
display: block !important;
|
||||
}
|
||||
.filter-form :deep(.el-button + .el-button) {
|
||||
margin-top: 8px !important;
|
||||
.form-actions :deep(.el-button + .el-button) {
|
||||
margin-left: 8px !important;
|
||||
}
|
||||
|
||||
/* 表格字号收紧 */
|
||||
:deep(.el-table) { font-size: 12px !important; }
|
||||
}
|
||||
</style>
|
||||
@@ -9,9 +9,9 @@
|
||||
<el-form-item label="会议名称"><el-input v-model="q.meetingName" placeholder="请输入会议名称" clearable style="width:160px" /></el-form-item>
|
||||
<el-form-item label="期数"><el-input-number v-model="q.periodNo" :min="1" placeholder="期数" style="width:140px" /></el-form-item>
|
||||
<el-form-item label="会议时间">
|
||||
<el-date-picker v-model="q.startTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="开始时间" style="width:170px" />
|
||||
<el-date-picker v-model="q.startTime" type="date" value-format="YYYY-MM-DD" placeholder="开始日期" style="width:170px" />
|
||||
<span class="date-sep">至</span>
|
||||
<el-date-picker v-model="q.endTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="结束时间" style="width:170px" />
|
||||
<el-date-picker v-model="q.endTime" type="date" value-format="YYYY-MM-DD" placeholder="结束日期" style="width:170px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="项目形式">
|
||||
<el-select v-model="q.projectForm" placeholder="请选择" clearable style="width:140px">
|
||||
@@ -28,7 +28,7 @@
|
||||
</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 type="primary" @click="load">查询</el-button>
|
||||
<el-button @click="reset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
@@ -49,7 +49,11 @@
|
||||
<el-table-column prop="projectNo" label="项目编号" width="170" 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 prop="meetingName" label="会议名称" min-width="180" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-link :underline="false" type="primary" @click="viewOnly(row)">{{ row.meetingName }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="会议开始时间" width="160" align="center">
|
||||
<template #default="{ row }">{{ fmtTime(row.startTime) }}</template>
|
||||
</el-table-column>
|
||||
@@ -71,13 +75,13 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="260" fixed="right" align="center">
|
||||
<el-table-column label="操作" :width="isRole('sponsor') ? 140 : 260" fixed="right" align="center">
|
||||
<template #default="{ row }"><div class="table-actions">
|
||||
<!-- 主操作: 查看 / 审核 / 编辑材料 (按角色显隐) -->
|
||||
<!-- 主操作: 查看 直链 (所有角色) -->
|
||||
<el-link :underline="false" type="primary" @click="viewOnly(row)">查看</el-link>
|
||||
<el-link :underline="false" v-if="isRole('manager') && isCompliancePending(row)" type="warning" @click="onAudit(row)">审核</el-link>
|
||||
<el-link :underline="false" v-if="isRole('admin', 'manager')" type="primary" @click="viewDetail(row)">编辑材料</el-link>
|
||||
<!-- 更多: admin/manager 视角 ≥7 个次操作折叠 (sponsor 视角只 1 个审核, 始终走外置) -->
|
||||
<!-- 更多: admin/manager 视角 ≥7 个次操作折叠 -->
|
||||
<el-dropdown v-if="isRole('admin', 'manager')" trigger="hover" @command="(cmd) => onMoreAction(cmd, row)">
|
||||
<el-link :underline="false" type="primary" class="op-dropdown">
|
||||
更多<el-icon class="op-caret"><ArrowDown /></el-icon>
|
||||
@@ -97,8 +101,17 @@
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<!-- sponsor 视角: 额外只看合规审核 -->
|
||||
<el-link :underline="false" v-if="isRole('sponsor') && isSponsorPending(row)" type="warning" @click="onAudit(row)">审核</el-link>
|
||||
<!-- sponsor 视角: 仅"审核"折叠到下拉 (sponsor 不参与其他次操作) -->
|
||||
<el-dropdown v-if="isRole('sponsor') && isSponsorPending(row)" trigger="hover" @command="(cmd) => onMoreAction(cmd, row)">
|
||||
<el-link :underline="false" type="primary" class="op-dropdown">
|
||||
更多<el-icon class="op-caret"><ArrowDown /></el-icon>
|
||||
</el-link>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="audit">审核</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div></template>
|
||||
</el-table-column>
|
||||
</GrTable>
|
||||
@@ -172,7 +185,7 @@ const newRouteName = computed(() => isAdmin.value ? 'admin-meetings-new' : 'mana
|
||||
// ========== 筛选 (按实际 8 项: 项目编号/会议ID/会议名称/第?期/会议时间/项目形式/当前阶段/备注) ==========
|
||||
const q = ref({
|
||||
projectNo: '', meetingId: '', meetingName: '', periodNo: null,
|
||||
projectForm: '', currentStage: '', remark: '',
|
||||
projectForm: '', currentStage: '', currentStageNotIn: '', remark: '',
|
||||
startTime: '', endTime: ''
|
||||
})
|
||||
const rows = ref([])
|
||||
@@ -197,7 +210,11 @@ function fmtTime(d) {
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const { data } = await bizList('meeting', { ...q.value, pageNum: page.pageNum, pageSize: page.pageSize })
|
||||
const params = { ...q.value, pageNum: page.pageNum, pageSize: page.pageSize }
|
||||
// 时间范围含两端: 开始日 00:00:00 ~ 结束日 23:59:59
|
||||
if (params.startTime) params.startTime = params.startTime + ' 00:00:00'
|
||||
if (params.endTime) params.endTime = params.endTime + ' 23:59:59'
|
||||
const { data } = await bizList('meeting', params)
|
||||
rows.value = data?.rows || []
|
||||
page.total = data?.total || 0
|
||||
} catch { rows.value = []; page.total = 0 }
|
||||
@@ -206,7 +223,7 @@ async function load() {
|
||||
function reset() {
|
||||
q.value = {
|
||||
projectNo:'', meetingId:'', meetingName:'', periodNo:null,
|
||||
projectForm:'', currentStage:'', remark:'',
|
||||
projectForm:'', currentStage:'', currentStageNotIn:'', remark:'',
|
||||
startTime:'', endTime:''
|
||||
}
|
||||
page.pageNum = 1
|
||||
@@ -218,6 +235,7 @@ function reset() {
|
||||
function readQueryFromRoute() {
|
||||
const q2 = route.query
|
||||
if (q2.currentStage != null && q2.currentStage !== '') q.value.currentStage = String(q2.currentStage)
|
||||
if (q2.currentStageNotIn != null && q2.currentStageNotIn !== '') q.value.currentStageNotIn = String(q2.currentStageNotIn)
|
||||
}
|
||||
|
||||
// ========== 路由跳转 ==========
|
||||
@@ -238,6 +256,7 @@ function onMoreAction(cmd, row) {
|
||||
else if (cmd === 'unfreeze') onUnfreeze(row)
|
||||
else if (cmd === 'downloadService') onDownloadService(row)
|
||||
else if (cmd === 'downloadLabor') onDownloadLabor(row)
|
||||
else if (cmd === 'audit') onAudit(row)
|
||||
else if (cmd === 'delete') onDelete(row)
|
||||
}
|
||||
// 修改 / 复制: 按角色选 admin-meetings-new 或 manager-meetings-new (MeetingNew.vue 公共页)
|
||||
@@ -252,13 +271,9 @@ function onCopy(row) {
|
||||
async function onDelete(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认删除会议「${row.meetingName}」?\n` +
|
||||
`会议ID: ${row.meetingId}\n项目编号: ${row.projectNo || '-'}\n\n` +
|
||||
`此操作将级联软删除:\n` +
|
||||
`· biz_meeting 主表 + 附件 + 参会人 + 监督员 + 执行人 + 审计日志 (6 张表)\n\n` +
|
||||
`数据保留在库, 不再展示给前台用户, 审计追溯可查。`,
|
||||
`确认删除会议「${row.meetingName}」?`,
|
||||
'删除确认',
|
||||
{ type: 'error', confirmButtonText: '确认软删除', cancelButtonText: '取消' }
|
||||
{ type: 'error', confirmButtonText: '确认删除', cancelButtonText: '取消' }
|
||||
)
|
||||
} catch { return }
|
||||
try {
|
||||
@@ -273,37 +288,55 @@ async function onBatchDelete() {
|
||||
if (!selectedIds.value.length) return ElMessage.warning('请先勾选会议')
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认批量软删除选中的 ${selectedIds.value.length} 个会议?\n\n` +
|
||||
`每个会议将级联软删除 6 张表 (会议主表 + 附件 + 参会人 + 监督员 + 执行人 + 审计日志).\n\n` +
|
||||
`数据保留, 不再展示给前台用户。`,
|
||||
`确认删除选中的 ${selectedIds.value.length} 个会议?`,
|
||||
'批量删除确认',
|
||||
{ type: 'error', confirmButtonText: `确认删除 ${selectedIds.value.length} 个`, cancelButtonText: '取消' }
|
||||
)
|
||||
} catch { return }
|
||||
try {
|
||||
await bizDelete('meeting', selectedIds.value)
|
||||
ElMessage.success(`已批量软删除 ${selectedIds.value.length} 个会议`)
|
||||
ElMessage.success(`已删除 ${selectedIds.value.length} 个会议`)
|
||||
load()
|
||||
} catch (e) { ElMessage.error(e?.msg || '批量删除失败') }
|
||||
}
|
||||
|
||||
// ========== 批量下载会务/劳务材料 (admin/manager) ==========
|
||||
// 后端代理打 zip 并以自定义文件名下发 (FC 固定命名 output_1-xxx.zip, 前端按 Content-Disposition 改名)
|
||||
async function downloadZipBlob(res, fallbackName) {
|
||||
// 后端出错时仍返回 200 + application/json (RuoYi 异常处理), blob 分支不放行, 按 content-type 识别
|
||||
const ct = res.headers?.['content-type'] || ''
|
||||
if (ct.includes('application/json')) {
|
||||
try {
|
||||
const text = await res.data.text()
|
||||
const j = JSON.parse(text)
|
||||
ElMessage.error(j.msg || '下载失败')
|
||||
} catch { ElMessage.error('下载失败') }
|
||||
return
|
||||
}
|
||||
const blob = res.data instanceof Blob ? res.data : new Blob([res.data])
|
||||
const dispo = res.headers?.['content-disposition'] || res.headers?.['Content-Disposition'] || ''
|
||||
const m = /filename\*=UTF-8''([^;]+)/i.exec(dispo)
|
||||
const a = document.createElement('a')
|
||||
a.href = URL.createObjectURL(blob)
|
||||
a.download = m ? decodeURIComponent(m[1]) : fallbackName
|
||||
a.click()
|
||||
URL.revokeObjectURL(a.href)
|
||||
}
|
||||
|
||||
async function onBatchDownloadService() {
|
||||
if (!selectedIds.value.length) return ElMessage.warning('请先勾选会议')
|
||||
try {
|
||||
const r = await request.post('/business/meetingMaterial/batchDownloadZip', { meetingIds: selectedIds.value })
|
||||
const url = r?.data
|
||||
if (!url) { ElMessage.warning('暂无可下载的会务材料'); return }
|
||||
window.open(url, '_blank')
|
||||
const res = await request.post('/business/meetingMaterial/batchDownloadZip', { meetingIds: selectedIds.value },
|
||||
{ responseType: 'blob', __silentError: true, timeout: 120000 })
|
||||
await downloadZipBlob(res, '会务.zip')
|
||||
} catch (e) { ElMessage.error(e?.msg || e?.message || '批量下载会务失败') }
|
||||
}
|
||||
async function onBatchDownloadLabor() {
|
||||
if (!selectedIds.value.length) return ElMessage.warning('请先勾选会议')
|
||||
try {
|
||||
const r = await request.post('/business/meetingMaterial/batchDownloadLaborZip', { meetingIds: selectedIds.value })
|
||||
const url = r?.data
|
||||
if (!url) { ElMessage.warning('暂无可下载的劳务材料'); return }
|
||||
window.open(url, '_blank')
|
||||
const res = await request.post('/business/meetingMaterial/batchDownloadLaborZip', { meetingIds: selectedIds.value },
|
||||
{ responseType: 'blob', __silentError: true, timeout: 120000 })
|
||||
await downloadZipBlob(res, '劳务.zip')
|
||||
} catch (e) { ElMessage.error(e?.msg || e?.message || '批量下载劳务失败') }
|
||||
}
|
||||
|
||||
@@ -437,27 +470,25 @@ async function onBatchAuditConfirm() {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 会务下载 (OSS 端打 zip: 后端 staging copy + 阿里云 FC 打包) ==========
|
||||
// ========== 会务下载 (OSS 端打 zip: 后端 staging copy + 阿里云 FC 打包 + 后端代理改名下发) ==========
|
||||
async function onDownloadService(row) {
|
||||
try {
|
||||
const r = await request.get(`/business/meetingMaterial/${row.meetingId}/downloadZip`)
|
||||
const url = r?.data
|
||||
if (!url) { ElMessage.warning('暂无可下载的会务材料'); return }
|
||||
window.open(url, '_blank')
|
||||
const res = await request.get(`/business/meetingMaterial/${row.meetingId}/downloadZip`,
|
||||
{ responseType: 'blob', __silentError: true, timeout: 120000 })
|
||||
await downloadZipBlob(res, '会务.zip')
|
||||
} catch (e) {
|
||||
// 拦截器已 toast 错误 (含后端 ServiceException 提示语), 这里静默
|
||||
ElMessage.error(e?.msg || e?.message || '会务下载失败')
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 劳务下载 (同会务下载: 劳务材料 + 参会人信息 Excel 一并打 zip) ==========
|
||||
async function onDownloadLabor(row) {
|
||||
try {
|
||||
const r = await request.get(`/business/meetingMaterial/${row.meetingId}/downloadLaborZip`)
|
||||
const url = r?.data
|
||||
if (!url) { ElMessage.warning('暂无可下载的劳务材料'); return }
|
||||
window.open(url, '_blank')
|
||||
const res = await request.get(`/business/meetingMaterial/${row.meetingId}/downloadLaborZip`,
|
||||
{ responseType: 'blob', __silentError: true, timeout: 120000 })
|
||||
await downloadZipBlob(res, '劳务.zip')
|
||||
} catch (e) {
|
||||
// 拦截器已 toast 错误 (含后端 ServiceException 提示语), 这里静默
|
||||
ElMessage.error(e?.msg || e?.message || '劳务下载失败')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<template>
|
||||
<div class="page-card">
|
||||
<div class="breadcrumb">首页 / 消息通知</div>
|
||||
<NoticeList :limit="50" :show-category="true" />
|
||||
<NoticeList :show-category="true" :pageable="true" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ async function onUserCmd(cmd) {
|
||||
if (cmd === 'logout') return goLogout()
|
||||
if (cmd === 'account') {
|
||||
const r = (userStore.user?.role || '')
|
||||
const map = { admin: '/admin/workbench', manager: '/manager/workbench', doctor: '/doctor/home', executor: '/executor/meetings', sponsor: '/sponsor/home' }
|
||||
const map = { admin: '/admin/workbench', manager: '/manager/workbench', doctor: '/doctor/home', executor: '/executor/overview', sponsor: '/sponsor/home' }
|
||||
router.push(map[r] || '/admin/workbench')
|
||||
}
|
||||
}
|
||||
@@ -134,6 +134,9 @@ body {
|
||||
}
|
||||
a { color: inherit; text-decoration: none; }
|
||||
|
||||
/* PortalLayout padding-top: 72px 是给 fixed navbar 留位, 本页 .top-nav 是 sticky 已占文档流, 上移抵消 (与 SpecialPlanDetail 同处理) */
|
||||
.detail-page { margin-top: -72px; }
|
||||
|
||||
/* ========== 顶部导航 ========== */
|
||||
.top-nav {
|
||||
position: sticky;
|
||||
@@ -150,7 +153,7 @@ a { color: inherit; text-decoration: none; }
|
||||
}
|
||||
.top-nav.is-scrolled { box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25); }
|
||||
|
||||
.logo { display: flex; align-items: center; gap: 12px; }
|
||||
.logo { display: flex; align-items: center; gap: 12px; cursor: pointer; }
|
||||
.logo-icon {
|
||||
width: 36px; height: 36px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
@@ -162,7 +165,10 @@ a { color: inherit; text-decoration: none; }
|
||||
.logo-subtitle { font-size: 11px; color: rgba(255, 255, 255, 0.6); margin-top: 2px; letter-spacing: 0.3px; }
|
||||
|
||||
.nav-list {
|
||||
flex: 1; display: flex; align-items: center; justify-content: center;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
gap: 36px; list-style: none;
|
||||
}
|
||||
.nav-item { position: relative; height: 72px; display: flex; align-items: center; }
|
||||
@@ -179,7 +185,7 @@ a { color: inherit; text-decoration: none; }
|
||||
.nav-item:hover .nav-link, .nav-link.active { color: #fff; }
|
||||
.nav-item:hover .nav-link::after, .nav-link.active::after { width: 100%; }
|
||||
|
||||
.top-tools { display: flex; align-items: center; gap: 16px; }
|
||||
.top-tools { display: flex; align-items: center; gap: 16px; margin-left: auto; }
|
||||
.login-btn {
|
||||
padding: 7px 20px;
|
||||
background: #fff;
|
||||
|
||||
+249
-177
@@ -2,42 +2,34 @@
|
||||
<div class="home-page">
|
||||
<PortalNavbar />
|
||||
|
||||
<div class="container">
|
||||
<div class="container container--full home-hero-wrap">
|
||||
<section class="hero">
|
||||
<div>
|
||||
<span class="hero-tag">2025-2030</span>
|
||||
<h1 class="hero-title">年度项目规划</h1>
|
||||
<p class="hero-subtitle">七 大 专 项</p>
|
||||
<p class="hero-desc">
|
||||
围绕健康中国战略,聚焦医学整合创新,系统推进七大专项计划,<br>
|
||||
全面构建覆盖诊疗、科研、人才、管理、公益、政学协作与组织建设的协同发展体系。
|
||||
</p>
|
||||
</div>
|
||||
<div class="hero-cta">
|
||||
<span v-if="canPropose" class="cta-btn" @click="onSubmit">项目提案</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="overview-section">
|
||||
<div class="section-title">年度项目规划概述</div>
|
||||
<p class="overview-text">
|
||||
依托国家战略层面《"健康中国2030"规划纲要》和健康中国行动的统领力量,切合《全民健康素养提升三年行动方案(2024-2027年)》的契机,践行学会的公益性服务的责任和使命,全方位、全领域、体系性、系统性、持续性地开展健康科普活动,通过征集全国范围300个病种的健康科普短视频、科普讲座、科普文章,进行整个医疗系统的健康科普总动员,以科普内容征集促进科普活动走近、走深、走实。
|
||||
</p>
|
||||
<div class="plan-image" v-html="planSvg"></div>
|
||||
<img class="hero-banner" :src="bannerImg" alt="年度项目规划" />
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="specialty-section">
|
||||
<div class="specialty-inner">
|
||||
<div class="section-title">七大专项计划</div>
|
||||
<div class="specialty-header">
|
||||
<div class="specialty-header-text">
|
||||
<div class="section-title">八大专项计划</div>
|
||||
<p class="specialty-subtitle">EIGHT SPECIALTY PROGRAMS · 围绕健康中国战略协同推进</p>
|
||||
</div>
|
||||
<span v-if="canPropose" class="cta-btn cta-btn--inline" @click="onSubmit">项目提案</span>
|
||||
</div>
|
||||
<div class="specialty-list">
|
||||
<div v-for="(p, i) in specialPlans" :key="p.id" class="specialty-card" @click="openPlan(p.id)">
|
||||
<div class="specialty-no">{{ String(i + 1).padStart(2, '0') }}</div>
|
||||
<div class="specialty-body">
|
||||
<h3 class="specialty-title">《{{ p.title }}》</h3>
|
||||
<div class="specialty-action">
|
||||
<span class="specialty-link">查看详情</span>
|
||||
</div>
|
||||
<div class="specialty-icon">
|
||||
<span class="specialty-icon-bar"></span>
|
||||
<span class="specialty-icon-no">0{{ i + 1 }}</span>
|
||||
</div>
|
||||
<h3 class="specialty-title">{{ p.title }}</h3>
|
||||
<div class="specialty-action">
|
||||
<span class="specialty-link">查看详情</span>
|
||||
<svg class="specialty-arrow" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
|
||||
<line x1="5" y1="12" x2="19" y2="12"/>
|
||||
<polyline points="13 6 19 12 13 18"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!specialPlans.length" class="specialty-empty">暂无专项计划</div>
|
||||
@@ -55,6 +47,7 @@ import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import request from '@/utils/request'
|
||||
import bannerImg from '@/assets/banner.jpg'
|
||||
import PortalFooter from '@/components/PortalFooter.vue'
|
||||
import PortalNavbar from '@/components/PortalNavbar.vue'
|
||||
|
||||
@@ -82,12 +75,6 @@ function openPlan(id) {
|
||||
window.open(`${import.meta.env.BASE_URL}#/special-plan/${id}`, '_blank')
|
||||
}
|
||||
const loggedIn = computed(() => !!userStore.token)
|
||||
// 投稿角色: admin/manager 不开放投稿 → 隐藏「项目提案」按钮; 未登录 user 为 null → 显示(点击引导登录)
|
||||
const canPropose = computed(() => {
|
||||
const r = userStore.user?.role || ''
|
||||
return r !== 'admin' && r !== 'manager'
|
||||
})
|
||||
|
||||
const planSvg = `<svg viewBox="0 150 1200 370" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="2025-2030年项目规划">
|
||||
<defs>
|
||||
<linearGradient id="arcGrad" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
@@ -145,13 +132,10 @@ const planSvg = `<svg viewBox="0 150 1200 370" xmlns="http://www.w3.org/2000/svg
|
||||
</g>
|
||||
</svg>`
|
||||
|
||||
onMounted(() => {
|
||||
loadSpecialPlans()
|
||||
// portal home 移动端适配: 解除 body min-width (避免手机端 1354px 横向滚动)
|
||||
document.body.classList.add('home-page-body')
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
document.body.classList.remove('home-page-body')
|
||||
// 投稿角色: admin/manager 不开放投稿 → 隐藏「项目提案」按钮; 未登录 user 为 null → 显示(点击引导登录)
|
||||
const canPropose = computed(() => {
|
||||
const r = userStore.user?.role || ''
|
||||
return r !== 'admin' && r !== 'manager'
|
||||
})
|
||||
|
||||
function onSubmit() {
|
||||
@@ -164,6 +148,16 @@ function onSubmit() {
|
||||
const map = { doctor: '/doctor/submissions', executor: '/executor/submissions', sponsor: '/sponsor/submissions' }
|
||||
router.push(map[role] || '/doctor/submissions')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadSpecialPlans()
|
||||
// portal home 移动端适配: 解除 body min-width (避免手机端 1354px 横向滚动)
|
||||
document.body.classList.add('home-page-body')
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
document.body.classList.remove('home-page-body')
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -207,6 +201,7 @@ a { color: inherit; text-decoration: none; }
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
@@ -328,12 +323,46 @@ a { color: inherit; text-decoration: none; }
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* ========== 主体容器 ========== */
|
||||
/* 项目提案按钮 (机构官网风格: 白底 + 直角 + 品牌色描边 + 略长字距, hover 反色填充) */
|
||||
.cta-btn--inline {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 24px;
|
||||
background: #fff;
|
||||
color: var(--brand-primary);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 2px;
|
||||
cursor: pointer;
|
||||
border: 1.5px solid var(--brand-primary);
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||
transition: background 0.2s, color 0.2s, transform 0.2s, box-shadow 0.2s, border-color 0.2s;
|
||||
}
|
||||
.cta-btn--inline::before {
|
||||
content: '+';
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
}
|
||||
.cta-btn--inline:hover {
|
||||
background: var(--brand-primary);
|
||||
color: #fff;
|
||||
border-color: var(--brand-primary);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1354px;
|
||||
margin: 0 auto;
|
||||
padding: 0 60px;
|
||||
}
|
||||
.container--full {
|
||||
max-width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* ========== 区块标题 ========== */
|
||||
.section-title {
|
||||
@@ -359,15 +388,172 @@ a { color: inherit; text-decoration: none; }
|
||||
|
||||
/* ========== Hero ========== */
|
||||
.hero {
|
||||
margin-top: 30px;
|
||||
background: var(--brand-primary);
|
||||
color: #fff;
|
||||
height: 280px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
/* 抵消 PortalLayout 的 padding-top: 72px, 让首页 banner 真正贴屏幕顶端, 与 fixed navbar 融合 */
|
||||
.home-hero-wrap { margin-top: -72px; }
|
||||
.hero-banner {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
.hero-cta {
|
||||
position: absolute;
|
||||
right: 60px;
|
||||
bottom: 30px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.hero-cta .cta-btn {
|
||||
display: inline-block;
|
||||
padding: 12px 40px;
|
||||
background: #fff;
|
||||
color: var(--brand-primary);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 2px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.hero-cta .cta-btn:hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.specialty-section {
|
||||
background: linear-gradient(180deg, #f5f8fc 0%, #ecf0f5 100%);
|
||||
width: 100vw;
|
||||
margin-left: calc(-50vw + 50%);
|
||||
padding: 80px 0 70px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.specialty-inner {
|
||||
max-width: 1354px;
|
||||
margin: 0 auto;
|
||||
padding: 0 60px;
|
||||
}
|
||||
|
||||
.specialty-header {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.specialty-header-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.specialty-header-text .section-title {
|
||||
margin-bottom: 0;
|
||||
padding-left: 0;
|
||||
font-size: 30px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 1.5px;
|
||||
}
|
||||
.specialty-header-text .section-title::before { display: none; }
|
||||
.specialty-subtitle {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
letter-spacing: 2px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.specialty-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.specialty-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 4px;
|
||||
padding: 32px 28px 24px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
.specialty-empty {
|
||||
grid-column: 1 / -1;
|
||||
text-align: center;
|
||||
color: #9ca3af;
|
||||
padding: 60px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.specialty-card:hover {
|
||||
border-color: #94a3b8;
|
||||
transform: translateY(-6px);
|
||||
box-shadow: 0 16px 36px rgba(15, 23, 42, 0.14);
|
||||
}
|
||||
.specialty-card:hover .specialty-icon-bar {
|
||||
width: 32px;
|
||||
}
|
||||
.specialty-card:hover .specialty-icon-no {
|
||||
color: var(--brand-primary);
|
||||
}
|
||||
.specialty-card:hover .specialty-arrow {
|
||||
transform: translateX(8px);
|
||||
}
|
||||
|
||||
.specialty-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
gap: 12px;
|
||||
margin-bottom: 22px;
|
||||
height: 24px;
|
||||
}
|
||||
.specialty-icon-bar {
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
height: 12px;
|
||||
background: var(--brand-primary);
|
||||
transition: width 0.25s ease;
|
||||
}
|
||||
.specialty-icon-no {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
color: #94a3b8;
|
||||
font-family: "SF Mono", "Menlo", "Consolas", "Roboto Mono", "Courier New", monospace;
|
||||
letter-spacing: 1.5px;
|
||||
transition: color 0.25s ease;
|
||||
}
|
||||
|
||||
.specialty-title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
margin-bottom: 16px;
|
||||
line-height: 1.5;
|
||||
letter-spacing: 0.5px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.specialty-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.specialty-link {
|
||||
font-size: 13px;
|
||||
color: var(--brand-primary);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.specialty-arrow {
|
||||
color: var(--brand-primary);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.hero-tag {
|
||||
@@ -456,108 +642,6 @@ a { color: inherit; text-decoration: none; }
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ========== 七大专项 ========== */
|
||||
.specialty-section {
|
||||
background: #f5f6f8;
|
||||
width: 100vw;
|
||||
margin-left: calc(-50vw + 50%);
|
||||
padding: 50px 0 40px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.specialty-inner {
|
||||
max-width: 1354px;
|
||||
margin: 0 auto;
|
||||
padding: 0 60px;
|
||||
}
|
||||
|
||||
.specialty-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.specialty-card {
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s, transform 0.2s;
|
||||
}
|
||||
.specialty-empty {
|
||||
grid-column: 1 / -1;
|
||||
text-align: center;
|
||||
color: #9ca3af;
|
||||
padding: 60px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.specialty-card:hover {
|
||||
border-color: var(--brand-primary);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.specialty-no {
|
||||
flex-shrink: 0;
|
||||
width: 80px;
|
||||
background: var(--brand-primary);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32px;
|
||||
font-weight: 300;
|
||||
font-family: ui-monospace, "Courier New", monospace;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.specialty-body {
|
||||
flex: 1;
|
||||
padding: 20px 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.specialty-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin-bottom: 10px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.specialty-desc {
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
line-height: 1.7;
|
||||
flex: 1;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.specialty-action {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px solid #f3f4f6;
|
||||
}
|
||||
|
||||
.specialty-link {
|
||||
font-size: 13px;
|
||||
color: var(--brand-primary);
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.specialty-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.specialty-link.disabled {
|
||||
color: #9ca3af;
|
||||
cursor: not-allowed;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* ========== 移动端适配 ========== */
|
||||
@media (max-width: 768px) {
|
||||
/* 解除 body 最小宽度 (桌面 1354px 在手机端会横向滚动) */
|
||||
@@ -574,30 +658,14 @@ a { color: inherit; text-decoration: none; }
|
||||
|
||||
/* 主体容器 padding 收紧 */
|
||||
.container { padding: 0 16px !important; }
|
||||
/* hero 用的是 .container--full (铺满贴边), 不缩进 */
|
||||
.container--full { padding: 0 !important; max-width: 100% !important; }
|
||||
.specialty-inner { padding: 0 16px !important; }
|
||||
|
||||
/* Hero: 垂直堆叠 + 高度自适应 + CTA 不再绝对定位 */
|
||||
.hero {
|
||||
margin-top: 12px !important;
|
||||
height: auto !important;
|
||||
min-height: 220px !important;
|
||||
padding: 24px 20px 20px !important;
|
||||
flex-direction: column !important;
|
||||
align-items: flex-start !important;
|
||||
}
|
||||
.hero-tag { margin-bottom: 10px !important; }
|
||||
.hero-title { font-size: 24px !important; letter-spacing: 1px !important; }
|
||||
.hero-subtitle { font-size: 13px !important; letter-spacing: 2px !important; margin-bottom: 14px !important; }
|
||||
.hero-desc { font-size: 13px !important; line-height: 1.7 !important; padding-bottom: 16px !important; }
|
||||
.hero-desc br { display: none; }
|
||||
.hero-cta {
|
||||
position: static !important;
|
||||
transform: none !important;
|
||||
flex-direction: row !important;
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.hero-cta .cta-btn { padding: 10px 28px !important; font-size: 13px !important; }
|
||||
.hero { margin-top: 0 !important; }
|
||||
/* mobile 下 PortalLayout padding-top 收窄到 56px, 这里同步收窄抵消, 让 banner 紧贴 viewport 顶端 */
|
||||
.home-hero-wrap { margin-top: -56px !important; }
|
||||
|
||||
/* 总述区 */
|
||||
.overview-section { padding: 32px 0 20px !important; }
|
||||
@@ -605,12 +673,16 @@ a { color: inherit; text-decoration: none; }
|
||||
.plan-image { padding: 16px !important; }
|
||||
.section-title { font-size: 17px !important; margin-bottom: 16px !important; }
|
||||
|
||||
/* 七大专项: 单列 */
|
||||
.specialty-section { padding: 32px 0 24px !important; }
|
||||
.specialty-list { grid-template-columns: 1fr !important; gap: 12px !important; }
|
||||
.specialty-no { width: 56px !important; font-size: 22px !important; }
|
||||
.specialty-body { padding: 14px 16px !important; }
|
||||
.specialty-title { font-size: 14px !important; }
|
||||
/* 八大专项: 单列 */
|
||||
.specialty-section { padding: 40px 0 32px !important; }
|
||||
.specialty-header { flex-direction: column; align-items: flex-start !important; gap: 12px; margin-bottom: 20px !important; }
|
||||
.specialty-header-text .section-title { font-size: 22px !important; }
|
||||
.specialty-subtitle { font-size: 11px !important; }
|
||||
.specialty-list { grid-template-columns: 1fr !important; gap: 14px !important; }
|
||||
.specialty-card { padding: 22px 20px 18px !important; }
|
||||
.specialty-icon { margin-bottom: 14px !important; }
|
||||
.specialty-icon-no { font-size: 32px !important; letter-spacing: -0.5px !important; }
|
||||
.specialty-title { font-size: 15px !important; margin-bottom: 12px !important; }
|
||||
}
|
||||
|
||||
/* 汉堡按钮 (桌面隐藏, 手机显示) */
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="pub-page">
|
||||
<PortalNavbar />
|
||||
<PortalNavbar theme="solid-brand" />
|
||||
|
||||
<main class="container">
|
||||
<div class="page-header">
|
||||
@@ -681,9 +681,11 @@ main.container {
|
||||
.section-title { font-size: 18px !important; }
|
||||
.section-subtitle { font-size: 13px !important; }
|
||||
|
||||
/* 筛选条单列 */
|
||||
.filter-bar { flex-direction: column !important; gap: 8px !important; }
|
||||
/* 筛选条: 输入框 + 查询/重置按钮同一行 (按钮跟在搜索框后面) */
|
||||
.filter-bar { flex-direction: row !important; flex-wrap: nowrap !important; gap: 8px !important; align-items: center !important; }
|
||||
.filter-input-wrap { flex: 1 1 auto !important; min-width: 0 !important; }
|
||||
.filter-input { width: 100% !important; font-size: 14px !important; }
|
||||
.filter-btn { flex-shrink: 0 !important; padding: 0 14px !important; white-space: nowrap !important; }
|
||||
.filter-select-wrap { width: 100% !important; }
|
||||
.filter-select { width: 100% !important; }
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="detail-page">
|
||||
<PortalNavbar />
|
||||
<PortalNavbar theme="solid-brand" />
|
||||
|
||||
<main class="container">
|
||||
<a class="back-link" @click.prevent="goPublicity">
|
||||
@@ -10,6 +10,16 @@
|
||||
返回项目公示
|
||||
</a>
|
||||
|
||||
<!-- H5 顶部 4 tab (仅移动端显示, 桌面隐藏) -->
|
||||
<el-tabs v-model="activeTab" class="mobile-tabs" stretch>
|
||||
<el-tab-pane
|
||||
v-for="t in TAB_DEFS"
|
||||
:key="t.key"
|
||||
:label="t.label"
|
||||
:name="t.key"
|
||||
/>
|
||||
</el-tabs>
|
||||
|
||||
<div class="detail-layout">
|
||||
<!-- 左: 项目名 + 公告 tabs -->
|
||||
<aside class="detail-side">
|
||||
@@ -87,7 +97,9 @@
|
||||
</aside>
|
||||
</main>
|
||||
|
||||
<PortalFooter />
|
||||
<div class="detail-footer">
|
||||
<PortalFooter />
|
||||
</div>
|
||||
|
||||
<!-- 分享二维码弹窗 (el-dialog 自带右上角 X 关闭按钮 + Esc + 遮罩点击) -->
|
||||
<el-dialog v-model="showQr" title="分享本页" width="420px" align-center destroy-on-close>
|
||||
@@ -236,7 +248,15 @@ const tabList = computed(() => {
|
||||
}))
|
||||
})
|
||||
|
||||
const currentTab = computed(() => tabList.value.find(t => t.key === activeTab.value) || tabList.value[0])
|
||||
// 4 个 tab 全量解析 (含无文件 tab), 让 H5 点击任意 tab 都能正确显示对应内容/空态
|
||||
const currentTab = computed(() => {
|
||||
const p = ann.value
|
||||
const all = TAB_DEFS.map(t => ({ ...t, fileUrl: p?.[t.urlKey] }))
|
||||
const hit = all.find(t => t.key === activeTab.value)
|
||||
if (hit) return hit
|
||||
// 兜底: 第一个有文件的 tab; 再兜底第一个 tab
|
||||
return all.find(t => t.fileUrl) || all[0]
|
||||
})
|
||||
|
||||
// 是否招标项目 (is_bid_project = 'Y'): 通知 tab 下显示"查看招标公告"按钮
|
||||
const isBidProject = computed(() => {
|
||||
@@ -369,7 +389,9 @@ async function onSignup() {
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 公示页-支持/执行意向 (匿名 + 已登录均可) ============
|
||||
// ============ 公示页-支持/执行意向 ============
|
||||
// 支持意向: 匿名 + 已登录均可 (未登录走匿名 dialog)
|
||||
// 执行申请: 仅已登录可提交 (未登录跳登录页)
|
||||
const supportSubmitting = ref(false)
|
||||
const supportSubmitted = ref(false)
|
||||
const executionSubmitting = ref(false)
|
||||
@@ -412,7 +434,7 @@ function resetGuestForm() {
|
||||
async function prefillGuestFormFromUser() {
|
||||
const u = userStore.user || {}
|
||||
guestDialog.form = {
|
||||
name: u.nickName || u.userName || '',
|
||||
name: u.nickName || '',
|
||||
phone: u.phonenumber || u.phoneNumber || '',
|
||||
workUnit: '',
|
||||
department: '',
|
||||
@@ -459,18 +481,16 @@ function onSupportIntent() {
|
||||
|
||||
function onExecutionIntent() {
|
||||
if (!canShowExecutionBtn.value) return // 防御
|
||||
if (executionSubmitting.value || executionSubmitted.value) return
|
||||
if (loggedIn.value) {
|
||||
guestDialog.type = 'execution'
|
||||
guestDialog.title = '项目执行申请'
|
||||
guestDialog.open = true
|
||||
prefillGuestFormFromUser() // 异步, 不 await
|
||||
} else {
|
||||
resetGuestForm()
|
||||
guestDialog.type = 'execution'
|
||||
guestDialog.title = '项目执行申请'
|
||||
guestDialog.open = true
|
||||
if (!loggedIn.value) {
|
||||
ElMessage.warning('请先登录系统')
|
||||
router.push('/login')
|
||||
return
|
||||
}
|
||||
if (executionSubmitting.value || executionSubmitted.value) return
|
||||
guestDialog.type = 'execution'
|
||||
guestDialog.title = '项目执行申请'
|
||||
guestDialog.open = true
|
||||
prefillGuestFormFromUser() // 异步, 不 await
|
||||
}
|
||||
|
||||
async function onGuestDialogConfirm() {
|
||||
@@ -700,9 +720,38 @@ onBeforeUnmount(() => {
|
||||
|
||||
.action-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
|
||||
/* ========== 主体容器 ========== */
|
||||
.container {
|
||||
max-width: 1354px;
|
||||
margin: 0 auto;
|
||||
padding: 0 60px 60px;
|
||||
}
|
||||
|
||||
/* ========== H5 顶部 4 tab (桌面隐藏, 移动端显示) ========== */
|
||||
.mobile-tabs { display: none; }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.detail-layout { grid-template-columns: 1fr; padding-right: 24px; }
|
||||
.detail-side { border: none; padding: 0; }
|
||||
.container { padding: 0 16px 96px; }
|
||||
.detail-layout { grid-template-columns: 1fr; padding: 12px 0 24px; }
|
||||
.detail-side { display: none; }
|
||||
.detail-footer { display: none; }
|
||||
|
||||
/* H5 顶部 4 tab (el-tabs 下划线风格, 简洁清爽) */
|
||||
.mobile-tabs {
|
||||
display: block;
|
||||
margin: 8px 0 0;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.mobile-tabs :deep(.el-tabs__header) { margin: 0; }
|
||||
.mobile-tabs :deep(.el-tabs__nav-wrap::after) { height: 1px; background: #ebeef5; }
|
||||
.mobile-tabs :deep(.el-tabs__item) { font-size: 14px; height: 48px; padding: 0; }
|
||||
.mobile-tabs :deep(.el-tabs__item:hover) { color: var(--brand-primary); }
|
||||
.mobile-tabs :deep(.el-tabs__item.is-active) { color: var(--brand-primary); }
|
||||
.mobile-tabs :deep(.el-tabs__active-bar) { height: 3px; border-radius: 2px 2px 0 0; background-color: var(--brand-primary); }
|
||||
.mobile-tabs :deep(.el-tabs__content) { display: none; }
|
||||
|
||||
/* 移动端隐藏悬浮按钮, 改成底部固定按钮栏 */
|
||||
.detail-actions {
|
||||
position: fixed;
|
||||
@@ -779,6 +828,7 @@ a { color: inherit; text-decoration: none; }
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
@@ -894,13 +944,6 @@ a { color: inherit; text-decoration: none; }
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
/* ========== 主体 ========== */
|
||||
.container {
|
||||
max-width: 1354px;
|
||||
margin: 0 auto;
|
||||
padding: 0 60px 60px;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -139,7 +139,7 @@ async function onUserCmd(cmd) {
|
||||
if (cmd === 'logout') return goLogout()
|
||||
if (cmd === 'account') {
|
||||
const r = (userStore.user?.role || '')
|
||||
const map = { admin: '/admin/workbench', manager: '/manager/workbench', doctor: '/doctor/home', executor: '/executor/meetings', sponsor: '/sponsor/home' }
|
||||
const map = { admin: '/admin/workbench', manager: '/manager/workbench', doctor: '/doctor/home', executor: '/executor/overview', sponsor: '/sponsor/home' }
|
||||
router.push(map[r] || '/admin/workbench')
|
||||
}
|
||||
}
|
||||
@@ -172,6 +172,9 @@ body {
|
||||
}
|
||||
a { color: inherit; text-decoration: none; }
|
||||
|
||||
/* PortalLayout padding-top: 72px 是给 fixed navbar 留位, 本页 .top-nav 是 sticky 已占文档流, 上移抵消 (与 PortalShell 同处理) */
|
||||
.detail-page { margin-top: -72px; }
|
||||
|
||||
/* ===== 顶部导航 (与 PublicityDetail 一致) ===== */
|
||||
.top-nav {
|
||||
position: sticky; top: 0; z-index: 1000;
|
||||
@@ -183,7 +186,7 @@ a { color: inherit; text-decoration: none; }
|
||||
transition: box-shadow 0.3s;
|
||||
}
|
||||
.top-nav.is-scrolled { box-shadow: 0 4px 20px rgba(0,0,0,0.25); }
|
||||
.logo { display: flex; align-items: center; gap: 12px; }
|
||||
.logo { display: flex; align-items: center; gap: 12px; cursor: pointer; }
|
||||
.logo-icon {
|
||||
width: 36px; height: 36px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
@@ -193,7 +196,13 @@ a { color: inherit; text-decoration: none; }
|
||||
.logo-text { display: flex; flex-direction: column; line-height: 1.2; }
|
||||
.logo-title { font-size: 16px; font-weight: 600; color: #fff; letter-spacing: 0.5px; }
|
||||
.logo-subtitle { font-size: 11px; color: rgba(255,255,255,0.6); margin-top: 2px; letter-spacing: 0.3px; }
|
||||
.nav-list { flex: 1; display: flex; align-items: center; justify-content: center; gap: 36px; list-style: none; }
|
||||
.nav-list {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
gap: 36px; list-style: none;
|
||||
}
|
||||
.nav-item { position: relative; height: 72px; display: flex; align-items: center; }
|
||||
.nav-link {
|
||||
font-size: 15px; font-weight: 500; color: rgba(255,255,255,0.85);
|
||||
@@ -207,7 +216,7 @@ a { color: inherit; text-decoration: none; }
|
||||
}
|
||||
.nav-item:hover .nav-link, .nav-link.active { color: #fff; }
|
||||
.nav-item:hover .nav-link::after, .nav-link.active::after { width: 100%; }
|
||||
.top-tools { display: flex; align-items: center; gap: 16px; }
|
||||
.top-tools { display: flex; align-items: center; gap: 16px; margin-left: auto; }
|
||||
.login-btn {
|
||||
padding: 7px 20px; background: #fff; color: var(--brand-primary);
|
||||
font-size: 13px; font-weight: 500;
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
</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="isAdmin ? 320 : 240" fixed="right">
|
||||
<el-table-column label="操作" :width="isAdmin ? 240 : 180" fixed="right">
|
||||
<template #default="{ row }"><div class="table-actions">
|
||||
<!-- 查看: 两角色都有 (manager 原版本有, admin 原版本用 alert, 这里统一用 dialog 更清晰) -->
|
||||
<el-link :underline="false" type="primary" @click="onView(row)">查看</el-link>
|
||||
@@ -366,36 +366,13 @@ onMounted(load)
|
||||
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
|
||||
|
||||
/* ========================================
|
||||
移动端适配 (≤768px)
|
||||
- 卡片 padding 收窄
|
||||
- 工具栏横向滚动 (6 个按钮铺不下, 用 overflow-x)
|
||||
- filter-form: 保持 label 左 + input 右横向布局
|
||||
- 表格字号收紧
|
||||
移动端适配 (≤768px) — 复用 Meetings.vue 标准模式
|
||||
======================================== */
|
||||
@media (max-width: 768px) {
|
||||
/* 卡片 padding */
|
||||
.page-card { padding: 12px !important; border-radius: 4px !important; }
|
||||
.breadcrumb { font-size: 12px !important; margin-bottom: 8px !important; }
|
||||
|
||||
/* 工具栏: 横向滚动 (按钮多时不换行) */
|
||||
.toolbar {
|
||||
flex-wrap: nowrap !important;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding-bottom: 6px;
|
||||
margin-bottom: 8px !important;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.toolbar::-webkit-scrollbar { height: 4px; }
|
||||
.toolbar::-webkit-scrollbar-thumb { background: var(--brand-slate-200); border-radius: 2px; }
|
||||
.toolbar :deep(.action-btn),
|
||||
.toolbar :deep(.el-button) {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px !important;
|
||||
padding: 0 10px !important;
|
||||
height: 30px !important;
|
||||
}
|
||||
|
||||
/* filter-form: label 与输入框横向 (label 左, input 右) */
|
||||
.filter-form {
|
||||
display: flex !important;
|
||||
@@ -427,7 +404,7 @@ onMounted(load)
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
/* 强制所有控件全宽, 覆盖 inline 260px / 140px / 120px */
|
||||
/* 强制所有控件全宽, 覆盖 inline 200px / 220px / 140px */
|
||||
.filter-form :deep(.el-select),
|
||||
.filter-form :deep(.el-input),
|
||||
.filter-form :deep(.el-date-editor),
|
||||
@@ -443,6 +420,25 @@ onMounted(load)
|
||||
margin-top: 8px !important;
|
||||
}
|
||||
|
||||
/* batch-bar: 横向滚动 */
|
||||
.batch-bar {
|
||||
flex-wrap: nowrap !important;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding-bottom: 6px;
|
||||
margin-bottom: 8px !important;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.batch-bar::-webkit-scrollbar { height: 4px; }
|
||||
.batch-bar::-webkit-scrollbar-thumb { background: var(--brand-slate-200); border-radius: 2px; }
|
||||
.batch-bar :deep(.action-btn),
|
||||
.batch-bar :deep(.el-button) {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px !important;
|
||||
padding: 0 10px !important;
|
||||
height: 30px !important;
|
||||
}
|
||||
|
||||
/* 表格字号收紧 */
|
||||
:deep(.el-table) { font-size: 12px !important; }
|
||||
}
|
||||
|
||||
@@ -61,18 +61,25 @@
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="340" fixed="right">
|
||||
<el-table-column label="操作" width="140" fixed="right">
|
||||
<template #default="{ row }"><div class="table-actions">
|
||||
<!-- 查看 (只读详情): 两角色都有, 跳独立详情页 -->
|
||||
<!-- 查看: 保留为直链 (高频操作) -->
|
||||
<el-link :underline="false" type="primary" @click="goView(row)">查看</el-link>
|
||||
<!-- 编辑: 两角色都有, 跳独立编辑页 -->
|
||||
<el-link :underline="false" type="primary" @click="goEdit(row)">编辑</el-link>
|
||||
<!-- 启用/禁用: 两角色都有 (沿用原行为) -->
|
||||
<el-link :underline="false" :type="isDisabled(row) ? 'success' : 'danger'" @click="onToggleStatus(row)">
|
||||
{{ isDisabled(row) ? '启用' : '禁用' }}
|
||||
</el-link>
|
||||
<!-- 重置密码: 重置为默认 123456 (与新建人员默认密码一致) -->
|
||||
<el-link :underline="false" v-if="row.userId" type="primary" @click="onResetPwd(row)">重置密码</el-link>
|
||||
<!-- 更多: 编辑 + 启用/禁用 折叠到 el-dropdown -->
|
||||
<el-dropdown trigger="click" @command="(cmd) => onMoreAction(cmd, row)">
|
||||
<el-link :underline="false" type="primary" class="more-link">
|
||||
更多<el-icon class="more-icon"><ArrowDown /></el-icon>
|
||||
</el-link>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="edit">编辑</el-dropdown-item>
|
||||
<el-dropdown-item command="resetPwd" v-if="row.userId">重置密码</el-dropdown-item>
|
||||
<el-dropdown-item :command="isDisabled(row) ? 'enable' : 'disable'">
|
||||
{{ isDisabled(row) ? '启用' : '禁用' }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -123,7 +130,7 @@ import { ref, reactive, onMounted, computed, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { bizList, bizUpdate, importPerson, downloadImportTemplate, changePersonAdmin, resetPersonPassword } from '@/api/public'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Upload } from '@element-plus/icons-vue'
|
||||
import { Upload, ArrowDown } from '@element-plus/icons-vue'
|
||||
import ImportResultDialog from '@/components/ImportResultDialog.vue'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -262,6 +269,13 @@ function goView(row) {
|
||||
})
|
||||
}
|
||||
|
||||
// 操作列 "更多" 下拉: 分发编辑/重置密码/启用/禁用
|
||||
function onMoreAction(cmd, row) {
|
||||
if (cmd === 'edit') return goEdit(row)
|
||||
if (cmd === 'resetPwd') return onResetPwd(row)
|
||||
if (cmd === 'enable' || cmd === 'disable') return onToggleStatus(row)
|
||||
}
|
||||
|
||||
// ========== 批量导入 ==========
|
||||
const importOpen = ref(false)
|
||||
const importing = ref(false)
|
||||
@@ -404,4 +418,8 @@ watch(() => route.fullPath, () => {
|
||||
/* 表格字号收紧 */
|
||||
:deep(.el-table) { font-size: 12px !important; }
|
||||
}
|
||||
|
||||
/* 操作列 "更多" 下拉样式 (桌面 + 移动端共用) */
|
||||
.more-link { display: inline-flex; align-items: center; gap: 2px; }
|
||||
.more-icon { font-size: 12px; }
|
||||
</style>
|
||||
|
||||
@@ -152,83 +152,33 @@ onMounted(loadDetail)
|
||||
|
||||
/* ========================================
|
||||
移动端适配 (≤768px)
|
||||
- 卡片 padding 收窄
|
||||
- 工具栏横向滚动
|
||||
- filter-form: label 与输入框横向
|
||||
- 表格字号收紧
|
||||
======================================== */
|
||||
@media (max-width: 768px) {
|
||||
/* 卡片 padding */
|
||||
.page-card { padding: 12px !important; border-radius: 4px !important; }
|
||||
.breadcrumb { font-size: 12px !important; margin-bottom: 8px !important; }
|
||||
|
||||
/* 工具栏: 横向滚动 */
|
||||
.toolbar {
|
||||
flex-wrap: nowrap !important;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding-bottom: 6px;
|
||||
margin-bottom: 8px !important;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.toolbar::-webkit-scrollbar { height: 4px; }
|
||||
.toolbar::-webkit-scrollbar-thumb { background: var(--brand-slate-200); border-radius: 2px; }
|
||||
.toolbar :deep(.action-btn),
|
||||
.toolbar :deep(.el-button) {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px !important;
|
||||
padding: 0 10px !important;
|
||||
height: 30px !important;
|
||||
}
|
||||
/* form-card 内部 padding 收窄 */
|
||||
:deep(.form-card .el-card__body) { padding: 12px !important; }
|
||||
|
||||
/* filter-form: 横向 (label 左 + input 右) */
|
||||
.filter-form {
|
||||
/* form-item: 横向 label + content, flex-start 让 error message 占独立行
|
||||
不动 __label — Element Plus 自带 label-width=100px 自然对齐 */
|
||||
:deep(.el-form-item) {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
align-items: stretch !important;
|
||||
gap: 10px !important;
|
||||
}
|
||||
.filter-form :deep(.el-form-item) {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
align-items: flex-start !important;
|
||||
margin-right: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.filter-form :deep(.el-form-item__label) {
|
||||
float: none !important;
|
||||
width: auto !important;
|
||||
min-width: 80px !important;
|
||||
text-align: right !important;
|
||||
padding: 0 8px 0 0 !important;
|
||||
font-size: 13px !important;
|
||||
color: var(--el-text-color-regular) !important;
|
||||
line-height: 32px !important;
|
||||
height: 32px !important;
|
||||
}
|
||||
.filter-form :deep(.el-form-item__content) {
|
||||
margin-left: 0 !important;
|
||||
line-height: 32px !important;
|
||||
:deep(.el-form-item__content) {
|
||||
flex: 1 !important;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
/* 强制所有控件全宽 */
|
||||
.filter-form :deep(.el-select),
|
||||
.filter-form :deep(.el-input),
|
||||
.filter-form :deep(.el-date-editor),
|
||||
.filter-form :deep(.el-button),
|
||||
.filter-form :deep(.el-cascader) {
|
||||
/* 底部按钮: 占满整行 (返回按钮单独占一行) */
|
||||
.form-actions {
|
||||
margin-top: 12px !important;
|
||||
}
|
||||
.form-actions :deep(.el-button) {
|
||||
width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
margin-left: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
display: block !important;
|
||||
}
|
||||
.filter-form :deep(.el-button + .el-button) {
|
||||
margin-top: 8px !important;
|
||||
}
|
||||
|
||||
/* 表格字号收紧 */
|
||||
:deep(.el-table) { font-size: 12px !important; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -182,83 +182,36 @@ onMounted(() => {
|
||||
|
||||
/* ========================================
|
||||
移动端适配 (≤768px)
|
||||
- 卡片 padding 收窄
|
||||
- 工具栏横向滚动
|
||||
- filter-form: label 与输入框横向
|
||||
- 表格字号收紧
|
||||
======================================== */
|
||||
@media (max-width: 768px) {
|
||||
/* 卡片 padding */
|
||||
.page-card { padding: 12px !important; border-radius: 4px !important; }
|
||||
.breadcrumb { font-size: 12px !important; margin-bottom: 8px !important; }
|
||||
|
||||
/* 工具栏: 横向滚动 */
|
||||
.toolbar {
|
||||
flex-wrap: nowrap !important;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding-bottom: 6px;
|
||||
margin-bottom: 8px !important;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.toolbar::-webkit-scrollbar { height: 4px; }
|
||||
.toolbar::-webkit-scrollbar-thumb { background: var(--brand-slate-200); border-radius: 2px; }
|
||||
.toolbar :deep(.action-btn),
|
||||
.toolbar :deep(.el-button) {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px !important;
|
||||
padding: 0 10px !important;
|
||||
height: 30px !important;
|
||||
}
|
||||
/* form-card 内部 padding 收窄 */
|
||||
:deep(.form-card .el-card__body) { padding: 12px !important; }
|
||||
|
||||
/* filter-form: 横向 (label 左 + input 右) */
|
||||
.filter-form {
|
||||
/* form-item: 横向 label + content, flex-start 让 error message 占独立行
|
||||
不动 __label — Element Plus 自带 label-width=100px 自然对齐 */
|
||||
:deep(.el-form-item) {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
align-items: stretch !important;
|
||||
gap: 10px !important;
|
||||
}
|
||||
.filter-form :deep(.el-form-item) {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
align-items: flex-start !important;
|
||||
margin-right: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.filter-form :deep(.el-form-item__label) {
|
||||
float: none !important;
|
||||
width: auto !important;
|
||||
min-width: 80px !important;
|
||||
text-align: right !important;
|
||||
padding: 0 8px 0 0 !important;
|
||||
font-size: 13px !important;
|
||||
color: var(--el-text-color-regular) !important;
|
||||
line-height: 32px !important;
|
||||
height: 32px !important;
|
||||
}
|
||||
.filter-form :deep(.el-form-item__content) {
|
||||
margin-left: 0 !important;
|
||||
line-height: 32px !important;
|
||||
:deep(.el-form-item__content) {
|
||||
flex: 1 !important;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
/* 强制所有控件全宽 */
|
||||
.filter-form :deep(.el-select),
|
||||
.filter-form :deep(.el-input),
|
||||
.filter-form :deep(.el-date-editor),
|
||||
.filter-form :deep(.el-button),
|
||||
.filter-form :deep(.el-cascader) {
|
||||
width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
margin-left: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
display: block !important;
|
||||
/* 底部按钮: 等宽并排 */
|
||||
.form-actions {
|
||||
flex-wrap: wrap !important;
|
||||
gap: 8px !important;
|
||||
margin-top: 12px !important;
|
||||
}
|
||||
.filter-form :deep(.el-button + .el-button) {
|
||||
margin-top: 8px !important;
|
||||
.form-actions :deep(.el-button) {
|
||||
flex: 1 1 0 !important;
|
||||
width: 0 !important;
|
||||
}
|
||||
|
||||
/* 表格字号收紧 */
|
||||
:deep(.el-table) { font-size: 12px !important; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -53,7 +53,7 @@ async function onSave() {
|
||||
await formRef.value.validate()
|
||||
saving.value = true
|
||||
try {
|
||||
await request({ url: '/system/user/profile', method: 'put', data: { nickName: form.nickName, phonenumber: form.phonenumber, sex: profile.value.sex } })
|
||||
await request({ url: '/business/person/profile', method: 'put', data: { name: form.nickName, phone: form.phonenumber } })
|
||||
if (form.newPassword) {
|
||||
await request({ url: '/system/user/profile/updatePwd', method: 'put', data: { oldPassword: form.oldPassword, newPassword: form.newPassword } })
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<div class="stat-value">{{ stats.completedProjects }}</div>
|
||||
<div class="stat-desc">查看已结题项目</div>
|
||||
</router-link>
|
||||
<router-link class="stat-card" to="/sponsor/meetings?currentStage=RUNNING">
|
||||
<router-link class="stat-card" to="/sponsor/meetings?currentStageNotIn=NOT_STARTED,IN_PROGRESS">
|
||||
<div class="stat-label">已执行会议</div>
|
||||
<div class="stat-value">{{ stats.executedMeetings }}</div>
|
||||
<div class="stat-desc">查看已执行会议</div>
|
||||
@@ -40,14 +40,14 @@
|
||||
<!-- 消息通知 (NoticeList 组件内部已含 SSE 实时刷新 + 详情 dialog + 全部已读) -->
|
||||
<section class="section">
|
||||
<h2 class="section-title">消息通知<router-link class="more" to="/sponsor/messages">更多 →</router-link></h2>
|
||||
<NoticeList :limit="50" />
|
||||
<NoticeList :pageable="true" :show-header="false" />
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { bizList } from '@/api/public'
|
||||
import request from '@/utils/request'
|
||||
import NoticeList from '@/components/NoticeList.vue'
|
||||
|
||||
// 字段名跟 Projects.vue / Meetings.vue q 一致, 之前 3/5 卡 stats 不更新或用错字段名 (status 后端不认, settledMeetings 误指项目级 isSettled)
|
||||
@@ -55,28 +55,31 @@ const stats = ref({
|
||||
totalProjects: 0,
|
||||
settledProjects: 0, // 项目级 isSettled=Y
|
||||
completedProjects: 0, // 项目级 isFinished=1
|
||||
executedMeetings: 0, // 会议 currentStage=RUNNING (字段名跟 Meetings.vue q 一致, 之前误用 status= 后端不认, 数字一直是错的)
|
||||
pendingMeetings: 0 // 总会议 - 已执行
|
||||
executedMeetings: 0, // 已执行 = 排除 NOT_STARTED(未执行)/IN_PROGRESS(执行中) 的其余 stage (含 FROZEN)
|
||||
pendingMeetings: 0 // 未执行 = NOT_STARTED
|
||||
})
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
// 项目总数量 (跟 manager/executor 一样调普通 list, 后续若需要按 sponsor 隔离再换 sponsorList 接口)
|
||||
const ps = await bizList('project', { pageNum: 1, pageSize: 1 })
|
||||
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 })
|
||||
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)
|
||||
// 项目统计走 sponsorList: 后端按当前账号隔离 (MAIN=本支持单位全部, SUB=自己负责的项目)
|
||||
const projectCount = async (extra = {}) => {
|
||||
const { data } = await request.get('/business/project/sponsorList', { params: { pageNum: 1, pageSize: 1, ...extra } })
|
||||
return data?.total || 0
|
||||
}
|
||||
stats.value.totalProjects = await projectCount()
|
||||
stats.value.settledProjects = await projectCount({ isSettled: 'Y' })
|
||||
stats.value.completedProjects = await projectCount({ isFinished: '1' })
|
||||
// 会议统计走 /business/meeting/stageStats (后端已按 sponsor MAIN/SUB 隔离), 返回 { stage: cnt }
|
||||
const ss = await request.get('/business/meeting/stageStats')
|
||||
const stageMap = ss?.data || {}
|
||||
let executed = 0, pending = 0
|
||||
for (const [stage, cnt] of Object.entries(stageMap)) {
|
||||
const n = Number(cnt) || 0
|
||||
if (stage === 'NOT_STARTED') pending += n
|
||||
else if (stage !== 'IN_PROGRESS') executed += n
|
||||
}
|
||||
stats.value.executedMeetings = executed
|
||||
stats.value.pendingMeetings = pending
|
||||
} catch (e) {
|
||||
console.warn('loadStats failed', e)
|
||||
}
|
||||
|
||||
@@ -1,413 +0,0 @@
|
||||
<template>
|
||||
<div class="page-card">
|
||||
<div class="breadcrumb">首页 / 会议列表</div>
|
||||
|
||||
<!-- ========== 筛选区 (按 sidebar=项目管理/... 的支持方原型 会议管理.html) ========== -->
|
||||
<el-form inline :model="q" class="filter-form">
|
||||
<el-form-item label="项目编号">
|
||||
<el-input v-model="q.projectNo" placeholder="项目编号" clearable style="width:160px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="会议ID">
|
||||
<el-input v-model="q.meetingId" placeholder="会议ID" clearable style="width:160px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="会议名称">
|
||||
<el-input v-model="q.meetingName" placeholder="会议名称" clearable style="width:180px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="期数">
|
||||
<el-input-number v-model="q.periodNo" :min="1" placeholder="期数" style="width:110px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="会议时间起">
|
||||
<el-date-picker v-model="q.startTimeRange" type="datetimerange" range-separator="至"
|
||||
start-placeholder="开始时间" end-placeholder="结束时间" style="width:340px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="项目形式">
|
||||
<el-select v-model="q.projectForm" clearable placeholder="全部" style="width:120px">
|
||||
<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" clearable placeholder="全部" style="width:140px">
|
||||
<el-option label="未执行" value="NOT_STARTED" />
|
||||
<el-option label="执行中" value="RUNNING" />
|
||||
<el-option label="待合规审核" value="AWAITING_COMPLIANCE" />
|
||||
<el-option label="待支持方审核" value="AWAITING_SPONSOR" />
|
||||
<el-option label="待结算" value="AWAITING_SETTLEMENT" />
|
||||
<el-option label="已完结" value="SETTLED" />
|
||||
<el-option label="冻结中" value="FROZEN" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="q.remark" placeholder="备注" clearable style="width:160px" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="loadList">查找</el-button>
|
||||
<el-button @click="reset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- ========== 批量按钮区 (top=232, 3 个按钮) ========== -->
|
||||
<div class="batch-bar">
|
||||
<el-button :disabled="!selected.length" @click="onBatch('labor')">批量下载劳务材料</el-button>
|
||||
<el-button :disabled="!selected.length" @click="onBatch('meeting')">批量下载会务材料</el-button>
|
||||
<el-button :disabled="!selected.length" type="warning" @click="onBatchAudit">批量审核</el-button>
|
||||
</div>
|
||||
|
||||
<!-- ========== 表格区 (按原型列头) ========== -->
|
||||
<GrTable :data="list" border stripe v-loading="loading" @selection-change="onSelectionChange"
|
||||
:main-cols="['projectNo', 'meetingName']"
|
||||
>
|
||||
<el-table-column type="selection" width="44" />
|
||||
<el-table-column prop="projectNo" label="项目编号" min-width="170" />
|
||||
<el-table-column prop="meetingName" label="会议名称" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="总期数" prop="totalPeriod" width="80" />
|
||||
<el-table-column label="期数" prop="periodNo" width="70" />
|
||||
<el-table-column label="当前阶段" prop="currentStage" width="100" />
|
||||
<el-table-column label="会议开始时间" prop="startTime" min-width="160" />
|
||||
<el-table-column label="会议结束时间" prop="endTime" min-width="160" />
|
||||
<el-table-column label="提交剩余时间" width="140">
|
||||
<template #default="{ row }">{{ calcRemain(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="项目形式" prop="projectForm" width="100" />
|
||||
<el-table-column label="操作" width="420" fixed="right">
|
||||
<template #default="{ row }"><div class="table-actions">
|
||||
<el-link :underline="false" size="small" type="primary" @click="onView(row)">查看</el-link>
|
||||
<el-link :underline="false" size="small" type="primary" @click="onEdit(row)">修改</el-link>
|
||||
<el-link :underline="false" size="small" type="warning" :disabled="!canSettle(row)" @click="onSettle(row)">结算</el-link>
|
||||
<el-link :underline="false" size="small" type="primary" @click="onAudit(row)">审核</el-link>
|
||||
<el-link :underline="false" size="small" @click="onDownload(row, 'labor')">劳务下载</el-link>
|
||||
<el-link :underline="false" size="small" @click="onDownload(row, 'meeting')">会务下载</el-link>
|
||||
<el-link :underline="false" size="small" type="danger" :disabled="row.currentStage !== 'FROZEN'" @click="onUnfreeze(row)">解冻</el-link>
|
||||
</div></template>
|
||||
</el-table-column>
|
||||
</GrTable>
|
||||
<div class="pager">
|
||||
<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="loadList" @size-change="loadList" />
|
||||
</div>
|
||||
|
||||
<!-- ========== Dialog: 详情 ========== -->
|
||||
<el-dialog v-model="viewOpen" title="会议详情" width="600px">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="项目编号">{{ view.projectNo }}</el-descriptions-item>
|
||||
<el-descriptions-item label="项目形式">{{ view.projectForm }}</el-descriptions-item>
|
||||
<el-descriptions-item label="会议名称" :span="2">{{ view.meetingName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="会议ID">{{ view.businessId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="当前阶段">{{ view.currentStage }}</el-descriptions-item>
|
||||
<el-descriptions-item label="开始时间" :span="2">{{ view.startTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="结束时间" :span="2">{{ view.endTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">{{ view.remark }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<template #footer>
|
||||
<el-button @click="viewOpen = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- ========== Dialog: 修改 ========== -->
|
||||
<el-dialog v-model="editOpen" title="修改会议" width="560px">
|
||||
<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-date-picker v-model="editForm.startTime" type="datetime" style="width:100%" /></el-form-item>
|
||||
<el-form-item label="会议结束时间"><el-date-picker v-model="editForm.endTime" type="datetime" style="width:100%" /></el-form-item>
|
||||
<el-form-item label="项目形式">
|
||||
<el-select v-model="editForm.projectForm" 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.remark" type="textarea" rows="3" /></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>
|
||||
|
||||
<!-- ========== Dialog: 审核 (原型 u3136: 通过/退回 + 意见) ========== -->
|
||||
<el-dialog v-model="auditOpen" title="审核" width="520px">
|
||||
<el-form :model="auditForm" label-width="100px">
|
||||
<el-form-item label="会议名称">
|
||||
<span>{{ auditRow?.meetingName }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="审核结果">
|
||||
<el-radio-group v-model="auditForm.action">
|
||||
<el-radio value="pass">通过</el-radio>
|
||||
<el-radio value="reject">退回</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="审核意见">
|
||||
<p style="margin:0 0 6px;font-size:12px;color:#909399">*意见为非必填项,未填写则默认显示无</p>
|
||||
<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="auditSubmitting" @click="submitAudit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- ========== Dialog: 解冻确认 ========== -->
|
||||
<el-dialog v-model="unfreezeOpen" title="解冻确认" width="420px">
|
||||
<p>您确定要解冻吗?</p>
|
||||
<p style="margin-top:8px;color:#606266">会议: {{ unfreezeRow?.meetingName }}</p>
|
||||
<template #footer>
|
||||
<el-button @click="unfreezeOpen = false">取消</el-button>
|
||||
<el-button type="primary" :loading="unfreezeSubmitting" @click="submitUnfreeze">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import GrTable from '@/components/GrTable.vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { bizList, bizUpdate } from '@/api/public'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const list = ref([])
|
||||
const loading = ref(false)
|
||||
const selected = ref([])
|
||||
const q = reactive({
|
||||
projectNo: '', meetingId: '', meetingName: '', periodNo: null,
|
||||
startTimeRange: null, projectForm: '', currentStage: '', remark: ''
|
||||
})
|
||||
const page = reactive({ pageNum: 1, pageSize: 20, total: 0 })
|
||||
|
||||
const view = ref({})
|
||||
const viewOpen = ref(false)
|
||||
const editOpen = ref(false)
|
||||
const editSubmitting = ref(false)
|
||||
const editForm = reactive({ meetingId: null, meetingName: '', startTime: null, endTime: null, projectForm: '', remark: '' })
|
||||
const auditOpen = ref(false)
|
||||
const auditRow = ref(null)
|
||||
const auditSubmitting = ref(false)
|
||||
const auditForm = reactive({ action: 'pass', opinion: '' })
|
||||
const unfreezeOpen = ref(false)
|
||||
const unfreezeRow = ref(null)
|
||||
const unfreezeSubmitting = ref(false)
|
||||
|
||||
// 读 URL query 写入 q (Home KPI 卡跳转时带 ?currentStage=RUNNING/NOT_STARTED, 让列表页自动应用筛选)
|
||||
// 只读不改 URL — Home 是 source of truth, 列表页内 reset()/search 不反向写 URL
|
||||
const route = useRoute()
|
||||
function readQueryFromRoute() {
|
||||
const q2 = route.query
|
||||
if (q2.currentStage != null && q2.currentStage !== '') q.currentStage = String(q2.currentStage)
|
||||
}
|
||||
|
||||
async function loadList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
pageNum: page.pageNum, pageSize: page.pageSize,
|
||||
...q,
|
||||
startTime: q.startTimeRange?.[0] || '',
|
||||
endTime: q.startTimeRange?.[1] || ''
|
||||
}
|
||||
delete params.startTimeRange
|
||||
const r = await bizList('meeting', params)
|
||||
list.value = (r.data && r.data.rows) || r.rows || []
|
||||
page.total = (r.data && r.data.total) || r.total || 0
|
||||
} catch (e) { list.value = []; page.total = 0 }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
function reset() {
|
||||
Object.assign(q, { projectNo: '', meetingId: '', meetingName: '', periodNo: null, startTimeRange: null, projectForm: '', currentStage: '', remark: '' })
|
||||
page.pageNum = 1
|
||||
loadList()
|
||||
}
|
||||
|
||||
function onSelectionChange(rows) { selected.value = rows }
|
||||
|
||||
function onView(row) { view.value = row; viewOpen.value = true }
|
||||
|
||||
function onEdit(row) {
|
||||
Object.assign(editForm, row)
|
||||
editForm.startTime = row.startTime || null
|
||||
editForm.endTime = row.endTime || null
|
||||
editOpen.value = true
|
||||
}
|
||||
async function submitEdit() {
|
||||
editSubmitting.value = true
|
||||
try {
|
||||
await bizUpdate('meeting', editForm)
|
||||
ElMessage.success('修改成功')
|
||||
editOpen.value = false
|
||||
loadList()
|
||||
} catch (e) { ElMessage.error(e?.msg || '修改失败') }
|
||||
finally { editSubmitting.value = false }
|
||||
}
|
||||
|
||||
function onAudit(row) {
|
||||
auditRow.value = row
|
||||
auditForm.action = 'pass'
|
||||
auditForm.opinion = ''
|
||||
auditOpen.value = true
|
||||
}
|
||||
async function submitAudit() {
|
||||
if (!auditRow.value) return
|
||||
auditSubmitting.value = true
|
||||
try {
|
||||
const text = auditForm.opinion || '无'
|
||||
await bizUpdate('meeting', {
|
||||
meetingId: auditRow.value.meetingId,
|
||||
auditStatus: auditForm.action === 'pass' ? '通过' : '退回',
|
||||
auditOpinion: text
|
||||
})
|
||||
ElMessage.success('审核成功')
|
||||
auditOpen.value = false
|
||||
loadList()
|
||||
} catch (e) { ElMessage.error(e?.msg || '审核失败') }
|
||||
finally { auditSubmitting.value = false }
|
||||
}
|
||||
|
||||
function canSettle(row) {
|
||||
return row && ['NOT_STARTED', 'RUNNING'].includes(row.currentStage)
|
||||
}
|
||||
function onSettle(row) {
|
||||
ElMessageBox.confirm(`确定发起「${row.meetingName}」结算吗?`, '结算', { type: 'warning' })
|
||||
.then(async () => {
|
||||
await bizUpdate('meeting', { meetingId: row.meetingId, currentStage: 'SETTLED' })
|
||||
ElMessage.success('已发起结算')
|
||||
loadList()
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
function onDownload(row, type) {
|
||||
const url = type === 'labor' ? row.laborUrl : row.meetingUrl
|
||||
if (!url) { ElMessage.warning('暂无文件'); return }
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
function onUnfreeze(row) {
|
||||
unfreezeRow.value = row
|
||||
unfreezeOpen.value = true
|
||||
}
|
||||
async function submitUnfreeze() {
|
||||
if (!unfreezeRow.value) return
|
||||
unfreezeSubmitting.value = true
|
||||
try {
|
||||
await bizUpdate('meeting', { meetingId: unfreezeRow.value.meetingId, currentStage: 'RUNNING' })
|
||||
ElMessage.success('解冻成功')
|
||||
unfreezeOpen.value = false
|
||||
loadList()
|
||||
} catch (e) { ElMessage.error(e?.msg || '解冻失败') }
|
||||
finally { unfreezeSubmitting.value = false }
|
||||
}
|
||||
|
||||
function onBatch(type) {
|
||||
if (!selected.value.length) return
|
||||
selected.value.forEach(r => onDownload(r, type))
|
||||
}
|
||||
function onBatchAudit() {
|
||||
if (!selected.value.length) return
|
||||
auditRow.value = selected.value[0]
|
||||
auditOpen.value = true
|
||||
}
|
||||
|
||||
function calcRemain(row) {
|
||||
if (!row.endTime || !row.submitDeadline) return '-'
|
||||
const diff = new Date(row.submitDeadline) - new Date(row.endTime)
|
||||
if (diff <= 0) return '冻结中'
|
||||
const h = Math.floor(diff / 3600000)
|
||||
return h > 24 ? `${Math.floor(h/24)}天${h%24}小时` : `${h}小时`
|
||||
}
|
||||
|
||||
onMounted(() => { readQueryFromRoute(); loadList() })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 1:1 抄 doctor/Submissions.vue + SponsorPeople.vue */
|
||||
.page-card { background: #fff; padding: 16px 20px; border-radius: 6px; border: 1px solid #f0f0f0; }
|
||||
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
|
||||
.batch-bar { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; }
|
||||
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
|
||||
|
||||
/* ========================================
|
||||
移动端适配 (≤768px)
|
||||
- 卡片 padding 收窄
|
||||
- 工具栏横向滚动 (6 个按钮铺不下, 用 overflow-x)
|
||||
- filter-form: 保持 label 左 + input 右横向布局
|
||||
- 表格字号收紧
|
||||
======================================== */
|
||||
@media (max-width: 768px) {
|
||||
/* 卡片 padding */
|
||||
.page-card { padding: 12px !important; border-radius: 4px !important; }
|
||||
.breadcrumb { font-size: 12px !important; margin-bottom: 8px !important; }
|
||||
|
||||
/* 工具栏: 横向滚动 (按钮多时不换行) */
|
||||
.toolbar {
|
||||
flex-wrap: nowrap !important;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding-bottom: 6px;
|
||||
margin-bottom: 8px !important;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.toolbar::-webkit-scrollbar { height: 4px; }
|
||||
.toolbar::-webkit-scrollbar-thumb { background: var(--brand-slate-200); border-radius: 2px; }
|
||||
.toolbar :deep(.action-btn),
|
||||
.toolbar :deep(.el-button) {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px !important;
|
||||
padding: 0 10px !important;
|
||||
height: 30px !important;
|
||||
}
|
||||
|
||||
/* filter-form: label 与输入框横向 (label 左, input 右) */
|
||||
.filter-form {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
align-items: stretch !important;
|
||||
gap: 10px !important;
|
||||
}
|
||||
.filter-form :deep(.el-form-item) {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
margin-right: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.filter-form :deep(.el-form-item__label) {
|
||||
float: none !important;
|
||||
width: auto !important;
|
||||
min-width: 80px !important;
|
||||
text-align: right !important;
|
||||
padding: 0 8px 0 0 !important;
|
||||
font-size: 13px !important;
|
||||
color: var(--el-text-color-regular) !important;
|
||||
line-height: 32px !important;
|
||||
height: 32px !important;
|
||||
}
|
||||
.filter-form :deep(.el-form-item__content) {
|
||||
margin-left: 0 !important;
|
||||
line-height: 32px !important;
|
||||
flex: 1 !important;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
/* 强制所有控件全宽, 覆盖 inline 260px / 140px / 120px */
|
||||
.filter-form :deep(.el-select),
|
||||
.filter-form :deep(.el-input),
|
||||
.filter-form :deep(.el-date-editor),
|
||||
.filter-form :deep(.el-button),
|
||||
.filter-form :deep(.el-cascader) {
|
||||
width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
margin-left: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
display: block !important;
|
||||
}
|
||||
.filter-form :deep(.el-button + .el-button) {
|
||||
margin-top: 8px !important;
|
||||
}
|
||||
|
||||
/* 表格字号收紧 */
|
||||
:deep(.el-table) { font-size: 12px !important; }
|
||||
}
|
||||
</style>
|
||||
@@ -41,7 +41,7 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="load">查找</el-button>
|
||||
<el-button type="primary" @click="load">查询</el-button>
|
||||
<el-button @click="reset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
@@ -62,7 +62,11 @@
|
||||
<el-table-column prop="projectNo" label="项目编号" min-width="170" fixed>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" :underline="false" @click="openDetail(row)">{{ row.projectNo }}</el-link></template></el-table-column>
|
||||
<el-table-column prop="projectName" label="项目名称" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="projectName" label="项目名称" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" :underline="false" @click="openDetail(row)">{{ row.projectName }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="totalSessions" label="总场次/总期数" width="120" align="center" />
|
||||
<el-table-column prop="doneSessions" label="已执行" width="80" align="center" />
|
||||
<el-table-column prop="todoSessions" label="未执行" width="80" align="center" />
|
||||
@@ -72,13 +76,13 @@
|
||||
<el-table-column label="已支付会务费" width="120" align="right"><template #default="{ row }">¥{{ formatMoney(row.paidMeetingAmount) }}</template></el-table-column>
|
||||
<el-table-column prop="managerScore" label="执行单位得分(合规)" width="150" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.managerScore != null">{{ row.managerScore }}</span>
|
||||
<el-link v-if="row.managerScore != null" :underline="false" type="primary" @click="openScoreDetail(row, 'manager')">{{ row.managerScore }}</el-link>
|
||||
<span v-else style="color:#c0c4cc">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sponsorScore" label="执行单位评价(支持方)" width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.sponsorScore != null">{{ row.sponsorScore }}</span>
|
||||
<el-link v-if="row.sponsorScore != null" :underline="false" type="primary" @click="openScoreDetail(row, 'sponsor')">{{ row.sponsorScore }}</el-link>
|
||||
<span v-else style="color:#c0c4cc">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -196,7 +200,6 @@
|
||||
<el-descriptions-item label="可用金额">¥{{ formatMoney(detail.availableAmount) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="已支付劳务费">¥{{ formatMoney(detail.paidLaborAmount) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="已支付会务费">¥{{ formatMoney(detail.paidMeetingAmount) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="执行单位得分">{{ detail.managerScore || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="是否结题">
|
||||
<el-tag :type="detail.isFinished === '1' ? 'success' : 'info'" disable-transitions>
|
||||
{{ detail.isFinished === '1' ? '已结题' : '未结题' }}
|
||||
@@ -207,6 +210,29 @@
|
||||
</el-descriptions>
|
||||
<template #footer><el-button @click="detailOpen = false">关闭</el-button></template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- ============= 评分详情 modal(只读 4 维度明细, 点列表列查看) ============= -->
|
||||
<el-dialog v-model="scoreDetailOpen" :title="scoreDetailTitle" width="520px" :show-close="false">
|
||||
<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:110px;padding:10px 12px;text-align:center">平均得分</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">{{ scoreDetail.qualityScore ?? '-' }}</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">{{ scoreDetail.responseScore ?? '-' }}</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">{{ scoreDetail.cooperationScore ?? '-' }}</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">{{ scoreDetail.complianceScore ?? '-' }}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div style="margin-top:12px;text-align:right;font-size:13px;color:#303133">
|
||||
平均分: <b>{{ scoreDetailTotal }}</b>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="scoreDetailOpen=false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -234,6 +260,12 @@ const form = reactive({
|
||||
const rateOpen = ref(false)
|
||||
const rateForm = reactive({ projectId: null, projectNo: '', projectName: '', q1: 0, q2: 0, q3: 0, q4: 0, remark: '' })
|
||||
|
||||
// ========== 评分详情 (只读 4 维度明细, 点列表 score 列查看) ==========
|
||||
const scoreDetailOpen = ref(false)
|
||||
const scoreDetailTitle = ref('评分详情')
|
||||
const scoreDetail = reactive({ qualityScore: null, responseScore: null, cooperationScore: null, complianceScore: null })
|
||||
const scoreDetailTotal = ref('—')
|
||||
|
||||
const assignOpen = ref(false)
|
||||
const assignForm = reactive({ projectId: '', projectNo: '', projectName: '', supervisor: '', supervisionPoint: '' })
|
||||
const personList = ref([])
|
||||
@@ -335,6 +367,26 @@ function openRate(row) {
|
||||
Object.assign(rateForm, { projectId: row.projectId, projectNo: row.projectNo, projectName: row.projectName, q1: 0, q2: 0, q3: 0, q4: 0, remark: '' })
|
||||
rateOpen.value = true
|
||||
}
|
||||
// 点击列表「执行单位得分(合规)/评价(支持方)」列 → 只读显示该角色 4 维度评分明细
|
||||
async function openScoreDetail(row, role) {
|
||||
scoreDetailTitle.value = role === 'sponsor' ? '执行单位评分详情(支持方)' : '执行单位评分详情(合规)'
|
||||
Object.assign(scoreDetail, { qualityScore: null, responseScore: null, cooperationScore: null, complianceScore: null })
|
||||
scoreDetailTotal.value = role === 'sponsor' ? (row.sponsorScore ?? '—') : (row.managerScore ?? '—')
|
||||
scoreDetailOpen.value = true
|
||||
try {
|
||||
const r = await request({ url: '/business/project/ratings', method: 'get', params: { projectId: row.projectId } })
|
||||
const list = (r.data && (Array.isArray(r.data) ? r.data : r.data.rows)) || r.rows || []
|
||||
const items = list.filter(x => String(x.projectId) === String(row.projectId) && x.raterRole === role)
|
||||
if (items.length) {
|
||||
const n = items.length
|
||||
const dimAvg = (key) => (items.reduce((s, x) => s + (Number(x[key]) || 0), 0) / n).toFixed(1)
|
||||
scoreDetail.qualityScore = dimAvg('qualityScore')
|
||||
scoreDetail.responseScore = dimAvg('responseScore')
|
||||
scoreDetail.cooperationScore = dimAvg('cooperationScore')
|
||||
scoreDetail.complianceScore = dimAvg('complianceScore')
|
||||
}
|
||||
} catch (e) { /* 拉取失败保持空, 弹窗显示 - */ }
|
||||
}
|
||||
async function submitRate() {
|
||||
if (!rateForm.q1 && !rateForm.q2 && !rateForm.q3 && !rateForm.q4) {
|
||||
ElMessage.warning('请至少完成一项评分'); return
|
||||
@@ -356,10 +408,7 @@ async function submitRate() {
|
||||
complianceScore: rateForm.q4,
|
||||
remark: rateForm.remark
|
||||
})
|
||||
// 2. 写聚合分到 biz_project.sponsor_score
|
||||
if (avg != null) {
|
||||
await bizUpdate('project', { projectId: rateForm.projectId, sponsorScore: Number(avg) })
|
||||
}
|
||||
// 聚合分由后端 /rate 重算, 前端不再单独写 sponsor_score
|
||||
ElMessage.success(`评分已提交, 平均分:${avg ?? '-'}`)
|
||||
rateOpen.value = false
|
||||
load()
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="项目名称" min-width="220" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<a class="link-bold">{{ row.projectName }}</a>
|
||||
<el-link :underline="false" type="primary" @click="onView(row)">{{ row.projectName }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="totalSessions" label="总场次" width="80" align="center" />
|
||||
@@ -70,12 +70,12 @@
|
||||
<el-table-column prop="paidMeetingAmount" label="会务费" width="120" align="right" :formatter="fmtMoney" />
|
||||
<el-table-column label="项目开始时间" width="170" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="mono">{{ fmtDate(row.startTime) }}</span>
|
||||
{{ fmtDateTime(row.startTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="项目结束时间" width="170" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="mono">{{ fmtDate(row.endTime) }}</span>
|
||||
{{ fmtDateTime(row.endTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="是否结题" width="90" align="center">
|
||||
@@ -83,13 +83,13 @@
|
||||
<span>{{ row.isFinished === '1' ? '已结题' : '未结题' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="执行单位评价(支持方)" width="150" align="center">
|
||||
<el-table-column label="执行单位评价(支持方)" width="180" align="center">
|
||||
<template #default="{ row }">
|
||||
<a v-if="row.sponsorScore" class="link">{{ row.sponsorScore }}</a>
|
||||
<el-link v-if="row.sponsorScore != null" :underline="false" type="primary" @click="openScoreDetail(row, 'sponsor')">{{ row.sponsorScore }}</el-link>
|
||||
<span v-else style="color:#909399">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="240" fixed="right">
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="{ row }"><div class="table-actions">
|
||||
<el-link :underline="false" type="primary" @click="openSingleScore(row)">项目评价</el-link>
|
||||
<!-- SUB 子账号仅查看, 不参与分配 -->
|
||||
@@ -159,6 +159,29 @@
|
||||
<el-button type="primary" :loading="assignSubmitting" :disabled="!assignForm.monitorUserIds.length" @click="saveAssign">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- ============= 评分详情 modal(只读 4 维度明细, 点列表列查看) ============= -->
|
||||
<el-dialog v-model="scoreDetailOpen" :title="scoreDetailTitle" width="520px" :show-close="false">
|
||||
<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:110px;padding:10px 12px;text-align:center">平均得分</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">{{ scoreDetail.qualityScore ?? '-' }}</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">{{ scoreDetail.responseScore ?? '-' }}</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">{{ scoreDetail.cooperationScore ?? '-' }}</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">{{ scoreDetail.complianceScore ?? '-' }}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div style="margin-top:12px;text-align:right;font-size:13px;color:#303133">
|
||||
平均分: <b>{{ scoreDetailTotal }}</b>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="scoreDetailOpen=false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -169,7 +192,6 @@ import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import GrTable from '@/components/GrTable.vue'
|
||||
import request from '@/utils/request'
|
||||
import { listSponsorProjects, rateProject, sponsorAssignProject, sponsorAssignBatch, getSponsorAssigns } from '@/api/business/project'
|
||||
import { bizUpdate } from '@/api/public'
|
||||
import { listSponsorPerson } from '@/api/business/person'
|
||||
import { useUserStore } from '@/store/user'
|
||||
|
||||
@@ -196,9 +218,9 @@ function fmtMoney(_row, _col, val) {
|
||||
if (val === null || val === undefined || val === '') return '—'
|
||||
return Number(val).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
}
|
||||
function fmtDate(val) {
|
||||
function fmtDateTime(val) {
|
||||
if (!val) return '—'
|
||||
return String(val).substring(0, 10)
|
||||
return String(val)
|
||||
}
|
||||
|
||||
async function load() {
|
||||
@@ -242,6 +264,12 @@ const singleScoreModalOpen = ref(false)
|
||||
const singleScoreTargetRow = ref(null)
|
||||
const singleScoreForm = reactive({ qualityScore: 0, responseScore: 0, cooperationScore: 0, complianceScore: 0 })
|
||||
|
||||
// ========== 评分详情 (只读 4 维度明细, 点列表 score 列查看) ==========
|
||||
const scoreDetailOpen = ref(false)
|
||||
const scoreDetailTitle = ref('评分详情')
|
||||
const scoreDetail = reactive({ qualityScore: null, responseScore: null, cooperationScore: null, complianceScore: null })
|
||||
const scoreDetailTotal = ref('—')
|
||||
|
||||
async function openSingleScore(row) {
|
||||
singleScoreTargetRow.value = row
|
||||
singleScoreForm.qualityScore = 0
|
||||
@@ -279,9 +307,7 @@ async function confirmSingleScore() {
|
||||
const avg = ((f.qualityScore + f.responseScore + f.cooperationScore + f.complianceScore) / 4).toFixed(1)
|
||||
const projectId = singleScoreTargetRow.value.projectId
|
||||
try {
|
||||
// 1. 写聚合分到 biz_project.sponsor_score
|
||||
await bizUpdate('project', { projectId, sponsorScore: Number(avg) })
|
||||
// 2. 写 4 维度明细到 biz_project_rating (rater_role='sponsor', 后端自动填 raterId)
|
||||
// 写 4 维度明细到 biz_project_rating (rater_role='sponsor', 后端自动填 raterId + 重算聚合分)
|
||||
await rateProject({
|
||||
projectId,
|
||||
raterRole: 'sponsor',
|
||||
@@ -298,6 +324,27 @@ async function confirmSingleScore() {
|
||||
}
|
||||
}
|
||||
|
||||
// 点击列表「执行单位评价(支持方)」列 → 只读显示 sponsor 4 维度评分明细
|
||||
async function openScoreDetail(row, role) {
|
||||
scoreDetailTitle.value = role === 'sponsor' ? '执行单位评分详情(支持方)' : '执行单位评分详情(合规)'
|
||||
Object.assign(scoreDetail, { qualityScore: null, responseScore: null, cooperationScore: null, complianceScore: null })
|
||||
scoreDetailTotal.value = role === 'sponsor' ? (row.sponsorScore ?? '—') : (row.managerScore ?? '—')
|
||||
scoreDetailOpen.value = true
|
||||
try {
|
||||
const r = await request({ url: '/business/project/ratings', method: 'get', params: { projectId: row.projectId } })
|
||||
const list = (r.data && (Array.isArray(r.data) ? r.data : r.data.rows)) || r.rows || []
|
||||
const items = list.filter(x => String(x.projectId) === String(row.projectId) && x.raterRole === role)
|
||||
if (items.length) {
|
||||
const n = items.length
|
||||
const dimAvg = (key) => (items.reduce((s, x) => s + (Number(x[key]) || 0), 0) / n).toFixed(1)
|
||||
scoreDetail.qualityScore = dimAvg('qualityScore')
|
||||
scoreDetail.responseScore = dimAvg('responseScore')
|
||||
scoreDetail.cooperationScore = dimAvg('cooperationScore')
|
||||
scoreDetail.complianceScore = dimAvg('complianceScore')
|
||||
}
|
||||
} catch (e) { /* 拉取失败保持空, 弹窗显示 - */ }
|
||||
}
|
||||
|
||||
// ========== Modal 2: 项目分配 (单选 / 批量) ==========
|
||||
const assignVisible = ref(false)
|
||||
const assignBatchMode = ref(false) // 批量模式开关
|
||||
@@ -312,7 +359,8 @@ async function loadMonitors() {
|
||||
// 跟 sponsor/people (人员管理) 完全一致: 走 sponsorList, SQL 硬编码 unit_type='sponsor'
|
||||
// + (u.parent_user_id = mainUid OR u.user_id = mainUid), 自动取当前主账号
|
||||
// 仅取 SUB 子账号 (监察员候选人), 排除主账号
|
||||
const res = await listSponsorPerson({ pageNum: 1, pageSize: 100 })
|
||||
// status='0': 只拉启用账号, 已禁用的监察员不进候选列表 (SQL 层过滤, 不影响人员管理列表)
|
||||
const res = await listSponsorPerson({ pageNum: 1, pageSize: 100, status: '0' })
|
||||
allMonitors.value = (res.rows || []).filter(p => p.accountType === 'SUB')
|
||||
}
|
||||
/** 拉项目当前已分配监察员 → 回显 monitorUserIds (单选模式) */
|
||||
@@ -393,7 +441,6 @@ onMounted(load)
|
||||
.toolbar { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
|
||||
.link { color: var(--brand-primary); }
|
||||
.link-bold { color: #303133; font-weight: 600; }
|
||||
.mono { font-family: monospace; color: #606266; font-size: 12px; }
|
||||
.pagination { margin-top: 12px; justify-content: flex-end; display: flex; }
|
||||
|
||||
.modal-project-info { background: #f5f7fa; padding: 12px 16px; border-radius: 4px; margin-bottom: 12px; }
|
||||
|
||||
Reference in New Issue
Block a user