Áp dụng cho api_version: "2026-01".
Extension Worker UI (block, action modal/fullscreen, trang render: "worker") chỉ dựng giao diện bằng các element pos-* dưới đây. POS vẽ mỗi element bằng component thật của POS, nên giao diện luôn đồng bộ với POS.
Trang iframe không dùng danh sách này; dùng HTML/React tự do, có thể kèm @pancake-pos/ui để giao diện giống POS.
Quy tắc chung
- Chỉ dùng element trong danh sách. Thẻ HTML thường (
div,span,img,table,script…) và element lạ không được vẽ; extension báo lỗi trong nhật ký của app. - Text chỉ hiển thị khi nằm trong
pos-text,pos-heading,pos-badge,pos-link,pos-button,pos-banner. - Không có
style,className, HTML thô. Màu sắc, khoảng cách, font theo POS. - URL (
href,src) chỉ chấp nhậnhttps://. - Sự kiện truyền giá trị qua
event.detail. Trong JSX dùng thuộc tínhonClick,onChange… như component thông thường. Với JS thuần, tên sự kiện là phần sauonviết thường:onPageChange→addEventListener('pagechange', …). - Giới hạn 2.000 element mỗi extension;
pos-tabletối đa 500 dòng mỗi lần render (dùng phân trang). - Thuộc tính dạng mảng/đối tượng (
options,columns,rows,pagination) truyền trực tiếp bằng JSX, không cần JSON.stringify.
<pos-stack gap="small">
<pos-text-field label="Mã phiếu" value={code} onChange={(e) => setCode(e.detail)} />
<pos-button variant="primary" loading={saving} onClick={save}>Lưu</pos-button>
</pos-stack>Không dùng framework thì dựng bằng DOM API của runtime (xem Môi trường worker):
let code = '';
const field = document.createElement('pos-text-field');
field.label = 'Mã phiếu';
field.addEventListener('change', (e) => { code = e.detail; });
const button = document.createElement('pos-button');
button.variant = 'primary';
button.textContent = 'Lưu';
button.addEventListener('click', () => save(code));
const stack = document.createElement('pos-stack');
stack.gap = 'small';
stack.append(field, button);
document.body.append(stack);Màn hình chuẩn
POS đặt element theo layout chung của các trang POS; app chỉ chọn đúng element và thứ tự, không chỉnh màu hay khoảng cách.
- Trang
app.page:pos-pagelà khung trang của POS.headingvàdescriptionnằm ở dòng tiêu đề trên cùng, slotactionsở bên phải cạnh nút chính (pos.page.setPrimaryAction); nội dung xếp dọc trên nền xám. - Khối:
pos-sectionđặt thẳng trongpos-pagelà thẻ trắng;pos-sectionlồng trongpos-sectionlà nhóm phẳng; trong block và modal,pos-sectionlà thẻ trắng viền mảnh.pos-table,pos-chartđặt thẳng trongpos-pagetự nằm trong thẻ trắng. - Số liệu:
pos-stattrongpos-grid3–5 cột; không ghéppos-sectionvớipos-headingđể làm ô số liệu. - Lọc: gom ô lọc vào một
pos-filter-barở đầu trang hoặc đầu khối; khoảng ngày dùngpos-date-range. Lọc theo trạng thái dùngpos-tabscóbadge: chip có số đếm như thanh trạng thái của các trang danh sách POS. - Nút: nút công cụ (tải lại, lọc, cài đặt) là nút chỉ có icon kèm
tooltip; nhiều thao tác phụ thì gom vàopos-menu-button. Trạng thái trong bảng dùng cộtformat: 'badge'; chi tiết một dòng mở trongpos-drawerhoặcpos-modalvớipos-description-list,pos-timeline. - Không lặp tiêu đề trang trong nội dung, không dùng emoji hay ký tự trang trí trong nhãn (dùng
tonecủapos-stat,pos-badge), không lồngpos-sectionquá hai cấp, không bọc từng element trong mộtpos-sectionriêng. - Bỏ khỏi biểu đồ và bảng các dòng toàn số 0 hoặc không có dữ liệu; hiện tên thay vì mã (tra tên qua Open API).
Trang báo cáo
<pos-page description="Đối chiếu thời gian giao thực tế với SLA cam kết của ĐVVC" width="full">
<pos-button slot="actions" icon="download" onClick={exportCsv}>Xuất</pos-button>
<pos-filter-bar>
<pos-date-range label="Ngày tạo đơn" value={range} onChange={(e) => e.detail && setRange(e.detail)} />
<pos-button icon="refresh" tooltip="Tải lại" onClick={load} />
</pos-filter-bar>
<pos-grid columns={4}>
<pos-stat label="Tỷ lệ đúng hạn" value={58.3} format="percent" help="12 đơn đã giao" />
<pos-stat label="Đúng hạn" value={7} tone="success" selected={state === 'on_time'} onClick={() => setState('on_time')} />
<pos-stat label="Giao trễ" value={5} tone="critical" selected={state === 'late'} onClick={() => setState('late')} />
<pos-stat label="Phí ship" value={2450000} format="money" />
</pos-grid>
<pos-section heading="Theo ĐVVC">
<pos-chart type="bar" xKey="carrier" data={byCarrier} series={[{ key: 'rate', label: 'Tỷ lệ đúng hạn (%)' }]} onSelect={(e) => setCarrier(e.detail.x)} />
</pos-section>
</pos-page>Trang danh sách
<pos-page width="full">
<pos-button slot="actions" icon="upload" onClick={importFile}>Nhập</pos-button>
<pos-menu-button slot="actions" items={[{ id: 'csv', label: 'Xuất CSV' }]} onSelect={exportAs} />
<pos-section>
<pos-filter-bar>
<pos-text-field label="Tìm mã đơn" value={search} onChange={(e) => setSearch(e.detail)} />
<pos-select label="Kho" options={warehouses} value={warehouse} onChange={(e) => setWarehouse(e.detail)} />
</pos-filter-bar>
<pos-tabs tabs={[{ id: 'all', label: 'Tất cả', badge: 169 }, { id: 'late', label: 'Giao trễ', badge: 12 }]} selected={status} onChange={(e) => setStatus(e.detail)}>
<pos-table
columns={[
{ key: 'code', title: 'Mã đơn', format: 'link' },
{ key: 'customer', title: 'Khách hàng', subtitle: 'phone' },
{ key: 'state', title: 'SLA', format: 'badge', tones: { 'Trễ': 'critical', 'Đúng hạn': 'success' } },
{ key: 'fee', title: 'Phí ship', format: 'money', align: 'right', sortable: true },
]}
rows={rows}
sort={sort}
pagination={{ page, pageSize: 30, total }}
onSortChange={(e) => setSort(e.detail)}
onPageChange={(e) => setPage(e.detail.page)}
onCellClick={(e) => setDetail(e.detail.row)}
/>
</pos-tabs>
</pos-section>
</pos-page><pos-drawer open={!!detail} heading={detail?.code} onClose={() => setDetail(null)}>
<pos-description-list items={[{ label: 'ĐVVC', value: detail?.carrier }, { label: 'Phí ship', value: detail?.fee, format: 'money' }]} />
<pos-timeline items={detail?.events ?? []} />
</pos-drawer>Nút chính của trang (ví dụ "Đồng bộ") đặt bằng pos.page.setPrimaryAction, luôn nằm cuối dòng tiêu đề. Xuất file (CSV, Excel) bằng pos.download.
Bố cục
pos-stack
Xếp các phần tử con theo hàng dọc hoặc ngang.
| Thuộc tính | Kiểu | Mặc định | Mô tả |
|---|---|---|---|
direction |
'vertical' | 'horizontal' |
'vertical' |
Hướng xếp |
gap |
'none' | 'small' | 'medium' | 'large' |
'medium' |
Khoảng cách giữa phần tử |
align |
'start' | 'center' | 'end' | 'stretch' |
'stretch' |
Căn theo trục phụ |
justify |
'start' | 'center' | 'end' | 'space-between' |
'start' |
Căn theo trục chính |
wrap |
boolean |
false |
Cho xuống dòng (khi horizontal) |
pos-grid
Lưới cột đều nhau; ô hẹp hơn 180 px thì tự xuống hàng (màn hình hẹp còn một cột).
| Thuộc tính | Kiểu | Mặc định | Mô tả |
|---|---|---|---|
columns |
1–6 |
2 |
Số cột |
gap |
'none' | 'small' | 'medium' | 'large' |
'medium' |
Khoảng cách |
pos-section
Khối có tiêu đề. Đặt thẳng trong pos-page là thẻ trắng; lồng trong pos-section khác là nhóm phẳng; trong block và modal là thẻ trắng viền mảnh (xem Màn hình chuẩn).
| Thuộc tính | Kiểu | Mô tả |
|---|---|---|
heading |
string |
Tiêu đề |
description |
string |
Mô tả ngắn dưới tiêu đề |
Slot actions: pos-button, pos-link, pos-menu-button hiển thị ở góc phải tiêu đề.
<pos-section heading="Công nợ">
<pos-button slot="actions" variant="tertiary" onClick={reload}>Tải lại</pos-button>
<pos-text>...</pos-text>
</pos-section>pos-divider
Đường kẻ ngang. Không có thuộc tính.
Hiển thị
pos-text
| Thuộc tính | Kiểu | Mặc định |
|---|---|---|
tone |
'default' | 'subdued' | 'success' | 'warning' | 'critical' |
'default' |
size |
'small' | 'medium' | 'large' |
'medium' |
weight |
'regular' | 'semibold' | 'bold' |
'regular' |
pos-heading
| Thuộc tính | Kiểu | Mặc định |
|---|---|---|
level |
1–4 |
3 |
Cỡ chữ: 1 20 px (bằng tiêu đề trang), 2 18 px, 3 16 px (bằng tiêu đề khối), 4 14 px.
pos-badge
Nhãn trạng thái ngắn.
| Thuộc tính | Kiểu | Mặc định |
|---|---|---|
tone |
'default' | 'info' | 'success' | 'warning' | 'critical' |
'default' |
pos-money
Hiển thị số tiền theo định dạng và đơn vị tiền tệ của shop.
| Thuộc tính | Kiểu | Mô tả |
|---|---|---|
value |
number |
Số tiền |
currency |
string |
Mã tiền tệ ISO 4217; mặc định là tiền tệ của shop |
tone |
như pos-text |
Màu chữ |
pos-link
| Thuộc tính | Kiểu | Mô tả |
|---|---|---|
href |
string |
URL https://, luôn mở tab mới |
Sự kiện onClick: dùng khi link thực hiện thao tác trong app thay vì mở URL (không đặt href).
pos-image
| Thuộc tính | Kiểu | Mặc định | Mô tả |
|---|---|---|---|
src |
string |
URL https:// thuộc domain trong network.allowed_origins, hoặc public/<tên file> trỏ tới file trong thư mục public/ của dự án |
|
alt |
string |
Văn bản thay thế | |
width, height |
number |
Kích thước (px) | |
fit |
'cover' | 'contain' |
'cover' |
Cách co ảnh |
POS tải ảnh qua máy chủ trung gian của sandbox: chỉ nhận định dạng ảnh, tối đa 5 MB. Ảnh từ domain chưa khai không hiển thị (chế độ phát triển có cảnh báo).
pos-description-list
Cặp nhãn – giá trị như khối thông tin đơn của POS (nhãn xám bên trái, giá trị bên phải); dùng trong modal, panel, khối chi tiết.
| Thuộc tính | Kiểu | Mặc định | Mô tả |
|---|---|---|---|
items |
Item[] |
[] |
Tối đa 50 dòng |
columns |
1–4 |
1 |
Số cột; ô hẹp hơn 220 px thì xuống hàng |
type Item = {
label: string;
value: string | number | null; // null hoặc rỗng hiện "—"
format?: 'text' | 'number' | 'money' | 'percent' | 'date' | 'datetime' | 'badge';
tone?: 'default' | 'info' | 'success' | 'warning' | 'critical'; // màu khi format là badge
};<pos-description-list columns={2} items={[
{ label: 'ĐVVC', value: 'GHN' },
{ label: 'Phí ship', value: 25000, format: 'money' },
{ label: 'SLA', value: 'Trễ 2 ngày', format: 'badge', tone: 'critical' },
]} />pos-timeline
Dòng thời gian: hành trình đơn, lịch sử thay đổi.
| Thuộc tính | Kiểu | Mô tả |
|---|---|---|
items |
{ title: string; time?: string; description?: string; tone?: Tone }[] |
Tối đa 100 mốc; time dạng ISO 8601 được định dạng theo POS, chữ khác giữ nguyên; tone là màu chấm |
Nhập liệu
Thuộc tính chung của mọi ô nhập:
| Thuộc tính | Kiểu | Mô tả |
|---|---|---|
label |
string |
Nhãn |
value |
tuỳ component | Giá trị hiện tại (component có kiểm soát) |
disabled |
boolean |
Khoá ô |
required |
boolean |
Hiện dấu bắt buộc (không tự kiểm tra) |
error |
string |
Thông báo lỗi dưới ô |
help |
string |
Gợi ý dưới ô |
Sự kiện chung: onChange (event.detail là giá trị mới), onBlur.
POS hiện ngay chữ người dùng gõ rồi mới gửi onChange cho app, nên ô không giật dù app đang bận. Nếu app đặt value khác giá trị người dùng vừa nhập (ví dụ chuẩn hoá chữ hoa hoặc xóa ô), POS hiển thị giá trị của app. Nên đặt value ngay trong onChange; đổi value sau một lệnh await có thể ghi đè chữ người dùng đang gõ.
pos-text-field
| Thuộc tính | Kiểu | Mô tả |
|---|---|---|
value |
string |
|
placeholder |
string |
|
multiline |
boolean | number |
Ô nhiều dòng; số là số dòng hiển thị |
maxLength |
number |
Giới hạn ký tự |
pos-number-field
| Thuộc tính | Kiểu | Mô tả |
|---|---|---|
value |
number | null |
|
min, max, step |
number |
|
suffix |
string |
Đơn vị hiển thị sau số, ví dụ "%" |
pos-select
| Thuộc tính | Kiểu | Mô tả |
|---|---|---|
options |
{ label: string; value: string; disabled?: boolean }[] |
Danh sách lựa chọn |
value |
string | string[] |
Mảng khi multiple |
multiple |
boolean |
Chọn nhiều |
placeholder |
string |
|
searchable |
boolean |
Cho gõ tìm trong danh sách |
pos-checkbox
| Thuộc tính | Kiểu | Mô tả |
|---|---|---|
checked |
boolean |
|
label |
string |
onChange: event.detail là boolean.
pos-date-picker
| Thuộc tính | Kiểu | Mô tả |
|---|---|---|
value |
string | null |
Ngày dạng YYYY-MM-DD |
min, max |
string |
Giới hạn ngày chọn, dạng YYYY-MM-DD |
pos-date-range
Khoảng ngày như bộ chọn thời gian của trang Thống kê, có sẵn lựa chọn nhanh: Hôm nay, Hôm qua, 7 ngày qua, 30 ngày qua, Đầu tuần đến nay, Đầu tháng đến nay, Tháng trước.
| Thuộc tính | Kiểu | Mặc định | Mô tả |
|---|---|---|---|
value |
{ from: string; to: string } | null |
Ngày dạng YYYY-MM-DD |
|
min, max |
string |
Giới hạn ngày chọn được | |
presets |
boolean |
true |
Hiện lựa chọn nhanh |
Cùng các thuộc tính chung của ô nhập (label, disabled, required, error, help). Sự kiện onChange: event.detail là { from, to } hoặc null khi xoá.
<pos-date-range label="Ngày tạo đơn" value={range} onChange={(e) => e.detail && setRange(e.detail)} />Phản hồi
pos-banner
Thông báo nổi bật trong extension.
| Thuộc tính | Kiểu | Mặc định | Mô tả |
|---|---|---|---|
tone |
'info' | 'success' | 'warning' | 'critical' |
'info' |
|
heading |
string |
Tiêu đề | |
dismissible |
boolean |
false |
Hiện nút đóng |
Sự kiện onDismiss.
pos-spinner
| Thuộc tính | Kiểu | Mặc định |
|---|---|---|
size |
'small' | 'medium' | 'large' |
'medium' |
pos-progress
Thanh tiến độ cho tỷ lệ (đúng hạn, hoàn thành chỉ tiêu).
| Thuộc tính | Kiểu | Mặc định | Mô tả |
|---|---|---|---|
value |
number |
0 |
Phần trăm 0–100 |
label |
string |
Nhãn bên trái, phần trăm bên phải | |
tone |
'default' | 'info' | 'success' | 'warning' | 'critical' |
'default' |
Màu thanh |
showValue |
boolean |
true |
Hiện số phần trăm |
pos-empty
Trạng thái trống như các trang POS: hình minh hoạ nhỏ, tiêu đề, mô tả và nút hành động.
| Thuộc tính | Kiểu | Mô tả |
|---|---|---|
heading |
string |
Tiêu đề |
description |
string |
Mô tả |
Slot actions cho pos-button, pos-link.
<pos-empty heading="Chưa có đơn trễ" description="Đổi khoảng ngày để xem thêm">
<pos-button slot="actions" onClick={reset}>Đặt lại bộ lọc</pos-button>
</pos-empty>Dữ liệu và thao tác
pos-button
| Thuộc tính | Kiểu | Mặc định | Mô tả |
|---|---|---|---|
variant |
'primary' | 'secondary' | 'tertiary' | 'danger' |
'secondary' |
Kiểu nút |
size |
'small' | 'medium' |
'medium' |
|
loading |
boolean |
false |
Hiện trạng thái đang xử lý, chặn bấm |
disabled |
boolean |
false |
|
icon |
xem dưới | Icon trước chữ; nút không có chữ là nút chỉ có icon | |
tooltip |
string |
Chú thích khi rê chuột; bắt buộc với nút chỉ có icon (cũng là tên cho trình đọc màn hình) |
Sự kiện onClick.
Icon (bộ icon của POS): plus, edit, delete, download, upload, refresh, filter, search, settings, print, copy, link, external, view, send, check, close, more, calendar, info, warning.
<pos-button icon="download" onClick={exportCsv}>Xuất</pos-button>
<pos-button icon="refresh" tooltip="Tải lại" onClick={reload} />pos-menu-button
Nút mở danh sách thao tác. Không có label thì là nút "⋯".
| Thuộc tính | Kiểu | Mặc định | Mô tả |
|---|---|---|---|
items |
{ id: string; label: string; danger?: boolean; disabled?: boolean }[] |
[] |
Tối đa 30 mục |
label |
string |
Chữ trên nút | |
icon |
như pos-button |
more khi không có label |
|
variant |
'primary' | 'secondary' | 'tertiary' |
'secondary' |
|
size |
'small' | 'medium' |
'medium' |
|
disabled |
boolean |
false |
|
tooltip |
string |
Chú thích khi rê chuột |
Sự kiện onSelect: event.detail là id của mục được bấm.
<pos-menu-button slot="actions" items={[{ id: 'csv', label: 'Xuất CSV' }, { id: 'delete', label: 'Xoá', danger: true }]} onSelect={(e) => run(e.detail)} />pos-table
Bảng dữ liệu như bảng danh sách của POS. Ô chỉ chứa giá trị (chuỗi, số); cách hiển thị (định dạng, badge, link, dòng phụ) khai ở cột.
| Thuộc tính | Kiểu | Mặc định | Mô tả |
|---|---|---|---|
columns |
Column[] |
Định nghĩa cột | |
rows |
object[] |
[] |
Dữ liệu, tối đa 500 dòng |
rowKey |
string |
'id' |
Trường dùng làm khoá dòng |
loading |
boolean |
false |
|
pagination |
{ page: number; pageSize: number; total: number } | null |
null |
Phân trang phía app |
emptyText |
string |
Nội dung khi không có dòng | |
sort |
{ key: string; order: 'asc' | 'desc' } | null |
null |
Cột đang sắp xếp (app tự sắp dữ liệu hoặc gọi lại API) |
summary |
object | null |
null |
Dòng tổng cuối bảng, giá trị theo key của cột, định dạng theo cột |
selectable |
boolean |
false |
Ô chọn đầu mỗi dòng |
selected |
(string | number)[] |
[] |
Khoá các dòng đang chọn |
type Column = {
key: string; // tên trường trong row
title: string;
align?: 'left' | 'center' | 'right';
width?: number; // px
format?: 'text' | 'number' | 'money' | 'percent' | 'date' | 'datetime' | 'badge' | 'link';
tones?: Record<string, 'default' | 'info' | 'success' | 'warning' | 'critical'>; // màu badge theo giá trị ô
subtitle?: string; // trường hiện thành dòng phụ màu xám (ví dụ mã SKU dưới tên)
sortable?: boolean;
fixed?: 'left' | 'right'; // cố định cột khi cuộn ngang
ellipsis?: boolean; // cắt chữ dài trên một dòng
};percent: số là phần trăm (12.5 → 12,5%).badge: giá trị ô thành nhãn màu theotones.link: chữ dạng link, bấm phátonCellClick.
Sự kiện:
onPageChange:event.detaillà{ page, pageSize }.onRowClick:event.detaillà row được bấm.onSortChange:event.detaillà{ key, order }, hoặcnullkhi bỏ sắp xếp.onSelectionChange:event.detaillà mảng khoá các dòng đang chọn (giữ cả dòng ở trang khác).onCellClick:event.detaillà{ key, row }khi bấm ôformat: 'link'.
<pos-table
columns={[
{ key: 'system_id', title: 'Mã đơn' },
{ key: 'total_price', title: 'Tổng tiền', format: 'money', align: 'right' },
]}
rows={orders}
rowKey="system_id"
pagination={{ page, pageSize: 20, total }}
onPageChange={(e) => setPage(e.detail.page)}
/><pos-table
columns={[
{ key: 'name', title: 'Sản phẩm', subtitle: 'sku', sortable: true, fixed: 'left' },
{ key: 'state', title: 'SLA', format: 'badge', tones: { 'Trễ': 'critical', 'Đúng hạn': 'success' } },
{ key: 'code', title: 'Mã đơn', format: 'link' },
{ key: 'rate', title: 'Tỷ lệ', format: 'percent', align: 'right' },
]}
rows={rows}
sort={sort}
summary={{ name: 'Tổng', rate: 62.5 }}
selectable
selected={selected}
onSortChange={(e) => setSort(e.detail)}
onSelectionChange={(e) => setSelected(e.detail)}
onCellClick={(e) => openOrder(e.detail.row)}
/>pos-modal
Hộp thoại mở từ bên trong extension (ví dụ từ một nút trong block).
| Thuộc tính | Kiểu | Mô tả |
|---|---|---|
open |
boolean |
Trạng thái mở |
heading |
string |
Tiêu đề |
Slot footer cho các nút cuối hộp thoại. Sự kiện onClose khi người dùng bấm đóng hoặc ra ngoài.
<pos-modal open={open} heading="Xác nhận" onClose={() => setOpen(false)}>
<pos-text>Đẩy đơn sang ERP?</pos-text>
<pos-button slot="footer" onClick={() => setOpen(false)}>Huỷ</pos-button>
<pos-button slot="footer" variant="primary" onClick={push}>Đẩy</pos-button>
</pos-modal>Action presentation: "modal" đã có sẵn modal của POS; không cần bọc thêm pos-modal.
pos-drawer
Panel trượt từ cạnh phải, như "Cài đặt" của trang Thống kê: xem chi tiết một dòng mà không rời danh sách.
| Thuộc tính | Kiểu | Mặc định | Mô tả |
|---|---|---|---|
open |
boolean |
Trạng thái mở | |
heading |
string |
Tiêu đề | |
width |
'small' | 'medium' | 'large' |
'medium' |
400, 560 hoặc 800 px |
Slot footer cho các nút cuối panel. Sự kiện onClose khi người dùng bấm đóng hoặc ra ngoài.
Trang Worker UI
Các element dưới đây dùng để dựng trang render: "worker" và màn báo cáo (xem Extension point › app.page); dùng được cả trong block và modal.
pos-page
Khung trang. Trên trang app.page, heading và description hiện ở dòng tiêu đề của POS (không có heading thì dùng tiêu đề đặt bằng pos.page.setTitle hoặc title của extension), slot actions nằm bên phải cạnh nút chính, nội dung xếp dọc trên nền xám. Trong block và modal, pos-page là khối có tiêu đề.
| Thuộc tính | Kiểu | Mặc định | Mô tả |
|---|---|---|---|
heading |
string |
Tiêu đề trang | |
description |
string |
Mô tả dưới tiêu đề | |
width |
'narrow' | 'default' | 'full' |
'default' |
Độ rộng nội dung: 720 px, tối đa 1.800 px hoặc toàn màn hình |
Slot actions cho pos-button, pos-link, pos-menu-button; trên trang app.page nằm trước nút chính.
pos-tabs
Thanh chọn dạng chip có số đếm (badge), như thanh trạng thái của các trang danh sách POS. App giữ tab đang chọn và vẽ nội dung của tab đó làm element con.
| Thuộc tính | Kiểu | Mô tả |
|---|---|---|
tabs |
{ id: string; label: string; badge?: number; disabled?: boolean }[] |
Tối đa 20 tab; badge là số hiện cạnh nhãn |
selected |
string |
id của tab đang chọn |
Sự kiện onChange: event.detail là id của tab được chọn. POS đổi tab ngay khi bấm, không chờ app.
pos-filter-bar
Hàng bộ lọc phía trên bảng hoặc biểu đồ. Element con: pos-text-field, pos-number-field, pos-select, pos-checkbox, pos-date-picker, pos-date-range, pos-button, pos-menu-button. Ô nhập vẽ gọn một hàng, label thành chữ gợi ý khi không có placeholder; hết chỗ thì xuống dòng.
<pos-filter-bar>
<pos-text-field label="Tìm mã đơn" value={search} onChange={(e) => setSearch(e.detail)} />
<pos-select label="Nguồn đơn" options={sources} value={source} onChange={(e) => setSource(e.detail)} />
<pos-date-picker label="Từ ngày" value={from} onChange={(e) => setFrom(e.detail)} />
<pos-button onClick={reset}>Đặt lại</pos-button>
</pos-filter-bar>pos-chart
Biểu đồ do POS vẽ; app chỉ truyền dữ liệu. Màu theo bảng màu của POS.
| Thuộc tính | Kiểu | Mặc định | Mô tả |
|---|---|---|---|
type |
'line' | 'bar' | 'area' | 'pie' |
'line' |
Loại biểu đồ |
data |
object[] |
[] |
Mỗi điểm một object, giá trị chuỗi hoặc số; tối đa 500 điểm, 21 trường |
xKey |
string |
Trường làm trục ngang (tên lát với pie) |
|
series |
{ key: string; label: string; format?: 'number' | 'money' | 'percent'; type?: 'bar' | 'line' }[] |
[] |
Tối đa 10 chuỗi số liệu; pie dùng chuỗi đầu tiên. type từng chuỗi (biểu đồ bar, line) để vẽ cột kết hợp đường |
selected |
string | number | null |
Giá trị trục ngang đang chọn: cột, lát khác mờ đi; biểu đồ đường, vùng có vạch đánh dấu | |
stacked |
boolean |
false |
Chồng cột/vùng |
horizontal |
boolean |
false |
Cột nằm ngang (chỉ bar): hợp khi nhãn dài hoặc nhiều nhóm; chiều cao tự tăng theo số nhóm |
height |
number |
300 |
120–600 px |
loading |
boolean |
false |
|
emptyText |
string |
Nội dung khi không có dữ liệu |
- Sự kiện
onSelect: có nghe thì bấm được vào cột, lát hoặc điểm;event.detaillà{ x, series }:xlà giá trịxKeyđược bấm,serieslàkeycủa cột được bấm (nullvớiline,area,pie). format: 'percent': giá trị là phần trăm; mọi chuỗi đều là phần trăm thì trục số ghi thêm "%". Các chuỗi dùng chung một trục số, nên chỉ kết hợp những chuỗi cùng cỡ giá trị (số đơn với số đơn trễ, không phải số đơn với doanh thu).- Nhãn trục ngang dài được cắt cho vừa chỗ, rê chuột để xem đủ; nhiều nhóm đến mức mỗi nhóm quá hẹp thì nhãn xoay nghiêng. Nên bỏ các nhóm toàn số 0 và gom nhóm nhỏ vào "Khác" trước khi truyền
data.
<pos-chart
type="bar"
xKey="day"
data={[{ day: '01/10', revenue: 12500000, cost: 8000000 }, { day: '02/10', revenue: 9800000, cost: 7100000 }]}
series={[{ key: 'revenue', label: 'Doanh thu', format: 'money' }, { key: 'cost', label: 'Giá vốn', format: 'money' }]}
/>pos-stat
Ô số liệu cho màn báo cáo: nhãn, giá trị lớn, dòng phụ và mức thay đổi so với kỳ trước. Đặt nhiều ô trong pos-grid. Trên nền trang là thẻ trắng; trong khối trắng (pos-section, modal, block) là ô nền xám nhạt.
| Thuộc tính | Kiểu | Mặc định | Mô tả |
|---|---|---|---|
label |
string |
Tên chỉ số | |
value |
number | string | null |
Số được định dạng theo format; chuỗi hiện nguyên văn; null hiện — |
|
format |
'number' | 'money' | 'percent' |
'number' |
percent: value là phần trăm (58.3 → 58,3%) |
currency |
string |
tiền tệ của shop | Mã ISO 4217, dùng với money |
tone |
'default' | 'info' | 'success' | 'warning' | 'critical' |
'default' |
Màu của giá trị |
help |
string |
Dòng phụ dưới giá trị, ví dụ "12 đơn đã giao" | |
change |
number |
Mức thay đổi theo %, hiện kèm mũi tên; tăng là tốt (xanh), giảm là xấu (đỏ) | |
changeLabel |
string |
Ghi chú cạnh mức thay đổi, ví dụ "so với tuần trước" | |
inverse |
boolean |
false |
Đảo màu của change cho chỉ số càng thấp càng tốt (số đơn trễ, chi phí) |
selected |
boolean |
false |
Viền nổi, dùng khi thẻ đang làm bộ lọc |
loading |
boolean |
false |
Hiện khung chờ thay cho giá trị |
Sự kiện onClick: có nghe thì thẻ bấm được (cả bằng phím Enter), ví dụ bấm "Giao trễ" để lọc bảng bên dưới.
<pos-grid columns={4}>
<pos-stat label="Tỷ lệ đúng hạn" value={58.3} format="percent" help="12 đơn đã giao" change={4.2} changeLabel="so với tuần trước" />
<pos-stat label="Giao trễ" value={5} tone="critical" change={12.5} inverse selected={filter === 'late'} onClick={() => setFilter('late')} />
<pos-stat label="Phí hoàn" value={2450000} format="money" />
</pos-grid>