diff --git a/uikit/src/main/ets/Index.ets b/uikit/src/main/ets/Index.ets index 881651c..de7a27b 100644 --- a/uikit/src/main/ets/Index.ets +++ b/uikit/src/main/ets/Index.ets @@ -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'; diff --git a/uikit/src/main/ets/components/AppButton.ets b/uikit/src/main/ets/components/AppButton.ets index 78ad60b..4e1ec05 100644 --- a/uikit/src/main/ets/components/AppButton.ets +++ b/uikit/src/main/ets/components/AppButton.ets @@ -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; + } } diff --git a/uikit/src/main/ets/components/Avatar.ets b/uikit/src/main/ets/components/Avatar.ets new file mode 100644 index 0000000..f48c19c --- /dev/null +++ b/uikit/src/main/ets/components/Avatar.ets @@ -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) + } +} diff --git a/uikit/src/main/ets/components/LabeledRow.ets b/uikit/src/main/ets/components/LabeledRow.ets new file mode 100644 index 0000000..34a329b --- /dev/null +++ b/uikit/src/main/ets/components/LabeledRow.ets @@ -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 }) + } +} diff --git a/uikit/src/main/ets/components/ProgressBar.ets b/uikit/src/main/ets/components/ProgressBar.ets new file mode 100644 index 0000000..06b4d9a --- /dev/null +++ b/uikit/src/main/ets/components/ProgressBar.ets @@ -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%') + } +} diff --git a/uikit/src/main/ets/components/SegmentedControl.ets b/uikit/src/main/ets/components/SegmentedControl.ets new file mode 100644 index 0000000..562292b --- /dev/null +++ b/uikit/src/main/ets/components/SegmentedControl.ets @@ -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) + } +} diff --git a/uikit/src/main/ets/components/StateViews.ets b/uikit/src/main/ets/components/StateViews.ets new file mode 100644 index 0000000..c2b3692 --- /dev/null +++ b/uikit/src/main/ets/components/StateViews.ets @@ -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) + } +} diff --git a/uikit/src/main/ets/components/StatusBadge.ets b/uikit/src/main/ets/components/StatusBadge.ets new file mode 100644 index 0000000..f991634 --- /dev/null +++ b/uikit/src/main/ets/components/StatusBadge.ets @@ -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) + } +} diff --git a/uikit/src/main/ets/components/StatusPill.ets b/uikit/src/main/ets/components/StatusPill.ets new file mode 100644 index 0000000..9d60ab5 --- /dev/null +++ b/uikit/src/main/ets/components/StatusPill.ets @@ -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(); + }) + } +} diff --git a/uikit/src/main/ets/theme/Theme.ets b/uikit/src/main/ets/theme/Theme.ets index 4733035..2e5abe5 100644 --- a/uikit/src/main/ets/theme/Theme.ets +++ b/uikit/src/main/ets/theme/Theme.ets @@ -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; }