资源附件+共享:浮层补「附件」「共享」两个真功能 laoyang/FamilyBook#25
用户原话(2026-10-28):「鸿蒙端对齐网页端」。资源浮层原来「附件与共享 还在网页端,App 暂未开放」,现在服务端补齐 API 后: 1) FbResourceRepo 补 attach/share 写操作(上传走 fbUploadOne 字段 file; 删除走 query 传 attachment_id / person_id —— DELETE 带 JSON body 服务端读不到) 2) FbResourceSheet:附件区块(列表 + 缩略图/图标 + 「+传附件」+ 逐条删)+ 共享区块(列表 + 「共享给…」复用选人组件 + 逐条取消) · 只读共享来的不摆入口(与 canWrite 对齐) 编译 BUILD SUCCESSFUL;服务端冒烟 _i25_resource_api.py 21/0 + 反证命中。
This commit is contained in:
@@ -9,18 +9,54 @@
|
||||
* ⇒ 端上原来既没有新建、也没有详情:`FbPrimaryButton` 无 `onTap`、
|
||||
* 列表项无 `onClick`。这个浮层把两件事一次补齐。
|
||||
*
|
||||
* ⚠️⚠️ 服务端只有 title / content(详见 `data/FbResourceRepo` 文件头):
|
||||
* 归属人固定是当前登录的人、附件与共享**没有 API**。
|
||||
* ⇒ 这里**故意不摆**「选归属人」「传附件」「共享给谁」这些入口 ——
|
||||
* 摆了也存不进去,用户会以为存上了。
|
||||
* 🔴 工单 #25(2026-10-28):附件与共享原来「故意不摆假入口」(服务端没 API)。
|
||||
* 服务端补齐 `attach` / `shares` 子端点后,这里补上两个真功能:
|
||||
* · 附件:列表 + 「+ 传附件」+ 逐条删除
|
||||
* · 共享:列表 + 「共享给…」+ 逐条取消(复用选人组件)
|
||||
*/
|
||||
|
||||
import { FbColors, FbMetric, FB_MASK, fbAlpha } from '../common/Theme';
|
||||
import { FbSheetClose } from './FbSheetClose';
|
||||
import { FbSheetRing } from './FbSheetRing';
|
||||
import { FbPersonPickSheet } from './FbPersonPickSheet';
|
||||
import { FbPersonRepo, FbRosterPerson } from '../data/FbPersonRepo';
|
||||
import { FB_TOKEN_STORE } from '../data/FbTokenStore';
|
||||
import { createResource, updateResource, deleteResource } from '../data/FbResourceRepo';
|
||||
import {
|
||||
createResource, updateResource, deleteResource,
|
||||
fetchResourceAttach, delResourceAttach,
|
||||
fetchResourceShares, shareResource, unshareResource,
|
||||
} from '../data/FbResourceRepo';
|
||||
import { fbUploadOne } from '../data/FbUpload';
|
||||
import { fbImageSrc } from '../data/FbAttachQueue';
|
||||
import { FbApiResult } from '../data/FbHttp';
|
||||
import { picker } from '@kit.CoreFileKit';
|
||||
import { BusinessError } from '@kit.BasicServicesKit';
|
||||
import { hilog } from '@kit.PerformanceAnalysisKit';
|
||||
|
||||
/** 一条附件(服务端 `Attachment::list`) */
|
||||
interface FbResourceAttach {
|
||||
id: number;
|
||||
name: string;
|
||||
key: string;
|
||||
isImage: boolean;
|
||||
}
|
||||
|
||||
/** 一条共享(服务端 `Resource::shares()`) */
|
||||
interface FbResourceShareRow {
|
||||
personId: number;
|
||||
name: string;
|
||||
perm: string;
|
||||
}
|
||||
|
||||
/** 附件原始行 */
|
||||
interface RawAttach {
|
||||
id?: number; key?: string | null; name?: string | null; is_image?: number;
|
||||
}
|
||||
|
||||
/** 共享原始行 */
|
||||
interface RawShareRow {
|
||||
person_id?: number; name?: string | null; perm?: string | null;
|
||||
}
|
||||
|
||||
@ComponentV2
|
||||
export struct FbResourceSheet {
|
||||
@@ -45,10 +81,22 @@ export struct FbResourceSheet {
|
||||
/** 删除的二次确认(网页也是先点一次再确认,避免误删证件) */
|
||||
@Local confirmingDelete: boolean = false;
|
||||
@Local note: string = '';
|
||||
// ---- 附件(工单 #25)----
|
||||
@Local attachs: FbResourceAttach[] = [];
|
||||
@Local uploading: boolean = false;
|
||||
// ---- 共享(工单 #25)----
|
||||
@Local shares: FbResourceShareRow[] = [];
|
||||
@Local roster: FbRosterPerson[] = [];
|
||||
@Local picking: boolean = false;
|
||||
|
||||
aboutToAppear(): void {
|
||||
this.title = this.editingTitle;
|
||||
this.content = this.editingContent;
|
||||
/* 已有资源才拉附件与共享(新建时还没有 id) */
|
||||
if (this.editingId > 0) {
|
||||
this.reloadAttach();
|
||||
this.reloadShares();
|
||||
}
|
||||
}
|
||||
|
||||
build() {
|
||||
@@ -153,13 +201,128 @@ export struct FbResourceSheet {
|
||||
.maxLines(3)
|
||||
}
|
||||
|
||||
// 附件 / 共享:服务端没有接口,照实说明(不摆假入口)
|
||||
// ---- 附件(工单 #25)----
|
||||
if (this.editingId > 0) {
|
||||
Text('附件与共享还在网页端「资源」里管 —— App 这边暂未开放。')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
Column({ space: 6 }) {
|
||||
Row({ space: 8 }) {
|
||||
Text('附件')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
.width(34)
|
||||
Text(this.attachs.length > 0 ? this.attachs.length.toString() + ' 个' : '还没有附件')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.muted)
|
||||
}
|
||||
.width('100%')
|
||||
.maxLines(3)
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
if (this.attachs.length > 0) {
|
||||
ForEach(this.attachs, (a: FbResourceAttach) => {
|
||||
Row({ space: 8 }) {
|
||||
if (a.isImage) {
|
||||
Image(fbImageSrc(a.key, FB_TOKEN_STORE.get()))
|
||||
.width(40).height(40)
|
||||
.objectFit(ImageFit.Cover)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.backgroundColor(FbColors.inputBg)
|
||||
} else {
|
||||
Text('📎')
|
||||
.fontSize(20)
|
||||
}
|
||||
Text(a.name)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.ink)
|
||||
.layoutWeight(1)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
if (this.canWrite) {
|
||||
Text('删')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.red)
|
||||
.padding({ left: 6, right: 2, top: 2, bottom: 2 })
|
||||
.onClick((): void => {
|
||||
this.dropAttach(a);
|
||||
})
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ top: 4, bottom: 4 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
}, (a: FbResourceAttach): string => 'ratt_' + a.id.toString())
|
||||
}
|
||||
|
||||
if (this.canWrite) {
|
||||
Text(this.uploading ? '正在上传…' : '+ 传附件')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(this.accent)
|
||||
.padding({ left: 10, right: 10, top: 6, bottom: 6 })
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.backgroundColor(fbAlpha(this.accent, 0.08))
|
||||
.onClick((): void => {
|
||||
this.pickAttach();
|
||||
})
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
|
||||
// ---- 共享(工单 #25)----
|
||||
if (this.editingId > 0) {
|
||||
Column({ space: 6 }) {
|
||||
Row({ space: 8 }) {
|
||||
Text('共享')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
.width(34)
|
||||
Text(this.shares.length > 0 ? this.shares.length.toString() + ' 人' : '还没有共享给谁')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.muted)
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
if (this.shares.length > 0) {
|
||||
ForEach(this.shares, (s: FbResourceShareRow) => {
|
||||
Row({ space: 8 }) {
|
||||
Text(s.name)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.ink)
|
||||
.layoutWeight(1)
|
||||
.maxLines(1)
|
||||
Text(s.perm === 'write' ? '可改' : '只看')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
if (this.canWrite) {
|
||||
Text('取消')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.red)
|
||||
.padding({ left: 6, right: 2, top: 2, bottom: 2 })
|
||||
.onClick((): void => {
|
||||
this.dropShare(s);
|
||||
})
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ top: 4, bottom: 4 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
}, (s: FbResourceShareRow): string => 'rsh_' + s.personId.toString())
|
||||
}
|
||||
|
||||
if (this.canWrite) {
|
||||
Text('共享给…')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(this.accent)
|
||||
.padding({ left: 10, right: 10, top: 6, bottom: 6 })
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.backgroundColor(fbAlpha(this.accent, 0.08))
|
||||
.onClick((): void => {
|
||||
this.openPicker();
|
||||
})
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
@@ -240,6 +403,22 @@ export struct FbResourceSheet {
|
||||
}
|
||||
}
|
||||
.width('92%')
|
||||
|
||||
/* 选人浮层:共享给谁(工单 #25,复用名册多选组件) */
|
||||
if (this.picking) {
|
||||
FbPersonPickSheet({
|
||||
people: this.roster,
|
||||
selected: [],
|
||||
title: '共享给谁',
|
||||
accent: this.accent,
|
||||
onConfirm: (ids: number[]): void => {
|
||||
this.addShares(ids);
|
||||
},
|
||||
onClose: (): void => {
|
||||
this.picking = false;
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
@@ -251,6 +430,136 @@ export struct FbResourceSheet {
|
||||
return this.canWrite;
|
||||
}
|
||||
|
||||
// ==================== 附件(工单 #25)====================
|
||||
private async reloadAttach(): Promise<void> {
|
||||
const res: FbApiResult = await fetchResourceAttach(FB_TOKEN_STORE.get(), this.editingId);
|
||||
if (!res.ok) {
|
||||
this.note = res.error !== '' ? res.error : '附件列表没拉到。';
|
||||
return;
|
||||
}
|
||||
const out: FbResourceAttach[] = [];
|
||||
try {
|
||||
const o: Record<string, Object> = JSON.parse(res.data) as Record<string, Object>;
|
||||
const items: Object | undefined = o['items'];
|
||||
const rows: RawAttach[] = (items !== undefined && items !== null)
|
||||
? (items as RawAttach[]) : [];
|
||||
for (const a of rows) {
|
||||
out.push({
|
||||
id: a.id !== undefined ? a.id : 0,
|
||||
name: a.name !== undefined && a.name !== null ? a.name : '附件',
|
||||
key: a.key !== undefined && a.key !== null ? a.key : '',
|
||||
isImage: a.is_image !== undefined ? a.is_image !== 0 : false,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// 解析失败就当空列表
|
||||
}
|
||||
this.attachs = out;
|
||||
}
|
||||
|
||||
private async pickAttach(): Promise<void> {
|
||||
if (this.uploading) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const opts: picker.PhotoSelectOptions = new picker.PhotoSelectOptions();
|
||||
opts.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE;
|
||||
opts.maxSelectNumber = 1;
|
||||
const p: picker.PhotoViewPicker = new picker.PhotoViewPicker();
|
||||
const sel: picker.PhotoSelectResult = await p.select(opts);
|
||||
if (sel.photoUris.length === 0) {
|
||||
return;
|
||||
}
|
||||
this.uploading = true;
|
||||
this.note = '';
|
||||
// 资源附件字段名 `file`,与任务附件同
|
||||
const err: string = await fbUploadOne('/api/v1/resources/' + this.editingId.toString() + '/attach',
|
||||
'file', sel.photoUris[0], FB_TOKEN_STORE.get());
|
||||
this.uploading = false;
|
||||
if (err !== '') {
|
||||
this.note = err;
|
||||
return;
|
||||
}
|
||||
await this.reloadAttach();
|
||||
} catch (e) {
|
||||
const be: BusinessError = e as BusinessError;
|
||||
hilog.warn(0x0000, 'FamilyBook', '资源传附件失败:%{public}s', JSON.stringify(be));
|
||||
this.uploading = false;
|
||||
this.note = '传附件失败,请再试一次。';
|
||||
}
|
||||
}
|
||||
|
||||
private async dropAttach(a: FbResourceAttach): Promise<void> {
|
||||
const res: FbApiResult = await delResourceAttach(FB_TOKEN_STORE.get(), this.editingId, a.id);
|
||||
if (!res.ok) {
|
||||
this.note = res.error !== '' ? res.error : '附件没删掉。';
|
||||
return;
|
||||
}
|
||||
await this.reloadAttach();
|
||||
}
|
||||
|
||||
// ==================== 共享(工单 #25)====================
|
||||
private async reloadShares(): Promise<void> {
|
||||
const res: FbApiResult = await fetchResourceShares(FB_TOKEN_STORE.get(), this.editingId);
|
||||
if (!res.ok) {
|
||||
this.note = res.error !== '' ? res.error : '共享列表没拉到。';
|
||||
return;
|
||||
}
|
||||
const out: FbResourceShareRow[] = [];
|
||||
try {
|
||||
const o: Record<string, Object> = JSON.parse(res.data) as Record<string, Object>;
|
||||
const items: Object | undefined = o['items'];
|
||||
const rows: RawShareRow[] = (items !== undefined && items !== null)
|
||||
? (items as RawShareRow[]) : [];
|
||||
for (const s of rows) {
|
||||
out.push({
|
||||
personId: s.person_id !== undefined ? s.person_id : 0,
|
||||
name: s.name !== undefined && s.name !== null ? s.name : '(无名)',
|
||||
perm: s.perm !== undefined && s.perm !== null ? s.perm : 'read',
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// 解析失败就当空列表
|
||||
}
|
||||
this.shares = out;
|
||||
}
|
||||
|
||||
private async openPicker(): Promise<void> {
|
||||
if (this.roster.length === 0) {
|
||||
try {
|
||||
this.roster = await FbPersonRepo.inst().list();
|
||||
} catch (e) {
|
||||
this.roster = [];
|
||||
}
|
||||
}
|
||||
this.picking = true;
|
||||
}
|
||||
|
||||
private async addShares(ids: number[]): Promise<void> {
|
||||
this.picking = false;
|
||||
if (ids.length === 0) {
|
||||
return;
|
||||
}
|
||||
let bad: string = '';
|
||||
for (const pid of ids) {
|
||||
const res: FbApiResult = await shareResource(FB_TOKEN_STORE.get(), this.editingId, pid, 'read');
|
||||
if (!res.ok && bad === '') {
|
||||
bad = res.error;
|
||||
}
|
||||
}
|
||||
this.note = bad !== '' ? bad : '';
|
||||
await this.reloadShares();
|
||||
}
|
||||
|
||||
private async dropShare(s: FbResourceShareRow): Promise<void> {
|
||||
const res: FbApiResult = await unshareResource(FB_TOKEN_STORE.get(), this.editingId, s.personId);
|
||||
if (!res.ok) {
|
||||
this.note = res.error !== '' ? res.error : '没取消成。';
|
||||
return;
|
||||
}
|
||||
await this.reloadShares();
|
||||
}
|
||||
|
||||
private async save(): Promise<void> {
|
||||
if (this.saving) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user