配置 eslint 与 prettier

This commit is contained in:
tao
2026-01-14 16:35:46 +08:00
parent 09b921009e
commit 317f2847ac
65 changed files with 6469 additions and 3487 deletions

5
.prettierignore Normal file
View File

@@ -0,0 +1,5 @@
# 忽略打包输出目录
dist
# 忽略依赖目录
node_modules

25
.prettierrc Normal file
View File

@@ -0,0 +1,25 @@
{
"arrowParens": "always",
"bracketSameLine": false,
"objectWrap": "preserve",
"bracketSpacing": true,
"semi": false,
"experimentalOperatorPosition": "end",
"experimentalTernaries": false,
"singleQuote": true,
"jsxSingleQuote": false,
"quoteProps": "preserve",
"trailingComma": "all",
"singleAttributePerLine": false,
"htmlWhitespaceSensitivity": "ignore",
"vueIndentScriptAndStyle": false,
"proseWrap": "preserve",
"endOfLine": "lf",
"insertPragma": false,
"printWidth": 80,
"requirePragma": false,
"tabWidth": 2,
"useTabs": true,
"embeddedLanguageFormatting": "auto",
"cursorOffset": -1
}

3
commitlint.config.js Normal file
View File

@@ -0,0 +1,3 @@
module.exports = {
extends: ['@commitlint/config-conventional'],
}

53
eslint.config.mjs Normal file
View File

@@ -0,0 +1,53 @@
/**
* ESLint Flat 配置ESLint 9
* 适用于Vue 3 + TypeScript + Prettier
*/
import pluginVue from 'eslint-plugin-vue' // Vue 官方 ESLint 插件
import {
defineConfigWithVueTs, // 用于组合 Vue + TS 的 Flat 配置
vueTsConfigs, // 官方提供的 TypeScript 推荐规则集合
} from '@vue/eslint-config-typescript'
import prettierPlugin from 'eslint-plugin-prettier' // 将 Prettier 作为 ESLint 规则使用
import prettierConfig from 'eslint-config-prettier' // 关闭与 Prettier 冲突的 ESLint 规则
export default defineConfigWithVueTs(
// 1. 全局忽略配置(必须放在最前面)
{
// 不对构建产物和依赖目录进行 ESLint 校验
ignores: ['dist/**', 'node_modules/**'],
},
// 2. Vue 相关基础规则(包含模板语法支持)
pluginVue.configs['flat/essential'],
// 3. TypeScript 推荐规则(脚本逻辑部分)
vueTsConfigs.recommended,
// 4. Prettier 相关配置:只负责格式化,不干扰语义规则
{
// 注册需要用到的插件
plugins: {
prettier: prettierPlugin, // Prettier 插件
},
// 自定义规则
rules: {
// 关闭与 Prettier 冲突的 ESLint 规则
...prettierConfig.rules,
// 将 Prettier 的格式化结果作为 ESLint 的告警输出
'prettier/prettier': 'warn',
},
},
// 5. 自定义规则
{
rules: {
// Vue 规则
'vue/multi-word-component-names': 'off',
// TypeScript 规则
'@typescript-eslint/no-explicit-any': 'off',
},
},
)

6
global.d.ts vendored
View File

@@ -1,4 +1,4 @@
// declare module '@/*'; // declare module '@/*';
declare module '@/components/*'; declare module '@/components/*'
declare module '@/views/*'; declare module '@/views/*'
declare module '@/api/*'; declare module '@/api/*'

10
lint-staged.config.cjs Normal file
View File

@@ -0,0 +1,10 @@
module.exports = {
'*.{js,jsx,ts,tsx}': ['eslint --fix', 'prettier --write'],
'{!(package)*.json,*.code-snippets,.!(browserslist)*rc}': [
'prettier --write--parser json',
],
'package.json': ['prettier --write'],
'*.vue': ['eslint --fix', 'prettier --write', 'stylelint --fix'],
'*.{scss,less,style,html}': ['stylelint --fix', 'prettier --write'],
'*.md': ['prettier --write'],
}

2248
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,10 @@
"scripts": { "scripts": {
"dev": "vite --host --open", "dev": "vite --host --open",
"build": "vite build", "build": "vite build",
"preview": "vite preview" "preview": "vite preview",
"lint": "eslint \"src/**/*.{ts,vue}\"",
"lint:fix": "eslint \"src/**/*.{ts,vue}\" --fix",
"format": "prettier --write \"src/**/*.{ts,vue,js,json,css,md}\""
}, },
"dependencies": { "dependencies": {
"@types/lodash": "^4.17.21", "@types/lodash": "^4.17.21",
@@ -24,14 +27,29 @@
"@types/event-source-polyfill": "^1.0.5", "@types/event-source-polyfill": "^1.0.5",
"@types/js-cookie": "^3.0.6", "@types/js-cookie": "^3.0.6",
"@types/node": "^24.0.3", "@types/node": "^24.0.3",
"@typescript-eslint/eslint-plugin": "^8.53.0",
"@typescript-eslint/parser": "^8.53.0",
"@vitejs/plugin-vue": "^5.2.3", "@vitejs/plugin-vue": "^5.2.3",
"@vue/eslint-config-typescript": "^14.6.0",
"@vue/tsconfig": "^0.7.0", "@vue/tsconfig": "^0.7.0",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-vue": "^10.6.2",
"event-source-polyfill": "^1.0.31", "event-source-polyfill": "^1.0.31",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"lint-staged": "^16.2.7",
"prettier": "^3.7.4",
"sass-embedded": "^1.89.2", "sass-embedded": "^1.89.2",
"typescript": "~5.8.3", "typescript": "~5.8.3",
"unplugin-icons": "^22.2.0", "unplugin-icons": "^22.2.0",
"vite": "^6.3.5", "vite": "^6.3.5",
"vue-tsc": "^2.2.8" "vue-tsc": "^2.2.8"
},
"lint-staged": {
"*.{ts,js,vue}": [
"eslint --fix",
"prettier --write"
]
} }
} }

View File

@@ -1,10 +1,10 @@
<template> <template>
<a-config-provider :locale="zhCN"> <a-config-provider :locale="zhCN">
<router-view /> <router-view />
</a-config-provider> </a-config-provider>
</template> </template>
<script setup> <script setup lang="ts">
import zhCN from 'ant-design-vue/es/locale/zh_CN'; import zhCN from 'ant-design-vue/es/locale/zh_CN'
import 'dayjs/locale/zh-cn'; import 'dayjs/locale/zh-cn'
</script> </script>

40
src/api/common.d.ts vendored
View File

@@ -1,11 +1,11 @@
export type ID = number | string; export type ID = number | string
export type IDS = (number | string)[]; export type IDS = (number | string)[]
export interface BaseEntity { export interface BaseEntity {
createBy?: string; createBy?: string
createTime?: string; createTime?: string
updateBy?: string; updateBy?: string
updateTime?: string; updateTime?: string
} }
/** /**
@@ -16,24 +16,24 @@ export interface BaseEntity {
* @param isAsc 是否升序 * @param isAsc 是否升序
*/ */
export interface PageQuery { export interface PageQuery {
isAsc?: string; isAsc?: string
orderByColumn?: string; orderByColumn?: string
pageNum?: number; pageNum?: number
pageSize?: number; pageSize?: number
[key: string]: any; [key: string]: any
} }
export interface ApiResponse<T = any> { export interface ApiResponse<T = any> {
code?: number; code?: number
msg?: string; msg?: string
data?: T; data?: T
rows: T[]; rows: T[]
total?: number; total?: number
token?: string; token?: string
img?: string; img?: string
uuid?: string; uuid?: string
captchaOnOff?: boolean; captchaOnOff?: boolean
} }

View File

@@ -1,93 +1,93 @@
import axios from "axios"; import axios from 'axios'
import router from "@/router"; import router from '@/router'
import { getToken, removeToken } from "@/utils/auth"; import { getToken, removeToken } from '@/utils/auth'
import { Modal, notification } from "ant-design-vue"; import { Modal, notification } from 'ant-design-vue'
import type { AxiosInstance, AxiosRequestConfig } from "axios"; import type { AxiosInstance, AxiosRequestConfig } from 'axios'
import type { ApiResponse } from "@/api/common"; import type { ApiResponse } from '@/api/common'
const errCodeMap: { [key: string]: string } = { const errCodeMap: { [key: string]: string } = {
"403": "当前操作没有权限", '403': '当前操作没有权限',
"404": "访问资源不存在", '404': '访问资源不存在',
default: "系统未知错误,请反馈给管理员", default: '系统未知错误,请反馈给管理员',
}; }
// 创建axios实例 // 创建axios实例
const service: AxiosInstance = axios.create({ const service: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API, baseURL: import.meta.env.VITE_APP_BASE_API,
timeout: 10000, timeout: 10000,
}); })
// 请求拦截器 // 请求拦截器
service.interceptors.request.use( service.interceptors.request.use(
(config) => { (config) => {
config.headers = config.headers || {}; config.headers = config.headers || {}
config.headers["Accept-Language"] = "zh-CN"; config.headers['Accept-Language'] = 'zh-CN'
const token = getToken(); const token = getToken()
if (token) { if (token) {
config.headers["Authorization"] = `Bearer ${token}`; config.headers['Authorization'] = `Bearer ${token}`
} }
return config; return config
}, },
(error) => { (error) => {
return Promise.reject(error); return Promise.reject(error)
} },
); )
// 响应拦截器 // 响应拦截器
service.interceptors.response.use( service.interceptors.response.use(
(response) => { (response) => {
if (response.config.responseType === "blob") { if (response.config.responseType === 'blob') {
return response.data; return response.data
} }
const code = response.data.code || 200; const code = response.data.code || 200
const data = response.data; const data = response.data
switch (code) { switch (code) {
case 200: case 200:
return data; return data
case 401: case 401:
Modal.error({ Modal.error({
title: "系统提示", title: '系统提示',
content: "登录状态已过期,请重新登录", content: '登录状态已过期,请重新登录',
onOk: () => { onOk: () => {
removeToken(); removeToken()
router.replace({ name: 'Login' }); router.replace({ name: 'Login' })
}, },
}); })
return Promise.reject(data); return Promise.reject(data)
case 500: case 500:
return Promise.reject(new Error(data.msg)); return Promise.reject(new Error(data.msg))
default: default:
notification.error({ notification.error({
message: "请求错误", message: '请求错误',
description: errCodeMap[code] || data?.msg || errCodeMap.default, description: errCodeMap[code] || data?.msg || errCodeMap.default,
}); })
return Promise.reject(data); return Promise.reject(data)
} }
}, },
(error) => { (error) => {
if (!error.__handled) { if (!error.__handled) {
const message = const message =
error.code === "ECONNABORTED" error.code === 'ECONNABORTED'
? "请求超时,请稍后重试" ? '请求超时,请稍后重试'
: error.message || "网络异常"; : error.message || '网络异常'
notification.error({ notification.error({
message: "网络错误", message: '网络错误',
description: message, description: message,
}); })
error.__handled = true; error.__handled = true
} }
return Promise.reject(error); return Promise.reject(error)
} },
); )
function request<T = any>(config: AxiosRequestConfig): Promise<ApiResponse<T>> { function request<T = any>(config: AxiosRequestConfig): Promise<ApiResponse<T>> {
return service(config) as Promise<ApiResponse<T>>; return service(config) as Promise<ApiResponse<T>>
} }
export default request; export default request

View File

@@ -2,51 +2,51 @@ import request from '@/api/request'
// 查询字典数据列表 // 查询字典数据列表
export function listData(params: any) { export function listData(params: any) {
return request({ return request({
url: '/system/dict/data/list', url: '/system/dict/data/list',
method: 'get', method: 'get',
params params,
}) })
} }
// 查询字典数据详细 // 查询字典数据详细
export function getData(dictCode: string) { export function getData(dictCode: string) {
return request({ return request({
url: '/system/dict/data/' + dictCode, url: '/system/dict/data/' + dictCode,
method: 'get' method: 'get',
}) })
} }
// 根据字典类型查询字典数据信息 // 根据字典类型查询字典数据信息
export function getDicts(dictType: string) { export function getDicts(dictType: string) {
return request({ return request({
url: '/system/dict/data/type/' + dictType, url: '/system/dict/data/type/' + dictType,
method: 'get' method: 'get',
}) })
} }
// 新增字典数据 // 新增字典数据
export function addData(data: any) { export function addData(data: any) {
return request({ return request({
url: '/system/dict/data', url: '/system/dict/data',
method: 'post', method: 'post',
data data,
}) })
} }
// 修改字典数据 // 修改字典数据
export function updateData(data: any) { export function updateData(data: any) {
return request({ return request({
url: '/system/dict/data', url: '/system/dict/data',
method: 'put', method: 'put',
data data,
}) })
} }
// 删除字典数据 // 删除字典数据
export function delData(dictCode: string) { export function delData(dictCode: string) {
return request({ return request({
url: '/system/dict/data/' + dictCode, url: '/system/dict/data/' + dictCode,
method: 'delete' method: 'delete',
}) })
} }

View File

@@ -1,2 +1,2 @@
export * from './data' export * from './data'
export * from './type' export * from './type'

View File

@@ -3,59 +3,59 @@ import type { ID } from '@/api/common'
// 查询字典类型列表 // 查询字典类型列表
export function listType(params: any) { export function listType(params: any) {
return request({ return request({
url: "/system/dict/type/list", url: '/system/dict/type/list',
method: "get", method: 'get',
params, params,
}); })
} }
// 查询字典类型详细 // 查询字典类型详细
export function getType(dictId: ID) { export function getType(dictId: ID) {
return request({ return request({
url: "/system/dict/type/" + dictId, url: '/system/dict/type/' + dictId,
method: "get", method: 'get',
}); })
} }
// 新增字典类型 // 新增字典类型
export function addType(data: any) { export function addType(data: any) {
return request({ return request({
url: "/system/dict/type", url: '/system/dict/type',
method: "post", method: 'post',
data, data,
}); })
} }
// 修改字典类型 // 修改字典类型
export function updateType(data: any) { export function updateType(data: any) {
return request({ return request({
url: "/system/dict/type", url: '/system/dict/type',
method: "put", method: 'put',
data, data,
}); })
} }
// 删除字典类型 // 删除字典类型
export function delType(dictId: ID) { export function delType(dictId: ID) {
return request({ return request({
url: "/system/dict/type/" + dictId, url: '/system/dict/type/' + dictId,
method: "delete", method: 'delete',
}); })
} }
// 刷新字典缓存 // 刷新字典缓存
export function refreshCache() { export function refreshCache() {
return request({ return request({
url: "/system/dict/type/refreshCache", url: '/system/dict/type/refreshCache',
method: "delete", method: 'delete',
}); })
} }
// 获取字典选择框列表 // 获取字典选择框列表
export function getOptions() { export function getOptions() {
return request({ return request({
url: "/system/dict/type/optionselect", url: '/system/dict/type/optionselect',
method: "get", method: 'get',
}); })
} }

View File

@@ -1,27 +1,27 @@
import request from '@/api/request' import request from '@/api/request'
import type { LoginInfo } from './model'; import type { LoginInfo } from './model'
// 用户登录 // 用户登录
export function login(data: LoginInfo) { export function login(data: LoginInfo) {
return request({ return request({
url: '/login', url: '/login',
method: 'post', method: 'post',
data data,
}) })
} }
// 获取验证码 // 获取验证码
export function getCaptcha() { export function getCaptcha() {
return request({ return request({
url: '/captchaImage', url: '/captchaImage',
method: 'get' method: 'get',
}) })
} }
// 退出登录 // 退出登录
export function logout() { export function logout() {
return request({ return request({
url: '/logout', url: '/logout',
method: 'post' method: 'post',
}) })
} }

View File

@@ -1,6 +1,6 @@
export interface LoginInfo { export interface LoginInfo {
username: string username: string
password: string password: string
uuid: string uuid: string
code: string code: string
} }

View File

@@ -1,36 +1,36 @@
import request from "@/api/request"; import request from '@/api/request'
import type { ID } from "@/api/common"; import type { ID } from '@/api/common'
// 查询设备管理详细 // 查询设备管理详细
export function getEquipmentByCode(code: string) { export function getEquipmentByCode(code: string) {
return request({ return request({
url: '/basic/equipment/code/' + code, url: '/basic/equipment/code/' + code,
method: 'get' method: 'get',
}) })
} }
// 查询设备进站记录列表 // 查询设备进站记录列表
export function listEquipmentEntry(params: any) { export function listEquipmentEntry(params: any) {
return request({ return request({
url: "/mes/equipment-entry-log/list", url: '/mes/equipment-entry-log/list',
method: "get", method: 'get',
params, params,
}); })
} }
// 新增设备进站记录 // 新增设备进站记录
export function addEquipmentEntry(data: any) { export function addEquipmentEntry(data: any) {
return request({ return request({
url: "/mes/equipment-entry-log", url: '/mes/equipment-entry-log',
method: "post", method: 'post',
data, data,
}); })
} }
// 删除设备进站记录 // 删除设备进站记录
export function delEquipmentEntry(id: ID) { export function delEquipmentEntry(id: ID) {
return request({ return request({
url: "/mes/equipment-entry-log/" + id, url: '/mes/equipment-entry-log/' + id,
method: "delete", method: 'delete',
}); })
} }

View File

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

View File

@@ -1,54 +1,54 @@
import request from "@/api/request"; import request from '@/api/request'
import type { ID } from "@/api/common"; import type { ID } from '@/api/common'
// 查询库位列表 // 查询库位列表
export function listStorageLocation(params: any) { export function listStorageLocation(params: any) {
return request({ return request({
url: "/wip/storageLocation/list", url: '/wip/storageLocation/list',
method: "get", method: 'get',
params, params,
}); })
} }
// 高级查询库位列表 // 高级查询库位列表
export function advListStorageLocation(params: any) { export function advListStorageLocation(params: any) {
return request({ return request({
url: "/wip/storageLocation/advList", url: '/wip/storageLocation/advList',
method: "get", method: 'get',
params, params,
}); })
} }
// 查询库位详细 // 查询库位详细
export function getStorageLocation(id: ID) { export function getStorageLocation(id: ID) {
return request({ return request({
url: "/wip/storageLocation/" + id, url: '/wip/storageLocation/' + id,
method: "get", method: 'get',
}); })
} }
// 新增库位 // 新增库位
export function addStorageLocation(data: any) { export function addStorageLocation(data: any) {
return request({ return request({
url: "/wip/storageLocation", url: '/wip/storageLocation',
method: "post", method: 'post',
data, data,
}); })
} }
// 修改库位 // 修改库位
export function updateStorageLocation(data: any) { export function updateStorageLocation(data: any) {
return request({ return request({
url: "/wip/storageLocation", url: '/wip/storageLocation',
method: "put", method: 'put',
data, data,
}); })
} }
// 删除库位 // 删除库位
export function delStorageLocation(id: ID) { export function delStorageLocation(id: ID) {
return request({ return request({
url: "/wip/storageLocation/" + id, url: '/wip/storageLocation/' + id,
method: "delete", method: 'delete',
}); })
} }

View File

@@ -1,37 +1,37 @@
import request from "@/api/request"; import request from '@/api/request'
import type { ID } from "@/api/common"; import type { ID } from '@/api/common'
// 查询站点绑定治具列表 // 查询站点绑定治具列表
export function listStationBindMask(params: any) { export function listStationBindMask(params: any) {
return request({ return request({
url: "/mes/station/mask/list", url: '/mes/station/mask/list',
method: "get", method: 'get',
params, params,
}); })
} }
// 为站点绑定治具 // 为站点绑定治具
export function batchBindMasksToStation(stationId: ID, maskIds: ID[]) { export function batchBindMasksToStation(stationId: ID, maskIds: ID[]) {
return request({ return request({
url: "/mes/station/" + stationId + "/bind/mask", url: '/mes/station/' + stationId + '/bind/mask',
method: "put", method: 'put',
data: maskIds, data: maskIds,
}); })
} }
// 为站点绑定治具组 // 为站点绑定治具组
export function bindMaskCombinationToStations(data: any) { export function bindMaskCombinationToStations(data: any) {
return request({ return request({
url: "/mes/station/bind/mask-combination", url: '/mes/station/bind/mask-combination',
method: "put", method: 'put',
data, data,
}); })
} }
// 站点解绑治具 // 站点解绑治具
export function unbindStationMask(stationMaskId: ID) { export function unbindStationMask(stationMaskId: ID) {
return request({ return request({
url: "/mes/station/bind-mask/" + stationMaskId, url: '/mes/station/bind-mask/' + stationMaskId,
method: "delete", method: 'delete',
}); })
} }

View File

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

View File

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

View File

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

View File

@@ -1,2 +1,2 @@
export * from './infeed' export * from './infeed'
export * from './outfeed' export * from './outfeed'

View File

@@ -1,61 +1,61 @@
import request from "@/api/request"; import request from '@/api/request'
import type { ID } from "@/api/common"; import type { ID } from '@/api/common'
// 查询主材进站列表 // 查询主材进站列表
export function listMainMaterialEntryLog(params: any) { export function listMainMaterialEntryLog(params: any) {
return request({ return request({
url: "/mes/station/entry-log/main-material/list", url: '/mes/station/entry-log/main-material/list',
method: "get", method: 'get',
params, params,
}); })
} }
// 查询主材进站详细 // 查询主材进站详细
export function getMainMaterialEntryLog(id: ID) { export function getMainMaterialEntryLog(id: ID) {
return request({ return request({
url: "/mes/station/entry-log/main-material/" + id, url: '/mes/station/entry-log/main-material/' + id,
method: "get", method: 'get',
}); })
} }
// 新增主材Wafer进站 // 新增主材Wafer进站
export function addWaferEntryLog(data: any) { export function addWaferEntryLog(data: any) {
return request({ return request({
url: "/mes/station/entry-log/main-material/wafer", url: '/mes/station/entry-log/main-material/wafer',
method: "post", method: 'post',
data, data,
}); })
} }
// 通过载具新增主材Wafer进站 // 通过载具新增主材Wafer进站
export function addWaferEntryLogByCarrier(data: any) { export function addWaferEntryLogByCarrier(data: any) {
return request({ return request({
url: "/mes/station/entry-log/main-material/wafer/by-carrier", url: '/mes/station/entry-log/main-material/wafer/by-carrier',
method: "post", method: 'post',
data, data,
}); })
} }
// 新增主材Die进站 // 新增主材Die进站
export function addDieEntryLog(data: any) { export function addDieEntryLog(data: any) {
return request({ return request({
url: "/mes/station/entry-log/main-material/die", url: '/mes/station/entry-log/main-material/die',
method: "post", method: 'post',
data, data,
}); })
} }
// 通过载具新增主材Die进站 // 通过载具新增主材Die进站
export function addDieEntryLogByCarrier(data: any) { export function addDieEntryLogByCarrier(data: any) {
return request({ return request({
url: "/mes/station/entry-log/main-material/die/by-carrier", url: '/mes/station/entry-log/main-material/die/by-carrier',
method: "post", method: 'post',
data, data,
}); })
} }
// 删除主材进站 // 删除主材进站
export function delMainMaterialEntryLog(id: ID) { export function delMainMaterialEntryLog(id: ID) {
return request({ return request({
url: "/mes/station/entry-log/main-material/" + id, url: '/mes/station/entry-log/main-material/' + id,
method: "delete", method: 'delete',
}); })
} }

View File

