AppML 案例 Customers
客户管理案例:必填校验、数字范围、搜索过滤。
Key Points客户案例的重点是校验与搜索:必填、额度范围、按城市过滤。
数据
JSON
[
{ "id": 1, "company": "北京云图科技", "contact": "张伟", "city": "北京", "credit": 50000 },
{ "id": 2, "company": "上海海联贸易", "contact": "李娜", "city": "上海", "credit": 80000 },
{ "id": 3, "company": "广州南方物流", "contact": "王强", "city": "广州", "credit": 35000 },
{ "id": 4, "company": "北京中科数据", "contact": "赵敏", "city": "北京", "credit": 120000 }
]模型
XML
<appml>
<datasource>
<query>data/customers.json</query>
</datasource>
<html>
<title>客户管理</title>
</html>
<items>
<item name="id" label="编号" type="number" primarykey="true" readonly="true"/>
<item name="company" label="公司" required="true" maxlength="60"/>
<item name="contact" label="联系人" required="true" maxlength="20"/>
<item name="city" label="城市" default="北京"/>
<item name="credit" label="额度" type="number" min="0" max="1000000" step="1000" default="0"/>
</items>
</appml>搜索
HTML
<appml-app src="models/customers.xml" id="cust">
<appml-view view="search"></appml-view>
<appml-view view="list"></appml-view>
<appml-view view="form"></appml-view>
</appml-app>
<div style="margin-top:1rem">
<button onclick="showCity('北京')">只看北京</button>
<button onclick="showCity('')">显示全部</button>
</div>
<script>
function showCity(city) {
const app = document.getElementById('cust').appml;
const rows = city ? app.data.filter(r => r.city === city) : app.data;
renderTable(rows);
}
function renderTable(rows) {
const tbody = document.querySelector('table tbody');
if (!tbody) return;
tbody.innerHTML = rows.map(r => `
<tr>
<td>${r.id}</td>
<td>${escapeHtml(r.company)}</td>
<td>${escapeHtml(r.contact)}</td>
<td>${escapeHtml(r.city)}</td>
<td>${r.credit.toLocaleString()}</td>
</tr>`).join('');
}
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, c =>
({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
}
</script>Output
编号 公司 联系人 城市 额度
1 北京云图科技 张伟 北京 50,000
4 北京中科数据 赵敏 北京 120,000Filtering app.data and redrawing gives the most control. Redrawing drops event bindings the framework attached, so row buttons stop working — rebind or use event delegation.
额度格式化
JavaScript
function money(n) {
return '¥' + Number(n).toLocaleString('zh-CN', { minimumFractionDigits: 2 });
}
money(50000); // ¥50,000.00toLocaleString handles grouping better than manual string work, and minimumFractionDigits: 2 keeps 50000 as 50,000.00 rather than 50,000.
必填与范围校验
| Rule | Where | Effect |
|---|---|---|
| 公司必填 | required="true" | 空值时表单不提交 |
| 额度 0–100 万 | min="0" max="1000000" | 超出范围标红 |
| 额度步进 1000 | step="1000" | 上下箭头按千递增 |
| 公司名长度 | maxlength="60" | 输入框限长 |
Frontend validation stops typos, not attackers:
min, max and required can all be removed in devtools. Enforce NOT NULL and CHECK in the database and validate again in the API.搜索的两种做法
JavaScript
// 做法一:前端过滤(数据量小,几百条以内)
const hits = app.data.filter(r =>
r.company.includes(kw) || r.contact.includes(kw));
// 做法二:后端查询(数据量大)
fetch(`/api/customers?kw=${encodeURIComponent(kw)}`)
.then(r => r.json())
.then(rows => renderTable(rows));Frontend filtering ships every row to the browser: fine for thousands, sluggish for tens of thousands. Beyond that, query the backend and keep only the current page in app.data.
数据
JSON
[
{ "id": 1, "company": "北京云图科技", "contact": "张伟", "city": "北京", "credit": 50000 },
{ "id": 2, "company": "上海海联贸易", "contact": "李娜", "city": "上海", "credit": 80000 },
{ "id": 3, "company": "广州南方物流", "contact": "王强", "city": "广州", "credit": 35000 },
{ "id": 4, "company": "北京中科数据", "contact": "赵敏", "city": "北京", "credit": 120000 }
]模型
XML
<appml>
<datasource>
<query>data/customers.json</query>
</datasource>
<html>
<title>客户管理</title>
</html>
<items>
<item name="id" label="编号" type="number" primarykey="true" readonly="true"/>
<item name="company" label="公司" required="true" maxlength="60"/>
<item name="contact" label="联系人" required="true" maxlength="20"/>
<item name="city" label="城市" default="北京"/>
<item name="credit" label="额度" type="number" min="0" max="1000000" step="1000" default="0"/>
</items>
</appml>搜索
HTML
<appml-app src="models/customers.xml" id="cust">
<appml-view view="search"></appml-view>
<appml-view view="list"></appml-view>
<appml-view view="form"></appml-view>
</appml-app>
<div style="margin-top:1rem">
<button onclick="showCity('北京')">只看北京</button>
<button onclick="showCity('')">显示全部</button>
</div>
<script>
function showCity(city) {
const app = document.getElementById('cust').appml;
const rows = city ? app.data.filter(r => r.city === city) : app.data;
renderTable(rows);
}
function renderTable(rows) {
const tbody = document.querySelector('table tbody');
if (!tbody) return;
tbody.innerHTML = rows.map(r => `
<tr>
<td>${r.id}</td>
<td>${escapeHtml(r.company)}</td>
<td>${escapeHtml(r.contact)}</td>
<td>${escapeHtml(r.city)}</td>
<td>${r.credit.toLocaleString()}</td>
</tr>`).join('');
}
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, c =>
({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
}
</script>Output
编号 公司 联系人 城市 额度
1 北京云图科技 张伟 北京 50,000
4 北京中科数据 赵敏 北京 120,000用 app.data.filter 拿到子集后自己重绘表格,是最灵活的做法。注意重绘会丢掉框架绑定的事件,之后点行内按钮可能失效——需要重新绑定或改用事件委托。
额度格式化
JavaScript
function money(n) {
return '¥' + Number(n).toLocaleString('zh-CN', { minimumFractionDigits: 2 });
}
money(50000); // ¥50,000.00toLocaleString 按区域格式化,比自己拼千分位可靠。minimumFractionDigits: 2 保证 50000 显示成 50,000.00 而不是 50,000。
必填与范围校验
| 规则 | 写在哪 | 效果 |
|---|---|---|
| 公司必填 | required="true" | 空值时表单不提交 |
| 额度 0–100 万 | min="0" max="1000000" | 超出范围标红 |
| 额度步进 1000 | step="1000" | 上下箭头按千递增 |
| 公司名长度 | maxlength="60" | 输入框限长 |
前端校验只挡误操作,挡不住恶意提交。
min / max / required 都能在开发者工具里删掉。数据库层面必须有 NOT NULL 与 CHECK 约束,接口层必须再校验一遍。搜索的两种做法
JavaScript
// 做法一:前端过滤(数据量小,几百条以内)
const hits = app.data.filter(r =>
r.company.includes(kw) || r.contact.includes(kw));
// 做法二:后端查询(数据量大)
fetch(`/api/customers?kw=${encodeURIComponent(kw)}`)
.then(r => r.json())
.then(rows => renderTable(rows));前端过滤把全部数据先下到浏览器,几千条还行,几万条就卡。数据量大时必须后端查询,app.data 只保留当前页。