多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

ArkTS 表单工程:ACG 发帖页的实时字数统计与匿名开关预览

ArkTS 表单工程:ACG 发帖页的实时字数统计与匿名开关预览 ArkTS 表单工程ACG 发帖页的实时字数统计与匿名开关预览应用背景12 ACG 社区acg-community第二个 Tab 是社区对应源码Func1Tab.ets共 265 行页面标题「发布帖子」。它是内容社区的内容生产入口玫红主题#DB2777。本文逐行拆解TitleInput 标题输入、BoardSelector 板块选择、TagSelector 标签选择、ContentInput 正文编辑与实时字数、AnonymousSwitch 匿名开关、PreviewCard 发布预览、TemplateSection 快捷模板、SubmitBtn 提交八大模块重点讲解内容发布表单与快递寄件表单的异同尤其是TextArea 富文本编辑区 实时字数统计这一社区特有的交互。发布帖子页是内容社区的生产车间用户在这里把想法变成帖子。它和 11 号寄件页共享长表单 状态驱动的骨架但业务差异巨大——寄件页是选参数发帖页是写内容。后者引入了标题输入、多行正文、板块/标签双选择、匿名开关、实时字数等一系列内容生产特有的控件。读懂这一页你不仅掌握表单的进阶写法更能理解内容型表单与参数型表单的本质区别。一、数据模型与状态中枢发帖页定义了两个接口其余数据用字符串数组内联interface TemplateItem { id: number; emoji: string; name: string; desc: string; tag: string; } interface PreviewItem { label: string; value: string; }TemplateItem比寄件页的模板多了tag字段——因为社区模板要显示所属板块标签如追番笔记带#追番。PreviewItem仍是标签 值的预览行。状态中枢有 5 个State比寄件页多一个State boardIdx: number 0; // 板块下标 State tagIdx: number 0; // 标签下标 State title: string ; // 标题文本 State content: string ; // 正文文本 State anonymous: boolean false; // 匿名开关对照寄件页的typeIdx / sizeIdx / senderName / receiverName / insured发帖页把两个输入框换成标题 正文两个文本状态把寄件方式/包裹大小换成板块/标签把保价换成匿名。5 个状态覆盖了内容发布的全部变量发到哪个板块、打什么标签、写什么标题、写什么正文、要不要匿名。这 5 个变量就是一篇帖子的完整元数据。项目源码开源https://gitee.com/codenestFlow/HarmonyOSHub二、页面骨架build() { Column() { this.Header() Scroll() { Column({ space: 16 }) { this.TitleInput() this.BoardSelector() this.TagSelector() this.ContentInput() this.AnonymousSwitch() this.PreviewCard() this.TemplateSection() this.SubmitBtn() } .width(100%) .padding({ left: D.pad, right: D.pad, top: 16, bottom: D.pad this.safeBottom 20 }) } .layoutWeight(1).scrollBar(BarState.Off).align(Alignment.Top) } .width(100%).height(100%).backgroundColor(C.bg) }骨架与寄件页一致但区块从 7 个变成 8 个多了独立的标题输入区。顺序遵循内容发布的自然流程先写标题 → 选板块 → 选标签 → 写正文 → 设匿名 → 预览 → 用模板 → 提交自上而下与用户的操作顺序一一对应。表单页的区块顺序本质上就是用户完成任务的步骤顺序排布时务必顺着用户的思维流走。三、TitleInput 标题输入标题是内容生产的第一要素本页用独立的输入卡承载并加了maxLength限制// TitleInput — 标题 Builder TitleInput() { Column({ space: 8 }) { Text(帖子标题).fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text).width(100%) TextInput({ placeholder: 请输入标题最多50字, text: this.title }) .backgroundColor(C.cardSoft).borderRadius(D.rSm).height(48) .placeholderColor(C.textDim).placeholderFont({ size: 14 }) .maxLength(50) .onChange((val: string) { this.title val; }) } .width(100%).padding(14) .backgroundColor(C.card).borderRadius(D.rLg) .border({ width: 1, color: C.stroke }) }两个新细节其一是.maxLength(50)——标题限长是内容社区的通用约束既能防止标题过长撑破布局又能引导用户提炼要点。TextInput内置的maxLength会在用户输入到 50 字时自动截断无需手写校验。其二是backgroundColor(C.cardSoft)——输入框用了浅灰底#F4F6FB而非寄件页的transparent让输入区域在白色卡片里凹下去视觉上更明确这是可输入的区域。这两种风格透明融入 vs 浅底凹陷都是合理的输入框设计选哪种取决于整体扁平度偏好。占位文案「请输入标题最多50字」把最多50字的约束直接写进 placeholder用户在输入前就知道限制比输入到一半被截断再提示体验更好。这是用占位文案传递约束的小技巧成本极低、体验收益明显。四、BoardSelector 板块选择板块选择用Flex换行布局每个板块带 emoji 图标选中态玫红底白字// BoardSelector — 板块选择 Builder BoardSelector() { Column({ space: 12 }) { Text(选择板块).fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text).width(100%) Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) { ForEach(this.boards, (b: string, idx: number) { Row({ space: 4 }) { Text(idx 0 ? : idx 1 ? : idx 2 ? : idx 3 ? : ).fontSize(14) Text(b).fontSize(13).fontColor(this.boardIdx idx ? #FFFFFF : C.textSub) } .padding({ left: 14, right: 14, top: 10, bottom: 10 }) .backgroundColor(this.boardIdx idx ? C.primary : C.card) .borderRadius(D.rSm) .border({ width: 1, color: this.boardIdx idx ? C.primary : C.stroke }) .margin({ right: 10, bottom: 10 }) .onClick(() { this.boardIdx idx; }) }, (b: string) b) }.width(100%) } .width(100%).padding(14) .backgroundColor(C.card).borderRadius(D.rLg) .border({ width: 1, color: C.stroke }) }五个板块追番/游戏/创作/轻小说/杂谈emoji 用idx 0 ? : idx 1 ? : ...的链式三元做内联映射。注意这里比寄件页的三选项多了两层嵌套三元五个分支这已经接近三元嵌套可读性的临界点——选项一多就该考虑把 emoji 也建模进数据如interface Board { name: string; emoji: string }而非继续堆三元。选中态逻辑this.boardIdx idx ? C.primary : C.card与寄件页 TypeSelector 完全一致但这里用的是FlexWrap可换行而非等宽Row——因为 5 个板块宽度不一、且未来可能增减板块换行布局更灵活。选中项玫红底白字 实边框未选中白底灰字 描边反差清晰。五、TagSelector 标签选择标签选择是内容社区的特色控件用#话题的胶囊形态呈现// TagSelector — 标签选择 Builder TagSelector() { Column({ space: 12 }) { Text(帖子标签).fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text).width(100%) Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) { ForEach(this.tags, (t: string, idx: number) { Text(# t) .fontSize(13).fontColor(this.tagIdx idx ? #FFFFFF : C.textSub) .padding({ left: 14, right: 14, top: 8, bottom: 8 }) .backgroundColor(this.tagIdx idx ? C.primary : C.cardSoft) .borderRadius(16) .border({ width: 1, color: this.tagIdx idx ? C.primary : C.stroke }) .margin({ right: 10, bottom: 10 }) .onClick(() { this.tagIdx idx; }) }, (t: string) t) }.width(100%) } .width(100%).padding(14) .backgroundColor(C.card).borderRadius(D.rLg) .border({ width: 1, color: C.stroke }) }两个特色其一是# t前缀拼接——标签显示为#日常、#攻略的形式#是社交平台通用的话题符号用户看到#就本能知道这是可点击的话题标签这是内容社区的强语义符号。其二是borderRadius(16)的胶囊圆角——比板块选择的D.rSm10更圆因为标签是小颗粒话题胶囊形态更符合话题标签的心智而板块是大分类用稍小的圆角更稳重。未选中态用C.cardSoft浅灰底而非纯白让胶囊在白色卡片里微微浮起。标签的选中/未选中反差玫红 vs 浅灰比板块更柔和因为标签是附加信息、板块是主要分类——再次体现主次分级的排布心法主要维度强反差、次要维度弱反差。六、ContentInput 正文编辑与实时字数核心这是全页与寄件页差异最大的模块多行正文 富文本快捷工具 实时字数统计。// ContentInput — 正文编辑 Builder ContentInput() { Column({ space: 8 }) { Text(帖子内容).fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text).width(100%) TextArea({ placeholder: 分享你的想法..., text: this.content }) .backgroundColor(C.cardSoft).borderRadius(D.rSm).height(120) .placeholderColor(C.textDim).placeholderFont({ size: 14 }) .onChange((val: string) { this.content val; }) Row({ space: 12 }) { Text( 图片).fontSize(12).fontColor(C.primary) Text( 链接).fontSize(12).fontColor(C.primary) Text( 提及).fontSize(12).fontColor(C.primary) Text( 投票).fontSize(12).fontColor(C.primary) Blank() Text(this.content.length 字).fontSize(11).fontColor(C.textDim) }.width(100%) } .width(100%).padding(14) .backgroundColor(C.card).borderRadius(D.rLg) .border({ width: 1, color: C.stroke }) }三个亮点逐一拆解其一TextArea多行输入。标题用TextInput单行正文用TextArea多行固定高 120vp。TextArea是 ArkUI 的多行文本编辑器适合长内容输入.height(120)固定了可见高度超出后可滚动。这是标题单行 正文多行的内容生产标准配置任何发帖、评论、写文章的表单都离不开这对组合。其二富文本快捷工具栏。「 图片 / 链接 / 提及 / 投票」四个快捷入口横排在正文下方都用C.primary玫红色。它们是内容社区富文本编辑的占位——真实场景下点会唤起相册、点会插入链接、点 提及会唤起用户搜索、点会创建投票。demo 里它们只是展示但已经勾勒出社区编辑器的产品轮廓社区帖子不止纯文字还支持图片、链接、提及、投票等富内容。其三实时字数统计this.content.length 字。这是全页最灵的交互——因为content是State用户在TextArea里每敲一个字onChange就把新值写回content而字数那行Text直接读this.content.length于是字数实时跳动无需任何手动刷新。这就是声明式 UI 状态驱动视图最直观的体现你只负责更新状态UI 自动跟着变。this.content.length统计的是字符数含标点、空格对中文场景足够准确。若要做按字节统计或排除空格需要额外处理。真实项目里字数统计常和最大字数限制联动——当content.length逼近上限时变红提醒配合TextArea的maxLength做硬限制。这个实时字数 上限提醒的组合是内容编辑器的基础体验。七、AnonymousSwitch 匿名发布匿名开关用Toggle实现文案针对内容社区做了本地化// AnonymousSwitch — 匿名发布 Builder AnonymousSwitch() { Row({ space: 12 }) { Column({ space: 4 }) { Text(匿名发布).fontSize(14).fontWeight(FontWeight.Medium).fontColor(C.text) Text(隐藏你的昵称和头像).fontSize(11).fontColor(C.textDim) } .alignItems(HorizontalAlign.Start).layoutWeight(1) Toggle({ type: ToggleType.Switch, isOn: this.anonymous }) .selectedColor(C.primary) .onChange((on: boolean) { this.anonymous on; promptAction.showToast({ message: on ? 已开启匿名 : 已关闭匿名 }); }) } .width(100%).padding(14) .backgroundColor(C.card).borderRadius(D.rLg) .border({ width: 1, color: C.stroke }) }匿名发布是社区类应用的特有功能——用户可能想吐槽、提问又不愿暴露身份。文案「隐藏你的昵称和头像」精确解释了匿名的作用比单纯写匿名更贴心。开关逻辑与寄件页的保价开关完全一致ToggleselectedColoronChange更新状态 Toast 反馈又一次印证开关类控件的统一写法。anonymous这个布尔状态在真实项目里会关联到帖子详情页的是否显示作者头像/昵称以及后端存储的is_anonymous字段。它和title、content、boardIdx、tagIdx一起构成一篇帖子的完整元数据——提交时把这些状态打包成一个对象发给后端即可。八、PreviewCard 发布预览预览卡把用户前面的选择汇总成发帖前确认单// PreviewCard — 发布预览 Builder PreviewCard() { Column({ space: 12 }) { Text(发布预览).fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text).width(100%) ForEach(this.previewItems, (p: PreviewItem) { Row() { Text(p.label).fontSize(13).fontColor(C.textDim) Blank() Text(p.value).fontSize(13).fontColor(C.text).fontWeight(FontWeight.Medium) }.width(100%) }, (p: PreviewItem) p.label) } .width(100%).padding(14) .backgroundColor(C.card).borderRadius(D.rLg) .border({ width: 1, color: C.stroke }) }预览行是发布板块 / 帖子标签 / 字数统计 / 预计阅读 / 匿名发布五项Blank()右对齐。注意字数统计 0 字和预计阅读 0 分钟这两项在 demo 里是写死的——真实场景下它们应分别读this.content.length和Math.ceil(this.content.length / 400)按每分钟 400 字的阅读速度估算。这里和寄件页的 PreviewCard 一样是状态联动的可扩展点。预计阅读时长是内容社区特有的贴心设计——它让用户在发布前知道读者大概要花多久读完既是作者自我审视的参考也是读者点击前的决策依据。这个看似微小的字段背后是内容长度 → 阅读时长的换算逻辑值得在真实项目里做实。九、TemplateSection 快捷模板模板区提供追番笔记 / 游戏攻略 / 同人创作 / 书评推荐四个预设每个带板块标签// TemplateSection — 快捷模板 Builder TemplateSection() { Column({ space: 12 }) { Text(快捷模板).fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text).width(100%) ForEach(this.templates, (t: TemplateItem) { Row({ space: 12 }) { Row() { Text(t.emoji).fontSize(22) } .width(42).height(42) .backgroundColor(C.primarySoft).borderRadius(D.rSm) .justifyContent(FlexAlign.Center) Column({ space: 3 }) { Text(t.name).fontSize(14).fontColor(C.text).fontWeight(FontWeight.Medium) Text(t.desc).fontSize(11).fontColor(C.textDim) } .alignItems(HorizontalAlign.Start).layoutWeight(1) Text(# t.tag).fontSize(11).fontColor(C.primary) .padding({ left: 8, right: 8, top: 3, bottom: 3 }) .backgroundColor(C.primarySoft).borderRadius(8) } .width(100%).padding({ left: 12, right: 12, top: 10, bottom: 10 }) .backgroundColor(C.cardSoft).borderRadius(D.rSm) .onClick(() { promptAction.showToast({ message: 使用模板: t.name }); }) }, (t: TemplateItem) t.id.toString()) }.width(100%).padding(14) .backgroundColor(C.card).borderRadius(D.rLg) .border({ width: 1, color: C.stroke }) }模板卡片是emoji 名称描述 板块标签三段式右侧标签用# t.tag显示#追番、#游戏等C.primarySoft浅粉底、C.primary玫红字。这与 TagSelector 的胶囊风格呼应形成标签符号的视觉统一。模板的深层语义是预设状态组合点追番笔记模板应同时设置boardIdx 0追番板块 填充标题/正文占位。demo 里用 Toast 占位但你已经能看出——模板就是一键填充多个 State的快捷方式把它做实只需在onClick里改几个状态变量。十、SubmitBtn 与表单校验// SubmitBtn — 发布帖子 Builder SubmitBtn() { Button(发布帖子) .width(100%).height(50) .fontSize(16).fontColor(#FFFFFF).backgroundColor(C.primary) .borderRadius(D.rMd) .onClick(() { promptAction.showToast({ message: 发布成功 }); }) }通栏玫红按钮与 HeroCard 的发布帖子按钮同色同语。真实上线时onClick里应先校验title非空、content非空、板块已选默认已选第一个。校验逻辑因为状态集中在 5 个State而变得极其简单——读变量、判空、提示三步搞定不用在 DOM 里翻找。十一、与寄件表单的参数型 vs 内容型对比发帖页12 号与寄件页11 号都是长表单 状态驱动但本质是两种表单范式参数型表单寄件用户从有限选项里选输出的是结构化参数方式/大小/保价输入以点选为主内容型表单发帖用户写文字输出的是内容本身标题/正文输入以键入为主配合板块/标签等分类元数据。因此发帖页多了TextInput标题TextArea正文 实时字数统计而寄件页更依赖选择器。理解参数型 vs 内容型这对范畴能帮你快速判断一个新表单该用什么控件选参数就用选择器Radio/Toggle/Flex 标签写内容就用输入框TextInput/TextArea 字数/字数上限。十二、本章小结与工程经验模块核心技术学习价值TitleInputmaxLength(50)限长标题输入与约束BoardSelectorFlex 换行 链式 emoji板块单选器TagSelector# t胶囊 borderRadius(16)话题标签选择ContentInputTextArea 实时字数富文本编辑区AnonymousSwitchToggle 开关匿名发布开关PreviewCardBlank 右对齐汇总发帖前确认TemplateSectionemoji 描述 #tag预设状态组合SubmitBtn通栏主按钮提交与校验入口发帖页最值得迁移的是内容编辑器范式TextInput单行标题TextArea多行正文 富文本工具栏图片/链接//投票this.content.length实时字数这套组合几乎能覆盖一切写内容的场景发帖、评论、写笔记、提反馈。其中实时字数统计是声明式 UI 最生动的教学案例——状态一变、字数自跳零手动刷新。其次标签的#前缀与胶囊圆角是内容社区的强语义符号值得在主题类里固化参数型 vs 内容型表单的区分能帮你快速选对控件。最后maxLength与 placeholder 传递约束、模板即预设状态组合这两条都是小而美的工程细节用好了能显著提升表单的体验与可维护性。
返回列表