Compare commits

...

10 Commits

Author SHA1 Message Date
tao
cef3725a6e 更新组件引入文件 2025-12-23 13:38:34 +08:00
tao
edaa445b63 新增工单管理/进出站界面 2025-12-23 13:38:13 +08:00
tao
9e45b78f31 新增工单管理界面 2025-12-23 13:37:48 +08:00
tao
7025a5304f 优化接口文件结构
更新接口文件
2025-12-23 13:37:06 +08:00
tao
90d6b542ee 优化登录逻辑 2025-12-23 13:31:14 +08:00
tao
200e57931a 优化主页显示 2025-12-23 13:31:04 +08:00
tao
652ebfb5ef 优化 auth 方法
新增加密方法
2025-12-23 13:30:25 +08:00
tao
448a22df5e 配置全局状态管理 2025-12-23 13:30:06 +08:00
tao
e8a69a3536 优化 Header 组件 2025-12-23 13:29:21 +08:00
tao
fb55334a1d 新增全局按钮样式 2025-12-23 13:29:03 +08:00
27 changed files with 1886 additions and 72 deletions

29
components.d.ts vendored
View File

@@ -8,17 +8,46 @@ export {}
/* prettier-ignore */ /* prettier-ignore */
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
AAvatar: typeof import('ant-design-vue/es')['Avatar']
AButton: typeof import('ant-design-vue/es')['Button'] AButton: typeof import('ant-design-vue/es')['Button']
ACard: typeof import('ant-design-vue/es')['Card'] ACard: typeof import('ant-design-vue/es')['Card']
ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
ACol: typeof import('ant-design-vue/es')['Col']
AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider'] AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider']
ActionButtons: typeof import('./src/components/common/ActionButtons/index.vue')['default'] ActionButtons: typeof import('./src/components/common/ActionButtons/index.vue')['default']
ADescriptions: typeof import('ant-design-vue/es')['Descriptions']
ADescriptionsItem: typeof import('ant-design-vue/es')['DescriptionsItem']
ADivider: typeof import('ant-design-vue/es')['Divider']
ADropdown: typeof import('ant-design-vue/es')['Dropdown']
ADropdownButton: typeof import('ant-design-vue/es')['DropdownButton']
AForm: typeof import('ant-design-vue/es')['Form'] AForm: typeof import('ant-design-vue/es')['Form']
AFormItem: typeof import('ant-design-vue/es')['FormItem'] AFormItem: typeof import('ant-design-vue/es')['FormItem']
AInput: typeof import('ant-design-vue/es')['Input'] AInput: typeof import('ant-design-vue/es')['Input']
AInputPassword: typeof import('ant-design-vue/es')['InputPassword'] AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
AInputSearch: typeof import('ant-design-vue/es')['InputSearch']
AList: typeof import('ant-design-vue/es')['List']
AListItem: typeof import('ant-design-vue/es')['ListItem']
AListItemMeta: typeof import('ant-design-vue/es')['ListItemMeta']
AMenu: typeof import('ant-design-vue/es')['Menu']
AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
AModal: typeof import('ant-design-vue/es')['Modal']
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
AProgress: typeof import('ant-design-vue/es')['Progress']
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
ARow: typeof import('ant-design-vue/es')['Row']
ASpace: typeof import('ant-design-vue/es')['Space']
ASpin: typeof import('ant-design-vue/es')['Spin']
ASteps: typeof import('ant-design-vue/es')['Steps']
ATable: typeof import('ant-design-vue/es')['Table']
ATag: typeof import('ant-design-vue/es')['Tag']
Header: typeof import('./src/components/Header/index.vue')['default'] Header: typeof import('./src/components/Header/index.vue')['default']
ILucideArrow: typeof import('~icons/lucide/arrow')['default']
ILucideArrowDown: typeof import('~icons/lucide/arrow-down')['default']
ILucideBarChart3: typeof import('~icons/lucide/bar-chart3')['default'] ILucideBarChart3: typeof import('~icons/lucide/bar-chart3')['default']
ILucideBuilding: typeof import('~icons/lucide/building')['default'] ILucideBuilding: typeof import('~icons/lucide/building')['default']
ILucideChevronDown: typeof import('~icons/lucide/chevron-down')['default']
ILucideCopy: typeof import('~icons/lucide/copy')['default']
ILucideCpu: typeof import('~icons/lucide/cpu')['default'] ILucideCpu: typeof import('~icons/lucide/cpu')['default']
ILucideLoader: typeof import('~icons/lucide/loader')['default'] ILucideLoader: typeof import('~icons/lucide/loader')['default']
ILucideLock: typeof import('~icons/lucide/lock')['default'] ILucideLock: typeof import('~icons/lucide/lock')['default']

View File

@@ -1,8 +0,0 @@
export interface ApiResponse<T = any> {
code?: number;
msg?: string;
data?: T;
rows?: T[];
total?: number;
token?: string;
}

View File

@@ -0,0 +1,81 @@
import request from "@/api/request";
import type { FixtureCombinationData, FixtureCombinationQuery } from "./model";
import type { ID } from "@/api/common";
// 查询治具组合列表
export function listMaskCombination(params: FixtureCombinationQuery) {
return request({
url: "/tpm/mask/combination/list",
method: "get",
params,
});
}
// 查询治具组合包含的治具列表
export function listCombinationAssignMask(id: ID, params: FixtureCombinationQuery) {
return request({
url: "/tpm/mask/combination/" + id + "/masks",
method: "get",
params,
});
}
// 高级查询治具组合列表
export function advListMaskCombination(params: FixtureCombinationQuery) {
return request({
url: "/tpm/mask/combination/advList",
method: "get",
params,
});
}
// 查询治具组合详细
export function getMaskCombination(id: ID) {
return request({
url: "/tpm/mask/combination/" + id,
method: "get",
});
}
// 新增治具组合
export function addMaskCombination(data: FixtureCombinationData) {
return request({
url: "/tpm/mask/combination",
method: "post",
data,
});
}
// 修改治具组合
export function updateMaskCombination(data: FixtureCombinationData) {
return request({
url: "/tpm/mask/combination",
method: "put",
data,
});
}
// 删除治具组合
export function delMaskCombination(id: ID) {
return request({
url: "/tpm/mask/combination/" + id,
method: "delete",
});
}
// 新增治具组合与治具关联关系
export function addMaskCombinationAssignment(data: FixtureCombinationData) {
return request({
url: "/tpm/mask/combination/assignment",
method: "post",
data,
});
}
// 删除治具组合与治具关联关系
export function delMaskCombinationAssignment(id: ID) {
return request({
url: "/tpm/mask/combination/assignment/" + id,
method: "delete",
});
}

View File

@@ -0,0 +1,26 @@
import { BaseEntity, PageQuery, type ID } from "@/api/common";
/**
* 治具组合查询参数
*/
export interface FixtureCombinationQuery extends PageQuery {
combinationName?: string;
combinationCode?: string;
combinationStatus?: string;
remark?: string;
searchValue?: string;
tempId?: string;
timeRange?: [string, string];
}
/**
* 治具组合数据
*/
export interface FixtureCombinationData extends BaseEntity {
combinationName?: string;
combinationCode?: string;
combinationStatus?: string;
remark?: string;
searchValue?: string;
tempId?: string;
timeRange?: [string, string];
}

View File

@@ -0,0 +1,63 @@
import request from "@/api/request";
import type { LotTraceOrderData, LotTraceOrderQuery } from "./model";
import type { ID } from "@/api/common";
// 查询随工单列表
export function listLotTraceOrder(params: LotTraceOrderQuery) {
return request({
url: "/mes/lot-trace-order/list",
method: "get",
params,
});
}
// 高级查询随工单列表
export function advListLotTraceOrder(params: LotTraceOrderQuery) {
return request({
url: "/mes/lot-trace-order/advList",
method: "get",
params,
});
}
// 查询随工单详细
export function getLotTraceOrder(id: ID) {
return request({
url: "/mes/lot-trace-order/" + id,
method: "get",
});
}
// 新增随工单
export function addLotTraceOrder(data: LotTraceOrderData) {
return request({
url: "/mes/lot-trace-order",
method: "post",
data,
});
}
// 修改随工单
export function updateLotTraceOrder(data: LotTraceOrderData) {
return request({
url: "/mes/lot-trace-order",
method: "put",
data,
});
}
// 删除随工单
export function delLotTraceOrder(id: ID) {
return request({
url: "/mes/lot-trace-order/" + id,
method: "delete",
});
}
// 关闭随工单
export function closeLotTraceOrder(id: ID) {
return request({
url: "/mes/lot-trace-order/close/" + id,
method: "put",
});
}

