feat(uikit): 引入明缘主题色板与通用组件

This commit is contained in:
AndyChen1024
2026-09-11 20:47:22 +08:00
parent 35a70f5091
commit 8321991b45
10 changed files with 378 additions and 15 deletions

View File

@@ -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 { AppButton, ButtonVariant } from './components/AppButton';
export { LoadingView } from './components/LoadingView'; export { LoadingView } from './components/LoadingView';
export { EmptyView } from './components/EmptyView'; 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';

View File

@@ -1,8 +1,10 @@
import { AppColor } from '../theme/Theme'; import { AppColor, AppFontSize, AppRadius } from '../theme/Theme';
export enum ButtonVariant { export enum ButtonVariant {
Primary = 'primary', Primary = 'primary',
Secondary = 'secondary' Secondary = 'secondary',
Danger = 'danger',
Outline = 'outline'
} }
/** /**
@@ -13,16 +15,19 @@ export struct AppButton {
@Param text: string = ''; @Param text: string = '';
@Param variant: ButtonVariant = ButtonVariant.Primary; @Param variant: ButtonVariant = ButtonVariant.Primary;
@Param disabled: boolean = false; @Param disabled: boolean = false;
@Param radius: number = AppRadius.md;
@Event onTap: () => void; @Event onTap: () => void;
build() { build() {
Button(this.text) Button(this.text)
.width('100%') .width('100%')
.height(48) .height(48)
.fontSize(16) .fontSize(AppFontSize.title)
.fontColor(this.variant === ButtonVariant.Primary ? AppColor.white : AppColor.primary) .fontWeight(FontWeight.Medium)
.backgroundColor(this.variant === ButtonVariant.Primary ? AppColor.primary : AppColor.white) .fontColor(this.fontColor())
.borderRadius(8) .backgroundColor(this.bgColor())
.border({ width: this.variant === ButtonVariant.Outline ? 1 : 0, color: AppColor.gold })
.borderRadius(this.radius)
.enabled(!this.disabled) .enabled(!this.disabled)
.onClick(() => { .onClick(() => {
if (!this.disabled) { 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;
}
} }

View 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)
}
}

View 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 })
}
}

View 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%')
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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();
})
}
}

View File

@@ -1,16 +1,66 @@
/** /**
* 主题 token:颜色 / 间距 / 字号。 * 主题 token:颜色 / 间距 / 字号 / 圆角。
* 全 App 统一从这里取值,避免散落魔法值。 * 全 App 统一从这里取值,避免散落魔法值。
* 品牌色板对齐「明缘云道场」小程序(深棕 / 金 / 米白)。
*/ */
export class AppColor { export class AppColor {
static readonly primary: string = '#0A59F7'; // 品牌主色(金)
static readonly primaryPressed: string = '#094CCB'; static readonly primary: string = '#C67B30';
static readonly textPrimary: string = '#182431'; static readonly primaryPressed: string = '#A8611F';
static readonly textSecondary: string = '#66182431';
static readonly background: string = '#F1F3F5'; // 文本阶梯
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 white: string = '#FFFFFF';
static readonly divider: string = '#0D182431'; static readonly divider: string = '#F2EDE7';
static readonly danger: string = '#E84026'; 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 { export class AppSpacing {
@@ -26,4 +76,13 @@ export class AppFontSize {
static readonly body: number = 14; static readonly body: number = 14;
static readonly title: number = 16; static readonly title: number = 16;
static readonly headline: number = 20; 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;
} }