feat: 🚀 生产入库

This commit is contained in:
2025-07-04 17:18:03 +08:00
parent a037d66c08
commit a6519ae290
4 changed files with 546 additions and 6 deletions

View File

@@ -85,7 +85,7 @@ export const COLUMNS = [
}, },
{ {
align: "left", align: "left",
label: "已出库数", label: "已出库数",
prop: "realityQty", prop: "realityQty",
width: "120" width: "120"
}, },

132
src/views/login/index2.vue Normal file
View File

@@ -0,0 +1,132 @@
<template>
<el-main></el-main>
</template>
<script setup lang="ts">
//useRouter
import { useRoute } from "vue-router";
import { useMsg } from "@/hooks/useMsg";
//登录请求接口
import { loginApi } from "@/api/modules/login";
//全局状态
import { getGlobalStatusApi } from "@/api/modules/global";
//全局仓库
import { getStockApi } from "@/api/modules/global";
//用户信息存储
import { useUserStore } from "@/stores/modules/user";
import { usePathUrl } from "@/hooks/usePathUrl";
//全局状态
import { useStatusStore } from "@/stores/modules/status";
const userStore = useUserStore();
const statusStore = useStatusStore();
// 路由
const $route = useRoute();
const $router = useRouter();
const getGlobalStatus = async () => {
const result = await getGlobalStatusApi();
if (result.status === 200) {
const { data } = result;
//设置全局状态
statusStore.setGlobalStatus(data);
}
};
const getStock = async () => {
const result = await getStockApi();
if (result.status === 200) {
const { data } = result;
data.forEach((item: any) => {
item.code = item.code + "_" + "$" + item.erpOrgCode;
});
//设置全局仓库
userStore.setWarehouse(data);
}
};
const getUcOnline = (phpToken: any, refreshToken: any) => {
let httpUrl = import.meta.env.VITE_SINGLE_URL + "uc/online";
fetch(httpUrl, {
method: "POST",
credentials: "include",
keepalive: true,
headers: {
Authorization: phpToken,
"Refresh-Authorization": refreshToken
}
});
};
const getUcOffline = (redirectUrl: any) => {
let httpUrl = import.meta.env.VITE_SINGLE_URL + "uc/offline";
fetch(httpUrl, {
method: "POST",
credentials: "include",
keepalive: true,
headers: {
Authorization: userStore.phpToken,
"Refresh-Authorization": userStore.refreshToken
}
})
.then((res: any) => {
if (res.status === 200) {
useMsg("success", "退出登录成功 ");
setTimeout(() => {
location.href = redirectUrl;
}, 300);
}
})
.catch(() => {
useMsg("error", "退出登录失败,请稍后再试 ");
});
};
// 设置用户数据
const setUserData = (data: any) => {
// 组装token
let newUserToken = data.tokenInfo.tokenType + " " + data.tokenInfo.token;
let RefreshToken = data.tokenInfo.tokenType + " " + data.tokenInfo.refreshToken;
let phpToken = data.tokenInfo.tokenType + " " + data.tokenInfo.phpToken;
// 设置token
userStore.setToken(newUserToken);
userStore.setRefreshToken(RefreshToken);
userStore.setPhpToken(phpToken);
// 设置用户信息
userStore.setUserInfo(data.userInfo);
// 设置组织id
userStore.setOrgId(data.userInfo.orgId);
getUcOnline(phpToken, RefreshToken);
getGlobalStatus();
getStock();
//跳转
setTimeout(() => {
$router.push({ path: "/" });
}, 500);
};
// 登录
const loginHttp = async (code: any) => {
const result: Record<string, any> = await loginApi({ code: code });
if (result.status === 200) {
setUserData(result.data);
} else {
getUcOffline(usePathUrl());
}
};
// 登录前的判断
const login = () => {
const { code } = $route.query;
// 没有code直接跳转到登录页
if (!code) {
getUcOffline(usePathUrl());
return;
}
// 有code就登录请求
loginHttp(code);
};
login();
</script>
<style lang="scss">
.el-main {
height: 100vh;
}
</style>