188
src/api/pwoManage/model.d.ts vendored Normal file
View File

@@ -0,0 +1,188 @@
import { BaseEntity, PageQuery, type ID } from "@/api/common";
/**
* 随工单查询参数
*/
export interface LotTraceOrderQuery extends PageQuery {
/** 编码 */
code?: string;
/** 状态 */
status?: string;
/** 主生产计划的ID */
mpsId?: number;
/** 主生产计划编码 */
mpsCode?: string;
/** 订单类型 */
orderType?: string;
/** 主生产计划明细ID */
mpsDetailId?: number;
/** 批号 */
batchNo?: string;
/** 主生产计划明细序号 */
mpsDetailSeq?: number;
/** 目标产品ID */
tarMaterialId?: number;
/** 主物料ID */
masterMaterialId?: number;
/** 生产版本ID */
prodVersionId?: number;
/** 计划数量 */
planQty?: number;
/** OK数量 */
okQty?: number;
/** NG数量 */
ngQty?: number;
/** 未完成数量 */
unfinishedQty?: number;
/** 单位ID */
unitId?: number;
/** 计划开始时间范围 */
planStartTimeRange?: [string, string];
/** 计划结束时间范围 */
planEndTimeRange?: [string, string];
/** 扩展字段1 */
extStr1?: string;
/** 扩展字段2 */
extStr2?: string;
/** 扩展字段3 */
extStr3?: string;
/** 扩展字段4 */
extStr4?: string;
/** 扩展字段5 */
extStr5?: string;
/** 扩展字段6 */
extStr6?: string;
/** 扩展字段7 */
extStr7?: string;
/** 扩展字段8 */
extStr8?: string;
/** 扩展字段9 */
extStr9?: string;
/** 扩展字段10 */
extStr10?: string;
/** 扩展字段11 */
extStr11?: string;
/** 扩展字段12 */
extStr12?: string;
/** 扩展字段13 */
extStr13?: string;
/** 扩展字段14 */
extStr14?: string;
/** 扩展字段15 */
extStr15?: string;
/** 扩展字段16 */
extStr16?: string;
/** 扩展整型1 */
extInt1?: number;
/** 扩展整型2 */
extInt2?: number;
/** 扩展小数1 */
extDec1?: number;
/** 扩展小数2 */
extDec2?: number;
/** 扩展日期1范围 */
extDate1Range?: [string, string];
/** 扩展日期2范围 */
extDate2Range?: [string, string];
/** 删除标志 */
delStatus?: string;
/** 创建时间范围 */
createTimeRange?: [string, string];
/** 更新时间范围 */
updateTimeRange?: [string, string];
}
/**
* 随工单数据
*/
export interface LotTraceOrderData extends BaseEntity {
/** 主键ID */
id?: ID;
/** 编码 */
code?: string;
/** 状态 */
status?: string;
/** 主生产计划的ID */
mpsId?: number;
/** 主生产计划编码 */
mpsCode?: string;
/** 订单类型 */
orderType?: string;
/** 主生产计划明细ID */
mpsDetailId?: number;
/** 批号 */
batchNo?: string;
/** 主生产计划明细序号 */
mpsDetailSeq?: number;
/** 目标产品ID */
tarMaterialId?: number;
/** 目标产品名称 */
tarMaterialName?: string;
/** 目标产品编码 */
tarMaterialCode?: string;
/** 主物料ID */
masterMaterialId?: number;
/** 生产版本ID */
prodVersionId?: number;
/** 计划数量 */
planQty?: number;
/** OK数量 */
okQty?: number;
/** NG数量 */
ngQty?: number;
/** 未完成数量 */
unfinishedQty?: number;
/** 单位ID */
unitId?: number;
/** 计划开始时间 */
planStartTime?: string;
/** 计划结束时间 */
planEndTime?: string;
/** 扩展字段1 */
extStr1?: string;
/** 扩展字段2 */
extStr2?: string;
/** 扩展字段3 */
extStr3?: string;
/** 扩展字段4 */
extStr4?: string;
/** 扩展字段5 */
extStr5?: string;
/** 扩展字段6 */
extStr6?: string;
/** 扩展字段7 */
extStr7?: string;
/** 扩展字段8 */
extStr8?: string;
/** 扩展字段9 */
extStr9?: string;
/** 扩展字段10 */
extStr10?: string;
/** 扩展字段11 */
extStr11?: string;
/** 扩展字段12 */
extStr12?: string;
/** 扩展字段13 */
extStr13?: string;
/** 扩展字段14 */
extStr14?: string;
/** 扩展字段15 */
extStr15?: string;
/** 扩展字段16 */
extStr16?: string;
/** 扩展整型1 */
extInt1?: number;
/** 扩展整型2 */
extInt2?: number;
/** 扩展小数1 */
extDec1?: number;
/** 扩展小数2 */
extDec2?: number;
/** 扩展日期1 */
extDate1?: string;
/** 扩展日期2 */
extDate2?: string;
/** 备注 */
remark?: string;
/** 删除标志 */
delStatus?: string;
}

55
src/api/station/index.ts Normal file
View File

@@ -0,0 +1,55 @@
import request from "@/api/request";
import type { ID } from "@/api/common";
import type { MesStationQuery, MesStationData } from "./model";
// 查询站点列表
export function listStation(params: MesStationQuery) {
return request({
url: "/mes/station/list",
method: "get",
params,
});
}
// 高级查询站点列表
export function advListStation(params: MesStationQuery) {
return request({
url: "/mes/station/advList",
method: "get",
params,
});
}
// 查询站点详细
export function getStation(id: ID) {
return request({
url: "/mes/station/" + id,
method: "get",
});
}
// 新增站点
export function addStation(data: MesStationData) {
return request({
url: "/mes/station",
method: "post",
data,
});
}
// 修改站点
export function updateStation(data: MesStationData) {
return request({
url: "/mes/station",
method: "put",
data,
});
}
// 删除站点
export function delStation(id: ID) {
return request({
url: "/mes/station/" + id,
method: "delete",
});
}

71
src/api/station/model.d.ts vendored Normal file
View File

@@ -0,0 +1,71 @@
import { BaseEntity, PageQuery, type ID } from "@/api/common";
/**
* 站点查询参数
*/
export interface MesStationQuery extends PageQuery {
/** 随工单ID */
traceOrderId?: number;
/** 随工单编码 */
traceOrderCode?: string;
/** 站点序号 */
seqNo?: number;
/** 站点名称 */
name?: string;
/** 站点编码 */
code?: string;
/** 状态 */
status?: string;
/** 计划数量最小值 */
planQtyMin?: number;
/** 计划数量最大值 */
planQtyMax?: number;
/** 合格数量最小值 */
okQtyMin?: number;
/** 合格数量最大值 */
okQtyMax?: number;
/** 不合格数量最小值 */
ngQtyMin?: number;
/** 不合格数量最大值 */
ngQtyMax?: number;
/** 进站时间范围开始 */
arrivalTimeStart?: string;
/** 进站时间范围结束 */
arrivalTimeEnd?: string;
/** 出战时间范围开始 */
departureTimeStart?: string;
/** 出战时间范围结束 */
departureTimeEnd?: string;
/** 删除标志 */
delStatus?: string;
}
/**
* 站点数据
*/
export interface MesStationData extends BaseEntity {
/** 主键ID */
id?: number;
/** 随工单ID */
traceOrderId?: number;
/** 站点序号 */
seqNo?: number;
/** 站点名称 */
name?: string;
/** 站点编码 */
code?: string;
/** 状态 */
status?: string;
/** 计划数量 */
planQty?: number;
/** 合格数量 */
okQty?: number;
/** 不合格数量 */
ngQty?: number;
/** 进站时间 */
arrivalTime?: string;
/** 出战时间 */
departureTime?: string;
/** 删除标志 */
delStatus?: string;
}

View File