@@ -1,54 +1,54 @@
import request from "@/api/request"; import request from '@/api/request'
import type { ID } from "@/api/common"; import type { ID } from '@/api/common'
// 查询主材出站列表 // 查询主材出站列表
export function listMainMaterialOutboundLog(params: any) { export function listMainMaterialOutboundLog(params: any) {
return request({ return request({
url: "/mes/station/out-log/main-material/list", url: '/mes/station/out-log/main-material/list',
method: "get", method: 'get',
params, params,
}); })
} }
// 查询主材出站详细 // 查询主材出站详细
export function getMainMaterialOutboundLog(id: ID) { export function getMainMaterialOutboundLog(id: ID) {
return request({ return request({
url: "/mes/station/out-log/main-material/" + id, url: '/mes/station/out-log/main-material/' + id,
method: "get", method: 'get',
}); })
} }
// 新增主材出站 // 新增主材出站
export function batchAddMainMaterialOutboundLog(data: any) { export function batchAddMainMaterialOutboundLog(data: any) {
return request({ return request({
url: "/mes/station/out-log/main-material/batch", url: '/mes/station/out-log/main-material/batch',
method: "post", method: 'post',
data, data,
}); })
} }
// 新增主材出站 // 新增主材出站
export function addWaferDieOutboundLogBySpecifiedNg(data: any) { export function addWaferDieOutboundLogBySpecifiedNg(data: any) {
return request({ return request({
url: "/mes/station/out-log/main-material/wafer-die/by-specified-ng", url: '/mes/station/out-log/main-material/wafer-die/by-specified-ng',
method: "post", method: 'post',
data, data,
}); })
} }
// 修改主材出站 // 修改主材出站
export function updateMainMaterialOutboundLog(data: any) { export function updateMainMaterialOutboundLog(data: any) {
return request({ return request({
url: '/mes/station/out-log/main-material', url: '/mes/station/out-log/main-material',
method: 'put', method: 'put',
data data,
}) })
} }
// 删除主材出站 // 删除主材出站
export function delMainMaterialOutboundLog(id: ID) { export function delMainMaterialOutboundLog(id: ID) {
return request({ return request({
url: "/mes/station/out-log/main-material/" + id, url: '/mes/station/out-log/main-material/' + id,
method: "delete", method: 'delete',
}); })
} }

View File

@@ -1,80 +1,80 @@
import request from "@/api/request"; import request from '@/api/request'
import type { ID } from "@/api/common"; import type { ID } from '@/api/common'
import type { MesStationQuery, MesStationData } from "./model"; import type { MesStationQuery, MesStationData } from './model'
// 查询站点列表 // 查询站点列表
export function listStation(params: MesStationQuery) { export function listStation(params: MesStationQuery) {
return request({ return request({
url: "/mes/station/list", url: '/mes/station/list',
method: "get", method: 'get',
params, params,
}); })
} }
// 高级查询站点列表 // 高级查询站点列表
export function advListStation(params: MesStationQuery) { export function advListStation(params: MesStationQuery) {
return request({ return request({
url: "/mes/station/advList", url: '/mes/station/advList',
method: "get", method: 'get',
params, params,
}); })
} }
// 查询站点详细 // 查询站点详细
export function getStation(id: ID) { export function getStation(id: ID) {
return request({ return request({
url: "/mes/station/" + id, url: '/mes/station/' + id,
method: "get", method: 'get',
}); })
} }
// 判断站点是否为最后一个站点 // 判断站点是否为最后一个站点
export function isLastStation(id: ID) { export function isLastStation(id: ID) {
return request({ return request({
url: '/mes/station/' + id + '/isLast', url: '/mes/station/' + id + '/isLast',
method: 'get' method: 'get',
}) })
} }
// 新增站点 // 新增站点
export function addStation(data: MesStationData) { export function addStation(data: MesStationData) {
return request({ return request({
url: "/mes/station", url: '/mes/station',
method: "post", method: 'post',
data, data,
}); })
} }
// 修改站点 // 修改站点
export function updateStation(data: MesStationData) { export function updateStation(data: MesStationData) {
return request({ return request({
url: "/mes/station", url: '/mes/station',
method: "put", method: 'put',
data, data,
}); })
} }
// 删除站点 // 删除站点
export function delStation(id: ID) { export function delStation(id: ID) {
return request({ return request({
url: "/mes/station/" + id, url: '/mes/station/' + id,
method: "delete", method: 'delete',
}); })
} }
// 站点开工 // 站点开工
export function startStation(id: ID) { export function startStation(id: ID) {
return request({ return request({
url: "/mes/station/" + id + "/start", url: '/mes/station/' + id + '/start',
method: "put", method: 'put',
}); })
} }
// 站点完工 // 站点完工
export function completeStation(id: ID, location: any) { export function completeStation(id: ID, location: any) {
return request({ return request({
url: '/mes/station/' + id + '/complete', url: '/mes/station/' + id + '/complete',
method: 'put', method: 'put',
data: location data: location,
}) })
} }

View File

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

View File

@@ -1,123 +1,155 @@
<template> <template>
<div> <div>
<template v-for="(item, index) in options"> <template v-for="(item, index) in options">
<template v-if="isValueMatch(item.value)"> <template v-if="isValueMatch(item.value)">
<span <span
v-if="(item.elTagType == 'default' || item.elTagType == '') && (item.elTagClass == '' || item.elTagClass == null)" v-if="
:key="item.value" :index="index" :class="item.elTagClass">{{ item.label + " " }}</span> (item.elTagType == 'default' || item.elTagType == '') &&
<a-tag v-else :disable-transitions="true" :key="item.value + ''" :index="index" :color="getColor(item.elTagType)" (item.elTagClass == '' || item.elTagClass == null)
:class="item.elTagClass + ' ' + size">{{ item.label + " " }}</a-tag> "
</template> :key="item.value"
</template> :index="index"
<template v-if="unmatch && showValue"> :class="item.elTagClass"
{{ unmatchArray || handleArray }} >
</template> {{ item.label + ' ' }}
</div> </span>
<a-tag
v-else
:disable-transitions="true"
:key="item.value + ''"
:index="index"
:color="getColor(item.elTagType)"
:class="item.elTagClass + ' ' + size"
>
{{ item.label + ' ' }}
</a-tag>
</template>
</template>
<template v-if="unmatch && showValue">
{{ unmatchArray || handleArray }}
</template>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
interface DictOption { interface DictOption {
label: string; label: string
value: string; value: string
elTagType?: string; elTagType?: string
elTagClass?: string; elTagClass?: string
} }
// 记录未匹配的项 // 记录未匹配的项
const unmatchArray = ref<any>([]) const unmatchArray = ref<any>([])
const props = defineProps({ const props = defineProps({
// 数据 // 数据
options: { options: {
type: Array as () => Array<DictOption>, type: Array as () => Array<DictOption>,
default: null, default: null,
}, },
// 当前的值 // 当前的值
value: [Number, String, Array], value: [Number, String, Array],
// 当未找到匹配的数据时显示value // 当未找到匹配的数据时显示value
showValue: { showValue: {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
separator: { separator: {
type: String, type: String,
default: ",", default: ',',
}, },
size: { size: {
type: String, type: String,
default: 'mini', default: 'mini',
}, },
}) })
const values = computed(() => { const values = computed(() => {
if (props.value === null || typeof props.value === 'undefined' || props.value === '') return [] if (
if (typeof props.value === 'number' || typeof props.value === 'boolean') return [props.value] props.value === null ||
return Array.isArray(props.value) ? props.value.map(item => '' + item) : String(props.value).split(props.separator) typeof props.value === 'undefined' ||
props.value === ''
)
return []
if (typeof props.value === 'number' || typeof props.value === 'boolean')
return [props.value]
return Array.isArray(props.value)
? props.value.map((item) => '' + item)
: String(props.value).split(props.separator)
}) })
const unmatch = computed(() => { const unmatch = computed(() => {
unmatchArray.value = [] // eslint-disable-next-line vue/no-side-effects-in-computed-properties
// 没有value不显示 unmatchArray.value = []
if (props.value === null || typeof props.value === 'undefined' || props.value === '' || !Array.isArray(props.options) || props.options.length === 0) return false // 没有value不显示
// 传入值为数组 if (
let unmatch = false // 添加一个标志来判断是否有未匹配项 props.value === null ||
values.value.forEach(item => { typeof props.value === 'undefined' ||
if (!props.options.some(v => v.value == item)) { props.value === '' ||
unmatchArray.value.push(item) !Array.isArray(props.options) ||
unmatch = true // 如果有未匹配项将标志设置为true props.options.length === 0
} )
}) return false
return unmatch // 返回标志的值 // 传入值为数组
let unmatch = false // 添加一个标志来判断是否有未匹配项
values.value.forEach((item) => {
if (!props.options.some((v) => v.value == item)) {
unmatchArray.value.push(item)
unmatch = true // 如果有未匹配项将标志设置为true
}
})
return unmatch // 返回标志的值
}) })
function handleArray(array: string[]) { function handleArray(array: string[]) {
if (array.length === 0) return "" if (array.length === 0) return ''
return array.reduce((pre, cur) => { return array.reduce((pre, cur) => {
return pre + " " + cur return pre + ' ' + cur
}) })
} }
function isValueMatch(itemValue: string) { function isValueMatch(itemValue: string) {
return values.value.some(val => val == itemValue) return values.value.some((val) => val == itemValue)
} }
function getColor(tagType: string | undefined) { function getColor(tagType: string | undefined) {
switch (tagType) { switch (tagType) {
case 'primary': case 'primary':
return 'processing' return 'processing'
case 'success': case 'success':
return 'success' return 'success'
case 'info': case 'info':
return 'info' return 'info'
case 'warning': case 'warning':
return 'warning' return 'warning'
case 'danger': case 'danger':
return 'error' return 'error'
default: default:
return 'default' return 'default'
} }
} }
</script> </script>
<style scoped> <style scoped>
.ant-tag+.ant-tag { .ant-tag + .ant-tag {
margin-left: 10px; margin-left: 10px;
} }
.ant-tag{ .ant-tag {
&.large { &.large {
font-size: 18px; font-size: 18px;
line-height: 28px; line-height: 28px;
} }
&.medium { &.medium {
font-size: 16px; font-size: 16px;
line-height: 24px; line-height: 24px;
} }
&.mini { &.mini {
font-size: 12px; font-size: 12px;
line-height: 20px; line-height: 20px;
} }
} }
</style> </style>

View File

@@ -1,143 +1,160 @@
<template> <template>
<header class="header-container" :class="{ 'hide-shadow': hideShadow }" <header
:style="{ height, zIndex, lineHeight: height }"> class="header-container"
<div class="opts left-opts" v-if="$slots['left-opts'] || title || $slots.title"> :class="{ 'hide-shadow': hideShadow }"
<a-button v-if="showBack" @click="back">返回</a-button> :style="{ height, zIndex, lineHeight: height }"
<a-button v-if="showHome" @click="backToHome">首页</a-button> >
<slot name="left-opts" /> <div
</div> class="opts left-opts"
<div class="title" v-if="title || $slots.title"> v-if="$slots['left-opts'] || title || $slots.title"
<slot name="title"> >
{{ title }} <a-button v-if="showBack" @click="back">返回</a-button>
</slot> <a-button v-if="showHome" @click="backToHome">首页</a-button>
</div> <slot name="left-opts" />
<div class="opts right-opts" v-if="$slots['right-opts'] || title || $slots.title"> </div>
<slot name="right-opts" /> <div class="title" v-if="title || $slots.title">
<a-button @click="handleLogout" type="primary" danger <slot name="title">
v-if="showLogout && username">退出{{ username }}</a-button> {{ title }}
</div> </slot>
<slot /> </div>
</header> <div
class="opts right-opts"
v-if="$slots['right-opts'] || title || $slots.title"
>
<slot name="right-opts" />
<a-button
@click="handleLogout"
type="primary"
danger
v-if="showLogout && username"
>
退出{{ username }}
</a-button>
</div>
<slot />
</header>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router'
import { Modal } from 'ant-design-vue'; import { Modal } from 'ant-design-vue'
import { useAuthStore, useUserStore } from '@/store'; import { useAuthStore, useUserStore } from '@/store'
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia'
defineProps({ defineProps({
showHome: { showHome: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
showBack: { showBack: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
showLogout: { showLogout: {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
title: { title: {
type: String, type: String,
default: null, default: null,
}, },
hideShadow: { hideShadow: {
type: Boolean, type: Boolean,
default: false default: false,
}, },
height: { height: {
type: String, type: String,
default: '', default: '',
}, },
zIndex: { zIndex: {
type: Number, type: Number,
default: 999, default: 999,
} },
}); })
defineSlots<{ defineSlots<{
'default'(): any; 'default'(): any
'left-opts'(): any; 'left-opts'(): any
'title'(): any; 'title'(): any
'right-opts'(): any; 'right-opts'(): any
}>(); }>()
const emit = defineEmits(['back']); const emit = defineEmits(['back'])
const router = useRouter(); const router = useRouter()
const userStore = useUserStore(); const userStore = useUserStore()
const { username } = storeToRefs(userStore); const { username } = storeToRefs(userStore)
const authStore = useAuthStore(); const authStore = useAuthStore()
const handleLogout = () => { const handleLogout = () => {
Modal.confirm({ Modal.confirm({
title: '提示', title: '提示',
content: `是否确认退出登录:${ username.value }`, content: `是否确认退出登录:${username.value}`,
okText: '确定', okText: '确定',
cancelText: '取消', cancelText: '取消',
onOk: () => { onOk: () => {
authStore.logout(); authStore.logout()
}, },
}); })
}; }
const back = () => { const back = () => {
emit('back'); emit('back')
defaultBack(); defaultBack()
}; }
const defaultBack = () => { const defaultBack = () => {
router.go(-1); router.go(-1)
}; }
const backToHome = () => { const backToHome = () => {
router.push({ name: 'Index' }); router.push({ name: 'Index' })
}; }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.header-container { .header-container {
width: 100%; width: 100%;
padding: 6px 1rem; padding: 6px 1rem;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
background-color: #1f2e54; background-color: #1f2e54;
color: #fff; 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 {
box-shadow: none; box-shadow: none;
} }
.title { .title {
flex: 14; flex: 14;
color: #fff; color: #fff;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-align: center; text-align: center;
text-overflow: ellipsis; text-overflow: ellipsis;
font-size: clamp(16px, 2.5vw, 32px); font-size: clamp(16px, 2.5vw, 32px);
font-weight: bold; font-weight: bold;
} }
.opts { .opts {
height: 100%; height: 100%;
flex: 5; flex: 5;
display: flex; display: flex;
align-items: center; align-items: center;
flex-shrink: 0; flex-shrink: 0;
gap: 8px; gap: 8px;
&.left-opts { &.left-opts {
justify-content: flex-start; justify-content: flex-start;
} }
&.right-opts { &.right-opts {
justify-content: flex-end; justify-content: flex-end;
} }
} }
} }
</style> </style>

View File

@@ -1,45 +1,46 @@
<template> <template>
<div class="title"> <div class="title">
<span>{{ name }}</span> <span>{{ name }}</span>
<a-space class="subtitle"> <a-space class="subtitle">
<slot /> <slot />
<a-button v-if="showRefresh" size="small" @click="handleRefresh"> <a-button v-if="showRefresh" size="small" @click="handleRefresh">
<template #icon><i-lucide-rotate-ccw /></template>刷新 <template #icon><i-lucide-rotate-ccw /></template>
</a-button> 刷新
</a-space> </a-button>
</div> </a-space>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
defineProps({ defineProps({
name: { name: {
type: String, type: String,
default: '', default: '',
}, },
showRefresh: { showRefresh: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
}); })
const slots = defineSlots<{ defineSlots<{
'default'(): any; 'default'(): any
}>(); }>()
const emit = defineEmits(['refresh']); const emit = defineEmits(['refresh'])
const handleRefresh = () => { const handleRefresh = () => {
emit('refresh'); emit('refresh')
}; }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.title { .title {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
font-size: 20px; font-size: 20px;
font-weight: 600; font-weight: 600;
border-bottom: 1px solid #c9c9c9; border-bottom: 1px solid #c9c9c9;
padding-bottom: 7px; padding-bottom: 7px;
} }
</style> </style>

View File

@@ -1,82 +1,101 @@
<template> <template>
<a-button class="action-btn" @click="handleHold">Hold</a-button> <a-button class="action-btn" @click="handleHold">Hold</a-button>
<!-- Hold Modal --> <!-- Hold Modal -->
<a-modal v-model:open="openHoldModal" title="Hold 操作" @cancel="handleCloseHold" @ok="handleSubmitHold"> <a-modal
<a-form :colon="false" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }"> v-model:open="openHoldModal"
<a-form-item label="工单编码"> title="Hold 操作"
<a-input v-model:value="traceOrderStore.traceOrderInfo.code" readonly /> @cancel="handleCloseHold"
</a-form-item> @ok="handleSubmitHold"
<a-form-item label="目标产品编码"> >
<a-input v-model:value="traceOrderStore.traceOrderInfo.tarMaterialCode" readonly /> <a-form :colon="false" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
</a-form-item> <a-form-item label="工单编码">
<a-form-item label="目标产品名称"> <a-input v-model:value="traceOrderStore.traceOrderInfo.code" readonly />
<a-input v-model:value="traceOrderStore.traceOrderInfo.tarMaterialName" readonly /> </a-form-item>
</a-form-item> <a-form-item label="目标产品编码">
<a-form-item label="发起工序名称"> <a-input
<a-input v-model:value="traceOrderStore.stationInfo.operationTitle" readonly /> v-model:value="traceOrderStore.traceOrderInfo.tarMaterialCode"
</a-form-item> readonly
<a-form-item label="产品规格"> />
<a-input readonly /> </a-form-item>
</a-form-item> <a-form-item label="目标产品名称">
<a-form-item label="计划完成日期"> <a-input
<a-date-picker v-model:value="planFinishDate" placeholder="选择计划完成日期" valueFormat="YYYY-MM-DD HH:mm:ss" show-time v-model:value="traceOrderStore.traceOrderInfo.tarMaterialName"
style="width: 100%" /> readonly
</a-form-item> />
</a-form> </a-form-item>
</a-modal> <a-form-item label="发起工序名称">
<a-input
v-model:value="traceOrderStore.stationInfo.operationTitle"
readonly
/>
</a-form-item>
<a-form-item label="产品规格">
<a-input readonly />
</a-form-item>
<a-form-item label="计划完成日期">
<a-date-picker
v-model:value="planFinishDate"
placeholder="选择计划完成日期"
valueFormat="YYYY-MM-DD HH:mm:ss"
show-time
style="width: 100%"
/>
</a-form-item>
</a-form>
</a-modal>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue'
import { useDialog } from '@/utils/useDialog'; import { useDialog } from '@/utils/useDialog'
import { useTraceOrderStore } from '@/store'; import { useTraceOrderStore } from '@/store'
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue'
import { addQualityAbnormalContact } from '@/api/traceOrderManage'; import { addQualityAbnormalContact } from '@/api/traceOrderManage'
const traceOrderStore = useTraceOrderStore(); const traceOrderStore = useTraceOrderStore()
// useDialog 管理弹窗状态 // useDialog 管理弹窗状态
const { visible: openHoldModal, show, hide } = useDialog(); const { visible: openHoldModal, show, hide } = useDialog()
const planFinishDate = ref(''); const planFinishDate = ref('')
const handleHold = () => { const handleHold = () => {
if (!traceOrderStore.currentTraceOrderCode) { if (!traceOrderStore.currentTraceOrderCode) {
message.error('请先选择工单!'); message.error('请先选择工单!')
return; return
} }
show(); show()
}; }
const handleCloseHold = () => { const handleCloseHold = () => {
planFinishDate.value = ''; planFinishDate.value = ''
hide(); hide()
}; }
const handleSubmitHold = async () => { const handleSubmitHold = async () => {
const tmpPlanFinishDate = planFinishDate.value; const tmpPlanFinishDate = planFinishDate.value
// 修改随工单状态 // 修改随工单状态
try { try {
message.success('Hold 成功!') message.success('Hold 成功!')
planFinishDate.value = ''; planFinishDate.value = ''
openHoldModal.value = false; openHoldModal.value = false
} catch (error: any) { } catch (error: any) {
message.error(error.message || 'Hold 失败'); message.error(error.message || 'Hold 失败')
return; return
} }
// 添加修改记录 // 添加修改记录
try { try {
addQualityAbnormalContact({ addQualityAbnormalContact({
materialCode: traceOrderStore.traceOrderInfo.tarMaterialCode, materialCode: traceOrderStore.traceOrderInfo.tarMaterialCode,
abnormalOperation: traceOrderStore.stationInfo.operationCode, abnormalOperation: traceOrderStore.stationInfo.operationCode,
planFinishDate: tmpPlanFinishDate, planFinishDate: tmpPlanFinishDate,
status: "Hold", status: 'Hold',
}) })
} catch (error: any) { } catch (error: any) {
message.error(error.message || '添加记录异常'); message.error(error.message || '添加记录异常')
return; return
} }
}; }
</script> </script>

View File

@@ -1,21 +1,21 @@
import { createApp } from "vue"; import { createApp } from 'vue'
import App from "./App.vue"; import App from './App.vue'
// Pinia 状态管理 // Pinia 状态管理
import { createPinia } from "pinia"; import { createPinia } from 'pinia'
const pinia = createPinia(); const pinia = createPinia()
// Vue Router // Vue Router
import router from "./router"; import router from './router'
// 样式文件 // 样式文件
import "ant-design-vue/dist/reset.css"; import 'ant-design-vue/dist/reset.css'
import "@/assets/styles/index.scss"; import '@/assets/styles/index.scss'
const app = createApp(App); const app = createApp(App)
// 字典方法 // 字典方法
import { useDict } from "@/utils/dict"; import { useDict } from '@/utils/dict'
app.config.globalProperties.useDict = useDict; app.config.globalProperties.useDict = useDict
app.use(pinia).use(router).mount("#app"); app.use(pinia).use(router).mount('#app')

View File

@@ -1,120 +1,119 @@
import { createRouter, createWebHistory } from 'vue-router'; import { createRouter, createWebHistory } from 'vue-router'
import { getToken } from "@/utils/auth"; import { getToken } from '@/utils/auth'
const whiteList = ["/login"]; const whiteList = ['/login']
const routes = [ const routes = [
{ {
path: "/", path: '/',
name: "Index", name: 'Index',
component: () => import("@/views/index.vue"), component: () => import('@/views/index.vue'),
}, },
{ {
path: "/login", path: '/login',
name: "Login", name: 'Login',
component: () => import("@/views/login.vue"), component: () => import('@/views/login.vue'),
}, },
{ {
path: "/traceOrderManage", path: '/traceOrderManage',
name: "TraceOrderManage", name: 'TraceOrderManage',
component: () => import("@/views/traceOrderManage/layout.vue"), component: () => import('@/views/traceOrderManage/layout.vue'),
redirect: { name: "TraceOrderManageIndex" }, redirect: { name: 'TraceOrderManageIndex' },
children: [ children: [
{ {
path: "", path: '',
name: "TraceOrderManageIndex", name: 'TraceOrderManageIndex',
component: () => import("@/views/traceOrderManage/index.vue"), component: () => import('@/views/traceOrderManage/index.vue'),
}, },
{ {
path: "infeed", path: 'infeed',
name: "Infeed", name: 'Infeed',
component: () => import("@/views/traceOrderManage/infeed/layout.vue"), component: () => import('@/views/traceOrderManage/infeed/layout.vue'),
redirect: { name: "PrimaryMaterial" }, redirect: { name: 'PrimaryMaterial' },
children: [ children: [
{ {
path: "primaryMaterial", path: 'primaryMaterial',
name: "PrimaryMaterial", name: 'PrimaryMaterial',
component: () => component: () =>
import("@/views/traceOrderManage/infeed/primaryMaterial/index.vue"), import('@/views/traceOrderManage/infeed/primaryMaterial/index.vue'),
}, },
{ {
path: "rawMaterial", path: 'rawMaterial',
name: "RawMaterial", name: 'RawMaterial',
component: () => component: () =>
import("@/views/traceOrderManage/infeed/rawMaterial/index.vue"), import('@/views/traceOrderManage/infeed/rawMaterial/index.vue'),
}, },
{ {
path: "mask", path: 'mask',
name: "Mask", name: 'Mask',
component: () => import("@/views/traceOrderManage/infeed/mask/index.vue"), component: () =>
}, import('@/views/traceOrderManage/infeed/mask/index.vue'),
{ },
path: "equipment", {
name: "Equipment", path: 'equipment',
component: () => name: 'Equipment',
import("@/views/traceOrderManage/infeed/equipment/index.vue"), component: () =>
}, import('@/views/traceOrderManage/infeed/equipment/index.vue'),
], },
}, ],
{ },
path: "outfeed", {
name: "Outfeed", path: 'outfeed',
component: () => import("@/views/traceOrderManage/outfeed/layout.vue"), name: 'Outfeed',
redirect: { name: "JobReport" }, component: () => import('@/views/traceOrderManage/outfeed/layout.vue'),
children: [ redirect: { name: 'JobReport' },
{ children: [
path: "jobReport", {
name: "JobReport", path: 'jobReport',
component: () => name: 'JobReport',
import("@/views/traceOrderManage/outfeed/jobReport/index.vue"), component: () =>
}, import('@/views/traceOrderManage/outfeed/jobReport/index.vue'),
{ },
path: "parameterConfiguration", {
name: "ParameterConfiguration", path: 'parameterConfiguration',
component: () => name: 'ParameterConfiguration',
import( component: () =>
"@/views/traceOrderManage/outfeed/parameterConfiguration/index.vue" import('@/views/traceOrderManage/outfeed/parameterConfiguration/index.vue'),
), },
}, {
{ path: 'processGuidance',
path: "processGuidance", name: 'ProcessGuidance',
name: "ProcessGuidance", component: () =>
component: () => import('@/views/traceOrderManage/outfeed/processGuidance/index.vue'),
import("@/views/traceOrderManage/outfeed/processGuidance/index.vue"), },
}, ],
], },
}, ],
], },
}, ]
];
const router = createRouter({ const router = createRouter({
history: createWebHistory(), history: createWebHistory(),
routes routes,
}); })
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
const token = getToken(); const token = getToken()
// 已登录,访问 login → 跳首页 // 已登录,访问 login → 跳首页
if (token && to.path === "/login") { if (token && to.path === '/login') {
return next("/"); return next('/')
} }
// 白名单放行 // 白名单放行
if (whiteList.includes(to.path)) { if (whiteList.includes(to.path)) {
return next(); return next()
} }
// 未登录,访问受保护路由 // 未登录,访问受保护路由
if (!token) { if (!token) {
return next({ return next({
path: "/login", path: '/login',
query: { redirect: to.fullPath }, query: { redirect: to.fullPath },
}); })
} }
// 已登录,正常访问 // 已登录,正常访问
next(); next()
}); })
export default router; export default router