411
src/views/login/index3.vue Normal file
View File

@@ -0,0 +1,411 @@
<template>
<div class="curve-container relative w-full h-full overflow-hidden">
<svg :width="svgWidth" :height="svgHeight">
<!-- 绘制曲线 -->
<path
v-for="(segment, index) in curveSegments"
:key="index"
:d="generateCurvePath(segment.startNode, segment.endNode)"
:stroke="segment.color"
stroke-width="14"
stroke-linecap="round"
fill="none"
/>
<!-- 绘制节点 -->
<circle
v-for="(node, index) in nodes"
:key="index"
:cx="node.x"
:cy="node.y"
:r="isHovered[index] ? hoverRadius : nodeRadius"
:fill="isHovered[index] ? hoverFill : nodeFill"
:stroke="isHovered[index] ? hoverStroke : nodeStroke"
:stroke-width="isHovered[index] ? hoverStrokeWidth : nodeStrokeWidth"
:filter="isHovered[index] ? 'drop-shadow(0 0 5px rgba(0,0,0,0.3))' : 'none'"
@mouseenter="handleNodeMouseEnter(index)"
@mouseleave="handleNodeMouseLeave(index)"
@mousedown="startDragging(index, $event)"
cursor="pointer"
/>
<!-- 绘制需求池背景 -->
<rect
x="20"
y="80"
width="140"
height="220"
rx="12"
ry="12"
fill="#f0f7ff"
stroke="#aac5e8"
stroke-width="4"
opacity="0.9"
filter="drop-shadow(0 4px 6px rgba(0,0,0,0.1))"
/>
<rect
x="700"
y="80"
width="120"
height="220"
rx="12"
ry="12"
fill="#e6f5ef"
stroke="#98d7c2"
stroke-width="2"
opacity="0.9"
filter="drop-shadow(0 4px 6px rgba(0,0,0,0.1))"
/>
<!-- 绘制需求池标签 -->
<text x="80" y="65" text-anchor="middle" font-size="14" fill="#2c6aa0" font-weight="bold">待处理</text>
<text x="760" y="65" text-anchor="middle" font-size="14" fill="#278865" font-weight="bold">已完成</text>
<!-- 绘制需求池中的小节点 -->
<circle
v-for="(poolNode, index) in poolNodes"
:key="index"
:cx="poolNode.isJumping ? poolNode.jumpPosition.x : constrainedPoolNodePosition[index].x"
:cy="poolNode.isJumping ? poolNode.jumpPosition.y : constrainedPoolNodePosition[index].y + getBounceOffset(index)"
:r="poolNodeRadius"
:fill="poolNode.fill"
:stroke="poolNode.stroke"
:stroke-width="1.5"
:filter="
poolNode.isJumping
? 'drop-shadow(0 0 8px rgba(0,0,0,0.5))'
: poolNode.isHovered
? 'drop-shadow(0 0 6px rgba(0,0,0,0.35))'
: 'none'
"
@mouseenter="handlePoolNodeMouseEnter(index)"
@mouseleave="handlePoolNodeMouseLeave(index)"
cursor="pointer"
/>
<!-- 需求池装饰元素 -->
<g v-if="!isMobileView">
<path d="M30,80 L30,300" stroke="#aac5e8" stroke-width="1" stroke-dasharray="5,5" opacity="0.5" />
<path d="M110,80 L110,300" stroke="#aac5e8" stroke-width="1" stroke-dasharray="5,5" opacity="0.5" />
<path d="M710,80 L710,300" stroke="#98d7c2" stroke-width="1" stroke-dasharray="5,5" opacity="0.5" />
<path d="M790,80 L790,300" stroke="#98d7c2" stroke-width="1" stroke-dasharray="5,5" opacity="0.5" />
</g>
</svg>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, onUnmounted } from "vue";
const svgWidth = ref(800);
const svgHeight = ref(400);
// 节点样式配置
const nodeRadius = ref(8);
const hoverRadius = ref(12);
const nodeFill = ref("#fff");
const hoverFill = ref("#e8f4ff");
const nodeStroke = ref("#3498db");
const hoverStroke = ref("#2980b9");
const nodeStrokeWidth = ref(2.5);
const hoverStrokeWidth = ref(3.5);
// 需求池节点样式配置
const poolNodeRadius = ref(6);
// 节点数据
const nodes = ref([
{ x: 150, y: 220 },
{ x: 350, y: 180 },
{ x: 550, y: 250 },
{ x: 750, y: 220 }
]);
// 曲线分段信息
const curveSegments = ref([
{ startNode: 0, endNode: 1, color: "#3498db" },
{ startNode: 1, endNode: 2, color: "#2ecc71" },
{ startNode: 2, endNode: 3, color: "#3498db" }
]);
// 需求池节点数据
const poolNodes = ref([
{ poolIndex: 0, fill: "#3498db", stroke: "#2980b9", isHovered: false, isJumping: false },
{ poolIndex: 0, fill: "#e74c3c", stroke: "#c0392b", isHovered: false, isJumping: false },
{ poolIndex: 0, fill: "#9b59b6", stroke: "#8e44ad", isHovered: false, isJumping: false },
{ poolIndex: 0, fill: "#e67e22", stroke: "#d35400", isHovered: false, isJumping: false },
{ poolIndex: 0, fill: "#f1c40f", stroke: "#f39c12", isHovered: false, isJumping: false },
{ poolIndex: 0, fill: "#1abc9c", stroke: "#16a085", isHovered: false, isJumping: false },
{ poolIndex: 0, fill: "#3498db", stroke: "#2980b9", isHovered: false, isJumping: false },
{ poolIndex: 0, fill: "#e74c3c", stroke: "#c0392b", isHovered: false, isJumping: false },
{ poolIndex: 1, fill: "#1abc9c", stroke: "#16a085", isHovered: false, isJumping: false },
{ poolIndex: 1, fill: "#3498db", stroke: "#2980b9", isHovered: false, isJumping: false },
{ poolIndex: 1, fill: "#95a5a6", stroke: "#7f8c8d", isHovered: false, isJumping: false }
]);
// 跟踪鼠标悬停状态
const isHovered = reactive(nodes.value.map(() => false));
// 响应式视图标志
const isMobileView = ref(false);
// 动画相关
const bounceOffsets = ref(poolNodes.value.map(() => 0));
let animationFrameId = null;
let lastTime = 0;
let jumpTimer = null;
// 存储约束后的节点位置(避免超出需求池)
const constrainedPoolNodePosition = ref(poolNodes.value.map(() => ({ x: 0, y: 0 })));
// 获取需求池节点位置(带边界约束)
const getPoolNodePosition = (poolIndex, nodeIndex) => {
const poolX = poolIndex === 0 ? 80 : 760;
const poolY = 100;
const nodesPerColumn = 4;
const column = Math.floor(nodeIndex / nodesPerColumn);
const row = nodeIndex % nodesPerColumn;
const spacingX = 25;
const spacingY = 25;
// 基础位置计算
let baseX = poolX + (column - 1) * spacingX;
let baseY = poolY + row * spacingY;
// 需求池边界(考虑节点半径)
const nodeRadius = poolNodeRadius.value;
const poolLeft = poolIndex === 0 ? 20 + nodeRadius : 700 + nodeRadius;
const poolRight = poolIndex === 0 ? 20 + 120 - nodeRadius : 700 + 120 - nodeRadius;
const poolTop = 80 + nodeRadius;
const poolBottom = 80 + 220 - nodeRadius;
// 边界约束逻辑
const constrainedX = Math.max(poolLeft, Math.min(baseX, poolRight));
const constrainedY = Math.max(poolTop, Math.min(baseY, poolBottom));
return { x: constrainedX, y: constrainedY };
};
// 获取节点跳动偏移量
const getBounceOffset = index => {
return bounceOffsets.value[index];
};
// 动态生成曲线路径
const generateCurvePath = (startNodeIndex, endNodeIndex) => {
const start = nodes.value[startNodeIndex];
const end = nodes.value[endNodeIndex];
const dx = end.x - start.x;
const dy = end.y - start.y;
const controlDistance = Math.max(Math.abs(dx), Math.abs(dy)) * 0.3;
const cp1x = start.x + dx * 0.3;
const cp1y = start.y + dy * 0.2 - controlDistance;
const cp2x = start.x + dx * 0.7;
const cp2y = start.y + dy * 0.8 + controlDistance;
return `M${start.x},${start.y} C${cp1x},${cp1y} ${cp2x},${cp2y} ${end.x},${end.y}`;
};
// 处理节点鼠标事件
const handleNodeMouseEnter = index => {
isHovered[index] = true;
};
const handleNodeMouseLeave = index => {
isHovered[index] = false;
};
// 处理需求池节点鼠标事件
const handlePoolNodeMouseEnter = index => {
poolNodes.value[index].isHovered = true;
};
const handlePoolNodeMouseLeave = index => {
poolNodes.value[index].isHovered = false;
};
// 开始节点自动跳入曲线
const startAutoJumping = () => {
const minInterval = 3000;
const maxInterval = 5000;
const jumpRandomNode = () => {
const leftPoolNodes = poolNodes.value.filter(node => node.poolIndex === 0 && !node.isJumping);
if (leftPoolNodes.length > 0) {
const randomIndex = Math.floor(Math.random() * leftPoolNodes.length);
const nodeToJump = leftPoolNodes[randomIndex];
const poolNodeIndex = poolNodes.value.indexOf(nodeToJump);
jumpNodeToCurve(poolNodeIndex);
}
jumpTimer = setTimeout(jumpRandomNode, Math.random() * (maxInterval - minInterval) + minInterval);
};
jumpTimer = setTimeout(jumpRandomNode, Math.random() * (maxInterval - minInterval) + minInterval);
};
// 节点跳入曲线动画
const jumpNodeToCurve = poolNodeIndex => {
const poolNode = poolNodes.value[poolNodeIndex];
if (poolNode.isJumping) return;
const targetNodeIndex = Math.floor(Math.random() * nodes.value.length);
const targetNode = nodes.value[targetNodeIndex];
const startPos = getPoolNodePosition(poolNode.poolIndex, poolNodeIndex);
startPos.y += getBounceOffset(poolNodeIndex);
poolNode.isJumping = true;
poolNode.jumpPosition = { ...startPos };
poolNode.jumpProgress = 0;
poolNode.targetNodeIndex = targetNodeIndex;
const startTime = Date.now();
const duration = 1500;
const animateJump = () => {
if (!poolNode.isJumping) return;
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
const easeOutCubic = t => 1 - Math.pow(1 - t, 3);
const easedProgress = easeOutCubic(progress);
poolNode.jumpPosition.x = startPos.x + (targetNode.x - startPos.x) * easedProgress;
poolNode.jumpPosition.y = startPos.y + (targetNode.y - startPos.y) * easedProgress;
if (progress < 1) {
requestAnimationFrame(animateJump);
} else {
completeNodeJump(poolNodeIndex);
}
};
requestAnimationFrame(animateJump);
};
// 完成节点跳跃并添加到曲线上
const completeNodeJump = poolNodeIndex => {
const poolNode = poolNodes.value[poolNodeIndex];
const newNode = { ...poolNode.jumpPosition };
nodes.value.push(newNode);
const newNodeIndex = nodes.value.length - 1;
const targetNodeIndex = poolNode.targetNodeIndex;
const segmentIndex = findSegmentContainingNode(targetNodeIndex);
if (segmentIndex !== -1) {
const segment = curveSegments.value[segmentIndex];
if (segment.startNode === targetNodeIndex) {
curveSegments.value.push({
startNode: newNodeIndex,
endNode: segment.endNode,
color: segment.color
});
segment.endNode = newNodeIndex;
} else if (segment.endNode === targetNodeIndex) {
curveSegments.value.push({
startNode: segment.startNode,
endNode: newNodeIndex,
color: segment.color
});
segment.startNode = newNodeIndex;
}
}
poolNodes.value.splice(poolNodeIndex, 1);
bounceOffsets.value = poolNodes.value.map(() => 0);
setTimeout(() => {
addNewPoolNode();
}, 1000);
};
// 添加新的需求池节点
const addNewPoolNode = () => {
const colors = [
{ fill: "#3498db", stroke: "#2980b9" },
{ fill: "#e74c3c", stroke: "#c0392b" },
{ fill: "#9b59b6", stroke: "#8e44ad" },
{ fill: "#e67e22", stroke: "#d35400" },
{ fill: "#f1c40f", stroke: "#f39c12" },
{ fill: "#1abc9c", stroke: "#16a085" },
{ fill: "#95a5a6", stroke: "#7f8c8d" },
{ fill: "#3498db", stroke: "#2980b9" },
{ fill: "#e74c3c", stroke: "#c0392b" },
{ fill: "#9b59b6", stroke: "#8e44ad" }
];
const randomColor = colors[Math.floor(Math.random() * colors.length)];
poolNodes.value.push({
poolIndex: 0,
...randomColor,
isHovered: false,
isJumping: false
});
updateConstrainedPositions(); // 新增节点后更新约束位置
};
// 找到包含指定节点的线段
const findSegmentContainingNode = nodeIndex => {
return curveSegments.value.findIndex(segment => segment.startNode === nodeIndex || segment.endNode === nodeIndex);
};
// 更新所有需求池节点的约束位置
const updateConstrainedPositions = () => {
constrainedPoolNodePosition.value = poolNodes.value.map((node, index) => getPoolNodePosition(node.poolIndex, index));
};
// 动画函数
const animate = time => {
if (!lastTime) lastTime = time;
poolNodes.value.forEach((_, index) => {
if (!poolNodes.value[index].isJumping) {
const frequency = 0.0025 * (index + 1);
const amplitude = 2.5;
bounceOffsets.value[index] = amplitude * Math.sin(time * frequency);
}
});
lastTime = time;
animationFrameId = requestAnimationFrame(animate);
};
// 生命周期钩子
onMounted(() => {
updateConstrainedPositions(); // 初始化约束位置
animationFrameId = requestAnimationFrame(animate);
startAutoJumping();
const handleResize = () => {
isMobileView.value = window.innerWidth < 768;
updateConstrainedPositions(); // 窗口大小变化时更新约束位置
};
window.addEventListener("resize", handleResize);
handleResize();
});
onUnmounted(() => {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
}
if (jumpTimer) {
clearTimeout(jumpTimer);
}
window.removeEventListener("resize", handleResize);
});
</script>
<style scoped>
.curve-container {
width: 100%;
height: 100%;
}
</style>

View File

@@ -27,11 +27,8 @@
</template> </template>
<!-- successSync --> <!-- successSync -->
<template #successSync="scope"> <template #successSync="scope">
<a <a @click="handleSuccessSync(scope.row)" :class="scope.row.successSync == '失败' ? 'break-word to-detail1' : ''">
@click="handleSuccessSync(scope.row)" {{ scope.row.successSync }}
:class="scope.row.successSync == '失败' && scope.row.type == '采购入库' ? 'break-word to-detail1' : ''"
>
{{ scope.row.type == "采购入库" ? scope.row.successSync : "--" }}
</a> </a>
</template> </template>
</ProTable> </ProTable>