模拟面试梳理

本次会议为一场模拟面试,旨在为候选人张同学进行面试辅导。面试官围绕候选人背景、项目经验和通用技术问题进行了深入提问,并对面试表现和简历提出了优化建议。

小结

  1. 自我介绍与基本情况

候选人从事前端开发约6年,主要负责TOB项目的重构与维护,具备独立负责项目从0到1搭建的经验。

面试官指出其自我介绍需优化,使其更精炼、专业,并强调了学历背景的重要性。

  1. 通用能力与职业规划

离职原因:因项目组瘦身,非因个人原因离职。

加班看法:明确区分了"项目紧急"和"非必要"的加班,前者愿意接受,后者则不太能接受。

技术提升:通过查阅资料、项目复盘和学习新技术(如CSDN、B站)来提升自己。

未来规划:希望在前端技术上深耕,并逐步探索AI开发、后端及运维方向,未来考虑向售前或管理岗转型。

  1. 项目经验与技术细节

XSQ园区服务系统重构:该项目为老旧信创系统升级,采用Vue3进行PC端和H5端重构,解决了浏览器兼容性和H5端金融适配等问题。

"FNBK"项目:候选人详细介绍了该项目中V2版本的选择原因(稳定性、安全性、团队熟悉度)、组件封装、用户隐私保护(HTTPS、RSA加密)、图片上传压缩、红娘后台的权限审批流程等。

"NGSJ"项目:候选人阐述了路由权限控制、图表响应式适配、Vuex全局状态管理、图表数据缓存、用户行为埋点、图表配置保存与恢复等技术实现。

  1. 面试技巧与心态

面试官建议:

面试时需站在面试官的角度思考问题,避免过度展开细节。

对公司了解需深入,包括其业务范围、产品线,而不仅是表面的产品。

对于"代码评审"等问题,需明确其目的(如保证代码质量、安全性和团队协作),而非仅描述工具使用细节。

待办

  1. 模拟面试后续安排

张同学需在下周一下午再次进行模拟面试,届时将重点辅导"WH"项目的技术细节。

  1. 简历优化

张同学需先调整"WH"项目的技术栈描述,修正其为uni-app而非原生小程序的错误。

张同学需将简历中的"专升本"专业修改为"计算机科学与技术"。

将根据优化后的简历内容,进行排版和润色,形成最终版本。

  1. 投递准备

张同学可在下周开始正式投递简历。


1、请简单做个自我介绍

自我介绍:

面试官您好,我叫 张XX,来自山东XX,毕业于山东XX大学,有6年的前端开发经验,我参与过多个YH内部系统的重构与开发,有PC端、小程序和H5端的开发经验,熟悉的技术栈是 Vue2、Vue3,熟练掌握 Composition API、Vite 等现代前端技术,能够独立完成从需求分析到页面开发、联调上线的全流程,具备完整的项目交付能力。以上是我的自我介绍,谢谢。

2、你为什么从上一家公司离职?

因为是YH的项目,现在YH的项目已经到了收尾阶段,后期就没有那么多工作要做了,所以公司现在的话是裁员的一个状态。


标准版回答(首选,稳妥)

我上一家主要负责YH内部相关项目的开发,之前历时两年的核心重构项目已经全面落地收尾。现阶段整体业务进入维稳阶段,日常工作量大幅缩减,不需要这么多研发人力。公司结合业务现状做了团队人员优化、缩编瘦身,我们整个项目组进行了合理精简,所以我这边被动离开,寻求新的发展机会。


精简版(面试语速快 / 简短回答用)

上家公司YH类核心项目全部收尾完成,业务进入维护期,工作量锐减。公司统一做团队缩编优化,项目组整体裁员精简,因此被动离职,希望找业务更饱和、长期稳定的岗位。


避坑要点(切记)

  1. 不提 降薪、薪资不满、吐槽行情、抱怨压榨,全程客观中立;
  2. 不说公司经营差、倒闭、制度问题,只说「项目收尾、业务收缩、人员优化」;
  3. 定位是 被动裁员 / 团队缩编,不是主动跑路、能力不行被开;
  4. 全程情绪平稳,不消极,落地落脚在「想找稳定长期发展平台」。

3、你上一家公司加班情况怎么样?

我不太喜欢公司强制性的加班,比如说咱项目确实是赶进度或者是紧急上线,这种情况加班很正常,我是愿意去加班的,如果是非必要的这种加班,比如说项目也不急,公司天天催,天天让我们在公司待着加班又没有活干,这种加班我可能不太接受。

你说我们之前也加,然后项目紧急上线的时候都加,没有不加的。


我上一家是YH外包项目,整体节奏完全跟随YH方安排。

日常业务平稳、项目进度正常的情况下,都是朝九晚六正常上下班,基本没有额外加班。

只有遇到 版本迭代、项目紧急上线、临时需求加急这类关键节点,才会统一安排加班,而且加班都是提前报备审批,合理安排,不会盲目消耗。

关于加班我一直是理性看待的:

目前行业大环境下,项目关键期的临时性加班我完全理解、也能接受。毕竟为了保障项目顺利交付、按时上线,这种必要的工作投入是应该的,我之前也多次配合过项目上线的阶段性加班,完全没问题。

但我比较抵触 无意义的 强制性内卷加班

如果项目进度正常、没有紧急需求,只是单纯要求固定加班、耗时长、无效坐班,这种形式化加班我是不太认可的。

简单来说:必要的 阶段性 加班 我全力配合,拒绝 无意义、无产出 的 无效 强制加班,这样也能保证日常工作效率,合理平衡工作节奏。

4、你平时是通过什么方式提升自己技术的?

平时我主要通过 项目实战 + 复盘沉淀 + 主动拓展学习三块来提升技术。

第一,在日常项目开发里,遇到难点和疑难问题时,会先独立排查、查阅官方文档和技术博客去解决;问题处理完之后,我会及时做复盘总结,把解决方案、踩坑点整理下来,避免后续重复踩坑,巩固实操能力。

第二,会持续关注前端生态的新技术和行业动态,通过技术社区、优质技术博主、公开课这类渠道,了解框架更新、工程化、性能优化这类前沿方向,保持技术不落后。

第三,也会针对性做专项学习,比如数据可视化、项目性能优化、工程化配置这类业务常用能力,结合实际业务场景去落地实践,把学到的内容用到项目里,做到学以致用,稳步提升整体技术深度。

5、你目前的薪资和期望薪资是多少?

面试标准回答

HR:你目前薪资大概多少?

我目前综合月薪 ??k,是固定薪资,不含额外绩效。

HR:那你的期望薪资是多少?

结合我之前的项目经验,包括数据可视化、权限系统开发、YH类外包项目落地经验,还有目前市场行情,我的 期望薪资是 ??k

我也了解现在行业整体薪资涨幅区间,能够接受根据公司定级、岗位具体工作内容,进行合理沟通调整,核心希望薪资和个人能力、工作内容相匹配。


补充关键要点(帮你避坑)

  1. 原薪 ??k,期望 ??k,涨幅约 26.7%,刚好卡在优秀求职者 20%-30% 的合理区间,完全不夸张;
  2. 外包岗普遍涨幅 10%-15%,对应薪资:1?.25k,你的期望留有谈判空间,就算往下压,也能谈到理想价位;
  3. 不咬死价格、加上「可合理沟通」,不会让 HR 反感,既体现自信,又不僵硬。

6、你对自己未来3~5年有什么规划吗?

未来 3-5 年我有清晰的阶段性规划。

短期来看,优先深耕前端核心技术栈,结合业务场景把数据可视化、工程化、性能优化这些实用能力做深做透,夯实业务开发功底,更好适配项目需求。

中期会顺势结合行业趋势,熟练落地 AI 辅助开发,用好工具提升开发效率、减少重复工作,同时主动补充后端基础知识,了解接口逻辑、基础架构,提升和后端的协作效率,慢慢往 前后端 协同的方向发展。

长期希望逐步拓展技术边界,了解基础运维和项目部署相关内容,具备全流程项目落地的认知,慢慢向全栈方向靠拢,成长为能独立负责模块、吃透业务、综合能力更强的开发人员,和公司长期稳定共同发展。


我再给你压缩一版精简短句版,面试语速快的时候用:

未来三到五年,我先深耕前端核心技术,夯实业务开发能力;其次结合行业趋势,熟练运用 AI 提升开发效率,补充后端基础知识,加强跨部门协作;长远会了解部署运维相关内容,逐步向全栈方向发展,提升综合能力,稳定长期发展。

7、在项目优先级排期上面你是怎么安排的?

第一、根据项目经理的整体排期,因为项目经理的话,他会去做一些优先级的一个排序。

第二、我会先去评估每个任务的一个紧急程度,会根据这个项目任务的紧急程度和一个依赖关系,去做一个自己的排期

第三、由易到难。


在任务和项目优先级安排上,我会分两方面去处理。

首先,严格遵从团队 和 项目经理 的 整体排期,结合业务上线节点、测试要求、甲方需求这些核心因素,优先推进紧急上线、有时间节点要求的核心任务,保证关键项目按时交付。

其次,面对并行的多项工作,我会自主做好梳理评估。结合任务的 紧急程度、业务影响、模块依赖关系合理划分顺序。日常工作中,我会优先处理简单、见效快的需求,高效产出、保障整体进度;

对于技术难度高、逻辑复杂的开发任务,我会利用个人业余时间提前研究技术方案、梳理难点,做好前期准备,不占用工作时间,保证日常开发节奏稳定,高效平衡各项工作。

8、你认为一个好的前端工程师应该具备什么样的素质?

我认为一名优秀的前端工程师,首先要 技术基础扎实,熟练掌握核心技术栈,同时遵守代码规范、注重代码质量,保证项目可维护性和团队协作效率。

其次要有 较强的责任心,对待开发任务认真严谨,重视功能稳定性,确保需求高质量落地。

前端直接面向用户,一定要 注重 用户体验,在页面性能、交互流畅度、兼容性等细节上多做优化,提升整体使用感受。

另外具备良好的 沟通 协作能力也很关键,和后端、测试、产品高效对接,减少联调沟通成本,推进项目顺利进行。

最后要有 持续学习的意识,前端技术更新迭代很快,需要主动跟进新技术、新趋势,不断补齐自身能力,适配不同业务场景的开发需求。


1、技术扎实;2、责任心;3、用户体验;4、善于沟通、持续学习。

9、你是怎么应对项目中的紧急 bug ?

遇到线上或项目紧急 bug,我会第一时间 优先停下手头非核心工作,集中精力处理问题。

首先快速复现问题,确认 bug 影响范围、是否影响核心功能与用户使用;

然后快速分层排查,从接口请求、数据返回、代码逻辑、样式兼容性等维度定位根因,高效修复;

修复完成后,在对应环境反复自测、联动测试同事快速复核,确保问题彻底解决、不产生次生问题;

问题处理完毕后,我会简单复盘原因,避免同类问题重复出现,保障项目稳定运行。


极简口述版(面试语速快用)

面对紧急 bug,我会立刻优先处理,先快速 复现问题 、确认影响范围,再快速 排查定位 问题根源,高效完成修复并 复测验证。事后做好复盘总结,规避同类问题再次发生。