10
src/shim-vue.d.ts vendored
View File

@@ -1,5 +1,7 @@
declare module "*.vue" { declare module '*.vue' {
import type { DefineComponent } from "vue"; import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>;
export default component; // eslint-disable-next-line @typescript-eslint/no-empty-object-type
const component: DefineComponent<{}, {}, any>
export default component
} }

View File

@@ -1,7 +1,7 @@
// 系统管理 // 系统管理
export * from './system/auth'; export * from './system/auth'
export * from './system/dict'; export * from './system/dict'
export * from './system/user'; export * from './system/user'
// 工单管理 // 工单管理
export * from './traceOrderManage/traceOrder'; export * from './traceOrderManage/traceOrder'

View File

@@ -1,68 +1,74 @@
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, notification } 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 { setToken, getToken, removeToken, removeAccount, getRememberMe } from "@/utils/auth"; import {
import type { LoginInfo } from "@/api/system/model"; setToken,
getToken,
removeToken,
removeAccount,
getRememberMe,
} from '@/utils/auth'
import type { LoginInfo } from '@/api/system/model'
export const useAuthStore = defineStore("auth", () => { export const useAuthStore = defineStore('auth', () => {
const route = useRoute(); const route = useRoute()
const router = useRouter(); const router = useRouter()
const userStore = useUserStore(); const userStore = useUserStore()
const loginLoading = ref(false); const loginLoading = ref(false)
const token = ref(getToken() || null); const token = ref(getToken() || null)
async function authLogin(params: LoginInfo, rememberMe: boolean) { async function authLogin(params: LoginInfo, rememberMe: boolean) {
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()
notification.success({ notification.success({
message: "登录成功", message: '登录成功',
description: `欢迎回来,${params.username}`, description: `欢迎回来,${params.username}`,
duration: 3, duration: 3,
}); })
if (rememberMe) { if (rememberMe) {
userStore.setUserInfo({ ...params, rememberMe }); userStore.setUserInfo({ ...params, rememberMe })
} else { } else {
userStore.clearUserInfo(); userStore.clearUserInfo()
userStore.setUsername(params.username); userStore.setUsername(params.username)
} }
const redirect = route.query.redirect || "/"; const redirect = route.query.redirect || '/'
router.replace(redirect as string); router.replace(redirect as string)
return res; return res
} else { } else {
// 抛出错误,让调用方处理 // 抛出错误,让调用方处理
throw new Error(res.msg || "登录失败"); throw new Error(res.msg || '登录失败')
} }
} catch (error) { } catch (error) {
throw error; throw error
} finally { } finally {
loginLoading.value = false; loginLoading.value = false
} }
} }
async function logout() { async function logout() {
// 在实际应用中,这里可以调用后端的退出登录接口 // 在实际应用中,这里可以调用后端的退出登录接口
await logoutApi(); await logoutApi()
removeToken(); removeToken()
!getRememberMe() && removeAccount(); if (!getRememberMe()) removeAccount()
await router.push("/login"); await router.push('/login')
message.success("已成功退出"); message.success('已成功退出')
} }
return { return {
token, token,
loginLoading, loginLoading,
authLogin, authLogin,
logout, logout,
}; }
}); })

View File

@@ -1,63 +1,65 @@
import { defineStore } from "pinia"; import { defineStore } from 'pinia'
import { ref } from "vue"; import { ref } from 'vue'
export const useDictStore = defineStore("dict", () => { export const useDictStore = defineStore('dict', () => {
const dict = ref<any[]>([]); const dict = ref<any[]>([])
// 获取字典 // 获取字典
function getDict(_key: string) { function getDict(_key: string) {
if (_key == null && _key == "") { if (_key == null && _key == '') {
return null; return null
} }
try { try {
dict.value.forEach(item => { dict.value.forEach((item) => {
if (item.key == _key) { if (item.key == _key) {
return item.value; return item.value
} }
}); })
} catch (e) { } catch (e) {
return null; console.error(e)
} return null
} }
}
// 设置字典 // 设置字典
function setDict(_key: string, value: string | number) { function setDict(_key: string, value: string | number) {
if (!_key) return; if (!_key) return
dict.value.push({ dict.value.push({
key: _key, key: _key,
value: value value: value,
}); })
} }
// 删除字典 // 删除字典
function removeDict(_key: string) { function removeDict(_key: string) {
var bln = false; let bln = false
try { try {
dict.value.forEach((item, index) => { dict.value.forEach((item, index) => {
if (item.key == _key) { if (item.key == _key) {
dict.value.splice(index, 1); dict.value.splice(index, 1)
bln = true; bln = true
} }
}); })
} catch (e) { } catch (e) {
bln = false; console.error(e)
} bln = false
return bln; }
} return bln
}
// 清空字典 // 清空字典
function cleanDict() { function cleanDict() {
dict.value = new Array(); dict.value = []
} }
// 初始字典 // 初始字典
function initDict() { } function initDict() {}
return { return {
dict, dict,
getDict, getDict,
setDict, setDict,
removeDict, removeDict,
cleanDict, cleanDict,
initDict, initDict,
}; }
}); })

View File

@@ -1,39 +1,41 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia'
import Cookies from 'js-cookie'; import Cookies from 'js-cookie'
import { setAccount, removeAccount } from "@/utils/auth"; import { setAccount, removeAccount } from '@/utils/auth'
import { ref } from 'vue'; import { ref } from 'vue'
export const useUserStore = defineStore("user", () => { export const useUserStore = defineStore('user', () => {
const username = ref(Cookies.get('username') || ''); const username = ref(Cookies.get('username') || '')
async function fetchUserInfo() {
// Simulate API call
}
function setUsername(name: string) { async function fetchUserInfo() {
username.value = name; // Simulate API call
} }
function setUserInfo(params: any) { function setUsername(name: string) {
username.value = params.username || ''; username.value = name
}
params.rememberMe && setAccount({ function setUserInfo(params: any) {
username: params.username, username.value = params.username || ''
password: params.password,
rememberMe: params.rememberMe,
});
}
function clearUserInfo() { if (params.rememberMe) {
username.value = ''; setAccount({
removeAccount(); username: params.username,
} password: params.password,
rememberMe: params.rememberMe,
})
}
}
return { function clearUserInfo() {
username, username.value = ''
fetchUserInfo, removeAccount()
setUsername, }
setUserInfo,
clearUserInfo, return {
} username,
}); fetchUserInfo,
setUsername,
setUserInfo,
clearUserInfo,
}
})

View File

@@ -1,148 +1,148 @@
import { defineStore } from "pinia"; import { defineStore } from 'pinia'
import { ref } from "vue"; import { ref } from 'vue'
import { message } from "ant-design-vue"; import { message } from 'ant-design-vue'
import { debounce } from 'lodash'; import { debounce } from 'lodash'
import { listStation, getStation } from "@/api/traceOrderManage/station"; import { listStation, getStation } from '@/api/traceOrderManage/station'
import { import { listLotTraceOrder, getLotTraceOrder } from '@/api/traceOrderManage'
listLotTraceOrder,
getLotTraceOrder,
} from "@/api/traceOrderManage";
export const useTraceOrderStore = defineStore("traceOrder", () => { export const useTraceOrderStore = defineStore('traceOrder', () => {
/* ========= 核心业务状态 ========= */ /* ========= 核心业务状态 ========= */
const currentTraceOrderId = ref<string | number>(''); const currentTraceOrderId = ref<string | number>('')
const currentTraceOrderCode = ref<string>(''); const currentTraceOrderCode = ref<string>('')
const currentStationId = ref<string | number>(''); const currentStationId = ref<string | number>('')
const currentStationCode = ref<string>(''); const currentStationCode = ref<string>('')
const traceOrderOptions = ref<any[]>([]); const traceOrderOptions = ref<any[]>([])
const traceOrderInfo = ref<any>({}); const traceOrderInfo = ref<any>({})
const stationInfo = ref<any>({}); const stationInfo = ref<any>({})
const stationList = ref<any[]>([]); const stationList = ref<any[]>([])
/* ========= actions ========= */ /* ========= actions ========= */
// 加载工单列表 // 加载工单列表
const fetchTraceOrderOption = debounce(async (code: string) => { const fetchTraceOrderOption = debounce(async (code: string) => {
if (!code) return; if (!code) return
try { try {
const { rows } = await listLotTraceOrder({ const { rows } = await listLotTraceOrder({
code, code,
pageSize: 10, pageSize: 10,
pageNum: 1, pageNum: 1,
}); })
traceOrderOptions.value = rows.map((item) => { traceOrderOptions.value = rows.map((item) => {
return { return {
id: item.id, id: item.id,
label: item.code, label: item.code,
value: item.code, value: item.code,
}; }
}); })
} catch (error: any) { } catch (error: any) {
message.error(error.message || "获取工单列表失败"); message.error(error.message || '获取工单列表失败')
} }
}, 500); }, 500)
// 选择工单 // 选择工单
async function selectTraceOrder(value: string, option: any) { async function selectTraceOrder(value: string, option: any) {
currentTraceOrderId.value = option.id; currentTraceOrderId.value = option.id
currentTraceOrderCode.value = value; currentTraceOrderCode.value = value
try { try {
loadingTraceOrderInfo.value = true; loadingTraceOrderInfo.value = true
if (!option.id) throw new Error(); if (!option.id) throw new Error()
const { data } = await getLotTraceOrder(option.id); const { data } = await getLotTraceOrder(option.id)
traceOrderInfo.value = data; traceOrderInfo.value = data
await fetchStationList(); await fetchStationList()
} catch (error: any) { } catch (error: any) {
message.error(error.message || "获取工单信息失败"); message.error(error.message || '获取工单信息失败')
} finally { } finally {
loadingTraceOrderInfo.value = false; loadingTraceOrderInfo.value = false
} }
} }
// 获取工单信息 // 获取工单信息
const loadingTraceOrderInfo = ref(false); const loadingTraceOrderInfo = ref(false)
async function fetchTraceOrderInfo() { async function fetchTraceOrderInfo() {
if (!currentTraceOrderId.value) return; if (!currentTraceOrderId.value) return
try { try {
loadingTraceOrderInfo.value = true; loadingTraceOrderInfo.value = true
const { data } = await getLotTraceOrder(currentTraceOrderId.value); const { data } = await getLotTraceOrder(currentTraceOrderId.value)
traceOrderInfo.value = data; traceOrderInfo.value = data
fetchStationList(); fetchStationList()
} catch (error: any) { } catch (error: any) {
message.error(error.message || '刷新工单信息失败'); message.error(error.message || '刷新工单信息失败')
} finally { } finally {
loadingTraceOrderInfo.value = false; loadingTraceOrderInfo.value = false
} }
} }
// 获取站点列表
const loadingStations = ref(false);
async function fetchStationList() {
if (!currentTraceOrderCode.value) return;
try {
loadingStations.value = true;
const res = await listStation({ traceOrderCode: currentTraceOrderCode.value });
stationList.value = (res.rows || []).map((item: any, idx: number) => {
return {
key: String(item.id ?? idx),
index: item.seqNo ?? idx + 1,
...item,
};
});
} catch (error: any) {
message.error(error?.msg || error?.message || "查询站点失败");
} finally {
loadingStations.value = false;
}
};
// 获取站点信息 // 获取站点列表
const loadingStationInfo = ref(false); const loadingStations = ref(false)
async function fetchStationInfo() { async function fetchStationList() {
if (!currentTraceOrderId.value) return; if (!currentTraceOrderCode.value) return
try { try {
loadingStationInfo.value = true; loadingStations.value = true
const { data } = await getStation(currentStationId.value); const res = await listStation({
stationInfo.value = data; traceOrderCode: currentTraceOrderCode.value,
} catch (error: any) { })
console.log(error.message || '获取站点信息失败'); stationList.value = (res.rows || []).map((item: any, idx: number) => {
} finally { return {
loadingStationInfo.value = false; key: String(item.id ?? idx),
} index: item.seqNo ?? idx + 1,
} ...item,
}
})
} catch (error: any) {
message.error(error?.msg || error?.message || '查询站点失败')
} finally {
loadingStations.value = false
}
}
// 选择站点 // 获取站点信息
async function selectStation(stationId: string) { const loadingStationInfo = ref(false)
currentStationId.value = stationId; async function fetchStationInfo() {
stationInfo.value = stationList.value.find((s: any) => s.id === stationId) ?? {}; if (!currentTraceOrderId.value) return
currentStationCode.value = stationInfo.value.code; try {
} loadingStationInfo.value = true
const { data } = await getStation(currentStationId.value)
stationInfo.value = data
} catch (error: any) {
console.log(error.message || '获取站点信息失败')
} finally {
loadingStationInfo.value = false
}
}
async function refreshAll() { // 选择站点
await Promise.all([fetchTraceOrderInfo(), fetchStationList()]); async function selectStation(stationId: string) {
} currentStationId.value = stationId
stationInfo.value =
stationList.value.find((s: any) => s.id === stationId) ?? {}
currentStationCode.value = stationInfo.value.code
}
return { async function refreshAll() {
currentTraceOrderId, await Promise.all([fetchTraceOrderInfo(), fetchStationList()])
currentTraceOrderCode, }
currentStationId,
currentStationCode, return {
traceOrderOptions, currentTraceOrderId,
traceOrderInfo, currentTraceOrderCode,
stationInfo, currentStationId,
stationList, currentStationCode,
loadingTraceOrderInfo, traceOrderOptions,
loadingStations, traceOrderInfo,
loadingStationInfo, stationInfo,
selectTraceOrder, stationList,
selectStation, loadingTraceOrderInfo,
refreshAll, loadingStations,
fetchTraceOrderOption, loadingStationInfo,
fetchTraceOrderInfo, selectTraceOrder,
fetchStationList, selectStation,
fetchStationInfo, refreshAll,
}; fetchTraceOrderOption,
}); fetchTraceOrderInfo,
fetchStationList,
fetchStationInfo,
}
})

View File

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

View File

@@ -1,8 +1,8 @@
import { message } from "ant-design-vue"; import { message } from 'ant-design-vue'
// 复制到剪贴板 // 复制到剪贴板
export const handleCopy = async (text: string | number | undefined) => { export const handleCopy = async (text: string | number | undefined) => {
if (!text) return; if (!text) return
await navigator.clipboard.writeText(String(text)); await navigator.clipboard.writeText(String(text))
message.success(`已复制: ${text}`); message.success(`已复制: ${text}`)
}; }

View File

@@ -1,4 +1,4 @@
import { ref, onMounted, onBeforeUnmount } from 'vue'; import { ref, onMounted, onBeforeUnmount } from 'vue'
/** /**
* 格式化日期时间 * 格式化日期时间
@@ -6,27 +6,30 @@ import { ref, onMounted, onBeforeUnmount } from 'vue';
* @param format 格式字符串,默认 'YYYY-MM-DD HH:mm:ss' * @param format 格式字符串,默认 'YYYY-MM-DD HH:mm:ss'
* @returns 格式化后的时间字符串 * @returns 格式化后的时间字符串
*/ */
export function formatDateTime(date: Date | number | string, format = 'YYYY-MM-DD HH:mm:ss'): string { export function formatDateTime(
const d = new Date(date); date: Date | number | string,
format = 'YYYY-MM-DD HH:mm:ss',
if (isNaN(d.getTime())) { ): string {
return ''; const d = new Date(date)
}
const year = d.getFullYear(); if (isNaN(d.getTime())) {
const month = String(d.getMonth() + 1).padStart(2, '0'); return ''
const day = String(d.getDate()).padStart(2, '0'); }
const hours = String(d.getHours()).padStart(2, '0');
const minutes = String(d.getMinutes()).padStart(2, '0');
const seconds = String(d.getSeconds()).padStart(2, '0');
return format const year = d.getFullYear()
.replace('YYYY', String(year)) const month = String(d.getMonth() + 1).padStart(2, '0')
.replace('MM', month) const day = String(d.getDate()).padStart(2, '0')
.replace('DD', day) const hours = String(d.getHours()).padStart(2, '0')
.replace('HH', hours) const minutes = String(d.getMinutes()).padStart(2, '0')
.replace('mm', minutes) const seconds = String(d.getSeconds()).padStart(2, '0')
.replace('ss', seconds);
return format
.replace('YYYY', String(year))
.replace('MM', month)
.replace('DD', day)
.replace('HH', hours)
.replace('mm', minutes)
.replace('ss', seconds)
} }
/** /**
@@ -35,7 +38,7 @@ export function formatDateTime(date: Date | number | string, format = 'YYYY-MM-D
* @returns 当前时间字符串 * @returns 当前时间字符串
*/ */
export function getCurrentTime(format = 'YYYY-MM-DD HH:mm:ss'): string { export function getCurrentTime(format = 'YYYY-MM-DD HH:mm:ss'): string {
return formatDateTime(new Date(), format); return formatDateTime(new Date(), format)
} }
/** /**
@@ -44,28 +47,28 @@ export function getCurrentTime(format = 'YYYY-MM-DD HH:mm:ss'): string {
* @returns 响应式时间字符串 * @returns 响应式时间字符串
*/ */
export function useRealTime(format = 'YYYY-MM-DD HH:mm:ss') { export function useRealTime(format = 'YYYY-MM-DD HH:mm:ss') {
const currentTime = ref(getCurrentTime(format)); const currentTime = ref(getCurrentTime(format))
let timer: NodeJS.Timeout | null = null; let timer: NodeJS.Timeout | null = null
const startTimer = () => { const startTimer = () => {
timer = setInterval(() => { timer = setInterval(() => {
currentTime.value = getCurrentTime(format); currentTime.value = getCurrentTime(format)
}, 1000); }, 1000)
}; }
const stopTimer = () => { const stopTimer = () => {
if (timer) { if (timer) {
clearInterval(timer); clearInterval(timer)
timer = null; timer = null
} }
}; }
onMounted(startTimer); onMounted(startTimer)
onBeforeUnmount(stopTimer); onBeforeUnmount(stopTimer)
return { return {
currentTime, currentTime,
startTimer, startTimer,
stopTimer stopTimer,
}; }
} }

View File

@@ -1,4 +1,4 @@
import { useDictStore } from "@/store"; import { useDictStore } from '@/store'
import { getDicts } from '@/api/system/dict' import { getDicts } from '@/api/system/dict'
import { ref, toRefs } from 'vue' import { ref, toRefs } from 'vue'
@@ -6,21 +6,26 @@ import { ref, toRefs } from 'vue'
* 获取字典数据 * 获取字典数据
*/ */
export function useDict(...args: string[]) { export function useDict(...args: string[]) {
const res = ref<any>({}) const res = ref<any>({})
return (() => { return (() => {
args.forEach((dictType, index) => { args.forEach((dictType) => {
res.value[dictType] = [] res.value[dictType] = []
const dicts = useDictStore().getDict(dictType) const dicts = useDictStore().getDict(dictType)
if (dicts) { if (dicts) {
res.value[dictType] = dicts res.value[dictType] = dicts
} else { } else {
getDicts(dictType).then((resp: any) => { getDicts(dictType).then((resp: any) => {
res.value[dictType] = resp.data.map((p: any) => ({ label: p.dictLabel, value: p.dictValue, elTagType: p.listClass, elTagClass: p.cssClass })) res.value[dictType] = resp.data.map((p: any) => ({
useDictStore().setDict(dictType, res.value[dictType]) label: p.dictLabel,
}) value: p.dictValue,
} elTagType: p.listClass,
}) elTagClass: p.cssClass,
return toRefs(res.value) }))
})() useDictStore().setDict(dictType, res.value[dictType])
})
}
})
return toRefs(res.value)
})()
} }

View File

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

View File

@@ -1,7 +1,7 @@
export const delay = (time: number, data?: any) => { export const delay = (time: number, data?: any) => {
return new Promise((resolve) => { return new Promise((resolve) => {
setTimeout(() => { setTimeout(() => {
resolve(data); resolve(data)
}, time); }, time)
}) as any; }) as any
}; }

View File

@@ -1,33 +1,33 @@
import { ref } from 'vue'; import { ref } from 'vue'
/** /**
* Dialog控制Hook * Dialog控制Hook
* @returns visible显隐状态、show显示、hide隐藏、toggle切换 * @returns visible显隐状态、show显示、hide隐藏、toggle切换
*/ */
export function useDialog(initialVisible:boolean = false): { export function useDialog(initialVisible: boolean = false): {
visible: import('vue').Ref<boolean>, visible: import('vue').Ref<boolean>
show: () => void, show: () => void
hide: () => void, hide: () => void
toggle: () => void toggle: () => void
} { } {
const visible = ref(initialVisible); const visible = ref(initialVisible)
const show = () => { const show = () => {
visible.value = true; visible.value = true
}; }
const hide = () => { const hide = () => {
visible.value = false; visible.value = false
}; }
const toggle = () => { const toggle = () => {
visible.value = !visible.value; visible.value = !visible.value
}; }
return { return {
visible, visible,
show, show,
hide, hide,
toggle toggle,
}; }
} }

