样式

响应式

  • 本就不明显,且尺寸/位移不应该受显示屏大小影响的样式允许使用 px 单位,比如圆角半径 border-radius、阴影 box-shadow 和关键帧动画里的位移 translate
  • 除此之外的样式都使用 rem 单位而不是 px 单位。你可以认为 1rem = 10px
  • 多使用 flexgrid 布局,尽量使用 auto% 以及 frvhvw 等相对单位。
  • 支持大部分 PC 显示屏,不支持手机。相关代码集中在 _breakpoints.scss

风格

  • 首页、404 页和“作者信息”融合点描、蚀刻和铜版画风格,人物半写实,主要使用米色和黑色,辅以主色点缀。
    图-3.1_参谋页面风格
    • 皮肤和布料用小点/短划线堆出来,远看像柔和阴影,近看全是颗粒。
    • 背景略像羊皮纸。
    • 使用战术地图、战术符号等作为背景装饰。
  • 人格头像融合数字厚涂与写实油画风格
    图-3.2_人格肖像风格
    • 方形笔刷,大色块。
    • 单光源、大面积暗背景,带有轻微雕塑感,人物轮廓从黑场里“浮”出来。
    • 可以让魂兮归乡提供头像。
  • 一切其他组件和页面使用现代简约风格,让用户感觉简单、干净、稳重。禁止写实或拟物

字体

  • 若无特殊情况,_variables.scss 中的 1.333 - Perfect Fourth1.500 - Perfect Fifth 中选择字号
    • 默认 1.6rem16px),你通常没有必要手动设置 font-size
    • 对于次要提示消息(比如,Field 校验不通过时下方显示的红色提示文本),设置 font-size: 1.2rem
  • 默认字重为 500,对于重点信息(比如按钮上的文本),可以设置 font-weight: 700
  • 请勿使用衬线体。
  • 间距也可以从 _variables.scss 中的 1.333 - Perfect Fourth1.500 - Perfect Fifth 中选(无强制要求)
  • 强烈建议下图红色框位置的标题统一使用 <h4>font-size: 3.79rem; font-weight: 700;)。
    图-3.3_h4

Icon

统一使用 Phosphor Icons

  • size 通常选用 3224
  • weight 通常选用 boldfillregular

SCSS 代码组织

图-3.4_CSS 相关文件

  • 把 SCSS 静态变量定义在 styles/_variables.scss,把关键帧动画定义在 styles/_keyframes.scss 中。不要在任何 partial(_ 开头的 .scss 文件)中编写选择器。之后,你可以在 main.scss 以及各 .vue<style> 块内引用这些定义。
    • 这些 SCSS 静态颜色变量的名字通常体现“色彩原理”的语义,比如“主色”、“辅色”。如果有些颜色有跨越组件的全局语义(比如,不管是前端校验不合格、错误弹窗、删除提醒,我都要用相同的“警告色”警告你),也应该用 SCSS 静态变量记录它(比如 $d-warn-color: c92a2a)——总之,确定 SCSS 变量的语义时基本不考虑组件。此外,还要在最开头加上 d-n- 前缀,表示白日或黑夜主题下的配色。
    • main.scss.vue<style lang="scss"> 中使用 border-radius: #{$border-radius}。圆角会影响网页的气质,我们可以在项目收尾时调整 SCSS 变量 $border-radius 的值进而一键更改项目中的所有圆角,观察不同取值下的网页气质并最终确定一个值。
  • main.scss:root[data-theme="night"] 选择器内声明同名不同值的 CSS 动态颜色变量。它们的取值引用 _variables.scss 中定义的静态 SCSS 颜色变量,比如 --field-input-color: #{$d-text-color}
    • :root 内的变量值对应白日主题、首页和 404 页的配色。
    • [data-theme="night"] 内的变量值对应黑夜主题的配色。当这个选择器生效时,其内颜色变量声明覆盖 :root 中同名变量声明,进而实现用黑夜配色覆盖白日配色。
    • 这些 CSS 变量名通常体现“哪个组件的哪个部分”的语义,比如 --field-border-color 这个名字说明这个变量对应“Field 组件的边框颜色”。它们不涉及时间(“白日”、“黑夜”)或色彩原理(“主色”、“辅色”、“对比色”……)上的语义!
    • main.scss 的选择器和 .vue<style> 内总是使用 var() 引用这些 CSS 颜色变量,比如 border: 2px solid var(--field-border-color)
    • 务必遵守命名规范、附加清晰的注释,否则维护会很痛苦。