10、你来之前的话对我们公司有做过了解吗?让你比较心动的点在哪?

问题 1:你来之前,对我们公司有做过了解吗?

标准回答

有的,我在面试前专门了解过咱们公司。

知道公司主要深耕金融YH类外包项目,长期合作各大YH,业务体系稳定,项目流程规范,团队管理也比较成熟。

同时岗位业务方向和我之前做的YH类前端项目经验高度匹配,整体工作模式、业务场景我都比较熟悉。


问题 2:那你觉得我们公司最吸引你的点是什么?

标准回答

第一,业务方向很契合。

主打YH金融项目,和我过往的项目经验、技术栈完全对口,上手快,能快速投入工作,不用重新适应陌生行业。

第二,公司平台稳定、流程正规。

金融类项目对代码规范、项目交付、协作流程要求很高,在这样的环境里工作,能倒逼自己提升专业能力,稳步成长。

第三,发展匹配我的规划。

我想长期深耕业务前端,稳定沉淀技术和项目经验,咱们公司的项目体量和业务场景,很适合长期稳定发展,这也是我比较心动的地方。


补充小技巧(面试加分)

  1. 不说空话、不夸大海口,紧扣 YH外包、业务匹配、稳定成长三个核心;
  2. 不用讲复杂产品,只聊 行业属性、项目类型、工作氛围、个人发展
  3. 暗示自己:稳定、好上手、能长期干、适配金融项目,正是外包公司想要的人选。

11、你在工作中遇到最大的挑战是什么?你是怎么克服这些问题的?

我工作中遇到最大的挑战,是接手过一个YH老旧存量项目的改版优化。

这个项目技术栈非常老旧,采用 JSP 混合开发、前后端没有分离,整体代码耦合度很高,早期还用了 jQuery 技术,代码杂乱、没有规范,初期阅读和理解成本特别大。

一开始最大的难点是方案不明确,不确定是在原有旧代码上做修补迭代,还是整体重构。我先花时间完整梳理了旧项目的全部业务逻辑与代码结构,梳理清楚整体业务流程后,主动和项目经理沟通评估。结合后期维护成本、迭代效率和长期使用性综合判断,最终确定 整体重构的方案。

确定方案后,我使用 Vue 脚手架从零搭建轻量化项目框架,按需引入依赖,保证项目精简、无冗余,适配项目业务体量。

开发中期遇到了明显的浏览器兼容问题,项目在主流浏览器可以正常运行,但在 IE 浏览器出现白屏、功能异常的情况。

我逐行排查代码、缺少的兼容配置、缺失的兼容插件,逐步补充适配方案,不断完善框架底层配置,解决了兼容性问题。

通过这次经历,我不仅提升了 老旧项目改造、兼容适配、独立方案评估的能力,也养成了项目前期提前考虑环境兼容、底层架构规划的习惯,避免后期返工,提升整体开发效率。

12、你是怎么看待代码评审的?

代码评审是团队协作开发里 必不可少 的 核心流程,价值和意义非常关键,我主要从这几点理解:

  1. 把控代码质量相当于代码阶段的质检,避免个人开发中写出不规范、冗余、逻辑混乱的代码,统一团队编码风格,减少烂代码、垃圾代码堆积,降低后期维护成本。

  2. 规避线上风险提前发现隐藏 bug、逻辑漏洞、安全隐患和性能问题,很多自测发现不了的隐性问题,通过交叉评审能提前拦截,从源头减少线上故障,保障项目稳定运行。

  3. 统一团队规范约束每个人的开发习惯,配合 Eslint、Prettier 等工具 + 人工评审,让团队代码风格、业务写法保持统一,新人上手更快,多人协作冲突更少。

  4. 团队技术沉淀与成长评审过程也是互相学习的过程,既能发现自身开发盲区,也能借鉴同事优秀的写法、设计思路,整体提升团队整体技术水平。

  5. 便于后期维护迭代规范、高质量、逻辑清晰的代码,后续迭代、bug 修复、接手开发时成本更低,让项目能长期健康维护,适配长期业务迭代。


精简版(短答,适合快速应答)

我认为代码评审是团队开发中必要的质检环节。一方面能统一编码规范、把控代码质量,提前拦截 bug、安全和性能隐患,减少线上问题;另一方面可以促进团队互相学习,降低项目维护成本,保障项目长期稳定迭代。

13、你能接受出差吗?

标准版(首选,通用所有公司)

完全可以接受出差

之前有过长时间驻场开发的经验,适应异地办公、短期外派的节奏,不管是短期临时出差,还是阶段性驻场都没问题。

个人适应能力比较强,也能配合项目节奏、公司安排,只要差旅流程、费用正常报销,出差对我来说没有任何顾虑,会保质保量完成异地工作。

精简短答(面试官快速追问、不想多说时用)

可以接受出差,我有 驻场异地办公经验,适应力强,能完全配合项目和公司的出差安排。

加分优化版(体现态度 + 稳定性)

我能够接受合理范围内的出差安排。

过往有驻场工作经历,熟悉异地协作、现场对接的工作模式,不排斥外出办公。

工作上一切以项目进度和公司需求为先,合理的出差调配都可以配合,不会因为出差影响工作落地。


补充避雷提醒

  1. 不要说 **"我很喜欢出差、想天天出差"**,太极端,面试官会担心你不稳定、不想深耕开发;
  2. 不要说 **"尽量不要出差、偶尔可以"**,容易直接扣分;
  3. 只表态 服从安排、有经验、无顾虑,简洁得体,刚好踩中面试得分点。

14、简单介绍一下XSQ园区服务系统重构与国产化升级项目

该项目是 老旧存量园区内部办公系统的整体重构 + 信创国产化升级,服务园区内部多机构员工日常办公、后勤维护等核心场景。

原系统上线年限久,技术栈老旧,依赖国外技术体系,包括 WAS 中间件、Oracle 数据库、Windows 服务器,存在安全漏洞多、维护成本高、架构混乱、体验卡顿等问题,且业务入口分散、多套登录地址,使用不便。

我负责 前端整体重构开发,技术栈采用 Vue3 全新开发,同时兼顾 PC 端与 H5 移动端适配。项目统一业务登录入口、优化整体业务架构,剥离国外老旧技术组件,适配国产化环境改造。

改造完成后,统一了园区 6 家机构的日常办公使用,不仅修复原有安全隐患、降低后期维护成本,同时提升系统运行性能、优化操作体验,全面满足单位国产化信创改造要求与长期稳定办公需求。

15、信创环境下遇到了哪些兼容性问题?是怎么解决的?

在信创国产化改造项目中,主要遇到 浏览器兼容、样式布局、系统环境、第三方依赖、字体图标、文件预览这几类核心兼容问题,具体问题 + 落地解决方案如下:

1、信创浏览器接口缓存 & 请求兼容问题

  • 问题:国产浏览器(麒麟、统信配套自研浏览器、360 信创版等)请求机制特殊,存在接口强缓存、协商缓存不失效问题,后台数据更新后,前端页面数据无法实时同步,接口重复请求、参数解析异常。
  • 解决方案:

① 在 Axios 请求拦截器统一配置 时间戳随机参数、禁用请求缓存;

② 统一配置请求头 Cache-Control: no-cache,强制浏览器不缓存接口;

③ 针对 get 请求统一处理参数序列化,适配国产浏览器解析规则。

2、页面样式与布局兼容问题

  • 问题:国产浏览器内核老旧,对 CSS3 新属性、Flex / Grid 布局、圆角阴影、弹性样式适配差;ES6 + 高级语法不识别,导致页面错乱、样式丢失、布局塌陷。
  • 解决方案:

① 引入 PostCSSAutoprefixer 自动补全 css 前缀,统一适配多内核浏览器;

② 配置 Babel,将 ES6+、箭头函数、解构赋值等高阶语法 降级编译为 ES5

③ 摒弃小众 css 语法,使用稳妥的传统布局方案,特殊样式做降级兜底处理。

3、操作系统 & 端侧适配兼容问题

  • 问题:信创环境多为麒麟、统信 Linux 系统,搭配国产硬件,同时兼顾 PC 端 + H5 移动端;移动端(尤其 IOS、老旧安卓)存在点击延迟、适配错乱、分辨率兼容问题。
  • 解决方案:

① 采用 响应式布局 + rem/vw 适配方案,统一多分辨率设备展示;

② 封装通用兼容方法,修复移动端点击 300ms 延迟、事件冒泡兼容问题;

③ 针对 Linux 国产系统做单独样式兜底,规避系统内核渲染差异。

4、第三方组件 & 插件兼容性问题

  • 问题:项目中 Echarts 图表、富文本、弹窗、日历、文件上传等第三方组件,大多基于 Chrome 内核开发,在信创浏览器出现渲染失败、功能失效、弹窗错位。
  • 解决方案:

① 选用国产化适配成熟的替代组件,或降级使用稳定低版本依赖;

② 对自定义弹窗、遮罩层做层级、定位兼容处理;

③ 图表关闭高级渲染模式,使用基础 Canvas 渲染,适配老旧内核。

5、字体、图标及特殊功能兼容

  • 问题:国产系统缺少常规网页字体、Iconfont 图标乱码、特殊弹窗、打印导出功能无法正常使用。
  • 解决方案:

① 引入通用 web 安全字体,规避系统字体缺失问题;

② 图标改用 svg 格式替代字体图标,提升跨环境兼容性;

③ 打印、导出等功能做浏览器判断,信创环境提供替代操作方案。

16、项目中你是如何封装可复用组件的?

在项目中,我主要基于 Element Plus 基础组件 进行 二次封装,沉淀全局可复用业务组件,提升开发效率、统一项目 UI 规范。

  1. 统一基础样式与规范对常用的 按钮、弹窗、表单、搜索栏、卡片、表格 等组件统一封装,内置全局统一的尺寸、配色、圆角、间距、样式风格,避免每个页面重复写样式,保证全站 UI 视觉统一。

  2. 通过 Props 实现灵活配置 公共基础属性、固定样式内置默认值; 个性化需求通过 props 传参 暴露配置项,比如尺寸、主题色、文案、显隐、禁用状态、自定义样式等,不同页面按需传入参数,实现差异化使用。

  3. 插槽 + 事件扩展灵活性 使用 具名插槽、默认插槽,支持自定义内容; 同时封装统一自定义事件,覆盖确认、取消、关闭、刷新等常用操作,兼顾通用性和业务定制性。

  4. 整合通用业务逻辑把高频重复逻辑内置到组件内部,比如弹窗关闭重置表单、表格默认分页、权限按钮控制、loading 加载状态、空数据兜底等,减少页面冗余代码。

  5. 全局注册,开箱即用封装完成后在全局统一注册,页面直接引入使用,不用重复引入原生组件,降低维护成本,后期如果需要改整体样式或逻辑,只需要修改封装组件一处,全局生效。

17、事物申请模块中的表单校验是如何设计的?