View File

@@ -1,41 +1,43 @@
import { ref } from 'vue'; import { ref } from 'vue'
/** /**
* Loading控制Hook * Loading控制Hook
* @returns loading加载状态、startLoading开始加载、stopLoading停止加载、withLoading包装异步函数 * @returns loading加载状态、startLoading开始加载、stopLoading停止加载、withLoading包装异步函数
*/ */
export function useLoading(initialLoading = false) { export function useLoading(initialLoading = false) {
const loading = ref(initialLoading); const loading = ref(initialLoading)
const startLoading = () => { const startLoading = () => {
loading.value = true; loading.value = true
}; }
const stopLoading = () => { const stopLoading = () => {
loading.value = false; loading.value = false
}; }
/** /**
* 包装异步函数自动控制loading状态 * 包装异步函数自动控制loading状态
* @param fn 异步函数 * @param fn 异步函数
* @returns 包装后的函数 * @returns 包装后的函数
*/ */
const withLoading = <T extends (...args: any[]) => Promise<any>>(fn: T): T => { const withLoading = <T extends (...args: any[]) => Promise<any>>(
return (async (...args: any[]) => { fn: T,
startLoading(); ): T => {
try { return (async (...args: any[]) => {
const result = await fn(...args); startLoading()
return result; try {
} finally { const result = await fn(...args)
stopLoading(); return result
} } finally {
}) as T; stopLoading()
}; }
}) as T
}
return { return {
loading, loading,
startLoading, startLoading,
stopLoading, stopLoading,
withLoading withLoading,
}; }
} }

View File

@@ -3,9 +3,9 @@
* @returns 生成的UUID字符串 * @returns 生成的UUID字符串
*/ */
export function generateUUID(): string { export function generateUUID(): string {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = Math.random() * 16 | 0; const r = (Math.random() * 16) | 0
const v = c === 'x' ? r : (r & 0x3 | 0x8); const v = c === 'x' ? r : (r & 0x3) | 0x8
return v.toString(16); return v.toString(16)
}); })
} }

View File

