重构: flex布局撑满全屏 + 记录区下沉到子树底部 + 底部Footer钉死

This commit is contained in:
2026-08-05 15:16:03 +08:00
parent 430c0d2342
commit 3b02c87067
2 changed files with 88 additions and 45 deletions

View File

@ -4,7 +4,7 @@
<!-- 卡片 -->
<view :class="['tnode-card', statusColor(task.status), { 'is-rework': task.is_rework }]">
<!-- 信息区 -->
<!-- 第一层任务名称标签状态 Badge负责人 -->
<view class="tnode-body">
<view class="tnode-name-row">
<text v-if="task.is_rework" class="tag tag-rework">返工</text>
@ -15,39 +15,7 @@
<text v-if="task.assignee_id">负责人: {{ task.assignee_id }}</text>
<text v-if="task.reject_reason" class="reject-reason">驳回: {{ task.reject_reason }}</text>
</view>
<!-- 进度记录 只展示最新一条 + 查看全部入口 -->
<view v-if="task.records && task.records.length" class="records-area">
<!-- 查看全部记录按钮 -->
<view class="records-bar" @tap.stop="$emit('viewRecords', task)">
<text class="records-bar-icon">📋</text>
<text class="records-bar-text">查看全部记录 ({{ task.records.length }})</text>
<text class="records-bar-arrow"></text>
</view>
<!-- 最新一条预览 -->
<view class="record-item">
<view class="record-top">
<text class="record-time">{{ formatTime(task.records[0].created_at) }}</text>
<view v-if="canEditRecord" class="record-actions">
<text class="rec-act" @tap.stop="$emit('editRecord', { task, record: task.records[0] })"></text>
<text class="rec-act" @tap.stop="confirmDelete(task.records[0])">🗑</text>
</view>
</view>
<text v-if="task.records[0].remark" class="record-remark">{{ task.records[0].remark }}</text>
<view v-if="task.records[0].images && task.records[0].images.length" class="record-images">
<image
v-for="(img, i) in task.records[0].images"
:key="i"
:src="imageUrl(img)"
class="record-thumb"
mode="aspectFill"
@tap.stop="previewImage(task.records[0].images, i)"
/>
</view>
</view>
</view>
</view>
<!-- 状态标签 -->
<text :class="['badge', statusColor(task.status)]">{{ statusLabel(task.status) }}</text>
</view>
@ -64,7 +32,7 @@
@tap.stop="$emit('action', { task, type: 'record' })">📝 记录/拍照</button>
</view>
<!-- 递归子树 -->
<!-- 第二层递归子任务 -->
<view v-if="task.child_tasks && task.child_tasks.length" class="tnode-children">
<TaskTreeNode
v-for="(child, idx) in task.child_tasks"
@ -79,6 +47,37 @@
@viewRecords="(t) => $emit('viewRecords', t)"
/>
</view>
<!-- 第三层最底部进度记录 最新一条 + 查看全部入口 -->
<view v-if="task.records && task.records.length" class="records-area">
<!-- 查看全部记录按钮 -->
<view class="records-bar" @tap.stop="$emit('viewRecords', task)">
<text class="records-bar-icon">📋</text>
<text class="records-bar-text">查看全部记录 ({{ task.records.length }})</text>
<text class="records-bar-arrow"></text>
</view>
<!-- 最新一条预览 -->
<view class="record-item">
<view class="record-top">
<text class="record-time">{{ formatTime(task.records[0].created_at) }}</text>
<view v-if="canEditRecord" class="record-actions">
<text class="rec-act" @tap.stop="$emit('editRecord', { task, record: task.records[0] })"></text>
<text class="rec-act" @tap.stop="confirmDelete(task.records[0])">🗑</text>
</view>
</view>
<text v-if="task.records[0].remark" class="record-remark">{{ task.records[0].remark }}</text>
<view v-if="task.records[0].images && task.records[0].images.length" class="record-images">
<image
v-for="(img, i) in task.records[0].images"
:key="i"
:src="imageUrl(img)"
class="record-thumb"
mode="aspectFill"
@tap.stop="previewImage(task.records[0].images, i)"
/>
</view>
</view>
</view>
</view>
</template>

View File

