三、项目开发规范
诤略参谋项目开发规范。
目录
样式
响应式
- 本就不明显,且尺寸/位移不应该受显示屏大小影响的样式允许使用
px
单位,比如圆角半径border-radius
、阴影box-shadow
和关键帧动画里的位移translate
。 - 除此之外的样式都使用
rem
单位而不是px
单位。你可以认为1rem = 10px
。 - 多使用
flex
与grid
布局,尽量使用auto
、%
以及fr
、vh
、vw
等相对单位。 - 支持大部分 PC 显示屏,不支持手机。相关代码集中在
_breakpoints.scss
。
风格
- 首页、404 页和“作者信息”融合点描、蚀刻和铜版画风格,人物半写实,主要使用米色和黑色,辅以主色点缀。
- 皮肤和布料用小点/短划线堆出来,远看像柔和阴影,近看全是颗粒。
- 背景略像羊皮纸。
- 使用战术地图、战术符号等作为背景装饰。
- 人格头像融合数字厚涂与写实油画风格。
- 方形笔刷,大色块。
- 单光源、大面积暗背景,带有轻微雕塑感,人物轮廓从黑场里“浮”出来。
- 可以让魂兮归乡提供头像。
- 一切其他组件和页面使用现代简约风格,让用户感觉简单、干净、稳重。禁止写实或拟物。
字体
- 若无特殊情况,从
_variables.scss
中的1.333 - Perfect Fourth
和1.500 - Perfect Fifth
中选择字号。- 默认
1.6rem
(16px
),你通常没有必要手动设置font-size
。 - 对于次要提示消息(比如,
Field
校验不通过时下方显示的红色提示文本),设置font-size: 1.2rem
。
- 默认
- 默认字重为
500
,对于重点信息(比如按钮上的文本),可以设置font-weight: 700
。 - 请勿使用衬线体。
- 间距也可以从
_variables.scss
中的1.333 - Perfect Fourth
和1.500 - Perfect Fifth
中选(无强制要求)。 - 强烈建议下图红色框位置的标题统一使用
<h4>
(font-size: 3.79rem; font-weight: 700;
)。
Icon
统一使用 Phosphor Icons。
size
通常选用32
或24
。weight
通常选用bold
、fill
或regular
。
SCSS 代码组织
- 把 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
的值进而一键更改项目中的所有圆角,观察不同取值下的网页气质并最终确定一个值。
- 这些 SCSS 静态颜色变量的名字通常体现“色彩原理”的语义,比如“主色”、“辅色”。如果有些颜色有跨越组件的全局语义(比如,不管是前端校验不合格、错误弹窗、删除提醒,我都要用相同的“警告色”警告你),也应该用 SCSS 静态变量记录它(比如
- 在
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)
。 - 务必遵守命名规范、附加清晰的注释,否则维护会很痛苦。
- 如果你不理解为什么要这么做,可以阅读《四、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>
外,基本都是直接父级__我自己
): - 如何控制特殊样式:
前端其他部分
路由
我们利用 Vue Router 处理单页应用程序的路由。我认为我们不该把路由理解成“(点击 RouterLink
后)跳转到了新页面”,而该把路由理解成“我们(通过点击 RouterLink
等手段)改变了 URL 这一全局状态的值,不同的 RouterView
时刻关注着这一全局状态的不同部分,根据所关注的部分的当前取值,决定此刻自己要渲染什么内容。这些 RouterView
一直都待在那里,它们只是在不同时刻根据状态端上不同的菜”。
诤略参谋的 Vue 组件分为 layout
、view
和 component
三级,layout
对应顶层 RouterView
会渲染的组件,view
对应非顶层 RouterView
会渲染的组件,component
用于组成 view
或 layout
。router/index.js
中的路径嵌套关系完全对应各个含有 RouterView
的组件的嵌套关系。
为了便于开发管理,诤略参谋使用如下规定:
- 为每一个路径命名(
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
里的非侧边栏区域,就把路径写到/app
的children
里。比如,“项目详情”和“计划详情”都想用那块区域,于是/app/project/:projectId/plan/:planId
和/app/project/:projectId
都放到/app
的children
里,而不是把/app/project/:projectId
放到/app
的children
、把/app/project/:projectId/plan/:planId
放到/app/project/:projectId
的children
。记住,路径的嵌套关系对应的不是逻辑所属关系,而是渲染层次关系。
- 只要想使用
- 把长路径写在短路径前,把静态路径写在带参路径前。路由匹配是“先到先得”的。
- 提供必要的“返回项目”“返回计划”等按钮(利用
Back
组件),不要迫使用户点击左上角的回退键。
Pinia store
- 用
plan
store 全局管理项目系统和计划系统所需的状态。多个view
可以共用这些数据。具体结构由负责对应部分的队员设计。- 建议设置“概览”数据结构(比如
projects
、plans
),存储多条项目/计划的概况。 - 建议设置
currentlyProject
、currentPlan
之类的状态,当用户正在阅读或处理指定一个详细项目/计划时,用这个状态存储所有需要用到的详细信息。记得同步“概览”数据结构和current××
中的数据。 view
根据状态渲染内容(可以利用getters
),利用 store 的响应式特性实现局部刷新,提高性能。- 对外提供修改这些状态的函数(
actions
),view
可以调用这些函数以更新数据结构内的信息。
- 建议设置“概览”数据结构(比如
- 组件加载时应该先检查状态中是否已经存储了需要的值,若已经存储,不再发出请求。在成功任务的一键路由中尤其要重视这一点。
- 强烈建议人格管理、记忆管理等功能也借助专属的 store 实现。
音效
- 注意版权限制。请自己建一个文件,详细记录你使用的音效文件的来源,我们会在最后统计资源引用时用到。
- 除了氛围音效外,最好
1~2s
。 - 不要用刻板科幻风格的音效。
- 将音频文件存放在
public/sounds
下,sounds
包括ambience
(氛围音效)文件夹和各组件对应音效文件夹,文件命名要清晰(例如,用播放音效时对应的交互命名,toggle
、select
、close
;用状态命名,success
、error
)。 - 必须通过调用
utilities/tools.js
中的playSound
函数播放音效。这是为了保证音效设置能管理所有音效的启用与否。你直接去看一眼函数体就知道该怎么调用这个函数了。
任务
涉及到 LLM 输出的操作需要通过任务系统完成,用户用 TaskBtn
及其变种提交任务(比如“生成计划”“生成流程图”“生成预算/风险分析”),在前端配置任务的 whenSucceed
钩子、在后端编写 WebClient 异步请求函数,在任务完成后更新后端任务对象的状态、将生成的结果的 id
写入 resultId
、根据与前端的约定配置 routerPathName
和 routerParams
用于一键路由……返回一个 StdResponse<TaskVO>
对象。
- 需要考虑“抢先删除问题”,举个例子,LLM 要根据计划
13
的内容生成结果,但是用户在结果返回前就想把计划13
删除,这该怎么办?禁止用户在这一时间段删除,还是异步请求的成功回调先检测一下依赖的信息是否仍存在?需要日后确定,我目前倾向于为这些结果实体类加入标志位,控制此时能否删除。这些控制位的值由后端启动任务的 API 和返回结果的 API 设置。 - 如果删除了任务对应的结果,需要在前后端一并删除对应任务(否则前端一键路由会找不到资源,当然也可以不处理任务而是专门编写一个类似 404 的页面,日后确定)。
对这些问题的初步讨论,详见《三、数据通讯(下)》的“打磨细节”一节。
常量
把各常量集中到 config/config.js
中 。
组件
- 总是先检查自己的开发需求是否能直接利用组件文档中提供的组件。这有助于提高开发效率、代码质量和样式统一性。
- 遇到表单场景,总是使用
FormWrapper
、Field
(及其变种)和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 null
(Promise
类对象的 fulfilled 值为null
)。对于成功响应,如果StdResponse.data
非空,拦截器返回StdResponse.data
;如果StdResponse.data
为空,拦截器返回true
。总之,响应成功时Promise
类对象的 fulfilled 值是StdResponse.data ?? true
,一定不为null
。 - 你可以在
if
下继续按照code
细分if
,以对某些响应做出特殊行为。
后端
lombok
- 若无必要,不要自己写
getter
和setter
,只需要在类上加入@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
对应的邮件。 - 使用
GET
、POST
、PATCH
、DELETE
方法。- 使用
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 push
和 git pull
,这利人利己,有利于其他人和你同步进度、减少冲突。就几条指令的事,不要拖延。
更多推荐
所有评论(0)