@omit-design/preset-mobile 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PATTERNS.md +134 -0
- package/README.md +29 -0
- package/catalog.tsx +365 -0
- package/components/OmAppBar.tsx +63 -0
- package/components/OmButton.tsx +41 -0
- package/components/OmCard.tsx +28 -0
- package/components/OmCouponCard.tsx +71 -0
- package/components/OmDialog.tsx +96 -0
- package/components/OmEmptyState.tsx +32 -0
- package/components/OmHeader.tsx +21 -0
- package/components/OmInput.tsx +38 -0
- package/components/OmListRow.tsx +30 -0
- package/components/OmMenuCard.tsx +47 -0
- package/components/OmNumpad.tsx +82 -0
- package/components/OmOrderFooter.tsx +78 -0
- package/components/OmPage.tsx +30 -0
- package/components/OmProductCard.tsx +75 -0
- package/components/OmSearchBar.tsx +51 -0
- package/components/OmSelect.tsx +47 -0
- package/components/OmSettingRow.tsx +95 -0
- package/components/OmSheet.tsx +49 -0
- package/components/OmStatCard.tsx +30 -0
- package/components/OmTabBar.tsx +26 -0
- package/components/OmTag.tsx +28 -0
- package/components/index.ts +30 -0
- package/components/inspect-attrs.ts +34 -0
- package/components/om-app-bar.css +83 -0
- package/components/om-coupon-card.css +107 -0
- package/components/om-dialog.css +81 -0
- package/components/om-empty-state.css +55 -0
- package/components/om-input.css +43 -0
- package/components/om-menu-card.css +68 -0
- package/components/om-numpad.css +49 -0
- package/components/om-order-footer.css +121 -0
- package/components/om-page.css +43 -0
- package/components/om-product-card.css +124 -0
- package/components/om-search-bar.css +39 -0
- package/components/om-select.css +28 -0
- package/components/om-setting-row.css +82 -0
- package/components/om-sheet.css +73 -0
- package/components/om-stat-card.css +40 -0
- package/components/om-tag.css +51 -0
- package/index.ts +14 -0
- package/package.json +48 -0
- package/preset.manifest.ts +62 -0
- package/templates/dashboard.tmpl.tsx +90 -0
- package/templates/detail-view.tmpl.tsx +60 -0
- package/templates/dialog-view.tmpl.tsx +34 -0
- package/templates/form-view.tmpl.tsx +52 -0
- package/templates/list-view.tmpl.tsx +58 -0
- package/templates/sheet-action.tmpl.tsx +52 -0
- package/templates/tab-view.tmpl.tsx +51 -0
- package/templates/welcome-view.tmpl.tsx +38 -0
- package/theme/baseline.ts +32 -0
- package/theme/presets/light.ts +8 -0
- package/theme/variables.css +183 -0
- package/tokens/index.ts +51 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { IonItem, IonLabel, IonSelect, IonSelectOption } from "@ionic/react";
|
|
2
|
+
import { inspectAttrs } from "./inspect-attrs";
|
|
3
|
+
import "./om-select.css";
|
|
4
|
+
|
|
5
|
+
export interface OmSelectOption {
|
|
6
|
+
label: string;
|
|
7
|
+
value: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface OmSelectProps {
|
|
11
|
+
label: string;
|
|
12
|
+
value?: string;
|
|
13
|
+
options: OmSelectOption[];
|
|
14
|
+
placeholder?: string;
|
|
15
|
+
onChange?: (value: string) => void;
|
|
16
|
+
/** Ionic 弹出样式;默认 action-sheet,移动端更贴 iOS */
|
|
17
|
+
interfaceType?: "alert" | "action-sheet" | "popover";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function OmSelect({
|
|
21
|
+
label,
|
|
22
|
+
value,
|
|
23
|
+
options,
|
|
24
|
+
placeholder,
|
|
25
|
+
onChange,
|
|
26
|
+
interfaceType = "action-sheet",
|
|
27
|
+
}: OmSelectProps) {
|
|
28
|
+
return (
|
|
29
|
+
<div className="om-select">
|
|
30
|
+
<IonItem {...inspectAttrs("OmSelect", { spacing: "md" })}>
|
|
31
|
+
<IonLabel position="stacked">{label}</IonLabel>
|
|
32
|
+
<IonSelect
|
|
33
|
+
interface={interfaceType}
|
|
34
|
+
value={value}
|
|
35
|
+
placeholder={placeholder}
|
|
36
|
+
onIonChange={(e) => onChange?.(e.detail.value)}
|
|
37
|
+
>
|
|
38
|
+
{options.map((o) => (
|
|
39
|
+
<IonSelectOption key={o.value} value={o.value}>
|
|
40
|
+
{o.label}
|
|
41
|
+
</IonSelectOption>
|
|
42
|
+
))}
|
|
43
|
+
</IonSelect>
|
|
44
|
+
</IonItem>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { IonIcon, IonToggle } from "@ionic/react";
|
|
3
|
+
import { chevronForward } from "ionicons/icons";
|
|
4
|
+
import { inspectAttrs } from "./inspect-attrs";
|
|
5
|
+
import "./om-setting-row.css";
|
|
6
|
+
|
|
7
|
+
type OmSettingKind =
|
|
8
|
+
| {
|
|
9
|
+
kind: "toggle";
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
onToggle?: (next: boolean) => void;
|
|
12
|
+
}
|
|
13
|
+
| {
|
|
14
|
+
kind: "navigate";
|
|
15
|
+
onClick?: () => void;
|
|
16
|
+
href?: string;
|
|
17
|
+
}
|
|
18
|
+
| {
|
|
19
|
+
kind: "value";
|
|
20
|
+
value: ReactNode;
|
|
21
|
+
onClick?: () => void;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
interface OmSettingRowBaseProps {
|
|
25
|
+
label: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
/** 左侧图标(可选) */
|
|
28
|
+
icon?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type OmSettingRowProps = OmSettingRowBaseProps & OmSettingKind;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 设置项单行 —— 三种形态:toggle / navigate / value。
|
|
35
|
+
* 用于 workstation / settings 设置页,列表语义统一。
|
|
36
|
+
*/
|
|
37
|
+
export function OmSettingRow(props: OmSettingRowProps) {
|
|
38
|
+
const { label, description, icon } = props;
|
|
39
|
+
const interactive = props.kind !== "toggle";
|
|
40
|
+
const onClick =
|
|
41
|
+
props.kind === "navigate" || props.kind === "value" ? props.onClick : undefined;
|
|
42
|
+
|
|
43
|
+
const inspect = inspectAttrs("OmSettingRow", { bg: "background", spacing: "lg" });
|
|
44
|
+
const className = `om-setting-row${interactive ? " pos-setting-row--interactive" : ""}`;
|
|
45
|
+
|
|
46
|
+
const inner = (
|
|
47
|
+
<>
|
|
48
|
+
{icon && (
|
|
49
|
+
<div className="om-setting-row__icon" aria-hidden>
|
|
50
|
+
<IonIcon icon={icon} />
|
|
51
|
+
</div>
|
|
52
|
+
)}
|
|
53
|
+
<div className="om-setting-row__body">
|
|
54
|
+
<span className="om-setting-row__label">{label}</span>
|
|
55
|
+
{description && <span className="om-setting-row__desc">{description}</span>}
|
|
56
|
+
</div>
|
|
57
|
+
<div className="om-setting-row__right">
|
|
58
|
+
{props.kind === "toggle" && (
|
|
59
|
+
<IonToggle
|
|
60
|
+
checked={props.enabled}
|
|
61
|
+
onIonChange={(e) => props.onToggle?.(e.detail.checked)}
|
|
62
|
+
aria-label={label}
|
|
63
|
+
/>
|
|
64
|
+
)}
|
|
65
|
+
{props.kind === "value" && (
|
|
66
|
+
<>
|
|
67
|
+
<span className="om-setting-row__value">{props.value}</span>
|
|
68
|
+
<IonIcon icon={chevronForward} className="om-setting-row__chevron" aria-hidden />
|
|
69
|
+
</>
|
|
70
|
+
)}
|
|
71
|
+
{props.kind === "navigate" && (
|
|
72
|
+
<IonIcon icon={chevronForward} className="om-setting-row__chevron" aria-hidden />
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
</>
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
if (props.kind === "navigate" && props.href) {
|
|
79
|
+
return (
|
|
80
|
+
<a className={className} href={props.href} {...inspect}>
|
|
81
|
+
{inner}
|
|
82
|
+
</a>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return interactive ? (
|
|
87
|
+
<button type="button" className={className} onClick={onClick} {...inspect}>
|
|
88
|
+
{inner}
|
|
89
|
+
</button>
|
|
90
|
+
) : (
|
|
91
|
+
<div className={className} {...inspect}>
|
|
92
|
+
{inner}
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { IonIcon } from "@ionic/react";
|
|
3
|
+
import { closeOutline } from "ionicons/icons";
|
|
4
|
+
import { useNavigate } from "react-router-dom";
|
|
5
|
+
import { inspectAttrs } from "./inspect-attrs";
|
|
6
|
+
import "./om-sheet.css";
|
|
7
|
+
|
|
8
|
+
interface OmSheetProps {
|
|
9
|
+
/** 顶部标题(可选;传 null 则不展示 title bar) */
|
|
10
|
+
title?: ReactNode;
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
/** 关闭:点击 scrim 或右上角 × —— 默认 navigate(-1) */
|
|
13
|
+
onDismiss?: () => void;
|
|
14
|
+
/** 关闭后的跳转路由(和 onDismiss 二选一) */
|
|
15
|
+
dismissHref?: string;
|
|
16
|
+
/** sheet 高度策略:auto(内容自适应)/ tall(70% 视口) */
|
|
17
|
+
size?: "auto" | "tall";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 底部抽屉 sheet —— 从下往上弹出的内容面板。
|
|
22
|
+
* 用于:优惠详情、快捷操作菜单、行内动作列表。
|
|
23
|
+
* 作为独立"弹窗稿"时,放在 OmPage 里叠一层(参考 dialog-view 的做法)。
|
|
24
|
+
*/
|
|
25
|
+
export function OmSheet({ title, children, onDismiss, dismissHref, size = "auto" }: OmSheetProps) {
|
|
26
|
+
const navigate = useNavigate();
|
|
27
|
+
const dismiss = () => {
|
|
28
|
+
if (onDismiss) return onDismiss();
|
|
29
|
+
if (dismissHref) return navigate(dismissHref);
|
|
30
|
+
navigate(-1);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className="om-sheet" {...inspectAttrs("OmSheet", { bg: "background", radius: "xl" })}>
|
|
35
|
+
<div className="om-sheet__scrim" aria-hidden onClick={dismiss} />
|
|
36
|
+
<div className={`pos-sheet__panel pos-sheet__panel--${size}`} role="dialog">
|
|
37
|
+
{title && (
|
|
38
|
+
<div className="om-sheet__head">
|
|
39
|
+
<span className="om-sheet__title">{title}</span>
|
|
40
|
+
<button className="om-sheet__close" type="button" onClick={dismiss} aria-label="关闭">
|
|
41
|
+
<IonIcon icon={closeOutline} />
|
|
42
|
+
</button>
|
|
43
|
+
</div>
|
|
44
|
+
)}
|
|
45
|
+
<div className="om-sheet__body">{children}</div>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { inspectAttrs } from "./inspect-attrs";
|
|
3
|
+
import "./om-stat-card.css";
|
|
4
|
+
|
|
5
|
+
interface OmStatCardProps {
|
|
6
|
+
label: string;
|
|
7
|
+
/** 大号数字 —— 可传格式化好的字符串("¥12,480.50") */
|
|
8
|
+
value: string;
|
|
9
|
+
/** 右上角辅助 slot(如「月份」小字) */
|
|
10
|
+
meta?: ReactNode;
|
|
11
|
+
/** 副标题(如单位、对比) */
|
|
12
|
+
caption?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 统计数字卡 —— 用于工作台营收 / 客单等核心指标展示。
|
|
17
|
+
* 单一职责:标签 + 大号数字 + 可选副标题 / meta。
|
|
18
|
+
*/
|
|
19
|
+
export function OmStatCard({ label, value, meta, caption }: OmStatCardProps) {
|
|
20
|
+
return (
|
|
21
|
+
<div className="om-stat" {...inspectAttrs("OmStatCard", { bg: "background", radius: "lg", shadow: "sm" })}>
|
|
22
|
+
<div className="om-stat__head">
|
|
23
|
+
<span className="om-stat__label">{label}</span>
|
|
24
|
+
{meta && <span className="om-stat__meta">{meta}</span>}
|
|
25
|
+
</div>
|
|
26
|
+
<div className="om-stat__value">{value}</div>
|
|
27
|
+
{caption && <div className="om-stat__caption">{caption}</div>}
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { IonIcon, IonLabel, IonTabBar, IonTabButton } from "@ionic/react";
|
|
2
|
+
import { inspectAttrs } from "./inspect-attrs";
|
|
3
|
+
|
|
4
|
+
export interface OmTabItem {
|
|
5
|
+
tab: string;
|
|
6
|
+
href: string;
|
|
7
|
+
label: string;
|
|
8
|
+
icon: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface OmTabBarProps {
|
|
12
|
+
items: OmTabItem[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function OmTabBar({ items }: OmTabBarProps) {
|
|
16
|
+
return (
|
|
17
|
+
<IonTabBar slot="bottom" {...inspectAttrs("OmTabBar", { bg: "background" })}>
|
|
18
|
+
{items.map((item) => (
|
|
19
|
+
<IonTabButton key={item.tab} tab={item.tab} href={item.href}>
|
|
20
|
+
<IonIcon icon={item.icon} aria-hidden="true" />
|
|
21
|
+
<IonLabel>{item.label}</IonLabel>
|
|
22
|
+
</IonTabButton>
|
|
23
|
+
))}
|
|
24
|
+
</IonTabBar>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { inspectAttrs } from "./inspect-attrs";
|
|
3
|
+
import type { ColorTokenName } from "../tokens";
|
|
4
|
+
import "./om-tag.css";
|
|
5
|
+
|
|
6
|
+
interface OmTagProps {
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
color?: ColorTokenName;
|
|
9
|
+
/** 视觉:solid 填充色、soft 浅色底、outline 描边 */
|
|
10
|
+
variant?: "solid" | "soft" | "outline";
|
|
11
|
+
size?: "sm" | "md";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 小尺寸的 chip / badge / 标签。
|
|
16
|
+
* 用于:门槛标签("无门槛")、VIP 级别、状态徽章。
|
|
17
|
+
*/
|
|
18
|
+
export function OmTag({ children, color = "primary", variant = "soft", size = "sm" }: OmTagProps) {
|
|
19
|
+
return (
|
|
20
|
+
<span
|
|
21
|
+
className={`pos-tag pos-tag--${variant} pos-tag--${size}`}
|
|
22
|
+
data-color={color}
|
|
23
|
+
{...inspectAttrs("OmTag", { color, radius: "sm", fontSize: "xs" })}
|
|
24
|
+
>
|
|
25
|
+
{children}
|
|
26
|
+
</span>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* preset-mobile 组件白名单 — 业务页面(design/**)唯一允许 import 的来源。
|
|
3
|
+
*
|
|
4
|
+
* **不要**在业务页面里 `import from '@ionic/react'`,
|
|
5
|
+
* 如果发现某个移动端模式 Om* 没覆盖到,先来这里加封装。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { OmPage } from "./OmPage";
|
|
9
|
+
export { OmHeader } from "./OmHeader";
|
|
10
|
+
export { OmAppBar } from "./OmAppBar";
|
|
11
|
+
export { OmButton } from "./OmButton";
|
|
12
|
+
export { OmCard } from "./OmCard";
|
|
13
|
+
export { OmListRow } from "./OmListRow";
|
|
14
|
+
export { OmInput } from "./OmInput";
|
|
15
|
+
export { OmSelect } from "./OmSelect";
|
|
16
|
+
export type { OmSelectOption } from "./OmSelect";
|
|
17
|
+
export { OmDialog } from "./OmDialog";
|
|
18
|
+
export { OmTabBar } from "./OmTabBar";
|
|
19
|
+
export type { OmTabItem } from "./OmTabBar";
|
|
20
|
+
export { OmNumpad } from "./OmNumpad";
|
|
21
|
+
export { OmSearchBar } from "./OmSearchBar";
|
|
22
|
+
export { OmProductCard } from "./OmProductCard";
|
|
23
|
+
export { OmEmptyState } from "./OmEmptyState";
|
|
24
|
+
export { OmTag } from "./OmTag";
|
|
25
|
+
export { OmOrderFooter } from "./OmOrderFooter";
|
|
26
|
+
export { OmCouponCard } from "./OmCouponCard";
|
|
27
|
+
export { OmStatCard } from "./OmStatCard";
|
|
28
|
+
export { OmMenuCard } from "./OmMenuCard";
|
|
29
|
+
export { OmSettingRow } from "./OmSettingRow";
|
|
30
|
+
export { OmSheet } from "./OmSheet";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ColorTokenName, SpacingTokenName, RadiusTokenName, FontSizeTokenName, ShadowTokenName } from "../tokens";
|
|
2
|
+
|
|
3
|
+
export type TokenRefs = {
|
|
4
|
+
color?: ColorTokenName;
|
|
5
|
+
bg?: ColorTokenName;
|
|
6
|
+
spacing?: SpacingTokenName | SpacingTokenName[];
|
|
7
|
+
radius?: RadiusTokenName;
|
|
8
|
+
fontSize?: FontSizeTokenName;
|
|
9
|
+
shadow?: ShadowTokenName;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Inspect 用:把组件用到的 token 编码到 data-omit-tokens 属性,
|
|
14
|
+
* 运行时由 packages/engine/src/inspect 读取并展示「token 名」而不是字面量值。
|
|
15
|
+
*/
|
|
16
|
+
export function inspectAttrs(component: string, tokens?: TokenRefs) {
|
|
17
|
+
const attrs: Record<string, string> = {
|
|
18
|
+
"data-omit-component": component,
|
|
19
|
+
};
|
|
20
|
+
if (tokens) {
|
|
21
|
+
const flat: string[] = [];
|
|
22
|
+
if (tokens.color) flat.push(`color:${tokens.color}`);
|
|
23
|
+
if (tokens.bg) flat.push(`bg:${tokens.bg}`);
|
|
24
|
+
if (tokens.spacing) {
|
|
25
|
+
const arr = Array.isArray(tokens.spacing) ? tokens.spacing : [tokens.spacing];
|
|
26
|
+
flat.push(`spacing:${arr.join(",")}`);
|
|
27
|
+
}
|
|
28
|
+
if (tokens.radius) flat.push(`radius:${tokens.radius}`);
|
|
29
|
+
if (tokens.fontSize) flat.push(`fontSize:${tokens.fontSize}`);
|
|
30
|
+
if (tokens.shadow) flat.push(`shadow:${tokens.shadow}`);
|
|
31
|
+
if (flat.length) attrs["data-omit-tokens"] = flat.join("|");
|
|
32
|
+
}
|
|
33
|
+
return attrs;
|
|
34
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/* OmAppBar —— POS 应用 header(非表单页的 header)。
|
|
2
|
+
与 OmHeader(标准标题栏)区分:OmAppBar 是信息密度更高的 app shell 级 header。 */
|
|
3
|
+
|
|
4
|
+
.om-app-bar {
|
|
5
|
+
display: flex;
|
|
6
|
+
align-items: center;
|
|
7
|
+
gap: var(--om-spacing-md);
|
|
8
|
+
padding: var(--om-spacing-sm) var(--om-spacing-lg);
|
|
9
|
+
background: var(--ion-background-color);
|
|
10
|
+
min-height: var(--om-control-height-xl);
|
|
11
|
+
border-bottom: 1px solid var(--ion-color-light);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.om-app-bar--store .om-app-bar__info {
|
|
15
|
+
display: flex;
|
|
16
|
+
flex-direction: column;
|
|
17
|
+
gap: var(--om-spacing-xs);
|
|
18
|
+
flex: 1 1 auto;
|
|
19
|
+
min-width: 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.om-app-bar__info-primary {
|
|
23
|
+
margin: 0;
|
|
24
|
+
font-size: var(--om-font-size-sm);
|
|
25
|
+
color: var(--ion-color-dark);
|
|
26
|
+
font-weight: 500;
|
|
27
|
+
white-space: nowrap;
|
|
28
|
+
overflow: hidden;
|
|
29
|
+
text-overflow: ellipsis;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.om-app-bar__info-secondary {
|
|
33
|
+
margin: 0;
|
|
34
|
+
font-size: var(--om-font-size-sm);
|
|
35
|
+
color: var(--ion-color-dark);
|
|
36
|
+
font-weight: 500;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.om-app-bar__divider {
|
|
40
|
+
width: 1px;
|
|
41
|
+
height: var(--om-spacing-2xl);
|
|
42
|
+
background: var(--ion-color-light);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.om-app-bar__right {
|
|
46
|
+
display: flex;
|
|
47
|
+
align-items: center;
|
|
48
|
+
gap: var(--om-spacing-sm);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* brand 变体(商户中心) */
|
|
52
|
+
.om-app-bar--brand {
|
|
53
|
+
justify-content: space-between;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.om-app-bar__brand {
|
|
57
|
+
display: flex;
|
|
58
|
+
align-items: center;
|
|
59
|
+
gap: var(--om-spacing-sm);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.om-app-bar__brand-icon {
|
|
63
|
+
font-size: var(--om-icon-size-lg);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.om-app-bar__brand-title {
|
|
67
|
+
font-size: var(--om-font-size-md);
|
|
68
|
+
font-weight: 500;
|
|
69
|
+
color: var(--ion-color-dark);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.om-app-bar__avatar {
|
|
73
|
+
width: var(--om-spacing-2xl);
|
|
74
|
+
height: var(--om-spacing-2xl);
|
|
75
|
+
border-radius: var(--om-radius-full);
|
|
76
|
+
background: var(--ion-color-light);
|
|
77
|
+
display: flex;
|
|
78
|
+
align-items: center;
|
|
79
|
+
justify-content: center;
|
|
80
|
+
overflow: hidden;
|
|
81
|
+
font-size: var(--om-font-size-sm);
|
|
82
|
+
color: var(--ion-color-medium);
|
|
83
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/* OmCouponCard —— 优惠券 / 奖励卡(会员详情 & 奖励确认两处复用) */
|
|
2
|
+
|
|
3
|
+
.om-coupon {
|
|
4
|
+
display: flex;
|
|
5
|
+
gap: var(--om-spacing-md);
|
|
6
|
+
padding: var(--om-spacing-md);
|
|
7
|
+
background: var(--ion-background-color);
|
|
8
|
+
border-radius: var(--om-radius-md);
|
|
9
|
+
border: 1px solid transparent;
|
|
10
|
+
align-items: center;
|
|
11
|
+
width: 100%;
|
|
12
|
+
text-align: left;
|
|
13
|
+
appearance: none;
|
|
14
|
+
cursor: pointer;
|
|
15
|
+
font: inherit;
|
|
16
|
+
color: inherit;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
button.om-coupon {
|
|
20
|
+
cursor: pointer;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.om-coupon--selected {
|
|
24
|
+
border-color: var(--ion-color-primary);
|
|
25
|
+
background: var(--om-surface-primary-softest);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.om-coupon__badge {
|
|
29
|
+
flex: 0 0 auto;
|
|
30
|
+
width: 72px;
|
|
31
|
+
min-height: var(--om-control-height-md);
|
|
32
|
+
text-align: center;
|
|
33
|
+
border-radius: var(--om-radius-sm);
|
|
34
|
+
padding: var(--om-spacing-sm);
|
|
35
|
+
background: var(--ion-color-light);
|
|
36
|
+
display: flex;
|
|
37
|
+
flex-direction: column;
|
|
38
|
+
align-items: center;
|
|
39
|
+
justify-content: center;
|
|
40
|
+
gap: var(--om-spacing-xs);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.om-coupon--selected .om-coupon__badge {
|
|
44
|
+
background: var(--om-surface-primary-soft);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.om-coupon__badge-value {
|
|
48
|
+
display: block;
|
|
49
|
+
font-size: var(--om-font-size-lg);
|
|
50
|
+
font-weight: 700;
|
|
51
|
+
color: var(--ion-color-dark);
|
|
52
|
+
line-height: 1;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.om-coupon--selected .om-coupon__badge-value {
|
|
56
|
+
color: var(--ion-color-primary);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.om-coupon__badge-unit {
|
|
60
|
+
display: block;
|
|
61
|
+
font-size: var(--om-font-size-xs);
|
|
62
|
+
color: var(--ion-color-medium);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.om-coupon__body {
|
|
66
|
+
flex: 1 1 auto;
|
|
67
|
+
min-width: 0;
|
|
68
|
+
display: flex;
|
|
69
|
+
flex-direction: column;
|
|
70
|
+
gap: var(--om-spacing-xs);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.om-coupon__title {
|
|
74
|
+
margin: 0;
|
|
75
|
+
font-size: var(--om-font-size-sm);
|
|
76
|
+
color: var(--ion-color-dark);
|
|
77
|
+
font-weight: 500;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.om-coupon__condition,
|
|
81
|
+
.om-coupon__expire {
|
|
82
|
+
margin: 0;
|
|
83
|
+
font-size: var(--om-font-size-xs);
|
|
84
|
+
color: var(--ion-color-medium);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.om-coupon__radio {
|
|
88
|
+
flex: 0 0 auto;
|
|
89
|
+
width: var(--om-spacing-xl);
|
|
90
|
+
height: var(--om-spacing-xl);
|
|
91
|
+
border-radius: var(--om-radius-full);
|
|
92
|
+
border: 1px solid var(--ion-color-light-shade);
|
|
93
|
+
display: flex;
|
|
94
|
+
align-items: center;
|
|
95
|
+
justify-content: center;
|
|
96
|
+
color: transparent;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.om-coupon--selected .om-coupon__radio {
|
|
100
|
+
background: var(--ion-color-primary);
|
|
101
|
+
border-color: var(--ion-color-primary);
|
|
102
|
+
color: var(--ion-color-primary-contrast);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.om-coupon__radio ion-icon {
|
|
106
|
+
font-size: var(--om-icon-size-sm);
|
|
107
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/* OmDialog —— 独立一张稿的对话框形态:全屏 scrim + 居中卡片。
|
|
2
|
+
不使用 IonModal —— 作为 dialog-view pattern,本身就是一个页面的主体。 */
|
|
3
|
+
|
|
4
|
+
.om-dialog {
|
|
5
|
+
position: absolute;
|
|
6
|
+
inset: 0;
|
|
7
|
+
display: flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
justify-content: center;
|
|
10
|
+
padding: var(--om-spacing-xl);
|
|
11
|
+
z-index: 10;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.om-dialog__scrim {
|
|
15
|
+
position: absolute;
|
|
16
|
+
inset: 0;
|
|
17
|
+
background: var(--om-overlay-scrim);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.om-dialog__card {
|
|
21
|
+
position: relative;
|
|
22
|
+
background: var(--ion-background-color);
|
|
23
|
+
border-radius: var(--om-radius-xl);
|
|
24
|
+
padding: var(--om-spacing-xl) var(--om-spacing-lg) var(--om-spacing-lg);
|
|
25
|
+
width: 100%;
|
|
26
|
+
max-width: 334px;
|
|
27
|
+
display: flex;
|
|
28
|
+
flex-direction: column;
|
|
29
|
+
align-items: center;
|
|
30
|
+
gap: var(--om-spacing-lg);
|
|
31
|
+
text-align: center;
|
|
32
|
+
box-shadow: var(--om-shadow-lg);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.om-dialog__icon {
|
|
36
|
+
display: inline-flex;
|
|
37
|
+
align-items: center;
|
|
38
|
+
justify-content: center;
|
|
39
|
+
width: 64px;
|
|
40
|
+
height: 64px;
|
|
41
|
+
line-height: 1;
|
|
42
|
+
}
|
|
43
|
+
.om-dialog__icon ion-icon {
|
|
44
|
+
font-size: 64px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.om-dialog__title {
|
|
48
|
+
margin: 0;
|
|
49
|
+
font-size: var(--om-font-size-lg);
|
|
50
|
+
font-weight: 600;
|
|
51
|
+
color: var(--ion-color-dark);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.om-dialog__subtitle {
|
|
55
|
+
margin: 0;
|
|
56
|
+
font-size: var(--om-font-size-sm);
|
|
57
|
+
color: var(--ion-color-medium);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.om-dialog__actions {
|
|
61
|
+
width: 100%;
|
|
62
|
+
margin-top: var(--om-spacing-sm);
|
|
63
|
+
display: flex;
|
|
64
|
+
flex-direction: column;
|
|
65
|
+
gap: var(--om-spacing-sm);
|
|
66
|
+
}
|
|
67
|
+
.om-dialog__actions--two {
|
|
68
|
+
flex-direction: row;
|
|
69
|
+
}
|
|
70
|
+
.om-dialog__actions--two ion-button {
|
|
71
|
+
flex: 1 1 0;
|
|
72
|
+
}
|
|
73
|
+
.om-dialog__actions ion-button {
|
|
74
|
+
--border-radius: var(--om-radius-md);
|
|
75
|
+
height: var(--om-control-height-xl);
|
|
76
|
+
font-size: var(--om-font-size-lg);
|
|
77
|
+
font-weight: 600;
|
|
78
|
+
}
|
|
79
|
+
.om-dialog__body {
|
|
80
|
+
width: 100%;
|
|
81
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/* OmEmptyState —— 空态面板 */
|
|
2
|
+
|
|
3
|
+
.om-empty {
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
align-items: center;
|
|
7
|
+
justify-content: center;
|
|
8
|
+
gap: var(--om-spacing-lg);
|
|
9
|
+
padding: var(--om-spacing-xl) var(--om-spacing-lg);
|
|
10
|
+
text-align: center;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.om-empty__icon {
|
|
14
|
+
/* 64×64 = 两倍 spacing-2xl,画报级占位大小 */
|
|
15
|
+
width: calc(var(--om-spacing-2xl) * 2);
|
|
16
|
+
height: calc(var(--om-spacing-2xl) * 2);
|
|
17
|
+
border-radius: var(--om-radius-full);
|
|
18
|
+
background: var(--ion-color-light);
|
|
19
|
+
display: flex;
|
|
20
|
+
align-items: center;
|
|
21
|
+
justify-content: center;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.om-empty__icon ion-icon {
|
|
25
|
+
font-size: var(--om-icon-size-xl);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.om-empty__title {
|
|
29
|
+
margin: 0;
|
|
30
|
+
font-size: var(--om-font-size-md);
|
|
31
|
+
font-weight: 500;
|
|
32
|
+
color: var(--ion-color-dark);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.om-empty__desc {
|
|
36
|
+
margin: 0;
|
|
37
|
+
font-size: var(--om-font-size-sm);
|
|
38
|
+
color: var(--ion-color-medium);
|
|
39
|
+
max-width: 280px;
|
|
40
|
+
line-height: 1.5;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.om-empty__actions {
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-direction: column;
|
|
46
|
+
align-items: center;
|
|
47
|
+
gap: var(--om-spacing-sm);
|
|
48
|
+
width: 100%;
|
|
49
|
+
max-width: 240px;
|
|
50
|
+
margin-top: var(--om-spacing-sm);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.om-empty__actions ion-button {
|
|
54
|
+
width: 100%;
|
|
55
|
+
}
|