feat(mobile): 登录修复 + 人名字典 + 留言板FAB + 流转算法重构
- login: catch补齐toast,修复密码错误静默失败 - detail: formatUserName渲染当前位置,dictVersion驱动响应式重绘 - detail: 留言板改为悬浮按钮+底部抽屉,未读徽标逻辑 - TaskSwipeCards: lanes终极递归算法,SPAWN不限层级开辟新Tab,先序push修复Tab顺序 - TreeCanvas: 父子相对坐标延伸算法,替代旧版奇偶距离递增
This commit is contained in:
@ -6,6 +6,7 @@
|
||||
<template v-if="product && !loading">
|
||||
<!-- 工作区模式:显示产品信息栏 -->
|
||||
<template v-if="currentMode === 'workspace'">
|
||||
<view :key="'prod-card-' + dictVersion">
|
||||
<view class="overall-bar" @tap="handleOverallBarClick">
|
||||
<text class="overall-label">宏观状态</text>
|
||||
<text :class="['overall-val', product.overall_status ? '' : 'overall-empty']">{{ product.overall_status || '未激活 — 点击发起首道工序' }}</text>
|
||||
@ -32,6 +33,7 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="product && product.current_location_id === 'virtual_warehouse' && !hasMyActiveTask"
|
||||
class="warehouse-transfer-banner" @tap="openWarehouseTransfer">
|
||||
@ -47,28 +49,6 @@
|
||||
:key="'wa-' + dictVersion"
|
||||
@action="handleTaskAction" @viewRecords="handleViewRecords" />
|
||||
|
||||
<!-- 💬 协同留言板 — 产品级功能,所有模式均可见 -->
|
||||
<view class="message-board">
|
||||
<view class="mb-title">💬 协同留言板</view>
|
||||
<scroll-view scroll-y class="mb-scroll-area" :scroll-into-view="bottomMsgId" scroll-with-animation>
|
||||
<view v-for="msg in messages" :key="msg.id" class="mb-item" :id="'msg-' + msg.id">
|
||||
<view class="mb-avatar">{{ formatUserAvatar(msg.operator_id) }}</view>
|
||||
<view class="mb-content-wrapper">
|
||||
<view class="mb-header-info">
|
||||
<text class="mb-name">{{ formatUserName(msg.operator_id) }}</text>
|
||||
<text class="mb-time">{{ fmtMsgTime(msg.created_at) }}</text>
|
||||
</view>
|
||||
<view class="mb-bubble">{{ msg.content }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view id="msg-bottom" class="mb-bottom-anchor"></view>
|
||||
</scroll-view>
|
||||
<view class="mb-input-bar">
|
||||
<input v-model="newMsgText" class="mb-input" placeholder="输入交接注意事项..." confirm-type="send" @confirm="submitMessage" />
|
||||
<view :class="['mb-send-btn', !newMsgText.trim() ? 'btn-disabled' : '']" @tap="submitMessage">发送</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 📇 流转卡片:探探式单张滑动 -->
|
||||
<TaskSwipeCards v-if="currentMode === 'swipe'" :product="product"
|
||||
:key="'sw-' + dictVersion"
|
||||
@ -77,6 +57,7 @@
|
||||
|
||||
<!-- 🌳 流转树:全屏独立视图 -->
|
||||
<TreeCanvas v-if="currentMode === 'tree'" :product="product" :key="'tc-' + dictVersion" @viewRecords="handleViewRecords" @back="currentMode = 'workspace'" @swipe="currentMode = 'swipe'" />
|
||||
|
||||
</template>
|
||||
|
||||
<!-- 状态定调 -->
|
||||
@ -182,6 +163,37 @@
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 💬 留言悬浮按钮 -->
|
||||
<view class="msg-fab" @tap="openMsgDrawer">
|
||||
<text class="msg-fab-icon">💬</text>
|
||||
<text v-if="msgUnreadCount" class="msg-fab-badge">{{ msgUnreadCount }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 💬 留言板底部抽屉 -->
|
||||
<view v-if="showMsgDrawer" class="msg-drawer-overlay" @tap="closeMsgDrawer">
|
||||
<view class="message-board-drawer" @tap.stop>
|
||||
<view class="mb-drawer-handle"></view>
|
||||
<view class="mb-title">💬 协同留言板</view>
|
||||
<scroll-view scroll-y class="mb-scroll-area" :scroll-into-view="bottomMsgId" scroll-with-animation>
|
||||
<view v-for="msg in messages" :key="msg.id" class="mb-item" :id="'msg-' + msg.id">
|
||||
<view class="mb-avatar">{{ formatUserAvatar(msg.operator_id) }}</view>
|
||||
<view class="mb-content-wrapper">
|
||||
<view class="mb-header-info">
|
||||
<text class="mb-name">{{ formatUserName(msg.operator_id) }}</text>
|
||||
<text class="mb-time">{{ fmtMsgTime(msg.created_at) }}</text>
|
||||
</view>
|
||||
<view class="mb-bubble">{{ msg.content }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view id="msg-bottom" class="mb-bottom-anchor"></view>
|
||||
</scroll-view>
|
||||
<view class="mb-input-bar">
|
||||
<input v-model="newMsgText" class="mb-input" placeholder="输入交接注意事项..." confirm-type="send" @confirm="submitMessage" />
|
||||
<view :class="['mb-send-btn', !newMsgText.trim() ? 'btn-disabled' : '']" @tap="submitMessage">发送</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@ -214,8 +226,10 @@ export default {
|
||||
messages: [],
|
||||
// 🚀 字典版本号:驱动子组件强制重建,解决 userNameMap 非响应式问题
|
||||
dictVersion: 0,
|
||||
showMsgDrawer: false,
|
||||
newMsgText: '',
|
||||
bottomMsgId: '',
|
||||
lastMsgSeenAt: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -229,6 +243,7 @@ export default {
|
||||
const find = (tasks) => { if (!tasks) return false; for (const t of tasks) { if ((t.status === 'WIP' || t.status === 'PENDING') && (t.assignee_id == this.currentUserId || t.assignee_id == this.currentUsername)) return true; if (find(t.child_tasks)) return true; } return false; };
|
||||
return this.product ? find(this.product.task_tree) : false;
|
||||
},
|
||||
msgUnreadCount() { if (!this.lastMsgSeenAt) return this.messages.length; return this.messages.filter(m => m.created_at > this.lastMsgSeenAt).length; },
|
||||
},
|
||||
onLoad(options) { this.loadUsers(); this.loadCurrentUser(); const sn = options.serial || ""; if (sn) this.doQuery(sn); },
|
||||
methods: {
|
||||
@ -307,6 +322,8 @@ export default {
|
||||
// 💬 留言板
|
||||
async fetchMessages() { if (!this.product?.id) return; try { const res = await get(`/products/${this.product.id}/messages`); this.messages = res || []; this.scrollToBottom(); } catch (e) { console.error('获取留言失败', e); } },
|
||||
async submitMessage() { const content = this.newMsgText.trim(); if (!content) return; this.newMsgText = ''; const tempId = 'temp_' + Date.now(); const tempMsg = { id: tempId, operator_id: this.currentUsername || this.currentUserId || '?', content, created_at: new Date().toISOString() }; this.messages.push(tempMsg); this.scrollToBottom(); try { await post(`/products/${this.product.id}/messages`, { operator_id: this.currentUsername || this.currentUserId, content }); this.fetchMessages(); } catch (e) { uni.showToast({ title: '发送失败', icon: 'none' }); this.messages = this.messages.filter(m => m.id !== tempId); } },
|
||||
openMsgDrawer() { this.showMsgDrawer = true; this.$nextTick(() => { this.scrollToBottom(); }); },
|
||||
closeMsgDrawer() { const last = this.messages[this.messages.length - 1]; this.lastMsgSeenAt = last ? last.created_at : new Date().toISOString(); this.showMsgDrawer = false; },
|
||||
scrollToBottom() { this.$nextTick(() => { this.bottomMsgId = 'msg-bottom'; }); },
|
||||
fmtMsgTime(d) { if (!d) return ''; const dt = new Date(d); const pad = (n) => String(n).padStart(2, '0'); return `${pad(dt.getMonth()+1)}-${pad(dt.getDate())} ${pad(dt.getHours())}:${pad(dt.getMinutes())}`; },
|
||||
},
|
||||
@ -367,8 +384,15 @@ export default {
|
||||
.btn-add-branch::after { border: none; }
|
||||
.field-label { font-size: 14px; font-weight: 600; color: #374151; margin-top: 10px; margin-bottom: 4px; }
|
||||
|
||||
/* 💬 协同留言板 */
|
||||
.message-board { margin: 16px; background: #ffffff; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); display: flex; flex-direction: column; height: 400px; }
|
||||
/* 💬 留言悬浮按钮 */
|
||||
.msg-fab { position: fixed; right: 20px; bottom: 100px; z-index: 99; width: 50px; height: 50px; border-radius: 25px; background: #3b82f6; color: #fff; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 12px rgba(59,130,246,0.4); }
|
||||
.msg-fab-icon { font-size: 22px; }
|
||||
.msg-fab-badge { position: absolute; top: -4px; right: -4px; min-width: 18px; height: 18px; border-radius: 9px; background: #ef4444; color: #fff; font-size: 10px; font-weight: 700; display: flex; align-items: center; justify-content: center; padding: 0 5px; }
|
||||
|
||||
/* 💬 留言板底部抽屉 */
|
||||
.msg-drawer-overlay { position: fixed; inset: 0; z-index: 200; background: rgba(0,0,0,0.45); display: flex; align-items: flex-end; justify-content: center; }
|
||||
.message-board-drawer { height: 65vh; display: flex; flex-direction: column; background: #fff; border-radius: 16px 16px 0 0; width: 100%; max-width: 480px; }
|
||||
.mb-drawer-handle { width: 40px; height: 4px; border-radius: 2px; background: #d1d5db; margin: 8px auto; flex-shrink: 0; }
|
||||
.mb-title { font-size: 14px; font-weight: bold; padding: 12px 16px; border-bottom: 1px solid #f3f4f6; color: #374151; flex-shrink: 0; }
|
||||
.mb-scroll-area { flex: 1; padding: 12px; overflow-y: auto; }
|
||||
.mb-item { display: flex; margin-bottom: 16px; }
|
||||
|
||||
Reference in New Issue
Block a user