@ -1,5 +1,5 @@
<template>
<view class="page">
<view class="page-container">
<!-- ======== 加载/错误 ======== -->
<view v-if="loading" class="loading">加载中...</view>
<view v-if="error" class="error-box">{{ error }}</view>
@ -56,13 +56,13 @@
<!-- ================================================================ -->
<!-- 任务树撑满剩余空间可滚动 -->
<!-- ================================================================ -->
<view class="card card-tree">
<view class="card-header">
<text class="card-title">🌿 任务流转树</text>
<text class="task-count">{{ countTasks(product.task_tree) }} 任务</text>
</view>
<view class="tree-scroll-area">
<view class="card card-tree">
<view class="card-header">
<text class="card-title">🌿 任务流转树</text>
<text class="task-count">{{ countTasks(product.task_tree) }} 个任务</text>
</view>
<view class="tree-scroll">
<!-- 0任务 发起首道工序 -->
<view v-if="!product.task_tree || !product.task_tree.length" class="zero-task">
<text class="zero-icon">📋</text>
@ -86,6 +86,24 @@
/>
</view>
</view>
<!-- ================================================================ -->
<!-- 底部操作栏钉死不随滚动 -->
<!-- ================================================================ -->
<view v-if="activeTask" class="footer-actions">
<button v-if="activeTask.status === 'WIP'" class="footer-btn footer-transfer"
@tap="handleTaskAction({ task: activeTask, type: 'transfer' })">
🔄 完工转交
</button>
<button v-if="activeTask.status === 'WIP'" class="footer-btn footer-record"
@tap="handleTaskAction({ task: activeTask, type: 'record' })">
📝 记录/拍照
</button>
<button v-if="activeTask.status === 'PENDING'" class="footer-btn footer-receive"
@tap="handleTaskAction({ task: activeTask, type: 'receive' })">
接收任务
</button>
</view>
</template>
<!-- ================================================================ -->
@ -335,6 +353,21 @@ export default {
const u = this.userOptions.find(u => u.id === this.transferForm.selectedUserId);
return u ? u.name : "";
},
activeTask() {
// 查找树中第一个 WIP 或 PENDING 任务作为底部 Footer 的操作目标
const findActive = (tasks) => {
if (!tasks) return null;
for (const t of tasks) {
if (t.status === 'WIP' || t.status === 'PENDING') return t;
if (t.child_tasks && t.child_tasks.length) {
const found = findActive(t.child_tasks);
if (found) return found;
}
}
return null;
};
return this.product ? findActive(this.product.task_tree) : null;
},
},
onLoad(options) {
this.loadUsers();
@ -661,7 +694,7 @@ export default {
</script>
<style scoped>
.page { height: 100vh; display: flex; flex-direction: column; padding: 16px; padding-bottom: 0; overflow: hidden; box-sizing: border-box; }
.page-container { height: 100vh; display: flex; flex-direction: column; background-color: #f3f4f6; box-sizing: border-box; padding: 16px; padding-bottom: 0; overflow: hidden; }
.loading { text-align: center; padding: 48px 0; color: #6b7280; }
.error-box { padding: 12px; border-radius: 10px; background: #fef2f2; color: #dc2626; font-size: 13px; border: 1px solid #fecaca; }
@ -675,10 +708,10 @@ export default {
/* 卡片 */
.card { background: #fff; border-radius: 12px; padding: 14px; margin-bottom: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); flex-shrink: 0; }
/* 任务树卡片 → 撑满剩余空间 */
.card-tree { flex: 1; display: flex; flex-direction: column; min-height: 0; margin-bottom: 0; overflow: hidden; }
/* 任务树滚动区域 → 撑满剩余空间 */
.tree-scroll-area { flex: 1; overflow-y: auto; min-height: 0; -webkit-overflow-scrolling: touch; }
.card-tree { margin-bottom: 0; }
.card-tree .card-header { flex-shrink: 0; }
.tree-scroll { flex: 1; overflow-y: auto; min-height: 0; -webkit-overflow-scrolling: touch; }
.card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
.card-header-right { display: flex; align-items: center; gap: 8px; }
.card-title { font-size: 15px; font-weight: 700; }
@ -762,6 +795,17 @@ export default {
background: #f9fafb; color: #6b7280; font-size: 14px; line-height: 42px; margin: 8px 0; }
.btn-upload[disabled] { opacity: 0.5; }
/* 底部操作栏 (钉死在屏幕最下方) */
.footer-actions {
display: flex; gap: 20rpx; padding: 20rpx 30rpx; padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background: #ffffff; box-shadow: 0 -2rpx 10rpx rgba(0,0,0,0.05); z-index: 99; flex-shrink: 0;
}
.footer-btn { flex: 1; height: 88rpx; border: none; border-radius: 12rpx; font-size: 30rpx; font-weight: 700; line-height: 88rpx; }
.footer-btn::after { border: none; }
.footer-transfer { background: #dcfce7; color: #16a34a; }
.footer-record { background: #eff6ff; color: #2563eb; }
.footer-receive { background: #dbeafe; color: #1d4ed8; }
/* Picker 选择器 */
.form-item { margin: 10px 0; }
.form-label { font-size: 14px; font-weight: 600; color: #374151; display: block; margin-bottom: 4px; }