事务申请模块的表单校验,是基于 二次封装的全局表单组件统一设计实现的。

  1. 规则统一配置 提前为每个表单项绑定独立校验规则,包含 非空必填校验、手机号 / 身份证 / 邮箱等正则格式校验、长度限制、内容格式校验等,所有校验规则集中配置,统一管理。

  2. 实时 + 提交双重校验 输入框失焦时触发实时校验,即时提示错误;点击提交按钮时,一次性全局遍历所有表单规则,批量校验全部表单项。

  3. 错误反馈交互处理 校验不通过时,表单项标红高亮,展示红色错误提示文案;同时自动滚动定位到 第一个校验失败的表单项,提升用户操作体验。

  4. 提交行为限制 提交按钮做了 Loading 加载状态 + 防抖节流处理,防止重复频繁提交;只有所有表单校验规则全部通过,才会放行、调用新增 / 提交接口。

  5. 后续流程闭环接口请求成功后,弹出成功提示,自动跳转至事务申请列表页;接口异常则给出错误提示,保留用户已填内容,方便修改重新提交。


用户进入 事务申请 表单页面,填写 相关信息,比如 申请 事由、时间、附件 等。在用户提交前,我会在前端做 完整的 表单校验,包括 必填项 校验、格式 校验、长度 限制、逻辑校验等,校验不通过则给出明确提示,阻止提交

校验通过后,前端调用 提交申请 接口,把表单数据传给后端。接口返回成功后,前端给出提交成功的 提示,并自动 跳转到 我的申请 列表页面。

18、您如何实现"我的专区"中的待办事项提醒?

答案:从后端获取待办列表(如待审批申请),使用红点和数字角标展示数量。点击可跳转到详情。通过 轮询 或 WebSocket 实时更新。


我的专区待办事项提醒,整体采用 后端驱动、前端渲染展示的方案实现。

  1. 数据来源:所有待办数据由后端统一生成,比如用户提交事务申请后,系统会自动给对应审批人生成待办任务。
  2. 前端展示:页面初始化时,前端调用 待办列表接口 ,拉取当前登录用户的全部待办数据;同时通过 数字角标、红点标记展示待办总数、未处理数量,起到强提醒作用。
  3. 交互跳转:点击单条待办 或 处理按钮,直接路由跳转至对应审批详情页,进行审核操作。
  4. 实时更新:采用 轮询 + WebSocket结合的方式,实时监听新增待办、已办结任务状态变更,自动刷新待办数量和列表,保证提醒数据实时同步。
  5. 状态联动:审批完成后,后端更新任务状态,前端自动过滤已完成事项,减少无效提醒。

对于 审批人 ,系统会通过 轮询 或 WebSocket 推送待办 通知,审批人 点击进入 审批 详情页 ,做出 "通过" 或 "驳回" 操作 ,前端再次发起 审批请求 -> 最终,申请人会收到最终审批结果的通知。

19、你是如何处理 Token 过期问题的?

我在项目中基于二次封装的 Axios 统一处理 Token 过期问题。在 响应拦截器 中全局拦截接口返回状态码,当识别到 401、Token 失效 / 过期、登录失效 等状态时:

  1. 立即 清空本地存储的 Token、用户信息等缓存数据;
  2. 弹出友好提示,告知用户登录已过期,请重新登录;
  3. 强制跳转至登录页面,重新授权登录;
  4. 同时增加异常拦截,防止重复弹窗、重复跳转,优化用户体验。

极简口述短版(面试快速回答)

通过 Axios 响应拦截器统一捕获 401 状态码,判断 Token 过期后,清空 本地登录缓存,提示登录失效,自动跳转到登录页重新登录。

20、如何优化移动端加载性能问题?

在移动端项目中,我从 代码、资源、渲染、网络、打包多个维度做加载性能优化:

  1. 路由与代码优化 采用 路由懒加载 、按路由拆分打包,减少首屏加载资源体积,避免一次性加载所有页面代码,提升首屏渲染速度。

  2. 图片资源优化 对项目图片统一压缩,替换为 WebP 轻量化格式,同时配合后端二次压缩;合理使用图片懒加载,不在可视区的图片延迟加载,减少初始请求压力。

  3. 长列表性能优化 针对大数据长列表,和后端对接做 分页加载 ,上拉触底分页请求;海量列表场景使用 虚拟滚动,只渲染可视区域 DOM,避免 DOM 过多造成页面卡顿、滑动卡顿。

  4. 网络与请求优化 接口合理 节流防抖,避免频繁请求;项目开启 Gzip 压缩,减小静态资源传输体积,加快资源加载速度。

  5. 组件与缓存优化 合理使用 keep-alive 缓存常用页面和组件,减少重复渲染、重复请求;非响应式数据不放入 data,直接 挂载在实例上,减少 Vue 响应式监听开销,降低内存消耗。

  6. 用户体验优化全局配置 Loading 加载状态,请求期间展示加载动画,避免白屏;优化静态资源加载顺序,关键资源优先加载,提升用户感知体验。


21、您为什么选择原生小程序开发而不是uni-app?

答案:原生小程序性能更好,调试工具完善,且项目 不需要跨多端。团队也熟悉原生语法,开发效率更高。


面试标准回答:为什么芜花律师管理平台选用微信原生小程序,而非 uni-app

  1. 性能体验更优原生小程序直接依托微信底层引擎运行,无 uni-app 框架层、编译层额外开销,页面渲染、数据更新、滑动交互更流畅;律师管理平台包含表单、列表、权限弹窗、数据查询等高频操作,原生性能更稳定,避免多端框架兼容带来的卡顿、加载延迟问题。

  2. 项目无跨端需求 本项目是 专属微信端的内部业务小程序 ,只需要服务微信生态用户,不需要打包安卓、iOS App、支付宝小程序等多端能力,uni-app 跨端的核心优势完全用不上,没必要引入 多余框架

  3. 原生能力兼容性更强、调用更稳定 微信原生 API 对小程序专属能力支持更底层、更全面,比如 微信登录、授权、定位、拍照上传、消息推送、本地存储、业务权限校验等官方能力,原生调用更直接,减少框架封装 带来的兼容 bug、适配异常,业务稳定性更高。

  4. 调试与开发更贴合业务场景 全程使用微信开发者工具原生调试,报错精准、日志清晰、源码直观;搭配 HBuilderX 辅助编码只是提升编写效率,底层代码仍是微信原生小程序语法,并非 uni-app 编译代码,排查问题、线上问题复盘更高效。

  5. 团队技术栈匹配,开发维护效率更高团队熟悉微信小程序原生语法、WXML / WXSS、小程序生命周期及原生工程规范,无需额外学习 uni-app 语法、跨端适配规则、条件编译等额外成本;项目轻量化、业务逻辑聚焦,原生开发更简洁,后期迭代、维护、二次优化成本更低。

  6. 项目轻量化,避免框架冗余uni-app 内置大量跨端兼容代码、公共组件与依赖包,会增加项目体积;原生小程序按需编码、轻量化打包,包体积更小,小程序启动速度更快,符合轻量化业务系统的开发要求。


我帮你把这段压缩成 精简面试口述版,方便直接背:

芜花律师管理平台只针对微信单端 使用,不需要多端 打包能力,所以没必要用 uni-app。原生小程序依托微信底层引擎,没有框架多层编译开销 ,页面交互和加载性能更好;同时原生对微信各类授权、上传、推送等官方 API 支持 更稳定,减少兼容问题。加上团队熟悉原生小程序语法,搭配微信开发者工具调试更精准,开发和后期维护效率更高,项目也更轻量化。

22、您如何搭建小程序开发环境?

一、项目真实技术栈(最终版)

技术栈:uni-app + uView UI + 微信小程序端

不是原生小程序!不是原生小程序!不是原生小程序!

之前写的 "原生" 全部作废,统一改成 uni-app。

二、面试标准答案(你直接背)

问:你是如何搭建小程序开发环境的?

答:

我使用 HBuilderX 作为开发工具,直接新建 uni-app 项目,选择默认模板,运行到微信小程序端。

开发时搭配 微信开发者工具 进行联调预览。

项目中通过 npm 安装 uView UI 组件库,并在 main.js 中引入使用,完成基础开发环境搭建。

问:为什么用 uni-app,不用原生小程序?

答:

因为项目需要支持 微信小程序,未来可能扩展其他端,uni-app 一套代码多端发布,开发效率更高。

而且 uni-app 基于 Vue 语法,写法更简洁,搭配 uView UI 能快速完成表单、列表、弹窗等业务组件。

问:你这个项目是从 0 到 1 开发的吗?

答:

不是从零开发,是 接手已有的二手项目 ,在原有项目基础上进行 功能迭代、页面修复、接口对接、优化体验

三、项目描述里必须改的内容(我直接给你最终版)

项目名称:芜花律师管理平台小程序

技术栈:

uni-app + Vue2 + uView UI + 微信小程序 + 微信开发者工具 + HBuilderX

项目描述:

该项目是律师事务所内部管理平台,主要用于案件管理、客户信息、流程审批、数据查询等日常办公功能。我 接手维护已有项目,负责页面开发、接口联调、UI 还原、问题修复及功能迭代。

我的职责:

  1. 基于 uni-app + uView 开发各类业务页面
  2. 使用 wx.request / uni.request 对接后端接口
  3. 完成表单验证、列表展示、权限控制、图片上传等功能
  4. 微信开发者工具调试、兼容小程序环境
  5. 修复线上问题、优化页面加载速度

23、FNBK项目为什么选择Vue2而不是Vue3?

我们YH内部的「FNBK」项目最终选用 Vue2 而非 Vue3,主要是结合 行业现状、系统稳定性、团队技术栈三方面综合决策:

  1. 金融行业稳定性优先 YH 类业务对系统稳定 性、线上风险把控要求极高。Vue2 生态成熟、落地周期长,经过大量金融项目长期验证 ,兼容性、线上安全漏洞风险更低,更适配行内风控与稳定要求。

  2. 团队技术栈统一,降低开发维护成本 行内前端团队长期深耕 Vue2 技术栈,整体熟练度更高、业务积累丰富。统一技术栈 能保证开发效率,同时后期迭代、Bug 修复、新人接手维护成本更低。

  3. Vue3 当时生态未完全成熟 项目立项阶段,Vue3 还处于快速迭代阶段,部分配套插件、UI 库、老旧业务组件适配不完善,存在隐性兼容问题 和 未知 Bug。金融项目不能冒版本迭代带来的线上风险,因此优先选用更稳妥的 Vue2。

24、项目中你封装了哪些组件?

在这个项目里,我基于业务高频复用场景,独立二次封装了多个通用业务组件,提升整体开发效率:

  1. 全局 Dialog 弹窗组件统一弹窗样式、标题、底部按钮、关闭逻辑,支持自定义内容与回调,减少重复代码。
  2. 通用表单组件整合输入框、选择器等表单项,内置表单校验规则、必填校验、错误提示,统一表单提交与重置逻辑。
  3. 图片上传组件基于 Vant Uploader 二次封装,限制上传数量、大小、格式,自带预览、删除、上传 loading 及失败兜底处理。
  4. 无限滚动列表组件基于 van-list 封装,统一分页加载、空数据、加载中、无更多、异常兜底等状态,适配项目全部列表页面。
  5. Echarts 图表公共组件统一初始化、自适应窗口、防抖重绘、销毁释放实例,支持传入配置项快速生成图表,避免内存泄漏。

