WeKan 看板背景图(Board Backgrounds):存储机制、界面操作与 REST API 全流程解析
【免费下载链接】wekanThe Open Source kanban, built with Meteor. GitHub issues/PRs are only for FLOSS Developers, not for support, support is at https://wekan.fi/commercial-support/ . PR source translation to imports/i18n/data/en.i18n.json, other translations at https://app.transifex.com/wekan/wekan项目地址: https://gitcode.com/GitHub_Trending/we/wekan
WeKan 允许看板管理员把背景图片直接存储在 WeKan 自身(而不是依赖外部 URL),并通过「看板菜单 → 看板背景」完成上传、设为当前背景、下载与删除。本篇以docs/Features/Board/Board-Backgrounds/Board-Backgrounds.md为主体,逐条还原文档描述的存储目录、界面操作、导入导出行为,并结合源码(server/boardBackgrounds.js、server/routes/attachmentApi.js、models/boards.js)深入讲解其权限模型、存储策略与 DDP/REST 两套 API 的落地实现,读完你既能上手配置背景图,也能理解其底层调用链与回归测试。
一、背景图如何被存储:backgrounds目录与默认存储后端
文档开篇说明:看板背景图会存储在 WeKan 内部,具体做法是:
- 在
attachments与avatars旁边,创建一个backgrounds文件存储目录,并使用当前默认存储后端(default storage backend)。 - 默认存储后端由管理员在Admin Panel / Attachments / Default Storage中配置,与卡片附件共用同一套后端。
从源码结构看,背景图本质上是一条**看板级附件(board-level attachment)**记录。上传时meta字段里带有boardId与source: 'api-background'(REST)或source: 'board-background'(界面上传,见 client/components/sidebar/sidebar.js),并没有cardId。正因为它是“附件的一种”,它天然复用了 WeKan 的整套存储策略(filesystem / gridfs / S3)与软删除机制。
适用前提:背景图走的是附件存储体系,因此其可用后端受管理员在 Admin Panel 中配置的 Default Storage 限制。当后端为 filesystem 时落到
backgrounds目录;当为 gridfs/S3 时,文件会先落 filesystem,再异步moveToStorage迁移到目标后端(见 server/routes/attachmentApi.js)。
二、界面操作:上传 / 设为当前 / 下载 / 删除
文档列出的界面入口是Board menu → Board backgrounds,看板管理员可执行四类操作。对应前端实现位于 client/components/sidebar/sidebar.jade 的两个模板:
boardBackgroundUpload(上传区):选择本地图片,写入meta: { boardId, source: 'board-background' }。boardBackgroundList(管理区,classboard-backgrounds-grid):以缩略图网格列出该看板所有背景图,每项支持:- 点击缩略图 →设为当前背景(
js-set-board-background,title=set-as-active); - 下载(
js-download-board-background,href="{{link}}?download=true" download); - 删除(
js-delete-board-background,先弹出确认框)。
- 点击缩略图 →设为当前背景(
此外,模板里还提供了一个input.js-board-background-image-url,允许管理员直接粘贴一个图片 URL 作为背景(board-background-image-url)。
设为当前背景:setBackgroundImage
“设为当前”这一动作,会更新看板文档上的backgroundImageId与backgroundImageURL两个字段。核心实现在 models/boards.js:
// Set a board-level background attachment as the active board background. async setBackgroundImage(backgroundId) { const currentUser = await ReactiveCache.getCurrentUser(); if (currentUser.isBoardAdmin() || currentUser.isAdmin()) { const backgroundImageURL = generateUniversalAttachmentUrl(backgroundId); return await Boards.updateAsync(this._id, { $set: { backgroundImageId: backgroundId, backgroundImageURL }, }); } return false; }要点:
- 权限:仅
isBoardAdmin()(看板管理员)或isAdmin()(站点管理员)可写,其余成员返回false。 backgroundImageURL由generateUniversalAttachmentUrl(backgroundId)生成,是一条通用的附件访问 URL,前端据此渲染背景。- 对称地,
unsetBackgroundImage()会把两个字段都置空字符串,setBackgroundImageURL()则允许直接设置 URL。
删除背景:软删除与“当前背景”的联动清除
删除走的是看板级方法removeBoardBackground(server/boardBackgrounds.js),它被设计成 method 而非allow规则,因为需要做异步的看板管理员校验:
async removeBoardBackground(attachmentId) { // ... 权限校验:board.hasAdmin(this.userId) || user.isAdmin // If this background is the board's active one, clear it. if (board.backgroundImageId === attachmentId) { await Boards.updateAsync(boardId, { $set: { backgroundImageId: '', backgroundImageURL: '' }, }); } // A soft delete (History.md §12.3), like every attachment: the file stays // and the board's history can restore it. await softDeleteAttachment({ userId: this.userId, attachment }); return true; }这里有两个关键设计:
- 当前背景联动清除:被删除的图如果恰好是看板的当前背景(
board.backgroundImageId === attachmentId),会同时清空backgroundImageId与backgroundImageURL,避免指向一条已删除的附件。 - 软删除(soft delete):与所有附件一致,文件保留、可通过看板历史恢复,而非物理删除。
三、看板页与“全部看板”列表中的背景渲染
文档指出:当前背景图还会作为看板卡片(tile)的背景,显示在 All Boards 列表页,并配一层深色遮罩(dark overlay)保证标题可读。
渲染决策辅助函数:computeBoardBackground
前端如何决定“当前这块看板背景该画成什么”,由纯函数 models/lib/boardBackground.js 给出:
// Returns the background descriptor for `board`: // { type: 'image', url } - board has a background image (apply inline url) // { type: 'color' } - board has a color but no image // { type: 'none' } - board has neither: clear any stale inline bg function computeBoardBackground(board) { if (!board) return { type: 'none' }; const url = board.backgroundImageURL; if (typeof url === 'string' && url.length > 0) { return { type: 'image', url }; } if (board['background-color'] || board.color) { return { type: 'color' }; } return { type: 'none' }; }这个函数背后是一个真实的线上缺陷修复(Issue #4978)。文件头注释解释得很清楚:从收藏栏在看板 A 与 B 之间直接切换时,boardBody模板实例会被复用(currentBoard由 A 变 B 而非变 null),一次性的Utils.setBackgroundImage()不会重跑,导致 A 的background:url(...)内联样式“粘”在.board-wrapper上。修复思路是:(1) 在响应式 autorun 中调用setBackgroundImage(),使当前看板变化时重算;(2) 由本纯函数返回包含'none'在内的结果,主动清除过期的内联背景——旧代码只会 SET 背景、从不 RESET,所以 image→color 或 image→plain 的切换会残留旧图。
回归测试:boardBackground.test.cjs
tests/boardBackground.test.cjs 是纯 Node 单测(不依赖 Meteor),可直接node tests/boardBackground.test.cjs运行,正是为守住 #4978 的回归:
test('board with a background image URL -> type image + url', () => { const bg = computeBoardBackground({ backgroundImageURL: 'https://x/y.png' }); assert.deepStrictEqual(bg, { type: 'image', url: 'https://x/y.png' }); }); // NEGATIVE: board with neither image nor color -> none (clears stale bg) test('NEGATIVE: board with neither image nor color -> none', () => { assert.deepStrictEqual(computeBoardBackground({ title: 'Plain' }), { type: 'none' }); });它同时覆盖了“图片优先级高于颜色”“空字符串 URL 不被误判为图片”“image→plain 切换绝不保留旧 url”“null/undefined board 不抛异常”等边界,是理解该功能健壮性的最佳入口。
四、导入与导出:背景图的随迁
文档在 Import and export 一节说明了两点,均可在源码中得到印证:
- 看板导出会包含该看板的背景图,并支持重新导入。
- Trello 看板的背景图会在导入时下载并存入本地,这样即便原始 Trello URL 日后失效,背景仍可用。
第 2 点的实现落在 models/trelloCreator.js:导入时先读取prefs.backgroundImageScaled(缩放后优先)或prefs.backgroundImage作为backgroundImageURL兜底;随后在拿到本地文件引用后,把backgroundImageId与backgroundImageURL(generateUniversalAttachmentUrl(fileRef._id))写入看板文档。这正是“先保留 Trello 公共 URL 兜底、再替换为本地存储附件 URL”的两段式处理。
结论:背景图作为看板的一部分参与导入/导出,且 Trello 迁移时做了“下载落地”以摆脱对外部 URL 的长期依赖。
五、REST API:上传与下载背景图
这是文档中参数最密集的一节。背景图可经 REST API 上传与下载,是卡片附件上传/下载 API 的看板级对应物。
5.1 端点总览
| Method | Path | 用途 |
|---|---|---|
POST | /api/attachment/upload-background | 上传图片并设为看板背景。JSON body:{ boardId, fileData (base64), fileName, fileType? } |
GET | /api/attachment/download-background/:boardId | 下载看板当前背景图(返回base64Data+ 元数据) |
配套 CLI(api.py):
python3 api.py uploadbackground BOARDID /path/to/background.png python3 api.py downloadbackground BOARDID /path/to/saved-background.png5.2 权限模型(与文档一致,源码可验证)
文档强调:上传需要看板管理员;下载需要看板成员。
- 上传(server/routes/attachmentApi.js):校验
board.hasAdmin(userId) || 站点管理员,否则返回403 Board admin required。这与 DDP 侧的api.board.uploadBackground(server/attachmentApi.js)以及 methodremoveBoardBackground的权限口径完全一致。 - 下载(server/routes/attachmentApi.js):校验
board.hasMember(userId),即任意成员均可下载当前背景。
5.3 上传流程的关键细节(源码级)
/api/attachment/upload-background的完整处理链:
- 鉴权:
authenticateApiRequest(req),支持 accounts-express 上下文、可信 SSO 头登录、以及兼容性的X-User-Id/X-Auth-Token。 - 传输限额:
getApiTransferLimits()读取AttachmentStorageSettings.limitSettings的apiUploadBlocked/apiUploadMaxBytes;并存在硬性安全上限HARD_MAX_API_FILE_BYTES = 64 * 1024 * 1024(64MB)。超限时413。 - 存储后端:始终使用管理员配置的默认后端(
settings.getDefaultStorage()),若为 gridfs/S3 则异步moveToStorage。 - 写入并设为当前背景:
const file = new File([fileBuffer], fileName, { type: fileType || 'image/png' }); const meta = { boardId, fileId, source: 'api-background', storageBackend: targetStorage }; const uploader = await Attachments.insertAsync({ file, meta, isBase64: false, transport: 'http' }); await board.setBackgroundImage(uploader._id); - 响应:返回
success、attachmentId、fileName、fileSize、storageBackend、backgroundImageURL与提示消息。
注意:这里fileType缺省值是image/png(背景图默认按 PNG 处理),而卡片附件缺省是application/octet-stream——这是背景端点与通用附件端点的细微差别。
5.4 下载流程的关键细节
/api/attachment/download-background/:boardId:
- 校验成员资格 → 读取
board.backgroundImageId;若无则404 Board has no background image set。 - 通过
fileStoreStrategyFactory.getFileStrategy(attachment, 'original')拿到读流,按apiDownloadMaxBytes限额流式读取。 - 成功响应 JSON:
{ success, attachmentId, fileName, fileSize, fileType, base64Data, backgroundImageURL, storageBackend }。
5.5 DDP 方法对:SDK / DDP 客户端
文档还提到存在一对 DDP 方法供 SDK/DDP 客户端使用,实现在 server/attachmentApi.js:
api.board.uploadBackground(boardId, fileData, fileName, fileType):与 REST 上传逻辑对齐——校验登录、看板存在、看板管理员权限、默认存储后端、上传限额,写入附件后board.setBackgroundImage(uploader._id)。api.board.downloadBackground(boardId):校验成员资格,读取当前背景附件并返回 base64 与元数据。
两条链(REST 与 DDP)在权限口径、存储后端选择、限额与“上传即设为当前背景”的行为上保持镜像一致,方便不同集成方式(HTTP 或 SDK)选择。
六、与主题的其它能力如何衔接
背景图功能并非孤立,它与 WeKan 的看板与附件体系多处交汇:
- 看板(Boards)字段:
backgroundImageId/backgroundImageURL/color/background-color是看板文档上的可写字段,写入受看板管理员权限保护(models/boards.js)。 - 附件与文件存储:背景图复用 docs/Features/Cards/Attachments/Attachments.md 所述的存储/软删除/迁移体系。
- 主题与自定义 CSS:背景图是看板级“图片”背景,区别于 docs/Features/Theme/Custom-CSS-themes.md 的主题/自定义 CSS,二者可并存(图片经
background:url()内联,颜色经.board-wrapper的 colorClass)。 - 从 Trello 迁移:见 docs/Features/ImportExport/Trello/trello/Migrating-from-Trello.md 与 models/trelloCreator.js 的背景落地逻辑。
- 相关文档:docs/Features/Board/Boards/Boards.md。
七、要点回顾与可验证路径
| 关注点 | 事实 | 证据路径 |
|---|---|---|
| 存储位置 | backgrounds目录,随默认存储后端 | server/routes/attachmentApi.js |
| 设为当前背景 | setBackgroundImage写backgroundImageId/URL,仅看板/站点管理员 | models/boards.js |
| 删除 | methodremoveBoardBackground,软删除且联动清空当前背景 | server/boardBackgrounds.js |
| 渲染决策 | computeBoardBackground返回 image/color/none,支持清除旧背景(#4978) | models/lib/boardBackground.js |
| 回归测试 | 纯 Node 单测,可node tests/boardBackground.test.cjs | tests/boardBackground.test.cjs |
| Trello 导入 | 下载 Trello 背景并落地为本地附件 | models/trelloCreator.js |
| REST 上传 | POST /api/attachment/upload-background,看板管理员,默认后端 | server/routes/attachmentApi.js |
| REST 下载 | GET /api/attachment/download-background/:boardId,看板成员 | server/routes/attachmentApi.js |
| DDP 方法 | api.board.uploadBackground/api.board.downloadBackground | server/attachmentApi.js |
| CLI | api.py uploadbackground/downloadbackground | api.py |
综上,WeKan 的看板背景图是一条“以附件为底、看板管理员可控、导入导出随迁、REST/DDP 双通道”的完整能力链。理解computeBoardBackground的三态返回与setBackgroundImage/removeBoardBackground的权限口径,是把握该功能设计与健壮性的关键。
【免费下载链接】wekanThe Open Source kanban, built with Meteor. GitHub issues/PRs are only for FLOSS Developers, not for support, support is at https://wekan.fi/commercial-support/ . PR source translation to imports/i18n/data/en.i18n.json, other translations at https://app.transifex.com/wekan/wekan项目地址: https://gitcode.com/GitHub_Trending/we/wekan
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考