@@ -1,4 +1,4 @@
import request from '../request' import request from '@/api/request'
import type { LoginInfo } from './model'; import type { LoginInfo } from './model';
// 用户登录 // 用户登录
@@ -16,4 +16,12 @@ export function getCaptcha() {
url: '/captchaImage', url: '/captchaImage',
method: 'get' method: 'get'
}) })
} }
// 退出登录
export function logout() {
return request({
url: '/logout',
method: 'post'
})
}

View File

@@ -0,0 +1,134 @@
// Color variables
$orange: #ff9800;
$orange-hover: #ffb74d;
$blue: #2196f3;
$blue-hover: #42a5f5;
$red: #f44336;
$red-hover: #ef5350;
$green: #4caf50;
$green-hover: #66bb6a;
$purple: #9c27b0;
$purple-hover: #ab47bc;
$teal: #009688;
$teal-hover: #26a69a;
$indigo: #3f51b5;
$indigo-hover: #5c6bc0;
$pink: #e91e63;
$pink-hover: #ec407a;
$cyan: #00bcd4;
$cyan-hover: #26c6da;
$amber: #ffc107;
$amber-hover: #ffd54f;
$brown: #795548;
$brown-hover: #8d6e63;
$grey: #9e9e9e;
$grey-hover: #bdbdbd;
.action-btn {
&.orange-btn {
background-color: $orange;
& &:hover {
background-color: $orange-hover;
}
}
&.blue-btn {
background-color: $blue;
&:hover {
background-color: $blue-hover;
}
}
&.red-btn {
background-color: $red;
&:hover {
background-color: $red-hover;
}
}
&.green-btn {
background-color: $green;
&:hover {
background-color: $green-hover;
}
}
&.purple-btn {
background-color: $purple;
&:hover {
background-color: $purple-hover;
}
}
&.teal-btn {
background-color: $teal;
&:hover {
background-color: $teal-hover;
}
}
&.indigo-btn {
background-color: $indigo;
&:hover {
background-color: $indigo-hover;
}
}
&.pink-btn {
background-color: $pink;
&:hover {
background-color: $pink-hover;
}
}
&.cyan-btn {
background-color: $cyan;
&:hover {
background-color: $cyan-hover;
}
}
&.amber-btn {
background-color: $amber;
&:hover {
background-color: $amber-hover;
}
}
&.brown-btn {
background-color: $brown;
&:hover {
background-color: $brown-hover;
}
}
&.grey-btn {
background-color: $grey;
&:hover {
background-color: $grey-hover;
}
}
}

View File

@@ -0,0 +1 @@
@forward './button';

View File

@@ -1,3 +1,4 @@
@forward './base'; @forward './base';
@forward './variables'; @forward './variables';
@forward './ant-design'; @forward './ant-design';
@forward './custom/index';

View File

