feat(uikit): 引入明缘主题色板与通用组件
This commit is contained in:
@@ -1,4 +1,11 @@
|
||||
export { AppColor, AppSpacing, AppFontSize } from './theme/Theme';
|
||||
export { AppColor, AppSpacing, AppFontSize, AppRadius } from './theme/Theme';
|
||||
export { AppButton, ButtonVariant } from './components/AppButton';
|
||||
export { LoadingView } from './components/LoadingView';
|
||||
export { EmptyView } from './components/EmptyView';
|
||||
export { SegmentedControl } from './components/SegmentedControl';
|
||||
export { ProgressBar } from './components/ProgressBar';
|
||||
export { Avatar } from './components/Avatar';
|
||||
export { LabeledRow } from './components/LabeledRow';
|
||||
export { EmptyState, ErrorState, LoggedOutState } from './components/StateViews';
|
||||
export { StatusBadge } from './components/StatusBadge';
|
||||
export { StatusPill } from './components/StatusPill';
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { AppColor } from '../theme/Theme';
|
||||
import { AppColor, AppFontSize, AppRadius } from '../theme/Theme';
|
||||
|
||||
export enum ButtonVariant {
|
||||
Primary = 'primary',
|
||||
Secondary = 'secondary'
|
||||
Secondary = 'secondary',
|
||||
Danger = 'danger',
|
||||
Outline = 'outline'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -13,16 +15,19 @@ export struct AppButton {
|
||||
@Param text: string = '';
|
||||
@Param variant: ButtonVariant = ButtonVariant.Primary;
|
||||
@Param disabled: boolean = false;
|
||||
@Param radius: number = AppRadius.md;
|
||||
@Event onTap: () => void;
|
||||
|
||||
build() {
|
||||
Button(this.text)
|
||||
.width('100%')
|
||||
.height(48)
|
||||
.fontSize(16)
|
||||
.fontColor(this.variant === ButtonVariant.Primary ? AppColor.white : AppColor.primary)
|
||||
.backgroundColor(this.variant === ButtonVariant.Primary ? AppColor.primary : AppColor.white)
|
||||
.borderRadius(8)
|
||||
.fontSize(AppFontSize.title)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(this.fontColor())
|
||||
.backgroundColor(this.bgColor())
|
||||
.border({ width: this.variant === ButtonVariant.Outline ? 1 : 0, color: AppColor.gold })
|
||||
.borderRadius(this.radius)
|
||||
.enabled(!this.disabled)
|
||||
.onClick(() => {
|
||||
if (!this.disabled) {
|
||||
@@ -30,4 +35,24 @@ export struct AppButton {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fontColor(): string {
|
||||
if (this.variant === ButtonVariant.Primary || this.variant === ButtonVariant.Danger) {
|
||||
return AppColor.white;
|
||||
}
|
||||
return AppColor.gold;
|
||||
}
|
||||
|
||||
private bgColor(): string {
|
||||
if (this.disabled) {
|
||||
return AppColor.disabled;
|
||||
}
|
||||
if (this.variant === ButtonVariant.Primary) {
|
||||
return AppColor.gold;
|
||||
}
|
||||
if (this.variant === ButtonVariant.Danger) {
|
||||
return AppColor.danger;
|
||||
}
|
||||
return AppColor.white;
|
||||
}
|
||||
}
|
||||
|
||||
23
uikit/src/main/ets/components/Avatar.ets
Normal file
23
uikit/src/main/ets/components/Avatar.ets
Normal file
@@ -0,0 +1,23 @@
|
||||
import { AppColor } from '../theme/Theme';
|
||||
|
||||
/**
|
||||
* 首字母圆形头像:金色底白字。
|
||||
*/
|
||||
@ComponentV2
|
||||
export struct Avatar {
|
||||
@Param name: string = '';
|
||||
@Param diameter: number = 40;
|
||||
|
||||
build() {
|
||||
Stack({ alignContent: Alignment.Center }) {
|
||||
Text(this.name.length > 0 ? this.name.charAt(0) : '明')
|
||||
.fontSize(this.diameter * 0.42)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor(AppColor.white)
|
||||
}
|
||||
.width(this.diameter)
|
||||
.height(this.diameter)
|
||||
.borderRadius(this.diameter / 2)
|
||||
.backgroundColor(AppColor.gold)
|
||||
}
|
||||
}
|
||||
27
uikit/src/main/ets/components/LabeledRow.ets
Normal file
27
uikit/src/main/ets/components/LabeledRow.ets
Normal file
@@ -0,0 +1,27 @@
|
||||
import { AppColor, AppFontSize } from '../theme/Theme';
|
||||
|
||||
/**
|
||||
* 左标签右值的信息行(用于「我的」页个人信息)。
|
||||
*/
|
||||
@ComponentV2
|
||||
export struct LabeledRow {
|
||||
@Param label: string = '';
|
||||
@Param value: string = '';
|
||||
|
||||
build() {
|
||||
Row() {
|
||||
Text(this.label)
|
||||
.fontSize(AppFontSize.body)
|
||||
.fontColor(AppColor.textMuted)
|
||||
Blank()
|
||||
Text(this.value)
|
||||
.fontSize(AppFontSize.body)
|
||||
.fontColor(AppColor.textPrimary)
|
||||
.textAlign(TextAlign.End)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ top: 14, bottom: 14 })
|
||||
}
|
||||
}
|
||||
25
uikit/src/main/ets/components/ProgressBar.ets
Normal file
25
uikit/src/main/ets/components/ProgressBar.ets
Normal file
@@ -0,0 +1,25 @@
|
||||
import { AppColor, AppRadius } from '../theme/Theme';
|
||||
|
||||
/**
|
||||
* 金色进度条:0-100 百分比。
|
||||
*/
|
||||
@ComponentV2
|
||||
export struct ProgressBar {
|
||||
@Param percent: number = 0;
|
||||
|
||||
build() {
|
||||
Stack({ alignContent: Alignment.Start }) {
|
||||
Row()
|
||||
.width('100%')
|
||||
.height(10)
|
||||
.backgroundColor(AppColor.divider)
|
||||
.borderRadius(AppRadius.pill)
|
||||
Row()
|
||||
.width(this.percent + '%')
|
||||
.height(10)
|
||||
.linearGradient({ angle: 90, colors: [[AppColor.gold, 0.0], [AppColor.goldBright, 1.0]] })
|
||||
.borderRadius(AppRadius.pill)
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
}
|
||||
35
uikit/src/main/ets/components/SegmentedControl.ets
Normal file
35
uikit/src/main/ets/components/SegmentedControl.ets
Normal file
@@ -0,0 +1,35 @@
|
||||
import { AppColor, AppFontSize, AppRadius } from '../theme/Theme';
|
||||
|
||||
/**
|
||||
* 分段切换控件:多个等宽选项,选中项白底金字。
|
||||
* 用于「周/月/年计划」类型切换、「小组/个人」tab 切换。
|
||||
*/
|
||||
@ComponentV2
|
||||
export struct SegmentedControl {
|
||||
@Param items: string[] = [];
|
||||
@Param selectedIndex: number = 0;
|
||||
@Event onSelect: (index: number) => void;
|
||||
|
||||
build() {
|
||||
Row() {
|
||||
ForEach(this.items, (item: string, index: number) => {
|
||||
Text(item)
|
||||
.fontSize(AppFontSize.body)
|
||||
.fontWeight(index === this.selectedIndex ? FontWeight.Bold : FontWeight.Normal)
|
||||
.fontColor(index === this.selectedIndex ? AppColor.gold : AppColor.textMuted)
|
||||
.backgroundColor(index === this.selectedIndex ? AppColor.white : Color.Transparent)
|
||||
.borderRadius(AppRadius.pill)
|
||||
.textAlign(TextAlign.Center)
|
||||
.height(36)
|
||||
.layoutWeight(1)
|
||||
.onClick(() => {
|
||||
this.onSelect(index);
|
||||
})
|
||||
}, (item: string) => item)
|
||||
}
|
||||
.width('100%')
|
||||
.padding(4)
|
||||
.backgroundColor(AppColor.segmentedTrack)
|
||||
.borderRadius(AppRadius.pill)
|
||||
}
|
||||
}
|
||||
103
uikit/src/main/ets/components/StateViews.ets
Normal file
103
uikit/src/main/ets/components/StateViews.ets
Normal file
@@ -0,0 +1,103 @@
|
||||
import { AppColor, AppFontSize, AppRadius, AppSpacing } from '../theme/Theme';
|
||||
|
||||
/**
|
||||
* 空态:emoji + 说明。
|
||||
*/
|
||||
@ComponentV2
|
||||
export struct EmptyState {
|
||||
@Param emoji: string = '📭';
|
||||
@Param message: string = '暂无数据';
|
||||
|
||||
build() {
|
||||
Column({ space: AppSpacing.md }) {
|
||||
Text(this.emoji)
|
||||
.fontSize(40)
|
||||
Text(this.message)
|
||||
.fontSize(AppFontSize.body)
|
||||
.fontColor(AppColor.textFaint)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ top: 48, bottom: 48 })
|
||||
.justifyContent(FlexAlign.Center)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误态:圆形「!」+ 文案 + 重新加载。
|
||||
*/
|
||||
@ComponentV2
|
||||
export struct ErrorState {
|
||||
@Param message: string = '加载失败,请重试';
|
||||
@Event onRetry: () => void;
|
||||
|
||||
build() {
|
||||
Column({ space: AppSpacing.md }) {
|
||||
Stack({ alignContent: Alignment.Center }) {
|
||||
Text('!')
|
||||
.fontSize(28)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor(AppColor.danger)
|
||||
}
|
||||
.width(56)
|
||||
.height(56)
|
||||
.borderRadius(28)
|
||||
.backgroundColor(AppColor.errorBg)
|
||||
|
||||
Text(this.message)
|
||||
.fontSize(AppFontSize.body)
|
||||
.fontColor(AppColor.textMuted)
|
||||
.textAlign(TextAlign.Center)
|
||||
|
||||
Text('重新加载')
|
||||
.fontSize(AppFontSize.body)
|
||||
.fontColor(AppColor.white)
|
||||
.padding({ left: 32, right: 32, top: 10, bottom: 10 })
|
||||
.backgroundColor(AppColor.gold)
|
||||
.borderRadius(AppRadius.pill)
|
||||
.onClick(() => {
|
||||
this.onRetry();
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.padding({ top: 80, bottom: 80 })
|
||||
.justifyContent(FlexAlign.Center)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 未登录占位:🔒 + 说明 + 去登录。
|
||||
*/
|
||||
@ComponentV2
|
||||
export struct LoggedOutState {
|
||||
@Param message: string = '登录后即可查看';
|
||||
@Event onLogin: () => void;
|
||||
|
||||
build() {
|
||||
Column({ space: AppSpacing.md }) {
|
||||
Text('🔒')
|
||||
.fontSize(40)
|
||||
Text('未登录')
|
||||
.fontSize(AppFontSize.title)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor(AppColor.textPrimary)
|
||||
Text(this.message)
|
||||
.fontSize(AppFontSize.body)
|
||||
.fontColor(AppColor.textMuted)
|
||||
.textAlign(TextAlign.Center)
|
||||
Text('去登录')
|
||||
.fontSize(AppFontSize.body)
|
||||
.fontColor(AppColor.white)
|
||||
.padding({ left: 32, right: 32, top: 10, bottom: 10 })
|
||||
.backgroundColor(AppColor.gold)
|
||||
.borderRadius(AppRadius.pill)
|
||||
.onClick(() => {
|
||||
this.onLogin();
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.padding({ top: 80, bottom: 80 })
|
||||
.justifyContent(FlexAlign.Center)
|
||||
}
|
||||
}
|
||||
26
uikit/src/main/ets/components/StatusBadge.ets
Normal file
26
uikit/src/main/ets/components/StatusBadge.ets
Normal file
@@ -0,0 +1,26 @@
|
||||
import { AppColor, AppFontSize, AppRadius } from '../theme/Theme';
|
||||
|
||||
/**
|
||||
* 状态徽标:彩色圆点 + 文字(浅底)。label/color 由调用方提供,保持组件领域无关。
|
||||
*/
|
||||
@ComponentV2
|
||||
export struct StatusBadge {
|
||||
@Param label: string = '';
|
||||
@Param color: string = AppColor.gold;
|
||||
|
||||
build() {
|
||||
Row({ space: 6 }) {
|
||||
Row()
|
||||
.width(8)
|
||||
.height(8)
|
||||
.borderRadius(4)
|
||||
.backgroundColor(this.color)
|
||||
Text(this.label)
|
||||
.fontSize(AppFontSize.caption)
|
||||
.fontColor(this.color)
|
||||
}
|
||||
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
|
||||
.backgroundColor(AppColor.neutralChip)
|
||||
.borderRadius(AppRadius.pill)
|
||||
}
|
||||
}
|
||||
33
uikit/src/main/ets/components/StatusPill.ets
Normal file
33
uikit/src/main/ets/components/StatusPill.ets
Normal file
@@ -0,0 +1,33 @@
|
||||
import { AppColor, AppFontSize, AppRadius } from '../theme/Theme';
|
||||
|
||||
/**
|
||||
* 可点选状态胶囊:选中态 = 状态色底白字,未选中 = 白底灰字。label/color 由调用方提供。
|
||||
*/
|
||||
@ComponentV2
|
||||
export struct StatusPill {
|
||||
@Param label: string = '';
|
||||
@Param color: string = AppColor.gold;
|
||||
@Param selected: boolean = false;
|
||||
@Event onTap: () => void;
|
||||
|
||||
build() {
|
||||
Row({ space: 6 }) {
|
||||
Row()
|
||||
.width(8)
|
||||
.height(8)
|
||||
.borderRadius(4)
|
||||
.backgroundColor(this.selected ? AppColor.white : this.color)
|
||||
Text(this.label)
|
||||
.fontSize(13)
|
||||
.fontColor(this.selected ? AppColor.white : AppColor.textSecondary)
|
||||
}
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.padding({ left: 16, right: 16, top: 10, bottom: 10 })
|
||||
.backgroundColor(this.selected ? this.color : AppColor.white)
|
||||
.border({ width: 1, color: this.selected ? this.color : AppColor.pillBorder })
|
||||
.borderRadius(AppRadius.pill)
|
||||
.onClick(() => {
|
||||
this.onTap();
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,66 @@
|
||||
/**
|
||||
* 主题 token:颜色 / 间距 / 字号。
|
||||
* 主题 token:颜色 / 间距 / 字号 / 圆角。
|
||||
* 全 App 统一从这里取值,避免散落魔法值。
|
||||
* 品牌色板对齐「明缘云道场」小程序(深棕 / 金 / 米白)。
|
||||
*/
|
||||
export class AppColor {
|
||||
static readonly primary: string = '#0A59F7';
|
||||
static readonly primaryPressed: string = '#094CCB';
|
||||
static readonly textPrimary: string = '#182431';
|
||||
static readonly textSecondary: string = '#66182431';
|
||||
static readonly background: string = '#F1F3F5';
|
||||
// 品牌主色(金)
|
||||
static readonly primary: string = '#C67B30';
|
||||
static readonly primaryPressed: string = '#A8611F';
|
||||
|
||||
// 文本阶梯
|
||||
static readonly textPrimary: string = '#2C1810';
|
||||
static readonly textSecondary: string = '#5A4A3A';
|
||||
static readonly textMuted: string = '#8B7A6A';
|
||||
static readonly textMutedLight: string = '#A99A89';
|
||||
static readonly textFaint: string = '#B6A996';
|
||||
|
||||
// 背景
|
||||
static readonly background: string = '#FDFBF7';
|
||||
static readonly creamDark: string = '#F6F2EC';
|
||||
static readonly white: string = '#FFFFFF';
|
||||
static readonly divider: string = '#0D182431';
|
||||
static readonly danger: string = '#E84026';
|
||||
static readonly divider: string = '#F2EDE7';
|
||||
static readonly dividerDark: string = '#E4D9CB';
|
||||
|
||||
// 品牌深棕 / 金系
|
||||
static readonly deepBrown: string = '#2C1810';
|
||||
static readonly gold: string = '#C67B30';
|
||||
static readonly goldLight: string = '#E3B37C';
|
||||
static readonly goldMid: string = '#E4A05E';
|
||||
static readonly goldBright: string = '#EFA45D';
|
||||
static readonly goldChip: string = '#2EC67B30';
|
||||
static readonly goldChipStrong: string = '#40C67B30';
|
||||
|
||||
// Hero 渐变(135°)
|
||||
static readonly heroStart: string = '#2C1810';
|
||||
static readonly heroMid: string = '#4A2B1D';
|
||||
static readonly heroEnd: string = '#6B3F24';
|
||||
|
||||
// 打卡状态五色
|
||||
static readonly statusNormal: string = '#2E7D32';
|
||||
static readonly statusLate: string = '#C67B30';
|
||||
static readonly statusLeave: string = '#3A7BD5';
|
||||
static readonly statusEarlyLeave: string = '#8B7A6A';
|
||||
static readonly statusAbsent: string = '#C0392B';
|
||||
|
||||
// 语义
|
||||
static readonly danger: string = '#C0392B';
|
||||
static readonly errorBg: string = '#FDEAEA';
|
||||
static readonly successBg: string = '#E8F4EB';
|
||||
static readonly warningBg: string = '#FFF3E6';
|
||||
static readonly warningBorder: string = '#F7D9B9';
|
||||
|
||||
// 组件
|
||||
static readonly neutralChip: string = '#F6EFE6';
|
||||
static readonly segmentedTrack: string = '#EFE7DC';
|
||||
static readonly pillBorder: string = '#D8C7B4';
|
||||
static readonly disabled: string = '#D8C7B4';
|
||||
static readonly skeleton: string = '#EFE6D9';
|
||||
|
||||
// 底部导航
|
||||
static readonly tabBg: string = '#FFFFFF';
|
||||
static readonly tabInactive: string = '#5A4A3A';
|
||||
static readonly tabActive: string = '#C67B30';
|
||||
}
|
||||
|
||||
export class AppSpacing {
|
||||
@@ -26,4 +76,13 @@ export class AppFontSize {
|
||||
static readonly body: number = 14;
|
||||
static readonly title: number = 16;
|
||||
static readonly headline: number = 20;
|
||||
static readonly display: number = 28;
|
||||
}
|
||||
|
||||
export class AppRadius {
|
||||
static readonly sm: number = 8;
|
||||
static readonly md: number = 12;
|
||||
static readonly lg: number = 16;
|
||||
static readonly xl: number = 20;
|
||||
static readonly pill: number = 999;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user