feat(mobile): 登录修复 + 人名字典 + 留言板FAB + 流转算法重构
- login: catch补齐toast,修复密码错误静默失败 - detail: formatUserName渲染当前位置,dictVersion驱动响应式重绘 - detail: 留言板改为悬浮按钮+底部抽屉,未读徽标逻辑 - TaskSwipeCards: lanes终极递归算法,SPAWN不限层级开辟新Tab,先序push修复Tab顺序 - TreeCanvas: 父子相对坐标延伸算法,替代旧版奇偶距离递增
This commit is contained in:
@ -55,8 +55,10 @@ async function handleLogin() {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
uni.switchTab({ url: "/pages/scan/index" });
|
uni.switchTab({ url: "/pages/scan/index" });
|
||||||
}, 500);
|
}, 500);
|
||||||
} catch {
|
} catch (e) {
|
||||||
// request.js 已弹 toast
|
// request.js 对 /auth/login 的 401 直接 reject 不弹 toast,此处补齐
|
||||||
|
const detail = e?.data?.detail || "";
|
||||||
|
uni.showToast({ title: detail || "账号或密码错误", icon: "none", duration: 2000 });
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -134,40 +134,35 @@ export default {
|
|||||||
if (!this.product || !this.product.task_tree) return result;
|
if (!this.product || !this.product.task_tree) return result;
|
||||||
|
|
||||||
const mainLane = { _key: 'main', label: '主分支', cards: [], _cardIdx: 0 };
|
const mainLane = { _key: 'main', label: '主分支', cards: [], _cardIdx: 0 };
|
||||||
const isMainFn = (t) => !t.parent_task_id || t.task_type === 'TRANSFER' || t.task_type === 'RECOVERY';
|
|
||||||
|
|
||||||
const sortTasks = (tasks) => {
|
const sortTasks = (tasks) => {
|
||||||
if (!tasks) return [];
|
if (!tasks) return [];
|
||||||
return [...tasks].sort((a, b) => {
|
return [...tasks].sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
|
||||||
const aMain = isMainFn(a) ? -1 : 1;
|
|
||||||
const bMain = isMainFn(b) ? -1 : 1;
|
|
||||||
if (aMain !== bMain) return aMain - bMain;
|
|
||||||
return new Date(a.created_at) - new Date(b.created_at);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 🚀 终极递归收集算法
|
||||||
const followMain = (taskList, lane) => {
|
const followMain = (taskList, lane) => {
|
||||||
if (!taskList || !taskList.length) return;
|
if (!taskList || !taskList.length) return;
|
||||||
const sorted = sortTasks(taskList);
|
const sorted = sortTasks(taskList);
|
||||||
|
|
||||||
for (const t of sorted) {
|
for (const t of sorted) {
|
||||||
if (isMainFn(t)) {
|
// 1. 无条件入列:既然进到了这个 lane,就属于这个 lane 的卡片
|
||||||
lane.cards.push({ ...t, _key: t.id + '_' + lane._key, _isMain: lane._key === 'main' });
|
lane.cards.push({ ...t, _key: t.id + '_' + lane._key, _isMain: lane._key === 'main' });
|
||||||
if (t.child_tasks && t.child_tasks.length) {
|
|
||||||
const nextMain = t.child_tasks.filter(c =>
|
if (t.child_tasks && t.child_tasks.length) {
|
||||||
c.task_type === 'TRANSFER' || c.task_type === 'RECOVERY');
|
// 1. 同一分支线性延续
|
||||||
followMain(nextMain, lane);
|
const nextOnThisLane = t.child_tasks.filter(c =>
|
||||||
// SPAWN 子节点 → 新分支(含嵌套 spawn)
|
c.task_type === 'TRANSFER' || c.task_type === 'RECOVERY'
|
||||||
const spawns = t.child_tasks.filter(c => c.task_type === 'SPAWN');
|
);
|
||||||
for (const sc of spawns) {
|
followMain(nextOnThisLane, lane);
|
||||||
const blabel = this.branchLabelMap[sc.id] || '分支';
|
|
||||||
const branchLane = { _key: 'branch_' + sc.id, label: blabel, cards: [], _cardIdx: 0 };
|
// 2. 凡是 SPAWN,必定开辟新分支 (不限层级)
|
||||||
branchLane.cards.push({ ...sc, _key: sc.id + '_' + branchLane._key, _isMain: false });
|
const spawns = t.child_tasks.filter(c => c.task_type === 'SPAWN');
|
||||||
if (sc.child_tasks && sc.child_tasks.length) {
|
for (const sc of spawns) {
|
||||||
followMain(sc.child_tasks, branchLane);
|
const blabel = (this.branchLabelMap && this.branchLabelMap[sc.id]) || '协助分支';
|
||||||
}
|
const branchLane = { _key: 'branch_' + sc.id, label: blabel, cards: [], _cardIdx: 0 };
|
||||||
result.push(branchLane);
|
result.push(branchLane); // 先占坑:父分支排在前面
|
||||||
}
|
followMain([sc], branchLane); // 再递归:孙子分支自然排在后面
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -74,7 +74,7 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 🚀 十字星并发拓扑算法
|
// 🚀 父子相对坐标延伸算法
|
||||||
// ============================================================
|
// ============================================================
|
||||||
treeNodes() {
|
treeNodes() {
|
||||||
if (!this.product || !this.product.task_tree) return [];
|
if (!this.product || !this.product.task_tree) return [];
|
||||||
@ -93,16 +93,6 @@ export default {
|
|||||||
|
|
||||||
// 🚀 主干判定
|
// 🚀 主干判定
|
||||||
const isMainFn = (t) => !t.parent_task_id || t.task_type === 'TRANSFER' || t.task_type === 'RECOVERY';
|
const isMainFn = (t) => !t.parent_task_id || t.task_type === 'TRANSFER' || t.task_type === 'RECOVERY';
|
||||||
// 🚀 寻根算法
|
|
||||||
const getRootMainId = (t) => {
|
|
||||||
let curr = t;
|
|
||||||
while (curr.parent_task_id && flatMap[curr.parent_task_id]) {
|
|
||||||
const p = flatMap[curr.parent_task_id];
|
|
||||||
if (isMainFn(p)) return p.id;
|
|
||||||
curr = p;
|
|
||||||
}
|
|
||||||
return curr.parent_task_id || curr.id;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 分支编号
|
// 分支编号
|
||||||
const branchLabelMap = {};
|
const branchLabelMap = {};
|
||||||
@ -117,16 +107,14 @@ export default {
|
|||||||
const rootCopies = this.product.task_tree.map(t => flatMap[t.id]).filter(Boolean);
|
const rootCopies = this.product.task_tree.map(t => flatMap[t.id]).filter(Boolean);
|
||||||
rootCopies.forEach(t => { branchLabelMap[t.id] = '主分支'; traverse(t.child_tasks, ''); });
|
rootCopies.forEach(t => { branchLabelMap[t.id] = '主分支'; traverse(t.child_tasks, ''); });
|
||||||
|
|
||||||
// 🚀 真实父子分组(按直接 parent_task_id)
|
// 🚀 构建真实的全局 childMap(按 parent_task_id)
|
||||||
const subByParent = {};
|
const childMap = {};
|
||||||
allTasks.forEach(t => {
|
allTasks.forEach(t => {
|
||||||
if (t.parent_task_id) {
|
const pid = t.parent_task_id || '';
|
||||||
if (!subByParent[t.parent_task_id]) subByParent[t.parent_task_id] = [];
|
if (!childMap[pid]) childMap[pid] = [];
|
||||||
subByParent[t.parent_task_id].push(t);
|
childMap[pid].push(t);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
// 每个 parent 下按时间排序
|
Object.values(childMap).forEach(arr => arr.sort((a, b) => new Date(a.created_at) - new Date(b.created_at)));
|
||||||
Object.values(subByParent).forEach(arr => arr.sort((a, b) => new Date(a.created_at) - new Date(b.created_at)));
|
|
||||||
|
|
||||||
// 主管分类
|
// 主管分类
|
||||||
const mains = [];
|
const mains = [];
|
||||||
@ -135,67 +123,46 @@ export default {
|
|||||||
|
|
||||||
const nodes = [];
|
const nodes = [];
|
||||||
|
|
||||||
// 🚀 递归挂载:从父节点向外延伸 X,Y 保持与根主干同高
|
// 🚀 全新递归放置算法(相对坐标系)
|
||||||
const placeChildren = (parentNode, rootY) => {
|
const placeChildren = (parentNode, currentSide) => {
|
||||||
const children = subByParent[parentNode.id] || [];
|
const children = childMap[parentNode.id] || [];
|
||||||
if (!children.length) return;
|
children.forEach((child, index) => {
|
||||||
|
// 主干的第一层协助分支:均衡分发左右
|
||||||
// 父节点是主干 → 交替左右;父节点已有侧向 → 继承方向
|
let side = currentSide;
|
||||||
const parentIsMain = parentNode._isMain;
|
if (parentNode._isMain) {
|
||||||
let leftCount = 0, rightCount = 0;
|
side = index % 2 === 0 ? 'right' : 'left';
|
||||||
|
|
||||||
children.forEach((c) => {
|
|
||||||
let side, childX;
|
|
||||||
if (parentIsMain) {
|
|
||||||
// 主干直系子节点:交替左右,距离递增
|
|
||||||
side = leftCount <= rightCount ? 'left' : 'right';
|
|
||||||
if (side === 'left') leftCount++; else rightCount++;
|
|
||||||
const dist = side === 'left' ? leftCount : rightCount;
|
|
||||||
childX = side === 'left'
|
|
||||||
? CENTER_X - dist * (CARD_W + GAP_X)
|
|
||||||
: CENTER_X + dist * (CARD_W + GAP_X);
|
|
||||||
} else {
|
|
||||||
// 非主干子节点:继承父节点方向,继续向外延伸
|
|
||||||
side = parentNode._side;
|
|
||||||
childX = side === 'left'
|
|
||||||
? parentNode.x - (CARD_W + GAP_X)
|
|
||||||
: parentNode.x + (CARD_W + GAP_X);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🚀 核心:永远基于真实父亲 (parentNode) 的坐标向外延伸!
|
||||||
|
const childX = side === 'right'
|
||||||
|
? parentNode.x + CARD_W + GAP_X
|
||||||
|
: parentNode.x - CARD_W - GAP_X;
|
||||||
|
|
||||||
|
// 同级多子节点略微错开 Y 轴防重叠
|
||||||
|
const childY = parentNode.y + (index * (CARD_H + 20));
|
||||||
|
|
||||||
const childNode = {
|
const childNode = {
|
||||||
...c,
|
...child,
|
||||||
x: childX, y: rootY,
|
x: childX, y: childY,
|
||||||
_isMain: false, _side: side,
|
_isMain: false, _side: side,
|
||||||
_blabel: branchLabelMap[c.id] || '分支',
|
_blabel: branchLabelMap[child.id] || '分支',
|
||||||
_rootMainY: rootY,
|
_rootMainId: parentNode._isMain ? parentNode.id : parentNode._rootMainId,
|
||||||
_rootMainId: parentIsMain ? parentNode.id : parentNode._rootMainId,
|
|
||||||
};
|
};
|
||||||
nodes.push(childNode);
|
nodes.push(childNode);
|
||||||
flatMap[childNode.id] = childNode;
|
flatMap[childNode.id] = childNode;
|
||||||
|
|
||||||
// 递归挂载孙子节点
|
// 带着当前的方向(side)继续递归,保证孙子永远顺着儿子的方向长
|
||||||
placeChildren(childNode, rootY);
|
placeChildren(childNode, side);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 遍历主干
|
// 🚀 渲染入口
|
||||||
mains.forEach((m, mi) => {
|
mains.forEach((m, mi) => {
|
||||||
const rootY = mi * (CARD_H + GAP_Y) * 2;
|
const rootY = mi * (CARD_H + GAP_Y) * 2;
|
||||||
const mainNode = { ...m, x: CENTER_X, y: rootY, _isMain: true, _blabel: '主分支', _rootMainId: m.id };
|
const mainNode = { ...m, x: CENTER_X, y: rootY, _isMain: true, _blabel: '主分支', _rootMainId: m.id };
|
||||||
nodes.push(mainNode);
|
nodes.push(mainNode);
|
||||||
flatMap[mainNode.id] = mainNode;
|
flatMap[mainNode.id] = mainNode;
|
||||||
|
placeChildren(mainNode, null);
|
||||||
placeChildren(mainNode, rootY);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 遗留分支(父任务不在任何已渲染节点中)
|
|
||||||
const seenIds = new Set(nodes.map(n => n.id));
|
|
||||||
allTasks.forEach(t => {
|
|
||||||
if (!seenIds.has(t.id) && !isMainFn(t)) {
|
|
||||||
const legacyNode = { ...t, x: CENTER_X + CARD_W + GAP_X, y: nodes.length ? nodes[nodes.length - 1].y + CARD_H + GAP_Y : 0, _isMain: false, _blabel: '分支', _rootMainId: '' };
|
|
||||||
nodes.push(legacyNode);
|
|
||||||
flatMap[legacyNode.id] = legacyNode;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 全局偏移
|
// 全局偏移
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
<template v-if="product && !loading">
|
<template v-if="product && !loading">
|
||||||
<!-- 工作区模式:显示产品信息栏 -->
|
<!-- 工作区模式:显示产品信息栏 -->
|
||||||
<template v-if="currentMode === 'workspace'">
|
<template v-if="currentMode === 'workspace'">
|
||||||
|
<view :key="'prod-card-' + dictVersion">
|
||||||
<view class="overall-bar" @tap="handleOverallBarClick">
|
<view class="overall-bar" @tap="handleOverallBarClick">
|
||||||
<text class="overall-label">宏观状态</text>
|
<text class="overall-label">宏观状态</text>
|
||||||
<text :class="['overall-val', product.overall_status ? '' : 'overall-empty']">{{ product.overall_status || '未激活 — 点击发起首道工序' }}</text>
|
<text :class="['overall-val', product.overall_status ? '' : 'overall-empty']">{{ product.overall_status || '未激活 — 点击发起首道工序' }}</text>
|
||||||
@ -32,6 +33,7 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<view v-if="product && product.current_location_id === 'virtual_warehouse' && !hasMyActiveTask"
|
<view v-if="product && product.current_location_id === 'virtual_warehouse' && !hasMyActiveTask"
|
||||||
class="warehouse-transfer-banner" @tap="openWarehouseTransfer">
|
class="warehouse-transfer-banner" @tap="openWarehouseTransfer">
|
||||||
@ -47,28 +49,6 @@
|
|||||||
:key="'wa-' + dictVersion"
|
:key="'wa-' + dictVersion"
|
||||||
@action="handleTaskAction" @viewRecords="handleViewRecords" />
|
@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"
|
<TaskSwipeCards v-if="currentMode === 'swipe'" :product="product"
|
||||||
:key="'sw-' + dictVersion"
|
: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'" />
|
<TreeCanvas v-if="currentMode === 'tree'" :product="product" :key="'tc-' + dictVersion" @viewRecords="handleViewRecords" @back="currentMode = 'workspace'" @swipe="currentMode = 'swipe'" />
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 状态定调 -->
|
<!-- 状态定调 -->
|
||||||
@ -182,6 +163,37 @@
|
|||||||
</template>
|
</template>
|
||||||
</view>
|
</view>
|
||||||
</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>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -214,8 +226,10 @@ export default {
|
|||||||
messages: [],
|
messages: [],
|
||||||
// 🚀 字典版本号:驱动子组件强制重建,解决 userNameMap 非响应式问题
|
// 🚀 字典版本号:驱动子组件强制重建,解决 userNameMap 非响应式问题
|
||||||
dictVersion: 0,
|
dictVersion: 0,
|
||||||
|
showMsgDrawer: false,
|
||||||
newMsgText: '',
|
newMsgText: '',
|
||||||
bottomMsgId: '',
|
bottomMsgId: '',
|
||||||
|
lastMsgSeenAt: '',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
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; };
|
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;
|
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); },
|
onLoad(options) { this.loadUsers(); this.loadCurrentUser(); const sn = options.serial || ""; if (sn) this.doQuery(sn); },
|
||||||
methods: {
|
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 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); } },
|
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'; }); },
|
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())}`; },
|
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; }
|
.btn-add-branch::after { border: none; }
|
||||||
.field-label { font-size: 14px; font-weight: 600; color: #374151; margin-top: 10px; margin-bottom: 4px; }
|
.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-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-scroll-area { flex: 1; padding: 12px; overflow-y: auto; }
|
||||||
.mb-item { display: flex; margin-bottom: 16px; }
|
.mb-item { display: flex; margin-bottom: 16px; }
|
||||||
|
|||||||
Reference in New Issue
Block a user