@@ -2,8 +2,8 @@
<header class="header-container" :class="{ 'hide-shadow': hideShadow }" <header class="header-container" :class="{ 'hide-shadow': hideShadow }"
:style="{ height, zIndex, lineHeight: height }"> :style="{ height, zIndex, lineHeight: height }">
<div class="opts left-opts" v-if="$slots['left-opts'] || title || $slots.title"> <div class="opts left-opts" v-if="$slots['left-opts'] || title || $slots.title">
<a-button v-if="showHome" class="header-btn" @click="backToHome">首页</a-button> <a-button v-if="showBack" @click="back">返回</a-button>
<a-button v-if="showBack" class="header-btn" @click="back">返回</a-button> <a-button v-if="showHome" @click="backToHome">首页</a-button>
<slot name="left-opts" /> <slot name="left-opts" />
</div> </div>
<div class="title" v-if="title || $slots.title"> <div class="title" v-if="title || $slots.title">
@@ -13,6 +13,8 @@
</div> </div>
<div class="opts right-opts" v-if="$slots['right-opts'] || title || $slots.title"> <div class="opts right-opts" v-if="$slots['right-opts'] || title || $slots.title">
<slot name="right-opts" /> <slot name="right-opts" />
<a-button @click="handleLogout" type="primary" danger
v-if="showLogout && username">退出{{ username }}</a-button>
</div> </div>
<slot /> <slot />
</header> </header>
@@ -20,6 +22,9 @@
<script setup> <script setup>
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { Modal } from 'ant-design-vue';
import { useAuthStore, useUserStore } from '@/store';
import { storeToRefs } from 'pinia';
defineProps({ defineProps({
showHome: { showHome: {
@@ -30,6 +35,10 @@ defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
showLogout: {
type: Boolean,
default: true,
},
title: { title: {
type: String, type: String,
default: null, default: null,
@@ -51,6 +60,22 @@ defineProps({
const emit = defineEmits(['back']); const emit = defineEmits(['back']);
const router = useRouter(); const router = useRouter();
const userStore = useUserStore();
const { username } = storeToRefs(userStore);
const authStore = useAuthStore();
const handleLogout = () => {
Modal.confirm({
title: '提示',
content: `是否确认退出登录:${ username.value }`,
okText: '确定',
cancelText: '取消',
onOk: () => {
authStore.logout();
},
});
};
const back = () => { const back = () => {
emit('back'); emit('back');
defaultBack(); defaultBack();
@@ -61,7 +86,7 @@ const defaultBack = () => {
}; };
const backToHome = () => { const backToHome = () => {
router.push({ name: 'ipc' }); router.push({ name: 'Index' });
}; };
</script> </script>
@@ -72,7 +97,8 @@ const backToHome = () => {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
background-color: #fff; background-color: #1f2e54;
color: #fff;
box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 6px -1px, rgba(0, 0, 0, 0.06) 0px 2px 4px -1px; box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 6px -1px, rgba(0, 0, 0, 0.06) 0px 2px 4px -1px;
&.hide-shadow { &.hide-shadow {
@@ -81,6 +107,7 @@ const backToHome = () => {
.title { .title {
flex: 14; flex: 14;
color: #fff;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-align: center; text-align: center;
@@ -105,12 +132,5 @@ const backToHome = () => {
justify-content: flex-end; justify-content: flex-end;
} }
} }
:deep(.header-btn.ant-btn) {
height: 80%;
text-align: center;
font-size: clamp(14px, 1vw, 18px);
padding: 0 0.75rem;
}
} }
</style> </style>

View File

@@ -1,12 +1,11 @@
import Cookies from "js-cookie";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { ref } from "vue"; import { ref } from "vue";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { message } from "ant-design-vue"; import { message, notification } from "ant-design-vue";
import { useUserStore } from "./user"; import { useUserStore } from "./user";
import { login, logout as logoutApi } from "@/api/system"; import { login, logout as logoutApi } from "@/api/system";
import { TokenKey as TOKEN_KEY } from "@/utils/auth"; import { setToken, getToken, removeToken, setAccount, removeAccount } from "@/utils/auth";
import type { LoginInfo } from "@/api/system/model"; import type { LoginInfo } from "@/api/system/model";
export const useAuthStore = defineStore("auth", () => { export const useAuthStore = defineStore("auth", () => {
@@ -15,26 +14,29 @@ export const useAuthStore = defineStore("auth", () => {
const userStore = useUserStore(); const userStore = useUserStore();
const loginLoading = ref(false); const loginLoading = ref(false);
const token = ref(Cookies.get(TOKEN_KEY) || null); const token = ref(getToken() || null);
function setToken(newToken: string) { async function authLogin(params: LoginInfo, rememberMe: boolean) {
token.value = newToken;
Cookies.set(TOKEN_KEY, newToken);
}
function clearToken() {
token.value = null;
Cookies.remove(TOKEN_KEY);
}
async function authLogin(params: LoginInfo) {
try { try {
loginLoading.value = true; loginLoading.value = true;
const res = await login(params); const res = await login(params);
if (res.code === 200) { if (res.code === 200) {
setToken(res.token as string); setToken(res.token as string);
await userStore.fetchUserInfo(); await userStore.fetchUserInfo();
message.success("登录成功"); notification.success({
message: "登录成功",
description: `欢迎回来,${params.username}`,
duration: 3,
});
if (rememberMe) {
userStore.setUserInfo({
...params,
rememberMe
});
} else {
userStore.clearUserInfo();
}
const redirect = route.query.redirect || "/"; const redirect = route.query.redirect || "/";
router.replace(redirect as string); router.replace(redirect as string);
return res; return res;
@@ -52,7 +54,7 @@ export const useAuthStore = defineStore("auth", () => {
async function logout() { async function logout() {
// 在实际应用中,这里可以调用后端的退出登录接口 // 在实际应用中,这里可以调用后端的退出登录接口
await logoutApi(); await logoutApi();
clearToken(); removeToken();
userStore.clearUserInfo(); userStore.clearUserInfo();
await router.push("/login"); await router.push("/login");
message.success("已成功退出"); message.success("已成功退出");

View File

@@ -1,22 +1,24 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { setAccount, removeAccount } from "@/utils/auth";
const USERNAME_KEY = 'username'; export const useUserStore = defineStore("user", {
export const useUserStore = defineStore('user', {
state: () => ({ state: () => ({
username: Cookies.get(USERNAME_KEY) || null, username: Cookies.get('username') || null,
}), }),
actions: { actions: {
async fetchUserInfo() { async fetchUserInfo() {
// Simulate API call // Simulate API call
const fetchedUsername = 'mock_user'; },
this.username = fetchedUsername; setUserInfo(params: any) {
Cookies.set(USERNAME_KEY, fetchedUsername); setAccount({
username: params.username,
password: params.password,
rememberMe: params.rememberMe ? "true" : "false",
});
}, },
clearUserInfo() { clearUserInfo() {
this.username = null; removeAccount();
Cookies.remove(USERNAME_KEY);
}, },
}, },
}); });

View File

@@ -1,4 +1,8 @@
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { encrypt } from '@/utils/jsencrypt';
// 30 天
export const expiresTime = 30;
export const TokenKey = 'Admin-Token' export const TokenKey = 'Admin-Token'
@@ -13,3 +17,29 @@ export function setToken(token: string) {
export function removeToken() { export function removeToken() {
return Cookies.remove(TokenKey) return Cookies.remove(TokenKey)
} }
export interface AccountInfo {
username?: string;
password?: string;
rememberMe?: 'true' | 'false';
}
export function getAccount() {
return {
username: Cookies.get("username"),
password: Cookies.get("password"),
rememberMe: Cookies.get("rememberMe"),
};
}
export function setAccount(account: AccountInfo) {
account.username && Cookies.set("username", account.username, { expires: expiresTime});
account.password && Cookies.set("password", encrypt(account.password) as string, { expires: expiresTime});
account.rememberMe != null && Cookies.set("rememberMe", account.rememberMe, { expires: expiresTime});
}
export function removeAccount() {
Cookies.remove("username");
Cookies.remove("password");
Cookies.remove("rememberMe");
}

31
src/utils/jsencrypt.ts Normal file
View File

@@ -0,0 +1,31 @@
import JSEncrypt from "jsencrypt";
// 密钥对生成 http://web.chacuo.net/netrsakeypair
const publicKey =
"MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdH\n" +
"nzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==";
const privateKey =
"MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY\n" +
"7Nt+PrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKN\n" +
"PuH3owIDAQABAkAfoiLyL+Z4lf4Myxk6xUDgLaWGximj20CUf+5BKKnlrK+Ed8gA\n" +
"kM0HqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWow\n" +
"cSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21v+F25WaHYPxCFMvwxpcw99Ecv\n" +
"DQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthh\n" +
"YhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3\n" +
"UP8iWi1Qw0Y=";
// 加密
export function encrypt(txt: string) {
const encryptor = new JSEncrypt();
encryptor.setPublicKey(publicKey); // 设置公钥
return encryptor.encrypt(txt); // 对数据进行加密
}
// 解密
export function decrypt(txt: string) {
const encryptor = new JSEncrypt();
encryptor.setPrivateKey(privateKey); // 设置私钥
return encryptor.decrypt(txt); // 对数据进行解密
}

View File

@@ -1,13 +1,9 @@
<template> <template>
<div class="ipc-dashboard"> <div class="ipc-dashboard">
<Header title="过站工控机"> <Header title="过站工控机" showLogout />
<template #right>
<a-button @click="handleLogout">退出登录</a-button>
</template>
</Header>
<div class="menu-grid"> <div class="menu-grid">
<a-card class="menu-card" shadow="hover" @click="handleJumpTo('/pwoManage')"> <a-card class="menu-card" shadow="hover" @click="handleJumpTo('PwoManage')">
<div class="icon-wrap"> <div class="icon-wrap">
<i-lucide-building /> <i-lucide-building />
</div> </div>
@@ -16,8 +12,8 @@
<div class="desc">管理生产工单和进度</div> <div class="desc">管理生产工单和进度</div>
</div> </div>
</a-card> </a-card>
<!--
<a-card class="menu-card" shadow="hover" @click="handleJumpTo('/stationControl')"> <a-card class="menu-card" shadow="hover" @click="handleJumpTo('stationControl')">
<div class="icon-wrap"> <div class="icon-wrap">
<i-lucide-monitor /> <i-lucide-monitor />
</div> </div>
@@ -27,7 +23,7 @@
</div> </div>
</a-card> </a-card>
<a-card class="menu-card" shadow="hover" @click="handleJumpTo('/dispatch')"> <a-card class="menu-card" shadow="hover" @click="handleJumpTo('dispatch')">
<div class="icon-wrap"> <div class="icon-wrap">
<i-lucide-package /> <i-lucide-package />
</div> </div>
@@ -37,7 +33,7 @@
</div> </div>
</a-card> </a-card>
<a-card class="menu-card" shadow="hover" @click="handleJumpTo('/hold')"> <a-card class="menu-card" shadow="hover" @click="handleJumpTo('hold')">
<div class="icon-wrap"> <div class="icon-wrap">
<i-lucide-server /> <i-lucide-server />
</div> </div>
@@ -47,7 +43,7 @@
</div> </div>
</a-card> </a-card>
<a-card class="menu-card" shadow="hover" @click="handleJumpTo('/dataAnalysis')"> <a-card class="menu-card" shadow="hover" @click="handleJumpTo('dataAnalysis')">
<div class="icon-wrap"> <div class="icon-wrap">
<i-lucide-bar-chart-3 /> <i-lucide-bar-chart-3 />
</div> </div>
@@ -57,7 +53,7 @@
</div> </div>
</a-card> </a-card>
<a-card class="menu-card" shadow="hover" @click="handleJumpTo('/maintenance')"> <a-card class="menu-card" shadow="hover" @click="handleJumpTo('maintenance')">
<div class="icon-wrap"> <div class="icon-wrap">
<i-lucide-wrench /> <i-lucide-wrench />
</div> </div>
@@ -67,7 +63,7 @@
</div> </div>
</a-card> </a-card>
<a-card class="menu-card" shadow="hover" @click="handleJumpTo('/personnel')"> <a-card class="menu-card" shadow="hover" @click="handleJumpTo('personnel')">
<div class="icon-wrap"> <div class="icon-wrap">
<i-lucide-users /> <i-lucide-users />
</div> </div>
@@ -77,7 +73,7 @@
</div> </div>
</a-card> </a-card>
<a-card class="menu-card" shadow="hover" @click="handleJumpTo('/settings')"> <a-card class="menu-card" shadow="hover" @click="handleJumpTo('settings')">
<div class="icon-wrap"> <div class="icon-wrap">
<i-lucide-settings /> <i-lucide-settings />
</div> </div>
@@ -85,7 +81,7 @@
<div class="title">系统设置</div> <div class="title">系统设置</div>
<div class="desc">系统参数配置</div> <div class="desc">系统参数配置</div>
</div> </div>
</a-card> </a-card> -->
</div> </div>
</div> </div>
</template> </template>
@@ -113,20 +109,17 @@ const handleLogout = () => {
okText: '确定', okText: '确定',
cancelText: '取消', cancelText: '取消',
onOk: () => { onOk: () => {
message.success('已退出'); authStore.logout();
}, },
onCancel: () => {
message.error('操作失败');
}
}); });
}; };
const handleJumpTo = (path) => { const handleJumpTo = (name) => {
if (!loggedIn.value) { if (!loggedIn.value) {
message.warning("尚未登录,请先登录"); message.warning("尚未登录,请先登录");
return; return;
} }
router.push({ path }); router.push({ name });
}; };
</script> </script>
@@ -136,6 +129,7 @@ const handleJumpTo = (path) => {
height: 100vh; height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden;
} }
.menu-grid { .menu-grid {

View File

@@ -6,6 +6,8 @@ import type { LoginInfo } from '@/api/system/model'
import type { Rule } from 'ant-design-vue/es/form'; import type { Rule } from 'ant-design-vue/es/form';
import { useAuthStore } from '@/store'; import { useAuthStore } from '@/store';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { getAccount } from '@/utils/auth';
import { decrypt } from '@/utils/jsencrypt';
const authStore = useAuthStore(); const authStore = useAuthStore();
const { loginLoading } = storeToRefs(authStore); const { loginLoading } = storeToRefs(authStore);
@@ -15,13 +17,14 @@ const formData = reactive<LoginInfo>({
username: '', username: '',
password: '', password: '',
uuid: '', uuid: '',
code: '' code: '',
}) })
// 验证码图片 // 验证码图片
const captchaImg = ref('') const captchaImg = ref('')
const captchaLoading = ref(false) const captchaLoading = ref(false)
const loadCaptchaFail = ref(false) const loadCaptchaFail = ref(false)
const rememberMe = ref(false)
// 表单验证规则 // 表单验证规则
const rules: Record<string, Rule[]> = { const rules: Record<string, Rule[]> = {
@@ -61,16 +64,27 @@ const refreshCaptcha = async () => {
} }
} }
// 初始化
const initLoginForm = () => {
const { username, password, rememberMe: isRememberMe } = getAccount();
if (isRememberMe && isRememberMe === 'true') {
rememberMe.value = true;
formData.username = username || '';
formData.password = decrypt(password || '') || '';
}
}
// 登录处理 // 登录处理
const handleLogin = async () => { const handleLogin = async () => {
try { try {
await authStore.authLogin(formData); await authStore.authLogin(formData, rememberMe.value);
} catch (error: any) { } catch (error: any) {
message.error(error.message || '登录失败'); message.error(error.message || error.msg || '登录失败');
await refreshCaptcha(); await refreshCaptcha();
} }
} }
initLoginForm()
// 组件挂载时获取验证码 // 组件挂载时获取验证码
refreshCaptcha() refreshCaptcha()
</script> </script>
@@ -104,6 +118,7 @@ refreshCaptcha()
placeholder="请输入用户名" placeholder="请输入用户名"
size="large" size="large"
class="login-input" class="login-input"
allow-clear
> >
<template #prefix> <template #prefix>
<i-lucide-user class="input-icon" /> <i-lucide-user class="input-icon" />
@@ -118,6 +133,8 @@ refreshCaptcha()
placeholder="请输入密码" placeholder="请输入密码"
size="large" size="large"
class="login-input" class="login-input"
:visibility-toggle="false"
allow-clear
> >
<template #prefix> <template #prefix>
<i-lucide-lock class="input-icon" /> <i-lucide-lock class="input-icon" />
@@ -133,6 +150,7 @@ refreshCaptcha()
placeholder="请输入验证码" placeholder="请输入验证码"
size="large" size="large"
class="login-input captcha-input" class="login-input captcha-input"
allow-clear
> >
<template #prefix> <template #prefix>
<i-lucide-shield-check class="input-icon" /> <i-lucide-shield-check class="input-icon" />
@@ -158,6 +176,10 @@ refreshCaptcha()
</div> </div>
</div> </div>
</a-form-item> </a-form-item>
<a-form-item name="rememberMe" class="form-item">
<a-checkbox v-model:checked="rememberMe" style="color: #ddd;">记住密码</a-checkbox>
</a-form-item>
<!-- 登录按钮 --> <!-- 登录按钮 -->
<a-form-item class="form-item"> <a-form-item class="form-item">

View File

@@ -0,0 +1,121 @@
<script setup lang="ts">
import { ref, reactive, onMounted, watch } from 'vue';
import { useRoute } from 'vue-router';
import { message } from 'ant-design-vue';
import type { ColumnsType } from 'ant-design-vue/es/table/interface';
import { listStation } from '@/api/station';
import { usePwoStore } from '@/store';
interface TableItem {
key: string;
index: number;
operationTitle: string;
planQty: number;
okQty: number;
ngQty: number;
status: string;
[key: string]: any;
}
const route = useRoute();
const pwoStore = usePwoStore();
const tableData = ref<TableItem[]>([]);
const columns = [
{ title: '序号', dataIndex: 'index', key: 'index', align: 'center', width: 80 },
{ title: '制程', dataIndex: 'operationTitle', key: 'operationTitle', align: 'center' },
{ title: '总数量', dataIndex: 'planQty', key: 'planQty', align: 'center' },
{ title: '合格数量', dataIndex: 'okQty', key: 'okQty', align: 'center' },
{ title: '报废数量', dataIndex: 'ngQty', key: 'ngQty', align: 'center' },
{ title: '状态', dataIndex: 'status', key: 'status', align: 'center' },
{ title: '操作', key: 'action', align: 'center', width: 120 },
];
const loadingStations = ref(false);
const fetchStations = async (traceOrderCode: string) => {
try {
loadingStations.value = true;
const res = await listStation({ traceOrderCode });
tableData.value = (res.rows || []).map((item: any, idx: number) => {
return {
key: String(item.id ?? idx),
index: item.seqNo ?? idx + 1,
...item,
} as TableItem;
});
} catch (error: any) {
message.error(error?.msg || error?.message || '查询站点失败');
} finally {
loadingStations.value = false;
}
};
const handleSubmit = (record: TableItem) => {
message.success(`提交了序号: ${record.index}`);
};
onMounted(() => {
const traceOrderCode = route.query.traceOrderCode as string;
if (traceOrderCode) {
fetchStations(traceOrderCode);
}
});
watch(
() => route.query.t,
(val) => {
if (val) {
fetchStations(route.query.traceOrderCode as string);
}
}
);
function rowClick(record: TableItem) {
return {
onClick: () => {
pwoStore.setCurrentJob(record);
},
};
}
</script>
<template>
<div class="table-wrapper">
<a-table
:dataSource="tableData"
:columns="columns as ColumnsType<TableItem>"
:pagination="false"
:loading="loadingStations"
bordered
sticky
size="middle"
rowKey="key"
class="custom-table"
:customRow="rowClick"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-button type="primary" @click="handleSubmit(record as TableItem)">进入</a-button>
</template>
</template>
</a-table>
</div>
</template>
<style scoped lang="scss">
.table-wrapper {
height: 100%;
overflow: auto;
.custom-table {
:deep(.ant-table-thead > tr > th) {
background-color: #e6f7ff;
font-weight: bold;
}
:deep(.ant-table-tbody > tr > td) {
padding: 12px 8px;
}
}
}
</style>

View File

@@ -0,0 +1,184 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { message } from 'ant-design-vue';
import type { ColumnsType } from 'ant-design-vue/es/table/interface';
import { listMaskCombination, listCombinationAssignMask } from "@/api/pwoManage/fixtureCombination";
interface FixtureTableItem {
key: string;
operationTitle: string;
operationCode: string;
maskName: string;
maskCode: string;
[key: string]: any;
}
interface FixtureCombinationItem {
key: string;
combinationCode: string;
combinationName: string;
combinationStatus: string;
[key: string]: any;
}
const fixtureColumns = [
{ title: '序号', dataIndex: 'index', key: 'index', align: 'center', width: 80 },
{ title: '工序编码', dataIndex: 'operationCode', key: 'operationCode', align: 'center' },
{ title: '工序名称', dataIndex: 'operationTitle', key: 'operationTitle', align: 'center' },
{ title: '治具编码', dataIndex: 'maskCode', key: 'maskCode', align: 'center' },
{ title: '治具名称', dataIndex: 'maskName', key: 'maskName', align: 'center' },
];
// 治具表格
const fixtureTableData = ref<FixtureTableItem[]>([]);
const fixtureInput = ref<string>('');
const openFixtureModal = ref(false)
const fixtureCombinationColumns = [
{ title: '序号', dataIndex: 'index', key: 'index', align: 'center', width: 80 },
{ title: '组合编码', dataIndex: 'combinationCode', key: 'combinationCode', align: 'center' },
{ title: '组合名称', dataIndex: 'combinationName', key: 'combinationName', align: 'center' },
{ title: '组合状态', dataIndex: 'combinationStatus', key: 'combinationStatus', align: 'center' },
{ title: '操作', dataIndex: 'action', key: 'action', align: 'center', width: 100 },
]
const fixtureCombinationTableData = ref<FixtureCombinationItem[]>([])
const fetchCombinationList = async () => {
try {
const { rows, total } = await listMaskCombination({})
if (total as number <= 0) throw new Error('未查询到组合列表');
fixtureCombinationTableData.value = rows;
} catch (error: any) {
message.error(error.message || '获取工单信息失败');
}
}
const existingCombination = ref(new Set())
const handleBind = async (id: number) => {
const { rows } = await listCombinationAssignMask(id, {
maskStatus: "AVAILABLE",
orderByColumn: 'id',
orderBy: 'desc',
})
const newCombination = rows.filter(item => !existingCombination.value.has(item.id))
newCombination.forEach(item => {
fixtureTableData.value.push({
key: item.id,
operationCode: item.operationCode,
operationTitle: item.operationTitle,
maskCode: item.maskCode,
maskName: item.maskName
})
existingCombination.value.add(item.id)
})
message.success('绑定成功')
openFixtureModal.value = false
}
// 计算表格高度
const customTable = ref<HTMLElement | null>(null)
const tableHeight = ref(200)
const renderTableHeight = () => {
if (customTable.value) {
tableHeight.value = customTable.value.clientHeight - 50
console.log('元素高度:', tableHeight.value)
}
}
onMounted(() => {
renderTableHeight()
})
defineExpose({
renderTableHeight
});
fetchCombinationList()
</script>
<template>
<div class="fixture__container">
<a-row class="main-content" :gutter="24">
<a-col :span="14" class="fixture-item__container">
<div class="main-title">治具组合信息</div>
<a-button size="large" @click="() => openFixtureModal = true">绑定治具组合</a-button>
<div class="table-wrapper" ref="customTable">
<a-table :dataSource="fixtureTableData" :columns="fixtureColumns as ColumnsType<FixtureTableItem>"
:pagination="false" bordered sticky size="middle" :scroll="{ y: tableHeight }">
<template #bodyCell="{ column, index }">
<template v-if="column.key === 'index'">{{ index + 1 }}</template>
</template>
</a-table>
</div>
</a-col>
<a-col :span="10" class="fixture-item__container">
<div class="main-title">治具校验</div>
<a-row>
<a-col :span="20">
<a-input size="large" v-model:value="fixtureInput" @pressEnter="" placeholder="按下回车校验" />
</a-col>
<a-col :span="3" :offset="1">
<a-button size="large" @click="">校验</a-button>
</a-col>
</a-row>
<div class="table-wrapper">
<a-descriptions bordered :column="1">
<a-descriptions-item label="治具编码">Cloud Database</a-descriptions-item>
<a-descriptions-item label="治具名称">Prepaid</a-descriptions-item>
<a-descriptions-item label="治具组合">YES</a-descriptions-item>
<a-descriptions-item label="标准使用次数">YES</a-descriptions-item>
<a-descriptions-item label="当前使用次数">YES</a-descriptions-item>
<a-descriptions-item label="关联工序">YES</a-descriptions-item>
</a-descriptions>
</div>
</a-col>
</a-row>
<a-modal v-model:open="openFixtureModal" title="绑定治具组合" width="50vw" :bodyStyle="{ padding: '20px 0' }">
<a-table :dataSource="fixtureCombinationTableData" :columns="fixtureCombinationColumns as ColumnsType<FixtureCombinationItem>"
:pagination="false" bordered sticky :scroll="{ y: tableHeight }">
<template #bodyCell="{ column, index, record }">
<template v-if="column.key === 'index'">{{ index + 1 }}</template>
<template v-if="column.key === 'action'">
<a-button @click="handleBind(record.id)">绑定</a-button>
</template>
</template>
</a-table>
</a-modal>
</div>
</template>
<style scoped lang="scss">
.fixture__container {
height: 100%;
display: flex;
flex-direction: column;
}
.main-content {
flex: 1;
overflow: hidden;
.fixture-item__container {
display: flex;
flex-direction: column;
gap: 16px;
overflow: hidden;
height: 100%;
.main-title {
font-size: 20px;
font-weight: 600;
border-bottom: 1px solid #c9c9c9;
padding: 0 0 8px;
}
.table-wrapper {
flex: 1;
overflow: auto;
}
}
}
</style>

View File

@@ -0,0 +1,136 @@
<script setup lang="ts">
import { ref, computed } from 'vue';
import { useRouter, useRoute } from 'vue-router';
const router = useRouter();
const route = useRoute();
const menuItems = [
{ label: '主材', key: 'PrimaryMaterial', progress: 30 },
{ label: '材料', key: 'RawMaterial', progress: 50 },
{ label: '治具', key: 'Fixture', progress: 80 },
];
const activeKey = computed(() => route.name as string);
const handleMenuClick = (key: string) => {
router.push({ name: key });
};
const infeedChildRef = ref<any>(null);
/** 父组件对外暴露的方法 */
function renderTableHeight() {
infeedChildRef.value?.renderTableHeight();
}
defineExpose({
renderTableHeight
});
</script>
<template>
<a-row :gutter="24" class="infeed-layout">
<a-col :span="4">
<div class="menu-list">
<span class="infeed-title">进站</span>
<div
v-for="item in menuItems"
:key="item.key"
class="menu-item"
:class="{ active: activeKey === item.key }"
@click="handleMenuClick(item.key)"
>
<div class="menu-content">
<span class="menu-title">{{ item.label }}</span>
<a-progress
:percent="item.progress"
:show-info="false"
size="small"
:stroke-color="activeKey === item.key ? '#1890ff' : undefined"
class="menu-progress"
/>
</div>
</div>
<a-button type="primary" size="large">确认进站</a-button>
</div>
</a-col>
<a-col :span="20" class="content-wrapper">
<router-view v-slot="{ Component }">
<component :is="Component" ref="infeedChildRef" />
</router-view>
</a-col>
</a-row>
</template>
<style scoped lang="scss">
.infeed-layout {
height: 100%;
}
.menu-list {
display: flex;
flex-direction: column;
gap: 16px;
height: 100%;
}
.infeed-title {
font-size: 20px;
font-weight: 600;
border-bottom: 1px solid #c9c9c9;
padding: 0 0 8px;
}
.menu-item {
flex: 1;
background: #fff;
border-radius: 8px;
padding: 1vh 1vw;
cursor: pointer;
transition: all 0.3s;
border: 1px solid #f0f0f0;
box-shadow: 0 2px 4px rgba(0,0,0,0.02);
position: relative;
overflow: hidden;
&.active {
background: #e6f7ff;
border-color: #1890ff;
.menu-title {
color: #1890ff;
}
}
&:hover {
border-color: #47a5fd;
}
}
.menu-content {
display: flex;
flex-direction: column;
justify-content: space-around;
height: 100%;
}
.menu-title {
font-size: 18px;
font-weight: bold;
color: #333;
line-height: 1.5;
}
.menu-progress {
margin-bottom: 0 !important;
:deep(.ant-progress-bg) {
transition: all 0.3s;
}
}
.content-wrapper {
height: 100%;
}
</style>

View File

@@ -0,0 +1,119 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import type { ColumnsType } from 'ant-design-vue/es/table/interface';
interface MaterialTableItem {
key: string;
carrierCode: string;
qrCode: string;
[key: string]: any;
}
const materialColumns = [
{ title: '序号', dataIndex: 'index', key: 'index', align: 'center', width: 80 },
{ title: '载具 ID', dataIndex: 'carrierCode', key: 'carrierCode', align: 'center' },
{ title: '二维码', dataIndex: 'qrCode', key: 'qrCode', align: 'center' },
{ title: '操作', key: 'action', align: 'center', width: 120 },
]
const carrierInput = ref<string>('');
// 录入载具
const insertCarrier = () => {
if (!carrierInput.value) return;
materialTableData.value.push({
key: String(Date.now()),
carrierCode: String(carrierInput.value),
qrCode: String(materialInput.value),
})
carrierInput.value = '';
}
// 主物料表格
const materialTableData = ref<MaterialTableItem[]>([]);
const materialInput = ref<string>('');
// 录入主物料
const insertMaterial = () => {
if (!materialInput.value) return;
materialTableData.value.push({
key: String(Date.now()),
carrierCode: String(carrierInput.value),
qrCode: String(materialInput.value),
})
materialInput.value = '';
}
// 移除主物料
const handleRemoveMaterial = (index: number) => {
console.log(index)
materialTableData.value.splice(index, 1)
}
// 计算表格高度
const customTable = ref<HTMLElement | null>(null)
const tableHeight = ref(200)
const renderTableHeight = () => {
if (customTable.value) {
tableHeight.value = customTable.value.clientHeight - 50
console.log('元素高度:', tableHeight.value)
}
}
onMounted(() => {
renderTableHeight()
})
defineExpose({
renderTableHeight
});
</script>
<template>
<div class="primary-material__container">
<div class="main-title">主物料进站</div>
<a-form layout="inline" :model="{}" size="large">
<a-form-item label="载具 ID">
<a-input v-model:value="carrierInput" @pressEnter="insertCarrier" placeholder="按下回车录入" />
</a-form-item>
<a-form-item label="物料编码">
<a-input v-model:value="materialInput" @pressEnter="insertMaterial" placeholder="按下回车录入" />
</a-form-item>
<a-form-item>
<a-button type="primary" @click="insertMaterial">录入</a-button>
</a-form-item>
</a-form>
<div class="table-wrapper" ref="customTable">
<a-table :dataSource="materialTableData" :columns="materialColumns as ColumnsType<MaterialTableItem>"
:pagination="false" bordered sticky :scroll="{ y: tableHeight }">
<template #bodyCell="{ column, index }">
<template v-if="column.key === 'index'">{{ index + 1 }}</template>
<template v-if="column.key === 'action'">
<a-button type="text" danger @click="handleRemoveMaterial(index)">删除</a-button>
</template>
</template>
</a-table>
</div>
</div>
</template>
<style scoped lang="scss">
.primary-material__container {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
gap: 16px;
overflow: hidden;
height: 100%;
.main-title {
font-size: 20px;
font-weight: 600;
border-bottom: 1px solid #c9c9c9;
padding: 0 0 8px;
}
.table-wrapper {
flex: 1;
overflow: auto;
}
}
</style>

View File

@@ -0,0 +1,103 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import type { ColumnsType } from 'ant-design-vue/es/table/interface';
import { message } from 'ant-design-vue';
interface MaterialTableItem {
key: string;
materialCode: string;
materialName: string;
num: number;
unit: string;
[key: string]: any;
}
const materialColumns = [
{ title: '序号', dataIndex: 'index', key: 'index', align: 'center', width: 80 },
{ title: '物料编码', dataIndex: 'materialCode', key: 'materialCode', align: 'center' },
{ title: '物料名称', dataIndex: 'materialName', key: 'materialName', align: 'center' },
{ title: '数量', dataIndex: 'num', key: 'num', align: 'center' },
{ title: '单位', dataIndex: 'unit', key: 'unit', align: 'center' },
{ title: '操作', key: 'action', align: 'center' },
]
// 材料表格
const materialTableData = ref<MaterialTableItem[]>([
{ key: '1', materialCode: '123', materialName: '物料1', num: 10, unit: '片' },
{ key: '2', materialCode: '123', materialName: '物料1', num: 10, unit: '片' },
{ key: '3', materialCode: '123', materialName: '物料1', num: 10, unit: '片' },
{ key: '4', materialCode: '123', materialName: '物料1', num: 10, unit: '片' },
{ key: '5', materialCode: '123', materialName: '物料1', num: 10, unit: '片' },
{ key: '6', materialCode: '123', materialName: '物料1', num: 10, unit: '片' },
{ key: '7', materialCode: '123', materialName: '物料1', num: 10, unit: '片' },
{ key: '8', materialCode: '123', materialName: '物料1', num: 10, unit: '片' },
{ key: '9', materialCode: '123', materialName: '物料1', num: 10, unit: '片' },
{ key: '10', materialCode: '123', materialName: '物料1', num: 10, unit: '片' },
{ key: '11', materialCode: '123', materialName: '物料1', num: 10, unit: '片' },
{ key: '12', materialCode: '123', materialName: '物料1', num: 10, unit: '片' },
]);
// 确认材料
const handleSubmitMaterial = (index: number) => {
message.success(`${ index + 1 } 号进站`)
}
// 计算表格高度
const customTable = ref<HTMLElement | null>(null)
const tableHeight = ref(200)
const renderTableHeight = () => {
if (customTable.value) {
tableHeight.value = customTable.value.clientHeight - 60
console.log('元素高度:', tableHeight.value)
}
}
onMounted(() => {
renderTableHeight()
})
defineExpose({
renderTableHeight
});
</script>
<template>
<div class="raw-material__container">
<div class="main-title">材料确认</div>
<div class="table-wrapper" ref="customTable">
<a-table :dataSource="materialTableData" :columns="materialColumns as ColumnsType<MaterialTableItem>"
:pagination="false" bordered sticky :scroll="{ y: tableHeight }">
<template #bodyCell="{ column, index }">
<template v-if="column.key === 'index'">{{ index + 1 }}</template>
<template v-if="column.key === 'action'">
<a-button @click="handleSubmitMaterial(index)">确认</a-button>
</template>
</template>
</a-table>
</div>
</div>
</template>
<style scoped lang="scss">
.raw-material__container {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
gap: 16px;
overflow: hidden;
height: 100%;
.main-title {
font-size: 20px;
font-weight: 600;
border-bottom: 1px solid #c9c9c9;
padding: 0 0 8px;
}
.table-wrapper {
flex: 1;
overflow: auto;
}
}
</style>

View File

@@ -0,0 +1,376 @@
<script setup lang="ts">
import { ref, reactive, nextTick } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import Header from '@/components/Header/index.vue';
import { message } from 'ant-design-vue';
import { useAuthStore, usePwoStore } from '@/store';
import type { WorkOrderInfo } from './model';
import { listLotTraceOrder } from '@/api/pwoManage';
import { storeToRefs } from 'pinia';
const route = useRoute();
const router = useRouter();
const authStore = useAuthStore();
const pwoStore = usePwoStore();
const workOrderInfo = reactive<WorkOrderInfo>({
code: '',
batchNo: '',
status: '',
tarMaterialName: '',
tarMaterialCode: '',
planQty: undefined,
});
const processInfo: any = storeToRefs(pwoStore).currentJob;
const loadingPwoInfo = ref(false);
const fetchLotTraceOrder = async () => {
if (!workOrderInfo.code) return;
try {
loadingPwoInfo.value = true;
const res = await listLotTraceOrder({
code: workOrderInfo.code,
});
if (res.total as number <= 0) {
throw new Error('未查询到工单信息,请检查工单编码')
};
workOrderInfo.status = res.rows[0].status;
workOrderInfo.batchNo = res.rows[0].batchNo;
workOrderInfo.tarMaterialName = res.rows[0].tarMaterialName;
workOrderInfo.tarMaterialCode = res.rows[0].tarMaterialCode;
workOrderInfo.planQty = res.rows[0].planQty;
router.replace({ query: { ...route.query, traceOrderCode: workOrderInfo.code, t: Date.now() } });
} catch (error: any) {
message.error(error.message || '获取工单信息失败');
} finally {
loadingPwoInfo.value = false;
}
};
// 孙组件
const infeedRef = ref<any>(null);
const collapsed = ref(false);
const toggleCollapse = async () => {
collapsed.value = !collapsed.value;
await nextTick();
infeedRef.value?.renderTableHeight();
}
const handlePwoManage = () => {
router.push({ name: 'PwoManage' })
}
const handleWaferAppearance = () => {
message.info('点击了 Wafer');
};
const handleDieAppearance = () => {
message.info('点击了 Die');
};
const openHoldModal = ref(false);
const holdForm = reactive({
planCompleteDate: '',
});
const handleHold = () => {
openHoldModal.value = true;
};
const handleCloseHold = () => {
holdForm.planCompleteDate = '';
openHoldModal.value = false;
};
const handleSubmitHold = () => {
message.success('Hold 住!')
holdForm.planCompleteDate = '';
openHoldModal.value = false;
};
const handleInfeed = () => {
router.push({ name: 'Infeed' });
};
const handleOutfeed = () => {
router.push({ name: 'Outfeed' });
};
const handleCopy = async (text: string | number | undefined) => {
if (!text) return;
await navigator.clipboard.writeText(String(text));
message.success(`已复制: ${ text }`);
}
</script>
<template>
<div class="page-container">
<Header title="过站工控机" showHome showBack showLogout>
<template #right-opts>
<a-button @click="toggleCollapse">{{ collapsed ? '展开' : '折叠' }}</a-button>
</template>
</Header>
<div class="content-wrapper">
<!-- Top Section -->
<div class="top-section">
<a-row :gutter="16" class="full-height-row">
<!-- Work Order Info -->
<a-col :span="13">
<a-spin :spinning="loadingPwoInfo">
<a-card title="工单信息" class="info-card" :bordered="false">
<a-form :model="workOrderInfo" :colon="false" v-show="!collapsed">
<a-row :gutter="36">
<a-col :span="12">
<a-form-item label="工单批次">
<a-input v-model:value="workOrderInfo.batchNo" readonly>
<template #suffix>
<a-button @click="handleCopy(workOrderInfo.batchNo)">
<template #icon><i-lucide-copy /></template>
</a-button>
</template>
</a-input>
</a-form-item>
<a-form-item label="工单状态">
<a-input v-model:value="workOrderInfo.status" readonly>
<template #suffix>
<a-button @click="handleCopy(workOrderInfo.status)">
<template #icon><i-lucide-copy /></template>
</a-button>
</template>
</a-input>
</a-form-item>
<a-form-item label="总计数量">
<a-input v-model:value="workOrderInfo.planQty" readonly>
<template #suffix>
<a-button @click="handleCopy(workOrderInfo.planQty)">
<template #icon><i-lucide-copy /></template>
</a-button>
</template>
</a-input>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="产品编码">
<a-input v-model:value="workOrderInfo.tarMaterialCode" readonly>
<template #suffix>
<a-button @click="handleCopy(workOrderInfo.tarMaterialCode)">
<template #icon><i-lucide-copy /></template>
</a-button>
</template>
</a-input>
</a-form-item>
<a-form-item label="产品名称">
<a-input v-model:value="workOrderInfo.tarMaterialName" readonly>
<template #suffix>
<a-button @click="handleCopy(workOrderInfo.tarMaterialName)">
<template #icon><i-lucide-copy /></template>
</a-button>
</template>
</a-input>
</a-form-item>
<a-form-item label="产品规格">
<a-input v-model:value="workOrderInfo.planQty" readonly>
<template #suffix>
<a-button @click="handleCopy(workOrderInfo.planQty)">
<template #icon><i-lucide-copy /></template>
</a-button>
</template>
</a-input>
</a-form-item>
</a-col>
</a-row>
</a-form>
<template #extra>
<a-input-search v-model:value="workOrderInfo.code" @search="fetchLotTraceOrder" placeholder="输入工单编码" enter-button />
</template>
</a-card>
</a-spin>
</a-col>
<!-- Process Info -->
<a-col :span="8">
<a-card title="工序信息" class="info-card" :bordered="false">
<a-form :model="processInfo" :colon="false" :label-col="{ span: 4 }" :wrapper-col="{ span: 20, offset: 2 }"
v-show="!collapsed">
<a-form-item label="工序名称">
<a-input v-model:value="processInfo.operationTitle" readonly>
<template #suffix>
<a-tag color="#f50" v-if="processInfo.status">{{ processInfo.status }}</a-tag>
<a-button @click="handleCopy(processInfo.operationTitle)">
<template #icon><i-lucide-copy /></template>
</a-button>
</template>
</a-input>
</a-form-item>
<a-form-item label="作业编码">
<a-input v-model:value="processInfo.code" readonly>
<template #suffix>
<a-button @click="handleCopy(processInfo.code)">
<template #icon><i-lucide-copy /></template>
</a-button>
</template>
</a-input>
</a-form-item>
<a-form-item label="设备名称">
<a-input v-model:value="processInfo.tarMaterialName" readonly>
<template #suffix>
<a-button @click="handleCopy(processInfo.tarMaterialName)">
<template #icon><i-lucide-copy /></template>
</a-button>
</template>
</a-input>
</a-form-item>
</a-form>
</a-card>
</a-col>
<!-- Action Buttons -->
<a-col :span="3" class="action-buttons-col">
<div class="action-buttons" v-show="!collapsed">
<a-button class="action-btn green-btn" @click="handlePwoManage">工单管理</a-button>
<a-button class="action-btn orange-btn" @click="handleWaferAppearance">Wafer 清单</a-button>
<a-button class="action-btn blue-btn" @click="handleDieAppearance">Die 清单</a-button>
<a-space>
<a-button class="action-btn red-btn" @click="handleHold">Hold</a-button>
<a-button class="action-btn orange-btn" @click="handleInfeed">进站</a-button>
<a-button class="action-btn blue-btn" @click="handleOutfeed">出站</a-button>
</a-space>
</div>
<a-card title="操作" class="info-card" :bordered="false" v-show="collapsed">
<template #extra>
<a-dropdown>
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="handlePwoManage">工单管理</a-menu-item>
<a-menu-item key="2" @click="handleWaferAppearance">Wafer 清单</a-menu-item>
<a-menu-item key="3" @click="handleDieAppearance">Die 清单</a-menu-item>
<a-menu-item key="4" @click="handleHold">Hold</a-menu-item>
<a-menu-item key="5" @click="handleInfeed">进站</a-menu-item>
<a-menu-item key="6" @click="handleOutfeed">出站</a-menu-item>
</a-menu>
</template>
<a-button>
更多
<i-lucide-chevron-down />
</a-button>
</a-dropdown>
</template>
</a-card>
</a-col>
</a-row>
</div>
<!-- Bottom Section -->
<main class="bottom-section">
<router-view v-slot="{ Component }">
<component :is="Component" ref="infeedRef" />
</router-view>
</main>
</div>
<!-- Hold Modal -->
<a-modal
v-model:open="openHoldModal"
title="Hold 操作"
@cancel="handleCloseHold"
@ok="handleSubmitHold"
>
<a-form :model="holdForm" :colon="false" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-form-item label="工单编码">
<a-input v-model:value="workOrderInfo.code" readonly />
</a-form-item>
<a-form-item label="目标产品编码">
<a-input v-model:value="workOrderInfo.tarMaterialCode" readonly />
</a-form-item>
<a-form-item label="目标产品名称">
<a-input v-model:value="workOrderInfo.tarMaterialName" readonly />
</a-form-item>
<a-form-item label="工序名称">
<a-input v-model:value="processInfo.operationTitle" readonly />
</a-form-item>
<a-form-item label="产品规格">
<a-input v-model:value="processInfo.code" readonly />
</a-form-item>
<a-form-item label="计划完成日期">
<a-input v-model:value="holdForm.planCompleteDate" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<style scoped lang="scss">
.page-container {
height: 100vh;
background-color: #f0f2f5;
display: flex;
flex-direction: column;
overflow: hidden;
}
.content-wrapper {
padding: 16px;
flex: 1;
display: flex;
flex-direction: column;
gap: 16px;
min-height: 0;
}
.top-section {
background: transparent;
}
:deep(.ant-spin-nested-loading),
:deep(.ant-spin-container) {
height: 100%;
}
.info-card {
height: 100%;
border-radius: 8px;
:deep(.ant-card-head) {
min-height: 48px;
padding: 0 16px;
border-bottom: none;
font-size: 20px;
}
:deep(.ant-card-body) {
padding: 1px 16px;
}
:deep(.ant-form-item) {
margin-bottom: 12px;
}
}
.action-buttons-col {
display: flex;
flex-direction: column;
}
.action-buttons {
display: flex;
flex-direction: column;
gap: 6px;
height: 100%;
.action-btn {
height: 48px;
font-size: 16px;
color: #fff;
border: none;
border-radius: 4px;
}
}
.bottom-section {
background: #fff;
padding: 16px;
border-radius: 8px;
flex: 1;
min-height: 0;
overflow: auto;
}
</style>

16
src/views/pwoManage/model.d.ts vendored Normal file
View File

@@ -0,0 +1,16 @@
export interface WorkOrderInfo {
code?: string;
batchNo?: string;
status?: string;
tarMaterialName?: string;
tarMaterialCode?: string;
planQty?: number;
}
export interface ProcessInfo {
process?: string;
cut?: string;
jobCode?: string;
status?: string;
equipment?: string;
}

View File

@@ -0,0 +1,9 @@
<script setup lang="ts"></script>
<template>
<div>
outfeed
</div>
</template>
<style scoped lang="scss"></style>