图-3.5_主题切换

  • 如果你不理解为什么要这么做,可以阅读《四、UX 设计》的“白天/黑夜主题切换”一节,如果仍不理解,去问魂兮归乡。
  • main.scss 包含全局重置(比如 box-sizing: border-box)、:root[data-theme="night"] CSS 颜色变量声明和对特定 HTML 元素(比如)的样式的全局重置。除此之外不要在 main.scss 中写其他东西。
  • 若无必要,总是使用类名选择器。
  • 使用 BEM 规范。
动画
  • _keyframes.scss 里定义关键帧动画,再直接从 .vue<style> 块内引用。
  • 给重点内容和提醒加上动画以吸引注意力,而不是给无关紧要的元素加上动画导致分散注意力
  • 动画长度最好在 300ms 以内。
组件内样式(.vue<style> 块内样式)
  • <style scoped lang="scss">scoped 防止本 <style> 块内声明的样式误选中其他文件里的 HTML 元素,lang="scss" 允许使用 SCSS 变量和嵌套选择器等功能。
  • 使用 var() 引用 main.scss 中定义的 CSS 颜色变量。
  • 通常,<template> 下首先是一个包裹一切的 <div class="组件名">,比如 <div class="deletor"><div class="welcome">
  • 如何给各个类命名(除了顶层 <div> 外,基本都是 直接父级__我自己):
    图-3.6_类名命名
  • 如何控制特殊样式:
    图-3.7_特殊样式规范

前端其他部分

路由

我们利用 Vue Router 处理单页应用程序的路由。我认为我们不该把路由理解成“(点击 RouterLink 后)跳转到了新页面”,而该把路由理解成“我们(通过点击 RouterLink 等手段)改变了 URL 这一全局状态的值,不同的 RouterView 时刻关注着这一全局状态的不同部分,根据所关注的部分的当前取值,决定此刻自己要渲染什么内容。这些 RouterView 一直都待在那里,它们只是在不同时刻根据状态端上不同的菜”。

诤略参谋的 Vue 组件分为 layoutviewcomponent 三级,layout 对应顶层 RouterView 会渲染的组件,view 对应非顶层 RouterView 会渲染的组件,component 用于组成 viewlayoutrouter/index.js 中的路径嵌套关系完全对应各个含有 RouterView 的组件的嵌套关系。