@@ -1,18 +1,22 @@
<template> <template>
<div class="ipc-dashboard"> <div class="ipc-dashboard">
<Header title="MES 过站平台" showLogout /> <Header title="MES 过站平台" showLogout />
<div class="menu-grid"> <div class="menu-grid">
<a-card class="menu-card" shadow="hover" @click="handleJumpTo('TraceOrderManage')"> <a-card
<div class="icon-wrap"> class="menu-card"
<i-lucide-building /> shadow="hover"
</div> @click="handleJumpTo('TraceOrderManage')"
<div class="text"> >
<div class="title">工单管理</div> <div class="icon-wrap">
<div class="desc">管理生产工单和进度</div> <i-lucide-building />
</div> </div>
</a-card> <div class="text">
<!-- <div class="title">工单管理</div>
<div class="desc">管理生产工单和进度</div>
</div>
</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 />
@@ -82,187 +86,187 @@
<div class="desc">系统参数配置</div> <div class="desc">系统参数配置</div>
</div> </div>
</a-card> --> </a-card> -->
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup lang="ts">
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router'
const router = useRouter(); const router = useRouter()
const handleJumpTo = (name) => { const handleJumpTo = (name: string) => {
router.push({ name }); router.push({ name })
}; }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.ipc-dashboard { .ipc-dashboard {
background: #f5f7fa; background: #f5f7fa;
height: 100vh; height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
} }
.menu-grid { .menu-grid {
flex: 1; flex: 1;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
padding: 8vh 6vh; padding: 8vh 6vh;
gap: 4%; gap: 4%;
row-gap: 6vh; row-gap: 6vh;
.menu-card { .menu-card {
width: 22%; width: 22%;
cursor: pointer; cursor: pointer;
border-radius: 1vw; border-radius: 1vw;
overflow: visible; overflow: visible;
:deep(.ant-card-body) { :deep(.ant-card-body) {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding: 24px 16px; padding: 24px 16px;
gap: 1vh; gap: 1vh;
.icon-wrap { .icon-wrap {
width: 5vw; width: 5vw;
height: 5vw; height: 5vw;
border-radius: 50%; border-radius: 50%;
background: linear-gradient(180deg, #0c6bd1 0%, #1c6fb8 100%); background: linear-gradient(180deg, #0c6bd1 0%, #1c6fb8 100%);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: #fff; color: #fff;
font-size: 3vw; font-size: 3vw;
} }
.text { .text {
text-align: center; text-align: center;
.title { .title {
font-size: 1.2vw; font-size: 1.2vw;
font-weight: 500; font-weight: 500;
margin-bottom: 6px; margin-bottom: 6px;
} }
.desc { .desc {
color: #8b96a7; color: #8b96a7;
font-size: 0.9vw; font-size: 0.9vw;
} }
} }
} }
} }
} }
:deep(.login-dialog) { :deep(.login-dialog) {
.ant-modal { .ant-modal {
width: 48vw; width: 48vw;
left: 26vw; left: 26vw;
border-radius: 16px !important; border-radius: 16px !important;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15) !important; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15) !important;
overflow: hidden; overflow: hidden;
} }
.ant-modal-header { .ant-modal-header {
background: linear-gradient(135deg, #0c6bd1 0%, #1c6fb8 100%); background: linear-gradient(135deg, #0c6bd1 0%, #1c6fb8 100%);
border-bottom: none; border-bottom: none;
padding: 24px 20px !important; padding: 24px 20px !important;
.ant-modal-title { .ant-modal-title {
color: #fff; color: #fff;
font-size: 20px; font-size: 20px;
font-weight: 600; font-weight: 600;
} }
.ant-modal-close-x { .ant-modal-close-x {
color: rgba(255, 255, 255, 0.7); color: rgba(255, 255, 255, 0.7);
font-size: 20px; font-size: 20px;
&:hover { &:hover {
color: #fff; color: #fff;
} }
} }
} }
.ant-modal-body { .ant-modal-body {
padding: 32px 28px !important; padding: 32px 28px !important;
} }
.login-form { .login-form {
.ant-form-item { .ant-form-item {
margin-bottom: 22px; margin-bottom: 22px;
.ant-form-item-label > label { .ant-form-item-label > label {
color: #1f2937; color: #1f2937;
font-size: 16px; font-size: 16px;
font-weight: 500; font-weight: 500;
padding-bottom: 10px; padding-bottom: 10px;
} }
.ant-input { .ant-input {
font-size: 16px; font-size: 16px;
height: 42px; height: 42px;
border-radius: 8px; border-radius: 8px;
border: 1px solid #e5e7eb; border: 1px solid #e5e7eb;
transition: all 0.3s ease; transition: all 0.3s ease;
padding: 0 1rem; padding: 0 1rem;
&:focus, &:focus,
&:hover { &:hover {
border-color: #0c6bd1; border-color: #0c6bd1;
box-shadow: 0 0 0 3px rgba(12, 107, 209, 0.1); box-shadow: 0 0 0 3px rgba(12, 107, 209, 0.1);
} }
} }
.ant-input-password-icon { .ant-input-password-icon {
color: #bfbfbf; color: #bfbfbf;
&:hover { &:hover {
color: #0c6bd1; color: #0c6bd1;
} }
} }
} }
} }
.ant-modal-footer { .ant-modal-footer {
padding: 20px 28px !important; padding: 20px 28px !important;
border-top: 1px solid #f0f0f0; border-top: 1px solid #f0f0f0;
background: #fafafa; background: #fafafa;
border-radius: 0 0 16px 16px; border-radius: 0 0 16px 16px;
.ant-btn { .ant-btn {
font-size: 16px; font-size: 16px;
font-weight: 500; font-weight: 500;
border-radius: 8px; border-radius: 8px;
padding: 10px 32px !important; padding: 10px 32px !important;
transition: all 0.3s ease; transition: all 0.3s ease;
&:not(.btn-primary) { &:not(.btn-primary) {
border-color: #d9d9d9; border-color: #d9d9d9;
color: #595959; color: #595959;
&:hover { &:hover {
border-color: #0c6bd1; border-color: #0c6bd1;
color: #0c6bd1; color: #0c6bd1;
background: #f0f7ff; background: #f0f7ff;
} }
} }
} }
.btn-primary { .btn-primary {
background: linear-gradient(135deg, #0c6bd1 0%, #1c6fb8 100%); background: linear-gradient(135deg, #0c6bd1 0%, #1c6fb8 100%);
border: none; border: none;
color: #fff; color: #fff;
&:hover { &:hover {
box-shadow: 0 4px 16px rgba(12, 107, 209, 0.4); box-shadow: 0 4px 16px rgba(12, 107, 209, 0.4);
} }
} }
} }
} }
</style> </style>

View File

@@ -3,21 +3,21 @@ import { ref, reactive } from 'vue'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { getCaptcha } from '@/api/system' import { getCaptcha } from '@/api/system'
import type { LoginInfo } from '@/api/system/model' 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, getRememberMe } from '@/utils/auth'; import { getAccount, getRememberMe } from '@/utils/auth'
import { decrypt } from '@/utils/jsencrypt'; import { decrypt } from '@/utils/jsencrypt'
const authStore = useAuthStore(); const authStore = useAuthStore()
const { loginLoading } = storeToRefs(authStore); const { loginLoading } = storeToRefs(authStore)
// 表单数据 // 表单数据
const formData = reactive<LoginInfo>({ const formData = reactive<LoginInfo>({
username: '', username: '',
password: '', password: '',
uuid: '', uuid: '',
code: '', code: '',
}) })
// 验证码图片 // 验证码图片
@@ -28,61 +28,55 @@ const rememberMe = ref(false)
// 表单验证规则 // 表单验证规则
const rules: Record<string, Rule[]> = { const rules: Record<string, Rule[]> = {
username: [ username: [{ required: true, message: '请输入用户名', trigger: 'change' }],
{ required: true, message: '请输入用户名', trigger: 'change' } password: [{ required: true, message: '请输入密码', trigger: 'change' }],
], code: [{ required: true, message: '请输入验证码', trigger: 'change' }],
password: [
{ required: true, message: '请输入密码', trigger: 'change' }
],
code: [
{ required: true, message: '请输入验证码', trigger: 'change' }
]
} }
// 获取验证码 // 获取验证码
const refreshCaptcha = async () => { const refreshCaptcha = async () => {
try { try {
captchaImg.value = '' captchaImg.value = ''
captchaLoading.value = true captchaLoading.value = true
await getCaptcha().then((res: any) => { await getCaptcha().then((res: any) => {
loadCaptchaFail.value = false; loadCaptchaFail.value = false
if (res.captchaOnOff) { if (res.captchaOnOff) {
captchaImg.value = "data:image/gif;base64," + res.img captchaImg.value = 'data:image/gif;base64,' + res.img
rules.code[0].required = true; rules.code[0].required = true
} else { } else {
rules.code[0].required = false; rules.code[0].required = false
} }
formData.uuid = res.uuid formData.uuid = res.uuid
formData.code = '' // 清空验证码输入 formData.code = '' // 清空验证码输入
}) })
} catch (error) { } catch (error) {
loadCaptchaFail.value = true; loadCaptchaFail.value = true
message.error('获取验证码失败') message.error('获取验证码失败')
console.error('获取验证码失败:', error) console.error('获取验证码失败:', error)
} finally { } finally {
captchaLoading.value = false captchaLoading.value = false
} }
} }
// 初始化 // 初始化
const initLoginForm = () => { const initLoginForm = () => {
const { username, password } = getAccount(); const { username, password } = getAccount()
const isRememberMe = getRememberMe(); const isRememberMe = getRememberMe()
if (isRememberMe && isRememberMe === 'true') { if (isRememberMe && isRememberMe === 'true') {
rememberMe.value = true; rememberMe.value = true
formData.username = username || ''; formData.username = username || ''
formData.password = decrypt(password || '') || ''; formData.password = decrypt(password || '') || ''
} }
} }
// 登录处理 // 登录处理
const handleLogin = async () => { const handleLogin = async () => {
try { try {
await authStore.authLogin(formData, rememberMe.value); await authStore.authLogin(formData, rememberMe.value)
} catch (error: any) { } catch (error: any) {
message.error(error.message || error.msg || '登录失败'); message.error(error.message || error.msg || '登录失败')
await refreshCaptcha(); await refreshCaptcha()
} }
} }
initLoginForm() initLoginForm()
@@ -91,113 +85,122 @@ refreshCaptcha()
</script> </script>
<template> <template>
<div class="login-container"> <div class="login-container">
<!-- 背景图 --> <!-- 背景图 -->
<div class="login-background"></div> <div class="login-background"></div>
<!-- 登录框 -->
<div class="login-box">
<div class="login-header">
<h2 class="login-title">
<i-lucide-cpu class="title-icon" />
宏禧 MES 过站平台
</h2>
<p class="login-subtitle">请登录您的账户</p>
</div>
<a-form
:model="formData"
:rules="rules"
@finish="handleLogin"
class="login-form"
layout="vertical"
>
<!-- 用户名 -->
<a-form-item name="username" class="form-item">
<a-input
v-model:value="formData.username"
placeholder="请输入用户名"
size="large"
class="login-input"
allow-clear
>
<template #prefix>
<i-lucide-user class="input-icon" />
</template>
</a-input>
</a-form-item>
<!-- 密码 -->
<a-form-item name="password" class="form-item">
<a-input-password
v-model:value="formData.password"
placeholder="请输入密码"
size="large"
class="login-input"
:visibility-toggle="false"
allow-clear
>
<template #prefix>
<i-lucide-lock class="input-icon" />
</template>
</a-input-password>
</a-form-item>
<!-- 验证码 -->
<a-form-item name="code" class="form-item" v-if="rules.code[0].required">
<div class="captcha-container">
<a-input
v-model:value="formData.code"
placeholder="请输入验证码"
size="large"
class="login-input captcha-input"
allow-clear
>
<template #prefix>
<i-lucide-shield-check class="input-icon" />
</template>
</a-input>
<div class="captcha-image-container" @click="refreshCaptcha">
<img
v-if="captchaImg"
:src="captchaImg"
alt="验证码"
class="captcha-image"
/>
<div v-else-if="captchaLoading" class="captcha-loading">
<i-lucide-loader class="loading-icon" />
</div>
<div v-else-if="!captchaLoading && loadCaptchaFail" class="captcha-loading">
获取失败
</div>
<div class="captcha-refresh-hint">
<i-lucide-refresh-cw class="refresh-icon" />
点击刷新
</div>
</div>
</div>
</a-form-item>
<a-form-item name="rememberMe" class="form-item"> <!-- 登录框 -->
<a-checkbox v-model:checked="rememberMe" style="color: #ddd;">记住密码</a-checkbox> <div class="login-box">
</a-form-item> <div class="login-header">
<h2 class="login-title">
<!-- 登录按钮 --> <i-lucide-cpu class="title-icon" />
<a-form-item class="form-item"> 宏禧 MES 过站平台
<a-button </h2>
type="primary" <p class="login-subtitle">请登录您的账户</p>
html-type="submit" </div>
size="large"
:loading="loginLoading" <a-form
class="login-button" :model="formData"
block :rules="rules"
> @finish="handleLogin"
{{ loginLoading ? '登录中...' : '登录' }} class="login-form"
</a-button> layout="vertical"
</a-form-item> >
</a-form> <!-- 用户名 -->
</div> <a-form-item name="username" class="form-item">
</div> <a-input
v-model:value="formData.username"
placeholder="请输入用户名"
size="large"
class="login-input"
allow-clear
>
<template #prefix>
<i-lucide-user class="input-icon" />
</template>
</a-input>
</a-form-item>
<!-- 密码 -->
<a-form-item name="password" class="form-item">
<a-input-password
v-model:value="formData.password"
placeholder="请输入密码"
size="large"
class="login-input"
:visibility-toggle="false"
allow-clear
>
<template #prefix>
<i-lucide-lock class="input-icon" />
</template>
</a-input-password>
</a-form-item>
<!-- 验证码 -->
<a-form-item
name="code"
class="form-item"
v-if="rules.code[0].required"
>
<div class="captcha-container">
<a-input
v-model:value="formData.code"
placeholder="请输入验证码"
size="large"
class="login-input captcha-input"
allow-clear
>
<template #prefix>
<i-lucide-shield-check class="input-icon" />
</template>
</a-input>
<div class="captcha-image-container" @click="refreshCaptcha">
<img
v-if="captchaImg"
:src="captchaImg"
alt="验证码"
class="captcha-image"
/>
<div v-else-if="captchaLoading" class="captcha-loading">
<i-lucide-loader class="loading-icon" />
</div>
<div
v-else-if="!captchaLoading && loadCaptchaFail"
class="captcha-loading"
>
获取失败
</div>
<div class="captcha-refresh-hint">
<i-lucide-refresh-cw class="refresh-icon" />
点击刷新
</div>
</div>
</div>
</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-button
type="primary"
html-type="submit"
size="large"
:loading="loginLoading"
class="login-button"
block
>
{{ loginLoading ? '登录中...' : '登录' }}
</a-button>
</a-form-item>
</a-form>
</div>
</div>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
@@ -225,319 +228,319 @@ $spacing-xl: 32px;
$transition: all 0.3s ease; $transition: all 0.3s ease;
.login-container { .login-container {
position: relative; position: relative;
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
overflow: hidden; overflow: hidden;
} }
.login-background { .login-background {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background-image: url('/bg.jpg'); background-image: url('/bg.jpg');
background-size: cover; background-size: cover;
background-position: center; background-position: center;
background-repeat: no-repeat; background-repeat: no-repeat;
filter: blur(2px); filter: blur(2px);
z-index: 1; z-index: 1;
&::after { &::after {
content: ''; content: '';
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: linear-gradient( background: linear-gradient(
135deg, 135deg,
rgba(74, 144, 226, 0.3) 0%, rgba(74, 144, 226, 0.3) 0%,
rgba(58, 123, 213, 0.4) 50%, rgba(58, 123, 213, 0.4) 50%,
rgba(91, 160, 242, 0.3) 100% rgba(91, 160, 242, 0.3) 100%
); );
} }
} }
.login-box { .login-box {
position: relative; position: relative;
z-index: 2; z-index: 2;
width: 420px; width: 420px;
padding: $spacing-xl; padding: $spacing-xl;
background: $bg-glass; background: $bg-glass;
backdrop-filter: blur(20px); backdrop-filter: blur(20px);
border: 1px solid $border-light; border: 1px solid $border-light;
border-radius: $border-radius-lg; border-radius: $border-radius-lg;
box-shadow: box-shadow:
0 8px 32px rgba(0, 0, 0, 0.1), 0 8px 32px rgba(0, 0, 0, 0.1),
0 4px 16px rgba(0, 0, 0, 0.1), 0 4px 16px rgba(0, 0, 0, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.2); inset 0 1px 0 rgba(255, 255, 255, 0.2);
animation: slideInUp 0.6s ease-out; animation: slideInUp 0.6s ease-out;
} }
@keyframes slideInUp { @keyframes slideInUp {
from { from {
opacity: 0; opacity: 0;
transform: translateY(30px); transform: translateY(30px);
} }
to { to {
opacity: 1; opacity: 1;
transform: translateY(0); transform: translateY(0);
} }
} }
.login-header { .login-header {
text-align: center; text-align: center;
margin-bottom: $spacing-xl; margin-bottom: $spacing-xl;
.login-title { .login-title {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: $spacing-sm; gap: $spacing-sm;
font-size: 24px; font-size: 24px;
font-weight: 600; font-weight: 600;
color: $white; color: $white;
margin: 0 0 $spacing-sm 0; margin: 0 0 $spacing-sm 0;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
.title-icon { .title-icon {
font-size: 28px; font-size: 28px;
color: $primary-light; color: $primary-light;
} }
} }
.login-subtitle { .login-subtitle {
font-size: 14px; font-size: 14px;
color: $text-light; color: $text-light;
margin: 0; margin: 0;
opacity: 0.9; opacity: 0.9;
} }
} }
.login-form { .login-form {
.form-item { .form-item {
margin-bottom: $spacing-lg; margin-bottom: $spacing-lg;
&:last-child { &:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
} }
.login-input { .login-input {
height: 48px; height: 48px;
background: rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2); border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: $border-radius; border-radius: $border-radius;
transition: $transition; transition: $transition;
&:hover { &:hover {
border-color: rgba(255, 255, 255, 0.4); border-color: rgba(255, 255, 255, 0.4);
background: rgba(255, 255, 255, 0.15); background: rgba(255, 255, 255, 0.15);
} }
&:focus-within { &:focus-within {
border-color: $primary-light; border-color: $primary-light;
background: rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.2);
box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.2); box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.2);
} }
:deep(.ant-input) { :deep(.ant-input) {
background: transparent; background: transparent;
border: none; border: none;
color: $white; color: $white;
font-size: 14px; font-size: 14px;
&::placeholder { &::placeholder {
color: rgba(255, 255, 255, 0.6); color: rgba(255, 255, 255, 0.6);
} }
} }
:deep(.ant-input-password-icon) { :deep(.ant-input-password-icon) {
color: rgba(255, 255, 255, 0.6); color: rgba(255, 255, 255, 0.6);
&:hover { &:hover {
color: $white; color: $white;
} }
} }
} }
.input-icon { .input-icon {
color: rgba(255, 255, 255, 0.6); color: rgba(255, 255, 255, 0.6);
font-size: 16px; font-size: 16px;
} }
} }
.captcha-container { .captcha-container {
display: flex; display: flex;
gap: $spacing-md; gap: $spacing-md;
align-items: stretch; align-items: stretch;
.captcha-input { .captcha-input {
flex: 1; flex: 1;
} }
.captcha-image-container { .captcha-image-container {
position: relative; position: relative;
width: 120px; width: 120px;
height: 48px; height: 48px;
border: 1px solid rgba(255, 255, 255, 0.2); border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: $border-radius; border-radius: $border-radius;
overflow: hidden; overflow: hidden;
cursor: pointer; cursor: pointer;
transition: $transition; transition: $transition;
background: rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.1);
&:hover { &:hover {
border-color: $primary-light; border-color: $primary-light;
.captcha-refresh-hint { .captcha-refresh-hint {
opacity: 1; opacity: 1;
} }
} }
.captcha-image { .captcha-image {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
display: block; display: block;
} }
.captcha-loading { .captcha-loading {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.1);
.loading-icon { .loading-icon {
color: $white; color: $white;
font-size: 20px; font-size: 20px;
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
} }
} }
.captcha-refresh-hint { .captcha-refresh-hint {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: rgba(0, 0, 0, 0.7); background: rgba(0, 0, 0, 0.7);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: $white; color: $white;
font-size: 12px; font-size: 12px;
opacity: 0; opacity: 0;
transition: $transition; transition: $transition;
.refresh-icon { .refresh-icon {
font-size: 16px; font-size: 16px;
margin-bottom: 2px; margin-bottom: 2px;
} }
} }
} }
} }
@keyframes spin { @keyframes spin {
from { from {
transform: rotate(0deg); transform: rotate(0deg);
} }
to { to {
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
.login-button { .login-button {
height: 48px; height: 48px;
background: linear-gradient(135deg, $primary-color 0%, $primary-dark 100%); background: linear-gradient(135deg, $primary-color 0%, $primary-dark 100%);
border: none; border: none;
border-radius: $border-radius; border-radius: $border-radius;
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 600;
transition: $transition; transition: $transition;
box-shadow: 0 4px 12px rgba(74, 144, 226, 0.3); box-shadow: 0 4px 12px rgba(74, 144, 226, 0.3);
&:hover { &:hover {
background: linear-gradient(135deg, $primary-light 0%, $primary-color 100%); background: linear-gradient(135deg, $primary-light 0%, $primary-color 100%);
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(74, 144, 226, 0.4); box-shadow: 0 6px 20px rgba(74, 144, 226, 0.4);
} }
&:active { &:active {
transform: translateY(0); transform: translateY(0);
} }
.button-icon { .button-icon {
margin-right: $spacing-sm; margin-right: $spacing-sm;
font-size: 18px; font-size: 18px;
} }
:deep(.ant-btn-loading-icon) { :deep(.ant-btn-loading-icon) {
margin-right: $spacing-sm; margin-right: $spacing-sm;
} }
} }
// 表单验证错误样式 // 表单验证错误样式
:deep(.ant-form-item-has-error) { :deep(.ant-form-item-has-error) {
.login-input { .login-input {
border-color: $error-color; border-color: $error-color;
&:focus-within { &:focus-within {
box-shadow: 0 0 0 2px rgba(255, 77, 79, 0.2); box-shadow: 0 0 0 2px rgba(255, 77, 79, 0.2);
} }
} }
.captcha-image-container { .captcha-image-container {
border-color: $error-color; border-color: $error-color;
} }
} }
:deep(.ant-form-item-explain-error) { :deep(.ant-form-item-explain-error) {
color: rgba(255, 255, 255, 0.9); color: rgba(255, 255, 255, 0.9);
background: rgba(255, 77, 79, 0.1); background: rgba(255, 77, 79, 0.1);
padding: 4px 8px; padding: 4px 8px;
border-radius: 4px; border-radius: 4px;
margin-top: 4px; margin-top: 4px;
font-size: 12px; font-size: 12px;
} }
// 响应式设计 // 响应式设计
@media (max-width: 768px) { @media (max-width: 768px) {
.login-box { .login-box {
width: 90%; width: 90%;
max-width: 380px; max-width: 380px;
padding: $spacing-lg; padding: $spacing-lg;
} }
.captcha-container { .captcha-container {
flex-direction: column; flex-direction: column;
.captcha-image-container { .captcha-image-container {
width: 100%; width: 100%;
height: 60px; height: 60px;
} }
} }
} }
@media (max-width: 480px) { @media (max-width: 480px) {
.login-box { .login-box {
padding: $spacing-md; padding: $spacing-md;
} }
.login-header .login-title { .login-header .login-title {
font-size: 20px; font-size: 20px;
.title-icon { .title-icon {
font-size: 24px; font-size: 24px;
} }
} }
} }
</style> </style>

View File

@@ -1,86 +1,108 @@
<script setup lang="ts"> <script setup lang="ts">
import { getCurrentInstance } from 'vue'; import { getCurrentInstance } from 'vue'
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router'
import type { ColumnsType } from 'ant-design-vue/es/table/interface'; import type { ColumnsType } from 'ant-design-vue/es/table/interface'
import { useTraceOrderStore } from '@/store'; import { useTraceOrderStore } from '@/store'
const { proxy } = getCurrentInstance() as any const { proxy } = getCurrentInstance() as any
const { mes_station_status } = proxy.useDict("mes_station_status", "lot_trace_order_status") const { mes_station_status } = proxy.useDict('mes_station_status')
interface TableItem { interface TableItem {
key: string; key: string
index: number; index: number
operationTitle: string; operationTitle: string
planQty: number; planQty: number
okQty: number; okQty: number
ngQty: number; ngQty: number
status: string; status: string
[key: string]: any; [key: string]: any
} }
const router = useRouter(); const router = useRouter()
const traceOrderStore = useTraceOrderStore(); const traceOrderStore = useTraceOrderStore()
const columns = [ const columns = [
{ title: '序号', dataIndex: 'index', key: 'index', align: 'center', width: 80 }, {
{ title: '制程', dataIndex: 'operationTitle', key: 'operationTitle', align: 'center' }, title: '序号',
{ title: '总数量', dataIndex: 'planQty', key: 'planQty', align: 'center' }, dataIndex: 'index',
{ title: '合格数量', dataIndex: 'okQty', key: 'okQty', align: 'center' }, key: 'index',
{ title: '报废数量', dataIndex: 'ngQty', key: 'ngQty', align: 'center' }, align: 'center',
{ title: '状态', dataIndex: 'status', key: 'status', align: 'center' }, width: 80,
{ title: '操作', key: 'action', align: 'center' }, },
]; {
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' },
]
const handleInfeed = (record: any) => { const handleInfeed = (record: any) => {
router.push({ name: 'Infeed' }); router.push({ name: 'Infeed' })
traceOrderStore.selectStation(record.id); traceOrderStore.selectStation(record.id)
}; }
const handleOutfeed = (record: any) => { const handleOutfeed = (record: any) => {
router.push({ name: 'Outfeed' }); router.push({ name: 'Outfeed' })
traceOrderStore.selectStation(record.id); traceOrderStore.selectStation(record.id)
}; }
function rowClick(record: TableItem) { function rowClick(record: TableItem) {
return { return {
onClick: () => { onClick: () => {
traceOrderStore.selectStation(record.id); traceOrderStore.selectStation(record.id)
}, },
}; }
} }
</script> </script>
<template> <template>
<div class="table-wrapper"> <div class="table-wrapper">
<a-table <a-table
:dataSource="traceOrderStore.stationList" :dataSource="traceOrderStore.stationList"
:columns="columns as ColumnsType<TableItem>" :columns="columns as ColumnsType<TableItem>"
:pagination="false" :pagination="false"
:loading="traceOrderStore.loadingStations" :loading="traceOrderStore.loadingStations"
bordered bordered
sticky sticky
rowKey="key" rowKey="key"
class="custom-table" class="custom-table"
:customRow="rowClick" :customRow="rowClick"
> >
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'"> <template v-if="column.key === 'status'">
<DictTag :options="mes_station_status" :value="record.status" size="large" /> <DictTag
</template> :options="mes_station_status"
<template v-if="column.key === 'action'"> :value="record.status"
<a-space> size="large"
<a-button size="large" @click="handleInfeed(record)">进站</a-button> />
<a-button size="large" type="primary" danger @click="handleOutfeed(record)">出站</a-button> </template>
</a-space> <template v-if="column.key === 'action'">
</template> <a-space>
</template> <a-button size="large" @click="handleInfeed(record)">进站</a-button>
</a-table> <a-button
</div> size="large"
type="primary"
danger
@click="handleOutfeed(record)"
>
出站
</a-button>
</a-space>
</template>
</template>
</a-table>
</div>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.table-wrapper { .table-wrapper {
height: 100%; height: 100%;
overflow: auto; overflow: auto;
} }
</style> </style>

View File

@@ -1,182 +1,214 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, reactive } from 'vue'; import { ref, onMounted, reactive } from 'vue'
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue'
import { useTraceOrderStore } from '@/store'; import { useTraceOrderStore } from '@/store'
import type { ColumnsType } from 'ant-design-vue/es/table/interface'; import type { ColumnsType } from 'ant-design-vue/es/table/interface'
import { import {
getEquipmentByCode, getEquipmentByCode,
listEquipmentEntry, listEquipmentEntry,
addEquipmentEntry, addEquipmentEntry,
delEquipmentEntry, delEquipmentEntry,
} from '@/api/traceOrderManage/equipment'; } from '@/api/traceOrderManage/equipment'
const traceOrderStore = useTraceOrderStore(); const traceOrderStore = useTraceOrderStore()
interface EquipmentTableItem { interface EquipmentTableItem {
key: string; key: string
equipmentName: string; equipmentName: string
equipmentCode: string; equipmentCode: string
[key: string]: any; [key: string]: any
} }
const equipmentColumns = [ const equipmentColumns = [
{ title: '序号', dataIndex: 'index', key: 'index', align: 'center', width: 80 }, {
{ title: '设备编码', dataIndex: 'equipmentCode', key: 'equipmentCode', align: 'center' }, title: '序号',
{ title: '设备名称', dataIndex: 'equipmentTitle', key: 'equipmentTitle', align: 'center' }, dataIndex: 'index',
{ title: '操作', dataIndex: 'action', key: 'action', align: 'center' }, key: 'index',
]; align: 'center',
width: 80,
},
{
title: '设备编码',
dataIndex: 'equipmentCode',
key: 'equipmentCode',
align: 'center',
},
{
title: '设备名称',
dataIndex: 'equipmentTitle',
key: 'equipmentTitle',
align: 'center',
},
{ title: '操作', dataIndex: 'action', key: 'action', align: 'center' },
]
const equipmentInfo = reactive({ const equipmentInfo = reactive({
equipmentTitle: '', equipmentTitle: '',
equipmentCode: '', equipmentCode: '',
}); })
const fetchEquipmentInfo = async () => { const fetchEquipmentInfo = async () => {
const { data } = await getEquipmentByCode(equipmentInput.value) const { data } = await getEquipmentByCode(equipmentInput.value)
if (!data) { if (!data) {
message.error('设备不存在'); message.error('设备不存在')
return; return
} }
Object.assign(equipmentInfo, data); Object.assign(equipmentInfo, data)
} }
// 治具表格 // 治具表格
const equipmentTableData = ref<EquipmentTableItem[]>([]); const equipmentTableData = ref<EquipmentTableItem[]>([])
const equipmentInput = ref<string>(''); const equipmentInput = ref<string>('')
const handleBind = async () => { const handleBind = async () => {
if (!equipmentInput.value) return; if (!equipmentInput.value) return
try { try {
await addEquipmentEntry({ await addEquipmentEntry({
equipmentCode: equipmentInput.value, equipmentCode: equipmentInput.value,
stationCode: traceOrderStore.currentStationCode, stationCode: traceOrderStore.currentStationCode,
}); })
message.success('绑定成功'); message.success('绑定成功')
equipmentInput.value = ''; equipmentInput.value = ''
fetchBoundEquipmentList(); fetchBoundEquipmentList()
} catch (error: any) { } catch (error: any) {
message.error(`绑定失败: ${ error.message }`); message.error(`绑定失败: ${error.message}`)
} }
} }
const handleUnbind = async (id: number) => { const handleUnbind = async (id: number) => {
try { try {
await delEquipmentEntry(id); await delEquipmentEntry(id)
message.success('解绑成功'); message.success('解绑成功')
fetchBoundEquipmentList(); fetchBoundEquipmentList()
} catch (error: any) { } catch (error: any) {
message.error(`解绑失败: ${ error.message }`); message.error(`解绑失败: ${error.message}`)
} }
} }
// 获取已绑定的设备列表 // 获取已绑定的设备列表
const loadingEquipmentTableData = ref(false); const loadingEquipmentTableData = ref(false)
const fetchBoundEquipmentList = async () => { const fetchBoundEquipmentList = async () => {
const stationId = traceOrderStore.currentStationId; const stationId = traceOrderStore.currentStationId
if (!stationId) return; if (!stationId) return
loadingEquipmentTableData.value = true; loadingEquipmentTableData.value = true
try { try {
const { rows } = await listEquipmentEntry({ stationId }); const { rows } = await listEquipmentEntry({ stationId })
equipmentTableData.value = rows; equipmentTableData.value = rows
} catch (error: any) { } catch (error: any) {
message.error(error.message || '查询治具列表失败'); message.error(error.message || '查询治具列表失败')
} finally { } finally {
loadingEquipmentTableData.value = false; loadingEquipmentTableData.value = false
} }
} }
const handleRefresh = () => { const handleRefresh = () => {
fetchBoundEquipmentList(); fetchBoundEquipmentList()
renderTableHeight(); renderTableHeight()
} }
// 计算表格高度 // 计算表格高度
const customTable = ref<HTMLElement | null>(null) const customTable = ref<HTMLElement | null>(null)
const tableHeight = ref(200) const tableHeight = ref(200)
const renderTableHeight = () => { const renderTableHeight = () => {
if (customTable.value) { if (customTable.value) {
tableHeight.value = customTable.value.clientHeight - 50 tableHeight.value = customTable.value.clientHeight - 50
console.log('元素高度:', tableHeight.value) console.log('元素高度:', tableHeight.value)
} }
} }
onMounted(() => { onMounted(() => {
renderTableHeight() renderTableHeight()
}) })
defineExpose({ defineExpose({
renderTableHeight renderTableHeight,
}); })
fetchBoundEquipmentList() fetchBoundEquipmentList()
</script> </script>
<template> <template>
<div class="equipment__container"> <div class="equipment__container">
<Title name="绑定设备列表" showRefresh @refresh="handleRefresh" /> <Title name="绑定设备列表" showRefresh @refresh="handleRefresh" />
<a-row class="main-content" :gutter="24"> <a-row class="main-content" :gutter="24">
<a-col :span="10" class="input__container"> <a-col :span="10" class="input__container">
<a-row :gutter="12"> <a-row :gutter="12">
<a-col :span="21"> <a-col :span="21">
<a-input size="large" v-model:value="equipmentInput" @pressEnter="fetchEquipmentInfo" placeholder="按下回车搜索" /> <a-input
</a-col> size="large"
<a-col :span="3"> v-model:value="equipmentInput"
<a-button size="large" @click="handleBind">绑定</a-button> @pressEnter="fetchEquipmentInfo"
</a-col> placeholder="按下回车搜索"
</a-row> />
<div class="description-wrapper"> </a-col>
<a-descriptions :column="1" bordered> <a-col :span="3">
<a-descriptions-item label="设备编码">{{ equipmentInfo.equipmentCode }}</a-descriptions-item> <a-button size="large" @click="handleBind">绑定</a-button>
<a-descriptions-item label="设备名称">{{ equipmentInfo.equipmentTitle }}</a-descriptions-item> </a-col>
</a-descriptions> </a-row>
</div> <div class="description-wrapper">
</a-col> <a-descriptions :column="1" bordered>
<a-col :span="14"> <a-descriptions-item label="设备编码">
<div class="table-wrapper" ref="customTable"> {{ equipmentInfo.equipmentCode }}
<a-table :dataSource="equipmentTableData" :columns="equipmentColumns as ColumnsType<EquipmentTableItem>" </a-descriptions-item>
:pagination="false" bordered sticky :scroll="{ y: tableHeight }" :loading="loadingEquipmentTableData"> <a-descriptions-item label="设备名称">
<template #bodyCell="{ column, index, record }"> {{ equipmentInfo.equipmentTitle }}
<template v-if="column.key === 'index'">{{ index + 1 }}</template> </a-descriptions-item>
<template v-if="column.key === 'action'"> </a-descriptions>
<a-button @click="handleUnbind(record.id)">解绑</a-button> </div>
</template> </a-col>
</template> <a-col :span="14">
</a-table> <div class="table-wrapper" ref="customTable">
</div> <a-table
</a-col> :dataSource="equipmentTableData"
</a-row> :columns="equipmentColumns as ColumnsType<EquipmentTableItem>"
</div> :pagination="false"
bordered
sticky
:scroll="{ y: tableHeight }"
:loading="loadingEquipmentTableData"
>
<template #bodyCell="{ column, index, record }">
<template v-if="column.key === 'index'">{{ index + 1 }}</template>
<template v-if="column.key === 'action'">
<a-button @click="handleUnbind(record.id)">解绑</a-button>
</template>
</template>
</a-table>
</div>
</a-col>
</a-row>
</div>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.equipment__container { .equipment__container {
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: 16px;
} }
.main-content { .main-content {
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
} }
.input__container { .input__container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: 16px;
overflow: hidden; overflow: hidden;
height: 100%; height: 100%;
.description-wrapper { .description-wrapper {
flex: 1; flex: 1;
overflow: auto; overflow: auto;
} }
} }
.table-wrapper { .table-wrapper {
height: 100%; height: 100%;
overflow: auto; overflow: auto;
} }
</style> </style>

View File

@@ -1,167 +1,169 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, inject } from 'vue'; import { ref, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'; import { useRouter, useRoute } from 'vue-router'
import { startStation } from '@/api/traceOrderManage/station'; import { startStation } from '@/api/traceOrderManage/station'
import { useTraceOrderStore } from '@/store'; import { useTraceOrderStore } from '@/store'
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue'
const router = useRouter(); const router = useRouter()
const route = useRoute(); const route = useRoute()
const traceOrderStore = useTraceOrderStore(); const traceOrderStore = useTraceOrderStore()
const menuItems = [ const menuItems = [
{ label: '主材', key: 'PrimaryMaterial', progress: 30 }, { label: '主材', key: 'PrimaryMaterial', progress: 30 },
{ label: '治具', key: 'Mask', progress: 80 }, { label: '治具', key: 'Mask', progress: 80 },
{ label: '设备', key: 'Equipment', progress: 80 }, { label: '设备', key: 'Equipment', progress: 80 },
]; ]
const activeKey = computed(() => route.name as string); const activeKey = computed(() => route.name as string)
const handleMenuClick = (name: string) => { const handleMenuClick = (name: string) => {
router.push({ name}); router.push({ name })
}; }
// 确认进站 // 确认进站
const infeeding = ref(false); const infeeding = ref(false)
const handleInfeed = async () => { const handleInfeed = async () => {
infeeding.value = true; infeeding.value = true
try { try {
await startStation(traceOrderStore.currentStationId); await startStation(traceOrderStore.currentStationId)
message.success('进站成功'); message.success('进站成功')
traceOrderStore.fetchStationInfo(); traceOrderStore.fetchStationInfo()
traceOrderStore.fetchStationList(); traceOrderStore.fetchStationList()
router.push({ name: 'TraceOrderManage' }); router.push({ name: 'TraceOrderManage' })
} catch (error: any) { } catch (error: any) {
message.error(error.message || '进站失败'); message.error(error.message || '进站失败')
} finally { } finally {
infeeding.value = false; infeeding.value = false
} }
}; }
const infeedChildRef = ref<any>(null); const infeedChildRef = ref<any>(null)
/** 父组件对外暴露的方法 */ /** 父组件对外暴露的方法 */
function renderTableHeight() { function renderTableHeight() {
infeedChildRef.value?.renderTableHeight(); infeedChildRef.value?.renderTableHeight()
} }
defineExpose({ defineExpose({
renderTableHeight renderTableHeight,
}); })
</script> </script>
<template> <template>
<a-spin :spinning="infeeding"> <a-spin :spinning="infeeding">
<a-row class="infeed-layout"> <a-row class="infeed-layout">
<a-col :span="20" class="content-wrapper"> <a-col :span="20" class="content-wrapper">
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">
<component :is="Component" ref="infeedChildRef" /> <component :is="Component" ref="infeedChildRef" />
</router-view> </router-view>
</a-col> </a-col>
<a-col :span="4" class="menu-wrapper"> <a-col :span="4" class="menu-wrapper">
<div class="menu-list"> <div class="menu-list">
<span class="infeed-title">进站</span> <span class="infeed-title">进站</span>
<div <div
v-for="item in menuItems" v-for="item in menuItems"
:key="item.key" :key="item.key"
class="menu-item" class="menu-item"
:class="{ active: activeKey === item.key }" :class="{ active: activeKey === item.key }"
@click="handleMenuClick(item.key)" @click="handleMenuClick(item.key)"
> >
<div class="menu-content"> <div class="menu-content">
<span class="menu-title">{{ item.label }}</span> <span class="menu-title">{{ item.label }}</span>
<a-progress <a-progress
:percent="item.progress" :percent="item.progress"
:show-info="false" :show-info="false"
size="small" size="small"
:stroke-color="activeKey === item.key ? '#1890ff' : undefined" :stroke-color="activeKey === item.key ? '#1890ff' : undefined"
class="menu-progress" class="menu-progress"
/> />
</div> </div>
</div> </div>
<a-button type="primary" size="large" @click="handleInfeed">确认进站</a-button> <a-button type="primary" size="large" @click="handleInfeed">
</div> 确认进站
</a-col> </a-button>
</a-row> </div>
</a-spin> </a-col>
</a-row>
</a-spin>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.infeed-layout { .infeed-layout {
height: 100%; height: 100%;
} }
.menu-list { .menu-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: 16px;
height: 100%; height: 100%;
} }
.infeed-title { .infeed-title {
font-size: 20px; font-size: 20px;
font-weight: 600; font-weight: 600;
border-bottom: 1px solid #c9c9c9; border-bottom: 1px solid #c9c9c9;
padding: 0 0 8px; padding: 0 0 8px;
} }
.menu-item { .menu-item {
flex: 1; flex: 1;
background: #fff; background: #fff;
border-radius: 8px; border-radius: 8px;
padding: 1vh 1vw; padding: 1vh 1vw;
cursor: pointer; cursor: pointer;
transition: all 0.3s; transition: all 0.3s;
border: 1px solid #f0f0f0; border: 1px solid #f0f0f0;
box-shadow: 0 2px 4px rgba(0,0,0,0.02); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.02);
position: relative; position: relative;
overflow: hidden; overflow: hidden;
&.active { &.active {
background: #e6f7ff; background: #e6f7ff;
border-color: #1890ff; border-color: #1890ff;
.menu-title {
color: #1890ff;
}
}
&:hover { .menu-title {
border-color: #47a5fd; color: #1890ff;
} }
}
&:hover {
border-color: #47a5fd;
}
} }
.menu-content { .menu-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-around; justify-content: space-around;
height: 100%; height: 100%;
} }
.menu-title { .menu-title {
font-size: 18px; font-size: 18px;
font-weight: bold; font-weight: bold;
color: #333; color: #333;
line-height: 1.5; line-height: 1.5;
} }
.menu-progress { .menu-progress {
margin-bottom: 0 !important; margin-bottom: 0 !important;
:deep(.ant-progress-bg) { :deep(.ant-progress-bg) {
transition: all 0.3s; transition: all 0.3s;
} }
} }
.menu-wrapper { .menu-wrapper {
padding: 12px; padding: 12px;
border-radius: 7px; border-radius: 7px;
background-color: #f3f3f3; background-color: #f3f3f3;
} }
.content-wrapper { .content-wrapper {
height: 100%; height: 100%;
padding: 12px; padding: 12px;
} }
</style> </style>

View File

