Author SHA1 Message Date
cykj 0f7115205d Merge pull request 'fix(mobile): 按确认原型美化财务备注弹窗' (#9) from codex/oa-finance-remark-ui into V2.0 2026-10-30 14:18:30 +00:00
zlwu a8ee0fcebd fix(mobile): polish finance remark dialog from approved Figma design
Use compact layout, independent buttons and keyboard-aware viewport; retain drafts on failure and block closing during save. Require Figma approval for future screens and dialogs unless explicitly waived.
2026-10-30 22:17:49 +08:00
cykj 5b1367c775 feat(oa): add permission-controlled shared finance remarks (#8)
Refs #7. H5 acceptance confirmed; approved test database migration verified. No release tag or deployment.
2026-10-30 13:44:33 +00:00
7 changed files with 445 additions and 51 deletions

No files matched your search

+1
View File
@@ -21,6 +21,7 @@
- 提交使用 Conventional Commits,完整说明“改了什么、为什么”。
- 需求开发与 Bug 修复:用户给出 Gitea 工单编号或链接时,先读取该工单描述、评论和验收条件再处理;未给编号时,先在当前工程 Gitea 仓库创建工单,再开发。继续当前已关联任务时复用原工单,不重复创建;已有授权下由 AI 创建和回写,不再要求用户代建。
- 修改端不明确时,先查当前任务和工单的已确认范围;仍不明确则询问修改 Web、小程序、移动 OA、后端中的哪些端,还是所有相关端。确认前仅只读定位、回写工单待确认范围,不修改业务代码;截图不能代替确认。已有明确范围直接沿用,答复“所有端”须记录具体应用清单;共享后端/API 影响范围外的端时,说明影响并确认扩展后再实施。
- 新界面、新弹窗及视觉改版,按 [UI 原型确认规则](openspec/changes/zkxw-engineering-standards/specs/ui-conventions/spec.md#requirement-界面与弹窗先经-figma-原型确认)先完成 Figma 原型并取得用户确认后开发;仅用户明确不用生成 Figma 原型时豁免。
- 默认由 Agent 测试:自动进入任务分支/worktree,核对实际运行版本,完成测试与审查后合入 `V2.0`、提交推送、同步本地主分支并关闭 Gitea 工单。用户已授予这条常规收尾链路持续授权,不逐次询问合并;打 tag、部署与服务启停仍分别受原有授权限制。
- 用户明确“我来测试”时,该任务由用户测试:Agent 不运行测试或构建,仍完成代码审查和 Git 核对;交付时提醒切换到具体分支,提供 worktree 路径、提交、前后端配套入口和验证要点。用户确认通过后直接合并推送、关闭工单;未确认或失败时不合并、不关闭。
- 合并前核对验收提交与最新目标;目标更新或冲突处理改变代码时,按原测试责任补验,用户模式仍交用户验证。合并后代码树与验收版本一致不重复测试;确认远端同步成功后回写验收结果、合并提交和 PR 并关单。
+309 -47
View File
@@ -77,38 +77,75 @@
<view v-if="loadingMore" class="oa-task-list__end">加载更多…</view>
<view v-else-if="!hasMore" class="oa-task-list__end">已经到底了</view>
</view>
<view v-if="financeRemarkVisible" class="confirm-dialog" @touchmove.stop.prevent>
<view class="confirm-dialog__mask" />
<view class="confirm-dialog__panel finance-remark-panel">
<view class="confirm-dialog__body">
<view class="confirm-dialog__title">财务备注</view>
<text v-if="financeRemarkLoading" class="finance-remark__hint">正在加载最新备注…</text>
<view
v-if="financeRemarkVisible"
class="finance-dialog"
:style="financeRemarkViewportStyle"
@touchmove.stop.prevent
>
<view class="finance-dialog__mask" />
<view class="finance-dialog__panel" role="dialog" aria-label="财务备注" @touchmove.stop>
<view class="finance-dialog__header">
<view class="finance-dialog__heading">
<text class="finance-dialog__title">财务备注</text>
<text class="finance-dialog__subtitle">同一审批单在各端共享</text>
</view>
<button
class="finance-dialog__close"
aria-label="关闭财务备注"
hover-class="finance-dialog__pressed"
:disabled="financeRemarkSaving"
@click="cancelFinanceRemark"
>
<image class="finance-dialog__close-icon" src="/static/finance-remark-close.svg" />
</button>
</view>
<view class="finance-dialog__body">
<view class="finance-dialog__field-header">
<text class="finance-dialog__label">备注内容</text>
<text class="finance-dialog__count">{{ financeRemarkText.length }} / 512</text>
</view>
<view v-if="financeRemarkLoading" class="finance-dialog__loading" role="status">
正在加载最新备注…
</view>
<textarea
v-else
v-model="financeRemarkText"
class="finance-remark__input"
class="finance-dialog__input"
:class="{
'is-error': financeRemarkError,
'is-saving': financeRemarkSaving
}"
:maxlength="512"
:disabled="financeRemarkSaving"
:adjust-position="true"
placeholder="请输入财务备注,留空可清空"
:adjust-position="false"
:show-confirm-bar="false"
:fixed="true"
aria-label="备注内容"
placeholder="填写需要补充的财务说明…"
placeholder-class="finance-dialog__placeholder"
@keyboardheightchange="onFinanceRemarkKeyboardChange"
/>
<text class="finance-remark__hint">{{ financeRemarkText.length }}/512</text>
<text class="finance-dialog__hint" :class="{ 'is-error': financeRemarkError }">
{{ financeRemarkError || '留空并保存,可清空原有备注' }}
</text>
</view>
<view class="confirm-dialog__actions">
<view class="finance-dialog__actions">
<button
class="confirm-dialog__btn confirm-dialog__btn--cancel finance-remark__button"
class="finance-dialog__button finance-dialog__button--cancel"
hover-class="finance-dialog__pressed"
:disabled="financeRemarkSaving"
@click="cancelFinanceRemark"
>
取消
</button>
<button
class="confirm-dialog__btn confirm-dialog__btn--confirm finance-remark__button"
:loading="financeRemarkSaving"
class="finance-dialog__button finance-dialog__button--save"
hover-class="finance-dialog__pressed"
:disabled="financeRemarkLoading || financeRemarkSaving"
@click="saveFinanceRemark"
>
保存
{{ financeRemarkSaving ? '保存中…' : '保存备注' }}
</button>
</view>
</view>
@@ -161,6 +198,9 @@
financeRemarkLoading: false,
financeRemarkSaving: false,
financeRemarkText: '',
financeRemarkError: '',
financeRemarkViewportStyle: {},
financeRemarkWindowHeight: 0,
financeRemarkInstanceId: '',
financeRemarkVersion: 0,
listVersion: 0,
@@ -210,22 +250,68 @@
},
methods: {
cancelFinanceRemark() {
if (this.financeRemarkSaving) return
this.resetFinanceRemark()
},
resetFinanceRemark() {
if (this._financeRemarkViewportCleanup) this._financeRemarkViewportCleanup()
this._financeRemarkViewportCleanup = null
this.financeRemarkViewportStyle = {}
this.financeRemarkWindowHeight = 0
this.financeRemarkVersion++
this.financeRemarkVisible = false
this.financeRemarkText = ''
this.financeRemarkError = ''
this.financeRemarkInstanceId = ''
this.financeRemarkLoading = false
this.financeRemarkSaving = false
},
clearFinanceAccess() {
this.canManageFinanceRemark = false
this.cancelFinanceRemark()
this.resetFinanceRemark()
this.list = this.list.map((row) => {
const sanitized = { ...row }
delete sanitized.financeRemark
return sanitized
})
},
watchFinanceRemarkViewport() {
// H5 uses the visual viewport so keyboard resize and browser panning are both covered.
// #ifdef H5
if (typeof window !== 'undefined' && window.visualViewport) {
const viewport = window.visualViewport
const update = () => {
if (!this.financeRemarkVisible) return
this.financeRemarkViewportStyle = {
height: viewport.height + 'px',
top: viewport.offsetTop + 'px'
}
}
update()
viewport.addEventListener('resize', update)
viewport.addEventListener('scroll', update)
this._financeRemarkViewportCleanup = () => {
viewport.removeEventListener('resize', update)
viewport.removeEventListener('scroll', update)
}
return
}
// #endif
const info = uni.getWindowInfo ? uni.getWindowInfo() : uni.getSystemInfoSync()
this.financeRemarkWindowHeight = info.windowHeight
this.financeRemarkViewportStyle = {
height: info.windowHeight + 'px',
top: '0px'
}
},
onFinanceRemarkKeyboardChange(event) {
if (!this.financeRemarkVisible || !this.financeRemarkWindowHeight) return
const keyboardHeight = Math.max(0, Number(event?.detail?.height) || 0)
this.financeRemarkViewportStyle = {
height: Math.max(0, this.financeRemarkWindowHeight - keyboardHeight) + 'px',
top: '0px'
}
},
async openFinanceRemark(row) {
if (!this.canManageFinanceRemark || this.financeRemarkSaving) return
const id = String(row.instanceId || '').trim()
@@ -235,6 +321,7 @@
this.financeRemarkInstanceId = id
this.financeRemarkVisible = true
this.financeRemarkLoading = true
this.watchFinanceRemarkViewport()
try {
const result = await fetchOaFinanceRemark(id)
if (version !== this.financeRemarkVersion) return
@@ -259,23 +346,21 @@
)
return
if (this.financeRemarkText.length > 512) {
uni.showToast({ title: '财务备注最多 512 个字符', icon: 'none' })
this.financeRemarkError = '财务备注最多 512 个字符'
return
}
const version = this.financeRemarkVersion
this.financeRemarkError = ''
this.financeRemarkSaving = true
try {
await fetchUpdateOaFinanceRemark(this.financeRemarkInstanceId, this.financeRemarkText)
if (version !== this.financeRemarkVersion) return
this.cancelFinanceRemark()
this.resetFinanceRemark()
uni.showToast({ title: '财务备注已保存', icon: 'success' })
await this.loadList(true)
} catch (e) {
if (version !== this.financeRemarkVersion) return
uni.showToast({
title: (e && e.message) || '保存财务备注失败',
icon: 'none'
})
this.financeRemarkError = '保存失败,请稍后重试,输入已保留。'
} finally {
if (version === this.financeRemarkVersion) this.financeRemarkSaving = false
}
@@ -462,46 +547,223 @@
}
.finance-remark__edit::after,
.finance-remark__button::after {
.finance-dialog__button::after,
.finance-dialog__close::after {
border: 0;
}
.finance-remark-panel {
max-height: calc(100vh - 100px);
overflow-y: auto;
padding-bottom: env(safe-area-inset-bottom);
}
.finance-remark__input {
.finance-dialog {
position: fixed;
z-index: 2000;
top: 0;
left: 0;
width: 100%;
height: 240rpx;
padding: 16rpx;
height: 100vh;
height: 100dvh;
display: flex;
align-items: center;
justify-content: center;
padding: 16px 24px;
padding-top: max(16px, env(safe-area-inset-top));
padding-bottom: max(16px, env(safe-area-inset-bottom));
box-sizing: border-box;
border: 1rpx solid #dcebe4;
border-radius: 12rpx;
font-size: 28rpx;
text-align: left;
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', sans-serif;
}
.finance-remark__hint {
display: block;
margin-top: 12rpx;
font-size: 24rpx;
.finance-dialog__mask {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba(0, 26, 15, 0.38);
}
.finance-dialog__panel {
position: relative;
display: flex;
flex-direction: column;
gap: 20px;
width: 100%;
max-width: 480px;
max-height: 100%;
padding: 20px;
box-sizing: border-box;
overflow: hidden;
border-radius: 22px;
background: #fff;
box-shadow: 0 12px 40px rgba(0, 26, 15, 0.18);
color: #1f2a26;
}
.finance-dialog__header {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.finance-dialog__heading {
display: flex;
flex: 1;
min-width: 0;
flex-direction: column;
gap: 4px;
}
.finance-dialog__title {
font-size: 20px;
font-weight: 700;
line-height: 30px;
}
.finance-dialog__subtitle,
.finance-dialog__count,
.finance-dialog__hint {
color: #6b7b74;
text-align: right;
font-size: 12px;
line-height: 18px;
}
.finance-remark__button {
.finance-dialog__close {
display: flex;
align-items: center;
justify-content: center;
flex: 0 0 44px;
width: 44px;
height: 44px;
margin: 0;
border-radius: 0;
min-height: 88rpx;
line-height: 88rpx;
padding: 0;
border: 0;
border-radius: 22px;
background: #f5f8f7;
line-height: 1;
}
.finance-remark__button.confirm-dialog__btn--confirm {
background: #0a9f6d;
.finance-dialog__close-icon {
display: block;
width: 16px;
height: 16px;
}
.finance-dialog__body {
display: flex;
flex-direction: column;
gap: 8px;
min-height: 0;
overflow-y: auto;
overscroll-behavior: contain;
}
.finance-dialog__field-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
flex-shrink: 0;
gap: 8px;
}
.finance-dialog__label {
font-size: 13px;
font-weight: 500;
line-height: 20px;
}
.finance-dialog__input,
.finance-dialog__loading {
flex-shrink: 0;
width: 100%;
height: 120px;
margin: 0;
padding: 14px;
box-sizing: border-box;
border: 1px solid #dcebe4;
border-radius: 12px;
background: #f7faf9;
color: #1f2a26;
font-family: inherit;
font-size: 14px;
line-height: 21px;
text-align: left;
overflow-y: auto;
}
.finance-dialog__loading {
display: flex;
align-items: center;
justify-content: center;
color: #6b7b74;
}
.finance-dialog__placeholder {
color: #6b7b74;
}
.finance-dialog__input.is-error {
border-color: #d14343;
}
.finance-dialog__input.is-saving {
opacity: 0.6;
}
.finance-dialog__hint {
display: block;
flex-shrink: 0;
}
.finance-dialog__hint.is-error {
color: #d14343;
}
.finance-dialog__actions {
display: flex;
gap: 12px;
flex-shrink: 0;
}
.finance-dialog__button {
display: flex;
align-items: center;
justify-content: center;
height: 46px;
min-width: 0;
margin: 0;
padding: 0;
border: 0;
border-radius: 12px;
font-family: inherit;
font-size: 15px;
font-weight: 500;
line-height: 23px;
}
.finance-dialog__button--cancel,
.finance-dialog__button--cancel[disabled] {
flex: 0 0 100px;
width: 100px;
color: #6b7b74;
background: #f5f8f7;
}
.finance-dialog__button--save,
.finance-dialog__button--save[disabled] {
flex: 1;
color: #fff;
background: #0a9f6d;
}
.finance-dialog__button--cancel[disabled],
.finance-dialog__close[disabled] {
opacity: 0.55;
}
.finance-dialog__button--save[disabled] {
opacity: 0.65;
}
.finance-dialog__pressed {
opacity: 0.8;
}
.page {
@@ -0,0 +1,8 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Frame">
<g id="Vector">
<path d="M4 4L12 12ZM12 4L4 12Z" fill="black"/>
<path d="M4 4L12 12M12 4L4 12" stroke-width="1.6" stroke-linecap="round" stroke="#6B7B74"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 354 B

@@ -6,7 +6,13 @@ GET `/web/admin/oa-approval/tasks/done` 在 data 增加 `canManageFinanceRemark`
各端采用后端能力标记,不依赖角色名称或仅前端隐藏。打开弹窗时重新读取最新备注,保存后刷新列表;取消不写入,失败保留编辑内容。文字按纯文本渲染。单字段最后成功保存值生效。
简单列表字段和编辑交互沿用既有 OA/通用弹窗,无需另造复杂 UI 或 Figma 原型。权限菜单放在已办中心下,Web/移动对应菜单可用同一权限码,不自动授予角色。
权限菜单放在已办中心下,Web/移动对应菜单可用同一权限码,不自动授予角色。
## 移动弹窗视觉改版
初版通用弹窗的按钮贴边与留白不符合用户预期。本次以用户已明确确认的 [Figma 四状态原型](https://www.figma.com/design/jc7D8Fe7lKNTVj4jM3AoGW?node-id=14-56)为视觉依据:375px/414px 屏幕各留 24px 外边距,弹窗 22px 圆角、20px 内边距,独立的 46px 高圆角按钮,120px 高浅色输入区,明确加载、失败与保存中状态。Figma 的 Noto Sans SC 为工具字体回退,实现保留移动端系统字体栈。
仅更新 mobile-h5 财务备注弹窗,不修改全局确认弹窗或 Web 视觉。关闭/取消不写入,遮罩不直接关闭,保存中禁止重复提交与关闭,失败保留输入,键盘缩小可用视口后仍可操作。权限、本人已办范围和接口契约保持不变。今后新增界面和弹窗按工程 UI 原型确认规则执行,不再因简单或已有参考自动跳过 Figma。
## 验证
SQLite 隔离集成测试真实角色/菜单/任务:允许、拒绝、跨租户、伪造租户、无本人已办、软删/停用、角色/职位并集、保存读取清空、超长、非字符串、其他中心不泄露。各端 UI 行为检查和构建;uni-app HBuilderX 编译及可用平台检查。缺失环境如实记录,必要验证未完成前不关单。
@@ -11,3 +11,13 @@
- [x] 移动端验收方式:用户明确本次可用嵌套 H5 验证,不要求微信平台交互验证;采用已通过的 H5 编译产物交互结果。微信小程序编译通过,但未做模拟器/真机交互验证。
- [x] 测试库迁移:用户确认当前远程库为测试库并授权执行;迁移后字段为可空 VARCHAR(512),为 2 个已办父菜单新增 2 个权限节点,角色授权数量未变。管理员须按需授予角色权限,其他环境仍需先迁移再更新应用。
- [x] 交付准备:任务分支及 PR #8 已推送,用户确认的验收方式与测试库迁移条件已满足。按持续授权核对最新 V2.0 后合并、同步本地主分支并关闭 #7;最终合并提交和关单结果记录在 Gitea 工单中。不打 tag、不部署、不启停服务。
## 弹窗视觉改版(工单 #7 重新打开)
用户已确认 [Figma 原型及四种状态](https://www.figma.com/design/jc7D8Fe7lKNTVj4jM3AoGW?node-id=14-56),同意按此实现。范围仅 mobile-h5 的财务备注弹窗(小程序/H5 共用);默认 Agent 验证,沿用用户确认的 H5 验收方式,小程序做编译检查。Web 与后端业务契约不变。
- [x] 读取 Figma 设计上下文,实现紧凑居中布局、标题与关闭、浅色输入区、字数及辅助提示、独立圆角按钮;处理加载、失败保留输入、保存中禁用和键盘可见性。
- [x] 前端行为测试 27/27 通过,HBuilderX H5/微信小程序编译通过。实际 H5 产物在 375px/414px 下弹窗为 327/366×352px,与原型一致;380px 短视口下按钮可见,长文本可滚动。模拟接口验证保存、取消、清空、失败保留、保存中锁定、权限隐藏和纯文本,无页面异常;未进行微信模拟器/真机交互。
- [x] 独立规格与质量审查通过,无阻塞项;交付材料已就绪。按持续授权推送 PR,核对目标分支、合入 V2.0 并同步主目录,最终合并提交及关闭 #7 的结果记录在 Gitea 工单。
个人开发规则另行沉淀至 personal-dev 的界面原型引用:所有新页面、新弹窗及视觉改版先生成 Figma 原型并经用户明确确认;只有用户明确本任务不用 Figma 时才豁免。
@@ -1,5 +1,24 @@
## ADDED Requirements
### Requirement: 界面与弹窗先经 Figma 原型确认
新页面、新弹窗及用户要求的视觉改版 SHALL 先在 Figma 创建可编辑原型,覆盖主要尺寸和交互状态,关联具体文件/节点,并经用户明确确认后再修改业务界面代码。简单界面、单字段弹窗、已有组件或赶时间不构成豁免。只有用户明确本任务不用生成 Figma 原型时才记录范围并跳过;用户沉默、工具生成成功或“代码完成再看”不能代替确认。原型确认不替代功能验证。
#### Scenario: 复用已有组件新增弹窗
- **WHEN** 新弹窗可以复用工程现有组件
- **THEN** 在原型中复用对应设计并向用户确认,确认后实现;已有明确确认的同版原型直接沿用,不重复询问。
#### Scenario: 原型服务不可用或尚未确认
- **WHEN** Figma 不可用或用户尚未确认原型,且没有明确豁免
- **THEN** 保持界面实现待处理,说明缺口并继续独立工作;不先编码再补原型。
#### Scenario: 明确豁免或非视觉变更
- **WHEN** 用户明确本任务不用 Figma,或任务仅为无布局变化的文案纠错、后台逻辑、恢复既有已确认设计
- **THEN** 按原任务规则实施与验证;原型豁免仅作用于用户指定范围,不推广到后续新界面。
### Requirement: Web 管理端以 Fresh Mint 规范为唯一视觉权威
`web-admin` 工程的界面实现 SHALL 以 `docs/web-admin-ui-spec-fresh-mint.md` 及其中引用的 `docs/fresh-mint-green.html` 为权威来源;主色链 SHALL 使用文档定义的 Mint 绿系(如 `--primary` `#10b981` 等),不得擅自替换为小程序品牌色。
+91 -3
View File
@@ -31,7 +31,7 @@ function evaluate(source, deps, tail = '') {
exports
)
}
function harness(platform) {
function harness(platform, overrides = {}) {
const calls = []
const hooks = {}
const api = {
@@ -58,7 +58,7 @@ function harness(platform) {
useMenuPerm: () => ({ hasMenuPerm: () => false }),
useUserStore: () => ({}),
ElMessage: { success() {}, error() {}, warning() {} },
uni: { showToast() {}, navigateTo() {} },
uni: { showToast() {}, navigateTo() {}, getSystemInfoSync: () => ({ windowHeight: 800 }) },
VanEmpty: {},
getToken: () => 'token',
forceRelogin() {},
@@ -73,7 +73,8 @@ function harness(platform) {
fetchUpdateOaFinanceRemark: async (id, text) => {
calls.push(['put', id, text])
return api.put(id, text)
}
},
...overrides
}
const path =
platform === 'web'
@@ -297,3 +298,90 @@ test('web: delayed refresh on an inactive cached page cannot restore finance dat
assert.equal(h.vm.canManageFinanceRemark, false)
assert.equal(h.calls.length, count)
})
test('mobile: save errors remain inline until retry or cancellation', async () => {
const h = harness('mobile')
await h.load()
await h.vm.openFinanceRemark(h.rows()[0])
h.vm.financeRemarkText = '保留输入'
h.api.put = async () => {
throw new Error('failure')
}
await h.vm.saveFinanceRemark()
assert.equal(h.vm.financeRemarkError, '保存失败,请稍后重试,输入已保留。')
assert.equal(h.vm.financeRemarkText, '保留输入')
let resolve
h.api.put = () =>
new Promise((r) => {
resolve = r
})
const retry = h.vm.saveFinanceRemark()
assert.equal(h.vm.financeRemarkError, '')
resolve({})
await retry
assert.equal(h.vm.financeRemarkVisible, false)
})
test('mobile: saving blocks user cancellation but page cleanup invalidates the response', async () => {
const h = harness('mobile')
await h.load()
await h.vm.openFinanceRemark(h.rows()[0])
let resolve
h.api.put = () =>
new Promise((r) => {
resolve = r
})
const saving = h.vm.saveFinanceRemark()
h.vm.cancelFinanceRemark()
assert.equal(h.vm.financeRemarkVisible, true)
assert.equal(h.vm.financeRemarkSaving, true)
h.hooks.hide()
assert.equal(h.vm.financeRemarkVisible, false)
assert.equal(h.vm.financeRemarkText, '')
const callCount = h.calls.length
resolve({})
await saving
assert.equal(h.calls.length, callCount)
})
test('mobile: keyboard height keeps the dialog inside available viewport and resets on close', async () => {
const h = harness('mobile')
await h.load()
await h.vm.openFinanceRemark(h.rows()[0])
assert.equal(h.vm.financeRemarkViewportStyle?.height, '800px')
h.vm.onFinanceRemarkKeyboardChange({ detail: { height: 320 } })
assert.equal(h.vm.financeRemarkViewportStyle.height, '480px')
h.vm.onFinanceRemarkKeyboardChange({ detail: { height: 0 } })
assert.equal(h.vm.financeRemarkViewportStyle.height, '800px')
h.vm.cancelFinanceRemark()
assert.deepEqual(h.vm.financeRemarkViewportStyle, {})
h.vm.onFinanceRemarkKeyboardChange({ detail: { height: 320 } })
assert.deepEqual(h.vm.financeRemarkViewportStyle, {})
})
test('mobile: H5 viewport resize and scroll track keyboard geometry and listeners are released', async () => {
const listeners = new Map()
const viewport = {
height: 800,
offsetTop: 0,
addEventListener: (name, fn) => listeners.set(name, fn),
removeEventListener: (name, fn) => {
assert.equal(listeners.get(name), fn)
listeners.delete(name)
}
}
const h = harness('mobile', { window: { visualViewport: viewport } })
await h.load()
await h.vm.openFinanceRemark(h.rows()[0])
assert.equal(listeners.size, 2)
viewport.height = 420
viewport.offsetTop = 20
listeners.get('resize')()
assert.deepEqual(h.vm.financeRemarkViewportStyle, { height: '420px', top: '20px' })
// A native keyboard notification must not subtract the same keyboard twice on H5.
h.vm.onFinanceRemarkKeyboardChange({ detail: { height: 380 } })
assert.equal(h.vm.financeRemarkViewportStyle.height, '420px')
h.hooks.hide()
assert.equal(listeners.size, 0)
assert.deepEqual(h.vm.financeRemarkViewportStyle, {})
})