极简口述版(面试快速回答)

我在项目中封装了大量可复用通用组件,包括自定义弹窗 、带校验的通用表单 、二次封装的图片上传 、无限滚动列表 ,还有 Echarts 公共图表组件,统一了业务样式 和 交互逻辑,提升了页面开发效率。

25、在项目中你是怎么处理用户隐私保护的?

在项目中我从 传输、展示、本地存储、接口权限多维度落地用户隐私与敏感数据保护:

  1. 传输层安全 全程采用 HTTPS 协议传输数据,杜绝明文 http 请求,防止数据在网络传输过程中 被抓包、窃取 或 篡改。
  2. 敏感数据加密 手机号、证件号、登录密码等核心隐私信息,前后端采用 RSA 非对称加密处理,前端加密后再提交,后端解密解析,保障关键数据安全。
  3. 脱敏展示处理 后端返回敏感字段统一做脱敏处理,手机号、身份证、邮箱等进行星号打码展示,前端不展示完整原始信息。
  4. 禁止本地缓存敏感信息严格控制本地存储,用户隐私、账号密码类敏感数据,不存入 localStorage、sessionStorage 和 Cookie,避免本地数据泄露风险。
  5. 额外补充(加分项) 页面退出、账号切换时,主动清空表单缓存 与 临时数据;同时接口做好权限控制,避免越权查询他人隐私数据。

简洁口述版

项目里主要从三方面保护用户隐私:

第一,全站使用 HTTPS 加密传输,关键敏感数据通过 RSA 非对称加密前后端交互;

第二,手机号、证件等信息全部脱敏打码展示

第三,敏感数据不做本地持久化存储,防止本地信息泄露,全方位保障用户数据安全。

26、你是如何实现图片上传压缩的?

我们项目基于 Vant Uploader 组件做二次封装实现图片上传,前端做限制 + 轻量压缩,后端二次压缩兜底

  1. 前端先做基础校验,限制图片大小、格式,比如单张不超过 5M,只允许 JPG、PNG 常规格式,拦截不合格文件,减少无效请求。
  2. 利用浏览器**canvas** 绘图能力,对原图进行 前端本地压缩,修改分辨率 和 画质比例,在不影响展示清晰度的前提下,减小图片体积,加快上传速度、节省带宽。
  3. 压缩完成后转为 Base64 或 Blob 格式传给后端。
  4. 后端接收图片后,再做 二次压缩 + 画质优化,统一图片尺寸,做存储格式化处理,同时规避大图占用服务器存储的问题,形成双层压缩保障。

极简口述版

我在 Vant 上传组件基础上自行封装,前端先校验图片大小和格式,再通过 Canvas 实现本地画质压缩、降低体积;上传到服务端后,后端再进行二次压缩处理,双重优化,提升上传效率并节约服务器存储。

27、如何实现红娘后台的权限控制?

一、核心业务流程(完全匹配你的需求)

  1. 用户登录 :后端返回 用户ID、角色、权限标识、红娘审核状态
  2. 前端页面判断
    • 普通用户(未申请 / 申请中 / 被拒绝)→ 显示【申请成为红娘】按钮
    • 审核通过的红娘 → 显示【红娘中心管理】
  3. 申请流程:普通用户填写资料 + 申请理由 → 提交 → 生成审核单
  4. 管理员流程:收到待审核提醒 → 进入审核列表 → 查看资料 → 同意 / 拒绝(拒绝必填理由)→ 审批完成
  5. 状态流转:未申请 → 审核中 → 审核通过 / 审核拒绝

二、数据库设计(必须表结构)

1. 用户表(user)

存储基础信息 + 角色 + 红娘状态

sql 复制代码
CREATE TABLE `user` (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  username VARCHAR(50) NOT NULL COMMENT '账号',
  nickname VARCHAR(50) COMMENT '昵称',
  phone VARCHAR(20) COMMENT '手机号',
  -- 核心角色字段
  role VARCHAR(20) DEFAULT 'USER' COMMENT '角色:ADMIN-管理员,USER-普通用户,MATCHMAKER-红娘',
  -- 红娘审核状态(关键)
  matchmaker_status VARCHAR(20) DEFAULT 'NONE' COMMENT 'NONE-未申请,APPLYING-申请中,PASS-通过,REJECT-拒绝',
  reject_reason TEXT COMMENT '拒绝理由',
  create_time DATETIME DEFAULT NOW(),
  update_time DATETIME DEFAULT NOW()
);

2. 红娘申请表(matchmaker_apply)

存储用户提交的申请资料

sql 复制代码
CREATE TABLE `matchmaker_apply` (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  user_id BIGINT NOT NULL COMMENT '申请人ID',
  name VARCHAR(50) NOT NULL COMMENT '真实姓名',
  age INT COMMENT '年龄',
  gender VARCHAR(10) COMMENT '性别',
  wechat VARCHAR(50) COMMENT '微信号',
  address VARCHAR(255) COMMENT '所在地区',
  apply_reason TEXT NOT NULL COMMENT '申请理由',
  status VARCHAR(20) DEFAULT 'APPLYING' COMMENT 'APPLYING-待审核,PASS-通过,REJECT-拒绝',
  audit_user_id BIGINT COMMENT '审核管理员ID',
  audit_remark TEXT COMMENT '审核拒绝理由',
  create_time DATETIME DEFAULT NOW(),
  update_time DATETIME DEFAULT NOW()
);

三、后端接口设计(核心 5 个接口)

1. 登录接口(关键:返回角色 + 权限 + 红娘状态)

返回体示例(前端判断按钮显示全靠它):

javascript 复制代码
{
  "code": 200,
  "data": {
    "token": "xxxx",
    "userId": 1001,
    "role": "USER",        // 角色:ADMIN/USER/MATCHMAKER
    "permissions": [],      // 权限标识列表
    "matchmakerStatus": "NONE"  // NONE/APPLYING/PASS/REJECT
  }
}

2. 提交红娘申请接口

  • 权限:仅 普通用户可调用
  • 逻辑:校验状态 → 新增申请单 → 用户状态改为 APPLYING
  • 不可重复提交

3. 管理员 - 获取待审核列表

  • 权限:仅 管理员
  • 返回:所有 APPLYING 状态的申请单

4. 管理员 - 审核红娘申请(核心)

入参:申请 ID、审核状态、拒绝理由(拒绝时必填)

逻辑:

  1. 校验管理员权限
  2. 同意:更新申请单状态 → 更新用户 role=MATCHMAKERmatchmakerStatus=PASS
  3. 拒绝:更新申请单 + 填写拒绝理由 → 更新用户 matchmakerStatus=REJECT + 记录拒绝理由

5. 红娘中心 - 获取我的牵线人员

  • 权限:仅 红娘
  • 逻辑:根据当前红娘 ID 查询其管理的用户

四、前端权限控制逻辑(最关键)

1. 【我的页面】按钮显示判断

javascript 复制代码
// 从登录信息中取
const { role, matchmakerStatus } = userInfo

// 规则:
if (role === 'MATCHMAKER') {
  // 红娘 → 显示【红娘中心管理】
  return <Button onClick={() => navigate('/matchmaker/home')}>红娘中心管理</Button>
} else {
  // 普通用户 → 显示【申请成为红娘】
  return <Button onClick={() => navigate('/matchmaker/apply')}>申请成为红娘</Button>
}

2. 页面路由权限拦截

  • /matchmaker/apply(申请页)→ 仅 普通用户可进
  • /matchmaker/home(红娘中心)→ 仅 红娘可进
  • /admin/matchmaker-audit(审核页)→ 仅 管理员可进

3. 申请提交页面

表单字段:

  • 真实姓名、年龄、性别、微信、地区、申请理由(必填)
  • 提交后状态变为:审核中

五、完整状态流转图

sql 复制代码
未申请(NONE)  
    ↓ 提交申请  
审核中(APPLYING)  
    ↓ 管理员审核  
├── 同意 → 红娘(PASS + 角色MATCHMAKER)  
└── 拒绝 → 已拒绝(REJECT + 拒绝理由)

六、权限控制核心规则(总结)

  1. 角色控制

    • 管理员:审核红娘申请
    • 红娘:进入红娘中心、管理牵线人员
    • 普通用户:查看个人中心、提交红娘申请
  2. 状态控制

    • 只有 matchmakerStatus=NONE/REJECT 才能重新申请
    • 审核中不可重复提交
    • 拒绝后可查看拒绝理由并重新提交申请
  3. 按钮 / 菜单 / 路由全部受控 前端不依赖写死的路径,全部根据 角色 + 状态动态渲染。


七、管理员审核提醒方案

  1. 后台申请提交后:
    • 新增一条 系统消息用户xxx提交了红娘申请,请及时审核
  2. 管理员登录后:
    • 右上角显示小红点 / 待办数量
    • 点击直接进入审核列表

总结

我给你的是 可直接落地开发的完整方案:

  1. 表结构:用户表 + 红娘申请表(包含所有状态和字段)
  2. 接口:登录、申请、审核、列表、红娘中心
  3. 前端:按钮显示逻辑 + 路由权限 + 表单
  4. 状态流:未申请 → 审核中 → 通过 / 拒绝
  5. 权限:基于 角色 + 状态双重控制,安全且符合业务

28、你是如何实现黑名单功能的?

我们当前FNBK社交项目,目前暂未开发用户黑名单拉黑功能。

现有陌生人聊天限制规则为:双方未互相关注时,陌生人仅可发送有限条数消息,超出后无法继续发起会话,以此规避陌生用户过度骚扰的问题。

但针对 已产生会话、聊天过程中言语反感、不想继续沟通的精准拉黑场景,目前确实存在功能缺失,用户无法主动拉黑指定联系人、屏蔽对方消息。

这也是我们项目后续迭代的优化点,后续会规划接入 黑名单功能:支持用户一键拉黑指定用户,拉黑后双向屏蔽会话、禁止对方发消息、禁止查看个人资料,从根本上解决用户主动不想沟通、被打扰的需求,完善产品体验。


29、你是如何实现拖拽式的问卷创建?

我们项目里的 拖拽式问卷创建 ,是基于 Vue.Draggable 这个拖拽组件实现的。

整体分为 左侧题型面板、中间编辑区域、右侧配置面板三部分:

  1. 左侧题型容器 我把它配置成 可向外拖拽、但不允许往内部拖入的纯源头容器,只用来存放 单选、多选、填空、评分 等 标准题型。

  2. 中间问卷编辑区这是核心拖拽区域,支持:

    • 从左侧把题型 拖入,自动生成对应题目组件
    • 在内部 自由上下拖拽排序
    • 支持复制、删除、清空 等操作
  3. 拖拽事件处理 我通过 Draggable 提供的生命周期函数,比如

    • onStart:记录拖拽开始信息
    • onEnd:拖拽结束后获取拖拽项、位置、目标容器,拿到这些数据后,动态往问卷列表里 push 对应题目结构,实时更新视图。
  4. 右侧属性配置面板 点击中间区域任意题目,右侧会 自动切换显示该题型的配置项,比如修改题目标题、添加 / 删除选项、设置是否必填、默认值等,配置后实时同步到题目数据里。

  5. 数据结构整个问卷最终会生成一个标准 JSON 数组,包含所有题目类型、标题、选项、配置,方便 后端保存 和 后续渲染 答卷页面。


