资讯动态

freehire 岗位页 “See also“ 卡片图标体系:从空白 PNG 到四类可识别标识

发布时间:2026/10/23 2:47:48 来源:尧图企业网站定制
【免费下载链接】freehirefreehire — the open-source search engine for job seekers项目地址https://gitcode.com/GitHub_Trending/fr/freehire点击查看免费下载本文以 freehire 仓库中的设计方案docs/superpowers/specs/2026-08-13-see-also-marks-design.md为主线完整梳理岗位详情页 See also 横滑卡片的图标体系改造如何把标题 岗位数的纯文本墙变成由品牌 logo、国家旗帜、语义化家族图标与原有 backer 图片共同构成的四类标识并给出已落地实现web/src/lib/seeAlsoMark.ts、web/src/lib/components/CollectionMark.svelte、web/src/lib/techmarks.ts的源码级说明、参数解析链路与测试验证方式。读完后你可以独立新增一个带品牌图标的 skill 集合、排查图标为什么不显示并理解解析优先级为何不能随意调整。问题一整排长得一模一样的卡片JobSeeAlso组件负责岗位页底部的 See also 区块它渲染一行指向既有/collections/:sluglanding 页的链接例如 Python jobs、Senior-Level jobs、AWS jobs每张卡片带一个实时开放岗位数 pill。设计方案对改造前的状态描述得很直接The See also row (web/src/lib/components/JobSeeAlso.svelte) on a job page links to related/collections/:slugpages… Today every card is visually identical — a title and a job-count pill — except the rare backer-collection card (YC, Techstars) that gets a small square PNG mark. The row reads as a flat wall of text and is hard to scan.也就是说除极少数 backer 集合YC、Techstars 等有一枚小方形 PNG 之外每张卡片视觉完全相同整行读起来像一堵平铺的文字墙横向滚动时缺乏可扫描的视觉锚点SeeAlsoCard上的mark字段是string | null为null时组件用{#if card.mark}直接不渲染图标槽位于是空白卡片成了常态。目标每张卡片都必须有一个可识别的圆形标识设计方案的 Goal 部分界定了改造边界这一点很关键——这是标识与细节的改造不是布局重构Give every card a recognizable circular mark: a real brand logo for technology/cloud/tool collections, the countrys flag for country collections, and a color-coded family icon for everything else (seniority, work mode, role/category). Keep the rows current horizontal-scroll layout — this is a mark/detail change, not a layout redesign.具体拆成四条产出技术 / 云 / 工具类集合 → 真实品牌 logo国家类集合 → 该国国旗其余seniority、work mode、role/category→ 按家族着色的语义图标不再存在空白卡片分支。核心抽象三种 kind一个字段设计方案把SeeAlsoCard.mark从图片 URL 字符串或 null升级为可辨识联合discriminated unionexport type SeeAlsoMark | { kind: image; src: string } // unchanged: existing backer PNG (backers.ts) | { kind: logo; path: string; hex: string } // brand SVG path data brand color, from simple-icons | { kind: flag; countryCode: string } // renders CountryFlag code{countryCode} / | { kind: family; icon: FamilyIconName; color: string }; // Lucide icon family color这段类型最终落在 collections.ts并比设计稿多带一个title用于 logo 的无障碍名称export type SeeAlsoMark | { kind: image; src: string } | { kind: logo; title: string; path: string; hex: string } | { kind: flag; countryCode: string } | { kind: family; icon: FamilyIconName; color: string };类型被放在collections.ts而不是路由文件里注释给出了原因服务端 load 计算它、组件渲染它双方都必须从同一处导入避免类型漂移。承载它的卡片类型 SeeAlsoCard 仍是{ slug, title, count, mark }四字段。由于mark变为必有不再可空组件里原来的{#if card.mark}守卫也随之消失。这一点在设计中写得很明确Every card gets a mark now — there is no longer a blank card case.JobSeeAlso.sveltes{#if card.mark}guard goes away.解析顺序四个分支的优先级与理由设计稿规定的解析顺序按已解析的集合 slug逐条判断Backer 集合COLLECTIONS.find(c c.kind backer)→{ kind: image, src: backerBadges([slug])[0].mark }与改造前一致带skills参数的FilterCollection→ 查技术标识注册表命中 →{ kind: logo, ... }未命中 → 回落到家族图标永远不会渲染成破图带countries参数的FilterCollection单一 ISO 码如{ countries: de }→{ kind: flag, countryCode }其余一切category、seniority、work_moderegions、work_modecountries: global这类不含具体国家的条目→ 家族图标家族由存在哪个参数键推导。设计稿特别强调这种做法的经济性This mirrors the existingparamsshape onFilterCollection— no new metadata field on the 90-entry registry, family is derived from the param key already there.即不新增元数据字段家族由params的键直接推导。落地实现纯函数resolveSeeAlsoMark实现阶段把这段解析从page.server.ts内联逻辑中抽成了可单测的纯函数见 seeAlsoMark.tsexport function resolveSeeAlsoMark( params: Recordstring, string | null, backerImageSrc: string | null ): SeeAlsoMark { if (backerImageSrc) return { kind: image, src: backerImageSrc }; if (!params) return familyMark(tech); if (params.skills) { const tech TECH_MARKS[params.skills]; if (tech) return { kind: logo, title: tech.title, path: tech.path, hex: tech.hex }; return familyMark(tech); } if (params.countries) return { kind: flag, countryCode: params.countries }; if (params.ai_archetype || params.employment_type || params.relocation) return familyMark(role); if (params.domains || params.company_type) return familyMark(company); if (params.seniority) return familyMark(seniority); if (params.category) return familyMark(role); if (params.work_mode) return familyMark(remote); if (params.collections) return familyMark(company); return familyMark(tech); }几点值得注意的实现细节backer 图片拥有最高优先级。函数签名把backerImageSrc作为入参而不是在内部查backers.ts这样解析函数对品牌图片从哪来零耦合测试也能直接注入。params null回落tech注释说明这是 backer-only 集合的情况这类集合不带任何 facet 参数。判断顺序即优先级。skills在countries之前seniority在category之前。因为一个集合可能同时带多个键例如senior-backend是{ category: backend, seniority: senior }谁先被读到决定了卡片形态把seniority放在category之前使Senior Backend这类落地页画成TrendingUp的绿色晋升图标而不是紫色的角色分层图标。比设计稿多出的两类键ai_archetype/employment_type/relocation归入roledomains/company_type/collections归入新增的company家族。collections分支的注释解释得很清楚——backer 类在上面已经通过backerImageSrc返回了走到这里说明这个公司归属类集合没有真实品牌标识。familyMark只是一层薄封装把家族名换成带颜色的联合成员function familyMark(icon: FamilyIconName): SeeAlsoMark { return { kind: family, icon, color: FAMILY_MARKS[icon].color }; }服务端调用点buildSeeAlso()解析函数在 web/src/routes/jobs/[slug]/page.server.ts 的buildSeeAlso()中被调用位置与设计稿所述一致——正是在它今天已经在解析图标的地方return resolvedLinks.map(({ link, resolved }): SeeAlsoCard { const backerImageSrc backerBadges([link.slug])[0]?.mark ?? null; if (!resolved) { return { slug: link.slug, title: link.title, count: null, mark: resolveSeeAlsoMark(null, backerImageSrc), }; } const { filter, lastKey, lastValue } splitParams(resolved.params); const groupKey new URLSearchParams(filter).toString(); const count readCount(groupedFacets.get(groupKey) ?? null, lastKey, lastValue); const mark resolveSeeAlsoMark(resolved.params, backerImageSrc); return { slug: link.slug, title: link.title, count, mark }; });可以看到mark与count在同一趟 map 里算完且backerBadges([link.slug])[0]?.mark ?? null是唯一的图片来源。count的取值逻辑还隐藏着一个与本主题相关的设计约束readCount在 facets 分布缺失时返回null而不是 0注释说明缺失条目意味着 Meilisearch 的MaxValuesPerFacet上限把该值挤掉了——降级为未解析而不是伪造 0 个岗位。这与图标缺失即降级、绝不破图是同一种产品姿态。技术标识注册表techmarks.ts设计稿要求新建web/src/lib/techmarks.ts形状与精神对齐backers.ts的MARKS一张手工维护的Record键是我们自己的集合 slug值来自simple-icons的具名导入。import { siPython, siAmazonaws, /* … */ } from simple-icons; export const TECH_MARKS: Recordstring, { path: string; hex: string } { python: siPython, aws: siAmazonaws, azure: siMicrosoftazure, gcp: siGooglecloud, csharp: siCsharp, cpp: siCplusplus, nodejs: siNodedotjs, nextjs: siNextdotjs, dotnet: siDotnet, // … one entry per skills value in FILTER_COLLECTIONS (~44 today) };为什么必须逐条具名导入设计稿对此有硬性要求Import icons individually (import { siPython, siAmazonaws, siMicrosoftazure, ... } from simple-icons) so the bundler tree-shakes everything unused — do not import the packages default export.如果导入包的默认导出一个包含全部图标的巨大对象tree-shaking 就无法裁掉未使用的图标客户端包体会被整套品牌库撑大。因此 techmarks.ts 里是两百多行具名 import随后在TECH_MARKS中逐一映射。simple-icons在 web/package.json 中作为新依赖加入与既有的lucide/svelte并列。缺失即降级不需要覆盖率测试A collection slug with askillsparam and no entry here silently falls back to the family icon — same we dont have a verified mark, so show nothing brand-specific posture asbackers.ts, just with a non-empty fallback instead of a blank card. No test needs to enforce full coverage; missing entries degrade, they dont break.这个姿态与backers.ts完全同源。后者注释写着a plausible-looking wrong mark is worse than none并且给出了真实案例logo 代理按公司名匹配时a16z 会指向一个无关的 Substack 页面所以 backer 图片改为人工提交到web/static/brands。落地后的三条重要事实实际落地的 techmarks.ts 注释记录了设计评审之后追加的工程经验值得单独抄录批量扩表的正确做法初版是约 40 个手工精选条目之后用完整技能字典SKILL_LABELS与 simple-icons 的每个slug求精确交集再逐个核对候选条的sourceURL 是否真的指向我们标签所指的工具/语言/公司。精确 slug 匹配不等于同一身份有三个候选被刻意剔除elk→ 指向 ElkMastodon 客户端而非 Elastic ELK 栈hive→ 指向 Hive 智能温控器品牌而非 Apache Hivebackbone→ 指向 backbone.com 的媒体资料包而非 Backbone.js。上游商标下架导致永久缺口simple-icons中不含 AWS、Azure、Java、C#因商标权利人下架请求被移除所以这四个skillsslug没有条目在 See also 卡片里显示通用 tech 家族图标在 skill chip 中则什么都不画——这正是未来任何新技能在有人补录前的默认行为。对照仓库现状可以确认缺口是真实存在的TECH_MARKS有 209 个条目而带skills参数的FilterCollection有 45 个其中aws、azure这类注定命中不到。测试里也把aws作为回落用例的输入见后文。图标对比度用 YIQ 一次算好不写例外清单设计稿里这条要求很具体Icon fill contrast: background is the brands ownhex(a filled circle); the SVG path is drawn white or near-black depending on the backgrounds luminance (YIQ formula, threshold ~128), computed once per mark at render time — not hardcoded per brand. This is whyreacts pale cyan background gets a dark glyph andawss orange gets a white one, without a manual exception list.实现落在 markColor.ts// Which glyph fill to draw over a brands own background color, so a logo or // family icon stays legible regardless of how light or dark that brands hex is. const LUMINANCE_THRESHOLD 128; export function glyphColorFor(hex: string): string { const value hex.replace(#, ); const r parseInt(value.slice(0, 2), 16); const g parseInt(value.slice(2, 4), 16); const b parseInt(value.slice(4, 6), 16); const yiq (r * 299 g * 587 b * 114) / 1000; return yiq LUMINANCE_THRESHOLD ? #16181d : #ffffff; }YIQ 中绿色权重最高587、蓝色最低114符合人眼亮度感知。阈值 128 恰好切在中间灰度因此亮色底React 的浅青得到近黑字形#16181d深色底得到白色字形。这就是为什么不需要为任何品牌维护文字用什么颜色的例外表——新增一个品牌只要给出hex对比度自动成立。这一函数同时服务于logo与若需要家族图标保证深色主题下也不会出现深底深字。家族图标五个家族、一张固定查找表设计稿给出的映射表并说明颜色取自设计系统语义色阶而非新造颜色Param key presentFamilyLucide iconColorskills(no tech-mark hit)tech (generic)Code2indigocategoryroleLayersvioletseniorityseniorityTrendingUpemeraldwork_moderegions/globalcountriesremoteGlobe2cyan落地后家族扩为五个并新增company代码评审时发现公司归属类集合无标识可画。数据表见 familymarks.tsexport type FamilyIconName tech | role | seniority | remote | company; export const FAMILY_MARKS: RecordFamilyIconName, { color: string } { tech: { color: #4f46e5 }, role: { color: #7c3aed }, seniority: { color: #059669 }, remote: { color: #0891b2 }, company: { color: #d97706 }, };该文件的头部注释修正了设计稿的一处假设。设计稿称颜色取自design-system 色权重的既有语义色阶而实际注释写道这些颜色是交互式 mockup 评审时批准的外加评审后补的company不是设计系统 token——因为设计系统的调色板里没有可供取用的分类多色阶所以这里是一个小的本地例外而不是硬造一个 token。实际图标也从Code2/Globe2调整为Code/Globe。为什么数据与图标组件必须分离这是落地时一个关键的架构决定也是阅读这份代码最容易踩坑的地方。familymarks.ts 的注释说得很清楚Pure data only, nolucide/svelteimport: this is imported by seeAlsoMark.ts, which vitest runs in plain Node — a Lucide icon component is a.sveltefile the Svelte compiler must transform, so the icon components themselves are looked up in CollectionMark.svelte instead, keyed by the same FamilyIconName.也就是说familymarks.ts只放纯数据家族名 → 颜色seeAlsoMark.ts是纯函数模块被纯 Node 环境的 vitest 直接 import如果把import { Code } from lucide/svelte写进数据模块测试进程会尝试加载.svelte文件而失败因此组件映射放在.svelte侧用同一个FamilyIconName做键const FAMILY_ICONS: RecordFamilyIconName, LucideIcon { tech: Code, role: Layers, seniority: TrendingUp, remote: Globe, company: Building2, };这是一个纯逻辑与视图分层以便测试的典型案例把可测的判定逻辑推向 Node 侧把需要编译器的部分留在.svelte侧。国旗复用既有CountryFlag不新增组件{ kind: flag, countryCode }renders the existingCountryFlag.svelte… unchanged — it already fills its circular container edge-to-edge viaflag-iconsfi fisclasses. No new component.国旗原语位于 design-system/src/country-flag.svelte通过#lib/ui/index.js整体再导出freehire-design-system。它的实现细节解释了为何无需改动即可圆形铺满.fis选中方形1:1变体因此rounded-full得到正圆ring-1 ring-black/10 dark:ring-white/15让日本、尼日利亚这类白底旗在浅色背景下仍有边线可见flag-icons以em计量所以旗子随外层字号缩放非两位 ASCII 字母的 code 没有对应旗帜会退化成大写code 纯文本而不是一个空盒子const renderable $derived(/^[a-z]{2}$/.test(cc));调用处还顺带传入了countryLabel(mark.countryCode)作为aria-label与title来源是 facets.ts 的countryLabel。国旗之外的稳健性FilterCollection.flag落地后FilterCollection多出一个可选字段见 collections.ts// ISO 3166-1 alpha-2 code whose flag marks the card, for a collection whose params // name a place without pinning its country (a city). Display only — never a filter. flag?: string;城市类落地面singapore、new-york、london…的params是{ cities: Singapore }并不包含countries键因此会掉进家族图标分支而失去地域感。注释解释为何不能顺手把国家塞进paramsThe country is NOT pinned beside it: the location facets combine with OR, socitiesAustincountriesusis every US job, not the Austin ones.位置类 facet 之间是 OR 关系加countriesus会把奥斯汀变成全美。所以国家只能作为纯展示的flag字段存在。这条设计是原方案出稿后为覆盖城市集合补上的也说明图标解析应跟随集合语义、而不是跟随 facet 语义。组件改造JobSeeAlso.svelte与抽出的CollectionMark.svelte设计稿对组件层的要求是卡片的骨架一律不动只动标识槽位The card layout is untouched (horizontal scroll strip,w-40 shrink-0, border,hover:bg-muted). Only the mark slot changes:Size bumps fromsize-6(24px) tosize-7(28px) to keep a padded logo legible.Shape:rounded-full(wasrounded-sm) for every kind, backer PNG included …Render branches oncard.mark.kind落地时这一槽位被抽成了独立组件 CollectionMark.svelteJobSeeAlso.svelte只负责布局与链接a href{resolve(/collections/[slug], { slug: card.slug })} classflex w-40 shrink-0 flex-col items-start gap-2 rounded-lg border border-border p-3 transition-colors hover:bg-muted div classflex w-full items-center justify-between gap-2 CollectionMark mark{card.mark} / {#if card.count ! null} span classshrink-0 rounded-full bg-secondary px-2 py-0.5 font-mono text-xs text-muted-foreground {card.count.toLocaleString()} jobs /span {/if} /div span classtext-sm font-medium text-foreground{card.title} jobs/span /a可以看到w-40 shrink-0、边框、hover:bg-muted、no-scrollbar overflow-x-auto的横向滚动条都按设计稿原样保留count null时 pill 整体消失与缺失计数不伪造成 0一致。标识组件本体实现四个分支{#if mark.kind image} img src{mark.src} alt classsize-7 shrink-0 rounded-full object-contain / {:else if mark.kind logo} span class{badgeClass} style:background-color#{mark.hex} svg viewBox0 0 24 24 classsize-4 roleimg aria-label{mark.title} path d{mark.path} fill{glyphColorFor(#${mark.hex})} / /svg /span {:else if mark.kind flag} CountryFlag code{mark.countryCode} label{countryLabel(mark.countryCode)} classshrink-0 text-3xl / {:else} {const Icon FAMILY_ICONS[mark.icon]} span class{badgeClass} style:background-color{mark.color} Icon classsize-4 text-white / /span {/if}以及几何尺寸// Shared by the logo and family mark kinds — both are an icon centered on a // colored circle, differing only in the color and whats drawn inside. const badgeClass flex size-7 shrink-0 items-center justify-center rounded-full;逐条对照设计稿与实现设计要求实现size-6→size-7badgeClass与img均为size-7所有 kind 统一rounded-full含 backer PNGimg用rounded-full object-containimage→ 圆形化后的img分支一logo→ 品牌底色圆 内联 SVG 对比色填充分支二glyphColorFor计算 fillflag→CountryFlag code{...} /分支三family→ 家族底色圆 居中 Lucide 白色图标{:else}分支size-4 text-white不新增组件抽出CollectionMark.svelte复用而非在JobSeeAlso内联logo分支的svg使用roleimgaria-label{mark.title}这是设计稿没写但实现补上的无障碍细节圆形标识对读屏用户没有意义必须靠title才能被正确朗读。image分支则用alt——backer 的品牌语义已由卡片标题承载避免重复播报。组件抽出的连带收益CollectionMark.svelte还被 CollectionRowMark.svelte 复用{#if row.companies.length 0} CompanyLogoStack names{row.companies} / {:else if row.flags.length 1} CountryFlagStack codes{row.flags} max{4} classshrink-0 text-3xl / {:else} CollectionMark mark{row.mark} / {/if}也就是说/collections枢纽页的行数据在没有公司 logo 堆叠、也没有多国旗堆叠时会退回同一个mark。这本来被设计稿列为out of scope不改/collections卡片但抽出组件后标识渲染这一层自然被复用。需要区分清楚枢纽页的卡片样式、图标种类仍是它自己的逻辑本改造没有去动它符合presentation-only的边界。测试按集合家族断言mark.kind设计稿的验收方式Existing coverage forbuildSeeAlso()/SeeAlsoCardresolution (if any) extends to assert the rightmark.kindper collection family — backer slug →image, askills-registry hit →logo, acountriesslug →flag, everything else →family.由于解析被抽成纯函数测试可以完全脱离 Svelte 与 HTTP见 seeAlsoMark.test.tstest(a backer collection gets its brand image, regardless of params, () { const mark resolveSeeAlsoMark({ skills: python }, /brands/yc.png); expect(mark).toEqual({ kind: image, src: /brands/yc.png }); }); test(a skills param with a curated tech mark gets that brand logo, () { const mark resolveSeeAlsoMark({ skills: python }, null); expect(mark.kind).toBe(logo); if (mark.kind logo) { expect(mark.hex).toMatch(/^[0-9a-f]{6}$/i); expect(mark.path.length).toBeGreaterThan(0); } }); test(a skills param with no curated tech mark falls back to the tech family icon, () { const mark resolveSeeAlsoMark({ skills: aws }, null); expect(mark).toEqual({ kind: family, icon: tech, color: expect.any(String) }); });这几条用例的信息量值得拆开看第一条同时证明优先级即便params.skills python能命中 logo只要传入 backer 图片结果仍是image。第二条不只断言kind还校验hex是合法的六位十六进制、path非空——这两条恰好是simple-icons条目被误接例如把hex写成带#的形式时最早暴露的信号。第三条用aws做输入正对应techmarks.ts注释中AWS 因商标下架而不在库中的现实缺口把已知必然落空的场景固化成回归测试。其余用例覆盖countries → flag输入{ work_mode: remote, countries: de }断言countryCode: de、seniority → seniority、category → role、work_mode regions → remote、collections → company以及resolveSeeAlsoMark(null, null) → tech。注意{ work_mode: remote, countries: de }这条恰好验证了顺序 3 优先于顺序 4同时含work_mode与具体国别时输出国旗而非远程图标。说明markColor.ts也有独立测试web/src/lib/markColor.test.ts用于固定阈值两侧的取色行为resolveSeeAlsoMark的用例不涉及它因为颜色计算发生在渲染前的最后一刻。视觉验收一行里凑齐四类标识设计稿给的验收动作可直接照做Visual check: load a job page whose facets hit all four kinds in one row (e.g. a senior Python AWS remote-Germany job) and confirm no broken images, readable contrast on both light and dark brand-color backgrounds.这个例子之所以精妙是因为它一次命中了四个分支的全部边角卡片来源解析结果验证点pythonlogosiPython蓝色底 白字logo 底色/字形对比度awsfamily: tech#4f46e5靛蓝 Code品牌库缺口 → 非空降级无破图seniorfamily: seniority#059669绿 TrendingUp家族推导取seniority键remote-germany{ work_mode: remote, countries: de }flag: de国旗正圆、白底旗有描边再切到深色主题检查一遍即可覆盖设计稿要求的light and dark brand-color backgrounds 都可读。若要额外验证image分支可找一个带有 YC / Techstars 等 backer 集合的公司的岗位页确认方形 PNG 在圆形裁切下仍居中可辨。明确不做的事改造边界设计稿的 Out of scope 一节把范围钉得很死这对判断 PR 是否越界非常有用不改变哪些集合出现在 See also 里relatedCollectionSlugs、POPULAR_COLLECTION_FALLBACK——本改造只是对既有卡片集合做展示层变更。仓库中 relatedCollectionSlugs 仍是任务 facet 命中 → 公司归属 → 热门池补齐的三段式函数签名与target 10的默认值均未变。不改/collections枢纽页自身的卡片只改岗位页的 See also 行。后续可以复用TECH_MARKS/ 家族图标但不是本次改动。上文的CollectionMark复用属于组件层复利不是对枢纽页视觉的重新设计。不新增 backer 标识、不改backers.ts。实际落地时MARKS表看起来扩展到 13 个品牌但那是独立的编辑性补充不是本设计的解析逻辑改动。公司页若复用JobSeeAlso自动继承本次改动——同一组件、同一SeeAlsoCard类型。关于最后一点有一点必须点明设计稿写作时认为buildSeeAlso()内联在路由中所以同一组件自动继承而落地后解析被抽到seeAlsoMark.ts任何调用方岗位页buildSeeAlso、枢纽页loadCollectionSections只要走resolveSeeAlsoMark就共享同一套优先级与降级行为。这正是把判定逻辑做成纯函数的长期价值。踩坑与维护指南结合设计稿与源码注释可以把这份体系的维护要点归纳为四条1. 新增一个技术集合时先确认商标可用。在TECH_MARKS里加条目之前必须核对simple-icons的sourceURL 是否指向你要表达的那个实体。elk/hive/backbone三个精确 slug 匹配就是反例AWS / Azure / Java / C# 则是上游已下架的永久缺口。漏加不会报错——卡片会自动落到 tech 家族图标。2. 不要试图用测试强制TECH_MARKS全覆盖。设计稿明确说 No test needs to enforce full coverage; missing entries degrade, they dont break. 强行加覆盖率断言只会把商标下架的既成事实变成 CI 红灯。3. 调整解析顺序要非常小心。resolveSeeAlsoMark里每个if的先后都是有语义的backer 优先于一切、skills优先于countries、seniority优先于category把一个键上移就可能让Senior Backend从绿色晋升图标变成紫色角色图标。修改后必须同步跑seeAlsoMark.test.ts。4. 颜色与图标组件不要跨层引用。颜色放familymarks.ts纯数据Node 可跑Lucide 组件放.svelte品牌 SVG 放techmarks.ts对比度统一走glyphColorFor不要给个别品牌写死字形颜色。小结freehire 的这次改造把岗位页底部一排无法扫描的文字卡片升级为一套由数据驱动、可降级、可测试的标识体系用一个四成员的可辨识联合image/logo/flag/family替换可空的图片 URL从类型层面消灭空白卡片用params的键推导家族避免了给上百条集合注册表新增元数据字段品牌色底 YIQ 阈值取字形色让 200 品牌无需维护对比度例外表纯数据 / 纯函数 /.svelte组件的三层切分使解析逻辑在纯 Node 的 vitest 下即可完整覆盖每一处缺失无品牌条目、无 facet、无计数都收敛到降级而非破损与仓库里backers.ts、readCount的一贯姿态一致。需要进一步深入时建议按 seeAlsoMark.ts → techmarks.ts → CollectionMark.svelte → seeAlsoMark.test.ts 的顺序阅读若关心卡片集合是怎么选出来的再看 collections.ts 的relatedCollectionSlugs与 岗位页 load 中的buildSeeAlso。赞分享【免费下载链接】freehirefreehire — the open-source search engine for job seekers项目地址https://gitcode.com/GitHub_Trending/fr/freehire点击查看免费下载相关推荐freehire 程序化 SEO 合集实战实时职位数标题与职位页 See also 内链设计freehire 程序化 SEO 合集实战实时职位数标题与职位页 See also 内链设计 本文以 freehire 仓库中的《Programmatic Sfreehire 背书徽章Backer Badge设计与实现在职位卡片上渲染 Y Combinator、Techstars 与 a16z 品牌标识freehire 背书徽章Backer Badge设计与实现在职位卡片上渲染 Y Combinator、Techstars 与 a16z 品牌标识 freOpenCvSharp车牌识别系统从定位到字符识别OpenCvSharp车牌识别系统从定位到字符识别 1. 引言车牌识别的技术挑战与解决方案 你是否曾在停车场出入口等待识别系统卡顿而焦躁是否在开发车辆管理计算机视觉图像处理上一篇brpc 从零开始构建指南、支持环境与快速验证Getting Started 全解析下一篇pstack贡献指南如何提交PR并通过生成器与测试检查创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