ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PhotoPrism 前端开发规范与工程实践:从代码风格、状态管理到测试与本地化

PhotoPrism 前端开发规范与工程实践:从代码风格、状态管理到测试与本地化 后端前端图像处理人工智能AI 应用【免费下载链接】photoprismAI-Powered Photos App ✨项目地址https://gitcode.com/gh_mirrors/ph/photoprism点击查看免费下载导读本文是 PhotoPrism 仓库内.claude/rules/frontend-rules.md的完整技术解读与工程落地指南覆盖前端代码风格、共享状态管理、Vue/Vuetify 组件约束、格式化与 lint 工具链、依赖锁定策略、单元测试与 TestCafe 验收测试的入口与坑点、焦点管理、翻译与本地化工作流以及 Web 模板与浏览器兼容基线。读完本文你将能够在frontend/目录下按仓库既定规范提交合规代码理解Model基类的 diff 与回滚语义正确运行 Vitest 与 TestCafe 而不触发看起来真实但实则环境导致的测试失败掌握 gettext 提取、编译与 lint 的完整流程以及在修改会话引导、模板或构建产物时知道哪些文件必须保持同步。1. 代码风格与测试覆盖基线1.1 注释与测试的最低要求规则文件要求遵循同目录下code-comments.md的注释规范任何新增的 JS 函数含 helper与 Vue 组件只要切实可行就要编写测试行为变更时同步更新既有测试。当单元测试不现实时如 DOM 重流程、第三方组件集成doc comment 依然强制要求这是最低标准。1.2 共享状态reactive 单例禁止引入 Pinia/Vuex共享状态必须放在src/common/与src/app/下的 reactive 单例模块中例如 app/session.js、common/config.js、common/clipboard.js、common/log.js这些模块直接导出一个reactive()/ref()对象组件通过import或全局安装的$config/$session插件访问。规则明确不要引入 Vuex、Pinia 或任何新的临时 store。需要扩展就扩展既有单例或在common/、app/中与既有单例并列新增一个。从源码结构看这种设计保证了一个来源的真相所有会话、配置、剪贴板与日志状态都以模块级单例形式存在组件间无需经过 store 中间层即可共享同时避免了额外依赖与样板代码。1.3 Model 基类快照、diff 与回滚frontend/src/model/下的所有类都继承 frontend/src/model/model.js 中的Model基类。它的核心机制是每次setValues()加载或保存后都会调用都会把当前值快照进__originalValuesgetValues(true)基于该快照做 diff只返回发生变化的字段因此update()只向 API 发送变更字段rollback()将快照恢复回去用于 API 拒绝后的乐观更新回滚wasChanged()是rollback()的天然逆操作——回滚后直到下一次变更前它都返回false。规则要求读取已保存的值时使用model.originalValue(key)而不是直接读__originalValues或额外的组件状态。原因从源码可见originalValue()在字段未被跟踪时还会回退到实例上的实时值调用方无需为临时字段做特判。-1/0/1三态选项的正确用法是flagEnabled(key, defaultEnabled)与setFlag(key, enabled, defaultEnabled)。以 model.js 的实现为例flagEnabled(key, defaultEnabled true) { const value Number(this[key]) || 0; return value 0 || (value 0 defaultEnabled); } setFlag(key, enabled, defaultEnabled true) { const saved Number(this.originalValue(key)) || 0; const savedEnabled saved 0 || (saved 0 defaultEnabled); if (savedEnabled !!enabled) { this[key] saved; // 已处于目标状态则保留原值 } else { this[key] enabled ? 1 : -1; // 否则写入显式状态 } }关键语义开关再切回时恢复保存值因此未触碰过的默认值保持0update()不会发送任何内容。这也解释了为什么getDefaults()必须为这类字段声明数字0作为默认值而绝不能是true——getValues()的强制转换以getDefaults()中每个字段的类型为准字符串字段把null/undefined归一为数字字段经parseFloat()布尔字段经Boolean()未被声明的字段则原样透传见 model.js。Settings模型frontend/src/model/settings.js就是典型应用load()通过$api.get(settings)加载并setValues()快照save()只把getValues(true)的 diffPOST回服务端。注意其setValues()覆写还会把maps.style为basic/offline的值归一为。1.4 Vue/Vuetify只使用 Options API纯 JSOptions API 是唯一选择与代码库其余部分保持一致禁止引入 Composition API 或script setup。禁止 TypeScript前端是纯 JS Vue SFC 代码库没有.ts文件、没有tsconfig.json、没有script langts块。注释中的 JSDoc 类型标注没问题但完整的 TS 迁移不在范围内。与frontend/package.json中vue^3.5.43、vuetify精确锁定4.2.2的依赖声明吻合。2. 格式化ESLint Prettier 的分工2.1 谁负责什么规则明确ESLint Prettier 各自负责格式二者不重叠编辑后运行make fmt-js等价于frontend/下的npm run fmt做 ESLint 自动修复再用make lint-js验证frontend/eslint.config.mjs是 flat config 的事实来源。Prettier 重排不在make fmt-js/eslint --fix范围内prettier/prettier规则被置为off以保留有意的换行多行方法链、垂直谓词列表。需要整体重排时显式运行prettier --write file不要对整个src/或tests/一把梭。从 frontend/eslint.config.mjs 可以看到细节它由eslint/js、eslint-plugin-vueflat/recommended与FlatCompat适配的eslint-config-prettier、plugin:vuetify/base组合而成CSS/SCSS/Sass 被整体忽略归 Prettier 所有实际生效规则包括 2 空格缩进switch 子句与成员链各缩进一级、unix换行、双引号、分号、curly全量花括号、brace-style1TBS 且禁单行以及no-unused-vars等。2.2 Prettier 配置速览frontend/.prettierrc.json 定义的基线是printWidth: 160、双引号、分号、trailingComma: es5、proseWrap: neverCSS/SCSS 使用tabWidth: 4。规则强调不要手工换行长行让 Prettier 决定。仓库根目录的.editorconfig负责缩进与换行风格不要在本地覆盖。2.3 Vue SFC 块顺序template→script→style与既有组件保持一致。2.4 Dev 容器中的工具链开发容器在全局PATH预装了与frontend/package.json锁定的同版本eslint与prettiereslint --version应与package.json的eslint条目一致直接在frontend/下调用例如eslint --fix tests/即可无需npx——后者多一次 spawn 步骤和一层解析。注意npm run fmt与npm run lint默认不检查测试树与版本覆盖层overlay目录。3. 依赖与锁定策略3.1 锁定是有意的规则与 frontend/README.md 一致无 caret 的版本如axios: 1.20.0、vuetify: 4.2.2都是有意锁定。改之前必须先读frontend/README.md并执行git log -p -S pkg -- frontend/package.json查明锁定原因。目前锁定的两个关键包包版本锁定理由来自package.json注释与 READMEvuetify4.2.2精确升级前必须在 Chrome 里实测照片编辑对话框的 Country / Time Zone 自动补全菜单必须保持打开且随输入过滤3.12.3→3.13.x 曾导致长自动补全和选择菜单在点击时关闭。UI 保持 Vuetify 3 外观依赖vite.config.mjs、src/css/vuetify/settings.scss、layers.css、vuetify-v3.css、vuetify-overrides.css与src/app.js的配套。axios1.20.0精确精确版本让传输层升级获得显式评审与验证升级时需检查请求取消、认证、重定向与错误处理——src/common/api.js的错误拦截器负责被取消的导航请求。3.2 overrides 层与 ESM 升级阻碍frontend/package.json与根package.json声明了匹配的overridesserialize-javascript: ^7.0.5npm 以根声明的为准两处必须保持对齐。ESM-only 升级阻碍与版本地板engine floor在 README 的Major-Version Upgrade Constraints表中登记例如vue3-gettext4.x、vue-router5.x、cssnano9.x、jsdom30.x 都要求先做兼容性评估。3.3 npm workspace 的正确安装姿势npm 是 workspace 结构根package.json拥有 workspace根package-lock.json是唯一锁文件必须从仓库根目录而不是frontend/运行npm ci --ignore-scripts --no-audit --no-fund --no-update-notifier # 全新安装 npm install --ignore-scripts --no-audit --no-fund --no-update-notifier # 变更依赖后--ignore-scripts是默认纪律生命周期脚本默认禁用需要重建原生 addon 时显式npm rebuild --ignore-scriptsfalse package。依赖变更后还要跑make audit、make build-js、make test-js、make notice。3.4 新增 / 删除依赖前必须做的孤儿审计删除一个看起来没用了的依赖前用rg与npm ls双重确认没有传递消费者或 peer 依赖rg -nF pkg frontend ... # 从仓库根执行含 plus/pro/portal 覆盖层与配置 npm ls pkg --all规则列举的真实先例postcss-url、vitejs/plugin-react、cheerio、testing-library/react、vite-tsconfig-paths都曾是消费者离开后真孤儿并被移除。MapLibre style-spec 的minimist则因传递依赖仍留在锁文件中——不要手工删除需要的传递条目。4. 测试入口Vitest 与 TestCafe4.1 单元测试Vitest任务命令仓库根单元与组件测试make test-jsWatch 模式make vitest-watch覆盖率make vitest-coverage组件专项make vitest-component铁律永远通过 npm/make 包装调用 Vitest绝不裸跑npx vitest run。frontend/package.json的test脚本用cross-env包了一层环境变量test: cross-env TZUTC BUILD_ENVdevelopment NODE_ENVdevelopment BABEL_ENVtest vitest run没有这些环境变量时约 50 个组件测试Vuetify 渲染、chip-selector、登录、location-input、batch-edit、people-tab、lightboxtoggleSidebar等以及依赖时区的日期测试会假性失败——看起来像真实失败但只在裸调用下复现。因此不要把裸npx vitest run的N failed, M passed与make test-js基线对比。临时筛选单个文件时显式镜像环境(cd frontend TZUTC BUILD_ENVdevelopment NODE_ENVdevelopment BABEL_ENVtest npx vitest run path)测试池是forks而非vmForks见frontend/README.mdsanitize-html依赖 ESM-only 的htmlparser2真实 Node 进程通过原生require(ESM)加载VM 执行器做不到Vuetify 在三个vitest.config*.mjs中被test.server.deps.inline: [/vuetify/]内联让 Vite 转换其 CSS 导入而非交给 Node。4.2 验收测试TestCafe按 testID 跑单例规则给出的一次性 TestCafe 流程从仓库根开始make storage/acceptance make acceptance-sqlite-restart-1 make wait-1 (cd frontend npm run testcafe -- chrome --headlessnew --use-glangle --use-angleswiftshader --disable-featuresLocalNetworkAccessChecks --config-file ./.testcaferc.cjs --test-meta modepublic,typeshort,testIDcomponents-001 tests/acceptance) make acceptance-sqlite-stop-1四个要点模式规则必须有数字后缀acceptance-sqlite-restart-%、wait-%、acceptance-sqlite-stop-%是 pattern rules缺后缀的make acceptance-sqlite-restart会报 No rule to make target。回到仓库根再执行 stopacceptance-sqlite-stop-1前必须cd回根目录。运行器配置是frontend/.testcaferc.cjs本仓库没有testcaferc.json——那个属于独立的photoprism-tests套件。--test-meta的键是 AND 关系目标测试必须声明全部键照抄typeshort去筛只声明mode: public的用例如moments-003会筛出空集。过滤前先查该用例的test.meta(...)调用。frontend/.testcaferc.cjs中的实际配置包括skipJsErrors: true、quarantineMode: false、selectorTimeout: 3500、assertionTimeout: 3500、retryTestPages: true、失败自动截图到tests/acceptance/screenshots/以及一个为 spec 报告器追加测试耗时的onBeforeWrite钩子。测试布局与更多工具版本见 frontend/tests/README.md。4.3 一个已知的测试坑隐藏路由的 UI 检查/library/hidden或/portal/hidden要求同时满足files.file_error与photos.photo_quality -1单设file_error不会让该行浮出水面。5. 焦点管理共享 view helper 的完整模式PhotoPrism 通过 frontend/src/common/view.js 中的View类全局以$view形式安装维护可预测的焦点跟踪当前活动组件、视图切换时应用焦点并在打开的对话框内困住焦点保证 Tab 永远不会漏到遮罩后的页面。详细指南见 frontend/src/common/README.md。5.1 Tabindex 速查值何时使用效果0自然 Tab 顺序中的交互控件参与顺序键盘焦点-1程序化焦点目标对话框容器、哨兵元素可被脚本聚焦但 Tab 跳过正数避免自定义 Tab 顺序难维护view helper 无法得知第一个元素实用建议根页面容器div classp-page ...用tabindex-1让路由激活时 helper 先聚焦容器、再立即移到第一个交互控件按钮、输入框、链接保持默认tabindex0。5.2 对话框实现的五步清单Vuetify 对话框被 teleport 到 overlay 容器因此一致的 ref 与生命周期钩子至关重要。标准形态来自common/README.mdv-dialog refdialog :model-valuevisible persistent max-width350 keydown.esc.exactclose keyup.enter.exactconfirm after-enterafterEnter after-leaveafterLeave v-card refcontent tabindex-1 !-- dialog body -- /v-card /v-dialogexport default { methods: { afterEnter() { this.$view.enter(this); }, afterLeave() { this.$view.leave(this); }, close() { this.$emit(close); }, confirm() { this.$emit(confirm); }, }, };要点refdialog让 helper 通过ref.contentEl拿到 teleport 后的 overlay$view.enter/leave是强制的helper 据此决定何时困住或释放焦点第一个可聚焦控件保持tabindex0helper 以它为 Tab 兜底。嵌套对话框时每个对话框都必须有自己的refdialoghelper 以当前活动组件$view.getCurrent()为陷阱所有者打开第二个对话框会自动暂停第一个的陷阱。5.3 键盘事件与全局快捷键转发优先keyup如keyup.enter.exactconfirm让容器内的输入元素先收到keydown需要保留按键时可event.stopPropagation()。persistent对话框必须用keydown.esc.exactcloseVuetify 内建 Escape 处理器在拒绝关闭时会播放抖动拒绝动画直接挂 keydown 监听可以覆盖内建处理器并抑制动画同时仍允许内部输入先取消事件。全局onShortCut(ev)钩子单独不足以覆盖 Vuetify 内建行为。全局快捷键走 view.js 中View.onKeyDown注册的单一keydown监听它只转发 Escape 与ctrl/meta组合键!ev.ctrlKey !ev.metaKey ev.code ! Escape直接返回且要求当前组件实现了onShortCut(ev)。onShortCut返回true时 helper 执行preventDefault()返回false则回退浏览器原生行为——所以不要指望它转发任意按键。对话框内嵌菜单如 combobox 建议列表要确认与全局陷阱兼容onDocumentFocusOut有一个专门的同级菜单门当焦点从输入移入v-autocomplete/v-select/v-comboboxteleport 到body的列表项时relatedTarget位于可见的、与对话框 overlay 同级的.v-menuoverlay 内陷阱会放行避免菜单被立刻拉回关闭见 view.js。5.4 排查清单要点焦点逃出对话框确认调用了$view.enter/leave、模板有refdialog、卡片内至少有一个tabindex0控件纯静态内容无法困住焦点。焦点落在遮罩而非按钮检查子元素上是否残留tabindex-1只有外层容器应该用-1可用this.$config.get(trace)开跟踪日志观察document.focusin/out由谁接收。自定义focusout与陷阱打架handler 必须检查this.$view.isActive(this)重定向焦点时调用ev.preventDefault()必要时干脆删掉自定义 handler。6. 翻译与本地化工作流6.1 铁律用户可见字符串必须走 gettext模板与脚本中绝不硬编码本地化字符串每个用户可见字符串必须经过$gettext/T使其进入frontend/src/locales/translations.pot。例外——标准化的技术标识符保持不译协议/缩写/标识符类字段标签与选项值按字面量渲染不走$gettext例如Client ID、Client Credentials、OIDC、UUID、Node UUID。翻译它们只会增加目录噪音并带来歧义渲染例如 Client 被翻成德语 Kunde。普通英文字段名如Site URL、Advertise URL则保持翻译。6.2 角色/提供者标签共享一份不重复列出显示名只存在一次于frontend/src/options/auth.jsRoles()/Providers()映射以及RoleOptions(keys, labelKey)/ProviderOptions(keys, labelKey)构建器。私有版本plus/pro/portal导入这些映射并传入自己的键列表——可选项集合确实因版本而异cluster_admin、LDAP/AD、精简的 Plus 集合但标签不得重复罗列。6.3 大小写约定Title Case工具提示、标签、按钮、占位符、:title/:aria-label、简短祈使短语Zoom In、Toggle Thumbnails、Add to Album、Edit Date Time。Sentence case连续行文、完整句子、通知、确认对话框正文Failed to save changes。小写化只适用于冠词a/an/the、短连词and/or/but与 ≤3 字母的介词in/to/for/on且不在句首时。frontend/src/locales.js的Messages块如Previous page、Go to page {0}是 Vuetify 强制原样采用的 UI 消息豁免大小写规则也不要当作参照来抄。仅为修正大小写就重命名已翻译字符串会破坏人工翻译需按 best-practices C6 权衡。6.4 提取、编译与 lint一条完整流水线提取extraction的事实来源是根make gettext-extract经scripts/gettext-extract.sh扫描frontend/src以及plus/frontend、pro/frontend、portal/frontend覆盖层。Weblate 从不运行提取器它只会自动翻译已在目录中的字符串因此新的$gettext字符串只有在开发者运行make gettext-extract并提交更新的.pot/.po后才会到达 Weblate。这类改动应独立成一次 Regenerate translation catalogs 提交而不是混进功能提交。编译产物是按语言分文件的frontend/src/locales/json/*.jsongit 跟踪每语言一个由gettext.config.js的splitJson控制——没有单一translations.json由common/config.js懒加载。改.po/.pot后运行npm run gettext-compile并提交更新的json/*.jsonmake build-js也会重新生成。注意make gettext-extract还会合并后端assets/locales/**目录别把无关的后端改动带进纯前端的目录提交。避免纯标点类 gettext 键如$gettext(—)它们只会弄脏translations.pot。目录完整性闸门任何.po/.pot修改后运行make gettext-lintscripts/gettext-lint.mjs。它会标记msgid与msgstr的占位符集合不匹配前端%{name}、后端 printf 动词、边缘空白漂移、msgfmt -c的 c-format 致命错误以及覆盖率——frontend/src或覆盖层中活着的$gettext字面量从未进入translations.pot。msgid内的首尾/内部空白在运行时是中性的vue3-gettext加载与查找时会 trim 折叠键所以尾随空格发现属于清洁度问题而非正确性问题。检查只看到活跃条目解析器丢弃所有#行因此#~废弃块内的占位符损坏在对应msgid被重新提升翻译前是不可见的。gettext.config.js 展示了提取配置的落地input的include覆盖src/**/*.{vue,js,ts}排除src/common/gettext.jsoutput定义potPath: translations.pot、jsonPath: json、splitJson: true、flat: true并从src/locales/*.po自动生成语言列表。6.5 两个高价值坑原生 void 元素与尾随空格裁剪原生 void 元素会悄悄杀死其后续兄弟的 gettext 提取。img、br、input、hr等会让序列化标记无法闭合导致同一父元素下每一个裸的{{ $gettext(…) }}从目录中消失在所有语言环境下都渲染英文。修复把插值包进元素span{{ … }}/spanfrontend/eslint.config.mjs中的vue/no-restricted-syntax规则会在make lint-js时标记该模式选择器匹配img|br|hr|input|area|base|col|embed|link|meta|param|source|track|wbr之后紧跟的$gettext/$npgettext调用。自闭合组件v-icon /、嵌套 void 元素、子元素内的插值都不受影响。裁剪源字符串尾随空格必须三处一致模板 .po.pot先裁剪字面量再make gettext-extract。注意msgmerge --no-fuzzy-matching会把裁剪后的msgid当作新条目清空其msgstr并把旧翻译移到废弃#~块——所以在make gettext-compile之前要把旧翻译迁移到活跃的裁剪条目上折叠多行#~ msgstr否则每个语言环境都会渲染未翻译。这只填充msgstrWeblate 可以干净地合并。7. Web 模板与共享资源7.1 HTML 入口与浏览器能力检查HTML 入口在assets/templates/index.gohtml、app.gohtml、app.js.gohtml、splash.gohtml。assets/static/js/browser-check.js 在主 bundle 之前运行能力检查它必须在app.js.gohtml中先于bundle 脚本加载且不要给 bundle 标签加defer/async除非重新引入有守卫的加载器。browser-check.js刻意保持 ES5 兼容不支持Promise、Symbol、fetch、URL、URLSearchParams、AbortController、CSS 相对颜色语法、CSS 级联层、CSS.registerProperty、Intl.Segmenter、Object.hasOwn或script.noModule的旧浏览器能在无法解析 bundle 的情况下先显示不支持浏览器提示。7.2 浏览器兼容基线三处必须同步基线是Chrome 与 Edge 119、Firefox 128、macOS/iOS 上的 Safari 16.4由三处共同声明改动时必须三处一起改frontend/package.json的browserslist查询chrome 119,edge 119,firefox 128,safari 16.4,ios_saf 16.4等frontend/vite.config.mjs的BROWSER_TARGET[chrome119, edge119, firefox128, safari16.4, ios16.4]见 vite.config.mjsassets/static/js/browser-check.js的运行时能力门。pdf.js worker 需要Promise.withResolversSafari 17.4 起才有由frontend/src/common/pdf-worker.js与frontend/src/common/with-resolvers.js在缺失处定义浏览器下限提升到 Safari 17.4 后即可移除。7.3 OIDC 回调与命名空间存储的一致性OIDC 登录完成通过assets/templates/auth.gohtml桥接把会话写入命名空间浏览器存储——它必须与frontend/src/common/session.js、frontend/src/common/storage.js以及frontend/src/page/auth/login.vue中的登录表单开关保持一致。storage 命名规则来自 frontend/src/common/storage.js 与common/README.md键格式为pp:storageNamespace:key无命名空间时回退前缀是pp:root:。会话恢复要求同时存在session.token与session.id只写 token 不会在启动时产生已认证会话。可选元数据键session.provider、session.user、session.scope、session.data、session.error。登录流键login.next深链目标由全局路由守卫经$session.setLoginRedirectUrl(to.href)捕获/login路由经hasLoginRedirectUrl()读取、认证后followLoginRedirectUrl()消费与login.logout一次性信号让下一次深链到达跳过自动 OIDC 跳转存在命名空间localStorage中、session.*前缀之外从而在 OIDC 回调的清理清单中幸存。存储偏好标志总是存于命名空间localStorage的session键下true表示活跃会话用命名空间sessionStorage其他值回退命名空间localStorage。登录页的Stay signed in on this device开关即此偏好。会话恢复测试应同时覆盖首选sessionStorage与真实Config形状对象防止命名空间解析 bug 静默回退pp:root:。7.4 覆盖层与共享 partialloader partial 被pro/assets/templates/index.gohtml与portal/assets/templates/index.gohtml复用只要app.js.gohtml或 bundle 加载方式变化都要验证它们仍包含该 partial。Plus 没有模板覆盖层、直接渲染 CE 模板所以plus/assets/无需检查plus/下唯一的index.gohtml是构建产物。Splash 样式在frontend/src/css/splash.css新增 splash 元素应放这里以保证跨版本一致。8. 从规则到源码的验证线索本仓库为上述每条规范提供了可验证的实现锚点状态管理common/config.js、common/session.js、common/clipboard.js、common/log.js等 reactive 单例以及app/session.jsModel 语义frontend/src/model/model.js快照/diff/回滚/flagEnabled/setFlag/getWriteOnly与 frontend/src/model/settings.jsload/save只传 difffrontend/src/model/下共 21 个模型文件焦点管理frontend/src/common/view.js 的View类enter/leave/apply/onKeyDown/onDocumentFocusOut/disableNativeZoom与 frontend/src/common/README.md 的完整模式格式化与 lintfrontend/eslint.config.mjsflat config、vue/no-restricted-syntax守卫与 frontend/.prettierrc.json依赖与 workspacefrontend/package.json含//vuetify锁定注释与overrides、frontend/README.md锁定理由表、孤儿审计命令、升级约束表测试工具链frontend/tests/README.md、frontend/.testcaferc.cjs、三个vitest.config*.mjs本地化frontend/gettext.config.js、scripts/gettext-extract.sh、scripts/gettext-lint.mjs、frontend/src/locales/translations.pot与按语言分文件的frontend/src/locales/json/*.json浏览器基线frontend/vite.config.mjs 的BROWSER_TARGET、assets/static/js/browser-check.js、frontend/package.json的browserslist。在提交代码前把本文档当作检查清单过一遍组件是否 Options API 且无 TS新状态是否落在既有 reactive 单例-1/0/1选项是否用flagEnabled/setFlag且默认值是数字0格式化是否走了make fmt-jsmake lint-js测试是否经由make test-js而非裸npx vitest run新字符串是否经过$gettext并触发一次独立的make gettext-extractgettext-lint提交浏览器基线改动是否三处同步——做到这些你的改动就能与 PhotoPrism 前端工程体系无缝衔接。赞分享后端前端图像处理人工智能AI 应用【免费下载链接】photoprismAI-Powered Photos App ✨项目地址https://gitcode.com/gh_mirrors/ph/photoprism点击查看免费下载相关推荐终极Odoo开发指南Python风格与前端规范最佳实践终极Odoo开发指南Python风格与前端规范最佳实践 Odoo作为一款开源企业应用套件其代码规范与最佳实践对于项目的可维护性和扩展性至关重要。本文将详细介企业应用后端电商mcp-go 开发规范指南测试、代码风格与代码生成的工程实践mcp go 开发规范指南测试、代码风格与代码生成的工程实践 导读 本文以 mcp goModel Context Protocol 的 Go 实现仓库根人工智能MCP 服务MCP Clients如何扩展Angular Full Stack微服务架构改造与高并发处理方案如何扩展Angular Full Stack微服务架构改造与高并发处理方案 Angular Full Stack是一个基于MEAN技术栈的企业级全栈应用模板后端示例工程上一篇drawio-desktop免费离线流程图绘制一条命令转换 Visio下一篇Fay框架API文档暗黑模式超链接下划线配置指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表