精简版(15 秒快速回答)

我们使用 Vue.Draggable 实现拖拽问卷。左侧是 题型面板,配置为只能向外拖拽;中间是 编辑区,支持拖入题目、自由排序;通过拖拽结束事件获取数据,自动生成题目;点击题目后,右侧出现 配置面板,可修改标题、选项、是否必填等;最终生成结构化 JSON 用于保存 和 渲染。

30、如何实现富文本的编辑?

我们项目中富文本编辑,采用 wangEditor开源编辑器实现。

首先安装并引入插件,在项目中进行 二次封装,统一管理编辑器 实例 与 配置。

原生编辑器部分功能不会默认展示,比如 图片上传、附件上传等,需要手动在配置项中开启对应菜单、注册上传能力。

同时自定义配置上传请求地址、请求头、文件大小、格式校验等规则,对接后端文件上传接口,实现图片、附件的本地上传与回显。

编辑器基础编辑能力直接开箱即用,支持文本加粗、字体大小、文字颜色、段落排版、列表、引用等常规富文本操作。

编辑完成后直接获取编辑器 HTML 内容进行提交存储,编辑回显时再把 HTML 内容赋值给编辑器,完成数据 双向渲染,满足业务图文编辑需求。


精简短句版(面试快速口述)

项目基于 wangEditor 实现富文本编辑,对组件进行二次封装。手动配置开启图片、附件上传菜单,对接后端上传接口,自定义文件校验规则。同时支持文字样式、排版等基础编辑功能,通过 HTML 格式存取内容,实现内容编辑与回显。

31、你们是如何实现首页数据大屏的?

我们项目首页数据大屏,整体基于 ECharts + 自适应布局 开发实现。

首先由产品和 UI 敲定整体布局、模块划分以及视觉交互效果,采用 上下左右分区 + 中间核心内容的经典大屏布局结构。

布局层面采用弹性布局搭配适配方案,做了 窗口缩放自适应处理,浏览器放大、缩小不会出现内容挤压、遮挡、布局错乱的问题,保证核心区域展示稳定。

图表部分全部使用 ECharts 实现:

左上角为数据统计卡片,展示问卷总量、各类核心数字指标;

右上角使用 环形饼图,可视化展示已答、未答问卷的占比,同时配置了加载动画、旋转动效提升大屏视觉效果;

左下角统计已发布问卷总数;

右下角通过柱状图 / 排行榜形式,展示各部门问卷填写数据排名。

中间核心区域为业务列表,展示已答、未答问卷事项,支持分类切换查看。

同时对图表样式、配色、文字大小、图例、提示框做了统一定制化优化,适配大屏展示风格,所有统计数据通过接口请求后端实时获取,渲染到页面,完成整体数据可视化大屏需求。


精简口述版(简短高效,面试快速回答)

首页数据大屏采用 ECharts 做数据可视化,结合弹性自适应布局。

页面分为四大统计区块和中间业务列表区,左右上下分别放置指标卡片、环形占比图、发布数量、部门填写排行榜。

单独处理了浏览器缩放适配,避免布局错乱;自定义 ECharts 样式和动画,接口拉取后端实时统计数据,实现问卷相关数据可视化展示。

32、如何实现问卷答案暂存的?

目前我们项目 暂未开发问卷答案自动暂存功能

现有逻辑为:用户填写完成点击提交时,前端先做全局校验,如果存在必填题目未作答,会弹出提示拦截提交,保证完整作答后才能提交成功。

如果后续需要迭代实现 问卷答案暂存,整体有两套落地方案,前后端结合实现:

  1. 手动 / 主动暂存增加「保存草稿」按钮,用户主动点击后,前端收集当前已填写的所有题目答案,调用后端暂存接口,将答题数据存入答题草稿表,关联用户 ID、问卷 ID,记录作答进度。

  2. 定时自动暂存通过定时器监听用户输入、选项切换等操作,间隔一段时间自动调用保存接口,实时同步作答内容到后端,持续保存答题进度。

  3. 双重兜底防丢失 服务端持久化存储为主,同时搭配前端 localStorage 本地缓存做兜底,避免断网、刷新页面、意外关闭页面导致答案丢失。下次用户再次进入同一份问卷时,前端先请求后端草稿数据,有暂存记录就自动回显题目答案,继续上次作答进度,实现断点续填


精简口述短版

当前项目没有做问卷答案暂存,仅在提交时校验必填项,未完成则拦截提交。

后续如需实现,可以通过 手动保存 或 定时自动保存,把作答数据提交后端存入草稿表,同时搭配本地缓存兜底;再次进入页面回显草稿数据,实现答题进度 暂存 与 续填。

33、你们是如何实现答题时长统计的?

目前项目中 暂时没有开发答题时长统计功能

如果要实现该功能,整体采用 前端触发、后端计算的方案来做:

用户从问卷列表进入答题页面时,在请求问卷详情接口的同时,前端携带 开始标识 ,后端记录 答题开始时间

当用户完成所有题目、点击提交问卷时,在提交接口中,后端自动记录 答题结束时间

后端通过 结束时间 减去 开始时间,自动计算出本次完整 答题时长,持久化存入 答题记录表单。

后续在需要展示数据的页面,后端直接返回答题时长字段,前端直接渲染展示即可。

同时也可以做前端兜底方案,页面初始化开启计时器,实时记录本地答题时长,用于页面实时展示,最终以后端统计时间为准,保证数据准确。


精简口述版

当前项目未做答题时长统计。

落地思路是:进入答题页后端记录开始时间,提交问卷时记录结束时间,由后端计算时间差并保存;前端直接展示后端返回的答题时长数据,保证统计数据真实可靠。

34、你们有问卷打印功能吗?

目前项目 没有开发问卷在线打印功能

但实现了 答题数据导出能力,管理员可以将用户的问卷作答记录、填写情况,一键导出为 Excel 文件,方便线下查看、汇总统计与归档使用。

如果后续需要新增打印功能,可以借助 浏览器原生 window.print() 方法,定制打印样式,隐藏无关布局,只展示问卷题目与内容,快速实现页面打印需求。

精简口述版

项目暂无问卷打印功能,现有替代方案是支持导出答题数据为 Excel,满足数据汇总查看需求。如需后续迭代,可调用 浏览器原生打印 API 快速实现 打印功能

35、你们如何实现答题路径分析?

项目中 没有单独做复杂的答题路径分析 ,主要通过 后端状态字段 + 前端路由拦截来控制用户的访问路径和答题权限。

首先,用户点击问卷或通过链接进入答题页时,会先请求后端问卷详情接口。

后端会根据用户账号、问卷作答记录,返回对应的状态标识:未作答、已作答、无权限等。

  1. 未答状态 正常渲染问卷题目,允许用户正常填写、提交答案。

  2. 已答状态 后端返回 已作答标识 ,前端拿到状态后做判断,自动重定向到 提示 页面,展示「您已答过无需作答」文案,同时提供返回列表按钮,防止重复答题。

  3. 无权限 / 未登录 校验 Token 登录令牌,未登录或身份非法时,直接拦截路由,重定向到登录页,杜绝非法访问。

整体通过 接口状态校验 + 前端路由控制,限制用户访问路径,规范答题流程,保证一份问卷每人仅能作答一次,数据不会重复冗余。


精简口述版

我们没有单独开发答题路径分析模块,依靠后端返回的问卷状态,结合前端路由拦截实现管控。进入答题页先请求接口,未答正常展示问卷;已答则跳转提示页禁止重复作答;未登录无 Token 直接重定向登录页,以此规范用户答题访问路径。


36、NGSJZT你是如何实现路由鉴权的?

我直接给你一套 可直接落地、企业级、数据中台通用的路由鉴权方案,完全匹配你说的场景:

  • 后端返回 权限路由列表
  • 前端 动态加载路由
  • 无 token → 重定向登录页
  • 无权限 → 重定向 404
  • 普通用户 / 管理员按权限访问页面

这套方案是 Vue3 + Vue Router + Pinia 最标准的实现,React 逻辑完全一样,只是 API 不同。


一、整体设计思路(核心逻辑)

  1. 登录成功 :后端返回 token + 当前用户可访问的 路由权限列表
  2. 前端存储:把 token 存在 localStorage,权限路由存在 Pinia
  3. 路由守卫 :全局监听路由跳转,做 3 层判断:
    • 有无 token → 无 → 跳登录
    • 有无权限路由 → 无 → 从后端拉取
    • 跳转目标是否在权限路由里 → 无 → 跳 404
  4. 动态添加路由 :只把用户有权限 的路由挂载router
  5. 菜单渲染:根据权限路由列表动态生成侧边栏

二、完整代码实现(可直接复制)

1. 路由基础结构(router/index.js)

javascript 复制代码
import { createRouter, createWebHistory } from 'vue-router'
import useUserStore from '@/store/user'

// 固定路由(所有人都能访问,不需要权限)
const constantRoutes = [
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/login/index.vue'),
    meta: { hidden: true }
  },
  {
    path: '/404',
    name: '404',
    component: () => import('@/views/error/404.vue'),
    meta: { hidden: true }
  },
  {
    path: '/',
    redirect: '/dashboard' // 默认首页
  }
]

// 需要权限的路由(基础模板,真正的从后端返回)
export const asyncRoutes = [
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import('@/views/dashboard/index.vue'),
    meta: { title: '数据概览', icon: 'dashboard' }
  },
  {
    path: '/data-manage',
    name: 'DataManage',
    component: () => import('@/views/data-manage/index.vue'),
    meta: { title: '数据管理', icon: 'database' }
  },
  {
    path: '/system',
    name: 'System',
    component: () => import('@/views/system/index.vue'),
    meta: { title: '系统管理', icon: 'setting', roles: ['admin'] } // 仅管理员
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes: constantRoutes
})

// 重置路由(退出登录用)
export function resetRouter() {
  const newRouter = createRouter({
    history: createWebHistory(),
    routes: constantRoutes
  })
  router.matcher = newRouter.matcher
}

export default router

2. Pinia 状态管理(store/user.js)

负责:存 token、存权限路由、拉取权限、动态添加路由

javascript 复制代码
import { defineStore } from 'pinia'
import router, { asyncRoutes, resetRouter } from '@/router'
import { getAuthRouters } from '@/api/user' // 后端接口:获取权限路由

