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:
@@ -13,8 +13,21 @@
|
||||
import { FbColors, FbMetric, fbPalette } from '../../common/Theme';
|
||||
import { FbAppState, fbAppState } from '../../common/Store';
|
||||
import { FbModuleDef, FB_MODULES, FB_PRESET_COLORS } from '../../model/Modules';
|
||||
import { FbAvatar, FbDot, FbTag } from '../../components/Widgets';
|
||||
import { FbAvatar, FbDot, FbPrimaryButton, FbTag } from '../../components/Widgets';
|
||||
import { FB_DOCK_RESERVE } from '../../components/FloatDock';
|
||||
import { FB_TOKEN_STORE } from '../../data/FbTokenStore';
|
||||
import { FB_SERVER_STORE } from '../../data/FbServerStore';
|
||||
import { FB_SYNC_ALL_PROGRESS, FbSyncAllResult, syncAll } from '../../data/FbSyncAll';
|
||||
import { common } from '@kit.AbilityKit';
|
||||
import { fbOnScroll, fbOnTouch } from '../../common/FbScroll';
|
||||
import { hilog } from '@kit.PerformanceAnalysisKit';
|
||||
import { BusinessError } from '@kit.BasicServicesKit';
|
||||
import { fbApi } from '../../data/FbHttp';
|
||||
import { FB_SESSION } from '../../data/FbSession';
|
||||
import { FB_DEVICE_STORE, FB_APP_VERSION } from '../../data/FbDeviceStore';
|
||||
|
||||
const DOMAIN: number = 0x0000;
|
||||
const TAG: string = 'FbSettings';
|
||||
|
||||
@ComponentV2
|
||||
export struct FbSettingsPage {
|
||||
@@ -31,6 +44,163 @@ export struct FbSettingsPage {
|
||||
'resources', 'tasks', 'days', 'ledger', 'books', 'notes'];
|
||||
/** 当前展开的配色分组('' = 都没展开) */
|
||||
@Local colorOpenKey: string = '';
|
||||
/** 令牌输入框的当前内容(「手动粘贴令牌」这个高级入口用) */
|
||||
@Local tokenInput: string = '';
|
||||
/** 账号区的状态说明(登录 / 同步 / 登出的结果) */
|
||||
@Local tokenNote: string = '';
|
||||
/** 「手动粘贴令牌」高级入口是否展开(v55:已不是主路径) */
|
||||
@Local advancedOpen: boolean = false;
|
||||
/** 设置页的主题色(与模块「个人设置」一致,取砖红) */
|
||||
private accent: string = '#A84A28';
|
||||
|
||||
/**
|
||||
* 页面出现时:不需要回填任何东西。
|
||||
*
|
||||
* ⚠️ v55 之前这里会把"已配置令牌"写进输入框 —— 现在登录是主路径,
|
||||
* 令牌输入框已经收进「高级」,不再需要在进页面时提示一次。
|
||||
*/
|
||||
aboutToAppear(): void {
|
||||
this.tokenNote = '';
|
||||
}
|
||||
|
||||
|
||||
/** 账号区状态文案 */
|
||||
private tokenStateText(): string {
|
||||
if (!FB_TOKEN_STORE.has()) {
|
||||
return '未登录(只显示本地已有的数据)';
|
||||
}
|
||||
// ⚠️ 显示的是**会话里的姓名**,不是令牌 —— 令牌绝不出现在界面上
|
||||
const who: string = this.app.session.me.name;
|
||||
const role: string = this.app.session.me.role === 'admin' ? '管理员'
|
||||
: (this.app.session.me.role === 'guest' ? '只读访客' : '家庭成员');
|
||||
return '已登录:' + who + ' · ' + role;
|
||||
}
|
||||
|
||||
/** 账号区状态颜色:已登录用绿、未登录用灰 */
|
||||
private tokenStateColor(): Resource {
|
||||
return FB_TOKEN_STORE.has() ? $r('app.color.green') : FbColors.muted;
|
||||
}
|
||||
|
||||
/** 当前服务器(去掉协议省一行宽度;没配就明说) */
|
||||
private serverHost(): string {
|
||||
const base: string = FB_SERVER_STORE.get();
|
||||
if (base === '') {
|
||||
return '还没设置';
|
||||
}
|
||||
return base.replace(/^https?:\/\//i, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动同步一次。
|
||||
*
|
||||
* ⚠️ 登录后会自动全量同步,但用户可能"隔了几天再打开、想知道有没有新数据"
|
||||
* —— 给他一个明确的、随时可按的入口。
|
||||
*/
|
||||
private async syncNow(): Promise<void> {
|
||||
if (!FB_TOKEN_STORE.has()) {
|
||||
this.tokenNote = '还没登录,无法同步';
|
||||
return;
|
||||
}
|
||||
this.tokenNote = '正在同步…';
|
||||
// 实时进度(定时器把当前步骤刷到界面上)。
|
||||
// ⚠️ 本机 hilog 读不出内容 ⇒ 不给实时进度,用户会以为卡死。
|
||||
const timer: number = setInterval((): void => {
|
||||
this.tokenNote = '同步中…(' + FB_SYNC_ALL_PROGRESS + ')';
|
||||
}, 700);
|
||||
const res: FbSyncAllResult = await syncAll();
|
||||
clearInterval(timer);
|
||||
this.tokenNote = res.ok ? ('✓ ' + res.note) : ('部分未完成:' + res.note);
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录:清令牌 + 回登录页(**地址保留**)。
|
||||
*
|
||||
* 🔴 为什么不顺手清本地数据:本地优先架构下"没登录"不等于"没数据"。
|
||||
* 用户可能只是在别人手机上登出、或暂时断网 —— 把已同步的数据一起删掉,
|
||||
* 等于让他下次进来面对一片空白(还得重新全量同步)。
|
||||
*/
|
||||
private async logout(): Promise<void> {
|
||||
const ctx: common.Context | undefined = this.getUIContext().getHostContext();
|
||||
if (ctx === undefined) {
|
||||
this.tokenNote = '拿不到应用上下文,无法退出';
|
||||
return;
|
||||
}
|
||||
/* 🔴 v57(用户 2026-10-17):「先删本地令牌(这是主防线,捡到手机的人拿不到)
|
||||
→ 再尽力通知服务端」。
|
||||
⚠️ 顺序不能反:离线时通知发不出去,如果等它成功再清本地,
|
||||
用户会看到"点了退出却没退"。本地那一刀是唯一可靠的。
|
||||
⚠️ 服务端那一条**失败也不报错**:它只是让"这台设备"从设备列表里消失得
|
||||
更及时,删不掉就等它 24h 后自然过期,或用户在网页端手动踢。 */
|
||||
const token: string = FB_TOKEN_STORE.get();
|
||||
await FB_TOKEN_STORE.clear(ctx);
|
||||
await FB_SESSION.clear(ctx);
|
||||
if (token !== '') {
|
||||
fbApi('/api/v1/logout', { method: 'POST', token: token, authProbe: true })
|
||||
.catch((err: BusinessError): void => {
|
||||
hilog.warn(DOMAIN, TAG, '通知服务端退出失败(已本地退出):%{public}s',
|
||||
JSON.stringify(err));
|
||||
});
|
||||
}
|
||||
this.tokenNote = '';
|
||||
// 回登录页,从"输账号密码"开始(地址还留着)
|
||||
this.app.lockReason = '你已退出登录';
|
||||
this.app.loginStartStep = 2;
|
||||
this.app.needLogin = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更换服务器:回登录页第一步重填地址。
|
||||
*
|
||||
* 🔴 必须**同时清掉令牌**:令牌是那台服务器签发的,换了机器一定无效 ——
|
||||
* 留着只会让用户在第二步莫名其妙地 401("密码明明是对的")。
|
||||
* ⚠️ 地址本身**不清**:登录页第一步会预填它,用户改起来更方便。
|
||||
*/
|
||||
private async changeServer(): Promise<void> {
|
||||
const ctx: common.Context | undefined = this.getUIContext().getHostContext();
|
||||
if (ctx !== undefined) {
|
||||
await FB_TOKEN_STORE.clear(ctx);
|
||||
// ⚠️ 会话校验记录也要清:换了服务器,"上次校验成功"是旧服务器的事,
|
||||
// 留着会让新服务器上的离线判定带着旧时间(v57)
|
||||
await FB_SESSION.clear(ctx);
|
||||
}
|
||||
this.tokenNote = '';
|
||||
this.app.lockReason = '换了服务器,需要重新登录';
|
||||
this.app.loginStartStep = 1;
|
||||
this.app.needLogin = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存手动粘贴的令牌并立刻同步一次(「高级」里的路)。
|
||||
*
|
||||
* ⚠️ 为什么还留着它:网页版「个人设置 → API 令牌」签发的令牌,
|
||||
* 在"不方便输密码"或"只给某台设备一把只读令牌"时仍然有用。
|
||||
* 但它**不再是主路径** —— 正常用户走登录。
|
||||
*/
|
||||
private async saveTokenAndSync(): Promise<void> {
|
||||
const t: string = this.tokenInput.trim();
|
||||
if (t === '') {
|
||||
this.tokenNote = '请先粘贴令牌';
|
||||
return;
|
||||
}
|
||||
// 🔴 没地址时令牌一定没用:请求拼出来是个相对地址,报的错还跟"令牌"无关
|
||||
if (FB_SERVER_STORE.get() === '') {
|
||||
this.tokenNote = '请先设置服务器地址';
|
||||
return;
|
||||
}
|
||||
const ctx: common.Context | undefined = this.getUIContext().getHostContext();
|
||||
if (ctx === undefined) {
|
||||
this.tokenNote = '拿不到应用上下文,无法保存';
|
||||
return;
|
||||
}
|
||||
this.tokenNote = '正在保存…';
|
||||
const ok: boolean = await FB_TOKEN_STORE.set(ctx, t);
|
||||
if (!ok) {
|
||||
this.tokenNote = '令牌保存失败';
|
||||
return;
|
||||
}
|
||||
this.tokenInput = '';
|
||||
await this.syncNow();
|
||||
}
|
||||
|
||||
build() {
|
||||
Scroll() {
|
||||
@@ -75,6 +245,124 @@ export struct FbSettingsPage {
|
||||
.borderRadius(FbMetric.radius)
|
||||
.margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12 })
|
||||
|
||||
// ==================== 账号与服务器(登录 / 同步 / 登出)====================
|
||||
// 🔴 v55(用户 2026-10-17「完整的登录功能」)——
|
||||
// 这里从"手工粘贴令牌"变成了**登录态管理**:
|
||||
// · 登录走登录页(地址 + 账号密码),这里只显示"当前是谁、连的哪台";
|
||||
// · 「同步全部数据」保留(用户想立刻拉新数据时按一下);
|
||||
// · 「退出登录 / 更换服务器」都回登录页(本应用没有路由栈,
|
||||
// 靠 `app.needLogin` 这个状态位回去,见 Index)。
|
||||
// ⚠️ 手工粘贴令牌降级成「高级」:它是"给某台设备一把只读令牌"这类
|
||||
// 特殊场景的备用路,不该出现在普通人的视线里。
|
||||
Column({ space: 0 }) {
|
||||
this.rowTitle('账号与服务器')
|
||||
|
||||
// 登录状态(谁、什么角色)
|
||||
Text(this.tokenStateText())
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(this.tokenStateColor())
|
||||
.width('100%')
|
||||
.maxLines(2)
|
||||
.padding({ top: 2, bottom: 8 })
|
||||
|
||||
// 当前服务器(可点「更换」)
|
||||
Row({ space: 8 }) {
|
||||
Text('服务器')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.ink)
|
||||
Text(this.serverHost())
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.faint)
|
||||
.layoutWeight(1)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
Text('更换')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(this.accent)
|
||||
.padding({ left: 8, top: 4, bottom: 4 })
|
||||
.onClick((): void => { this.changeServer(); })
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ top: 2, bottom: 10 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
// 操作:同步 / 退出(未登录时只有同步,且点了会提示)
|
||||
Row({ space: 8 }) {
|
||||
Text('同步全部数据')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(Color.White)
|
||||
.textAlign(TextAlign.Center)
|
||||
.height(36)
|
||||
.layoutWeight(1)
|
||||
.backgroundColor(this.accent)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.onClick((): void => { this.syncNow(); })
|
||||
if (FB_TOKEN_STORE.has()) {
|
||||
Text('退出登录')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
// ⚠️ FbColors 里没有 danger(只有 ink/muted/faint/line…),
|
||||
// 这里用字面色值。别写成 FbColors.danger —— 那会编译不过。
|
||||
.fontColor('#A32D2D')
|
||||
.textAlign(TextAlign.Center)
|
||||
.height(36)
|
||||
.width(88)
|
||||
.border({ width: 1, color: FbColors.line })
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.onClick((): void => { this.logout(); })
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
// ---- 高级:手动粘贴 API 令牌 ----
|
||||
Text(this.advancedOpen ? '收起「手动粘贴令牌」' : '高级:手动粘贴 API 令牌')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
.width('100%')
|
||||
.padding({ top: 12, bottom: 6 })
|
||||
.onClick((): void => { this.advancedOpen = !this.advancedOpen; })
|
||||
|
||||
if (this.advancedOpen) {
|
||||
TextInput({ placeholder: '粘贴 API 令牌(fb_ 开头)', text: this.tokenInput })
|
||||
.type(InputType.Normal)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.maxLength(128)
|
||||
.backgroundColor(FbColors.inputBg)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.padding({ left: 10, right: 10 })
|
||||
.onChange((v: string): void => {
|
||||
this.tokenInput = v;
|
||||
})
|
||||
FbPrimaryButton({
|
||||
label: '保存并同步',
|
||||
bg: this.accent,
|
||||
icon: $r('app.media.fb_ic_check'),
|
||||
onTap: (): void => { this.saveTokenAndSync(); }
|
||||
})
|
||||
.margin({ top: 8 })
|
||||
}
|
||||
|
||||
// 状态(登录 / 同步 / 登出的结果都落在这里)
|
||||
if (this.tokenNote !== '') {
|
||||
Text(this.tokenNote)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
.maxLines(3)
|
||||
.margin({ top: 8 })
|
||||
}
|
||||
|
||||
Text('退出登录不会删除已经同步到本机的数据,离线也能看;换个服务器则要重新登录。')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
.width('100%')
|
||||
.maxLines(3)
|
||||
.margin({ top: 8 })
|
||||
}
|
||||
.padding(FbMetric.cardPad)
|
||||
.backgroundColor(FbColors.surface)
|
||||
.borderRadius(FbMetric.radius)
|
||||
.margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12 })
|
||||
|
||||
// ==================== 通知 ====================
|
||||
Column({ space: 0 }) {
|
||||
this.rowTitle('通知')
|
||||
@@ -144,9 +432,13 @@ export struct FbSettingsPage {
|
||||
// ==================== 关于 ====================
|
||||
Column({ space: 0 }) {
|
||||
this.rowTitle('关于')
|
||||
this.plainRow('版本', '0.1.0(界面框架)')
|
||||
this.plainRow('数据来源', '待接入 · 当前为演示数据')
|
||||
this.plainRow('服务器', '未配置')
|
||||
this.plainRow('版本', FB_APP_VERSION)
|
||||
this.plainRow('服务器', this.serverHost())
|
||||
this.plainRow('账号', FB_TOKEN_STORE.has() ? this.app.session.me.name : '未登录')
|
||||
/* v57:这台设备在服务端叫什么。
|
||||
用户 2026-10-17:「只有 uuid 用户无法确定是哪台设备。」
|
||||
⇒ 网页端「设备管理」里显示的就是这个名字,用户要能对上号。 */
|
||||
this.plainRow('这台设备', FB_DEVICE_STORE.getDeviceName())
|
||||
}
|
||||
.padding(FbMetric.cardPad)
|
||||
.backgroundColor(FbColors.surface)
|
||||
@@ -162,6 +454,12 @@ export struct FbSettingsPage {
|
||||
.backgroundColor(FbColors.paper)
|
||||
.scrollBar(BarState.Off)
|
||||
.edgeEffect(EdgeEffect.Spring)
|
||||
// 🔴 滚动方向上报(驱动底部悬浮栏随滚收起 / 唤回)—— 见 common/FbScroll.ets
|
||||
.onDidScroll(fbOnScroll)
|
||||
|
||||
// 🔴 触摸方向兜底(内容不足一屏的页面滚不动,只能靠手指位移判断)
|
||||
// 与上面的 `onDidScroll` 互补、互不冲突 —— 见 common/FbScroll.ets
|
||||
.onTouch(fbOnTouch)
|
||||
.align(Alignment.TopStart)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user