图-3.8_路由分层
为了便于开发管理,诤略参谋使用如下规定:

  • 为每一个路径命名(name: ×。一方面这便于维护、router.push,另一方面任务系统的一键路由功能和 Txt 的“未保存提醒”功能依赖命名运转。
  • 凭资源和行为之间的逻辑所属关系设计路径。
    • “所有我的项目的列表”:/app/projects。使用复数资源名。
    • “创建项目”:/app/project/create单数资源名/围绕此类资源的行动
      • 路由自上而下依次匹配,匹配成功后不会继续检查其余 path。一定要把 /app/××/yy 写在 /app/××/:××Id 前,否则 /app/××/yy 会被认为对应路径 /app/××/:××Id××Id = yy
    • “我的指定项目的详细信息”:/app/project/:projectId单数资源名/资源主键
    • “所有该项目下计划构成的列表”:/app/project/:projectId/plans
    • “我的指定计划的详细信息”:/app/project/:projectId/plan/:planId
      • 同理,必须把这个写在 /app/project/:projectId 之前,否则会错误匹配,认为 projectId = :projectId/plan/:planId
  • 凭渲染层级设计路径的嵌套层次(谁在谁的 children 里)。不要根据逻辑关系设计层次
    • 只要想使用 Workspace 里的非侧边栏区域,就把路径写到 /appchildren。比如,“项目详情”和“计划详情”都想用那块区域,于是 /app/project/:projectId/plan/:planId/app/project/:projectId 都放到 /appchildren 里,而不是把 /app/project/:projectId 放到 /appchildren、把 /app/project/:projectId/plan/:planId 放到 /app/project/:projectIdchildren。记住,路径的嵌套关系对应的不是逻辑所属关系,而是渲染层次关系。
  • 把长路径写在短路径前,把静态路径写在带参路径前。路由匹配是“先到先得”的。
  • 提供必要的“返回项目”“返回计划”等按钮(利用 Back 组件),不要迫使用户点击左上角的回退键。

Pinia store

  • plan store 全局管理项目系统和计划系统所需的状态。多个 view 可以共用这些数据。具体结构由负责对应部分的队员设计
    • 建议设置“概览”数据结构(比如 projectsplans),存储多条项目/计划的概况。
    • 建议设置 currentlyProjectcurrentPlan 之类的状态,当用户正在阅读或处理指定一个详细项目/计划时,用这个状态存储所有需要用到的详细信息。记得同步“概览”数据结构和 current×× 中的数据。
    • view 根据状态渲染内容(可以利用 getters),利用 store 的响应式特性实现局部刷新,提高性能
    • 对外提供修改这些状态的函数(actions),view 可以调用这些函数以更新数据结构内的信息。
  • 组件加载时应该先检查状态中是否已经存储了需要的值,若已经存储,不再发出请求。在成功任务的一键路由中尤其要重视这一点。
  • 强烈建议人格管理、记忆管理等功能也借助专属的 store 实现。

音效

  • 注意版权限制。请自己建一个文件,详细记录你使用的音效文件的来源,我们会在最后统计资源引用时用到。
    • 例如,btn/forbidden.wav 对应音效原名 ××,来自 <网页链接>
    • 我目前用了来自 MixkitPixabay 的音效。
  • 除了氛围音效外,最好 1~2s
  • 不要用刻板科幻风格的音效。
  • 将音频文件存放在 public/sounds 下,sounds 包括 ambience(氛围音效)文件夹和各组件对应音效文件夹,文件命名要清晰(例如,用播放音效时对应的交互命名,toggleselectclose;用状态命名,successerror)。
  • 必须通过调用 utilities/tools.js 中的 playSound 函数播放音效。这是为了保证音效设置能管理所有音效的启用与否。你直接去看一眼函数体就知道该怎么调用这个函数了。

任务

涉及到 LLM 输出的操作需要通过任务系统完成,用户用 TaskBtn 及其变种提交任务(比如“生成计划”“生成流程图”“生成预算/风险分析”),在前端配置任务的 whenSucceed 钩子、在后端编写 WebClient 异步请求函数,在任务完成后更新后端任务对象的状态、将生成的结果的 id 写入 resultId、根据与前端的约定配置 routerPathNamerouterParams 用于一键路由……返回一个 StdResponse<TaskVO> 对象。
图-3.9_任务系统核心逻辑示意图

  • 需要考虑“抢先删除问题”,举个例子,LLM 要根据计划 13 的内容生成结果,但是用户在结果返回前就想把计划 13 删除,这该怎么办?禁止用户在这一时间段删除,还是异步请求的成功回调先检测一下依赖的信息是否仍存在?需要日后确定,我目前倾向于为这些结果实体类加入标志位,控制此时能否删除。这些控制位的值由后端启动任务的 API 和返回结果的 API 设置。
  • 如果删除了任务对应的结果,需要在前后端一并删除对应任务(否则前端一键路由会找不到资源,当然也可以不处理任务而是专门编写一个类似 404 的页面,日后确定)。

对这些问题的初步讨论,详见《三、数据通讯(下)》的“打磨细节”一节。

常量

把各常量集中到 config/config.js 中 。

组件

  • 总是先检查自己的开发需求是否能直接利用组件文档中提供的组件。这有助于提高开发效率、代码质量和样式统一性。
  • 遇到表单场景,总是使用 FormWrapperField(及其变种)和 Btn(及其变种)三件套。Field 使用的 validator 集中到 utilities/validators.js 中。
  • 重量级的添加和编辑行为请单独在 router/index.js/app children 下加新的 view,轻量级的添加行为使用 Adder,轻量级的编辑行为使用 Editor,删除特定条目使用 Deletor,删除所有条目使用 RemoveAll。“轻量级”指的是需要用户输入的文本框只有一两个。
  • 对于重要的长文本输入场景,使用 Txt 代替 Field,记得配置路由守卫。
  • 搜索使用 Search
  • 详见“四、组件/教程文档”。

请求与响应

  • 绝大部分请求用 utilities/http.js 提供的 http 对象发出。如果你的请求需要用户处于登录状态,请使用 http 对象,因为我为它配置了请求拦截器,它会自动在响应头上附上 JWT。
  • 我为 http 对象设置了响应拦截器,响应会先被拦截器内的代码处理,拦截器 return 的结果会成为 http.get 一类调用返回的 Promise 类对象的 fulfilled 值。
http.interceptors.response.use(
  (res) => {
    const response = res.data;
    if (response.code >= 0 && response.code < 1000) {
      // 成功
      if (LOGGED_IN_SUCCESSFULLY.includes(response.code)) {
        useUserStore().updateMe(response.data);
      } else if (LOGGED_OUT_SUCCESSFULLY.includes(response.code)) {
        router.push({ name: "home" });
      }
      if (response.userVisibility) useToastStore().success(response.msg);
      return response.data ?? true;
    } else {
      // 业务异常
      console.warn("[axios 响应拦截器]:发现业务异常");
      console.log(response.code, response.msg);
      if (response.userVisibility) useToastStore().error(response.msg);
      return null;
    }
  },
  (err) => {
    console.warn("[axios 响应拦截器]:发现 Axios Error");
    console.log(err);
    useToastStore().error("网络异常,请稍后再试");
    return null;
  }
);
  • 请查看后端 BizCode 中状态码的设计要求,同类反馈的状态码应该接近,构成“连续”的区间,以便响应拦截器可以用简单的 if 语句区分对响应的处理逻辑。
  • 响应拦截器会根据 BizCode 中的 userVisibility 确定是否弹窗,根据 code 落入哪个范围确定调用哪一种弹窗,在弹窗中显示 msg 作为提示消息。对于业务异常(根据 code 落入了特定区间察觉)和浏览器抛出的真正的异常(try catch 掉),它会给出错误弹窗并 return nullPromise 类对象的 fulfilled 值为 null)。对于成功响应,如果 StdResponse.data 非空,拦截器返回 StdResponse.data;如果 StdResponse.data 为空,拦截器返回 true。总之,响应成功时 Promise 类对象的 fulfilled 值是 StdResponse.data ?? true,一定不为 null
  • 你可以在 if 下继续按照 code 细分 if,以对某些响应做出特殊行为