const useUserStore = defineStore('user', {
  state: () => ({
    token: localStorage.getItem('token') || '',
    routes: [], // 最终展示的路由(权限过滤后)
    addRoutes: [] // 动态添加的路由
  }),

  actions: {
    // 设置 token
    setToken(token) {
      this.token = token
      localStorage.setItem('token', token)
    },

    // 退出登录
    logout() {
      this.token = ''
      this.routes = []
      this.addRoutes = []
      localStorage.removeItem('token')
      resetRouter()
    },

    // 后端返回的权限路由 → 过滤 + 生成可访问路由
    async generateRoutes() {
      // 1. 从后端拉取当前用户权限列表
      const res = await getAuthRouters()
      const permissionRoutes = res.data // 例如:['Dashboard', 'DataManage']

      // 2. 过滤出用户有权限的异步路由(需要权限的路由,res.data)
      const accessedRoutes = filterAsyncRoutes(asyncRoutes, permissionRoutes)

      // 3. 保存到 store
      this.addRoutes = accessedRoutes
      this.routes = constantRoutes.concat(accessedRoutes)

      // 4. 动态添加到路由系统
      accessedRoutes.forEach(route => {
        router.addRoute(route)
      })

      // 5. 最后加 404 兜底
      router.addRoute({
        path: '/:pathMatch(.*)*',
        redirect: '/404',
        meta: { hidden: true }
      })

      return accessedRoutes
    }
  }
})

// 工具函数:根据后端权限过滤路由(需要权限的路由,例如:['Dashboard', 'DataManage'])
function filterAsyncRoutes(routes, permissionRoutes) {
  const res = []
  routes.forEach(route => {
    const tmp = { ...route } // { ...单个路由对象 } ✅ 正常浅拷贝
    // 如果后端返回的权限包含当前路由 name → 保留(['Dashboard', 'DataManage', 'System'])
    if (permissionRoutes.includes(tmp.name)) {
      // 没有子路由的路由:tmp.children → undefined
      // 有子路由的路由:tmp.children → 递归过滤后的子路由数组
      if (tmp.children) {
        tmp.children = filterAsyncRoutes(tmp.children, permissionRoutes)
        // 关键:如果子路由过滤后为空,就不保留这个父路由
        if (tmp.children.length === 0) return
      }
      res.push(tmp)
    }
  })
  return res
}

export default useUserStore

3. 全局路由守卫(permission.js)

最核心的鉴权逻辑,判断跳转、token、权限

javascript 复制代码
import router from './router'
import useUserStore from '@/store/user'

// 白名单:不需要登录就能访问
const whiteList = ['/login']

router.beforeEach(async (to, from, next) => {
  const userStore = useUserStore()
  const hasToken = userStore.token

  // 1. 判断有没有 token
  if (hasToken) {
    if (to.path === '/login') {
      // 已登录,访问登录页 → 重定向到首页
      next('/dashboard')
    } else {
      // 判断是否已经获取过权限路由(动态添加的路由)
      const hasRoutes = userStore.addRoutes && userStore.addRoutes.length > 0
      if (hasRoutes) {
        next() // 有权限 → 放行
      } else {
        try {
          // 没拉取权限 → 拉取并动态添加路由
          await userStore.generateRoutes()
          next({ ...to, replace: true }) // 确保路由加载完成
        } catch (error) {
          // 异常:token 过期 / 异常 → 退出重登
          await userStore.logout()
          next(`/login?redirect=${to.path}`)
        }
      }
    }
  } else {
    // 2. 无 token
    if (whiteList.includes(to.path)) {
      next() // 白名单放行
    } else {
      // 其他页面 → 重定向到登录
      next(`/login?redirect=${to.path}`)
    }
  }
})

4. 入口文件挂载守卫(main.js)
import './permission'; // 关键:挂载路由鉴权守卫

javascript 复制代码
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

// 关键:挂载路由鉴权守卫
import './permission'

const app = createApp(App)
app.use(store).use(router).mount('#app')

三、后端返回格式示例(你直接按这个给前端)

javascript 复制代码
{
  "code": 200,
  "data": [
    "Dashboard",
    "DataManage"
    // "System" 管理员才会返回
  ]
}

规则 :返回的是路由的 name 字段,前端根据 name 做匹配。


四、最终效果(完全满足你的需求)

  1. 未登录 / 无 token 访问任何页面 → 自动跳转到 /login
  2. 已登录,但访问无权限页面 自动重定向到 /404
  3. 普通用户只能看到并访问:数据概览、数据管理
  4. 管理员能看到并访问:数据概览、数据管理、系统管理
  5. 路由完全动态 没有权限的路由 根本不会挂载,安全性极高

五、这套方案的优势(数据中台必备)

  • 安全:无权限路由不挂载,无法通过 URL 强行访问
  • 灵活:后端完全控制权限,前端无需改代码
  • 标准:企业中台最通用的权限架构
  • 可扩展:支持按钮级权限、多级路由、路由守卫

总结

  1. 核心靠 路由守卫做全局鉴权判断
  2. 动态添加路由实现权限控制
  3. Store统一管理权限与 token
  4. 无 token → 登录页;无权限 → 404 页
  5. 权限完全由 后端返回的路由名称列表控制

37、如何实现图表的响应式适配?

一、面试标准回答(直接背)

我们项目中对 ECharts 做了二次封装图表通用组件,统一管理基础配置与自适应逻辑。

  1. 尺寸自适应mounted 挂载完成后,监听浏览器窗口 resize 事件,统一调用 ECharts 实例的 resize() 方法 ,实现窗口缩放、浏览器大小变化时图表自动适配容器。
  2. 数据响应更新 通过 watch 深度监听图表 option 配置变化,搭配 nextTick 确保 DOM 更新完成后,再执行 setOption 刷新图表,保证数据、样式动态变更实时生效
  3. 销毁优化 在组件 unmounted 生命周期中,移除 resize 监听、销毁 ECharts 实例,防止内存泄漏、页面卡顿。

二、精简版技术细节(面试官追问用)

  1. 监听全局 window.resize节流处理避免频繁触发,提升性能;
  2. 容器采用 百分比 / 弹性布局,不给固定宽高,让图表容器先自适应;
  3. 深度监听 option,区分静态配置 和 动态业务数据变更,按需刷新;
  4. 页面销毁时解绑事件 + dispose 销毁实例,优化中台多页面切换性能。

三、Vue3 完整极简代码(和你话术完全对应)

javascript 复制代码
<template>
  <div class="chart-box" ref="chartRef"></div>
</template>

<script setup>
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
import * as echarts from 'echarts'

const chartRef = ref(null)
let myChart = null
// 外部传入的图表配置
const props = defineProps(['option'])

// 1. 监听option变化,更新图表
watch(() => props.option, () => {
  nextTick(() => {
    myChart?.setOption(props.option)
  })
}, { deep: true })

// 2. 挂载:初始化图表 + 绑定resize
onMounted(() => {
  myChart = echarts.init(chartRef.value)
  myChart.setOption(props.option)

  // 窗口缩放自适应
  window.addEventListener('resize', handleResize)
})

// 图表resize
const handleResize = () => {
  myChart?.resize()
}

// 3. 销毁:解绑事件 + 释放实例,防内存泄漏
onUnmounted(() => {
  window.removeEventListener('resize', handleResize)
  myChart?.dispose()
  myChart = null
})
</script>

<style scoped>
.chart-box {
  width: 100%;
  height: 100%;
}
</style>

四、补充加分点(面试官加分项)

  1. 增加 节流函数 包裹 resize,防止频繁执行造成页面卡顿;
  2. 适配侧边栏收缩 / 展开:侧边栏变化时,手动调用一次 resize
  3. 多图表页面统一封装全局 resize 管理,避免全局绑定多个监听。

38、你是如何使用Pinia全局管理状态的?

一、目录结构(和你完全一致)

bash 复制代码
src/
└── stores/           # 独立存放所有 Pinia 状态
    ├── home.ts       # 首页模块状态
    ├── list.ts       # 列表页模块状态
    └── common.ts     # 全局公共状态(核心)

二、核心思想

  1. 按业务模块拆分:一个页面 / 功能对应一个 store,互不污染
  2. 全局状态收敛 :所有全局共享数据只放在 common.ts
  3. 统一出口:组件只从对应 store 取数据,不分散管理

三、具体代码实现(可直接复制使用)

1. 公共状态:stores/common.ts(你说的 token、loading、用户信息)

javascript 复制代码
import { defineStore } from 'pinia'

// 定义公共 store,id 唯一
export const useCommonStore = defineStore('common', {
  // 状态:全局共享数据
  state: () => ({
    token: '',                // 登录令牌
    userInfo: {               // 用户信息
      name: '',
      gender: '',
      dept: '',
      jobNumber: '',
      phone: ''
    },
    loading: false            // 全局加载状态
  }),

  // 修改状态的方法(同步)
  actions: {
    // 设置 token
    setToken(token: string) {
      this.token = token
    },
    // 设置用户信息
    setUserInfo(info: any) {
      this.userInfo = { ...this.userInfo, ...info }
    },
    // 控制 loading
    setLoading(status: boolean) {
      this.loading = status
    },
    // 退出登录清空
    logout() {
      this.token = ''
      this.userInfo = {} as any
    }
  },

  // 计算属性:简化数据获取
  getters: {
    // 判断是否登录
    isLogin: (state) => !!state.token
  }
})

2. 业务模块:stores/home.ts

javascript 复制代码
import { defineStore } from 'pinia'

export const useHomeStore = defineStore('home', {
  state: () => ({
    bannerList: [],   // 首页轮播图
    hotList: []       // 首页热门数据
  }),
  actions: {
    setBanner(list: any[]) {
      this.bannerList = list
    }
  }
})

3. 列表模块:stores/list.ts

javascript 复制代码
import { defineStore } from 'pinia'

export const useListStore = defineStore('list', {
  state: () => ({
    tableData: [],    // 列表数据
    page: 1,          // 页码
    total: 0          // 总条数
  }),
  actions: {
    setTableData(data: any[]) {
      this.tableData = data
    }
  }
})

四、在 Vue 组件中使用(最常用方式)

1. 读取 / 修改全局公共状态

javascript 复制代码
<script setup lang="ts">
// 引入公共 store
import { useCommonStore } from '@/stores/common'
const commonStore = useCommonStore()

// 直接使用状态
console.log(commonStore.token)
console.log(commonStore.userInfo.name)

// 调用方法修改
commonStore.setToken('xxxx-xxxx')
commonStore.setLoading(true)
</script>

<template>
  <!-- 模板直接使用 -->
  <div v-if="commonStore.loading">加载中...</div>
  <div>欢迎:{{ commonStore.userInfo.name }}</div>
</template>

2. 使用业务模块状态

javascript 复制代码
<script setup>
import { useHomeStore } from '@/stores/home'
const homeStore = useHomeStore()

// 获取首页数据
console.log(homeStore.bannerList)
</script>

五、我常用的进阶优化(和你的结构兼容)

1. 状态持久化(刷新页面不丢失 token、用户信息)

安装插件:

bash 复制代码
npm i pinia-plugin-persistedstate

common.ts 开启持久化:

javascript 复制代码
export const useCommonStore = defineStore('common', {
  // ...其他代码
  persist: true  // 自动存入 localStorage
})

2. 解构状态(保持响应式)

javascript 复制代码
// 正确写法:解构后依然响应式
import { storeToRefs } from 'pinia'
const { token, userInfo } = storeToRefs(commonStore)

3. 统一注册 Pinia(main.ts)

javascript 复制代码
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
app.use(pinia)

六、和你的思路完全对齐的总结

