14 · 可复制的界面代码配方
适用:把设计要求落实成代码。示例采用原生 Web 能力与 Vue 3;生产项目优先使用已验证的项目组件与请求库。
CODE-01 · 在独立作用域使用 token
下载 tokens.css 后,按项目路径引用。它只作用于 .ui-kit,不会直接重置整个站点。
<link rel="stylesheet" href="/ui-ux/tokens.css">
<section class="ui-kit" data-palette="forest" data-mode="light">
<h2>项目</h2>
<p class="ui-muted">继续处理最近的项目。</p>
<button class="ui-button ui-button-primary" type="button">创建项目</button>
</section>
上面的片段只展示样式,按钮业务需要接入。完整的静态布局参考可打开 starter.html,交互行为见实验室。迁移时一起复制资源并调整路径。
CODE-02 · 容器和内容都能缩小
.project-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
gap: var(--ui-space-4);
}
.project-grid > * { min-inline-size: 0; }
.project-name { overflow-wrap: anywhere; }
.action-row { display: flex; flex-wrap: wrap; gap: var(--ui-space-3); }
不能用全局 overflow-x: hidden 掩盖内容溢出。应找到真正撑宽的元素,处理网格、不可换行文字或局部滚动区域。
CODE-03 · 表单错误与普通反馈分开
<label for="project-name">{{ copy.label }}</label>
<input id="project-name" v-model="name" name="name"
:aria-invalid="fieldError ? 'true' : undefined"
aria-describedby="name-help name-error">
<p id="name-help">{{ copy.help(rules.maxGraphemes) }}</p>
<p id="name-error">{{ fieldError }}</p>
<p id="save-status" role="status" aria-live="polite">{{ formMessage }}</p>
上面是 Vue 模板结构,字段策略、文案和保存状态由领域控制器提供;本仓库实例为 components/uiux/useProjectNameDraft.mjs。上限在领域规则中维护一次,提示与校验共同引用。按 grapheme 计数时不能直接复用原生 maxlength,具体见文案容量。
字段无效时填充对应错误,设置 aria-invalid="true" 并定位首个错误;修正后移除。保存进度与完成结果更新到 status 区域。服务器失败保留输入,不把网络错误误标成字段错误。
不要在用户仍然用输入法组词时自动提交。异步搜索可以在 compositionend 之后触发,示例:
function onSearchInput(event) {
if (!event.isComposing) scheduleSearch(event.target.value);
}
// compositionend 时再调用 scheduleSearch;函数由项目实现防抖。
CODE-04 · 只应用当前请求的结果
以下是 Vue 3 的只读搜索示例。它处理取消和响应乱序,不处理缓存、认证恢复和生产监控。
import { onUnmounted, ref } from 'vue';
const rows = ref([]), pending = ref(false), error = ref('');
let sequence = 0, controller;
async function search(query) {
const current = ++sequence;
controller?.abort();
controller = new AbortController();
pending.value = true;
error.value = '';
try {
const response = await fetch(`/api/projects?q=${encodeURIComponent(query)}`, {
signal: controller.signal,
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const result = await response.json(); // 真实项目还应校验响应结构。
if (current === sequence) rows.value = result.items;
} catch (cause) {
if (current === sequence && cause.name !== 'AbortError') {
error.value = '未能更新项目,请重试'; // 已有 rows 不清空。
}
} finally {
if (current === sequence) pending.value = false;
}
}
onUnmounted(() => { sequence++; controller?.abort(); });
写操作取消后服务端可能已执行;需要查询与幂等策略,不能把上面的读请求模式原样用于付款。见状态与恢复中的 ST-04。
CODE-05 · 用原生 dialog 提供模态基础
<button id="open-confirm" type="button">查看操作说明</button>
<dialog id="confirm-dialog" aria-labelledby="confirm-title">
<h2 id="confirm-title">确认操作范围</h2>
<p>这里显示具体对象与影响。</p>
<form method="dialog">
<button autofocus value="cancel">返回</button>
</form>
</dialog>
<script>
const trigger = document.querySelector('#open-confirm');
const dialog = document.querySelector('#confirm-dialog');
trigger.addEventListener('click', () => dialog.showModal());
dialog.addEventListener('close', () => trigger.focus());
</script>
示例只展示打开、关闭和焦点恢复,没有执行业务操作。在组件中应使用组件引用,并处理触发元素被移除等情况。showModal() 和 show() 的行为不同,参见 MDN dialog。
CODE-06 · 图片预留尺寸,失败不挡任务
<img src="/ui-ux/empty-projects.svg" width="480" height="240"
alt="" loading="lazy" decoding="async">
<h2>开始你的第一个项目</h2>
<p>把想法和进展放在一起。</p>
这里图片是装饰,标题独立表达含义。首屏关键图片是否延迟加载需要另行判断。业务图片缺失时应有对应的回退策略,不能所有失败都换成同一张无关占位图。
复制之后的检查
检查变量命名是否映射到项目 token、静态路径是否有效、事件是否清理、状态是否可复现、键盘能否完成操作。示例只保证它所解释的范围,不替代项目的错误处理与测试。