后端

lombok

  • 若无必要,不要自己写 gettersetter,只需要在类上加入 @Data 注解。
  • 不要用 @Autowired 依赖注入,用 @RequiredArgsConstructor 搭配 private final X x

实体类

  • 主键均为 Long ××Id
  • 对于已登录的用户,可以直接调用 userService.getCurrentUser() 获得对应的用户对象,可以直接用 Long userId = (Long) (SecurityContextHolder.getContext().getAuthentication().getPrincipal()) 从安全上下文中获取用户主键,也就是说你不需要在请求里携带用户主键。
  • 所以我们该手动维护不同记录的对应关系,还是使用外键约束?至少对于那些需要 userId 的实体类,我倾向于不使用外键约束,因为我们可以直接从安全上下文中拿到 userId,不需要先查询出整个用户对象、再得到主键、再拿着主键查询其他信息。

响应格式

  • RESTful API 均使用 StdResponse 作为返回值。StdResponse 包括 code(业务状态码)、msg(提示消息)、data(前端关注的核心数据)和 userVisibility(前端响应拦截器据此决定是否调用弹窗系统)。类里提供了多个快速构造 StdResponse 用的函数,请自行了解(不建议直接调用构造函数创建 StdResponse 对象)。
  • 如果 data 的结构与实体类的结构不匹配(比如,我可能不想传输实体类中某些字段的值,或我可能想把多个实体类对象的信息组合在一起)时,在 format 文件夹下按照你期望的 data 格式编写对应的 ××VO 类。
  • 如果出现异常,在 BizCode 中定义相关提示,然后直接 throw new BizException(BizCode 值) 即可。MVC 层全局异常处理器会自动捕获异常,把它们转为 StdResponse 格式作为正常的响应传回浏览器。

