AppML 案例 Products
产品案例:分类、单价、库存与状态显示。
Key Points产品案例的重点是数值字段与状态判断:单价精度、库存阈值、缺货标红。
数据
JSON
[
{ "id": 1, "name": "机械键盘 K8", "category": "外设", "price": 399.00, "stock": 42 },
{ "id": 2, "name": "27 寸显示器", "category": "显示", "price": 1899.00, "stock": 0 },
{ "id": 3, "name": "无线鼠标 M3", "category": "外设", "price": 129.00, "stock": 8 },
{ "id": 4, "name": "USB-C 扩展坞", "category": "配件", "price": 259.00, "stock": 3 }
]模型
XML
<appml>
<datasource>
<query>data/products.json</query>
</datasource>
<items>
<item name="id" label="编号" type="number" primarykey="true" readonly="true"/>
<item name="name" label="名称" required="true" maxlength="80"/>
<item name="category" label="分类" required="true"/>
<item name="price" label="单价" type="number" min="0" step="0.01" required="true"/>
<item name="stock" label="库存" type="number" min="0" default="0"/>
</items>
</appml>Price uses step="0.01", stock the default step of 1, and min="0" blocks negatives — meaningless for stock but easy to type by accident.
库存状态
JavaScript
function stockTag(stock) {
if (stock === 0) return '<span class="tag out">缺货</span>';
if (stock < 10) return '<span class="tag low">库存紧张</span>';
return '<span class="tag ok">有货</span>';
}CSS
.tag { display: inline-block; padding: .1rem .5rem; border-radius: .25rem; font-size: .8rem; }
.tag.out { background: #fee2e2; color: #b91c1c; }
.tag.low { background: #fef3c7; color: #b45309; }
.tag.ok { background: #d1fae5; color: #047857; }Output
机械键盘 K8 外设 ¥399.00 42 有货
27 寸显示器 显示 ¥1,899.00 0 缺货
无线鼠标 M3 外设 ¥129.00 8 库存紧张
USB-C 扩展坞 配件 ¥259.00 3 库存紧张自己渲染表格
JavaScript
function productsController(appml) {
appml.onload = function () {
const tbody = document.querySelector('#products tbody');
tbody.innerHTML = appml.data.map((p) => `
<tr>
<td>${p.id}</td>
<td>${escapeHtml(p.name)}</td>
<td>${escapeHtml(p.category)}</td>
<td>¥${Number(p.price).toLocaleString('zh-CN',
{ minimumFractionDigits: 2 })}</td>
<td>${p.stock}</td>
<td>${stockTag(p.stock)}</td>
</tr>`).join('');
};
}Rendering yourself gives full control: formatted money, status tags, row highlighting. The cost is escaping every value — not one can be missed.
分类汇总
JavaScript
function summarize(data) {
return data.reduce((acc, p) => {
acc[p.category] = acc[p.category] || { count: 0, value: 0 };
acc[p.category].count += 1;
acc[p.category].value += p.price * p.stock;
return acc;
}, {});
}
// { 外设: { count: 2, value: 17844 }, 显示: { count: 1, value: 0 }, 配件: { count: 1, value: 777 } }reduce summarises in one expression, clearer than a loop with temporaries. value here is unit price × stock (inventory value), not revenue — two easily confused numbers.
完整页面
HTML
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>产品管理</title>
<script src="js/appml.js"></script>
<style>
body { font-family: system-ui, sans-serif; margin: 2rem; color: #1f2937; }
table { border-collapse: collapse; min-width: 700px; }
th, td { border: 1px solid #d1d5db; padding: .45rem .8rem; }
th { background: #f3f4f6; text-align: left; }
td:nth-child(4), td:nth-child(5) { text-align: right; }
</style>
</head>
<body>
<h1>产品管理</h1>
<table id="products">
<thead>
<tr><th>编号</th><th>名称</th><th>分类</th><th>单价</th><th>库存</th><th>状态</th></tr>
</thead>
<tbody></tbody>
</table>
<appml-app src="models/products.xml" controller="productsController" hidden></appml-app>
</body>
</html>hidden makes AppML load data without rendering, letting the controller draw the table — a trick to use AppML purely as a data layer, at the cost of depending on its data shape and callback timing.三个案例的对比
| Employees | Customers | Products | |
|---|---|---|---|
| Hard part | 日期 | 校验与搜索 | 数值与状态 |
| Default view | 表格 | 表格 + 搜索 | 自定义表格 |
| Custom JS | 工龄计算 | 过滤与格式化 | 状态标签与汇总 |
数据
JSON
[
{ "id": 1, "name": "机械键盘 K8", "category": "外设", "price": 399.00, "stock": 42 },
{ "id": 2, "name": "27 寸显示器", "category": "显示", "price": 1899.00, "stock": 0 },
{ "id": 3, "name": "无线鼠标 M3", "category": "外设", "price": 129.00, "stock": 8 },
{ "id": 4, "name": "USB-C 扩展坞", "category": "配件", "price": 259.00, "stock": 3 }
]模型
XML
<appml>
<datasource>
<query>data/products.json</query>
</datasource>
<items>
<item name="id" label="编号" type="number" primarykey="true" readonly="true"/>
<item name="name" label="名称" required="true" maxlength="80"/>
<item name="category" label="分类" required="true"/>
<item name="price" label="单价" type="number" min="0" step="0.01" required="true"/>
<item name="stock" label="库存" type="number" min="0" default="0"/>
</items>
</appml>单价用 step="0.01",库存用默认步进 1。min="0" 挡住负数——库存为负在业务上没有意义,但用户手输时很容易多打一个减号。
库存状态
JavaScript
function stockTag(stock) {
if (stock === 0) return '<span class="tag out">缺货</span>';
if (stock < 10) return '<span class="tag low">库存紧张</span>';
return '<span class="tag ok">有货</span>';
}CSS
.tag { display: inline-block; padding: .1rem .5rem; border-radius: .25rem; font-size: .8rem; }
.tag.out { background: #fee2e2; color: #b91c1c; }
.tag.low { background: #fef3c7; color: #b45309; }
.tag.ok { background: #d1fae5; color: #047857; }Output
机械键盘 K8 外设 ¥399.00 42 有货
27 寸显示器 显示 ¥1,899.00 0 缺货
无线鼠标 M3 外设 ¥129.00 8 库存紧张
USB-C 扩展坞 配件 ¥259.00 3 库存紧张自己渲染表格
JavaScript
function productsController(appml) {
appml.onload = function () {
const tbody = document.querySelector('#products tbody');
tbody.innerHTML = appml.data.map((p) => `
<tr>
<td>${p.id}</td>
<td>${escapeHtml(p.name)}</td>
<td>${escapeHtml(p.category)}</td>
<td>¥${Number(p.price).toLocaleString('zh-CN',
{ minimumFractionDigits: 2 })}</td>
<td>${p.stock}</td>
<td>${stockTag(p.stock)}</td>
</tr>`).join('');
};
}自己渲染能完全控制输出:金额格式化、状态标签、缺货行加背景色都能做。代价是要自己处理转义——escapeHtml 一个都不能漏。
分类汇总
JavaScript
function summarize(data) {
return data.reduce((acc, p) => {
acc[p.category] = acc[p.category] || { count: 0, value: 0 };
acc[p.category].count += 1;
acc[p.category].value += p.price * p.stock;
return acc;
}, {});
}
// { 外设: { count: 2, value: 17844 }, 显示: { count: 1, value: 0 }, 配件: { count: 1, value: 777 } }reduce 一行做汇总,比 for 循环加临时变量清楚。value 用「单价 × 库存」算的是库存金额,不是销售额——这两个概念经常被混淆。
完整页面
HTML
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>产品管理</title>
<script src="js/appml.js"></script>
<style>
body { font-family: system-ui, sans-serif; margin: 2rem; color: #1f2937; }
table { border-collapse: collapse; min-width: 700px; }
th, td { border: 1px solid #d1d5db; padding: .45rem .8rem; }
th { background: #f3f4f6; text-align: left; }
td:nth-child(4), td:nth-child(5) { text-align: right; }
</style>
</head>
<body>
<h1>产品管理</h1>
<table id="products">
<thead>
<tr><th>编号</th><th>名称</th><th>分类</th><th>单价</th><th>库存</th><th>状态</th></tr>
</thead>
<tbody></tbody>
</table>
<appml-app src="models/products.xml" controller="productsController" hidden></appml-app>
</body>
</html>hidden 让 AppML 只做数据加载、不渲染界面,再由控制器自己画表格。这是「用 AppML 当数据层」的取巧写法,代价是依赖它的 data 结构与回调时序。三个案例的对比
| 项目 | Employees | Customers | Products |
|---|---|---|---|
| 难点 | 日期 | 校验与搜索 | 数值与状态 |
| 默认视图 | 表格 | 表格 + 搜索 | 自定义表格 |
| 自定义 JS | 工龄计算 | 过滤与格式化 | 状态标签与汇总 |