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.
This commit is contained in:
1 parent
5b1367c775
commit
a8ee0fcebd
7 files changed
+445
-51
No files matched your search
@@ -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 并关单。
|
||||
|
||||
@@ -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` 等),不得擅自替换为小程序品牌色。
|
||||
|
||||
@@ -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, {})
|
||||
})
|
||||
Reference in new issue
Block a user