API

  • 编写 RESTful API。API 端点通常以 /api 开头,涉及“资源名”、“动作名”,偶尔包含路径变量。
    • 例如,/api/auth/register/auth 是注册行为所属的大类(身份验证),register 表明是注册动作;/api/auth/register/check-email/{userEmail} 表明是注册动作下的“检查邮件”动作,正在检查路径变量 userEmail 对应的邮件。
    • 使用 GETPOSTPATCHDELETE 方法。
      • 使用 GET 通过主键查询特定条目时,端点格式为 /资源名/{路径变量 ××Id},在 API 形参中用 @PathVariable 注解拿到主键值。
      • 使用 GET 通过名称模糊查询条目时,端点格式为 /资源名/search,使用查询参数,在 API 形参中用 @RequestParam 拿到名称。
      • 使用 GET 获得初始化页面所需的数据时(通常是一个列表),端点格式为 /资源名复数,也可以叫 /init
      • 使用 DELETE 删除特定条目时,端点格式为 /资源名/{路径变量 ××Id}
      • 使用 DELETE 删除全部条目时,端点格式为 /资源名/all
      • 使用 POST 添加条目时,端点格式为 /资源名,前端将创建记录所需的数据放入请求体,后端根据请求体的格式在 format 文件夹下定义对应的 ××DTO 类(记得利用 @Data 生成 getters)。用 @RequestBody ××DTO 取出请求体中的数据。
      • 使用 PATCH 修改条目时,端点格式为 /资源名/{路径变量 ××Id}。前端把所需数据放入请求体,后端按需定义 ××DTO 类,用 @RequestBody ××DTO 取出请求体中的数据。
  • 如果想利用任务系统,用于提交任务(不是查询结果)、开始异步请求的后端 API 端点格式为 POST /api/llm/××,其中 ×× 对应你给 TaskBtn 传递的 target 值。

后端校验

  • ××DTO 上加上 validation 提供的注解,在 API 形参的 @RequestBody 注解前加上 @Valid 注解。
  • 限制各种文本的长度,以防过长的用户输入破坏网页布局
  • 数据统计相关场景下,限制浮点数的显示位数,以防它破坏网页布局

LLM

  • 使用 WebClient 发送异步而非同步(.block())请求。
  • 需要就异常处理、结构化提示词和结构化输出达成共识。初步讨论详见《三、数据通讯(下)》的“LLM 异常情况”一节。

全局配置

配置集中到 application.yml。一旦更新这个文件就立刻在群里发新版本。如果在写博客、需要对这份文件截图,不要把这个文件里的 API Key 不小心截进去。


Git

及时 git pushgit pull,这利人利己,有利于其他人和你同步进度、减少冲突。就几条指令的事,不要拖延。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