我就是 严格按照你的方式使用 Pinia:

  1. 独立 stores 目录:统一管理全局状态
  2. 模块化拆分home.ts / list.ts / common.ts
  3. 公共状态收敛tokenloading用户信息 全部放在 common.ts
  4. 组件按需引入:用哪个模块就引入哪个 store

这套方案 简洁、清晰、易维护,是中大型 Vue3 项目的标准用法!


总结

  1. 目录结构:独立 stores + 业务模块拆分,和你的设计完全一致
  2. 公共状态:common.ts 管理 token用户信息loading 等全局数据
  3. 使用方式:组件引入 → 读取 / 修改 → 响应式更新
  4. 工程化:支持持久化、模块化、可维护性极强

39、如何实现图表数据缓存?

一、先搞懂三种缓存的本质区别

方案 缓存层级 适用场景 优缺点
keep-alive 组件实例层 路由 / 组件切换,不想重复渲染 优点:实现简单;缺点:只缓存组件状态,不解决请求重复发、数据过期问题
Pinia 全局状态层 多组件共享的图表数据 优点:可控性强;缺点:需要手动写缓存 key、过期逻辑,重复造轮子
Vue Query 请求 / 数据层 服务器状态(图表、列表这类 API 数据) 优点:内置缓存、过期、后台刷新;缺点:需要学习 API,但代码量最少

二、三种方案的具体实现(图表数据场景)

1. keep-alive 实现:组件级缓存(最简单,但不解决请求优化)

适用场景:从列表页跳转到图表页,返回时不想重新加载组件和数据

javascript 复制代码
<!-- App.vue / 路由出口 -->
<router-view v-slot="{ Component }">
  <keep-alive include="ChartPage">
    <component :is="Component" />
  </keep-alive>
</router-view>

<!-- ChartPage.vue -->
<script setup>
import { onActivated, onDeactivated } from 'vue'
import { useChartStore } from '@/stores/chart'

const chartStore = useChartStore()

// 只有组件首次挂载时请求数据
onMounted(() => {
  if (!chartStore.chartData) {
    chartStore.fetchChartData()
  }
})

// 组件被激活时(从缓存恢复)可以做数据刷新判断
onActivated(() => {
  // 比如超过5分钟再刷新一次数据
  if (Date.now() - chartStore.lastFetchTime > 5 * 60 * 1000) {
    chartStore.fetchChartData()
  }
})
</script>

⚠️ 注意:keep-alive 只缓存 组件实例,如果不配合状态管理,每次进入组件还是会重新执行请求。


2. Pinia 实现:手动写缓存逻辑(你说的接口 + key 方式)

适用场景:不想引入新库,自己可控的缓存方案

javascript 复制代码
// stores/chart.ts
import { defineStore } from 'pinia'
import { fetchChartApi } from '@/api/chart'

export const useChartStore = defineStore('chart', {
  state: () => ({
    chartCache: new Map<string, { data: any; expire: number }>(), // key: 接口+参数拼接
    cacheTTL: 5 * 60 * 1000, // 5分钟过期
    loading: false
  }),

  actions: {
    async getChartData(params: { type: string; timeRange: string }) {
      // 1. 生成唯一key(接口地址+参数)
      const cacheKey = `/api/chart?${new URLSearchParams(params).toString()}`
      
      // 2. 检查缓存是否有效
      const cached = this.chartCache.get(cacheKey)
      if (cached && Date.now() < cached.expire) {
        return cached.data // 直接返回缓存,不发请求
      }

      // 3. 缓存无效,发起请求
      this.loading = true
      try {
        const res = await fetchChartApi(params)
        // 4. 更新缓存
        this.chartCache.set(cacheKey, {
          data: res.data,
          expire: Date.now() + this.cacheTTL
        })
        return res.data
      } finally {
        this.loading = false
      }
    },

    // 手动清除缓存(比如用户主动刷新)
    clearChartCache() {
      this.chartCache.clear()
    }
  }
})

组件中使用

javascript 复制代码
<script setup>
import { useChartStore } from '@/stores/chart'
const chartStore = useChartStore()

const loadData = async () => {
  const data = await chartStore.getChartData({ type: 'sales', timeRange: '7d' })
  // 渲染图表
}
loadData()
</script>

3. Vue Query 实现:开箱即用的服务器状态缓存(最推荐)

适用场景:所有从 API 获取的图表数据,完全解决你提到的所有痛点

第一步:安装

bash 复制代码
npm install @tanstack/vue-query

第二步:全局配置(main.ts)

javascript 复制代码
import { VueQueryPlugin } from '@tanstack/vue-query'

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000, // 数据5分钟内为"新鲜",不重复请求
      cacheTime: 30 * 60 * 1000, // 缓存保留30分钟
      refetchOnWindowFocus: false, // 窗口聚焦时不自动刷新(可选)
      retry: 1 // 失败重试1次
    }
  }
})

app.use(VueQueryPlugin, { queryClient })

第三步:图表数据缓存(组件内直接用)

javascript 复制代码
<script setup>
import { useQuery } from '@tanstack/vue-query'
import { fetchChartApi } from '@/api/chart'

// 1. 定义请求函数
const fetchChartData = async (params) => {
  const res = await fetchChartApi(params)
  return res.data
}

// 2. 使用useQuery,自动缓存、过期管理
const { data, isLoading, error } = useQuery({
  // queryKey 作为缓存key,自动去重相同请求
  queryKey: ['chart', 'sales', { type: 'sales', timeRange: '7d' }],
  queryFn: () => fetchChartData({ type: 'sales', timeRange: '7d' }),
  // 可以单独配置这个查询的缓存策略
  staleTime: 10 * 60 * 1000, // 这个图表数据10分钟内不刷新
  cacheTime: 60 * 60 * 1000 // 缓存保留1小时
})
</script>

<template>
  <div v-if="isLoading">加载中...</div>
  <div v-if="error">加载失败</div>
  <ECharts :option="data" />
</template>

关键特性说明(解决你说的所有痛点)

  • 自动缓存 :相同 queryKey 的请求,会直接从缓存拿数据,不发请求
  • 后台刷新:即使数据显示缓存,也会在后台自动请求更新(stale-while-revalidate 策略)
  • 请求去重:短时间内多个组件请求同一个图表,只会发一次请求
  • 过期控制staleTime 控制数据多久算过期,cacheTime 控制缓存多久清理

三、三种方案怎么选?(给你一个决策树)

  1. 如果只是想简单缓存组件状态,不想处理请求优化 → 用 keep-alive
  2. 如果不想引入新库,想自己掌控缓存逻辑 → 用 Pinia 手动实现(但要自己写 key、过期、刷新逻辑)
  3. 如果想彻底解决服务器数据的缓存、同步、过期问题 → 用 Vue Query(最适合你现在的 Vue3 项目)

四、推荐的组合拳(性能最优)

实际项目中可以组合使用:

  • keep-alive 缓存图表组件实例(避免重复渲染)
  • Vue Query 管理图表的 API 数据缓存(避免重复请求、处理过期)
  • (可选)Pinia 只存纯客户端状态(如当前选中的时间范围、图表类型)

40、你是如何实现用户行为埋点的?

封装公共 track () → 路由守卫自动上报 → 按钮 / 图片手动上报

我实现用户行为埋点的完整方案

一、整体思路(和你说的完全一致)

  1. 封装一个公共工具函数 track() ------ 统一处理埋点数据、发送请求
  2. 页面访问埋点 ------ 在 路由守卫 里自动调用 track()
  3. 点击 / 交互埋点 ------ 在按钮、图片、卡片等地方 手动调用 track()
  4. 统一数据格式 ------ 事件名、用户 ID、页面、时间、参数

二、直接给你可复制的完整代码

1. 新建埋点工具:utils/track.ts

javascript 复制代码
// 埋点公共方法
export function track(eventName: string, params = {}) {
  try {
    // 1. 基础公共信息(所有埋点都带)
    const baseInfo = {
      event: eventName,           // 事件名:page_view / click_btn / click_img
      userId: localStorage.getItem('token') || '', // 用户ID
      pagePath: location.pathname,// 当前页面路径
      pageTitle: document.title,  // 页面标题
      timestamp: Date.now(),      // 时间戳
      userAgent: navigator.userAgent // 设备信息
    }

    // 2. 合并自定义参数
    const data = { ...baseInfo, ...params }

    // 3. 上报埋点(sendBeacon 页面关闭也能上报)
    if (navigator.sendBeacon) {
      // 异步上报,不阻塞页面
      navigator.sendBeacon('/api/track', JSON.stringify(data))
    } else {
      // 降级方案
      fetch('/api/track', {
        method: 'POST',
        body: JSON.stringify(data),
        keepalive: true
      })
    }
  } catch (err) {
    console.log('埋点上报失败', err)
  }
}

2. 路由守卫自动上报页面访问(你说的核心)

router/index.ts

javascript 复制代码
import { track } from '@/utils/track'

router.beforeEach((to, from, next) => {
  // 页面访问埋点:自动上报
  track('page_view', {
    page: to.path,
    pageName: to.meta.title || ''
  })
  next()
})

效果:进入任何页面,自动上报埋点,不用手动写!


3. 按钮 / 图片 / 卡片点击埋点(手动调用)

任意 Vue 组件:

javascript 复制代码
<template>
  <!-- 按钮点击 -->
  <button @click="handleClick">提交</button>

  <!-- 图片点击 -->
  <img @click="trackImg" src="xxx.png" />
</template>

<script setup>
import { track } from '@/utils/track'

// 按钮点击
const handleClick = () => {
  track('click_button', {
    buttonName: '提交',
    page: '用户中心'
  })
}

// 图片点击
const trackImg = () => {
  track('click_image', {
    imageName: '轮播图1',
    position: '首页顶部'
  })
}
</script>

三、我在企业项目里的埋点规范(你可以直接用)

1. 事件名统一规范

  • page_view → 页面访问
  • click_button → 按钮点击
  • click_image → 图片点击
  • click_link → 链接点击
  • search → 搜索
  • submit_form → 表单提交
  • stay_time → 页面停留时长

2. 上报方式为什么用 sendBeacon?

  • 页面关闭、刷新也能上报
  • 不阻塞页面
  • 不会丢失埋点(比 fetch /axios 更稳)

3. 埋点数据包含什么?

  • 用户 ID
  • 事件类型
  • 页面路径
  • 点击内容 / 位置
  • 时间戳
  • 设备信息
  • 自定义参数

四、总结(和你说的一模一样)

我实现埋点就是 3 步

  1. 封装公共 track () 函数 统一上报
  2. 路由守卫自动上报页面
  3. 组件里手动触发点击 / 交互埋点

这就是 Vue2/Vue3/React 通用的标准埋点方案

41、你如何实现自定义图表配置(如颜色、类型)?

答案:提供配置面板 ,用户可修改图表类型(柱状图/折线图)、颜色等,配置保存到后端,下次加载恢复。


我如何实现自定义图表配置(标准回答 + 实现)

核心思路(和你说的完全一致)

  1. 提供可视化配置面板:让用户选择图表类型(柱状图 / 折线图)、自定义颜色
  2. 图表类型 :使用 Tab 切换,通过 v-model 绑定当前类型
  3. 颜色配置 :使用颜色选择器,双向绑定,通过 setOption 实时更新图表
  4. 父子组件通信:配置面板(父)→ 图表组件(子)传递颜色、类型参数
  5. 配置持久化:将用户自定义配置保存到后端,下次进入页面自动恢复

