v0.7.0 接上真实服务器:登录 / 设备会话 / 全量同步与各模块真实数据
从「演示数据壳子」走到「真的能连家里的服务器用」。
登录(用户 2026-10-17「完整的登录功能」)
· 首次启动先填服务器地址 → /api/v1/ping 自报门户(认得出"这地址上真是家书")
· 手机号或用户名 + 密码 → 拿令牌;失败提示统一,不泄露账号是否存在
· FbDeviceStore:首次启动生成并持久化设备 UUID(三方应用拿不到 UDID),
设备名取机型(marketName),并上报 platform / app_version
设备会话(v57,用户 2026-10-17)
· 一把令牌 = 一台设备的一条会话;每次启动打一次 /me(有网就验)
· 两种锁分开:need_login(401/被踢 → 清令牌)与 offline_expired
(离线超 24h → 只锁界面、不清数据、联网自动恢复)——
混成一种就会违背用户要的"一直联网就不用每天输密码"
· 运行期任何请求 401 → 网络层全局回调立刻锁定跳登录页(FbHttp 的 authProbe
让登录/校验类请求跳过,避免"两个地方同时跳登录页")
· 退出登录:先清本地令牌(主防线),再尽力通知服务端
· 设置页显示版本号与「这台设备」
数据层
· FbDb / FbSchema / FbParse / 各 Repo(日程 / 名册 / 观影 / 模块)+ FbSyncAll
· 首页改为服务端 /home(农历/节气/纪念日/生日/天气/统计/往年的今天)
· 日程页用 FbDayEventPanel,与网页端显示方式/红线/并排三处一致
验证:ArkTS 静态守卫 231/0;hvigor 编译 BUILD SUCCESSFUL(entry-default-signed.hap)。
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
/**
|
||||
* 家书 FamilyBook · 日程详情浮层(鸿蒙端)
|
||||
*
|
||||
* 用户 2026-10-15:「……包括下面日程的显示啊什么之类的,就按照 SyncCalendar 那个模式去做。」
|
||||
* SyncCalendar 的详情是**半屏 bindSheet**,内容为:
|
||||
* 标题(带日历本色点 + 系统/只读徽标 + 关闭)→ 时间 → 日历本/重复 →
|
||||
* 地点(可点导航)→ 备注 → 「编辑日程」。
|
||||
*
|
||||
* 🔴 这里用**页面内 Stack 覆盖层**而不是 `bindSheet`(与家书其他浮层一致,
|
||||
* 理由见 `FbMovieDetailSheet` 的文件头);
|
||||
* **功能与信息结构**照 SyncCalendar 对齐。
|
||||
*
|
||||
* ⚠️ 详情数据是**异步拉的**(`/events/<id>` 才给 note/rrule/reminder/ends_at)
|
||||
* ⇒ 三种态都要画:加载中 / 成功 / 失败。别只画成功态。
|
||||
*/
|
||||
|
||||
import { FbColors, FbMetric } from '../common/Theme';
|
||||
import { FbCalEventFull } from '../data/FbCalRepo';
|
||||
import { FbCalBook } from '../model/Types';
|
||||
import { FbDot } from './Widgets';
|
||||
|
||||
const FB_SHEET_BOTTOM_GAP: number = 16;
|
||||
|
||||
@ComponentV2
|
||||
export struct FbEventSheet {
|
||||
/** 日程详情(null = 还在加载) */
|
||||
@Param event: FbCalEventFull | null = null;
|
||||
/** 加载中 */
|
||||
@Param loading: boolean = false;
|
||||
/** 加载失败说明(空 = 没失败) */
|
||||
@Param error: string = '';
|
||||
/** 这条日程属于哪本日历(列表里那份已知,用来显示本色/名字) */
|
||||
@Param bookName: string = '';
|
||||
@Param bookColor: string = '#9AA0A6';
|
||||
/** 能不能编辑(不能写的日历本不给"编辑"按钮) */
|
||||
@Param canEdit: boolean = false;
|
||||
@Event onClose: () => void = (): void => {
|
||||
};
|
||||
@Event onEdit: () => void = (): void => {
|
||||
};
|
||||
|
||||
/** 时间显示:`10月15日 周三 09:00 - 10:30` / `全天 10月15日 周三` */
|
||||
private timeText(): string {
|
||||
const e: FbCalEventFull | null = this.event;
|
||||
if (e === null) {
|
||||
return '';
|
||||
}
|
||||
const d: string = this.cnDate(e.starts_at);
|
||||
if (e.all_day) {
|
||||
return '全天 ' + d;
|
||||
}
|
||||
const t1: string = this.hhmm(e.starts_at);
|
||||
if (e.ends_at === '') {
|
||||
return d + ' ' + t1;
|
||||
}
|
||||
// 同一天就只写一次日期
|
||||
const sameDay: boolean = e.ends_at.length >= 10 && e.starts_at.length >= 10
|
||||
&& e.ends_at.substring(0, 10) === e.starts_at.substring(0, 10);
|
||||
const t2: string = this.hhmm(e.ends_at);
|
||||
return sameDay ? (d + ' ' + t1 + ' - ' + t2) : (d + ' ' + t1 + ' → ' + this.cnDate(e.ends_at) + ' ' + t2);
|
||||
}
|
||||
|
||||
/** `YYYY-MM-DD HH:MM:SS` → `10月15日 周三` */
|
||||
private cnDate(dt: string): string {
|
||||
if (dt.length < 10) {
|
||||
return '';
|
||||
}
|
||||
const y: number = parseInt(dt.substring(0, 4), 10);
|
||||
const m: number = parseInt(dt.substring(5, 7), 10);
|
||||
const d: number = parseInt(dt.substring(8, 10), 10);
|
||||
const w: string[] = ['日', '一', '二', '三', '四', '五', '六'];
|
||||
const day: number = new Date(y, m - 1, d).getDay();
|
||||
return m.toString() + '月' + d.toString() + '日 周' + w[day];
|
||||
}
|
||||
|
||||
/** `YYYY-MM-DD HH:MM:SS` → `HH:mm` */
|
||||
private hhmm(dt: string): string {
|
||||
return dt.length >= 16 ? dt.substring(11, 16) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 重复规则 → 中文。
|
||||
*
|
||||
* ⚠️ 只翻译**端上能生成的那几种**(见 `FbEventEditSheet.REPEAT_OPTS`);
|
||||
* 认不出的(网页端配的"每两周"等)原样显示 RRULE 串 ——
|
||||
* "看不懂"也好过"显示成不重复"(那是错的)。
|
||||
*/
|
||||
private repeatText(): string {
|
||||
const e: FbCalEventFull | null = this.event;
|
||||
if (e === null || e.rrule === '') {
|
||||
return '';
|
||||
}
|
||||
const r: string = e.rrule;
|
||||
if (r.indexOf('FREQ=DAILY') === 0) { return '每天'; }
|
||||
if (r.indexOf('FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR') === 0) { return '每个工作日'; }
|
||||
if (r.indexOf('FREQ=WEEKLY') === 0) { return '每周'; }
|
||||
if (r.indexOf('FREQ=MONTHLY') === 0) { return '每月'; }
|
||||
if (r.indexOf('FREQ=YEARLY') === 0) { return '每年'; }
|
||||
return r;
|
||||
}
|
||||
|
||||
/** 提醒 → 中文(多个用顿号连) */
|
||||
private remindText(): string {
|
||||
const e: FbCalEventFull | null = this.event;
|
||||
if (e === null || e.reminder_minutes === '') {
|
||||
return '';
|
||||
}
|
||||
const out: string[] = [];
|
||||
for (const x of e.reminder_minutes.split(',')) {
|
||||
const n: number = parseInt(x.trim(), 10);
|
||||
if (isNaN(n)) {
|
||||
continue;
|
||||
}
|
||||
if (n === 0) { continue; }
|
||||
if (n < 60) { out.push('提前 ' + n.toString() + ' 分钟'); }
|
||||
else if (n === 60) { out.push('提前 1 小时'); }
|
||||
else if (n === 1440) { out.push('提前 1 天'); }
|
||||
else if (n % 1440 === 0) { out.push('提前 ' + (n / 1440).toString() + ' 天'); }
|
||||
else if (n % 60 === 0) { out.push('提前 ' + (n / 60).toString() + ' 小时'); }
|
||||
else { out.push('提前 ' + n.toString() + ' 分钟'); }
|
||||
}
|
||||
return out.join('、');
|
||||
}
|
||||
|
||||
@Builder
|
||||
row(label: string, value: string) {
|
||||
Row({ space: 10 }) {
|
||||
Text(label)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
.width(56)
|
||||
Text(value)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.ink)
|
||||
.layoutWeight(1)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ top: 8, bottom: 8 })
|
||||
.alignItems(VerticalAlign.Top)
|
||||
}
|
||||
|
||||
build() {
|
||||
// FB_WRAP_CENTER:把卡片**垂直居中**(用户 2026-10-16 要求:浮在屏幕中间)。
|
||||
// ⚠️ 居中放在**组件内部**做,而不是改宿主的 Stack 对齐 ——
|
||||
// 宿主 Stack 里还有别的子元素(日程页的悬浮 +),改对齐会连带挪走它们。
|
||||
// ⚠️ 外层铺满 + Center;内层仍是那张 92% 宽的圆角卡。
|
||||
Column() {
|
||||
Column({ space: 0 }) {
|
||||
// ---- 标题区 ----
|
||||
Row({ space: 10 }) {
|
||||
FbDot({ dia: 12, fillC: this.bookColor })
|
||||
Text(this.event !== null && this.event.title !== ''
|
||||
? this.event.title : '(无标题)')
|
||||
.fontSize(FbMetric.fontTitle)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
.maxLines(2)
|
||||
.layoutWeight(1)
|
||||
if (this.event !== null && this.event.all_day) {
|
||||
Text('全天')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.muted)
|
||||
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
|
||||
.backgroundColor(FbColors.inputBg)
|
||||
.borderRadius(FbMetric.radiusPill)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Top)
|
||||
|
||||
Divider()
|
||||
.color(FbColors.line)
|
||||
.margin({ top: 12, bottom: 4 })
|
||||
|
||||
if (this.loading) {
|
||||
Row({ space: 8 }) {
|
||||
LoadingProgress().width(18).height(18).color(FbColors.faint)
|
||||
Text('正在读取…')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ top: 20, bottom: 20 })
|
||||
.justifyContent(FlexAlign.Center)
|
||||
} else if (this.event === null) {
|
||||
Text(this.error !== '' ? ('读不到这条日程:' + this.error) : '读不到这条日程')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.faint)
|
||||
.width('100%')
|
||||
.padding({ top: 20, bottom: 20 })
|
||||
} else {
|
||||
Scroll() {
|
||||
Column({ space: 0 }) {
|
||||
this.row('时间', this.timeText())
|
||||
if (this.bookName !== '') {
|
||||
this.row('日历本', this.bookName)
|
||||
}
|
||||
// ⚠️ build() 里**不能声明变量**(只有 UI 组件语法)⇒
|
||||
// 条件直接调方法(方法返回空串就不显示)
|
||||
if (this.repeatText() !== '') {
|
||||
this.row('重复', this.repeatText())
|
||||
}
|
||||
if (this.remindText() !== '') {
|
||||
this.row('提醒', this.remindText())
|
||||
}
|
||||
if (this.event.location !== '') {
|
||||
this.row('地点', this.event.location)
|
||||
}
|
||||
if (this.event.note !== '') {
|
||||
this.row('备注', this.event.note)
|
||||
}
|
||||
Row().height(8)
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.width('100%')
|
||||
.scrollBar(BarState.Off)
|
||||
.align(Alignment.TopStart)
|
||||
}
|
||||
|
||||
// ---- 底部按钮 ----
|
||||
Row({ space: 10 }) {
|
||||
Text('关闭')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.muted)
|
||||
.textAlign(TextAlign.Center)
|
||||
.layoutWeight(1)
|
||||
.height(42)
|
||||
.backgroundColor(FbColors.inputBg)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.onClick((): void => {
|
||||
this.onClose();
|
||||
})
|
||||
if (this.canEdit && this.event !== null) {
|
||||
Text('编辑日程')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(Color.White)
|
||||
.textAlign(TextAlign.Center)
|
||||
.layoutWeight(1)
|
||||
.height(42)
|
||||
.backgroundColor('#4A7C82')
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.onClick((): void => {
|
||||
this.onEdit();
|
||||
})
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.margin({ top: 14 })
|
||||
}
|
||||
// 🔴 用户 2026-10-16:「它应该弹出一个**圆角的、浮在上面的卡片**……
|
||||
// **不要让它左右充满整个屏幕,下面也不要挨着屏幕**,
|
||||
// 而是放在**屏幕中间**,采用**圆角浮窗**式的形式。」
|
||||
// ⇒ 不再贴底满宽:左右留边、四角圆角、垂直居中(由宿主的 Stack 对齐)。
|
||||
.width('92%')
|
||||
.constraintSize({ maxHeight: '72%' })
|
||||
.padding({ left: 18, right: 18, top: 18, bottom: 20 + FB_SHEET_BOTTOM_GAP })
|
||||
.backgroundColor(FbColors.surface)
|
||||
.borderRadius(FbMetric.radius)
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.hitTestBehavior(HitTestMode.Transparent)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user