批量推送代码

This commit is contained in:
郭庆泰
2026-08-26 23:14:46 +08:00
parent 18f7e93d5b
commit 36528b6e0c
36 changed files with 933 additions and 779 deletions
+8 -114
View File
@@ -1,87 +1,6 @@
<template>
<div class="home-page">
<header class="top-nav" :class="topNavClass">
<a class="logo" title="返回首页" @click.prevent="onHome">
<div class="logo-icon"><img src="/logo.png" alt="logo" /></div>
<div class="logo-text">
<span class="logo-title">北京整合医学学会</span>
<span class="logo-subtitle">Beijing Association of Holistic Integrative Medicine</span>
</div>
</a>
<ul class="nav-list">
<li class="nav-item"><span class="nav-link active">年度项目规划</span></li>
<li class="nav-item"><a class="nav-link" @click.prevent="goPublicity">项目公示</a></li>
</ul>
<div class="top-tools">
<template v-if="!loggedIn">
<span class="login-btn" @click="goLogin">登录</span>
</template>
<template v-else>
<el-dropdown trigger="click" @command="onUserCmd">
<a class="user-link" @click.prevent>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
</svg>
<span>{{ userName }}</span>
</a>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="account">我的主页</el-dropdown-item>
<el-dropdown-item command="logout" divided>退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</div>
<!-- 手机端汉堡按钮 (桌面隐藏) -->
<button class="hamburger" type="button" aria-label="打开菜单" @click="drawerOpen = true">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<line x1="3" y1="6" x2="21" y2="6"/>
<line x1="3" y1="12" x2="21" y2="12"/>
<line x1="3" y1="18" x2="21" y2="18"/>
</svg>
</button>
</header>
<!-- 手机端抽屉菜单 (桌面隐藏) -->
<transition name="drawer-fade">
<div v-if="drawerOpen" class="drawer-mask" @click="drawerOpen = false"></div>
</transition>
<transition name="drawer-slide">
<aside v-if="drawerOpen" class="drawer-panel" role="dialog" aria-label="导航菜单">
<div class="drawer-header">
<span class="drawer-title">导航菜单</span>
<button class="drawer-close" type="button" aria-label="关闭菜单" @click="drawerOpen = false">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<line x1="6" y1="6" x2="18" y2="18"/>
<line x1="6" y1="18" x2="18" y2="6"/>
</svg>
</button>
</div>
<nav class="drawer-nav">
<a class="drawer-link" @click.prevent="goHomeAndClose">年度项目规划</a>
<a class="drawer-link" @click.prevent="goPublicityAndClose">项目公示</a>
</nav>
<div class="drawer-divider"></div>
<div class="drawer-tools">
<template v-if="!loggedIn">
<span class="drawer-login" @click="goLoginAndClose">登录</span>
</template>
<template v-else>
<div class="drawer-user">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
</svg>
<span>{{ userName }}</span>
</div>
<a class="drawer-link" @click.prevent="goAccountAndClose">我的主页</a>
<a class="drawer-link danger" @click.prevent="goLogoutAndClose">退出登录</a>
</template>
</div>
</aside>
</transition>
<PortalNavbar />
<div class="container">
<section class="hero">
@@ -135,16 +54,14 @@ import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
import { useRouter } from 'vue-router'
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'
import PortalNavbar from '@/components/PortalNavbar.vue'
const router = useRouter()
const userStore = useUserStore()
const isScrolled = ref(false)
const specialPlans = ref([])
const drawerOpen = ref(false)
async function loadSpecialPlans() {
try {
@@ -156,11 +73,15 @@ async function loadSpecialPlans() {
}
function openPlan(id) {
// 点击计划详情需登录: 未登录先跳登录, 登录后回跳详情
if (!loggedIn.value) {
ElMessage.warning('请先登录系统')
router.push({ path: '/login', query: { redirect: `/special-plan/${id}` } })
return
}
window.open(`${import.meta.env.BASE_URL}#/special-plan/${id}`, '_blank')
}
const topNavClass = computed(() => isScrolled.value ? 'is-scrolled' : '')
const loggedIn = computed(() => !!userStore.token)
const userName = computed(() => userStore.user?.userName || '用户')
// 投稿角色: admin/manager 不开放投稿 → 隐藏「项目提案」按钮; 未登录 user 为 null → 显示(点击引导登录)
const canPropose = computed(() => {
const r = userStore.user?.role || ''
@@ -224,42 +145,15 @@ const planSvg = `<svg viewBox="0 150 1200 370" xmlns="http://www.w3.org/2000/svg
</g>
</svg>`
function handleScroll() {
isScrolled.value = window.scrollY > 10
}
onMounted(() => {
window.addEventListener('scroll', handleScroll, { passive: true })
loadSpecialPlans()
// portal home 移动端适配: 解除 body min-width (避免手机端 1354px 横向滚动)
document.body.classList.add('home-page-body')
})
onBeforeUnmount(() => {
window.removeEventListener('scroll', handleScroll)
document.body.classList.remove('home-page-body')
})
function onHome() { isScrolled.value = false }
function goLogin() { router.push('/login') }
function goUser() { /* placeholder */ }
async function goLogout() { try { await logoutApi() } catch {}; userStore.logout(); router.replace('/login') }
// 抽屉版导航: 跳转后关闭抽屉
function goHomeAndClose() { drawerOpen.value = false; onHome() }
function goPublicityAndClose() { drawerOpen.value = false; goPublicity() }
function goLoginAndClose() { drawerOpen.value = false; goLogin() }
function goAccountAndClose() { drawerOpen.value = false; onUserCmd('account') }
async function goLogoutAndClose() { drawerOpen.value = false; await goLogout() }
async function onUserCmd(cmd) {
if (cmd === 'logout') return goLogout()
if (cmd === 'account') {
const r = (userStore.user?.role || '')
const map = { admin: '/admin/workbench', manager: '/manager/workbench', doctor: '/doctor/home', executor: '/executor/meetings', sponsor: '/sponsor/home' }
router.push(map[r] || '/admin/workbench')
}
}
function goPublicity() { router.push('/publicity') }
function onSubmit() {
if (!loggedIn.value) {
ElMessage.warning('请先登录系统')
+3 -60
View File
@@ -1,40 +1,6 @@
<template>
<div class="pub-page">
<header class="top-nav" :class="topNavClass">
<a class="logo" title="返回首页" @click.prevent="goHome">
<div class="logo-icon"><img src="/logo.png" alt="logo" /></div>
<div class="logo-text">
<span class="logo-title">北京整合医学学会</span>
<span class="logo-subtitle">Beijing Association of Holistic Integrative Medicine</span>
</div>
</a>
<ul class="nav-list">
<li class="nav-item"><a class="nav-link" @click.prevent="goHome">年度项目规划</a></li>
<li class="nav-item"><span class="nav-link active">项目公示</span></li>
</ul>
<div class="top-tools">
<template v-if="!loggedIn">
<span class="login-btn" @click="goLogin">登录</span>
</template>
<template v-else>
<el-dropdown trigger="click" @command="onUserCmd">
<a class="user-link" @click.prevent>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
</svg>
<span>{{ userName }}</span>
</a>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="account">我的主页</el-dropdown-item>
<el-dropdown-item command="logout" divided>退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</div>
</header>
<PortalNavbar />
<main class="container">
<div class="page-header">
@@ -106,23 +72,16 @@
</template>
<script setup>
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
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'
import PortalNavbar from '@/components/PortalNavbar.vue'
const router = useRouter()
const userStore = useUserStore()
const PAGE_SIZE = 10
const isScrolled = ref(false)
const topNavClass = computed(() => isScrolled.value ? 'is-scrolled' : '')
const loggedIn = computed(() => !!userStore.token)
const userName = computed(() => userStore.user?.userName || '用户')
const ALL_NOTICES = []
const currentKeyword = ref('')
@@ -203,19 +162,6 @@ function prevPage() { if (currentPage.value > 1) { currentPage.value--; window.s
function nextPage() { if (currentPage.value < totalPages.value) { currentPage.value++; window.scrollTo({ top: 0, behavior: 'smooth' }) } }
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') }
async function onUserCmd(cmd) {
if (cmd === 'logout') return goLogout()
if (cmd === 'account') {
const r = (userStore.user?.role || '')
const map = { admin: '/admin/workbench', manager: '/manager/workbench', doctor: '/doctor/home', executor: '/executor/meetings', sponsor: '/sponsor/home' }
router.push(map[r] || '/admin/workbench')
}
}
function goDetail(n) {
if (!n || !n.annId) return
router.push({ name: 'publicity-detail', params: { projectId: n.annId } })
@@ -229,9 +175,6 @@ function fmtDate(d) {
const day = String(date.getDate()).padStart(2, '0')
return `${y}-${m}-${day}`
}
function handleScroll() { isScrolled.value = window.scrollY > 10 }
onMounted(() => window.addEventListener('scroll', handleScroll, { passive: true }))
onBeforeUnmount(() => window.removeEventListener('scroll', handleScroll))
</script>
<style scoped>
+2 -54
View File
@@ -1,40 +1,6 @@
<template>
<div class="detail-page">
<header class="top-nav" :class="topNavClass">
<a class="logo" title="返回首页" @click.prevent="goHome">
<div class="logo-icon"><img src="/logo.png" alt="logo" /></div>
<div class="logo-text">
<span class="logo-title">北京整合医学学会</span>
<span class="logo-subtitle">Beijing Association of Holistic Integrative Medicine</span>
</div>
</a>
<ul class="nav-list">
<li class="nav-item"><a class="nav-link" @click.prevent="goHome">年度项目规划</a></li>
<li class="nav-item"><a class="nav-link active" @click.prevent="goPublicity">项目公示</a></li>
</ul>
<div class="top-tools">
<template v-if="!loggedIn">
<span class="login-btn" @click="goLogin">登录</span>
</template>
<template v-else>
<el-dropdown trigger="click" @command="onUserCmd">
<a class="user-link" @click.prevent>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
</svg>
<span>{{ userName }}</span>
</a>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="account">我的主页</el-dropdown-item>
<el-dropdown-item command="logout" divided>退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</div>
</header>
<PortalNavbar />
<main class="container">
<a class="back-link" @click.prevent="goPublicity">
@@ -183,8 +149,8 @@ import { ref, computed, watch, reactive, onMounted, onBeforeUnmount } from 'vue'
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 PortalNavbar from '@/components/PortalNavbar.vue'
import { listBizPerson } from '@/api/business/person'
import { bizGet } from '@/api/public'
import {
@@ -199,10 +165,7 @@ const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
const isScrolled = ref(false)
const topNavClass = computed(() => isScrolled.value ? 'is-scrolled' : '')
const loggedIn = computed(() => !!userStore.token)
const userName = computed(() => userStore.user?.userName || '用户')
// 意向按钮按角色显隐:
// sponsor (含 MAIN/SUB) → 仅支持意向; executor (含 MAIN/SUB) → 仅执行意向
// admin / manager / doctor → 两个都隐藏
@@ -359,19 +322,7 @@ async function load() {
}
}
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') }
async function onUserCmd(cmd) {
if (cmd === 'logout') return goLogout()
if (cmd === 'account') {
const r = (userStore.user?.role || '')
const map = { admin: '/admin/workbench', manager: '/manager/workbench', doctor: '/doctor/home', executor: '/executor/meetings', sponsor: '/sponsor/home' }
router.push(map[r] || '/admin/workbench')
}
}
const signed = ref(false)
async function checkSigned() {
@@ -572,7 +523,6 @@ function onViewBidAnnouncement() {
}
function onKeydown(e) { if (e.key === 'Escape') showQr.value = false }
function handleScroll() { isScrolled.value = window.scrollY > 10 }
// 下载二维码: qrUrl 是 QRCode.toDataURL 生成的 data:image/png;base64,... 直接 <a download> 即可
function onDownloadQr() {
@@ -586,12 +536,10 @@ function onDownloadQr() {
}
onMounted(() => {
window.addEventListener('scroll', handleScroll, { passive: true })
window.addEventListener('keydown', onKeydown)
load()
})
onBeforeUnmount(() => {
window.removeEventListener('scroll', handleScroll)
window.removeEventListener('keydown', onKeydown)
})
</script>
@@ -147,6 +147,11 @@ async function onUserCmd(cmd) {
function handleScroll() { isScrolled.value = window.scrollY > 10 }
onMounted(() => {
// 计划详情需登录 (点击验证登录状态): 未登录跳登录, 登录后回跳 (兜底拦截直链访问)
if (!loggedIn.value) {
router.replace({ path: '/login', query: { redirect: route.fullPath } })
return
}
window.addEventListener('scroll', handleScroll, { passive: true })
load()
})