AppML 案例 Products

产品案例:分类、单价、库存与状态显示。

Intermediate
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.

三个案例的对比

EmployeesCustomersProducts
Hard part日期校验与搜索数值与状态
Default view表格表格 + 搜索自定义表格
Custom JS工龄计算过滤与格式化状态标签与汇总

Friends & Partners