完整实现逻辑(可直接背、直接用)

1. 数据结构(存储用户图表配置)

javascript 复制代码
const chartConfig = reactive({
  type: 'bar',      // 图表类型 bar / line
  colors: [         // 自定义颜色组
    '#1890ff',
    '#2fc25b',
    '#facc14',
    '#223273'
  ],
  title: '自定义图表'
})

2. 图表类型 Tab 切换(v-model 绑定)

javascript 复制代码
<el-tabs v-model="chartConfig.type">
  <el-tab label="柱状图" name="bar" />
  <el-tab label="折线图" name="line" />
</el-tabs>

切换时,自动更新图表类型


3. 颜色配置(双向绑定 + setOption 更新)

javascript 复制代码
<el-color-picker
  v-for="(color, index) in chartConfig.colors"
  v-model="chartConfig.colors[index]"
  @change="updateChart"
/>

颜色改变后,调用 chart.setOption() 实时生效。


4. 父子组件传参(配置 → 图表组件)

javascript 复制代码
<!-- 父组件 -->
<ChartComponent
  :type="chartConfig.type"
  :colors="chartConfig.colors"
/>

<!-- 子组件接收 props -->
const props = defineProps({
  type: String,
  colors: Array
})

5. 图表实时更新(核心:setOption)

javascript 复制代码
function updateChart() {
  chart.setOption({
    xAxis: { ... },
    yAxis: { ... },
    series: [{
      type: props.type,  // 动态类型
      itemStyle: {
        color: (params) => props.colors[params.dataIndex] // 动态颜色
      }
    }]
  })
}

// 监听配置变化,自动更新图表
watch(() => [props.type, props.colors], updateChart, { deep: true })

6. 保存 + 恢复配置

javascript 复制代码
// 保存到后端
const saveConfig = () => {
  axios.post('/api/chart/save', chartConfig)
}

// 加载时从后端恢复
onMounted(async () => {
  const res = await axios.get('/api/chart/config')
  Object.assign(chartConfig, res.data)
})

最终标准答案(你可以直接复述)

问:您如何实现自定义图表配置(如颜色、类型)?

答:

  1. 提供可视化配置面板,支持用户切换图表类型(柱状图 / 折线图)、自定义颜色。
  2. 图表类型通过 Tab 切换 ,使用 v-model 双向绑定实现实时切换。
  3. 颜色通过颜色选择器 进行双向绑定,修改后通过 ECharts 的 setOption 方法更新图表样式。
  4. 使用 父子组件传参,将颜色、类型等配置传递给图表组件,实现图表实时更新。
  5. 将用户自定义配置 保存到后端,下次加载页面时自动恢复,保持用户配置持久化。

42、你是如何实现权限控制的菜单渲染?

核心思路(和你说的一模一样)

  1. 登录成功后 ,从 后端接口获取当前用户的权限菜单列表(路由 + 菜单数据)。
  2. 前端保存权限菜单到 Pinia 全局状态中。
  3. 路由守卫中判断权限,动态生成可访问路由。
  4. 侧边栏 循环渲染 Pinia 中的权限菜单,实现 根据权限动态显示菜单

完整实现步骤(可直接背)

1. 登录获取权限菜单

用户登录成功后,调用接口获取 后端返回的权限路由 / 菜单数据

javascript 复制代码
// 登录请求
const res = await loginAPI(data)
// 从返回结果中拿到:用户信息 + 权限菜单列表
const { token, menus } = res.data

2. 存入 Pinia 全局状态

把后端返回的菜单存入 Pinia,供全局组件使用。

javascript 复制代码
// stores/permission.ts
const usePermissionStore = defineStore('permission', {
  state: () => ({
    menuList: [] // 存储后端返回的权限菜单
  }),
  actions: {
    setMenuList(menus) {
      this.menuList = menus
    }
  }
})

3. 动态生成路由(路由守卫中处理)

在路由守卫里,根据权限菜单动态添加路由,控制页面访问权限。

javascript 复制代码
router.beforeEach((to, from, next) => {
  if (token) {
    // 已登录
    if (permissionStore.menuList.length === 0) {
      // 重新获取菜单 → 动态添加路由
      const menus = await getMenuListAPI()
      permissionStore.setMenuList(menus)
      
      // 动态添加路由
      menus.forEach(route => {
        router.addRoute(route)
      })
      next({ ...to, replace: true })
    } else {
      next()
    }
  } else {
    // 未登录,跳转登录
    next('/login')
  }
})

4. 侧边栏循环渲染菜单

侧边栏组件直接从 Pinia 中读取权限菜单,循环渲染

javascript 复制代码
<template>
  <el-menu>
    <el-menu-item
      v-for="menu in menuList"
      :key="menu.path"
      :index="menu.path"
    >
      {{ menu.name }}
    </el-menu-item>
  </el-menu>
</template>

<script setup>
const permissionStore = usePermissionStore()
const menuList = permissionStore.menuList // 权限菜单
</script>

最终标准答案(面试直接说)

问:你如何实现权限控制的菜单渲染?

答:

  1. 用户登录成功后 ,从 后端接口获取当前用户的权限菜单列表
  2. 将权限菜单 保存到 Pinia 全局状态中统一管理。
  3. 路由守卫 里,根据权限菜单 动态添加可访问路由,控制页面访问权限。
  4. 侧边栏组件 直接循环渲染 Pinia 中的权限菜单 ,实现 根据用户权限动态显示菜单

43、您如何处理浏览器缓存导致数据陈旧?

答案:请求头添加 Cache-Control: no-cache, 或者使用时间戳参数。用户手动刷新时强制请求新数据。


标准答案(面试直接背)

我会通过 三种方式处理浏览器缓存导致的数据陈旧问题:

  1. 在请求头中添加 Cache-Control: no-cache强制浏览器不使用缓存,每次都请求最新数据。
  2. 在请求 URL 后拼接时间戳参数?t=时间戳),让每次请求地址都不一样,绕过浏览器缓存。
  3. 用户手动刷新页面时,强制重新请求最新数据,确保数据最新。

这些方案通常会 统一封装在 axios 的请求拦截器里,全局生效,不用每个接口单独处理。


完整实现(axios 二次封装里直接用)

1. 请求头加缓存控制(最标准)

config.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'

javascript 复制代码
// axios 封装文件
import axios from 'axios'

const service = axios.create({
  baseURL: '/api'
})

// 请求拦截器
service.interceptors.request.use(config => {
  // 👇 核心:禁用浏览器缓存
  config.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
  config.headers['Pragma'] = 'no-cache' // 兼容旧浏览器
  return config
})

export default service

2. URL 加时间戳(你说的方案)

javascript 复制代码
service.interceptors.request.use(config => {
  // 👇 拼接时间戳,每次请求唯一,避免缓存
  if (config.method === 'get') {
    config.params = {
      ...config.params,
      _t: Date.now() // 时间戳
    }
  }
  return config
})

最终一句话总结(最精炼)

问:您如何处理浏览器缓存导致数据陈旧?

答:

我会在 二次封装的 axios 请求拦截器 中,统一添加 请求头 Cache-Control: no-cache ,或者给请求 URL 拼接时间戳参数 ,强制浏览器每次都获取最新数据,避免缓存导致数据陈旧;同时支持用户 手动刷新时强制拉取新数据

44、您如何处理图表在 v-show 隐藏后尺寸错乱?

答案:使用 ResizeObserver 监听容器尺寸变化,或者使用 nextTick 在显示后调用 resize。


标准答案(面试直接说)

当图表容器使用 v-show 隐藏后再显示时,容器宽高会变为 0,导致 ECharts 无法正确计算尺寸,出现错乱。

解决方案是:在容器显示后,使用 nextTick 等待 DOM 更新,然后调用 chart.resize () 重新计算尺寸;也可以使用 ResizeObserver 监听容器变化自动修复。


正确实现代码(最稳、最常用)

javascript 复制代码
<script setup>
import { ref, nextTick } from 'vue'
import * as echarts from 'echarts'

const chartRef = ref(null)
const showChart = ref(true)
let chart = null

// 初始化图表
onMounted(() => {
  chart = echarts.init(chartRef.value)
  chart.setOption(option)
})

// 切换显示/隐藏后 → 修复尺寸
const toggleShow = async () => {
  showChart.value = !showChart.value

  if (showChart.value) {
    // 👇 关键:DOM 显示后,nextTick 再执行 resize
    await nextTick()
    chart.resize()
  }
}
</script>

<template>
  <div v-show="showChart" ref="chartRef" style="width:100%;height:300px"></div>
  <button @click="toggleShow">切换显示</button>
</template>

你说的 clear () 为什么不是最优解?

你说的:隐藏时调用 echarts.clear ()

这个方法 能解决错乱,但不是最佳实践,原因:

  • clear()清空图表内容,不是修复尺寸
  • 再次显示需要 重新 setOption,浪费性能
  • 只是 "清空",不是 "自适应"

**真正修复尺寸的是:resize ()**它才是让图表重新读取容器宽高的方法。


最标准、最专业的答案(背这个)

问:您如何处理图表在 v-show 隐藏后尺寸错乱?

答:

v-show 隐藏时图表容器宽高会变为 0,导致 ECharts 尺寸计算异常。我会在 图表显示后,使用 nextTick 等待 DOM 渲染完成,再调用 chart.resize () 让图表重新计算宽高,从而恢复正常尺寸。也可以使用 ResizeObserver 监听容器尺寸变化,自动触发 resize。


总结

  • 你的方向 完全正确:知道隐藏显示会出问题,需要主动处理
  • 正确 API 不是 clear(),而是 resize()
  • 标准方案:nextTick + chart.resize()

简历完善调整:

相关推荐
鹏程十八少2 小时前
7. 2026金三银四 Java 虚拟机面试终极版:32 道必考题 + 图解 + 源码精讲
后端·面试·前端框架
穿条秋裤到处跑17 小时前
每日一道leetcode(2026.04.24):距离原点最远的点
算法·leetcode·职场和发展
EnCi Zheng18 小时前
S10-蓝桥杯 17822 乐乐的积木塔
职场和发展·蓝桥杯
AIDF202620 小时前
终章:回归常态——在平庸的世界里,靠“赛道”与“态度”突围
面试
笨蛋不要掉眼泪20 小时前
面试篇-java基础上
java·后端·面试·职场和发展
前端摸鱼匠20 小时前
【AI大模型春招面试题27】字节对编码(BPE)的分词过程?如何处理未登录词(OOV)?
人工智能·ai·面试·大模型·求职招聘
A_aspectJ21 小时前
如何抓住Java开发岗的市场红利?从需求端反推学习路径
java·开发语言·职场和发展
嘻嘻哈哈樱桃21 小时前
牛客经典101题题解集--二叉树
java·数据结构·python·算法·leetcode·职场和发展
李日灐1 天前
<5> Linux 开发工具:包管理 + Vim 实操 + GCC 编译流程 + 静态与动态链接详解
linux·运维·服务器·面试·vim·gcc