尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于Vue3的科研成果管理系统开发实践:从设计到部署

基于Vue3的科研成果管理系统开发实践:从设计到部署 简介基于Vue框架的科研成果管理系统设计源码专注于高校与科研机构的成果信息化管理面向教师、学生及科研管理员三种角色覆盖科研成果登记、审核、查询与统计等典型业务场景。系统将纵向课题、横向课题、论文、专利、软件著作权等科研成果纳入统一分类支持增删改查、按时间与类别筛选并提供Excel批量导入导出大幅简化日常成果登记与统计工作。源码包共167个文件、2.3MB包含44个Java源文件、42个Vue组件、25个JavaScript脚本以及用于项目配置的XML与YML、用于说明文档的Markdown、用于界面表现的CSS等辅助文件前后端分层明确兼顾业务逻辑与界面展示。已有420人学习下载可作为Java后端配合Vue前端的完整实战范例用于理解角色权限控制、成果数据建模、检索过滤和数据交换等关键实现也适合作为课程设计或毕业设计的参考原型。1. Vue框架下科研成果管理系统设计源码的落地思路在高校和科研院所里科研成果管理往往是“表多人少”申报、审核、汇总、统计都靠手工填Excel流程卡在一个领导那里就停滞。基于Vue框架来重做这套前端不是简单换皮肤而是把录入、校验、状态流转、权限控制这些常见场景变成可复用组件和清晰的数据流。这类系统的“设计源码”通常包括三部分前端工程结构、核心业务组件、接口与状态管理。这里从一套常见的Vue 3 Vite Pinia Ant Design Vue组合切入讲清楚成果管理系统的数据建模、权限路由、表单拆分、文件上传和部署配置。对要接手这类项目、或准备自己搭一套完整前端的工程师可以直接照着复制改造。先想清楚一个问题科研成果管理系统的复杂度不在界面而在状态。一条申报从“草稿”到“已立项”要经历多级审核不同角色看到的字段和按钮都不一样所以后面的方案都围绕“状态驱动界面”展开。2. Vue项目初始化与科研成果数据模型设计2.1 用Vite初始化项目并安装核心依赖我一般先用 Vite 创建 Vue 3 项目而不是 vue-cli因为 Vite 开发服务器启动快生产构建也默认带上代码分割。执行下面的命令npm create vitelatest research-system -- --template vue cd research-system npm install npm install vue-router4 pinia axios ant-design-vue echarts参数说明create vitelatest会拉取最新模板--template vue指定 Vue 3 单页应用模板。vue-router4对应 Vue 3 的官方路由pinia是官方推荐的状态管理库ant-design-vue提供表格、表单、上传这类现成组件echarts用于成果统计图表。如果团队已经用了 Element Plus也可以替换但下面代码里的 Upload、Form 组件名也相应调整。安装完成后删除src/components/HelloWorld.vue和默认样式按功能模块建目录。常见的科研管理系统目录如下src/ api/ # 接口请求定义 auth.js achievement.js upload.js assets/ components/ common/ # 通用组件 layout/ # 布局结构 router/ index.js stores/ user.js achievement.js views/ login/ dashboard/ achievement/ list.vue create.vue audit/ utils/ request.js auth.js这里把views按业务角色拆成申报、审核、统计而不是按页面类型拆因为后续权限控制和路由懒加载会简单很多。api目录单独放接口组件里不直接出现axios方便统一处理错误码和 token。2.2 科研成果实体的字段设计与前端类型定义成果类型一般包括论文、专利、软著作权、获奖、横向项目。虽然数据库表设计由后端负责但前端必须先把字段结构定清楚否则表单和列表会反复返工。常见做法是先定义一个统一的状态字段status用数字表示流转阶段。下面这一组字段对大多数高校场景够用字段名类型说明idnumber成果唯一标识titlestring成果名称列表页主标题typestring成果类型如“论文”“专利”ownerstring申报人姓名departmentstring所属学院或部门statusnumber0草稿1待审核2审核中3已立项4已驳回file_urlstring附件链接submit_timedatetime提交时间在src/api/achievement.js里用 JSDoc 注释把这个结构写清楚组件里引用时不会跑偏/** * typedef {Object} Achievement * property {number} id * property {string} title * property {论文|专利|软著|获奖} type * property {string} owner * property {string} department * property {0|1|2|3|4} status * property {string} file_url * property {string} submit_time */状态为什么用数字而不是字符串因为审核流程里的排序、筛选和徽标颜色都依赖大小比较数字状态映射到文案和颜色的操作放一个函数里集中管理。后面在列表页会用到。2.3 Pinia 状态管理为什么不用 Vuex老项目用 Vuex 写起来会有大量mutation样板代码科研管理系统因为要频繁更新“当前用户”“待办数量”“筛选条件”用 Pinia 的setup store更直接。对比一下维度Vuex 4Pinia类型推导需要额外模块类型声明原生 TS 友好mutation必须不需要模块嵌套actions 常写成多层按 store 文件拆分调试依赖严格模式默认支持 devtools用户 store 只需要保存 token、用户信息和权限标识登录后从接口拿刷新页面时再从localStorage恢复。下面这个stores/user.js是典型的写法import { defineStore } from pinia import { loginApi, getUserInfoApi } from /api/auth export const useUserStore defineStore(user, { state: () ({ token: localStorage.getItem(token) || , name: , role: , department: }), actions: { async login(username, password) { const { data } await loginApi({ username, password }) this.token data.token localStorage.setItem(token, data.token) await this.fetchUserInfo() }, async fetchUserInfo() { const { data } await getUserInfoApi() this.name data.name this.role data.role this.department data.department }, logout() { this.token this.name this.role localStorage.removeItem(token) } } })逻辑说明loginApi返回的 token 直接写入 store 和本地存储刷新不会丢。fetchUserInfo单独拆出来是因为有些页面打开时只有 token需要再拉一次用户资料。role字段后续在路由守卫里判断是否允许访问审核页面。注意不要直接把localStorage塞进组件里否则多个组件同时读会长出一堆重复代码。统一收口到 actions 里后续换成管控 cookie 或刷新 token 只改这一处。3. 科研成果申报流程中的登录权限、表单拆分与审核状态流转3.1 用路由守卫实现登录拦截和角色权限控制科研成果管理系统最核心的权限控制包括未登录用户只能进入登录页申报人可以创建和编辑草稿审核人员只能看到待处理列表。在router/index.js里定义路由 meta然后加全局前置守卫。import { createRouter, createWebHistory } from vue-router import { useUserStore } from /stores/user const routes [ { path: /login, component: () import(/views/login/index.vue), meta: { public: true } }, { path: /, component: () import(/components/layout/MainLayout.vue), children: [ { path: dashboard, component: () import(/views/dashboard/index.vue) }, { path: achievement/list, component: () import(/views/achievement/list.vue) }, { path: audit/todo, component: () import(/views/audit/todo.vue), meta: { roles: [auditor, admin] } } ] } ] const router createRouter({ history: createWebHistory(), routes }) router.beforeEach((to) { const userStore useUserStore() if (!to.meta.public !userStore.token) { return { path: /login, query: { redirect: to.fullPath } } } if (to.meta.roles !to.meta.roles.includes(userStore.role)) { return { path: /dashboard } } return true })参数说明query.redirect用来记录用户本来想去的页面登录成功后跳回去。to.meta.roles在路由配置里写静态角色比在组件里用 if 判断更直白如果角色数量特别多可以在meta里换成权限码数组。createWebHistory使用 HTML5 history 模式后文部署到 Nginx 时需要对应处理找不到路由的情况。3.2 成果申报表单的组件化拆分申报页面通常包含基础信息、成员列表、附件上传三块。不要把整个表单写成一个 500 行的create.vue拆成三个子组件父组件只负责聚合状态和提交。下面是用 Ant Design Vue 定义的BasicInfoForm.vuetemplate a-form :modelform :rulesrules layoutvertical refformRef a-form-item label成果名称 nametitle a-input v-model:valueform.title placeholder输入成果完整名称 / /a-form-item a-form-item label成果类型 nametype a-select v-model:valueform.type :optionstypeOptions / /a-form-item /a-form /template script setup import { reactive, ref } from vue const props defineProps({ initialData: { type: Object, default: () ({}) } }) const formRef ref() const form reactive({ title: props.initialData.title || , type: props.initialData.type || 论文 }) const typeOptions [ { value: 论文, label: 论文 }, { value: 专利, label: 专利 }, { value: 软著, label: 软著 }, { value: 获奖, label: 获奖 } ] const rules { title: [{ required: true, message: 请填写成果名称 }] } defineExpose({ form, formRef }) /script逻辑说明defineProps接收父组件传入的草稿数据reactive里用initialData来初始化这样编辑时表单有默认值新增时是空值。defineExpose把form和formRef暴露给父组件父组件在点“保存”时统一触发校验。这种拆分方式的好处是以后需要给“论文”动态增加影响因子字段时不用动父组件。父组件create.vue里用ref拿到子组件提交时调用validateconst basicInfoRef ref() const submit async () { const values await basicInfoRef.value.formRef.validate() const payload { ...values, ...memberRef.value.getValues() } await createAchievementApi(payload) router.push(/achievement/list) }这里的await validate()是 Ant Design Vue 表单实例提供的能力校验失败会抛异常成功返回字段值。把三个子组件的值合并后提交后端只收到一个结构化对象。3.3 列表到详情页的路由参数传递列表页点击某条成果后跳转详情不能把整条数据放在全局变量里否则刷新页面就丢。正确做法是把id放进路由参数详情页通过route.params.id重新拉接口。const goDetail (id) { router.push({ name: achievement-detail, params: { id } }) }对应的详情路由需要这样配置{ path: achievement/detail/:id, name: achievement-detail, component: () import(/views/achievement/detail.vue) }组件里用watch监听参数变化import { useRoute } from vue-router const route useRoute() watch(() route.params.id, async (id) { const { data } await getAchievementDetailApi(id) current.value data }, { immediate: true })参数说明使用params而不是queryURL 上展示的是/achievement/detail/1001更符合资源定位语义。immediate: true让组件首次进入时就执行一次后续在列表连续切不同成果时也不需要重新创建页面。3.4 审核状态机与操作按钮联动审核过程不是简单的改字段而是状态按规则流转草稿可提交变待审核审核中只能通过或驳回驳回后可编辑重新提交。维护一张状态表方便前后端对状态码的解释保持一致status含义可执行操作0草稿编辑、提交1待审核撤回、查看2审核中通过、驳回3已立项查看、导出4已驳回编辑、重新提交在stores/achievement.js里把状态映射放到常量文件组件里不出现魔法数字export const ACHIEVEMENT_STATUS { 0: { text: 草稿, color: default }, 1: { text: 待审核, color: processing }, 2: { text: 审核中, color: warning }, 3: { text: 已立项, color: success }, 4: { text: 已驳回, color: error } } export function getStatusText(status) { return ACHIEVEMENT_STATUS[status]?.text ?? 未知 }列表页的a-tag直接引用a-tag :colorACHIEVEMENT_STATUS[record.status].color {{ ACHIEVEMENT_STATUS[record.status].text }} /a-tag这里有个容易踩的坑有人会在模板里写record.status 1一旦状态码增减就要全局替换而且跟后端约定的状态对不上时很难排查。把状态映射单独放在constants文件里前后端联调时只要改一处。4. 科研成果管理系统的接口封装、文件上传与统计图表4.1 统一请求封装与token失效处理科研管理系统的接口通常挂在/api下每次请求都要带 token接口报 401 时自动跳登录。utils/request.js里这样封装import axios from axios import { message } from ant-design-vue import router from /router const request axios.create({ baseURL: /api, timeout: 15000 }) request.interceptors.request.use((config) { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use( (response) { const res response.data if (res.code ! 0) { message.error(res.message) return Promise.reject(new Error(res.message)) } return res }, (error) { if (error.response?.status 401) { localStorage.removeItem(token) router.push(/login) } message.error(error.message) return Promise.reject(error) } ) export default request参数说明baseURL: /api配合 Vite 的 proxy 转发到本地后端避免开发环境跨域生产环境由 Nginx 转发。timeout设 15 秒适合文件上传之外的接口如果上传大附件需要单独放宽。拦截器里判断res.code这是和后端约定的统一返回格式好处是接口报错不用在每个页面 try-catch 重复处理。401 时清 token 并跳登录避免页面白屏。约定常见的错误码表code含义前端处理0成功放行401未认证清 token 跳登录403无权限提示并返回首页500服务器异常提示稍后重试4.2 附件上传控件与进度条成果申报经常要传论文 PDF 和专利证书扫描件用 Ant Design Vue 的a-upload组件。设置自定义请求显示上传进度和回显已有文件a-upload v-model:file-listfileList :custom-requesthandleUpload :max-count3 accept.pdf,.jpg,.png a-button选择附件/a-button /a-uploadconst handleUpload async ({ file, onSuccess, onError }) { const formData new FormData() formData.append(file, file) try { const { data } await uploadApi(formData, { timeout: 60000, onUploadProgress: (e) { if (file.percent) file.percent Math.round((e.loaded / e.total) * 100) } }) form.file_url data.url onSuccess(data) } catch (e) { onError(e) } }说明custom-request会覆盖组件默认的action行为适合接口需要额外 header 或自定义返回结构的情况。onUploadProgress是 Axios 自带的上传进度事件file.percent直接写回 fileList 里的文件对象进度条自动更新。上传成功后把返回的 URL 存到form.file_url提交表单时一并发送。这里不要把fileList直接作为表单值提交因为组件内部对象结构对后端不友好。4.3 按院系和年度统计成果数量统计报表是科研成果管理系统的最终数据出口。使用 ECharts 时不建议在生命周期里写大量配置封装一个ChartCard.vue组件传入option自动渲染。script setup import * as echarts from echarts import { onMounted, ref, watch } from vue const props defineProps({ option: { type: Object, required: true } }) const chartRef ref() let chart null onMounted(() { chart echarts.init(chartRef.value) chart.setOption(props.option) }) watch(() props.option, (val) { chart?.setOption(val) }, { deep: true }) /script template div refchartRef styleheight: 360px/div /template在页面里先请求统计接口把返回的数组处理成 ECharts 需要的格式const statOption ref({}) const loadStat async () { const { data } await getAchievementStatApi({ userId: currentUser.id }) statOption.value { xAxis: { type: category, data: data.map((x) x.department) }, yAxis: { type: value }, series: [{ type: bar, data: data.map((x) x.count) }] } }watch加deep是为了当接口数据异步返回后ECharts 能收到新的配置。这里有个常见问题窗口大小改变时图表不会自动跟随需要监听resize事件并调用chart.resize()。给组件加一个window.addEventListener(resize, handler)在onUnmounted里移除。5. 科研成果管理系统前端性能优化与Nginx部署参数5.1 路由懒加载与首屏优化科研成果管理系统的入口路由有登录页、布局页和内部页面如果全部打包成单一index.js首屏会白白下载几百 KB 的组件代码。Vite 默认支持动态 import路由表里的组件路径用() import()写法即可。打包后每个 view 会被拆成独立 chunk首次只加载当前路由。不过注意一个副作用切到审核页面时可能短暂白屏因为对应 chunk 是异步加载的。我一般配合路由懒加载在MainLayout.vue里加一层面包屑骨架或者在router.afterEach里关闭全局 loading。这个做法不增加代码复杂度体验提升很大。5.2 Nginx 的 history 路由回退与接口代理前端使用createWebHistory时用户直接访问/achievement/listNginx 会先找磁盘上有没有同名文件。没有就会 404。需要在server块里配置server { listen 80; server_name research.example.com; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; } }参数说明try_files $uri $uri/ /index.html是所有 Vue history 路由部署的通用配置找不到文件就返回入口页面。location /api/把接口请求反向代理到后端服务proxy_pass末尾的/会去掉/api前缀如果后端接口本来就带/api则要写成proxy_pass http://127.0.0.1:8080;不写末尾斜杠。这个细节是生产环境最常见的排错点。5.3 验证路由回退和接口代理是否生效部署完先访问首页然后手动刷新/achievement/list页面如果刷新后停留在同一路由而不是回到首页说明try_files生效。再打开浏览器 devtools 的 Network 面板过滤api确认请求打到正确的接口地址。最后把构建产物的静态文件名改成带 hash 格式配合 Nginx 的Cache-Control: immutable能省略每次发版后的强缓存等待。本文还有配套的精品资源点击获取
返回列表