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,完整说明“改了什么、为什么”。
|
- 提交使用 Conventional Commits,完整说明“改了什么、为什么”。
|
||||||
- 需求开发与 Bug 修复:用户给出 Gitea 工单编号或链接时,先读取该工单描述、评论和验收条件再处理;未给编号时,先在当前工程 Gitea 仓库创建工单,再开发。继续当前已关联任务时复用原工单,不重复创建;已有授权下由 AI 创建和回写,不再要求用户代建。
|
- 需求开发与 Bug 修复:用户给出 Gitea 工单编号或链接时,先读取该工单描述、评论和验收条件再处理;未给编号时,先在当前工程 Gitea 仓库创建工单,再开发。继续当前已关联任务时复用原工单,不重复创建;已有授权下由 AI 创建和回写,不再要求用户代建。
|
||||||
- 修改端不明确时,先查当前任务和工单的已确认范围;仍不明确则询问修改 Web、小程序、移动 OA、后端中的哪些端,还是所有相关端。确认前仅只读定位、回写工单待确认范围,不修改业务代码;截图不能代替确认。已有明确范围直接沿用,答复“所有端”须记录具体应用清单;共享后端/API 影响范围外的端时,说明影响并确认扩展后再实施。
|
- 修改端不明确时,先查当前任务和工单的已确认范围;仍不明确则询问修改 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 测试:自动进入任务分支/worktree,核对实际运行版本,完成测试与审查后合入 `V2.0`、提交推送、同步本地主分支并关闭 Gitea 工单。用户已授予这条常规收尾链路持续授权,不逐次询问合并;打 tag、部署与服务启停仍分别受原有授权限制。
|
||||||
- 用户明确“我来测试”时,该任务由用户测试:Agent 不运行测试或构建,仍完成代码审查和 Git 核对;交付时提醒切换到具体分支,提供 worktree 路径、提交、前后端配套入口和验证要点。用户确认通过后直接合并推送、关闭工单;未确认或失败时不合并、不关闭。
|
- 用户明确“我来测试”时,该任务由用户测试:Agent 不运行测试或构建,仍完成代码审查和 Git 核对;交付时提醒切换到具体分支,提供 worktree 路径、提交、前后端配套入口和验证要点。用户确认通过后直接合并推送、关闭工单;未确认或失败时不合并、不关闭。
|
||||||
- 合并前核对验收提交与最新目标;目标更新或冲突处理改变代码时,按原测试责任补验,用户模式仍交用户验证。合并后代码树与验收版本一致不重复测试;确认远端同步成功后回写验收结果、合并提交和 PR 并关单。
|
- 合并前核对验收提交与最新目标;目标更新或冲突处理改变代码时,按原测试责任补验,用户模式仍交用户验证。合并后代码树与验收版本一致不重复测试;确认远端同步成功后回写验收结果、合并提交和 PR 并关单。
|
||||||
|
|||||||
@@ -77,38 +77,75 @@
|
|||||||
<view v-if="loadingMore" class="oa-task-list__end">加载更多…</view>
|
<view v-if="loadingMore" class="oa-task-list__end">加载更多…</view>
|
||||||
<view v-else-if="!hasMore" class="oa-task-list__end">已经到底了</view>
|
<view v-else-if="!hasMore" class="oa-task-list__end">已经到底了</view>
|
||||||
</view>
|
</view>
|
||||||
<view v-if="financeRemarkVisible" class="confirm-dialog" @touchmove.stop.prevent>
|
<view
|
||||||
<view class="confirm-dialog__mask" />
|
v-if="financeRemarkVisible"
|
||||||
<view class="confirm-dialog__panel finance-remark-panel">
|
class="finance-dialog"
|
||||||
<view class="confirm-dialog__body">
|
:style="financeRemarkViewportStyle"
|
||||||
<view class="confirm-dialog__title">财务备注</view>
|
@touchmove.stop.prevent
|
||||||
<text v-if="financeRemarkLoading" class="finance-remark__hint">正在加载最新备注…</text>
|
>
|
||||||
|
<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
|
<textarea
|
||||||
v-else
|
v-else
|
||||||
v-model="financeRemarkText"
|
v-model="financeRemarkText"
|
||||||
class="finance-remark__input"
|
class="finance-dialog__input"
|
||||||
|
:class="{
|
||||||
|
'is-error': financeRemarkError,
|
||||||
|
'is-saving': financeRemarkSaving
|
||||||
|
}"
|
||||||
:maxlength="512"
|
:maxlength="512"
|
||||||
:disabled="financeRemarkSaving"
|
:disabled="financeRemarkSaving"
|
||||||
:adjust-position="true"
|
:adjust-position="false"
|
||||||
placeholder="请输入财务备注,留空可清空"
|
: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>
|
||||||
<view class="confirm-dialog__actions">
|
<view class="finance-dialog__actions">
|
||||||
<button
|
<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"
|
:disabled="financeRemarkSaving"
|
||||||
@click="cancelFinanceRemark"
|
@click="cancelFinanceRemark"
|
||||||
>
|
>
|
||||||
取消
|
取消
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="confirm-dialog__btn confirm-dialog__btn--confirm finance-remark__button"
|
class="finance-dialog__button finance-dialog__button--save"
|
||||||
:loading="financeRemarkSaving"
|
hover-class="finance-dialog__pressed"
|
||||||
:disabled="financeRemarkLoading || financeRemarkSaving"
|
:disabled="financeRemarkLoading || financeRemarkSaving"
|
||||||
@click="saveFinanceRemark"
|
@click="saveFinanceRemark"
|
||||||
>
|
>
|
||||||
保存
|
{{ financeRemarkSaving ? '保存中…' : '保存备注' }}
|
||||||
</button>
|
</button>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -161,6 +198,9 @@
|
|||||||
financeRemarkLoading: false,
|
financeRemarkLoading: false,
|
||||||
financeRemarkSaving: false,
|
financeRemarkSaving: false,
|
||||||
financeRemarkText: '',
|
financeRemarkText: '',
|
||||||
|
financeRemarkError: '',
|
||||||
|
financeRemarkViewportStyle: {},
|
||||||
|
financeRemarkWindowHeight: 0,
|
||||||
financeRemarkInstanceId: '',
|
financeRemarkInstanceId: '',
|
||||||
financeRemarkVersion: 0,
|
financeRemarkVersion: 0,
|
||||||
listVersion: 0,
|
listVersion: 0,
|
||||||
@@ -210,22 +250,68 @@
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
cancelFinanceRemark() {
|
cancelFinanceRemark() {
|
||||||
|
if (this.financeRemarkSaving) return
|
||||||
|
this.resetFinanceRemark()
|
||||||
|
},
|
||||||
|
resetFinanceRemark() {
|
||||||
|
if (this._financeRemarkViewportCleanup) this._financeRemarkViewportCleanup()
|
||||||
|
this._financeRemarkViewportCleanup = null
|
||||||
|
this.financeRemarkViewportStyle = {}
|
||||||
|
this.financeRemarkWindowHeight = 0
|
||||||
this.financeRemarkVersion++
|
this.financeRemarkVersion++
|
||||||
this.financeRemarkVisible = false
|
this.financeRemarkVisible = false
|
||||||
this.financeRemarkText = ''
|
this.financeRemarkText = ''
|
||||||
|
this.financeRemarkError = ''
|
||||||
this.financeRemarkInstanceId = ''
|
this.financeRemarkInstanceId = ''
|
||||||
this.financeRemarkLoading = false
|
this.financeRemarkLoading = false
|
||||||
this.financeRemarkSaving = false
|
this.financeRemarkSaving = false
|
||||||
},
|
},
|
||||||
clearFinanceAccess() {
|
clearFinanceAccess() {
|
||||||
this.canManageFinanceRemark = false
|
this.canManageFinanceRemark = false
|
||||||
this.cancelFinanceRemark()
|
this.resetFinanceRemark()
|
||||||
this.list = this.list.map((row) => {
|
this.list = this.list.map((row) => {
|
||||||
const sanitized = { ...row }
|
const sanitized = { ...row }
|
||||||
delete sanitized.financeRemark
|
delete sanitized.financeRemark
|
||||||
return sanitized
|
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) {
|
async openFinanceRemark(row) {
|
||||||
if (!this.canManageFinanceRemark || this.financeRemarkSaving) return
|
if (!this.canManageFinanceRemark || this.financeRemarkSaving) return
|
||||||
const id = String(row.instanceId || '').trim()
|
const id = String(row.instanceId || '').trim()
|
||||||
@@ -235,6 +321,7 @@
|
|||||||
this.financeRemarkInstanceId = id
|
this.financeRemarkInstanceId = id
|
||||||
this.financeRemarkVisible = true
|
this.financeRemarkVisible = true
|
||||||
this.financeRemarkLoading = true
|
this.financeRemarkLoading = true
|
||||||
|
this.watchFinanceRemarkViewport()
|
||||||
try {
|
try {
|
||||||
const result = await fetchOaFinanceRemark(id)
|
const result = await fetchOaFinanceRemark(id)
|
||||||
if (version !== this.financeRemarkVersion) return
|
if (version !== this.financeRemarkVersion) return
|
||||||
@@ -259,23 +346,21 @@
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
if (this.financeRemarkText.length > 512) {
|
if (this.financeRemarkText.length > 512) {
|
||||||
uni.showToast({ title: '财务备注最多 512 个字符', icon: 'none' })
|
this.financeRemarkError = '财务备注最多 512 个字符'
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const version = this.financeRemarkVersion
|
const version = this.financeRemarkVersion
|
||||||
|
this.financeRemarkError = ''
|
||||||
this.financeRemarkSaving = true
|
this.financeRemarkSaving = true
|
||||||
try {
|
try {
|
||||||
await fetchUpdateOaFinanceRemark(this.financeRemarkInstanceId, this.financeRemarkText)
|
await fetchUpdateOaFinanceRemark(this.financeRemarkInstanceId, this.financeRemarkText)
|
||||||
if (version !== this.financeRemarkVersion) return
|
if (version !== this.financeRemarkVersion) return
|
||||||
this.cancelFinanceRemark()
|
this.resetFinanceRemark()
|
||||||
uni.showToast({ title: '财务备注已保存', icon: 'success' })
|
uni.showToast({ title: '财务备注已保存', icon: 'success' })
|
||||||
await this.loadList(true)
|
await this.loadList(true)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (version !== this.financeRemarkVersion) return
|
if (version !== this.financeRemarkVersion) return
|
||||||
uni.showToast({
|
this.financeRemarkError = '保存失败,请稍后重试,输入已保留。'
|
||||||
title: (e && e.message) || '保存财务备注失败',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
} finally {
|
} finally {
|
||||||
if (version === this.financeRemarkVersion) this.financeRemarkSaving = false
|
if (version === this.financeRemarkVersion) this.financeRemarkSaving = false
|
||||||
}
|
}
|
||||||
@@ -462,46 +547,223 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.finance-remark__edit::after,
|
.finance-remark__edit::after,
|
||||||
.finance-remark__button::after {
|
.finance-dialog__button::after,
|
||||||
|
.finance-dialog__close::after {
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.finance-remark-panel {
|
.finance-dialog {
|
||||||
max-height: calc(100vh - 100px);
|
position: fixed;
|
||||||
overflow-y: auto;
|
z-index: 2000;
|
||||||
padding-bottom: env(safe-area-inset-bottom);
|
top: 0;
|
||||||
}
|
left: 0;
|
||||||
|
|
||||||
.finance-remark__input {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 240rpx;
|
height: 100vh;
|
||||||
padding: 16rpx;
|
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;
|
box-sizing: border-box;
|
||||||
border: 1rpx solid #dcebe4;
|
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', sans-serif;
|
||||||
border-radius: 12rpx;
|
|
||||||
font-size: 28rpx;
|
|
||||||
text-align: left;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.finance-remark__hint {
|
.finance-dialog__mask {
|
||||||
display: block;
|
position: fixed;
|
||||||
margin-top: 12rpx;
|
top: 0;
|
||||||
font-size: 24rpx;
|
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;
|
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;
|
margin: 0;
|
||||||
border-radius: 0;
|
|
||||||
min-height: 88rpx;
|
|
||||||
line-height: 88rpx;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 22px;
|
||||||
|
background: #f5f8f7;
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.finance-remark__button.confirm-dialog__btn--confirm {
|
.finance-dialog__close-icon {
|
||||||
background: #0a9f6d;
|
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;
|
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 {
|
.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 编译及可用平台检查。缺失环境如实记录,必要验证未完成前不关单。
|
SQLite 隔离集成测试真实角色/菜单/任务:允许、拒绝、跨租户、伪造租户、无本人已办、软删/停用、角色/职位并集、保存读取清空、超长、非字符串、其他中心不泄露。各端 UI 行为检查和构建;uni-app HBuilderX 编译及可用平台检查。缺失环境如实记录,必要验证未完成前不关单。
|
||||||
@@ -11,3 +11,13 @@
|
|||||||
- [x] 移动端验收方式:用户明确本次可用嵌套 H5 验证,不要求微信平台交互验证;采用已通过的 H5 编译产物交互结果。微信小程序编译通过,但未做模拟器/真机交互验证。
|
- [x] 移动端验收方式:用户明确本次可用嵌套 H5 验证,不要求微信平台交互验证;采用已通过的 H5 编译产物交互结果。微信小程序编译通过,但未做模拟器/真机交互验证。
|
||||||
- [x] 测试库迁移:用户确认当前远程库为测试库并授权执行;迁移后字段为可空 VARCHAR(512),为 2 个已办父菜单新增 2 个权限节点,角色授权数量未变。管理员须按需授予角色权限,其他环境仍需先迁移再更新应用。
|
- [x] 测试库迁移:用户确认当前远程库为测试库并授权执行;迁移后字段为可空 VARCHAR(512),为 2 个已办父菜单新增 2 个权限节点,角色授权数量未变。管理员须按需授予角色权限,其他环境仍需先迁移再更新应用。
|
||||||
- [x] 交付准备:任务分支及 PR #8 已推送,用户确认的验收方式与测试库迁移条件已满足。按持续授权核对最新 V2.0 后合并、同步本地主分支并关闭 #7;最终合并提交和关单结果记录在 Gitea 工单中。不打 tag、不部署、不启停服务。
|
- [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
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 界面与弹窗先经 Figma 原型确认
|
||||||
|
|
||||||
|
新页面、新弹窗及用户要求的视觉改版 SHALL 先在 Figma 创建可编辑原型,覆盖主要尺寸和交互状态,关联具体文件/节点,并经用户明确确认后再修改业务界面代码。简单界面、单字段弹窗、已有组件或赶时间不构成豁免。只有用户明确本任务不用生成 Figma 原型时才记录范围并跳过;用户沉默、工具生成成功或“代码完成再看”不能代替确认。原型确认不替代功能验证。
|
||||||
|
|
||||||
|
#### Scenario: 复用已有组件新增弹窗
|
||||||
|
|
||||||
|
- **WHEN** 新弹窗可以复用工程现有组件
|
||||||
|
- **THEN** 在原型中复用对应设计并向用户确认,确认后实现;已有明确确认的同版原型直接沿用,不重复询问。
|
||||||
|
|
||||||
|
#### Scenario: 原型服务不可用或尚未确认
|
||||||
|
|
||||||
|
- **WHEN** Figma 不可用或用户尚未确认原型,且没有明确豁免
|
||||||
|
- **THEN** 保持界面实现待处理,说明缺口并继续独立工作;不先编码再补原型。
|
||||||
|
|
||||||
|
#### Scenario: 明确豁免或非视觉变更
|
||||||
|
|
||||||
|
- **WHEN** 用户明确本任务不用 Figma,或任务仅为无布局变化的文案纠错、后台逻辑、恢复既有已确认设计
|
||||||
|
- **THEN** 按原任务规则实施与验证;原型豁免仅作用于用户指定范围,不推广到后续新界面。
|
||||||
|
|
||||||
### Requirement: Web 管理端以 Fresh Mint 规范为唯一视觉权威
|
### Requirement: Web 管理端以 Fresh Mint 规范为唯一视觉权威
|
||||||
|
|
||||||
`web-admin` 工程的界面实现 SHALL 以 `docs/web-admin-ui-spec-fresh-mint.md` 及其中引用的 `docs/fresh-mint-green.html` 为权威来源;主色链 SHALL 使用文档定义的 Mint 绿系(如 `--primary` `#10b981` 等),不得擅自替换为小程序品牌色。
|
`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
|
exports
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
function harness(platform) {
|
function harness(platform, overrides = {}) {
|
||||||
const calls = []
|
const calls = []
|
||||||
const hooks = {}
|
const hooks = {}
|
||||||
const api = {
|
const api = {
|
||||||
@@ -58,7 +58,7 @@ function harness(platform) {
|
|||||||
useMenuPerm: () => ({ hasMenuPerm: () => false }),
|
useMenuPerm: () => ({ hasMenuPerm: () => false }),
|
||||||
useUserStore: () => ({}),
|
useUserStore: () => ({}),
|
||||||
ElMessage: { success() {}, error() {}, warning() {} },
|
ElMessage: { success() {}, error() {}, warning() {} },
|
||||||
uni: { showToast() {}, navigateTo() {} },
|
uni: { showToast() {}, navigateTo() {}, getSystemInfoSync: () => ({ windowHeight: 800 }) },
|
||||||
VanEmpty: {},
|
VanEmpty: {},
|
||||||
getToken: () => 'token',
|
getToken: () => 'token',
|
||||||
forceRelogin() {},
|
forceRelogin() {},
|
||||||
@@ -73,7 +73,8 @@ function harness(platform) {
|
|||||||
fetchUpdateOaFinanceRemark: async (id, text) => {
|
fetchUpdateOaFinanceRemark: async (id, text) => {
|
||||||
calls.push(['put', id, text])
|
calls.push(['put', id, text])
|
||||||
return api.put(id, text)
|
return api.put(id, text)
|
||||||
}
|
},
|
||||||
|
...overrides
|
||||||
}
|
}
|
||||||
const path =
|
const path =
|
||||||
platform === 'web'
|
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.vm.canManageFinanceRemark, false)
|
||||||
assert.equal(h.calls.length, count)
|
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