@@ -1,219 +1,306 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, reactive } from 'vue'; import { ref, onMounted, reactive } from 'vue'
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue'
import type { ColumnsType } from 'ant-design-vue/es/table/interface'; import type { ColumnsType } from 'ant-design-vue/es/table/interface'
import { listMaskCombination } from "@/api/traceOrderManage/maskCombination"; import { listMaskCombination } from '@/api/traceOrderManage/maskCombination'
import { listStationBindMask, bindMaskCombinationToStations, unbindStationMask } from "@/api/traceOrderManage/mask"; import {
import { useTraceOrderStore } from '@/store'; listStationBindMask,
bindMaskCombinationToStations,
unbindStationMask,
} from '@/api/traceOrderManage/mask'
import { useTraceOrderStore } from '@/store'
const traceOrderStore = useTraceOrderStore(); const traceOrderStore = useTraceOrderStore()
interface MaskTableItem { interface MaskTableItem {
key: string; key: string
operationTitle: string; operationTitle: string
operationCode: string; operationCode: string
maskName: string; maskName: string
maskCode: string; maskCode: string
[key: string]: any; [key: string]: any
} }
interface MaskCombinationItem { interface MaskCombinationItem {
key: string; key: string
combinationCode: string; combinationCode: string
combinationName: string; combinationName: string
combinationStatus: string; combinationStatus: string
[key: string]: any; [key: string]: any
} }
const maskColumns = [ const maskColumns = [
{ title: '序号', dataIndex: 'index', key: 'index', align: 'center', width: 80 }, {
{ title: '工序编码', dataIndex: 'operationCode', key: 'operationCode', align: 'center' }, title: '序号',
{ title: '工序名称', dataIndex: 'operationTitle', key: 'operationTitle', align: 'center' }, dataIndex: 'index',
{ title: '治具编码', dataIndex: 'maskCode', key: 'maskCode', align: 'center' }, key: 'index',
{ title: '治具名称', dataIndex: 'maskName', key: 'maskName', align: 'center' }, align: 'center',
{ title: '操作', dataIndex: 'action', key: 'action', 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',
},
{ title: '操作', dataIndex: 'action', key: 'action', align: 'center' },
]
// 治具表格 // 治具表格
const maskTableData = ref<MaskTableItem[]>([]); const maskTableData = ref<MaskTableItem[]>([])
const maskInput = ref<string>(''); const maskInput = ref<string>('')
const openMaskModal = ref(false) const openMaskModal = ref(false)
const maskCombinationColumns = [ const maskCombinationColumns = [
{ title: '序号', dataIndex: 'index', key: 'index', align: 'center', width: 80 }, {
{ title: '组合编码', dataIndex: 'combinationCode', key: 'combinationCode', align: 'center' }, title: '序号',
{ title: '组合名称', dataIndex: 'combinationName', key: 'combinationName', align: 'center' }, dataIndex: 'index',
{ title: '组合状态', dataIndex: 'combinationStatus', key: 'combinationStatus', align: 'center' }, key: 'index',
{ title: '操作', dataIndex: 'action', key: 'action', align: 'center', width: 100 }, 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 maskCombinationTableData = ref<MaskCombinationItem[]>([]) const maskCombinationTableData = ref<MaskCombinationItem[]>([])
const fetchCombinationList = async () => { const fetchCombinationList = async () => {
try { try {
const { rows, total } = await listMaskCombination({}) const { rows, total } = await listMaskCombination({})
if (total as number <= 0) throw new Error('未查询到组合列表'); if ((total as number) <= 0) throw new Error('未查询到组合列表')
maskCombinationTableData.value = rows; maskCombinationTableData.value = rows
} catch (error: any) { } catch (error: any) {
message.error(error.message || '获取工单信息失败'); message.error(error.message || '获取工单信息失败')
} }
} }
const handleBind = async (id: number) => { const handleBind = async (id: number) => {
try { try {
await bindMaskCombinationToStations({ await bindMaskCombinationToStations({
lotTraceOrderId: traceOrderStore.currentTraceOrderId, lotTraceOrderId: traceOrderStore.currentTraceOrderId,
maskCombinationId: id maskCombinationId: id,
}); })
message.success('绑定成功'); message.success('绑定成功')
fetchBoundMaskList(); fetchBoundMaskList()
openMaskModal.value = false; openMaskModal.value = false
} catch (error: any) { } catch (error: any) {
message.error(`绑定失败: ${ error.message }`); message.error(`绑定失败: ${error.message}`)
} }
} }
const handleUnbind = async (id: number) => { const handleUnbind = async (id: number) => {
try { try {
await unbindStationMask(id); await unbindStationMask(id)
message.success('解绑成功'); message.success('解绑成功')
fetchBoundMaskList(); fetchBoundMaskList()
openMaskModal.value = false; openMaskModal.value = false
} catch (error: any) { } catch (error: any) {
message.error(`解绑失败: ${ error.message }`); message.error(`解绑失败: ${error.message}`)
} }
} }
// 获取已绑定的治具列表 // 获取已绑定的治具列表
const loadingMask = ref(false); const loadingMask = ref(false)
const fetchBoundMaskList = async () => { const fetchBoundMaskList = async () => {
const traceOrderId = traceOrderStore.currentTraceOrderId; const traceOrderId = traceOrderStore.currentTraceOrderId
if (!traceOrderId) return; if (!traceOrderId) return
loadingMask.value = true; loadingMask.value = true
try { try {
const { rows, total } = await listStationBindMask({ traceOrderId }) const { rows, total } = await listStationBindMask({ traceOrderId })
if (total as number <= 0) return; if ((total as number) <= 0) return
maskTableData.value = rows; maskTableData.value = rows
} catch (error: any) { } catch (error: any) {
message.error(error.message || '查询治具列表失败'); message.error(error.message || '查询治具列表失败')
} finally { } finally {
loadingMask.value = false; loadingMask.value = false
} }
} }
const inspectingMask = reactive({ const inspectingMask = reactive({
maskCode: '', maskCode: '',
maskName: '', maskName: '',
}) })
const rowClick = (record: MaskTableItem) => { const rowClick = (record: MaskTableItem) => {
inspectingMask.maskCode = record.maskCode; return {
inspectingMask.maskName = record.maskName; onClick: () => {
inspectingMask.maskCode = record.maskCode
inspectingMask.maskName = record.maskName
},
}
} }
const handleRefresh = () => { const handleRefresh = () => {
fetchBoundMaskList(); fetchBoundMaskList()
renderTableHeight(); renderTableHeight()
} }
// 计算表格高度 // 计算表格高度
const customTable = ref<HTMLElement | null>(null) const customTable = ref<HTMLElement | null>(null)
const tableHeight = ref(200) const tableHeight = ref(200)
const renderTableHeight = () => { const renderTableHeight = () => {
if (customTable.value) { if (customTable.value) {
tableHeight.value = customTable.value.clientHeight - 50 tableHeight.value = customTable.value.clientHeight - 50
console.log('元素高度:', tableHeight.value) console.log('元素高度:', tableHeight.value)
} }
} }
onMounted(() => { onMounted(() => {
renderTableHeight() renderTableHeight()
}) })
defineExpose({ defineExpose({
renderTableHeight renderTableHeight,
}); })
fetchCombinationList() fetchCombinationList()
fetchBoundMaskList() fetchBoundMaskList()
</script> </script>
<template> <template>
<div class="mask__container"> <div class="mask__container">
<a-row class="main-content" :gutter="24"> <a-row class="main-content" :gutter="24">
<a-col :span="14" class="mask-item__container"> <a-col :span="14" class="mask-item__container">
<Title name="治具组合信息" showRefresh @refresh="handleRefresh" /> <Title name="治具组合信息" showRefresh @refresh="handleRefresh" />
<a-button size="large" @click="() => openMaskModal = true">绑定治具组合</a-button> <a-button size="large" @click="() => (openMaskModal = true)">
<div class="table-wrapper" ref="customTable"> 绑定治具组合
<a-table :dataSource="maskTableData" :columns="maskColumns as ColumnsType<MaskTableItem>" </a-button>
:pagination="false" bordered sticky :scroll="{ y: tableHeight }" :loading="loadingMask"> <div class="table-wrapper" ref="customTable">
<template #bodyCell="{ column, index, record }"> <a-table
<template v-if="column.key === 'index'">{{ index + 1 }}</template> :dataSource="maskTableData"
<template v-if="column.key === 'action'"> :columns="maskColumns as ColumnsType<MaskTableItem>"
<a-button @click="handleUnbind(record.id)">解绑</a-button> :pagination="false"
</template> bordered
</template> sticky
</a-table> :scroll="{ y: tableHeight }"
</div> :loading="loadingMask"
</a-col> :customRow="rowClick"
<a-col :span="10" class="mask-item__container"> >
<Title name="治具校验" /> <template #bodyCell="{ column, index, record }">
<a-row> <template v-if="column.key === 'index'">{{ index + 1 }}</template>
<a-col :span="20"> <template v-if="column.key === 'action'">
<a-input size="large" v-model:value="maskInput" @pressEnter="" placeholder="按下回车校验" /> <a-button @click="handleUnbind(record.id)">解绑</a-button>
</a-col> </template>
<a-col :span="3" :offset="1"> </template>
<a-button size="large" @click="">校验</a-button> </a-table>
</a-col> </div>
</a-row> </a-col>
<div class="table-wrapper"> <a-col :span="10" class="mask-item__container">
<a-descriptions bordered :column="1"> <Title name="治具校验" />
<a-descriptions-item label="治具编码"></a-descriptions-item> <a-row>
<a-descriptions-item label="治具名称"></a-descriptions-item> <a-col :span="20">
<a-descriptions-item label="治具组合"></a-descriptions-item> <a-input
<a-descriptions-item label="标准使用次数"></a-descriptions-item> size="large"
<a-descriptions-item label="当前使用次数"></a-descriptions-item> v-model:value="maskInput"
<a-descriptions-item label="关联工序"></a-descriptions-item> placeholder="按下回车校验"
</a-descriptions> />
</div> </a-col>
</a-col> <a-col :span="3" :offset="1">
</a-row> <a-button size="large">校验</a-button>
</a-col>
</a-row>
<div class="table-wrapper">
<a-descriptions bordered :column="1">
<a-descriptions-item label="治具编码"></a-descriptions-item>
<a-descriptions-item label="治具名称"></a-descriptions-item>
<a-descriptions-item label="治具组合"></a-descriptions-item>
<a-descriptions-item label="标准使用次数"></a-descriptions-item>
<a-descriptions-item label="当前使用次数"></a-descriptions-item>
<a-descriptions-item label="关联工序"></a-descriptions-item>
</a-descriptions>
</div>
</a-col>
</a-row>
<a-modal v-model:open="openMaskModal" title="绑定治具组合" width="50vw" :bodyStyle="{ padding: '20px 0' }" :footer="null"> <a-modal
<a-table :dataSource="maskCombinationTableData" :columns="maskCombinationColumns as ColumnsType<MaskCombinationItem>" v-model:open="openMaskModal"
:pagination="false" bordered sticky :scroll="{ y: tableHeight }"> title="绑定治具组合"
<template #bodyCell="{ column, index, record }"> width="50vw"
<template v-if="column.key === 'index'">{{ index + 1 }}</template> :bodyStyle="{ padding: '20px 0' }"
<template v-if="column.key === 'action'"> :footer="null"
<a-button @click="handleBind(record.id)">绑定</a-button> >
</template> <a-table
</template> :dataSource="maskCombinationTableData"
</a-table> :columns="maskCombinationColumns as ColumnsType<MaskCombinationItem>"
</a-modal> :pagination="false"
</div> 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> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.mask__container { .mask__container {
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.main-content { .main-content {
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
.mask-item__container { .mask-item__container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: 16px;
overflow: hidden; overflow: hidden;
height: 100%; height: 100%;
.table-wrapper { .table-wrapper {
flex: 1; flex: 1;
overflow: auto; overflow: auto;
} }
} }
} }
</style> </style>

View File

@@ -1,187 +1,233 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue'; import { ref, onMounted } from 'vue'
import { useTraceOrderStore } from '@/store' import { useTraceOrderStore } from '@/store'
import type { ColumnsType } from 'ant-design-vue/es/table/interface'; import type { ColumnsType } from 'ant-design-vue/es/table/interface'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { listMainMaterialEntryLog, addWaferEntryLogByCarrier, addDieEntryLogByCarrier, addWaferEntryLog, addDieEntryLog, delMainMaterialEntryLog } from '@/api/traceOrderManage/primaryMaterial' import {
listMainMaterialEntryLog,
addWaferEntryLogByCarrier,
addDieEntryLogByCarrier,
addWaferEntryLog,
addDieEntryLog,
delMainMaterialEntryLog,
} from '@/api/traceOrderManage/primaryMaterial'
const traceOrderStore = useTraceOrderStore(); const traceOrderStore = useTraceOrderStore()
interface MaterialTableItem { interface MaterialTableItem {
key: string; key: string
carrierCode: string; carrierCode: string
qrCode: string; qrCode: string
dieCode: string; dieCode: string
waferCode: string; waferCode: string
[key: string]: any; [key: string]: any
} }
const materialColumns = [ const materialColumns = [
{ title: '序号', dataIndex: 'index', key: 'index', align: 'center', width: 80 }, {
{ title: '载具 ID', dataIndex: 'carrierCode', key: 'carrierCode', align: 'center' }, title: '序号',
{ title: 'Wafer ID', dataIndex: 'waferCode', key: 'waferCode', align: 'center' }, dataIndex: 'index',
{ title: 'Die ID', dataIndex: 'dieCode', key: 'dieCode', align: 'center' }, key: 'index',
{ title: '二维码', dataIndex: 'qrCode', key: 'qrCode', align: 'center' }, align: 'center',
{ title: '操作', key: 'action', align: 'center', width: 120 }, width: 80,
},
{
title: '载具 ID',
dataIndex: 'carrierCode',
key: 'carrierCode',
align: 'center',
},
{
title: 'Wafer ID',
dataIndex: 'waferCode',
key: 'waferCode',
align: 'center',
},
{ title: 'Die ID', dataIndex: 'dieCode', key: 'dieCode', align: 'center' },
{ title: '二维码', dataIndex: 'qrCode', key: 'qrCode', align: 'center' },
{ title: '操作', key: 'action', align: 'center', width: 120 },
] ]
// 查询站点下主材信息 // 查询站点下主材信息
const loadingMaterialTableData = ref(false); const loadingMaterialTableData = ref(false)
const fetchPrimaryMaterialList = async () => { const fetchPrimaryMaterialList = async () => {
const stationId = traceOrderStore.currentStationId; const stationId = traceOrderStore.currentStationId
if (!stationId) return; if (!stationId) return
loadingMaterialTableData.value = true; loadingMaterialTableData.value = true
try { try {
const { rows } = await listMainMaterialEntryLog({ stationId }); const { rows } = await listMainMaterialEntryLog({ stationId })
materialTableData.value = rows; materialTableData.value = rows
} catch (error: any) { } catch (error: any) {
message.error(error.message || '查询站点下主材信息失败'); message.error(error.message || '查询站点下主材信息失败')
} finally { } finally {
loadingMaterialTableData.value = false; loadingMaterialTableData.value = false
} }
}; }
const carrierInput = ref<string>(''); const carrierInput = ref<string>('')
// 录入载具 // 录入载具
const insertCarrier = async () => { const insertCarrier = async () => {
if (!carrierInput.value) return; if (!carrierInput.value) return
const form = { const form = {
carrierCode: carrierInput.value, carrierCode: carrierInput.value,
stationCode: traceOrderStore.currentStationCode, stationCode: traceOrderStore.currentStationCode,
}; }
try { try {
if (materialType.value === "Wafer") { if (materialType.value === 'Wafer') {
await addWaferEntryLogByCarrier(form); await addWaferEntryLogByCarrier(form)
} else if (materialType.value === "Die") { } else if (materialType.value === 'Die') {
await addDieEntryLogByCarrier(form); await addDieEntryLogByCarrier(form)
} else throw new Error('主材类型异常'); } else throw new Error('主材类型异常')
carrierInput.value = ''; carrierInput.value = ''
message.success('添加成功'); message.success('添加成功')
fetchPrimaryMaterialList(); fetchPrimaryMaterialList()
} catch (error: any) { } catch (error: any) {
message.error(error.message || '添加载具失败'); message.error(error.message || '添加载具失败')
return; return
} }
} }
// 主材料表格 // 主材料表格
const materialTableData = ref<MaterialTableItem[]>([]); const materialTableData = ref<MaterialTableItem[]>([])
const materialInput = ref<string>(''); const materialInput = ref<string>('')
// 录入主材料 // 录入主材料
const insertMaterial = async () => { const insertMaterial = async () => {
if (!materialInput.value) return; if (!materialInput.value) return
const form = { const form = {
mainMaterialCodes: [materialInput.value], mainMaterialCodes: [materialInput.value],
stationCode: traceOrderStore.currentStationCode, stationCode: traceOrderStore.currentStationCode,
}; }
try { try {
if (materialType.value === "Wafer") { if (materialType.value === 'Wafer') {
await addWaferEntryLog(form); await addWaferEntryLog(form)
} else if (materialType.value === "Die") { } else if (materialType.value === 'Die') {
await addDieEntryLog(form); await addDieEntryLog(form)
} else throw new Error('主材类型异常'); } else throw new Error('主材类型异常')
materialInput.value = ''; materialInput.value = ''
message.success('添加成功'); message.success('添加成功')
fetchPrimaryMaterialList(); fetchPrimaryMaterialList()
} catch (error: any) { } catch (error: any) {
message.error(error.message || '添加主材失败'); message.error(error.message || '添加主材失败')
return; return
} }
} }
// 移除主材料 // 移除主材料
const handleRemoveMaterial = async (row: MaterialTableItem) => { const handleRemoveMaterial = async (row: MaterialTableItem) => {
try { try {
await delMainMaterialEntryLog(row.id); await delMainMaterialEntryLog(row.id)
message.success('删除成功'); message.success('删除成功')
fetchPrimaryMaterialList(); fetchPrimaryMaterialList()
} catch (error: any) { } catch (error: any) {
message.error(error.message || '删除主材失败'); message.error(error.message || '删除主材失败')
return; return
} }
} }
const handleRefresh = () => { const handleRefresh = () => {
fetchPrimaryMaterialList(); fetchPrimaryMaterialList()
renderTableHeight(); renderTableHeight()
} }
// 计算表格高度 // 计算表格高度
const customTable = ref<HTMLElement | null>(null) const customTable = ref<HTMLElement | null>(null)
const tableHeight = ref(200) const tableHeight = ref(200)
const renderTableHeight = () => { const renderTableHeight = () => {
if (customTable.value) { if (customTable.value) {
tableHeight.value = customTable.value.clientHeight - 50 tableHeight.value = customTable.value.clientHeight - 50
console.log('元素高度:', tableHeight.value) console.log('元素高度:', tableHeight.value)
} }
} }
onMounted(() => { onMounted(() => {
renderTableHeight() renderTableHeight()
}) })
defineExpose({ defineExpose({
renderTableHeight renderTableHeight,
}); })
// 初始化主材类型 // 初始化主材类型
const materialType = ref() const materialType = ref()
if (traceOrderStore.traceOrderInfo.orderType) { if (traceOrderStore.traceOrderInfo.orderType) {
const num = parseInt(traceOrderStore.traceOrderInfo.orderType) const num = parseInt(traceOrderStore.traceOrderInfo.orderType)
materialType.value = num % 4 === 0 ? 'Die' : 'Wafer' materialType.value = num % 4 === 0 ? 'Die' : 'Wafer'
} else { } else {
materialType.value = '主材类型异常' materialType.value = '主材类型异常'
} }
fetchPrimaryMaterialList() fetchPrimaryMaterialList()
</script> </script>
<template> <template>
<div class="primary-material__container"> <div class="primary-material__container">
<Title name="主材料进站" showRefresh @refresh="handleRefresh" /> <Title name="主材料进站" showRefresh @refresh="handleRefresh" />
<a-form layout="inline" size="large"> <a-form layout="inline" size="large">
<a-form-item label="主材类型"> <a-form-item label="主材类型">
<a-input v-model:value="materialType" disabled /> <a-input v-model:value="materialType" disabled />
</a-form-item> </a-form-item>
<a-form-item label="载具 ID"> <a-form-item label="载具 ID">
<a-input v-model:value="carrierInput" @pressEnter="insertCarrier" placeholder="按下回车录入" allow-clear /> <a-input
</a-form-item> v-model:value="carrierInput"
<a-form-item> @pressEnter="insertCarrier"
<a-button @click="insertCarrier">录入</a-button> placeholder="按下回车录入"
</a-form-item> allow-clear
<a-form-item label="主材 ID"> />
<a-input v-model:value="materialInput" @pressEnter="insertMaterial" placeholder="按下回车录入" allow-clear /> </a-form-item>
</a-form-item> <a-form-item>
<a-form-item> <a-button @click="insertCarrier">录入</a-button>
<a-button @click="insertMaterial">录入</a-button> </a-form-item>
</a-form-item> <a-form-item label="主材 ID">
</a-form> <a-input
<div class="table-wrapper" ref="customTable"> v-model:value="materialInput"
<a-table :dataSource="materialTableData" :columns="materialColumns as ColumnsType<MaterialTableItem>" @pressEnter="insertMaterial"
:pagination="false" bordered sticky :scroll="{ y: tableHeight }" :loading="loadingMaterialTableData"> placeholder="按下回车录入"
<template #bodyCell="{ column, index, record }"> allow-clear
<template v-if="column.key === 'index'">{{ index + 1 }}</template> />
<template v-if="column.key === 'action'"> </a-form-item>
<a-button type="text" danger @click="handleRemoveMaterial(record as MaterialTableItem)">删除</a-button> <a-form-item>
</template> <a-button @click="insertMaterial">录入</a-button>
</template> </a-form-item>
</a-table> </a-form>
</div> <div class="table-wrapper" ref="customTable">
</div> <a-table
:dataSource="materialTableData"
:columns="materialColumns as ColumnsType<MaterialTableItem>"
:pagination="false"
bordered
sticky
:scroll="{ y: tableHeight }"
:loading="loadingMaterialTableData"
>
<template #bodyCell="{ column, index, record }">
<template v-if="column.key === 'index'">{{ index + 1 }}</template>
<template v-if="column.key === 'action'">
<a-button
type="text"
danger
@click="handleRemoveMaterial(record as MaterialTableItem)"
>
删除
</a-button>
</template>
</template>
</a-table>
</div>
</div>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.primary-material__container { .primary-material__container {
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: 16px;
overflow: hidden; overflow: hidden;
height: 100%; height: 100%;
.table-wrapper { .table-wrapper {
flex: 1; flex: 1;
overflow: auto; overflow: auto;
} }
} }
</style> </style>

View File

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

View File

@@ -1,102 +1,159 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, nextTick, getCurrentInstance } from 'vue'; import { ref, nextTick, getCurrentInstance } from 'vue'
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router'
import { useTraceOrderStore } from '@/store'; import { useTraceOrderStore } from '@/store'
import type { LotTraceOrderData } from '@/api/traceOrderManage/model'; import type { LotTraceOrderData } from '@/api/traceOrderManage/model'
import { handleCopy } from '@/utils/copyToClipboard'; import { handleCopy } from '@/utils/copyToClipboard'
const { proxy } = getCurrentInstance() as any const { proxy } = getCurrentInstance() as any
const { mes_station_status, lot_trace_order_status } = proxy.useDict("mes_station_status", "lot_trace_order_status") const { mes_station_status, lot_trace_order_status } = proxy.useDict(
'mes_station_status',
'lot_trace_order_status',
)
const route = useRoute(); const route = useRoute()
const router = useRouter(); const router = useRouter()
const traceOrderStore = useTraceOrderStore(); const traceOrderStore = useTraceOrderStore()
const redirectTo = (routeName: string) => { const redirectTo = (routeName: string) => {
router.push({ name: routeName }); router.push({ name: routeName })
} }
// 孙组件 // 孙组件
const infeedRef = ref<any>(null); const infeedRef = ref<any>(null)
const collapsed = ref(false); const collapsed = ref(false)
// 折叠 // 折叠
const toggleCollapse = async () => { const toggleCollapse = async () => {
collapsed.value = !collapsed.value; collapsed.value = !collapsed.value
await nextTick(); await nextTick()
infeedRef.value?.renderTableHeight(); infeedRef.value?.renderTableHeight()
} }
</script> </script>
<template> <template>
<div class="page-container"> <div class="page-container">
<Header title="MES 过站平台" showHome showLogout> <Header title="MES 过站平台" showHome showLogout>
<template #right-opts> <template #right-opts>
<a-button @click="toggleCollapse"> <a-button @click="toggleCollapse">
<template #icon> <template #icon>
<i-lucide-chevron-up v-if="!collapsed" /> <i-lucide-chevron-up v-if="!collapsed" />
<i-lucide-chevron-down v-else /> <i-lucide-chevron-down v-else />
</template> </template>
{{ collapsed ? '展开' : '折叠' }} {{ collapsed ? '展开' : '折叠' }}
</a-button> </a-button>
</template> </template>
</Header> </Header>
<div class="content-wrapper"> <div class="content-wrapper">
<!-- Top Section --> <!-- Top Section -->
<div class="top-section"> <div class="top-section">
<a-row :gutter="16" class="full-height-row"> <a-row :gutter="16" class="full-height-row">
<a-col :span="13"> <a-col :span="13">
<a-spin :spinning="traceOrderStore.loadingTraceOrderInfo"> <a-spin :spinning="traceOrderStore.loadingTraceOrderInfo">
<a-card title="工单信息" class="info-card" :bordered="false"> <a-card title="工单信息" class="info-card" :bordered="false">
<a-form :model="traceOrderStore.traceOrderInfo" :colon="false" v-show="!collapsed"> <a-form
<a-row :gutter="36"> :model="traceOrderStore.traceOrderInfo"
<a-col :span="12"> :colon="false"
<a-form-item label="工单批次"> v-show="!collapsed"
<a-input v-model:value="traceOrderStore.traceOrderInfo.batchNo" readonly> >
<template #suffix> <a-row :gutter="36">
<a-button @click="handleCopy(traceOrderStore.traceOrderInfo.batchNo)" size="small"> <a-col :span="12">
<template #icon><i-lucide-copy /></template> <a-form-item label="工单批次">
</a-button> <a-input
</template> v-model:value="traceOrderStore.traceOrderInfo.batchNo"
</a-input> readonly
</a-form-item> >
<a-form-item label="工单状态"> <template #suffix>
<a-input readonly> <a-button
<template #prefix> @click="
<DictTag :options="lot_trace_order_status" :value="traceOrderStore.traceOrderInfo.status" size="medium" /> handleCopy(
</template> traceOrderStore.traceOrderInfo.batchNo,
</a-input> )
</a-form-item> "
<a-form-item label="总计数量"> size="small"
<a-input v-model:value="traceOrderStore.traceOrderInfo.planQty" readonly> >
<template #suffix> <template #icon><i-lucide-copy /></template>
<a-button @click="handleCopy(traceOrderStore.traceOrderInfo.planQty)" size="small"> </a-button>
<template #icon><i-lucide-copy /></template> </template>
</a-button> </a-input>
</template> </a-form-item>
</a-input> <a-form-item label="工单状态">
</a-form-item> <a-input readonly>
</a-col> <template #prefix>
<a-col :span="12"> <DictTag
<a-form-item label="产品编码"> :options="lot_trace_order_status"
<a-input v-model:value="traceOrderStore.traceOrderInfo.tarMaterialCode" readonly> :value="traceOrderStore.traceOrderInfo.status"
<template #suffix> size="medium"
<a-button @click="handleCopy(traceOrderStore.traceOrderInfo.tarMaterialCode)" size="small"> />
<template #icon><i-lucide-copy /></template> </template>
</a-button> </a-input>
</template> </a-form-item>
</a-input> <a-form-item label="总计数量">
</a-form-item> <a-input
<a-form-item label="产品名称"> v-model:value="traceOrderStore.traceOrderInfo.planQty"
<a-input v-model:value="traceOrderStore.traceOrderInfo.tarMaterialName" readonly> readonly
<template #suffix> >
<a-button @click="handleCopy(traceOrderStore.traceOrderInfo.tarMaterialName)" size="small"> <template #suffix>
<template #icon><i-lucide-copy /></template> <a-button
</a-button> @click="
</template> handleCopy(
</a-input> traceOrderStore.traceOrderInfo.planQty,
</a-form-item> )
<!-- <a-form-item label="产品规格"> "
size="small"
>
<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="
traceOrderStore.traceOrderInfo.tarMaterialCode
"
readonly
>
<template #suffix>
<a-button
@click="
handleCopy(
traceOrderStore.traceOrderInfo
.tarMaterialCode,
)
"
size="small"
>
<template #icon><i-lucide-copy /></template>
</a-button>
</template>
</a-input>
</a-form-item>
<a-form-item label="产品名称">
<a-input
v-model:value="
traceOrderStore.traceOrderInfo.tarMaterialName
"
readonly
>
<template #suffix>
<a-button
@click="
handleCopy(
traceOrderStore.traceOrderInfo
.tarMaterialName,
)
"
size="small"
>
<template #icon><i-lucide-copy /></template>
</a-button>
</template>
</a-input>
</a-form-item>
<!-- <a-form-item label="产品规格">
<a-input readonly> <a-input readonly>
<template #suffix> <template #suffix>
<a-button @click="handleCopy('')" size="small"> <a-button @click="handleCopy('')" size="small">
@@ -105,168 +162,238 @@ const toggleCollapse = async () => {
</template> </template>
</a-input> </a-input>
</a-form-item> --> </a-form-item> -->
</a-col> </a-col>
</a-row> </a-row>
</a-form> </a-form>
<template #extra> <template #extra>
<a-space> <a-space>
工单编码 工单编码
<a-select v-model:value="traceOrderStore.currentTraceOrderCode" show-search placeholder="输入工单编码" style="width: 300px" <a-select
:show-arrow="false" :options="traceOrderStore.traceOrderOptions" @search="traceOrderStore.fetchTraceOrderOption" :disabled="route.name !== 'TraceOrderManageIndex'" v-model:value="traceOrderStore.currentTraceOrderCode"
@change="(val, option) => traceOrderStore.selectTraceOrder(val as string, option as LotTraceOrderData)" /> show-search
<a-button @click="traceOrderStore.fetchTraceOrderInfo"><template #icon><i-lucide-rotate-cw /></template></a-button> placeholder="输入工单编码"
</a-space> style="width: 300px"
</template> :show-arrow="false"
</a-card> :options="traceOrderStore.traceOrderOptions"
</a-spin> @search="traceOrderStore.fetchTraceOrderOption"
</a-col> :disabled="route.name !== 'TraceOrderManageIndex'"
@change="
(val, option) =>
traceOrderStore.selectTraceOrder(
val as string,
option as LotTraceOrderData,
)
"
/>
<a-button @click="traceOrderStore.fetchTraceOrderInfo">
<template #icon><i-lucide-rotate-cw /></template>
</a-button>
</a-space>
</template>
</a-card>
</a-spin>
</a-col>
<a-col :span="8"> <a-col :span="8">
<a-spin :spinning="traceOrderStore.loadingStationInfo"> <a-spin :spinning="traceOrderStore.loadingStationInfo">
<a-card title="工序信息" class="info-card" :bordered="false"> <a-card title="工序信息" class="info-card" :bordered="false">
<a-form :model="traceOrderStore.stationInfo" :colon="false" v-show="!collapsed"> <a-form
<a-form-item label="工序名称"> :model="traceOrderStore.stationInfo"
<a-input v-model:value="traceOrderStore.stationInfo.operationTitle" readonly> :colon="false"
<template #suffix> v-show="!collapsed"
<a-button @click="handleCopy(traceOrderStore.stationInfo.operationTitle)" size="small"> >
<template #icon><i-lucide-copy /></template> <a-form-item label="工序名称">
</a-button> <a-input
</template> v-model:value="traceOrderStore.stationInfo.operationTitle"
</a-input> readonly
</a-form-item> >
<a-form-item label="工序状态"> <template #suffix>
<a-input readonly> <a-button
<template #prefix> @click="
<DictTag :options="mes_station_status" :value="traceOrderStore.stationInfo.status" size="medium" /> handleCopy(
</template> traceOrderStore.stationInfo.operationTitle,
</a-input> )
</a-form-item> "
<a-form-item label="作业编码"> size="small"
<a-input v-model:value="traceOrderStore.stationInfo.code" readonly> >
<template #suffix> <template #icon><i-lucide-copy /></template>
<a-button @click="handleCopy(traceOrderStore.stationInfo.code)" size="small"> </a-button>
<template #icon><i-lucide-copy /></template> </template>
</a-button> </a-input>
</template> </a-form-item>
</a-input> <a-form-item label="工序状态">
</a-form-item> <a-input readonly>
</a-form> <template #prefix>
<template #extra> <DictTag
<a-button @click="traceOrderStore.fetchStationInfo"><template #icon><i-lucide-rotate-cw /></template></a-button> :options="mes_station_status"
</template> :value="traceOrderStore.stationInfo.status"
</a-card> size="medium"
</a-spin> />
</a-col> </template>
</a-input>
</a-form-item>
<a-form-item label="作业编码">
<a-input
v-model:value="traceOrderStore.stationInfo.code"
readonly
>
<template #suffix>
<a-button
@click="handleCopy(traceOrderStore.stationInfo.code)"
size="small"
>
<template #icon><i-lucide-copy /></template>
</a-button>
</template>
</a-input>
</a-form-item>
</a-form>
<template #extra>
<a-button @click="traceOrderStore.fetchStationInfo">
<template #icon><i-lucide-rotate-cw /></template>
</a-button>
</template>
</a-card>
</a-spin>
</a-col>
<!-- Action Buttons --> <!-- Action Buttons -->
<a-col :span="3" class="action-buttons-col"> <a-col :span="3" class="action-buttons-col">
<div class="action-buttons" v-show="!collapsed"> <div class="action-buttons" v-show="!collapsed">
<a-button class="action-btn" @click="redirectTo('TraceOrderManage')" :disabled="route.name == 'TraceOrderManageIndex'">工单管理</a-button> <a-button
<a-button class="action-btn" @click="redirectTo('PrimaryMaterial')" disabled>主材清单</a-button> class="action-btn"
<HoldTraceOrder /> @click="redirectTo('TraceOrderManage')"
</div> :disabled="route.name == 'TraceOrderManageIndex'"
<a-card title="操作" class="info-card" :bordered="false" v-show="collapsed"> >
<template #extra> 工单管理
<a-dropdown> </a-button>
<template #overlay> <a-button
<a-menu> class="action-btn"
<a-menu-item key="1" @click="redirectTo('TraceOrderManage')">工单管理</a-menu-item> @click="redirectTo('PrimaryMaterial')"
<a-menu-item key="2" @click="redirectTo('PrimaryMaterial')" disabled>主材清单</a-menu-item> disabled
</a-menu> >
</template> 主材清单
<a-button> </a-button>
更多 <HoldTraceOrder />
<i-lucide-chevron-down /> </div>
</a-button> <a-card
</a-dropdown> title="操作"
</template> class="info-card"
</a-card> :bordered="false"
</a-col> v-show="collapsed"
</a-row> >
</div> <template #extra>
<a-dropdown>
<template #overlay>
<a-menu>
<a-menu-item
key="1"
@click="redirectTo('TraceOrderManage')"
>
工单管理
</a-menu-item>
<a-menu-item
key="2"
@click="redirectTo('PrimaryMaterial')"
disabled
>
主材清单
</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 --> <!-- Bottom Section -->
<main class="bottom-section"> <main class="bottom-section">
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">
<component :is="Component" ref="infeedRef" /> <component :is="Component" ref="infeedRef" />
</router-view> </router-view>
</main> </main>
</div> </div>
</div> </div>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.page-container { .page-container {
height: 100vh; height: 100vh;
background-color: #f0f2f5; background-color: #f0f2f5;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
} }
.content-wrapper { .content-wrapper {
padding: 16px; padding: 16px;
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: 16px;
min-height: 0; min-height: 0;
} }
.top-section { .top-section {
background: transparent; background: transparent;
} }
:deep(.ant-spin-nested-loading), :deep(.ant-spin-nested-loading),
:deep(.ant-spin-container) { :deep(.ant-spin-container) {
height: 100%; height: 100%;
} }
.info-card { .info-card {
height: 100%; height: 100%;
border-radius: 8px; border-radius: 8px;
:deep(.ant-card-head) { :deep(.ant-card-head) {
min-height: 48px; min-height: 48px;
padding: 0 16px; padding: 0 16px;
border-bottom: none; border-bottom: none;
font-size: 20px; font-size: 20px;
} }
:deep(.ant-card-body) { :deep(.ant-card-body) {
padding: 1px 16px; padding: 1px 16px;
} }
:deep(.ant-form-item) { :deep(.ant-form-item) {
margin-bottom: 12px; margin-bottom: 12px;
} }
} }
.action-buttons-col { .action-buttons-col {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
// 传递给子组件的样式 // 传递给子组件的样式
:deep(.action-buttons) { :deep(.action-buttons) {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 6px; gap: 6px;
height: 100%; height: 100%;
.action-btn { .action-btn {
height: 48px; height: 48px;
font-size: 20px; font-size: 20px;
font-weight: 500; font-weight: 500;
} }
} }
.bottom-section { .bottom-section {
background: #fff; background: #fff;
padding: 12px 14px; padding: 12px 14px;
border-radius: 8px; border-radius: 8px;
flex: 1; flex: 1;
min-height: 0; min-height: 0;
overflow: auto; overflow: auto;
} }
</style> </style>

View File

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

View File

@@ -1,342 +1,511 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, onMounted, computed, getCurrentInstance } from 'vue'; import { ref, reactive, onMounted, computed, getCurrentInstance } from 'vue'
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue'
import { useTraceOrderStore } from '@/store'; import { useTraceOrderStore } from '@/store'
import type { ColumnsType } from 'ant-design-vue/es/table/interface'; import type { ColumnsType } from 'ant-design-vue/es/table/interface'
import { listMainMaterialEntryLog, listMainMaterialOutboundLog, batchAddMainMaterialOutboundLog, getMainMaterialOutboundLog, updateMainMaterialOutboundLog, delMainMaterialOutboundLog } from "@/api/traceOrderManage/primaryMaterial"; import {
listMainMaterialEntryLog,
listMainMaterialOutboundLog,
batchAddMainMaterialOutboundLog,
updateMainMaterialOutboundLog,
delMainMaterialOutboundLog,
} from '@/api/traceOrderManage/primaryMaterial'
const { proxy } = getCurrentInstance() as any const { proxy } = getCurrentInstance() as any
const { main_material_ok_level, main_material_ng_level } = proxy.useDict("main_material_ok_level", "main_material_ng_level") const { main_material_ok_level, main_material_ng_level } = proxy.useDict(
'main_material_ok_level',
'main_material_ng_level',
)
const traceOrderStore = useTraceOrderStore(); const traceOrderStore = useTraceOrderStore()
interface PrimaryMaterialTableItem { interface PrimaryMaterialTableItem {
key: string; key: string
waferCode: string; waferCode: string
dieCode: string; dieCode: string
xcoordinates: string; xcoordinates: string
ycoordinates: string; ycoordinates: string
outputResult: 'OK' | 'NG'; outputResult: 'OK' | 'NG'
qualityLevel: string; qualityLevel: string
createBy: string; createBy: string
createTime: string; createTime: string
[key: string]: any; [key: string]: any
} }
const primaryMaterialColumns = [ const primaryMaterialColumns = [
{ title: '序号', dataIndex: 'index', key: 'index', align: 'center', width: 80 }, {
{ title: 'Wafer ID', dataIndex: 'waferCode', key: 'waferCode', align: 'center' }, title: '序号',
{ title: 'Die ID', dataIndex: 'dieCode', key: 'dieCode', align: 'center' }, dataIndex: 'index',
{ title: 'Die X 坐标', dataIndex: 'xcoordinates', key: 'xcoordinates', align: 'center' }, key: 'index',
{ title: 'Die Y 坐标', dataIndex: 'ycoordinates', key: 'ycoordinates', align: 'center' }, align: 'center',
{ title: '结果', dataIndex: 'outputResult', key: 'outputResult', align: 'center' }, width: 80,
{ title: '质量等级', dataIndex: 'qualityLevel', key: 'qualityLevel' }, },
{ title: '创建人', dataIndex: 'createBy', key: 'createBy' }, {
{ title: '创建时间', dataIndex: 'createTime', key: 'createTime' }, title: 'Wafer ID',
{ title: '操作', key: 'action', align: 'center', width: 180 }, dataIndex: 'waferCode',
]; key: 'waferCode',
align: 'center',
},
{ title: 'Die ID', dataIndex: 'dieCode', key: 'dieCode', align: 'center' },
{
title: 'Die X 坐标',
dataIndex: 'xcoordinates',
key: 'xcoordinates',
align: 'center',
},
{
title: 'Die Y 坐标',
dataIndex: 'ycoordinates',
key: 'ycoordinates',
align: 'center',
},
{
title: '结果',
dataIndex: 'outputResult',
key: 'outputResult',
align: 'center',
},
{ title: '质量等级', dataIndex: 'qualityLevel', key: 'qualityLevel' },
{ title: '创建人', dataIndex: 'createBy', key: 'createBy' },
{ title: '创建时间', dataIndex: 'createTime', key: 'createTime' },
{ title: '操作', key: 'action', align: 'center', width: 180 },
]
// 总计数据 // 总计数据
const totals = computed(() => { const totals = computed(() => {
let okNum = 0; let okNum = 0
let ngNum = 0; let ngNum = 0
primaryMaterialTableData.value.forEach(({ outputResult, qualityLevel }) => { primaryMaterialTableData.value.forEach(({ outputResult }) => {
if (outputResult === 'OK') { if (outputResult === 'OK') {
okNum++; okNum++
} else { } else {
ngNum++; ngNum++
} }
}); })
return { okNum, ngNum }; return { okNum, ngNum }
}); })
const primaryMaterialTableData = ref<PrimaryMaterialTableItem[]>([]); const primaryMaterialTableData = ref<PrimaryMaterialTableItem[]>([])
const loadingPrimaryMaterialList = ref(false); const loadingPrimaryMaterialList = ref(false)
const fetchPrimaryMaterialList = async () => { const fetchPrimaryMaterialList = async () => {
const stationId = traceOrderStore.currentStationId; const stationId = traceOrderStore.currentStationId
if (!stationId) return; if (!stationId) return
loadingPrimaryMaterialList.value = true; loadingPrimaryMaterialList.value = true
try { try {
const { rows } = await listMainMaterialOutboundLog({ stationId }); const { rows } = await listMainMaterialOutboundLog({ stationId })
primaryMaterialTableData.value = rows; primaryMaterialTableData.value = rows
} catch (error: any) { } catch (error: any) {
message.error(error.message || '查询失败'); message.error(error.message || '查询失败')
} finally { } finally {
loadingPrimaryMaterialList.value = false; loadingPrimaryMaterialList.value = false
} }
} }
const openPickPrimaryMaterial = ref(false); const openPickPrimaryMaterial = ref(false)
interface InfeedMaterialTableItem { interface InfeedMaterialTableItem {
key: string; key: string
waferCode: string; waferCode: string
dieCode: string; dieCode: string
result: 'OK' | 'NG'; result: 'OK' | 'NG'
qualityLevel: string; qualityLevel: string
[key: string]: any; [key: string]: any
} }
const infeedMaterialColumns = [ const infeedMaterialColumns = [
{ title: '序号', dataIndex: 'index', key: 'index', align: 'center', width: 80 }, {
{ title: 'Wafer ID', dataIndex: 'waferCode', key: 'waferCode', align: 'center' }, title: '序号',
{ title: 'Die ID', dataIndex: 'dieCode', key: 'dieCode', align: 'center' }, dataIndex: 'index',
{ title: '结果', dataIndex: 'result', key: 'result', align: 'center' }, key: 'index',
{ title: '质量等级', dataIndex: 'qualityLevel', key: 'qualityLevel' }, align: 'center',
]; width: 80,
const infeedMaterialTableData = ref<InfeedMaterialTableItem[]>([]); },
{
title: 'Wafer ID',
dataIndex: 'waferCode',
key: 'waferCode',
align: 'center',
},
{ title: 'Die ID', dataIndex: 'dieCode', key: 'dieCode', align: 'center' },
{ title: '结果', dataIndex: 'result', key: 'result', align: 'center' },
{ title: '质量等级', dataIndex: 'qualityLevel', key: 'qualityLevel' },
]
const infeedMaterialTableData = ref<InfeedMaterialTableItem[]>([])
// 挑选主材 // 挑选主材
const handlePickPrimaryMaterial = async () => { const handlePickPrimaryMaterial = async () => {
const stationId = traceOrderStore.currentStationId; const stationId = traceOrderStore.currentStationId
if (!stationId) return; if (!stationId) return
try { try {
const { rows } = await listMainMaterialEntryLog({ stationId }); const { rows } = await listMainMaterialEntryLog({ stationId })
infeedMaterialTableData.value = rows.map(item => { infeedMaterialTableData.value = rows.map((item) => {
return { return {
...item, ...item,
result: 'OK', result: 'OK',
qualityLevel: main_material_ok_level.value[0].value || '', qualityLevel: main_material_ok_level.value[0].value || '',
} }
}); })
openPickPrimaryMaterial.value = true; openPickPrimaryMaterial.value = true
} catch (err: any) { } catch (err: any) {
message.error(err.message); message.error(err.message)
} }
} }
const openEditRecordModal = ref(false); const openEditRecordModal = ref(false)
const editingRecord = reactive<any>({}); const editingRecord = reactive<any>({})
// 修改主材出站记录 // 修改主材出站记录
const handleEditRecord = (record: any) => { const handleEditRecord = (record: any) => {
Object.assign(editingRecord, record); Object.assign(editingRecord, record)
openEditRecordModal.value = true; openEditRecordModal.value = true
} }
// 提交修改主材出站记录 // 提交修改主材出站记录
const handleSubmitEdit = async () => { const handleSubmitEdit = async () => {
try { try {
await updateMainMaterialOutboundLog(editingRecord); await updateMainMaterialOutboundLog(editingRecord)
openEditRecordModal.value = false; openEditRecordModal.value = false
} catch (err: any) { } catch (err: any) {
message.error(err.message || '修改失败'); message.error(err.message || '修改失败')
} }
} }
// 删除主材出站记录 // 删除主材出站记录
const handleDeleteRecord = async (record: any) => { const handleDeleteRecord = async (record: any) => {
try { try {
await delMainMaterialOutboundLog(record.id); await delMainMaterialOutboundLog(record.id)
message.success('删除成功'); message.success('删除成功')
} catch (err: any) { } catch (err: any) {
message.error(err.message || '删除失败'); message.error(err.message || '删除失败')
} }
} }
// 选择主材 // 选择主材
const rowSelection = ref<any[]>([]); const rowSelection = ref<any[]>([])
const selectedKeys = ref<any>([]); const selectedKeys = ref<any>([])
const handleChangeSelection = (selectedRowKeys: any, selectedRows: InfeedMaterialTableItem[]) => { const handleChangeSelection = (
selectedKeys.value = selectedRowKeys; selectedRowKeys: any,
rowSelection.value = selectedRows.map(item => { selectedRows: InfeedMaterialTableItem[],
return { ) => {
mainMaterialId: item.waferId ?? item.dieId, selectedKeys.value = selectedRowKeys
result: item.result, rowSelection.value = selectedRows.map((item) => {
qualityLevel: item.qualityLevel, return {
} mainMaterialId: item.waferId ?? item.dieId,
}) result: item.result,
qualityLevel: item.qualityLevel,
}
})
} }
// 提交主材出站 // 提交主材出站
const submitOutfeed = async () => { const submitOutfeed = async () => {
const stationCode = traceOrderStore.currentStationCode; const stationCode = traceOrderStore.currentStationCode
try { try {
await batchAddMainMaterialOutboundLog({ stationCode, outbounds: rowSelection.value }); await batchAddMainMaterialOutboundLog({
message.success("提交成功"); stationCode,
handleCancel(); outbounds: rowSelection.value,
fetchPrimaryMaterialList(); })
} catch (err: any) { message.success('提交成功')
message.error(err.message || '提交失败'); handleCancel()
} fetchPrimaryMaterialList()
} catch (err: any) {
message.error(err.message || '提交失败')
}
} }
// 取消选择主材 // 取消选择主材
const handleCancel = () => { const handleCancel = () => {
rowSelection.value = []; rowSelection.value = []
selectedKeys.value = []; selectedKeys.value = []
} }
const handleRefresh = () => { const handleRefresh = () => {
fetchPrimaryMaterialList(); fetchPrimaryMaterialList()
renderTableHeight(); renderTableHeight()
} }
// 计算表格高度 // 计算表格高度
const customTable = ref<HTMLElement | null>(null) const customTable = ref<HTMLElement | null>(null)
const tableHeight = ref(200) const tableHeight = ref(200)
const renderTableHeight = () => { const renderTableHeight = () => {
if (customTable.value) { if (customTable.value) {
tableHeight.value = customTable.value.clientHeight - 60 tableHeight.value = customTable.value.clientHeight - 60
console.log('元素高度:', tableHeight.value) console.log('元素高度:', tableHeight.value)
} }
} }
onMounted(() => { onMounted(() => {
renderTableHeight() renderTableHeight()
}) })
defineExpose({ defineExpose({
totals, totals,
renderTableHeight renderTableHeight,
}); })
fetchPrimaryMaterialList() fetchPrimaryMaterialList()
</script> </script>
<template> <template>
<div class="equipment__container"> <div class="equipment__container">
<Title name="主材报工" showRefresh @refresh="handleRefresh" /> <Title name="主材报工" showRefresh @refresh="handleRefresh" />
<a-button size="large" @click="handlePickPrimaryMaterial">选择主材</a-button> <a-button size="large" @click="handlePickPrimaryMaterial">
<div class="table-wrapper" ref="customTable"> 选择主材
<a-table :dataSource="primaryMaterialTableData" :columns="primaryMaterialColumns as ColumnsType<PrimaryMaterialTableItem>" </a-button>
:pagination="false" bordered sticky :scroll="{ y: tableHeight }" <div class="table-wrapper" ref="customTable">
:loading="loadingPrimaryMaterialList"> <a-table
<template #bodyCell="{ column, index, record }"> :dataSource="primaryMaterialTableData"
<template v-if="column.key === 'index'">{{ index + 1 }}</template> :columns="
<template v-if="column.key === 'outputResult'"> primaryMaterialColumns as ColumnsType<PrimaryMaterialTableItem>
<a-switch v-model:checked="record.outputResult" checked-children="NG" checkedValue="NG" un-checked-children="OK" unCheckedValue="OK" /> "
</template> :pagination="false"
<template v-if="column.key === 'qualityLevel'"> bordered
<a-select v-model:value="record.qualityLevel" style="width: 80%" v-if="record.outputResult === 'NG'"> sticky
<a-select-option v-for="dict in main_material_ng_level" :value="dict.value">{{ dict.label }}</a-select-option> :scroll="{ y: tableHeight }"
</a-select> :loading="loadingPrimaryMaterialList"
<a-select v-model:value="record.qualityLevel" style="width: 80%" v-if="record.outputResult === 'OK'"> >
<a-select-option v-for="dict in main_material_ok_level" :value="dict.value">{{ dict.label }}</a-select-option> <template #bodyCell="{ column, index, record }">
</a-select> <template v-if="column.key === 'index'">{{ index + 1 }}</template>
</template> <template v-if="column.key === 'outputResult'">
<template v-if="column.key === 'action'"> <a-switch
<a-space> v-model:checked="record.outputResult"
<a-button @click="handleEditRecord(record)">修改</a-button> checked-children="NG"
<a-popconfirm title="确定删除该记录?" ok-text="" cancel-text="" @confirm="handleDeleteRecord(record)"> checkedValue="NG"
<a-button danger>删除</a-button> un-checked-children="OK"
</a-popconfirm> unCheckedValue="OK"
</a-space> />
</template> </template>
</template> <template v-if="column.key === 'qualityLevel'">
<template #summary> <a-select
<a-table-summary-row> v-model:value="record.qualityLevel"
<a-table-summary-cell>总计</a-table-summary-cell> style="width: 80%"
<a-table-summary-cell> v-if="record.outputResult === 'NG'"
<a-typography-text>OK: {{ totals.okNum }}</a-typography-text> >
</a-table-summary-cell> <a-select-option
<a-table-summary-cell> v-for="dict in main_material_ng_level"
<a-typography-text>NG: {{ totals.ngNum }}</a-typography-text> :key="dict.value"
</a-table-summary-cell> :value="dict.value"
</a-table-summary-row> >
</template> {{ dict.label }}
</a-table> </a-select-option>
</div> </a-select>
<a-select
v-model:value="record.qualityLevel"
style="width: 80%"
v-if="record.outputResult === 'OK'"
>
<a-select-option
v-for="dict in main_material_ok_level"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</a-select-option>
</a-select>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a-button @click="handleEditRecord(record)">修改</a-button>
<a-popconfirm
title="确定删除该记录?"
ok-text=""
cancel-text=""
@confirm="handleDeleteRecord(record)"
>
<a-button danger>删除</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
<template #summary>
<a-table-summary-row>
<a-table-summary-cell>总计</a-table-summary-cell>
<a-table-summary-cell>
<a-typography-text>OK: {{ totals.okNum }}</a-typography-text>
</a-table-summary-cell>
<a-table-summary-cell>
<a-typography-text>NG: {{ totals.ngNum }}</a-typography-text>
</a-table-summary-cell>
</a-table-summary-row>
</template>
</a-table>
</div>
<a-modal v-model:open="openEditRecordModal" title="修改主材出站记录" @ok="handleSubmitEdit"> <a-modal
<a-form :model="editingRecord" :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }"> v-model:open="openEditRecordModal"
<a-form-item label="Wafer ID"> title="修改主材出站记录"
<a-input v-model:value="editingRecord.waferCode" disabled /> @ok="handleSubmitEdit"
</a-form-item> >
<a-form-item label="Die ID"> <a-form
<a-input v-model:value="editingRecord.dieCode" disabled /> :model="editingRecord"
</a-form-item> :label-col="{ span: 4 }"
<a-form-item label="Die X 坐标"> :wrapper-col="{ span: 20 }"
<a-input v-model:value="editingRecord.xcoordinates" disabled /> >
</a-form-item> <a-form-item label="Wafer ID">
<a-form-item label="Die Y 坐标"> <a-input v-model:value="editingRecord.waferCode" disabled />
<a-input v-model:value="editingRecord.ycoordinates" disabled /> </a-form-item>
</a-form-item> <a-form-item label="Die ID">
<a-form-item label="结果" :rules="[{ required: true, message: '请选择结果!' }]"> <a-input v-model:value="editingRecord.dieCode" disabled />
<a-switch v-model:checked="editingRecord.outputResult" checked-children="NG" checkedValue="NG" </a-form-item>
un-checked-children="OK" unCheckedValue="OK" /> <a-form-item label="Die X 坐标">
</a-form-item> <a-input v-model:value="editingRecord.xcoordinates" disabled />
<a-form-item label="质量等级" :rules="[{ required: true, message: '请选择质量等级!' }]"> </a-form-item>
<a-select v-model:value="editingRecord.qualityLevel" style="width: 80%" v-if="editingRecord.outputResult === 'NG'"> <a-form-item label="Die Y 坐标">
<a-select-option v-for="dict in main_material_ng_level" :value="dict.value">{{ dict.label <a-input v-model:value="editingRecord.ycoordinates" disabled />
}}</a-select-option> </a-form-item>
</a-select> <a-form-item
<a-select v-model:value="editingRecord.qualityLevel" style="width: 80%" v-if="editingRecord.outputResult === 'OK'"> label="结果"
<a-select-option v-for="dict in main_material_ok_level" :value="dict.value">{{ dict.label :rules="[{ required: true, message: '请选择结果!' }]"
}}</a-select-option> >
</a-select> <a-switch
</a-form-item> v-model:checked="editingRecord.outputResult"
</a-form> checked-children="NG"
</a-modal> checkedValue="NG"
un-checked-children="OK"
unCheckedValue="OK"
/>
</a-form-item>
<a-form-item
label="质量等级"
:rules="[{ required: true, message: '请选择质量等级!' }]"
>
<a-select
v-model:value="editingRecord.qualityLevel"
style="width: 80%"
v-if="editingRecord.outputResult === 'NG'"
>
<a-select-option
v-for="dict in main_material_ng_level"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</a-select-option>
</a-select>
<a-select
v-model:value="editingRecord.qualityLevel"
style="width: 80%"
v-if="editingRecord.outputResult === 'OK'"
>
<a-select-option
v-for="dict in main_material_ok_level"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</a-select-option>
</a-select>
</a-form-item>
</a-form>
</a-modal>
<a-modal v-model:open="openPickPrimaryMaterial" title="选择主材出站" width="50vw" @ok="submitOutfeed" @cancel="handleCancel"> <a-modal
已选择 {{ rowSelection.length }} 条主材 v-model:open="openPickPrimaryMaterial"
<a-table :dataSource="infeedMaterialTableData" :row-selection="{ selectedRowKeys: selectedKeys, onChange: handleChangeSelection }" row-key="id" title="选择主材出站"
:columns="infeedMaterialColumns as ColumnsType<InfeedMaterialTableItem>" :pagination="false" bordered sticky> width="50vw"
<template #bodyCell="{ column, index, record }"> @ok="submitOutfeed"
<template v-if="column.key === 'index'">{{ index + 1 }}</template> @cancel="handleCancel"
<template v-if="column.key === 'result'"> >
<a-switch v-model:checked="record.result" checked-children="NG" checkedValue="NG" 已选择 {{ rowSelection.length }} 条主材
un-checked-children="OK" unCheckedValue="OK" /> <a-table
</template> :dataSource="infeedMaterialTableData"
<template v-if="column.key === 'qualityLevel'"> :row-selection="{
<a-select v-model:value="record.qualityLevel" style="width: 80%" v-if="record.result === 'NG'"> selectedRowKeys: selectedKeys,
<a-select-option v-for="dict in main_material_ng_level" :value="dict.value">{{ dict.label onChange: handleChangeSelection,
}}</a-select-option> }"
</a-select> row-key="id"
<a-select v-model:value="record.qualityLevel" style="width: 80%" v-if="record.result === 'OK'"> :columns="infeedMaterialColumns as ColumnsType<InfeedMaterialTableItem>"
<a-select-option v-for="dict in main_material_ok_level" :value="dict.value">{{ dict.label :pagination="false"
}}</a-select-option> bordered
</a-select> sticky
</template> >
</template> <template #bodyCell="{ column, index, record }">
</a-table> <template v-if="column.key === 'index'">{{ index + 1 }}</template>
</a-modal> <template v-if="column.key === 'result'">
</div> <a-switch
v-model:checked="record.result"
checked-children="NG"
checkedValue="NG"
un-checked-children="OK"
unCheckedValue="OK"
/>
</template>
<template v-if="column.key === 'qualityLevel'">
<a-select
v-model:value="record.qualityLevel"
style="width: 80%"
v-if="record.result === 'NG'"
>
<a-select-option
v-for="dict in main_material_ng_level"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</a-select-option>
</a-select>
<a-select
v-model:value="record.qualityLevel"
style="width: 80%"
v-if="record.result === 'OK'"
>
<a-select-option
v-for="dict in main_material_ok_level"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</a-select-option>
</a-select>
</template>
</template>
</a-table>
</a-modal>
</div>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.equipment__container { .equipment__container {
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: 16px;
} }
.main-content { .main-content {
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
} }
.input__container { .input__container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: 16px;
overflow: hidden; overflow: hidden;
height: 100%; height: 100%;
.description-wrapper { .description-wrapper {
flex: 1; flex: 1;
overflow: auto; overflow: auto;
} }
} }
.table-wrapper { .table-wrapper {
height: 100%; height: 100%;
overflow: auto; overflow: auto;
} }
::v-deep(.ant-switch) { ::v-deep(.ant-switch) {
background-color: #04d903; background-color: #04d903;
&:hover { &:hover {
background-color: #02eb02; background-color: #02eb02;
} }
&.ant-switch-checked { &.ant-switch-checked {
background-color: #ff0000; background-color: #ff0000;
&:hover { &:hover {
background-color: #ff2525; background-color: #ff2525;
} }
} }
} }
</style> </style>

View File

@@ -1,196 +1,223 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, reactive } from 'vue'; import { ref, computed, reactive } from 'vue'
import { useRouter, useRoute } from 'vue-router'; import { useRouter, useRoute } from 'vue-router'
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue'
import { listStorageLocation } from '@/api/traceOrderManage/location'; import { listStorageLocation } from '@/api/traceOrderManage/location'
import { completeStation, isLastStation as getIsLastStation } from '@/api/traceOrderManage/station'; import {
import { useTraceOrderStore } from '@/store'; completeStation,
isLastStation as getIsLastStation,
} from '@/api/traceOrderManage/station'
import { useTraceOrderStore } from '@/store'
const router = useRouter(); const router = useRouter()
const route = useRoute(); const route = useRoute()
const traceOrderStore = useTraceOrderStore(); const traceOrderStore = useTraceOrderStore()
const menuItems = [ const menuItems = [
{ label: '主材报工', key: 'JobReport', progress: 30 }, { label: '主材报工', key: 'JobReport', progress: 30 },
{ label: '工序参数', key: 'ParameterConfiguration', progress: 80 }, { label: '工序参数', key: 'ParameterConfiguration', progress: 80 },
{ label: '流转指引', key: 'ProcessGuidance', progress: 80 }, { label: '流转指引', key: 'ProcessGuidance', progress: 80 },
]; ]
const activeKey = computed(() => route.name as string); const activeKey = computed(() => route.name as string)
const handleMenuClick = (name: string) => { const handleMenuClick = (name: string) => {
router.push({ name }); router.push({ name })
}; }
const locationOptions = ref<any>([]); const locationOptions = ref<any>([])
const fetchLocationList = async () => { const fetchLocationList = async () => {
try { try {
const { rows } = await listStorageLocation({}); const { rows } = await listStorageLocation({})
locationOptions.value = rows; locationOptions.value = rows
} catch (error: any) { } catch (error: any) {
message.error(error.message || '获取库位列表失败'); message.error(error.message || '获取库位列表失败')
} }
}; }
const openSelectLocation = ref(false); const openSelectLocation = ref(false)
const handleOutfeed = async () => { const handleOutfeed = async () => {
// 判断是否为末站点 // 判断是否为末站点
const { data: isLastStation } = await getIsLastStation(traceOrderStore.currentStationId); const { data: isLastStation } = await getIsLastStation(
if (isLastStation || outfeedChildRef.value?.totals.ngNum) { traceOrderStore.currentStationId,
fetchLocationList(); )
openSelectLocation.value = true; if (isLastStation || outfeedChildRef.value?.totals.ngNum) {
} else { fetchLocationList()
submitOutfeed(); openSelectLocation.value = true
} } else {
}; submitOutfeed()
}
}
const storage = reactive({}); const storage = reactive({})
const handleChangeLocation = (value: any, option: any) => { const handleChangeLocation = (value: any, option: any) => {
Object.assign(storage, option); Object.assign(storage, option)
}; }
const closeOutfeed = () => { const closeOutfeed = () => {
openSelectLocation.value = false; openSelectLocation.value = false
}; }
// 确认出站 // 确认出站
const outfeeding = ref(false); const outfeeding = ref(false)
const submitOutfeed = async () => { const submitOutfeed = async () => {
outfeeding.value = true; outfeeding.value = true
try { try {
await completeStation(traceOrderStore.currentStationId, storage); await completeStation(traceOrderStore.currentStationId, storage)
openSelectLocation.value = false; openSelectLocation.value = false
message.success('出站成功'); message.success('出站成功')
traceOrderStore.fetchStationInfo(); traceOrderStore.fetchStationInfo()
traceOrderStore.fetchStationList(); traceOrderStore.fetchStationList()
router.push({ name: 'TraceOrderManage' }); router.push({ name: 'TraceOrderManage' })
} catch (error: any) { } catch (error: any) {
message.error(error.message || '出站失败'); message.error(error.message || '出站失败')
} finally { } finally {
outfeeding.value = false; outfeeding.value = false
} }
}; }
const outfeedChildRef = ref<any>(null); const outfeedChildRef = ref<any>(null)
/** 父组件对外暴露的方法 */ /** 父组件对外暴露的方法 */
function renderTableHeight() { function renderTableHeight() {
outfeedChildRef.value?.renderTableHeight(); outfeedChildRef.value?.renderTableHeight()
} }
defineExpose({ defineExpose({
renderTableHeight renderTableHeight,
}); })
fetchLocationList(); fetchLocationList()
</script> </script>
<template> <template>
<a-spin :spinning="outfeeding"> <a-spin :spinning="outfeeding">
<a-row class="outfeed-layout"> <a-row class="outfeed-layout">
<a-col :span="20" class="content-wrapper"> <a-col :span="20" class="content-wrapper">
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">
<component :is="Component" ref="outfeedChildRef" /> <component :is="Component" ref="outfeedChildRef" />
</router-view> </router-view>
</a-col> </a-col>
<a-col :span="4" class="menu-wrapper"> <a-col :span="4" class="menu-wrapper">
<div class="menu-list"> <div class="menu-list">
<span class="outfeed-title">出站</span> <span class="outfeed-title">出站</span>
<div v-for="item in menuItems" :key="item.key" class="menu-item" :class="{ active: activeKey === item.key }" <div
@click="handleMenuClick(item.key)"> v-for="item in menuItems"
<div class="menu-content"> :key="item.key"
<span class="menu-title">{{ item.label }}</span> class="menu-item"
<a-progress :percent="item.progress" :show-info="false" size="small" :class="{ active: activeKey === item.key }"
:stroke-color="activeKey === item.key ? '#1890ff' : undefined" class="menu-progress" /> @click="handleMenuClick(item.key)"
</div> >
</div> <div class="menu-content">
<a-button type="primary" size="large" @click="handleOutfeed">确认出站</a-button> <span class="menu-title">{{ item.label }}</span>
</div> <a-progress
</a-col> :percent="item.progress"
</a-row> :show-info="false"
size="small"
:stroke-color="activeKey === item.key ? '#1890ff' : undefined"
class="menu-progress"
/>
</div>
</div>
<a-button type="primary" size="large" @click="handleOutfeed">
确认出站
</a-button>
</div>
</a-col>
</a-row>
<a-modal title="选择出站库位" :open="openSelectLocation" @ok="submitOutfeed" @cancel="closeOutfeed"> <a-modal
<a-select style="width: 100%" @change="handleChangeLocation" :options="locationOptions" :fieldNames="{ label: 'storageLocationCode', value: 'id' }" /> title="选择出站库位"
</a-modal> :open="openSelectLocation"
</a-spin> @ok="submitOutfeed"
@cancel="closeOutfeed"
>
<a-select
style="width: 100%"
@change="handleChangeLocation"
:options="locationOptions"
:fieldNames="{ label: 'storageLocationCode', value: 'id' }"
/>
</a-modal>
</a-spin>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.outfeed-layout { .outfeed-layout {
height: 100%; height: 100%;
} }
.menu-list { .menu-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: 16px;
height: 100%; height: 100%;
} }
.outfeed-title { .outfeed-title {
font-size: 20px; font-size: 20px;
font-weight: 600; font-weight: 600;
border-bottom: 1px solid #c9c9c9; border-bottom: 1px solid #c9c9c9;
padding: 0 0 8px; padding: 0 0 8px;
} }
.menu-item { .menu-item {
flex: 1; flex: 1;
background: #fff; background: #fff;
border-radius: 8px; border-radius: 8px;
padding: 1vh 1vw; padding: 1vh 1vw;
cursor: pointer; cursor: pointer;
transition: all 0.3s; transition: all 0.3s;
border: 1px solid #f0f0f0; border: 1px solid #f0f0f0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.02); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.02);
position: relative; position: relative;
overflow: hidden; overflow: hidden;
&.active { &.active {
background: #e6f7ff; background: #e6f7ff;
border-color: #1890ff; border-color: #1890ff;
.menu-title { .menu-title {
color: #1890ff; color: #1890ff;
} }
} }
&:hover { &:hover {
border-color: #47a5fd; border-color: #47a5fd;
} }
} }
.menu-content { .menu-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-around; justify-content: space-around;
height: 100%; height: 100%;
} }
.menu-title { .menu-title {
font-size: 18px; font-size: 18px;
font-weight: bold; font-weight: bold;
color: #333; color: #333;
line-height: 1.5; line-height: 1.5;
} }
.menu-progress { .menu-progress {
margin-bottom: 0 !important; margin-bottom: 0 !important;
:deep(.ant-progress-bg) { :deep(.ant-progress-bg) {
transition: all 0.3s; transition: all 0.3s;
} }
} }
.menu-wrapper { .menu-wrapper {
padding: 12px; padding: 12px;
border-radius: 7px; border-radius: 7px;
background-color: #f3f3f3; background-color: #f3f3f3;
} }
.content-wrapper { .content-wrapper {
height: 100%; height: 100%;
padding: 12px; padding: 12px;
} }
</style> </style>

View File

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

View File

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

2
src/vite-env.d.ts vendored
View File

@@ -1 +1 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />

View File

@@ -1,56 +1,55 @@
import { defineConfig, loadEnv } from 'vite'; import { defineConfig, loadEnv } from 'vite'
import Components from "unplugin-vue-components/vite"; // 按需组件自动导入 import Components from 'unplugin-vue-components/vite' // 按需组件自动导入
import { AntDesignVueResolver } from "unplugin-vue-components/resolvers"; import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'
import vue from '@vitejs/plugin-vue'; import vue from '@vitejs/plugin-vue'
import Icons from 'unplugin-icons/vite'; import Icons from 'unplugin-icons/vite'
import IconsResolver from 'unplugin-icons/resolver'; import IconsResolver from 'unplugin-icons/resolver'
import path from 'path'; import path from 'path'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig(({ mode }) => { export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd())
const env = loadEnv(mode, process.cwd()); return {
resolve: {
return { alias: {
resolve: { '@': path.resolve(__dirname, 'src'),
alias: { },
"@": path.resolve(__dirname, "src"), },
}, server: {
}, proxy: {
server: { '/api': {
proxy: { target: env.VITE_APP_BASE_URL,
"/api": { changeOrigin: true,
target: env.VITE_APP_BASE_URL, rewrite: (path) => path.replace(/^\/api\//, '/'),
changeOrigin: true, },
rewrite: (path) => path.replace(/^\/api\//, "/"), '/prod-api': {
}, target: env.VITE_APP_BASE_URL,
"/prod-api": { changeOrigin: true,
target: env.VITE_APP_BASE_URL, rewrite: (path) => path.replace(/^\/prod-api\//, '/'),
changeOrigin: true, },
rewrite: (path) => path.replace(/^\/prod-api\//, "/"), },
}, },
}, plugins: [
}, vue(),
plugins: [ Components({
vue(), dts: true, //生成components.d.ts 全局定义文件
Components({ resolvers: [
dts: true, //生成components.d.ts 全局定义文件 AntDesignVueResolver({
resolvers: [ //对使用到的全局ant design vue组件进行类型导入
AntDesignVueResolver({ importStyle: false, // 不动态引入css,这个不强求
//对使用到的全局ant design vue组件进行类型导入 }),
importStyle: false, // 不动态引入css,这个不强求 // 自动导入 lucide 图标
}), IconsResolver({
// 自动导入 lucide 图标 prefix: 'i', // 比如用 <i-a-arrow-down />
IconsResolver({ enabledCollections: ['lucide'],
prefix: "i", // 比如用 <i-a-arrow-down /> }),
enabledCollections: ["lucide"], ],
}), include: [/\.vue$/, /\.vue\?vue/, /\.md$/, /\.tsx$/], //包含的文件类型
], }),
include: [/\.vue$/, /\.vue\?vue/, /\.md$/, /\.tsx$/], //包含的文件类型 Icons({
}), autoInstall: true, // 没安装的图标库会自动下载
Icons({ }),
autoInstall: true, // 没安装的图标库会自动下载 ],
}), }
], })
};
});