feat: 会议ID应用赋值 + 会议表单/详情优化 + admin工作台统计与用户管理
- meetingId 由 DB 自增改为应用赋值 10 位数字 (yyMMdd + 4 位序列, Redis 按日期计数) - 会议开始时间不能晚于结束时间校验; 参会人表单身份证附件/角色单独一行 - 会议详情左右列比例 7:3 -> 8:2 (材料审核列收窄) - admin/workbench 用户总数/角色类型数/各角色分布统计修复 (改用 listByRole 按 role_type 过滤) - 新增 admin 用户管理接口 + 门户页脚 + H5 签到/上传优化
This commit is contained in:
@@ -91,6 +91,8 @@ export const markMessageRead = (msgId) => request.put(`/business/message/read/${
|
||||
export const markAllMessagesRead = () => request.put('/business/message/readAll')
|
||||
|
||||
export const bizGet = (entity, id) => request.get(`/business/${entity}/${id}`)
|
||||
// executor 建会页"总场次": 分配给本执行方(公司)的场次 (非项目总场次)
|
||||
export const getProjectAssignedSessions = (projectId) => request.get(`/business/project/${projectId}/assignedSessions`)
|
||||
// opts 透传给 request (例如 { __silentError: true } 让拦截器不自动 toast, 由调用方控制)
|
||||
export const bizAdd = (entity, data, opts) => request.post(`/business/${entity}`, data, opts)
|
||||
export const bizUpdate = (entity, data, opts) => request.put(`/business/${entity}`, data, opts)
|
||||
@@ -103,6 +105,9 @@ export const bizDelete = (entity, ids) => request.delete(`/business/${entity}/${
|
||||
// 更换机构管理员 (admin/sponsor-people 管理员 switch): 目标人员晋升 MAIN, 原管理员降 SUB
|
||||
export const changePersonAdmin = (personId) => request.put('/business/person/changeAdmin', { personId })
|
||||
|
||||
// 重置人员登录密码 (默认 123456)
|
||||
export const resetPersonPassword = (personId) => request.put('/business/person/resetPassword', { personId })
|
||||
|
||||
// 人员批量导入 (unitType: 'sponsor' | 'executor')
|
||||
export const importPerson = (unitType, file, orgId) => {
|
||||
const form = new FormData()
|
||||
|
||||
@@ -54,6 +54,7 @@ import { uploadToOss } from '@/utils/oss'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: String, default: '' },
|
||||
name: { type: String, default: '' }, // 原文件名 (v-model:name 回传, 用于显示而非 OSS key)
|
||||
dir: { type: String, default: 'ry8080/file/' },
|
||||
placeholder: { type: String, default: '点击上传文件' },
|
||||
hint: { type: String, default: '' },
|
||||
@@ -64,16 +65,23 @@ const props = defineProps({
|
||||
showPreview: { type: Boolean, default: true } // 右侧预览框 (图片/PDF/其他)
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const emit = defineEmits(['update:modelValue', 'update:name'])
|
||||
|
||||
const fileInput = ref(null)
|
||||
const uploading = ref(false)
|
||||
|
||||
const fileName = computed(() => {
|
||||
// 优先用调用方回传的原文件名 (v-model:name); 没有则从 OSS key 还原原名
|
||||
if (props.name) return props.name
|
||||
if (!props.modelValue) return ''
|
||||
// 取 URL 最后一段, 去 query 参数
|
||||
// 取 URL 最后一段, 去 query 参数; 上传时已对路径做了 encodeURIComponent, 这里先还原
|
||||
const url = props.modelValue.split('?')[0]
|
||||
return url.substring(url.lastIndexOf('/') + 1)
|
||||
let last = url.substring(url.lastIndexOf('/') + 1)
|
||||
try { last = decodeURIComponent(last) } catch (e) { /* 已解码/旧 key, 原样用 */ }
|
||||
// 新 key 格式: 原名_时间戳_随机串.扩展名 → 还原 原名.扩展名; 旧 key (无原名) 原样返回
|
||||
const m = last.match(/^(.+)_\d{13}_[a-z0-9]{6}(\.[^.]+)?$/)
|
||||
if (m) return m[1] + (m[2] || '')
|
||||
return last
|
||||
})
|
||||
const ext = computed(() => {
|
||||
const n = fileName.value
|
||||
@@ -127,6 +135,7 @@ async function onFileChange(e) {
|
||||
try {
|
||||
const url = await uploadToOss(file, props.dir)
|
||||
emit('update:modelValue', url)
|
||||
emit('update:name', file.name)
|
||||
ElMessage.success('上传成功')
|
||||
} catch (err) {
|
||||
ElMessage.error(err.message || '上传失败')
|
||||
@@ -137,6 +146,7 @@ async function onFileChange(e) {
|
||||
|
||||
function onRemove() {
|
||||
emit('update:modelValue', '')
|
||||
emit('update:name', '')
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="footer-main">
|
||||
<div class="footer-brand">
|
||||
<div class="brand-row">
|
||||
<div class="footer-logo-icon"><img src="/logo.png" alt="logo" /></div>
|
||||
<div>
|
||||
<div class="footer-brand-name">北京整合医学学会</div>
|
||||
<div class="footer-brand-en">Beijing Association of Holistic Integrative Medicine</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="footer-desc">依托"健康中国2030"战略,整合医学资源,推动健康科普与公益事业,助力全民健康素养提升,共建共享健康中国。</p>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
<h4>快速导航</h4>
|
||||
<a @click.prevent="goHome">年度项目规划</a>
|
||||
<a @click.prevent="goPublicity">项目公示</a>
|
||||
<a href="https://v2.bahim.org.cn/computer/#/home" target="_blank" rel="noopener">北京整合医学学会首页</a>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
<h4>学会项目</h4>
|
||||
<a href="https://bxxcx.bahim.org.cn/#/" target="_blank" rel="noopener">百姓巡常行</a>
|
||||
<a href="https://v2.bahim.org.cn/computer/#/project/home/title=%E5%81%A5%E5%BA%B7%E4%B8%AD%E5%9B%BD%E8%A1%8C%E5%8A%A8%E7%B3%BB%E5%88%97%E9%87%8D%E7%82%B9%E9%A1%B9%E7%9B%AE?columnId=59&projectId=5" target="_blank" rel="noopener">健康中国行动</a>
|
||||
<a href="https://v2.bahim.org.cn/computer/#/project/home/title=%E4%B9%A1%E6%9D%91%E5%B9%B8%E7%A6%8F%E5%AE%89%E5%BA%B7%E5%B7%A5%E7%A8%8B?columnId=25&projectId=2" target="_blank" rel="noopener">乡村幸福安康工程</a>
|
||||
<a href="https://v2.bahim.org.cn/computer/#/project/home/title=%E4%B8%B4%E5%BA%8A%E7%A7%91%E7%A0%94%E8%B5%84%E5%8A%A9%E8%AE%A1%E5%88%92?columnId=27&projectId=4" target="_blank" rel="noopener">临床科研资助计划</a>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
<h4>联系方式</h4>
|
||||
<p>电话: 010-82089470</p>
|
||||
<p>邮箱: contactus@bahim.org.cn</p>
|
||||
<p>地址: 北京市海淀区知春路1号学院国际大厦908-2室</p>
|
||||
<p>邮编: 100083</p>
|
||||
</div>
|
||||
<div class="footer-qr">
|
||||
<div class="qr-image">
|
||||
<img src="/qrcode_1.png" alt="公众号二维码" style="width:100%;height:100%;object-fit:contain;" />
|
||||
</div>
|
||||
<div class="qr-label">公众号二维码</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<span>© 北京整合医学学会 BAHIM</span>
|
||||
<span>京ICP备2020035479号-1 京公网安备11010802034820</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
function goHome() {
|
||||
if (router.currentRoute.value.path === '/') {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
} else {
|
||||
router.push('/')
|
||||
}
|
||||
}
|
||||
function goPublicity() { router.push('/publicity') }
|
||||
</script>
|
||||
|
||||
<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-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 {
|
||||
width: 36px; height: 36px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
overflow: hidden; flex-shrink: 0;
|
||||
}
|
||||
.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-col h4 {
|
||||
font-size: 14px; font-weight: 600; color: #fff;
|
||||
letter-spacing: 1px; margin-bottom: 16px; padding-bottom: 10px;
|
||||
position: relative;
|
||||
}
|
||||
.footer-col h4::after {
|
||||
content: ''; position: absolute; left: 0; bottom: 0;
|
||||
width: 24px; height: 2px; background: #93c5fd;
|
||||
}
|
||||
.footer-col a, .footer-col p {
|
||||
display: block; font-size: 13px; color: rgba(255, 255, 255, 0.6);
|
||||
line-height: 2.1; transition: color 0.2s;
|
||||
}
|
||||
.footer-col a { cursor: pointer; }
|
||||
.footer-col a:hover { color: #fff; }
|
||||
.footer-qr { text-align: center; }
|
||||
.qr-image {
|
||||
width: 100px; height: 100px;
|
||||
background: #fff;
|
||||
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; }
|
||||
.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);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.container { padding: 0 24px; }
|
||||
}
|
||||
</style>
|
||||
@@ -43,52 +43,7 @@
|
||||
</main>
|
||||
|
||||
<!-- ========== 底部 ========== -->
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="footer-main">
|
||||
<div class="footer-brand">
|
||||
<div class="brand-row">
|
||||
<div class="footer-logo-icon"><img src="/logo.png" alt="logo" /></div>
|
||||
<div>
|
||||
<div class="footer-brand-name">北京整合医学学会</div>
|
||||
<div class="footer-brand-en">Beijing Association of Holistic Integrative Medicine</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="footer-desc">依托"健康中国2030"战略,整合医学资源,推动健康科普与公益事业,助力全民健康素养提升,共建共享健康中国。</p>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
<h4>快速导航</h4>
|
||||
<a @click.prevent="goHome">首页</a>
|
||||
<a @click.prevent="goHome">年度项目规划</a>
|
||||
<a @click.prevent="goPublicity">项目公示</a>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
<h4>学会项目</h4>
|
||||
<a>百姓巡常行</a>
|
||||
<a>专病联盟暨全国专家智库</a>
|
||||
<a>乡村幸福安康</a>
|
||||
<a>临床科研资助计划</a>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
<h4>联系方式</h4>
|
||||
<p>电话: 010-82089470</p>
|
||||
<p>邮箱: contactus@bahim.org.cn</p>
|
||||
<p>地址: 北京市海淀区知春路1号学院国际大厦908-2室</p>
|
||||
<p>邮编: 100083</p>
|
||||
</div>
|
||||
<div class="footer-qr">
|
||||
<div class="qr-image">
|
||||
<img src="/qrcode_1.png" alt="公众号二维码" style="width:100%;height:100%;object-fit:contain;" />
|
||||
</div>
|
||||
<div class="qr-label">公众号二维码</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<span>© 北京整合医学学会 BAHIM</span>
|
||||
<span>京ICP备2020035479号-1 京公网安备11010802034820</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<PortalFooter />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -97,6 +52,7 @@ import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { logout as logoutApi } from '@/api/auth'
|
||||
import PortalFooter from '@/components/PortalFooter.vue'
|
||||
|
||||
defineProps({
|
||||
activeNav: { type: String, default: '' } // 'home' | 'publicity' | ''
|
||||
@@ -112,7 +68,7 @@ const userName = computed(() => userStore.user?.userName || '用户')
|
||||
|
||||
function handleScroll() { isScrolled.value = window.scrollY > 10 }
|
||||
|
||||
function goHome() { isScrolled.value = false; router.push('/portal/home') }
|
||||
function goHome() { isScrolled.value = false; router.push('/') }
|
||||
function goPublicity() { router.push('/publicity') }
|
||||
function goLogin() { router.push('/login') }
|
||||
async function goLogout() {
|
||||
@@ -203,53 +159,8 @@ a { color: inherit; text-decoration: none; }
|
||||
|
||||
/* ========== 主体容器 ========== */
|
||||
.portal-main { min-height: calc(100vh - 72px - 350px); } /* 给 footer 留位 */
|
||||
.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-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 {
|
||||
width: 36px; height: 36px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
overflow: hidden; flex-shrink: 0;
|
||||
}
|
||||
.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-col h4 {
|
||||
font-size: 14px; font-weight: 600; color: #fff;
|
||||
letter-spacing: 1px; margin-bottom: 16px; padding-bottom: 10px;
|
||||
position: relative;
|
||||
}
|
||||
.footer-col h4::after {
|
||||
content: ''; position: absolute; left: 0; bottom: 0;
|
||||
width: 24px; height: 2px; background: #93c5fd;
|
||||
}
|
||||
.footer-col a, .footer-col p {
|
||||
display: block; font-size: 13px; color: rgba(255, 255, 255, 0.6);
|
||||
line-height: 2.1; transition: color 0.2s;
|
||||
}
|
||||
.footer-col a { cursor: pointer; }
|
||||
.footer-col a:hover { color: #fff; }
|
||||
.footer-qr { text-align: center; }
|
||||
.qr-image {
|
||||
width: 100px; height: 100px;
|
||||
background: #fff;
|
||||
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; }
|
||||
.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);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.top-nav { padding: 0 24px; }
|
||||
.container { padding: 0 24px; }
|
||||
}
|
||||
</style>
|
||||
@@ -1,10 +1,22 @@
|
||||
<template>
|
||||
<el-dialog v-model="open" :title="title" width="780" top="6vh" :close-on-click-modal="false" destroy-on-close>
|
||||
<div v-if="fileUrl" class="preview-box">
|
||||
<!-- 多图 (身份证正反面等): 并排缩略图, 点开走 el-image 全屏轮播 -->
|
||||
<div v-if="isMulti" class="preview-gallery">
|
||||
<el-image
|
||||
v-for="(f, i) in files"
|
||||
:key="i"
|
||||
:src="f"
|
||||
:preview-src-list="files"
|
||||
:initial-index="i"
|
||||
fit="contain"
|
||||
class="preview-gallery-item"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="primary" class="preview-box">
|
||||
<!-- 图片 -->
|
||||
<img v-if="kind === 'image'" :src="fileUrl" class="preview-img" :alt="title" />
|
||||
<img v-if="kind === 'image'" :src="primary" class="preview-img" :alt="title" />
|
||||
<!-- PDF:iframe 预览,浏览器内置 viewer -->
|
||||
<iframe v-else-if="kind === 'pdf'" :src="proxyUrl(fileUrl)" class="preview-iframe" />
|
||||
<iframe v-else-if="kind === 'pdf'" :src="proxyUrl(primary)" class="preview-iframe" />
|
||||
<!-- 其它(doc/xls/zip 等):给下载链接 -->
|
||||
<div v-else class="preview-fallback">
|
||||
<el-icon class="fallback-icon"><Document /></el-icon>
|
||||
@@ -16,7 +28,7 @@
|
||||
<el-empty v-else description="暂无附件" />
|
||||
<template #footer>
|
||||
<el-button @click="open = false">关闭</el-button>
|
||||
<el-button v-if="fileUrl" type="primary" @click="openNew">在新窗口打开</el-button>
|
||||
<el-button v-if="primary && !isMulti" type="primary" @click="openNew">在新窗口打开</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
@@ -29,6 +41,8 @@ import { ElMessage } from 'element-plus'
|
||||
const props = defineProps({
|
||||
modelValue: Boolean,
|
||||
url: { type: String, default: '' },
|
||||
// 多文件列表 (身份证正反面等): 传入后优先于 url, 走多图画廊预览
|
||||
urls: { type: Array, default: () => [] },
|
||||
title: { type: String, default: '附件预览' }
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
@@ -38,14 +52,21 @@ const open = computed({
|
||||
set: v => emit('update:modelValue', v)
|
||||
})
|
||||
|
||||
const fileUrl = computed(() => props.url || '')
|
||||
|
||||
const kind = computed(() => {
|
||||
const u = fileUrl.value.split('?')[0].toLowerCase()
|
||||
if (/\.(png|jpe?g|gif|webp|bmp|svg)(\?.*)?$/.test(u)) return 'image'
|
||||
if (/\.pdf(\?.*)?$/.test(u)) return 'pdf'
|
||||
return 'other'
|
||||
/** 统一文件列表: urls 优先, 否则 url 单条; 空段过滤 */
|
||||
const files = computed(() => {
|
||||
if (Array.isArray(props.urls) && props.urls.length) return props.urls.filter(Boolean)
|
||||
return props.url ? [props.url] : []
|
||||
})
|
||||
const primary = computed(() => files.value[0] || '')
|
||||
const isMulti = computed(() => files.value.length > 1)
|
||||
|
||||
function kindOf(u) {
|
||||
const x = (u || '').split('?')[0].toLowerCase()
|
||||
if (/\.(png|jpe?g|gif|webp|bmp|svg)(\?.*)?$/.test(x)) return 'image'
|
||||
if (/\.pdf(\?.*)?$/.test(x)) return 'pdf'
|
||||
return 'other'
|
||||
}
|
||||
const kind = computed(() => kindOf(primary.value))
|
||||
|
||||
// OSS bucket 设置了 Content-Disposition: attachment, iframe 直接访问会被强制下载 (空白+下载)
|
||||
// PDF 走 /common/oss/proxy 后端代理重写为 inline (与 publicity/{id} / doctor/Meetings / manager/Plans 同技术)
|
||||
@@ -59,13 +80,13 @@ function proxyUrl(url) {
|
||||
}
|
||||
|
||||
function openNew() {
|
||||
if (!fileUrl.value) return
|
||||
window.open(proxyUrl(fileUrl.value), '_blank', 'noopener')
|
||||
if (!primary.value) return
|
||||
window.open(proxyUrl(primary.value), '_blank', 'noopener')
|
||||
}
|
||||
async function copyUrl() {
|
||||
if (!fileUrl.value) return
|
||||
if (!primary.value) return
|
||||
try {
|
||||
await navigator.clipboard.writeText(fileUrl.value)
|
||||
await navigator.clipboard.writeText(primary.value)
|
||||
ElMessage.success('链接已复制')
|
||||
} catch {
|
||||
ElMessage.error('复制失败,请手动复制')
|
||||
@@ -80,4 +101,6 @@ async function copyUrl() {
|
||||
.preview-fallback { text-align: center; padding: 48px 24px; color: #606266; }
|
||||
.fallback-icon { font-size: 48px; color: #909399; margin-bottom: 12px; }
|
||||
.preview-fallback p { margin: 12px 0 24px; }
|
||||
</style>
|
||||
.preview-gallery { display: flex; flex-wrap: wrap; gap: 16px; justify-content: center; align-items: flex-start; min-height: 320px; padding: 8px 0; }
|
||||
.preview-gallery-item { width: 320px; max-width: 48%; max-height: 60vh; border: 1px solid #ebeef5; border-radius: 4px; }
|
||||
</style>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<el-container>
|
||||
<el-header v-if="!route.meta?.hideMenu" class="topbar">
|
||||
<div class="topbar-left">
|
||||
<span class="page-title">{{ route.meta?.title || '工作台' }}</span>
|
||||
<span class="page-title">{{ headerTitle }}</span>
|
||||
</div>
|
||||
<div class="topbar-right">
|
||||
<el-button text @click="$router.push('/')">门户首页</el-button>
|
||||
@@ -70,6 +70,7 @@ import { useUserStore } from '@/store/user'
|
||||
import { logout as logoutApi } from '@/api/auth'
|
||||
import { sseStart, sseStop, onGlobal } from '@/utils/sseClient'
|
||||
import { listMyMessages } from '@/api/public'
|
||||
import { usePageTitle } from '@/utils/pageTitle'
|
||||
import { getMyExpertProfile } from '@/api/business/expert'
|
||||
import { ElNotification } from 'element-plus'
|
||||
import { House, Document, Calendar, User, List, OfficeBuilding, Setting, Bell, EditPen, DataAnalysis, Tickets, CaretBottom, Folder, Medal, UserFilled, Connection, Box, Star, Grid, Files, Compass, Collection } from '@element-plus/icons-vue'
|
||||
@@ -79,6 +80,10 @@ const router = useRouter()
|
||||
const store = useUserStore()
|
||||
const role = computed(() => route.meta?.role || store.role)
|
||||
|
||||
// 顶栏标题: 页面动态标题 (如会议 新建/修改/复制) 优先, 否则回退 route.meta.title
|
||||
const dynamicTitle = usePageTitle()
|
||||
const headerTitle = computed(() => dynamicTitle.value || route.meta?.title || '工作台')
|
||||
|
||||
// ===== 全局未读数 (navbar bell 角标) =====
|
||||
// AdminLayout 是唯一永驻组件, 由它统一维护 unreadCount,
|
||||
// 任何路由切换都不会丢订阅, 永远正确
|
||||
|
||||
@@ -9,20 +9,10 @@
|
||||
* current_stage 仍是物理阶段缓存 (10 值), 仅供列表筛选精确匹配.
|
||||
*/
|
||||
|
||||
const H24 = 24 * 3600 * 1000
|
||||
|
||||
function isTrue(v) {
|
||||
return v === 1 || v === '1' || v === true
|
||||
}
|
||||
|
||||
/** 待结算: 材料审核通过 且 材料审核时间已超 24h */
|
||||
function settlementReady(row) {
|
||||
if (row.materialAuditStage !== 'APPROVED') return false
|
||||
const mat = row.materialAuditTime ? new Date(row.materialAuditTime).getTime() : 0
|
||||
if (!mat) return false
|
||||
return Date.now() - mat >= H24
|
||||
}
|
||||
|
||||
/**
|
||||
* 10 值物理阶段 (镜像后端 StageDeriver.derivePhysicalStage), 用于颜色/筛选.
|
||||
*/
|
||||
@@ -33,7 +23,7 @@ export function derivePhysicalStage(row) {
|
||||
if (isTrue(row.isSettled)) return 'SETTLED'
|
||||
const material = row.materialAuditStage
|
||||
if (material === 'REJECTED') return 'RECTIFYING'
|
||||
if (material === 'APPROVED') return settlementReady(row) ? 'AWAITING_SETTLEMENT' : 'SUPERVISION_APPROVED'
|
||||
if (material === 'APPROVED') return 'AWAITING_SETTLEMENT'
|
||||
if (material === 'SUBMITTED') return isTrue(row.materialComplianceApproved) ? 'AWAITING_SUPERVISION' : 'AWAITING_COMPLIANCE'
|
||||
return isTrue(row.isExecuted) ? 'RUNNING' : 'NOT_STARTED'
|
||||
}
|
||||
@@ -55,9 +45,9 @@ export function deriveStage(role, row) {
|
||||
return role === 'executor' ? '已退回' : '待整改'
|
||||
}
|
||||
|
||||
// 材料已支持方通过
|
||||
// 材料已支持方通过 → 待结算 (不再有 24h 慢路径)
|
||||
if (material === 'APPROVED') {
|
||||
return settlementReady(row) ? '待结算' : '审核通过'
|
||||
return '待结算'
|
||||
}
|
||||
|
||||
// 材料在审 (SUBMITTED)
|
||||
@@ -120,7 +110,6 @@ export const STAGE_OPTIONS = [
|
||||
{ label: '执行中', value: 'RUNNING' },
|
||||
{ label: '待合规审核', value: 'AWAITING_COMPLIANCE' },
|
||||
{ label: '待支持方审核', value: 'AWAITING_SUPERVISION' },
|
||||
{ label: '审核通过', value: 'SUPERVISION_APPROVED' },
|
||||
{ label: '待整改', value: 'RECTIFYING' },
|
||||
{ label: '待结算', value: 'AWAITING_SETTLEMENT' },
|
||||
{ label: '已结算', value: 'SETTLED' },
|
||||
|
||||
@@ -45,8 +45,14 @@ function getMimeByExt(name) {
|
||||
|
||||
export async function uploadToOss(file, dir) {
|
||||
const sign = await getOssSign(dir)
|
||||
const ext = (file.name || '').split('.').pop() || 'bin'
|
||||
const key = sign.dir + Date.now() + '_' + Math.random().toString(36).slice(2, 8) + '.' + ext
|
||||
// 原文件名进 OSS key: 原始名称_时间戳_随机串.原扩展名 (保留中文, 仅清洗 URL/路径危险字符)
|
||||
// 这样上传控件能从 URL 里还原原名显示, 无需额外 name 字段/列
|
||||
const rawName = file.name || 'file'
|
||||
const dot = rawName.lastIndexOf('.')
|
||||
const ext = (dot > 0 ? rawName.slice(dot) : '').replace(/[\\/:*?"<>|%\s]+/g, '')
|
||||
const base = (dot > 0 ? rawName.slice(0, dot) : rawName)
|
||||
.replace(/[\\/:*?"<>|%\s]+/g, '_') || 'file'
|
||||
const key = sign.dir + base + '_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8) + ext
|
||||
// 显式传 Content-Type,让 OSS 按此存文件元数据, 浏览器拿到 application/pdf 即可内嵌预览
|
||||
const mime = getMimeByExt(file.name)
|
||||
|
||||
@@ -68,5 +74,7 @@ export async function uploadToOss(file, dir) {
|
||||
if (!resp.ok) {
|
||||
throw new Error('OSS 上传失败: HTTP ' + resp.status)
|
||||
}
|
||||
return sign.host + '/' + key
|
||||
// 返回 URL 时对路径逐段编码 (保留 / 分隔), 让含中文/特殊字符的 key 在所有下载场景下都是 ASCII 安全 URL
|
||||
// OSS 对象 key 本身仍是原始值 (FormData 的 key 字段未编码), 下载时 OSS 会自动把 %XX 还原
|
||||
return sign.host + '/' + key.split('/').map(encodeURIComponent).join('/')
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
// navbar 顶栏动态标题 (AdminLayout 顶栏优先读它, 覆盖 route.meta.title)
|
||||
// 页面组件进入时 setPageTitle, 离开时 onBeforeUnmount(clearPageTitle),
|
||||
// 否则切到别的页面会残留上一个页面的标题.
|
||||
const dynamicTitle = ref('')
|
||||
|
||||
export function setPageTitle(t) {
|
||||
dynamicTitle.value = t || ''
|
||||
}
|
||||
|
||||
export function clearPageTitle() {
|
||||
dynamicTitle.value = ''
|
||||
}
|
||||
|
||||
export function usePageTitle() {
|
||||
return dynamicTitle
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
<el-form inline :model="q" class="filter-form">
|
||||
<el-form-item label="用户账号"><el-input v-model="q.userName" placeholder="输入用户账号" clearable /></el-form-item>
|
||||
<el-form-item label="姓名"><el-input v-model="q.nickName" placeholder="输入姓名" clearable /></el-form-item>
|
||||
<el-form-item label="单位名称"><el-input v-model="q.orgName" placeholder="输入单位名称" clearable /></el-form-item>
|
||||
<el-form-item label="角色">
|
||||
<el-select v-model="q.roleType" placeholder="全部角色" clearable style="width:140px">
|
||||
<el-option v-for="r in roleOptions" :key="r.value" :label="r.label" :value="r.value" />
|
||||
@@ -23,6 +24,11 @@
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 新建用户工具栏 -->
|
||||
<div class="toolbar">
|
||||
<el-button type="primary" @click="openCreate">新建用户</el-button>
|
||||
</div>
|
||||
|
||||
<!-- ========== 表格 (与 People.vue 风格一致, 无 page-card 包裹) ========== -->
|
||||
<el-table :data="rows" border stripe v-loading="loading">
|
||||
<el-table-column prop="userId" label="用户ID" width="80" />
|
||||
@@ -31,6 +37,7 @@
|
||||
<el-table-column prop="roleType" label="角色" width="100">
|
||||
<template #default="{ row }">{{ ROLE_LABEL[row.roleType] || row.roleType }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="orgName" label="单位名称" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="phonenumber" label="手机号" width="130" />
|
||||
<el-table-column prop="email" label="邮箱" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="status" label="状态" width="80">
|
||||
@@ -39,9 +46,10 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" width="170" />
|
||||
<el-table-column label="操作" width="240" fixed="right">
|
||||
<el-table-column label="操作" width="330" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" link type="primary" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button size="small" link type="primary" @click="onResetPwd(row)">重置密码</el-button>
|
||||
<el-button size="small" link :type="row.status === '0' ? 'danger' : 'success'" @click="toggleStatus(row)">
|
||||
{{ row.status === '0' ? '停用' : '启用' }}
|
||||
</el-button>
|
||||
@@ -94,11 +102,87 @@
|
||||
<el-button type="primary" @click="saveEdit">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 新建用户 dialog (按角色级联) -->
|
||||
<el-dialog v-model="createVisible" title="新建用户" width="560px">
|
||||
<el-form :model="createForm" label-width="110px">
|
||||
<el-form-item label="角色" required>
|
||||
<el-select v-model="createForm.roleType" placeholder="请选择角色" style="width:100%" @change="onRoleChange">
|
||||
<el-option v-for="r in roleOptions" :key="r.value" :label="r.label" :value="r.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="邮箱">
|
||||
<el-input v-model="createForm.email" placeholder="请输入邮箱" />
|
||||
</el-form-item>
|
||||
<el-form-item label="登录账号" required>
|
||||
<el-input v-model="createForm.userName" placeholder="4-20 位字母/数字/下划线" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密码" required>
|
||||
<el-input v-model="createForm.password" type="password" show-password placeholder="6-20 位" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- admin / manager: 简单账号 -->
|
||||
<template v-if="isSimpleRole">
|
||||
<el-form-item label="姓名"><el-input v-model="createForm.nickName" /></el-form-item>
|
||||
<el-form-item label="手机号"><el-input v-model="createForm.phonenumber" maxlength="11" /></el-form-item>
|
||||
</template>
|
||||
|
||||
<!-- doctor: 评审专家 -->
|
||||
<template v-else-if="createForm.roleType === 'doctor'">
|
||||
<el-form-item label="姓名" required><el-input v-model="createForm.nickName" /></el-form-item>
|
||||
<el-form-item label="手机号" required><el-input v-model="createForm.phonenumber" maxlength="11" /></el-form-item>
|
||||
<el-form-item label="工作单位"><el-input v-model="createForm.workUnit" /></el-form-item>
|
||||
<el-form-item label="科室"><el-input v-model="createForm.department" /></el-form-item>
|
||||
<el-form-item label="职称"><el-input v-model="createForm.title" /></el-form-item>
|
||||
</template>
|
||||
|
||||
<!-- executor / sponsor: 主账号 / 子账号 -->
|
||||
<template v-else-if="isUnitRole">
|
||||
<el-form-item label="账号类型" required>
|
||||
<el-radio-group v-model="createForm.accountType" @change="onAccountTypeChange">
|
||||
<el-radio value="MAIN">主账号(新建单位)</el-radio>
|
||||
<el-radio value="SUB">子账号(选已有单位)</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<template v-if="createForm.accountType === 'MAIN'">
|
||||
<el-form-item label="单位名称" required><el-input v-model="createForm.orgName" /></el-form-item>
|
||||
<el-form-item label="企业性质"><el-input v-model="createForm.businessNature" /></el-form-item>
|
||||
<el-form-item label="联系人"><el-input v-model="createForm.contactName" /></el-form-item>
|
||||
<el-form-item label="联系电话"><el-input v-model="createForm.contactPhone" maxlength="11" /></el-form-item>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<el-form-item label="所属单位" required>
|
||||
<el-select v-model="createForm.orgId" filterable placeholder="搜索选择单位" style="width:100%">
|
||||
<el-option v-for="o in orgOptions" :key="o.orgId" :label="o.orgName" :value="o.orgId" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="姓名" required><el-input v-model="createForm.nickName" /></el-form-item>
|
||||
<el-form-item label="手机号" required><el-input v-model="createForm.phonenumber" maxlength="11" /></el-form-item>
|
||||
<el-form-item label="部门"><el-input v-model="createForm.department" /></el-form-item>
|
||||
<el-form-item label="职务"><el-input v-model="createForm.position" /></el-form-item>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<el-form-item label="状态">
|
||||
<el-radio-group v-model="createForm.status">
|
||||
<el-radio value="0">启用</el-radio>
|
||||
<el-radio value="1">停用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="createVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveCreate">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { listUser } from '@/api/system'
|
||||
import request from '@/utils/request'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
@@ -112,7 +196,7 @@ const ROLE_LABEL = {
|
||||
}
|
||||
const roleOptions = Object.entries(ROLE_LABEL).map(([value, label]) => ({ value, label }))
|
||||
|
||||
const q = reactive({ userName: '', nickName: '', roleType: '', status: '', pageNum: 1, pageSize: 20 })
|
||||
const q = reactive({ userName: '', nickName: '', orgName: '', roleType: '', status: '', pageNum: 1, pageSize: 20 })
|
||||
const rows = ref([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
@@ -131,7 +215,7 @@ async function load() {
|
||||
}
|
||||
|
||||
function reset() {
|
||||
q.userName = ''; q.nickName = ''; q.roleType = ''; q.status = ''
|
||||
q.userName = ''; q.nickName = ''; q.orgName = ''; q.roleType = ''; q.status = ''
|
||||
q.pageNum = 1
|
||||
load()
|
||||
}
|
||||
@@ -174,6 +258,22 @@ async function toggleStatus(row) {
|
||||
}
|
||||
}
|
||||
|
||||
async function onResetPwd(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定重置「${row.userName}」的登录密码为默认 123456 吗?`,
|
||||
'重置密码',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
} catch { return }
|
||||
try {
|
||||
await request({ url: '/system/user/resetPwd', method: 'put', data: { userId: row.userId, password: '123456' } })
|
||||
ElMessage.success('已重置为 123456')
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.msg || e?.message || '重置失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function delUser(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
@@ -191,6 +291,75 @@ async function delUser(row) {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 新建用户 (按角色级联) ==========
|
||||
const createVisible = ref(false)
|
||||
const orgOptions = ref([])
|
||||
const createForm = reactive({
|
||||
roleType: '', userName: '', nickName: '', password: '', phonenumber: '',
|
||||
email: '', status: '0',
|
||||
workUnit: '', department: '', title: '', position: '',
|
||||
practiceCertUrl: '', titleCertUrl: '',
|
||||
accountType: 'MAIN', orgId: null, orgName: '', businessNature: '',
|
||||
contactName: '', contactPhone: ''
|
||||
})
|
||||
const isSimpleRole = computed(() => createForm.roleType === 'admin' || createForm.roleType === 'manager')
|
||||
const isUnitRole = computed(() => createForm.roleType === 'executor' || createForm.roleType === 'sponsor')
|
||||
|
||||
function openCreate() {
|
||||
Object.assign(createForm, {
|
||||
roleType: '', userName: '', nickName: '', password: '', phonenumber: '',
|
||||
email: '', status: '0',
|
||||
workUnit: '', department: '', title: '', position: '',
|
||||
practiceCertUrl: '', titleCertUrl: '',
|
||||
accountType: 'MAIN', orgId: null, orgName: '', businessNature: '',
|
||||
contactName: '', contactPhone: ''
|
||||
})
|
||||
orgOptions.value = []
|
||||
createVisible.value = true
|
||||
}
|
||||
|
||||
function onRoleChange() {
|
||||
createForm.accountType = 'MAIN'
|
||||
orgOptions.value = []
|
||||
}
|
||||
|
||||
async function onAccountTypeChange(val) {
|
||||
orgOptions.value = []
|
||||
if (val === 'SUB' && isUnitRole.value) await loadOrgOptions()
|
||||
}
|
||||
|
||||
async function loadOrgOptions() {
|
||||
try {
|
||||
const r = await request({
|
||||
url: '/business/org/list',
|
||||
method: 'get',
|
||||
params: { orgType: createForm.roleType, pageSize: 500 }
|
||||
})
|
||||
orgOptions.value = (r.data && r.data.rows) || r.rows || []
|
||||
} catch (e) { /* GET 错误拦截器已统一 toast, 这里静默 */ }
|
||||
}
|
||||
|
||||
async function saveCreate() {
|
||||
if (!createForm.roleType) return ElMessage.warning('请选择角色')
|
||||
if (!createForm.userName) return ElMessage.warning('请填写登录账号')
|
||||
if (!createForm.password) return ElMessage.warning('请填写密码')
|
||||
if (isUnitRole.value) {
|
||||
if (createForm.accountType === 'MAIN' && !createForm.orgName) return ElMessage.warning('请填写单位名称')
|
||||
if (createForm.accountType === 'SUB' && !createForm.orgId) return ElMessage.warning('请选择所属单位')
|
||||
}
|
||||
if ((createForm.roleType === 'doctor' || (isUnitRole.value && createForm.accountType === 'SUB')) && !createForm.nickName) {
|
||||
return ElMessage.warning('请填写姓名')
|
||||
}
|
||||
try {
|
||||
await request({ url: '/business/adminUser/create', method: 'post', data: { ...createForm } })
|
||||
ElMessage.success('新建成功')
|
||||
createVisible.value = false
|
||||
load()
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.msg || e?.message || '新建失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
@@ -199,5 +368,6 @@ onMounted(load)
|
||||
.admin-users { padding: 16px; }
|
||||
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
|
||||
.filter-form { margin-bottom: 12px; }
|
||||
.toolbar { margin-bottom: 12px; }
|
||||
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
|
||||
</style>
|
||||
@@ -41,11 +41,11 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { listUser } from '@/api/system'
|
||||
import { listUser, listByRole } from '@/api/system'
|
||||
import { bizList } from '@/api/public'
|
||||
|
||||
const stats = ref({
|
||||
totalUsers: 0, roleCount: 6, totalProjects: 0, finishedProjects: 0, totalMeetings: 0
|
||||
totalUsers: 0, roleCount: 0, totalProjects: 0, finishedProjects: 0, totalMeetings: 0
|
||||
})
|
||||
const roleStats = ref([])
|
||||
|
||||
@@ -58,20 +58,22 @@ async function load() {
|
||||
try {
|
||||
// 用户总数 (sys_user)
|
||||
const u = await listUser({ pageNum: 1, pageSize: 1 })
|
||||
stats.value.totalUsers = u.total || 0
|
||||
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 listUser({ pageNum: 1, pageSize: 1, roleType: code })
|
||||
all.push({ code, label: ROLE_LABEL[code], count: (r.data && r.data.total) || r.total || 0 })
|
||||
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 })
|
||||
}
|
||||
}
|
||||
roleStats.value = all
|
||||
// 角色类型数 = 实际定义的业务角色数 (admin/manager/doctor/executor/sponsor = 5)
|
||||
stats.value.roleCount = codes.length
|
||||
|
||||
// 项目数
|
||||
const ps = await bizList('project', { pageNum: 1, pageSize: 1 })
|
||||
|
||||
@@ -208,14 +208,6 @@ const registerTypes = [
|
||||
{ value: 'sponsor', name: '项目支持单位', desc: '注册后默认为管理员角色, 可建监察员子账号' }
|
||||
]
|
||||
|
||||
const userTypes = [
|
||||
{ value: 'admin', name: '系统管理员', desc: '系统配置、用户管理、权限控制' },
|
||||
{ value: 'manager', name: '项目经理', desc: '项目管理、方案审核、过程监督' },
|
||||
{ value: 'doctor', name: '评审专家', desc: '项目评审、评分反馈、查看任务' },
|
||||
{ value: 'executor', name: '执行单位', desc: '会议组织、人员安排、劳务结算' },
|
||||
{ value: 'sponsor', name: '支持单位', desc: '支持函管理、人员管理、合作记录' },
|
||||
]
|
||||
|
||||
async function loadCaptcha() {
|
||||
await runCaptchaOnce(async () => {
|
||||
try {
|
||||
@@ -245,7 +237,8 @@ async function onSubmit() {
|
||||
const res = await login({ username: form.username, password: form.password, code: form.code || '', uuid: form.uuid || '' })
|
||||
await afterLogin(res.token, form.username)
|
||||
} catch (e) {
|
||||
// 错误提示已由 utils/request.js 拦截器统一弹 (ElMessage.error), 这里只刷新验证码
|
||||
// 登录是 POST, 拦截器对写操作不自动 toast, 需在此显式弹 (否则锁定/密码错等 msg 看不到)
|
||||
ElMessage.error(e?.msg || e?.message || '登录失败')
|
||||
loadCaptcha()
|
||||
} finally {
|
||||
loading.value = false
|
||||
@@ -283,7 +276,7 @@ async function afterLogin(token, displayName) {
|
||||
ElMessage.error('获取用户信息失败, 请重新登录')
|
||||
return router.replace({ name: 'login' })
|
||||
}
|
||||
ElMessage.success(`欢迎,${displayName}(${userTypes.find(u => u.value === role)?.name || role})`)
|
||||
ElMessage.success(`欢迎,${displayName}`)
|
||||
// 角色不在角色首页映射里 → 拒绝
|
||||
if (!roleHome[role]) return router.replace({ name: 'login' })
|
||||
// 所有角色统一跳门户首页 '/' (业务方 2026-08-22 要求, 各自角色菜单从导航栏进入)
|
||||
@@ -331,7 +324,8 @@ async function sendLoginSmsCode() {
|
||||
}
|
||||
}, 1000)
|
||||
} catch (e) {
|
||||
// request.js 已弹错误, 这里只重置 uuid
|
||||
// 短信发送是 POST, 拦截器不自动 toast, 显式弹
|
||||
ElMessage.error(e?.msg || e?.message || '验证码发送失败')
|
||||
smsForm.uuid = ''
|
||||
}
|
||||
})
|
||||
@@ -347,7 +341,8 @@ async function onSmsLoginSubmit() {
|
||||
// 短信登录后无 username, 用 phone 作为显示名; fallbackRole 留空, 让 /getInfo 决定
|
||||
await afterLogin(res.token, smsForm.phone)
|
||||
} catch (e) {
|
||||
// request.js 已弹错误
|
||||
// 短信登录是 POST, 拦截器不自动 toast, 显式弹
|
||||
ElMessage.error(e?.msg || e?.message || '登录失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -437,7 +432,8 @@ async function sendForgotSms() {
|
||||
}
|
||||
}, 1000)
|
||||
} catch (e) {
|
||||
// request.js 拦截器已弹 ElMessage.error, 这里只重置
|
||||
// 忘记密码短信是 POST, 拦截器不自动 toast, 显式弹
|
||||
ElMessage.error(e?.msg || e?.message || '验证码发送失败')
|
||||
forgotSmsUuid = ''
|
||||
}
|
||||
})
|
||||
@@ -468,7 +464,8 @@ async function onResetSubmit() {
|
||||
ElMessage.success('密码重置成功, 请用新密码登录')
|
||||
closeForgot()
|
||||
} catch (e) {
|
||||
// request.js 已弹错误
|
||||
// 重置密码是 POST, 拦截器不自动 toast, 显式弹
|
||||
ElMessage.error(e?.msg || e?.message || '重置失败')
|
||||
} finally {
|
||||
forgotLoading.value = false
|
||||
}
|
||||
|
||||
@@ -40,10 +40,10 @@
|
||||
<el-form-item label="确认密码" prop="confirmPassword">
|
||||
<el-input v-model="form.confirmPassword" type="password" show-password placeholder="请再次输入登录密码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="执业证书/证明">
|
||||
<el-form-item label="执业证书/证明" prop="licenseCertUrl">
|
||||
<OssImageUploader v-model="form.licenseCertUrl" dir="ry8080/cert/license/" placeholder="+" />
|
||||
</el-form-item>
|
||||
<el-form-item label="职称证明">
|
||||
<el-form-item label="职称证明" prop="titleCertUrl">
|
||||
<OssImageUploader v-model="form.titleCertUrl" dir="ry8080/cert/title/" placeholder="+" />
|
||||
</el-form-item>
|
||||
|
||||
@@ -132,7 +132,9 @@ const rules = {
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
],
|
||||
licenseCertUrl: [{ required: true, message: '请上传执业证书/证明', trigger: 'change' }],
|
||||
titleCertUrl: [{ required: true, message: '请上传职称证明', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const agreed = ref(false)
|
||||
|
||||
@@ -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="260" fixed="right">
|
||||
<el-table-column label="操作" width="340" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<!-- 查看 (只读详情): 两角色都有, 跳独立详情页 -->
|
||||
<el-button link type="primary" @click="goView(row)">查看</el-button>
|
||||
@@ -75,6 +75,8 @@
|
||||
<el-button link :type="isDisabled(row) ? 'success' : 'danger'" @click="onToggleStatus(row)">
|
||||
{{ isDisabled(row) ? '启用' : '禁用' }}
|
||||
</el-button>
|
||||
<!-- 重置密码: 重置为默认 123456 (与新建人员默认密码一致) -->
|
||||
<el-button v-if="row.userId" link type="primary" @click="onResetPwd(row)">重置密码</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -123,7 +125,7 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { bizList, bizUpdate, importPerson, downloadImportTemplate, changePersonAdmin } from '@/api/public'
|
||||
import { bizList, bizUpdate, importPerson, downloadImportTemplate, changePersonAdmin, resetPersonPassword } from '@/api/public'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Upload } from '@element-plus/icons-vue'
|
||||
import ImportResultDialog from '@/components/ImportResultDialog.vue'
|
||||
@@ -217,6 +219,23 @@ async function onChangeAdmin(row) {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 重置密码 ==========
|
||||
async function onResetPwd(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定重置「${row.name}」的登录密码为默认 123456 吗?`,
|
||||
'重置密码',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
} catch { return }
|
||||
try {
|
||||
await resetPersonPassword(row.personId)
|
||||
ElMessage.success('已重置为 123456')
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.msg || e?.message || '重置失败')
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 跳转 (按角色) ==========
|
||||
function goNew() {
|
||||
router.push({
|
||||
|
||||
@@ -138,7 +138,6 @@ async function onSave() {
|
||||
// 从 org 页面跳来时 route.query 带 orgId, 直接作为 FK
|
||||
if (!isEdit && orgId.value) payload.orgId = orgId.value
|
||||
if (isEdit) {
|
||||
delete payload.personId
|
||||
await bizUpdate('person', payload)
|
||||
ElMessage.success('修改成功')
|
||||
} else {
|
||||
|
||||
@@ -2,66 +2,63 @@
|
||||
<div class="page-card executor-meetings">
|
||||
<div class="breadcrumb">首页 / 会议列表</div>
|
||||
|
||||
<!-- ========== 筛选区 (与 People.vue 风格一致) ========== -->
|
||||
<!-- ========== 筛选区 (仿 manager/meetings/Meetings.vue: 项目编号/会议ID/会议名称/期数/会议时间/项目形式/当前阶段/备注) ========== -->
|
||||
<el-form inline :model="q" class="filter-form">
|
||||
<el-form-item label="项目编号"><el-input v-model="q.projectNo" clearable placeholder="输入项目编号" style="width:160px" /></el-form-item>
|
||||
<el-form-item label="会议名称"><el-input v-model="q.meetingName" clearable placeholder="输入会议名称" style="width:200px" /></el-form-item>
|
||||
<el-form-item label="是否系列会">
|
||||
<el-select v-model="q.isSeries" clearable placeholder="请选择" style="width:140px">
|
||||
<el-option label="是" value="1" />
|
||||
<el-option label="否" value="0" />
|
||||
</el-select>
|
||||
<el-form-item label="项目编号"><el-input v-model="q.projectNo" placeholder="请输入项目编号" clearable style="width:140px" /></el-form-item>
|
||||
<el-form-item label="会议ID"><el-input v-model="q.meetingId" placeholder="请输入会议ID" clearable style="width:140px" /></el-form-item>
|
||||
<el-form-item label="会议名称"><el-input v-model="q.meetingName" placeholder="请输入会议名称" clearable style="width:160px" /></el-form-item>
|
||||
<el-form-item label="期数"><el-input-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" />
|
||||
<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-form-item>
|
||||
<el-form-item label="是否结题">
|
||||
<el-select v-model="q.isFinished" clearable placeholder="请选择" style="width:140px">
|
||||
<el-option label="已结题" value="1" />
|
||||
<el-option label="未结题" value="0" />
|
||||
<el-form-item label="项目形式">
|
||||
<el-select v-model="q.projectForm" placeholder="请选择" clearable style="width:140px">
|
||||
<el-option label="线上" value="线上" />
|
||||
<el-option label="线下" value="线下" />
|
||||
<el-option label="线上+线下" value="线上+线下" />
|
||||
<el-option label="其他" value="其他" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="当前阶段">
|
||||
<el-select v-model="q.currentStage" clearable placeholder="请选择" style="width:140px">
|
||||
<el-select v-model="q.currentStage" placeholder="请选择" clearable style="width:140px">
|
||||
<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 label="期数"><el-input v-model="q.sessionNo" clearable placeholder="输入期数" style="width:100px" /></el-form-item>
|
||||
<el-form-item label="备注"><el-input v-model="q.remark" placeholder="请输入备注" clearable style="width:140px" /></el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="load">查找</el-button>
|
||||
<el-button @click="reset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- ========== 表格 (与 People.vue 风格一致, 无 page-card 包裹) ========== -->
|
||||
<el-table :data="rows" v-loading="loading" stripe border @selection-change="onSelect">
|
||||
<el-table-column type="selection" width="48" />
|
||||
<el-table-column prop="projectNo" label="项目编号" width="120" />
|
||||
<el-table-column prop="meetingName" label="会议名称" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="isSeries" label="是否系列会" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.isSeries === '1' || row.isSeries === '是' ? 'success' : 'info'" size="small">
|
||||
{{ row.isSeries === '1' || row.isSeries === '是' ? '是' : '否' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
<!-- ========== 表格 (仿 manager/meetings/Meetings.vue 信息列, 去掉 admin/manager 专属列) ========== -->
|
||||
<el-table :data="rows" v-loading="loading" stripe border>
|
||||
<el-table-column prop="projectNo" label="项目编号" width="130" fixed />
|
||||
<el-table-column prop="meetingId" label="会议ID" width="120" align="center" />
|
||||
<el-table-column prop="projectForm" label="项目形式" width="100" align="center" />
|
||||
<el-table-column prop="meetingName" label="会议名称" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="会议开始时间" width="160" align="center">
|
||||
<template #default="{ row }">{{ fmtTime(row.startTime) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="seriesTopic" label="系列会主题" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="期数/总期数" width="110" align="center">
|
||||
<template #default="{ row }">{{ row.periodNo || 0 }}/{{ row.totalPeriods || 0 }}</template>
|
||||
<el-table-column label="会议结束时间" width="160" align="center">
|
||||
<template #default="{ row }">{{ fmtTime(row.endTime) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="totalPeriods" label="总期数" width="80" align="center" />
|
||||
<el-table-column label="期数" width="80" align="center">
|
||||
<template #default="{ row }">{{ row.periodNo ? '第 ' + row.periodNo + ' 期' : '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="currentStage" label="当前阶段" width="140" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.currentStage === 'RUNNING' ? 'success' : 'info'" size="small">{{ stageLabel('executor', row) }}</el-tag>
|
||||
<span :class="['stage-tag', 'stage-' + stageClass(row)]">{{ stageLabel('executor', row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="isFinished" label="是否结题" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.isFinished === '1' || row.isFinished === '已结题' ? 'warning' : 'info'" size="small">
|
||||
{{ row.isFinished === '1' || row.isFinished === '已结题' ? '已结题' : '未结题' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="200" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="onView(row)">查看</el-button>
|
||||
<el-button link type="primary" @click="onUpload(row)">上传材料</el-button>
|
||||
<el-button link type="primary" @click="onUpload(row)">编辑材料</el-button>
|
||||
<el-button link type="primary" @click="onEdit(row)">修改</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -78,13 +75,25 @@
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { bizList } from '@/api/public'
|
||||
import { stageLabel, STAGE_OPTIONS } from '@/utils/meetingStage'
|
||||
import { stageLabel, STAGE_OPTIONS, stageClass } from '@/utils/meetingStage'
|
||||
|
||||
const q = ref({ projectNo: '', meetingName: '', isSeries: '', isFinished: '', sessionNo: '', currentStage: '' })
|
||||
// 与 manager/meetings/Meetings.vue 一致: 8 项筛选 (项目编号/会议ID/会议名称/期数/会议时间/项目形式/当前阶段/备注)
|
||||
const q = ref({
|
||||
projectNo: '', meetingId: '', meetingName: '', periodNo: null,
|
||||
projectForm: '', currentStage: '', remark: '',
|
||||
startTime: '', endTime: ''
|
||||
})
|
||||
const rows = ref([])
|
||||
const loading = ref(false)
|
||||
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
|
||||
const selected = ref([])
|
||||
|
||||
// 工具: 时间格式 (yyyy.MM.dd HH:mm), 与 manager 列表一致
|
||||
function fmtTime(d) {
|
||||
if (!d) return ''
|
||||
const dt = new Date(d)
|
||||
const pad = n => String(n).padStart(2, '0')
|
||||
return `${dt.getFullYear()}.${pad(dt.getMonth() + 1)}.${pad(dt.getDate())} ${pad(dt.getHours())}:${pad(dt.getMinutes())}`
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
@@ -96,24 +105,25 @@ async function load() {
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
// 读 URL query 写入 q (Overview KPI 卡跳转时带 ?currentStage=RUNNING/NOT_STARTED + ?isFinished=1, 让列表页自动应用筛选)
|
||||
// 读 URL query 写入 q (Overview KPI 卡跳转时带 ?currentStage=RUNNING/NOT_STARTED, 让列表页自动应用筛选)
|
||||
// 只读不改 URL — Overview 是 source of truth, 列表页内 reset()/search 不反向写 URL
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
function readQueryFromRoute() {
|
||||
const q2 = route.query
|
||||
if (q2.currentStage != null && q2.currentStage !== '') q.value.currentStage = String(q2.currentStage)
|
||||
if (q2.isFinished != null && q2.isFinished !== '') q.value.isFinished = String(q2.isFinished)
|
||||
}
|
||||
|
||||
function reset() {
|
||||
q.value = { projectNo: '', meetingName: '', isSeries: '', isFinished: '', sessionNo: '', currentStage: '' }
|
||||
q.value = {
|
||||
projectNo:'', meetingId:'', meetingName:'', periodNo:null,
|
||||
projectForm:'', currentStage:'', remark:'',
|
||||
startTime:'', endTime:''
|
||||
}
|
||||
page.pageNum = 1
|
||||
load()
|
||||
}
|
||||
|
||||
function onSelect(arr) { selected.value = arr }
|
||||
|
||||
function onView(row) { router.push(`/executor/meetings/view/${row.meetingId}`) }
|
||||
|
||||
function onUpload(row) { router.push(`/executor/meetings/detail/${row.meetingId}`) }
|
||||
@@ -126,9 +136,26 @@ onMounted(() => { readQueryFromRoute(); load() })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 整页面板 (与 People.vue 风格一致) */
|
||||
/* 整页面板 (仿 manager/meetings/Meetings.vue 风格) */
|
||||
.executor-meetings { padding: 16px; }
|
||||
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
|
||||
.filter-form { margin-bottom: 12px; }
|
||||
.date-sep { margin: 0 4px; color: #606266; }
|
||||
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
|
||||
</style>
|
||||
|
||||
/* 当前阶段 tag 颜色 (与 manager 列表一致) */
|
||||
.stage-tag {
|
||||
display: inline-block;
|
||||
padding: 2px 10px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
border: 1px solid;
|
||||
}
|
||||
.stage-pending { color: #909399; border-color: #dcdfe6; background: #f5f7fa; }
|
||||
.stage-running { color: var(--brand-primary); border-color: #b3c5e0; background: #eef1f8; }
|
||||
.stage-done { color: #67c23a; border-color: #c2e7b0; background: #f0f9eb; }
|
||||
.stage-reviewing { color: #e6a23c; border-color: #f5dab1; background: #fdf6ec; }
|
||||
.stage-waiting { color: #ff4d4f; border-color: #fbc4c4; background: #fef0f0; }
|
||||
.stage-frozen { color: #909399; border-color: #dcdfe6; background: #f5f7fa; opacity: 0.6; }
|
||||
.stage-default { color: #606266; border-color: #dcdfe6; background: #fafafa; }
|
||||
</style>
|
||||
|
||||
@@ -41,10 +41,12 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right">
|
||||
<el-table-column label="操作" width="300" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" link type="primary" @click="onView(row)">查看</el-button>
|
||||
<el-button size="small" link type="primary" @click="goEdit(row)">编辑</el-button>
|
||||
<!-- 重置密码: 重置为默认 123456 (与新建人员默认密码一致) -->
|
||||
<el-button v-if="row.userId" size="small" link type="primary" @click="onResetPwd(row)">重置密码</el-button>
|
||||
<!-- 本人不显示禁用/恢复按钮 (跟 sponsor 一样, 避免主账号把自己禁用) -->
|
||||
<template v-if="row.userId !== store.user?.userId">
|
||||
<el-button v-if="row.status === '0'" size="small" link type="danger" @click="onToggleStatus(row, '禁用')">禁用</el-button>
|
||||
@@ -107,7 +109,7 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { bizUpdate } from '@/api/public'
|
||||
import { bizUpdate, resetPersonPassword } from '@/api/public'
|
||||
import { listExecutorPerson } from '@/api/business/person'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
@@ -151,6 +153,22 @@ function goEdit(row) { router.push(`/executor/people/edit/${row.personId}`) }
|
||||
|
||||
function onView(row) { router.push(`/executor/people/detail/${row.personId}`) }
|
||||
|
||||
async function onResetPwd(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定重置「${row.name}」的登录密码为默认 123456 吗?`,
|
||||
'重置密码',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
} catch { return }
|
||||
try {
|
||||
await resetPersonPassword(row.personId)
|
||||
ElMessage.success('已重置为 123456')
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.msg || e?.message || '重置失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function onToggleStatus(row, action) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定${action}「${row.name}」吗?`, action, { type: 'warning' })
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<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.sponsorAdminUserName || '-' }}</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-col :span="12"><el-form-item label="项目评价"><span class="info-value">{{ fmtScore(row.managerScore) }}</span></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="12">
|
||||
@@ -89,18 +89,18 @@
|
||||
|
||||
<!-- (已分配执行方 已折叠进上方"项目信息"section, 此处不再重复) -->
|
||||
|
||||
<!-- ================= 4. 已发布公告 (只读, 仅查看) ================= -->
|
||||
<div class="new-card-title">已发布公告</div>
|
||||
<el-table v-loading="loading" :data="announcements" border>
|
||||
<el-table-column prop="title" label="公告标题" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="createTime" label="发布时间" width="160" align="center" :formatter="(r)=>fmtDate(r.createTime)" />
|
||||
<el-table-column prop="creatorName" label="发布人" width="120" align="center" />
|
||||
<el-table-column label="操作" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openPreview(row.fileUrl || row.url, row.title)">查看</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- ================= 4. 公告文件 (只读, 仅查看: 邀请函/支持函/通知/日程) ================= -->
|
||||
<div class="new-card-title">公告文件</div>
|
||||
<div class="notice-list">
|
||||
<div v-for="(n, idx) in notices" :key="idx" class="notice-row">
|
||||
<span class="notice-label">{{ n.label }}</span>
|
||||
<span v-if="n.url" class="notice-file">
|
||||
<span class="file-name">{{ n.name || '已上传' }}</span>
|
||||
<el-button link type="primary" @click="openPreview(n.url, n.label)">查看</el-button>
|
||||
</span>
|
||||
<span v-else class="file-empty">未上传</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 公告预览 (只读) -->
|
||||
<Preview v-model="previewOpen" :url="previewUrl" :title="previewTitle" />
|
||||
@@ -108,9 +108,9 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { bizGet, bizList } from '@/api/public'
|
||||
import { bizGet } from '@/api/public'
|
||||
import { listExecutorOrgs } from '@/api/system'
|
||||
import request from '@/utils/request'
|
||||
import Preview from '@/components/Preview.vue'
|
||||
@@ -135,8 +135,22 @@ const form = reactive({
|
||||
// 监督员 (从 biz_project.monitors JSON 字段读)
|
||||
const monitors = ref([])
|
||||
|
||||
// 已发布公告
|
||||
const announcements = ref([])
|
||||
// 公告文件 (邀请函/支持函/通知/日程) — 数据源 biz_project 4 个独立列, 与 ProjectsNew 反显逻辑一致
|
||||
// 注: 后端无 publicityFiles 字段 (前端传了但不落库), 实际落库是 invitation_url/support_letter_url/publish_url/schedule_url
|
||||
const notices = computed(() => {
|
||||
const r = row.value || {}
|
||||
const items = [
|
||||
{ label: '邀请函', url: r.invitationUrl },
|
||||
{ label: '支持函', url: r.supportLetterUrl },
|
||||
{ label: '通知', url: r.publishUrl },
|
||||
{ label: '日程', url: r.scheduleUrl }
|
||||
]
|
||||
return items.map(m => ({
|
||||
label: m.label,
|
||||
url: m.url,
|
||||
name: m.url ? (m.url.split('/').pop() || '') : ''
|
||||
}))
|
||||
})
|
||||
|
||||
// 公告预览
|
||||
const previewOpen = ref(false)
|
||||
@@ -186,17 +200,6 @@ async function loadProject() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAnnouncements() {
|
||||
try {
|
||||
const resp = await bizList('publicity', { projectId: projectId.value })
|
||||
const arr = (resp && resp.rows) || ((resp && resp.data) && (resp && resp.data).rows) || (resp && resp.data) || []
|
||||
announcements.value = Array.isArray(arr) ? arr : []
|
||||
} catch (e) {
|
||||
console.error('[project-detail] announcements load failed', e)
|
||||
announcements.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 已分配执行方 (服务公司) =====================
|
||||
/**
|
||||
* 数据源同 manager/projects/edit/{id} 的 loadAssigns:
|
||||
@@ -272,7 +275,6 @@ function openPreview(url, title) {
|
||||
onMounted(async () => {
|
||||
await loadProject()
|
||||
await Promise.all([loadAssigns(), loadMonitors()])
|
||||
await loadAnnouncements()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -326,6 +328,14 @@ onMounted(async () => {
|
||||
.info-table td { padding: 10px 12px; border-bottom: 1px solid #f5f5f5; color: #595959; vertical-align: middle; }
|
||||
.empty-row { text-align: center; color: #c0c4cc; }
|
||||
|
||||
/* 公告文件 (只读 4 行: 邀请函/支持函/通知/日程) */
|
||||
.notice-list { display: flex; flex-direction: column; gap: 6px; }
|
||||
.notice-row { display: flex; align-items: center; gap: 12px; padding: 6px 0; }
|
||||
.notice-label { width: 90px; flex-shrink: 0; color: #606266; font-size: 14px; }
|
||||
.notice-file { flex: 1; display: flex; align-items: center; gap: 12px; min-width: 0; }
|
||||
.file-name { color: #1a1a1a; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.file-empty { color: #c0c4cc; font-size: 13px; }
|
||||
|
||||
/* 提示 (灰色, 跟 ProjectsNew 一致) */
|
||||
.hint-text { font-size: 12px; color: #909399; margin: 8px 0; line-height: 1.6; }
|
||||
.hint-text p { margin-bottom: 4px; }
|
||||
|
||||
@@ -66,7 +66,6 @@
|
||||
<span v-if="singleAmountOver" class="over-warn">⚠ 已超出 ¥ {{ Math.round((assignedAmount - assignForm.totalAmount) * 100) / 100 }}</span>
|
||||
<span class="sep">|</span>
|
||||
<span>可用金额: <b>¥ {{ availableAmount }}</b></span>
|
||||
<span class="hint">(公式: 总金额×(1-管理费/总金额)-累计劳务-累计会务)</span>
|
||||
</div>
|
||||
<!-- 批量模式: 提示保存时按各项目自身校验 -->
|
||||
<div v-else class="assign-sessions-bar">
|
||||
@@ -84,7 +83,7 @@
|
||||
placeholder="搜索执行单位"
|
||||
style="width:100%"
|
||||
@change="v => onExecUserPick(row, v)">
|
||||
<el-option v-for="u in (row._options || [])" :key="u.orgId"
|
||||
<el-option v-for="u in execOptions(row)" :key="u.orgId"
|
||||
:label="u.orgName" :value="u.orgId" />
|
||||
</el-select>
|
||||
</template>
|
||||
@@ -111,7 +110,7 @@
|
||||
|
||||
<el-form-item label="提交截止(天)" style="margin-top:12px">
|
||||
<el-input-number v-model="assignForm.deadlineDays" :min="1" :max="100" controls-position="right" />
|
||||
<span class="hint-text" style="margin-left:12px">自然日, 1~100 天 (超出报错)</span>
|
||||
<span class="hint-text" style="margin-left:12px">自然日, 1~100 天</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
@@ -326,6 +325,16 @@ function onExecUserPick(row, orgId) {
|
||||
if (u) { /* 兼容旧 _options 残留, 不写业务字段 */ }
|
||||
}
|
||||
|
||||
// 当前行下拉候选 = _options 排除「其它行已选」的执行单位
|
||||
// (避免同一项目重复分配同一执行单位; 当前行自己已选的保留, 否则 el-select 已选 label 会空白)
|
||||
function execOptions(row) {
|
||||
const excluded = new Set()
|
||||
for (const r of assignForm.execRows) {
|
||||
if (r !== row && r.executionUnitId) excluded.add(r.executionUnitId)
|
||||
}
|
||||
return (row._options || []).filter(u => !excluded.has(u.orgId))
|
||||
}
|
||||
|
||||
// ========== 初始化 ==========
|
||||
async function loadSingleProject() {
|
||||
if (!singleProjectId) return
|
||||
|
||||
@@ -101,7 +101,6 @@
|
||||
|
||||
<!-- ========== 第三区块:公告文件 ========== -->
|
||||
<div class="new-card-title">公告文件</div>
|
||||
<p class="hint-text">*支持 PDF / 图片, 单文件 ≤ 10MB; 名称规则: 公告文件类型 + 项目编号后两段 + 项目名称</p>
|
||||
<div class="notice-list">
|
||||
<div v-for="(n, idx) in form.notices" :key="idx" class="notice-row">
|
||||
<span class="notice-label">{{ n.label }}</span>
|
||||
@@ -335,11 +334,6 @@ async function submit(mode = 'save') {
|
||||
if (mode === 'publish') {
|
||||
payload.isPublished = '1'
|
||||
payload.publishTime = new Date().toISOString().slice(0, 19).replace('T', ' ')
|
||||
// announcement_type 取第一个已上传的文件类型
|
||||
const firstUploaded = uploaded[0]
|
||||
if (firstUploaded) {
|
||||
payload.announcementType = firstUploaded.key
|
||||
}
|
||||
}
|
||||
// 多执行方分配已移至"项目分配"页面单独管理
|
||||
try {
|
||||
|
||||
@@ -33,8 +33,9 @@
|
||||
<div class="info-row"><span class="info-label">总费用:</span><span class="info-value fee-val">¥ {{ fmtMoney(row.totalFee) }}</span>
|
||||
<el-tag v-if="row.feeCalcStatus === 0" type="info" size="small" effect="plain">统计中…</el-tag>
|
||||
</div>
|
||||
<!-- 监察员 + 执行人员 (各占整行, 跨 2 列). 临时: manager 详情页隐掉 -->
|
||||
<template v-if="!isManager">
|
||||
<!-- 监察员 + 执行人员: 暂时整体注释掉 (所有角色都不看, 含 admin). 需要时取消注释恢复 -->
|
||||
<!--
|
||||
<template v-if="!isManager && !isExecutor && !isSponsor">
|
||||
<div class="info-row info-row-full">
|
||||
<span class="info-label">监察员:</span>
|
||||
<div class="tag-list">
|
||||
@@ -52,6 +53,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -99,7 +101,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column v-if="!isSponsor" label="身份证附件" min-width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<a v-if="row.idCardAttachments" :href="row.idCardAttachments.split(',')[0]" target="_blank" class="file-link">查看</a>
|
||||
<el-button v-if="idCardFiles(row).length" link type="primary" size="small" @click="openPreview(idCardFiles(row), '身份证')">查看</el-button>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -119,7 +121,7 @@
|
||||
<el-table-column prop="laborForm" label="角色" min-width="90" show-overflow-tooltip />
|
||||
<el-table-column label="劳务协议" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<a v-if="protocolUrl(row)" :href="protocolUrl(row)" target="_blank" class="file-link">查看</a>
|
||||
<el-button v-if="protocolUrl(row)" link type="primary" size="small" @click="openPreview([protocolUrl(row)], '劳务协议')">查看</el-button>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -187,7 +189,7 @@
|
||||
<div v-for="r in laborMaterialRows" :key="r.label" class="file-row">
|
||||
<span class="file-label">{{ r.label }}:</span>
|
||||
<CameraQrUpload v-if="isCameraSubType(r.subType)" v-model="r.url" :meeting-id="meetingId" :sub-type="r.subType" :label="r.label" :readonly="isSponsor || isReadonly" class="file-uploader" />
|
||||
<OssFileUploader v-else v-model="r.url" :dir="`ry8080/meeting/${meetingId}/labor/`" :placeholder="`点击上传 ${r.label}`" class="file-uploader" :readonly="isSponsor || isReadonly" />
|
||||
<OssFileUploader v-else v-model="r.url" v-model:name="r.fileName" :dir="`ry8080/meeting/${meetingId}/labor/`" :placeholder="`点击上传 ${r.label}`" class="file-uploader" :readonly="isSponsor || isReadonly" />
|
||||
<span v-if="materialAmount(r.subType) > 0" class="invoice-amount">¥ {{ materialAmount(r.subType).toFixed(2) }}</span>
|
||||
<div v-if="r.hint" class="file-hint">{{ r.hint }}</div>
|
||||
</div>
|
||||
@@ -206,13 +208,13 @@
|
||||
<OssFileUploader v-model="scheduleUrl" :dir="`ry8080/meeting/${meetingId}/schedule/`" placeholder="点击上传日程海报" class="file-uploader" :readonly="isSponsor || isReadonly" />
|
||||
<div class="file-hint">会议日程海报</div>
|
||||
</div>
|
||||
<div class="file-row">
|
||||
<div v-if="!isExecutor" class="file-row">
|
||||
<span class="file-label">邀请函:</span>
|
||||
<OssFileUploader v-model="invitationUrl" :dir="`ry8080/meeting/${meetingId}/invitation/`" placeholder="点击上传邀请函" class="file-uploader" :readonly="isSponsor || isReadonly" />
|
||||
<div class="file-hint">会议邀请函</div>
|
||||
</div>
|
||||
<!-- 临时: manager 详情页隐掉海报生成 (含 生成海报/预览海报) -->
|
||||
<div v-if="!isManager && !isReadonly" class="file-row">
|
||||
<!-- 临时: manager / executor 详情页隐掉海报生成 (含 生成海报/预览海报) -->
|
||||
<div v-if="!isManager && !isExecutor && !isReadonly" class="file-row">
|
||||
<span class="file-label">海报生成:</span>
|
||||
<div class="poster-actions">
|
||||
<el-button v-if="!isSponsor" size="small" type="primary" :loading="genPosterLoading" @click="onGeneratePoster">生成海报</el-button>
|
||||
@@ -225,7 +227,7 @@
|
||||
<div class="file-list">
|
||||
<div v-for="r in serviceMaterialRows" :key="r.label" class="file-row">
|
||||
<span class="file-label">{{ r.label }}:</span>
|
||||
<OssFileUploader v-model="r.url" :dir="`ry8080/meeting/${meetingId}/service/`" :placeholder="`点击上传 ${r.label}`" class="file-uploader" :readonly="isSponsor || isReadonly" />
|
||||
<OssFileUploader v-model="r.url" v-model:name="r.fileName" :dir="`ry8080/meeting/${meetingId}/service/`" :placeholder="`点击上传 ${r.label}`" class="file-uploader" :readonly="isSponsor || isReadonly" />
|
||||
<span v-if="materialAmount(r.subType) > 0" class="invoice-amount">¥ {{ materialAmount(r.subType).toFixed(2) }}</span>
|
||||
<div v-if="r.hint" class="file-hint">{{ r.hint }}</div>
|
||||
</div>
|
||||
@@ -250,7 +252,7 @@
|
||||
<div class="file-list">
|
||||
<div v-for="r in laborVoucherRows" :key="r.label" class="file-row">
|
||||
<span class="file-label">{{ r.label }}:</span>
|
||||
<OssFileUploader v-model="r.url" :dir="`ry8080/meeting/${meetingId}/labor-voucher/`" :placeholder="`点击上传 ${r.label}`" class="file-uploader" :readonly="!canUploadVoucher" />
|
||||
<OssFileUploader v-model="r.url" v-model:name="r.fileName" :dir="`ry8080/meeting/${meetingId}/labor-voucher/`" :placeholder="`点击上传 ${r.label}`" class="file-uploader" :readonly="!canUploadVoucher" />
|
||||
<div v-if="r.hint" class="file-hint">{{ r.hint }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -259,7 +261,7 @@
|
||||
<div class="file-list">
|
||||
<div v-for="r in serviceVoucherRows" :key="r.label" class="file-row">
|
||||
<span class="file-label">{{ r.label }}:</span>
|
||||
<OssFileUploader v-model="r.url" :dir="`ry8080/meeting/${meetingId}/service-voucher/`" :placeholder="`点击上传 ${r.label}`" class="file-uploader" :readonly="!canUploadVoucher" />
|
||||
<OssFileUploader v-model="r.url" v-model:name="r.fileName" :dir="`ry8080/meeting/${meetingId}/service-voucher/`" :placeholder="`点击上传 ${r.label}`" class="file-uploader" :readonly="!canUploadVoucher" />
|
||||
<div v-if="r.hint" class="file-hint">{{ r.hint }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -275,7 +277,7 @@
|
||||
<!-- 结算 / 完结 (合规/管理员 手动点击) -->
|
||||
<el-button v-if="canSettle && !isReadonly" type="success" :loading="busy.settle" @click="onSettle">结算</el-button>
|
||||
<el-button v-if="canFinish && !isReadonly" type="primary" :loading="busy.finish" @click="onFinish">完结</el-button>
|
||||
<el-button v-if="!isSponsor && !isReadonly" type="primary" :loading="saving" @click="onSave">保存</el-button>
|
||||
<el-button v-if="!isSponsor && !isReadonly && materialsEditable" type="primary" :loading="saving" @click="onSave">保存</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -314,7 +316,7 @@
|
||||
<div v-else class="timeline-desc pending-text">待合规审核</div>
|
||||
<div v-if="round.compliance?.opinion" class="timeline-opinion">💬 {{ round.compliance.opinion }}</div>
|
||||
</div>
|
||||
<div :class="['timeline-item', partStatus(round.supervision)]">
|
||||
<div v-if="!(round.compliance?.auditResult === 'REJECTED')" :class="['timeline-item', partStatus(round.supervision)]">
|
||||
<div class="timeline-title">监察意见</div>
|
||||
<div v-if="round.supervision" class="timeline-meta">
|
||||
<span>{{ round.supervision.auditor }}</span>
|
||||
@@ -388,7 +390,7 @@
|
||||
<el-form-item label="身份证号">
|
||||
<el-input v-model="attendeeDialog.form.idCard" placeholder="18位身份证号" maxlength="18" />
|
||||
</el-form-item>
|
||||
<el-form-item label="身份证附件">
|
||||
<el-form-item label="身份证附件" style="grid-column: 1 / -1">
|
||||
<IdCardUploader v-model="attendeeDialog.form.idCardAttachments" :dir="`ry8080/meeting/${meetingId}/idcard/`" />
|
||||
</el-form-item>
|
||||
<el-form-item label="科室">
|
||||
@@ -416,7 +418,7 @@
|
||||
<el-form-item label="银行详细地址">
|
||||
<el-input v-model="attendeeDialog.form.bankAddress" placeholder="银行详细地址" />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色">
|
||||
<el-form-item label="角色" style="grid-column: 1 / -1">
|
||||
<project-role-multi-select v-model="attendeeDialog.form.laborForm" :roles="projectRoles" />
|
||||
</el-form-item>
|
||||
<el-form-item label="应发金额">
|
||||
@@ -497,6 +499,7 @@
|
||||
<div v-else class="text-muted">尚未生成海报</div>
|
||||
</el-dialog>
|
||||
<ImportResultDialog v-model="importResultOpen" :result="importResult" />
|
||||
<Preview v-model="previewOpen" :urls="previewUrls" :title="previewTitle" />
|
||||
|
||||
<div class="form-actions">
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
@@ -517,6 +520,7 @@ import CameraQrUpload from '@/components/CameraQrUpload.vue'
|
||||
import OssImageUploader from '@/components/OssImageUploader.vue'
|
||||
import IdCardUploader from '@/components/IdCardUploader.vue'
|
||||
import ImportResultDialog from '@/components/ImportResultDialog.vue'
|
||||
import Preview from '@/components/Preview.vue'
|
||||
import DoctorTitleSelect from '@/components/DoctorTitleSelect.vue'
|
||||
import DoctorDeptSelect from '@/components/DoctorDeptSelect.vue'
|
||||
import ProjectRoleMultiSelect from '@/components/ProjectRoleMultiSelect.vue'
|
||||
@@ -543,6 +547,10 @@ const genPosterVisible = ref(false)
|
||||
const genForm = reactive({ addText: false, textColor: '#FFFFFF' })
|
||||
/** 海报预览 dialog 显隐 */
|
||||
const posterPreviewVisible = ref(false)
|
||||
/** 通用附件预览 (身份证多图 / 劳务协议 PDF), 复用 components/Preview.vue */
|
||||
const previewOpen = ref(false)
|
||||
const previewUrls = ref([])
|
||||
const previewTitle = ref('附件预览')
|
||||
const loading = ref(false)
|
||||
const activeTab = ref('labor')
|
||||
const saving = ref(false)
|
||||
@@ -608,6 +616,10 @@ function protocolUrl(row) {
|
||||
if (isSponsor.value) return row.laborProtocolMasked || ''
|
||||
return row.laborProtocol || ''
|
||||
}
|
||||
/** 身份证附件 CSV "frontUrl,backUrl" 拆成非空数组 (正反面) */
|
||||
function idCardFiles(row) {
|
||||
return (row.idCardAttachments || '').split(',').map(s => s.trim()).filter(Boolean)
|
||||
}
|
||||
|
||||
// ===================== 材料管理 4 个 tab =====================
|
||||
const ROW_CONFIG = [
|
||||
@@ -720,6 +732,8 @@ const frozen = computed(() => isOne(row.value.isFrozen))
|
||||
// 材料可提交: 已执行 + 未冻结 + material ∈ {NOT_SUBMITTED, REJECTED}
|
||||
const canSubmitMaterial = computed(() => isExecutor.value && executed.value && !frozen.value
|
||||
&& ['NOT_SUBMITTED', 'REJECTED'].includes(row.value.materialAuditStage))
|
||||
// 材料可编辑 (保存修改): 未提交 / 被驳回 时; 提交审核后 (SUBMITTED) / 通过后 (APPROVED) 锁定, 隐藏保存按钮
|
||||
const materialsEditable = computed(() => ['NOT_SUBMITTED', 'REJECTED'].includes(row.value.materialAuditStage))
|
||||
function canComplianceAudit() {
|
||||
if (!isManager.value) return false
|
||||
return row.value.materialAuditStage === 'SUBMITTED' && !isOne(row.value.materialComplianceApproved)
|
||||
@@ -1501,6 +1515,13 @@ function openPosterPreview() {
|
||||
posterPreviewVisible.value = true
|
||||
}
|
||||
|
||||
/** 通用附件预览: urls 数组 → Preview 组件 (多图走画廊, 单 PDF 走 iframe) */
|
||||
function openPreview(urls, title) {
|
||||
previewUrls.value = Array.isArray(urls) ? urls : [urls]
|
||||
previewTitle.value = title || '附件预览'
|
||||
previewOpen.value = true
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
if (saving.value) return
|
||||
saving.value = true
|
||||
@@ -1803,7 +1824,7 @@ onBeforeUnmount(stopFeePolling)
|
||||
.info-value.fee-val { font-weight: 600; color: #f5222d; }
|
||||
.tag-list { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||
|
||||
.cols-row { display: grid; grid-template-columns: minmax(0, 7fr) minmax(0, 3fr); gap: 16px; align-items: flex-start; }
|
||||
.cols-row { display: grid; grid-template-columns: minmax(0, 8fr) minmax(0, 2fr); gap: 16px; align-items: flex-start; }
|
||||
/*
|
||||
* 防止页面横向溢出 (宽度超出屏幕) 的关键:
|
||||
* 1. grid 轨道用 minmax(0, Xfr) 而非 Xfr — 裸 Xfr = minmax(auto, Xfr), auto 最小 = 内容 min-content.
|
||||
|
||||
@@ -51,9 +51,10 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ref, reactive, computed, onMounted, onBeforeUnmount, watchEffect } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { bizAdd, bizGet, bizUpdate } from '@/api/public'
|
||||
import { bizAdd, bizGet, bizUpdate, getProjectAssignedSessions } from '@/api/public'
|
||||
import { setPageTitle, clearPageTitle } from '@/utils/pageTitle'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -82,6 +83,10 @@ const mode = computed(() => {
|
||||
})
|
||||
const pageTitle = computed(() => ({ new: '新建会议', edit: '修改会议', copy: '复制会议' }[mode.value]))
|
||||
|
||||
// 同步标题到 navbar 顶栏 (route.meta 是 shallowRef 不响应嵌套改动, 改用共享 pageTitle util)
|
||||
watchEffect(() => { setPageTitle(pageTitle.value) })
|
||||
onBeforeUnmount(clearPageTitle)
|
||||
|
||||
const formRef = ref(null)
|
||||
const submitting = ref(false)
|
||||
|
||||
@@ -121,6 +126,14 @@ async function loadProjectSnapshot(pid) {
|
||||
snap.projectName = d.projectName || ''
|
||||
snap.totalSessions = d.totalSessions ?? ''
|
||||
snap.sponsorOrgName = d.sponsorOrgName || ''
|
||||
// executor: "总场次" = 分配给本执行方(公司)的场次, 不是项目总场次
|
||||
if (roleSegment.value === 'executor') {
|
||||
try {
|
||||
const ar = await getProjectAssignedSessions(pid)
|
||||
const ad = (ar && ar.data) || {}
|
||||
if (ad.assignedSessions != null) snap.totalSessions = ad.assignedSessions
|
||||
} catch (e) { /* 静默, 不阻塞主流程 */ }
|
||||
}
|
||||
} catch (e) { /* 静默, 不阻塞主流程 */ }
|
||||
}
|
||||
|
||||
@@ -151,6 +164,21 @@ async function loadMeeting(mid) {
|
||||
// ========== 保存 ==========
|
||||
async function onSave() {
|
||||
try { await formRef.value.validate() } catch (e) { return }
|
||||
// 会议开始时间不能晚于会议结束时间 (通用校验)
|
||||
if (form.startTime && form.endTime
|
||||
&& new Date(form.startTime.replace(' ', 'T')) > new Date(form.endTime.replace(' ', 'T'))) {
|
||||
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
|
||||
}
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
const payload = {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<el-form-item label="项目编号"><el-input v-model="q.projectNo" placeholder="请输入项目编号" clearable style="width:140px" /></el-form-item>
|
||||
<el-form-item label="会议ID"><el-input v-model="q.meetingId" placeholder="请输入会议ID" clearable style="width:140px" /></el-form-item>
|
||||
<el-form-item label="会议名称"><el-input v-model="q.meetingName" placeholder="请输入会议名称" clearable style="width:160px" /></el-form-item>
|
||||
<el-form-item label="期数(第几期)"><el-input v-model="q.periodNo" placeholder="请输入期数" clearable style="width:140px" /></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" />
|
||||
<span class="date-sep">至</span>
|
||||
@@ -69,10 +69,10 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="500" fixed="right">
|
||||
<el-table-column label="操作" width="500" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="viewOnly(row)">查看</el-button>
|
||||
<el-button v-if="isRole('admin', 'manager')" link type="primary" @click="viewDetail(row)">上传材料</el-button>
|
||||
<el-button v-if="isRole('admin', 'manager')" link type="primary" @click="viewDetail(row)">编辑材料</el-button>
|
||||
<el-button v-if="isRole('admin', 'manager')" link type="primary" @click="onEdit(row)">修改</el-button>
|
||||
<el-button v-if="isRole('admin', 'manager')" link type="primary" @click="onCopy(row)">复制</el-button>
|
||||
<el-button v-if="isRole('admin', 'manager') && canSettleRow(row)" link type="success" @click="onSettle(row)">结算</el-button>
|
||||
@@ -150,7 +150,7 @@ const newRouteName = computed(() => isAdmin.value ? 'admin-meetings-new' : 'mana
|
||||
|
||||
// ========== 筛选 (按实际 8 项: 项目编号/会议ID/会议名称/第?期/会议时间/项目形式/当前阶段/备注) ==========
|
||||
const q = ref({
|
||||
projectNo: '', meetingId: '', meetingName: '', periodNo: '',
|
||||
projectNo: '', meetingId: '', meetingName: '', periodNo: null,
|
||||
projectForm: '', currentStage: '', remark: '',
|
||||
startTime: '', endTime: ''
|
||||
})
|
||||
@@ -184,7 +184,7 @@ async function load() {
|
||||
}
|
||||
function reset() {
|
||||
q.value = {
|
||||
projectNo:'', meetingId:'', meetingName:'', periodNo:'',
|
||||
projectNo:'', meetingId:'', meetingName:'', periodNo:null,
|
||||
projectForm:'', currentStage:'', remark:'',
|
||||
startTime:'', endTime:''
|
||||
}
|
||||
|
||||
@@ -46,52 +46,7 @@
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="footer-main">
|
||||
<div class="footer-brand">
|
||||
<div class="brand-row">
|
||||
<div class="footer-logo-icon"><img src="/logo.png" alt="logo" /></div>
|
||||
<div>
|
||||
<div class="footer-brand-name">北京整合医学学会</div>
|
||||
<div class="footer-brand-en">Beijing Association of Holistic Integrative Medicine</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="footer-desc">依托"健康中国2030"战略,整合医学资源,推动健康科普与公益事业,助力全民健康素养提升,共建共享健康中国。</p>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
<h4>快速导航</h4>
|
||||
<a @click.prevent="goHome">首页</a>
|
||||
<a @click.prevent="goHome">年度项目规划</a>
|
||||
<a @click.prevent="goPublicity">项目公示</a>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
<h4>学会项目</h4>
|
||||
<a>百姓巡常行</a>
|
||||
<a>专病联盟暨全国专家智库</a>
|
||||
<a>乡村幸福安康</a>
|
||||
<a>临床科研资助计划</a>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
<h4>联系方式</h4>
|
||||
<p>电话: 010-82089470</p>
|
||||
<p>邮箱: contactus@bahim.org.cn</p>
|
||||
<p>地址: 北京市海淀区知春路1号学院国际大厦908-2室</p>
|
||||
<p>邮编: 100083</p>
|
||||
</div>
|
||||
<div class="footer-qr">
|
||||
<div class="qr-image">
|
||||
<img src="/qrcode_1.png" alt="公众号二维码" style="width:100%;height:100%;object-fit:contain;" />
|
||||
</div>
|
||||
<div class="qr-label">公众号二维码</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<span>© 北京整合医学学会 BAHIM</span>
|
||||
<span>京ICP备2020035479号-1 京公网安备11010802034820</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<PortalFooter />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -101,6 +56,7 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { logout as logoutApi } from '@/api/auth'
|
||||
import request from '@/utils/request'
|
||||
import PortalFooter from '@/components/PortalFooter.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -311,58 +267,6 @@ a { color: inherit; text-decoration: none; }
|
||||
padding: 60px 0;
|
||||
}
|
||||
|
||||
/* ========== 底部 ========== */
|
||||
.footer {
|
||||
background: var(--brand-primary-darker);
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
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 {
|
||||
width: 36px; height: 36px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
overflow: hidden; flex-shrink: 0;
|
||||
}
|
||||
.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-col h4 {
|
||||
font-size: 14px; font-weight: 600; color: #fff;
|
||||
letter-spacing: 1px; margin-bottom: 16px; padding-bottom: 10px;
|
||||
position: relative;
|
||||
}
|
||||
.footer-col h4::after {
|
||||
content: ''; position: absolute; left: 0; bottom: 0;
|
||||
width: 24px; height: 2px; background: #93c5fd;
|
||||
}
|
||||
.footer-col a, .footer-col p {
|
||||
display: block; font-size: 13px; color: rgba(255, 255, 255, 0.6);
|
||||
line-height: 2.1; transition: color 0.2s;
|
||||
}
|
||||
.footer-col a { cursor: pointer; }
|
||||
.footer-col a:hover { color: #fff; }
|
||||
.footer-qr { text-align: center; }
|
||||
.qr-image {
|
||||
width: 100px; height: 100px;
|
||||
background: #fff;
|
||||
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; }
|
||||
.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);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.container { padding: 0 24px 40px; }
|
||||
.article-card { padding: 32px 24px; }
|
||||
|
||||
@@ -59,18 +59,6 @@
|
||||
</p>
|
||||
<div class="plan-image" v-html="planSvg"></div>
|
||||
</section>
|
||||
|
||||
<section class="notice-section">
|
||||
<div class="section-title">参与说明</div>
|
||||
<div class="notice-card">
|
||||
<ul class="notice-list">
|
||||
<li><strong>登录要求</strong> · 点击计划详情及项目提案,需先登录系统;</li>
|
||||
<li><strong>支持方注册</strong> · 注册后默认为管理员角色,可登录后新建监察员子账号;</li>
|
||||
<li><strong>支持方管理</strong> · 注册时选择的企业为主单位,默认进入该企业的人员管理页,由企业管理员(总负责人)进行管理,管理员账号可新建监察员账号;</li>
|
||||
<li><strong>执行方注册</strong> · 注册跳转至供应商系统,走入库流程,注册成功后该账号默认为管理员账号,管理员账号可新建执行人员账号。</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="specialty-section">
|
||||
@@ -91,52 +79,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="footer-main">
|
||||
<div class="footer-brand">
|
||||
<div class="brand-row">
|
||||
<div class="footer-logo-icon"><img src="/logo.png" alt="logo" /></div>
|
||||
<div>
|
||||
<div class="footer-brand-name">北京整合医学学会</div>
|
||||
<div class="footer-brand-en">Beijing Association of Holistic Integrative Medicine</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="footer-desc">依托"健康中国2030"战略,整合医学资源,推动健康科普与公益事业,助力全民健康素养提升,共建共享健康中国。</p>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
<h4>快速导航</h4>
|
||||
<a href="首页.html">首页</a>
|
||||
<a href="index2.html">年度项目规划</a>
|
||||
<a href="项目公示.html">项目公示</a>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
<h4>学会项目</h4>
|
||||
<a>百姓巡常行</a>
|
||||
<a>专病联盟暨全国专家智库</a>
|
||||
<a>乡村幸福安康</a>
|
||||
<a>临床科研资助计划</a>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
<h4>联系方式</h4>
|
||||
<p>电话: 010-82089470</p>
|
||||
<p>邮箱: contactus@bahim.org.cn</p>
|
||||
<p>地址: 北京市海淀区知春路1号学院国际大厦908-2室</p>
|
||||
<p>邮编: 100083</p>
|
||||
</div>
|
||||
<div class="footer-qr">
|
||||
<div class="qr-image">
|
||||
<img src="/qrcode_1.png" alt="公众号二维码" style="width:100%;height:100%;object-fit:contain;" />
|
||||
</div>
|
||||
<div class="qr-label">公众号二维码</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<span>© 北京整合医学学会 BAHIM</span>
|
||||
<span>京ICP备2020035479号-1 京公网安备11010802034820</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<PortalFooter />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -147,6 +90,7 @@ import { ElMessage } from 'element-plus'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { logout as logoutApi } from '@/api/auth'
|
||||
import request from '@/utils/request'
|
||||
import PortalFooter from '@/components/PortalFooter.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
@@ -660,157 +604,4 @@ a { color: inherit; text-decoration: none; }
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* ========== 提示 ========== */
|
||||
.notice-section {
|
||||
padding: 30px 0 50px;
|
||||
}
|
||||
|
||||
.notice-card {
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
padding: 20px 24px;
|
||||
}
|
||||
|
||||
.notice-list {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.notice-list li {
|
||||
padding: 8px 0;
|
||||
font-size: 13px;
|
||||
color: #4b5563;
|
||||
line-height: 1.7;
|
||||
position: relative;
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.notice-list li::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 18px;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background: var(--brand-primary);
|
||||
}
|
||||
|
||||
.notice-list li strong {
|
||||
color: var(--brand-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ========== 底部 ========== */
|
||||
.footer {
|
||||
background: var(--brand-primary-darker);
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
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 {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.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-col h4 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.footer-col h4::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 24px;
|
||||
height: 2px;
|
||||
background: #93c5fd;
|
||||
}
|
||||
|
||||
.footer-col a,
|
||||
.footer-col p {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
line-height: 2.1;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.footer-col a { cursor: pointer; }
|
||||
|
||||
.footer-col a:hover { color: #fff; }
|
||||
|
||||
.footer-qr {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.qr-image {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background: #fff;
|
||||
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;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -83,52 +83,7 @@
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="footer-main">
|
||||
<div class="footer-brand">
|
||||
<div class="brand-row">
|
||||
<div class="footer-logo-icon"><img src="/logo.png" alt="logo" /></div>
|
||||
<div>
|
||||
<div class="footer-brand-name">北京整合医学学会</div>
|
||||
<div class="footer-brand-en">Beijing Association of Holistic Integrative Medicine</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="footer-desc">依托"健康中国2030"战略,整合医学资源,推动健康科普与公益事业,助力全民健康素养提升,共建共享健康中国。</p>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
<h4>快速导航</h4>
|
||||
<a @click.prevent="goHome">首页</a>
|
||||
<a @click.prevent="goHome">年度项目规划</a>
|
||||
<span style="display: block; font-size: 13px; color: rgba(255,255,255,0.6);">项目公示</span>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
<h4>学会项目</h4>
|
||||
<a>百姓巡常行</a>
|
||||
<a>专病联盟暨全国专家智库</a>
|
||||
<a>乡村幸福安康</a>
|
||||
<a>临床科研资助计划</a>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
<h4>联系方式</h4>
|
||||
<p>电话: 010-82089470</p>
|
||||
<p>邮箱: contactus@bahim.org.cn</p>
|
||||
<p>地址: 北京市海淀区知春路1号学院国际大厦908-2室</p>
|
||||
<p>邮编: 100083</p>
|
||||
</div>
|
||||
<div class="footer-qr">
|
||||
<div class="qr-image">
|
||||
<img src="/qrcode_1.png" alt="公众号二维码" style="width:100%;height:100%;object-fit:contain;" />
|
||||
</div>
|
||||
<div class="qr-label">公众号二维码</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<span>© 北京整合医学学会 BAHIM</span>
|
||||
<span>京ICP备2020035479号-1 京公网安备11010802034820</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<PortalFooter />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -139,6 +94,7 @@ import { useUserStore } from '@/store/user'
|
||||
import { logout as logoutApi } from '@/api/auth'
|
||||
import request from '@/utils/request'
|
||||
import { bizList } from '@/api/public'
|
||||
import PortalFooter from '@/components/PortalFooter.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
@@ -163,26 +119,15 @@ async function load() {
|
||||
const res = await request({ url: '/business/public/announcements', method: 'get' })
|
||||
const list = res.data?.data || res.data || []
|
||||
allRows.value = list.map(r => {
|
||||
// ann_type 来自 announcement_type 字段; URL 来自邀请函/支持函/通知/日程/公示 URL
|
||||
const type = r.announcementType || '公示'
|
||||
// 根据 announcementType 选择对应 URL
|
||||
const urlMap = {
|
||||
invitation: r.invitationUrl,
|
||||
support: r.supportLetterUrl,
|
||||
supportLetter: r.supportLetterUrl,
|
||||
publish: r.publishUrl,
|
||||
notice: r.publishUrl,
|
||||
schedule: r.scheduleUrl
|
||||
}
|
||||
return {
|
||||
annId: r.projectId,
|
||||
date: r.startTime,
|
||||
endTime: r.endTime,
|
||||
type,
|
||||
type: '公示',
|
||||
projectNo: r.projectNo || '',
|
||||
projectName: r.projectName,
|
||||
title: r.projectName,
|
||||
fileUrl: urlMap[type] || r.invitationUrl || r.supportLetterUrl || r.publishUrl || ''
|
||||
fileUrl: r.invitationUrl || r.supportLetterUrl || r.publishUrl || r.scheduleUrl || ''
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
@@ -242,6 +187,7 @@ function nextPage() { if (currentPage.value < totalPages.value) { currentPage.va
|
||||
function goPage(p) { currentPage.value = p; window.scrollTo({ top: 0, behavior: 'smooth' }) }
|
||||
|
||||
function goHome() { router.push('/') }
|
||||
function goPublicity() { router.push('/publicity') }
|
||||
function goLogin() { router.push('/login') }
|
||||
function goUser() { /* placeholder */ }
|
||||
async function goLogout() { try { await logoutApi() } catch {}; userStore.logout(); router.replace('/login') }
|
||||
@@ -679,118 +625,4 @@ main.container {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
/* ========== 底部 ========== */
|
||||
.footer {
|
||||
background: var(--brand-primary-darker);
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
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 {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.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-col h4 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.footer-col h4::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 24px;
|
||||
height: 2px;
|
||||
background: #93c5fd;
|
||||
}
|
||||
|
||||
.footer-col a,
|
||||
.footer-col p {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
line-height: 2.1;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.footer-col a { cursor: pointer; }
|
||||
|
||||
.footer-col a:hover { color: #fff; }
|
||||
|
||||
.footer-qr {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.qr-image {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background: #fff;
|
||||
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;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -121,52 +121,7 @@
|
||||
</aside>
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="footer-main">
|
||||
<div class="footer-brand">
|
||||
<div class="brand-row">
|
||||
<div class="footer-logo-icon"><img src="/logo.png" alt="logo" /></div>
|
||||
<div>
|
||||
<div class="footer-brand-name">北京整合医学学会</div>
|
||||
<div class="footer-brand-en">Beijing Association of Holistic Integrative Medicine</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="footer-desc">依托"健康中国2030"战略,整合医学资源,推动健康科普与公益事业,助力全民健康素养提升,共建共享健康中国。</p>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
<h4>快速导航</h4>
|
||||
<a @click.prevent="goHome">首页</a>
|
||||
<a @click.prevent="goHome">年度项目规划</a>
|
||||
<a @click.prevent="goPublicity">项目公示</a>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
<h4>学会项目</h4>
|
||||
<a>百姓巡常行</a>
|
||||
<a>专病联盟暨全国专家智库</a>
|
||||
<a>乡村幸福安康</a>
|
||||
<a>临床科研资助计划</a>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
<h4>联系方式</h4>
|
||||
<p>电话: 010-82089470</p>
|
||||
<p>邮箱: contactus@bahim.org.cn</p>
|
||||
<p>地址: 北京市海淀区知春路1号学院国际大厦908-2室</p>
|
||||
<p>邮编: 100083</p>
|
||||
</div>
|
||||
<div class="footer-qr">
|
||||
<div class="qr-image">
|
||||
<img src="/qrcode_1.png" alt="公众号二维码" style="width:100%;height:100%;object-fit:contain;" />
|
||||
</div>
|
||||
<div class="qr-label">公众号二维码</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<span>© 北京整合医学学会 BAHIM</span>
|
||||
<span>京ICP备2020035479号-1 京公网安备11010802034820</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<PortalFooter />
|
||||
|
||||
<!-- 分享二维码弹窗 (el-dialog 自带右上角 X 关闭按钮 + Esc + 遮罩点击) -->
|
||||
<el-dialog v-model="showQr" title="分享本页" width="420px" align-center destroy-on-close>
|
||||
@@ -229,6 +184,7 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { logout as logoutApi } from '@/api/auth'
|
||||
import PortalFooter from '@/components/PortalFooter.vue'
|
||||
import { listBizPerson } from '@/api/business/person'
|
||||
import { bizGet } from '@/api/public'
|
||||
import {
|
||||
@@ -819,21 +775,6 @@ onBeforeUnmount(() => {
|
||||
.logo-title,
|
||||
.logo-subtitle,
|
||||
.top-tools,
|
||||
.footer-main,
|
||||
.footer-brand,
|
||||
.footer-col,
|
||||
.footer-col h4,
|
||||
.brand-row,
|
||||
.footer-desc,
|
||||
.footer-col a,
|
||||
.footer-col p,
|
||||
.footer-qr,
|
||||
.footer-logo,
|
||||
.qr-image,
|
||||
.qr-label,
|
||||
.footer-bottom,
|
||||
.footer-brand-name,
|
||||
.footer-brand-en,
|
||||
.action-bar,
|
||||
.modal,
|
||||
.modal-qr,
|
||||
@@ -1329,118 +1270,4 @@ a { color: inherit; text-decoration: none; }
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ========== 底部 ========== */
|
||||
.footer {
|
||||
background: var(--brand-primary-darker);
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
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 {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.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-col h4 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.footer-col h4::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 24px;
|
||||
height: 2px;
|
||||
background: #93c5fd;
|
||||
}
|
||||
|
||||
.footer-col a,
|
||||
.footer-col p {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
line-height: 2.1;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.footer-col a { cursor: pointer; }
|
||||
|
||||
.footer-col a:hover { color: #fff; }
|
||||
|
||||
.footer-qr {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.qr-image {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background: #fff;
|
||||
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;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -67,52 +67,7 @@
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="footer-main">
|
||||
<div class="footer-brand">
|
||||
<div class="brand-row">
|
||||
<div class="footer-logo-icon"><img src="/logo.png" alt="logo" /></div>
|
||||
<div>
|
||||
<div class="footer-brand-name">北京整合医学学会</div>
|
||||
<div class="footer-brand-en">Beijing Association of Holistic Integrative Medicine</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="footer-desc">依托"健康中国2030"战略,整合医学资源,推动健康科普与公益事业,助力全民健康素养提升,共建共享健康中国。</p>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
<h4>快速导航</h4>
|
||||
<a @click.prevent="goHome">首页</a>
|
||||
<a @click.prevent="goHome">年度项目规划</a>
|
||||
<a @click.prevent="goPublicity">项目公示</a>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
<h4>学会项目</h4>
|
||||
<a>百姓巡常行</a>
|
||||
<a>专病联盟暨全国专家智库</a>
|
||||
<a>乡村幸福安康</a>
|
||||
<a>临床科研资助计划</a>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
<h4>联系方式</h4>
|
||||
<p>电话: 010-82089470</p>
|
||||
<p>邮箱: contactus@bahim.org.cn</p>
|
||||
<p>地址: 北京市海淀区知春路1号学院国际大厦908-2室</p>
|
||||
<p>邮编: 100083</p>
|
||||
</div>
|
||||
<div class="footer-qr">
|
||||
<div class="qr-image">
|
||||
<img src="/qrcode_1.png" alt="公众号二维码" style="width:100%;height:100%;object-fit:contain;" />
|
||||
</div>
|
||||
<div class="qr-label">公众号二维码</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<span>© 北京整合医学学会 BAHIM</span>
|
||||
<span>京ICP备2020035479号-1 京公网安备11010802034820</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<PortalFooter />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -122,6 +77,7 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { logout as logoutApi } from '@/api/auth'
|
||||
import request from '@/utils/request'
|
||||
import PortalFooter from '@/components/PortalFooter.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -293,45 +249,6 @@ a { color: inherit; text-decoration: none; }
|
||||
.content-empty p { margin: 8px 0 0; }
|
||||
.content-empty .hint { font-size: 12px; }
|
||||
|
||||
/* ===== 底部 ===== */
|
||||
.footer { background: var(--brand-primary-darker); color: rgba(255,255,255,0.65); 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 {
|
||||
width: 36px; height: 36px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
overflow: hidden; flex-shrink: 0;
|
||||
}
|
||||
.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-col h4 {
|
||||
font-size: 14px; font-weight: 600; color: #fff;
|
||||
letter-spacing: 1px; margin-bottom: 16px; padding-bottom: 10px; position: relative;
|
||||
}
|
||||
.footer-col h4::after { content: ''; position: absolute; left: 0; bottom: 0; width: 24px; height: 2px; background: #93c5fd; }
|
||||
.footer-col a, .footer-col p {
|
||||
display: block; font-size: 13px; color: rgba(255,255,255,0.6);
|
||||
line-height: 2.1; transition: color 0.2s;
|
||||
}
|
||||
.footer-col a { cursor: pointer; }
|
||||
.footer-col a:hover { color: #fff; }
|
||||
.footer-qr { text-align: center; }
|
||||
.qr-image {
|
||||
width: 100px; height: 100px;
|
||||
background: #fff;
|
||||
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; }
|
||||
.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);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.container { padding: 0 24px 40px; }
|
||||
.plan-card { padding: 32px 24px; }
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="260" fixed="right">
|
||||
<el-table-column label="操作" width="340" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<!-- 查看 (只读详情): 两角色都有, 跳独立详情页 -->
|
||||
<el-button link type="primary" @click="goView(row)">查看</el-button>
|
||||
@@ -71,6 +71,8 @@
|
||||
<el-button link :type="isDisabled(row) ? 'success' : 'danger'" @click="onToggleStatus(row)">
|
||||
{{ isDisabled(row) ? '启用' : '禁用' }}
|
||||
</el-button>
|
||||
<!-- 重置密码: 重置为默认 123456 (与新建人员默认密码一致) -->
|
||||
<el-button v-if="row.userId" link type="primary" @click="onResetPwd(row)">重置密码</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -119,7 +121,7 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { bizList, bizUpdate, importPerson, downloadImportTemplate, changePersonAdmin } from '@/api/public'
|
||||
import { bizList, bizUpdate, importPerson, downloadImportTemplate, changePersonAdmin, resetPersonPassword } from '@/api/public'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Upload } from '@element-plus/icons-vue'
|
||||
import ImportResultDialog from '@/components/ImportResultDialog.vue'
|
||||
@@ -214,6 +216,23 @@ async function onChangeAdmin(row) {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 重置密码 ==========
|
||||
async function onResetPwd(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定重置「${row.name}」的登录密码为默认 123456 吗?`,
|
||||
'重置密码',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
} catch { return }
|
||||
try {
|
||||
await resetPersonPassword(row.personId)
|
||||
ElMessage.success('已重置为 123456')
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.msg || e?.message || '重置失败')
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 跳转 (按角色) ==========
|
||||
function goNew() {
|
||||
router.push({
|
||||
@@ -225,10 +244,22 @@ function goNew() {
|
||||
})
|
||||
}
|
||||
function goEdit(row) {
|
||||
router.push(listBasePath.value + '/edit/' + row.personId)
|
||||
router.push({
|
||||
path: listBasePath.value + '/edit/' + row.personId,
|
||||
query: {
|
||||
...(orgId.value ? { orgId: orgId.value } : {}),
|
||||
...(orgName.value ? { orgName: orgName.value } : {})
|
||||
}
|
||||
})
|
||||
}
|
||||
function goView(row) {
|
||||
router.push(listBasePath.value + '/view/' + row.personId)
|
||||
router.push({
|
||||
path: listBasePath.value + '/view/' + row.personId,
|
||||
query: {
|
||||
...(orgId.value ? { orgId: orgId.value } : {}),
|
||||
...(orgName.value ? { orgName: orgName.value } : {})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ========== 批量导入 ==========
|
||||
|
||||
@@ -63,6 +63,8 @@ const personId = route.params.id
|
||||
// 角色感知: admin/manager 都能跳到此页, 回跳路径按 route 区分
|
||||
const isAdmin = computed(() => route.path.startsWith('/admin/'))
|
||||
const listBasePath = computed(() => isAdmin.value ? '/admin/sponsor-people' : '/manager/sponsor-people')
|
||||
const orgId = computed(() => route.query.orgId ? Number(route.query.orgId) : null)
|
||||
const orgName = computed(() => route.query.orgName || '')
|
||||
|
||||
const loading = ref(false)
|
||||
const form = reactive({
|
||||
@@ -79,7 +81,13 @@ const form = reactive({
|
||||
})
|
||||
|
||||
function goBack() {
|
||||
router.push(listBasePath.value)
|
||||
router.push({
|
||||
path: listBasePath.value,
|
||||
query: {
|
||||
...(orgId.value ? { orgId: orgId.value } : {}),
|
||||
...(orgName.value ? { orgName: orgName.value } : {})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function loadDetail() {
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
|
||||
<el-card v-loading="loadingDetail" shadow="never" class="form-card nested">
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
|
||||
<el-form-item label="用户名" prop="userName">
|
||||
<el-input v-model="form.userName" placeholder="登录用户名" maxlength="50" :disabled="isEdit" />
|
||||
<el-form-item v-if="!isEdit" label="用户名" prop="userName">
|
||||
<el-input v-model="form.userName" placeholder="登录用户名" maxlength="50" />
|
||||
</el-form-item>
|
||||
<el-form-item label="姓名" prop="name">
|
||||
<el-input v-model="form.name" placeholder="姓名" maxlength="20" />
|
||||
@@ -82,7 +82,13 @@ const rules = {
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
router.push(listBasePath.value)
|
||||
router.push({
|
||||
path: listBasePath.value,
|
||||
query: {
|
||||
...(orgId.value ? { orgId: orgId.value } : {}),
|
||||
...(orgName.value ? { orgName: orgName.value } : {})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function loadDetail() {
|
||||
@@ -129,7 +135,6 @@ async function onSave() {
|
||||
// 从 org 页面跳来时 route.query 带 orgId, 直接作为 FK
|
||||
if (!isEdit && orgId.value) payload.orgId = orgId.value
|
||||
if (isEdit) {
|
||||
delete payload.personId
|
||||
await bizUpdate('person', payload)
|
||||
ElMessage.success('修改成功')
|
||||
} else {
|
||||
|
||||
@@ -13,9 +13,8 @@
|
||||
<el-form-item label="会议名称">
|
||||
<el-input v-model="q.meetingName" placeholder="会议名称" clearable style="width:180px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="第">
|
||||
<el-input v-model="q.periodNo" placeholder="期数" clearable style="width:80px" />
|
||||
<span style="margin:0 4px">期</span>
|
||||
<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="至"
|
||||
@@ -172,7 +171,7 @@ const list = ref([])
|
||||
const loading = ref(false)
|
||||
const selected = ref([])
|
||||
const q = reactive({
|
||||
projectNo: '', meetingId: '', meetingName: '', periodNo: '',
|
||||
projectNo: '', meetingId: '', meetingName: '', periodNo: null,
|
||||
startTimeRange: null, projectForm: '', currentStage: '', remark: ''
|
||||
})
|
||||
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
|
||||
@@ -216,7 +215,7 @@ async function loadList() {
|
||||
}
|
||||
|
||||
function reset() {
|
||||
Object.assign(q, { projectNo: '', meetingId: '', meetingName: '', periodNo: '', startTimeRange: null, projectForm: '', currentStage: '', remark: '' })
|
||||
Object.assign(q, { projectNo: '', meetingId: '', meetingName: '', periodNo: null, startTimeRange: null, projectForm: '', currentStage: '', remark: '' })
|
||||
page.pageNum = 1
|
||||
loadList()
|
||||
}
|
||||
|
||||
@@ -66,10 +66,12 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" width="170" />
|
||||
<el-table-column label="操作" width="240" fixed="right">
|
||||
<el-table-column label="操作" width="320" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="onView(row)">查看</el-button>
|
||||
<el-button link type="primary" @click="onEdit(row)">编辑</el-button>
|
||||
<!-- 重置密码: 重置为默认 123456 (与新建人员默认密码一致) -->
|
||||
<el-button v-if="row.userId" link type="primary" @click="onResetPwd(row)">重置密码</el-button>
|
||||
<!-- 本人不显示禁用/恢复按钮 (避免主账号把自己禁用导致登录不上) -->
|
||||
<el-button v-if="row.userId !== store.user?.userId" link :type="row.status === '0' ? 'warning' : 'success'" @click="onToggleStatus(row)">
|
||||
{{ row.status === '0' ? '禁用' : '恢复' }}
|
||||
@@ -137,7 +139,7 @@
|
||||
import { reactive, ref, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { bizUpdate, bizDelete } from '@/api/public'
|
||||
import { bizUpdate, bizDelete, resetPersonPassword } from '@/api/public'
|
||||
import { listSponsorPerson } from '@/api/business/person'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { accountTypeRoleLabel, accountTypeRoleTagType } from '@/utils/roleMap'
|
||||
@@ -193,6 +195,22 @@ function onEdit(row) {
|
||||
router.push({ path: `/sponsor/people/edit/${row.personId}` })
|
||||
}
|
||||
|
||||
async function onResetPwd(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定重置「${row.name}」的登录密码为默认 123456 吗?`,
|
||||
'重置密码',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
} catch { return }
|
||||
try {
|
||||
await resetPersonPassword(row.personId)
|
||||
ElMessage.success('已重置为 123456')
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.msg || e?.message || '重置失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function onToggleStatus(row) {
|
||||
const newStatus = row.status === '0' ? '1' : '0'
|
||||
const label = newStatus === '0' ? '恢复' : '禁用'
|
||||
|
||||
Reference in New Issue
Block a user