Compare commits

14 Commits

Author SHA1 Message Date
zc
630a98af65 优化 2026-03-06 18:25:24 +08:00
zc
31178da436 Merge remote-tracking branch 'refs/remotes/origin/master' into dev
# Conflicts:
#	src/views/weightManage/index.vue
2026-03-06 18:15:58 +08:00
zc
323a641fa3 Merge branch 'refs/heads/master' into dev
# Conflicts:
#	src/views/system/user/index.vue
2026-03-06 18:06:34 +08:00
zc
c858ace541 优化 2026-03-06 18:04:40 +08:00
f35d64bd91 称重抓取 2026-03-06 18:03:28 +08:00
zc
006130b4b7 优化 2026-03-06 16:29:31 +08:00
zc
b4ebebe2a8 优化 2026-03-06 15:54:36 +08:00
zc
60933ee1a6 优化 2026-03-06 14:17:19 +08:00
zc
d014699b95 优化 2026-03-05 18:14:25 +08:00
15ce1cfa13 Merge branch 'refs/heads/dev' into master_lz
# Conflicts:
#	src/views/system/user/index.vue
2026-03-05 16:35:28 +08:00
zc
0c40b20df7 优化追溯 2026-03-04 18:04:32 +08:00
zc
1e64e776ce 优化追溯 2026-03-04 16:46:52 +08:00
zc
31b7d3237a Merge branch 'refs/heads/master' into dev 2026-03-04 10:34:25 +08:00
zc
85dd8102a3 优化称重页面 2026-03-03 17:59:37 +08:00
16 changed files with 1683 additions and 217 deletions

24
package-lock.json generated
View File

@@ -28,6 +28,7 @@
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dayjs": "^1.11.4", "dayjs": "^1.11.4",
"echarts": "^5.4.2", "echarts": "^5.4.2",
"flv.js": "^1.6.2",
"jsencrypt": "^3.3.2", "jsencrypt": "^3.3.2",
"lint-staged": "^15.2.10", "lint-staged": "^15.2.10",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
@@ -765,7 +766,6 @@
}, },
"node_modules/@clack/prompts/node_modules/is-unicode-supported": { "node_modules/@clack/prompts/node_modules/is-unicode-supported": {
"version": "1.3.0", "version": "1.3.0",
"extraneous": true,
"inBundle": true, "inBundle": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -6560,6 +6560,12 @@
"es6-symbol": "^3.1.1" "es6-symbol": "^3.1.1"
} }
}, },
"node_modules/es6-promise": {
"version": "4.2.8",
"resolved": "https://registry.npmmirror.com/es6-promise/-/es6-promise-4.2.8.tgz",
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==",
"license": "MIT"
},
"node_modules/es6-symbol": { "node_modules/es6-symbol": {
"version": "3.1.4", "version": "3.1.4",
"resolved": "https://registry.npmmirror.com/es6-symbol/-/es6-symbol-3.1.4.tgz", "resolved": "https://registry.npmmirror.com/es6-symbol/-/es6-symbol-3.1.4.tgz",
@@ -8254,6 +8260,16 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/flv.js": {
"version": "1.6.2",
"resolved": "https://registry.npmmirror.com/flv.js/-/flv.js-1.6.2.tgz",
"integrity": "sha512-xre4gUbX1MPtgQRKj2pxJENp/RnaHaxYvy3YToVVCrSmAWUu85b9mug6pTXF6zakUjNP2lFWZ1rkSX7gxhB/2A==",
"license": "Apache-2.0",
"dependencies": {
"es6-promise": "^4.2.8",
"webworkify-webpack": "^2.1.5"
}
},
"node_modules/follow-redirects": { "node_modules/follow-redirects": {
"version": "1.15.9", "version": "1.15.9",
"resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz", "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz",
@@ -16043,6 +16059,12 @@
"node": ">=4.0" "node": ">=4.0"
} }
}, },
"node_modules/webworkify-webpack": {
"version": "2.1.5",
"resolved": "https://registry.npmmirror.com/webworkify-webpack/-/webworkify-webpack-2.1.5.tgz",
"integrity": "sha512-2akF8FIyUvbiBBdD+RoHpoTbHMQF2HwjcxfDvgztAX5YwbZNyrtfUMgvfgFVsgDhDPVTlkbb5vyasqDHfIDPQw==",
"license": "MIT"
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",

View File

@@ -34,6 +34,7 @@
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dayjs": "^1.11.4", "dayjs": "^1.11.4",
"echarts": "^5.4.2", "echarts": "^5.4.2",
"flv.js": "^1.6.2",
"jsencrypt": "^3.3.2", "jsencrypt": "^3.3.2",
"lint-staged": "^15.2.10", "lint-staged": "^15.2.10",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",

View File

@@ -25,6 +25,11 @@ export function socialLogin(req: any) {
return http.post<T.LoginResp>(`${BASE_URL}/login`, req) return http.post<T.LoginResp>(`${BASE_URL}/login`, req)
} }
/** @desc 三方账号登录 */
export function cardLogin(req: any) {
return http.post<T.LoginResp>(`${BASE_URL}/login`, req)
}
/** @desc 三方账号登录授权 */ /** @desc 三方账号登录授权 */
export function socialAuth(source: string) { export function socialAuth(source: string) {
return http.get<T.SocialAuthAuthorizeResp>(`${BASE_URL}/${source}`) return http.get<T.SocialAuthAuthorizeResp>(`${BASE_URL}/${source}`)

View File

@@ -42,13 +42,14 @@ export interface RouteItem {
} }
/** 认证类型 */ /** 认证类型 */
export type AuthType = 'ACCOUNT' | 'PHONE' | 'EMAIL' | 'SOCIAL' export type AuthType = 'ACCOUNT' | 'PHONE' | 'EMAIL' | 'SOCIAL' | 'CARD'
export const AuthTypeConstants = { export const AuthTypeConstants = {
ACCOUNT: 'ACCOUNT', ACCOUNT: 'ACCOUNT',
PHONE: 'PHONE', PHONE: 'PHONE',
EMAIL: 'EMAIL', EMAIL: 'EMAIL',
SOCIAL: 'SOCIAL', SOCIAL: 'SOCIAL',
CARD: 'CARD',
} as const } as const
/** 基础认证请求接口 */ /** 基础认证请求接口 */
@@ -77,6 +78,16 @@ export interface EmailLoginReq extends AuthReq {
captcha: string captcha: string
} }
/** 刷卡登录请求参数 */
export interface CardLoginReq extends AuthReq {
cardNumber: string
}
/** 邮箱登录请求参数 */
export interface CardLoginReq extends AuthReq {
card: string
}
/** 登录响应类型 */ /** 登录响应类型 */
export interface LoginResp { export interface LoginResp {
token: string token: string

View File

@@ -78,7 +78,12 @@ export function importMaterial(data: any) {
return http.post(`${BASE_URL}/import`, data) return http.post(`${BASE_URL}/import`, data)
} }
/** @desc 解析物料信息导入数据 */ /** @desc 物料照片批量导入数据 */
export function uploadMaterialPhotos(data: FormData) { export function uploadMaterialPhotos(data: FormData) {
return http.post(`${BASE_URL}/import/uploadMaterialPhotos`, data) return http.post(`${BASE_URL}/import/uploadMaterialPhotos`, data)
} }
/** @desc 物料照片抓取数据 */
export function catchPhoto(data: FormData) {
return http.post(`${BASE_URL}/import/catch`, data)
}

View File

@@ -4,21 +4,21 @@ const BASE_URL = '/weighManage/material'
export interface WeighManageResp { export interface WeighManageResp {
id: string id: string
materialCode: string encoding: string
materialName: string materialName: string
materialSpec: string materialSpec: string
weight: number unitWeight: number
imageUrl: string photoUrl: string
matchResult: string matchResult: string
} }
export interface WeighManageQuery { export interface WeighManageQuery {
materialCode: string encoding: string
} }
/** @desc 查询物料信息 */ /** @desc 查询物料信息 */
export function getMaterialDetail(query: WeighManageQuery) { export function getMaterialDetail(code: string) {
return http.get<WeighManageResp>(`${BASE_URL}/detail`, query) return http.get<WeighManageResp>(`/admin/materialInfo/code/${code}`)
} }
/** @desc 新增人员管理 */ /** @desc 新增人员管理 */

View File

@@ -0,0 +1,67 @@
import http from '@/utils/http'
const BASE_URL = '/weighManage/workOrder'
export interface WorkOrderResp {
id: string
title: string
orderNo: string
materialName: string
encoding: string
unitWeight: string
materialSpec: string
photoUrl: string
totalWeight: string
totalCount: string
createUserString: string
updateUserString: string
matchResult: string
}
export interface WorkOrderInfoResp {
id: string
workOrderId: string
materialId: string
weightTime: string
quantity: string
weight: string
imgUrl: string
calculatedWeight: string
}
export interface WorkOrderQuery {
orderNo: string | undefined
materialName: string | undefined
encoding: string | undefined
userName: string | undefined
carNo: string | undefined
startDate: string | undefined
endDate: string | undefined
sort: Array<string>
}
export interface WorkOrderPageQuery extends WorkOrderQuery, PageQuery {}
/** @desc 查询工作订单列表 */
export function listWorkOrder(query: WorkOrderPageQuery) {
return http.get<PageRes<WorkOrderResp[]>>(`${BASE_URL}`, query)
}
/** @desc 查询工作订单详情 */
export function getWorkOrder(id: string) {
return http.get<Array<WorkOrderInfoResp>>(`${BASE_URL}/${id}`)
}
/** @desc 新增工作订单 */
export function addWorkOrder(data: any) {
return http.post(`${BASE_URL}`, data)
}
/** @desc 删除工作订单 */
export function deleteWorkOrder(ids: string | Array<string>) {
return http.del(`${BASE_URL}/${ids}`)
}
/** @desc 导出工作订单 */
export function exportWorkOrder(query: WorkOrderQuery) {
return http.download(`${BASE_URL}/export`, query)
}

View File

@@ -4,10 +4,12 @@ import { resetRouter } from '@/router'
import { import {
type AccountLoginReq, type AccountLoginReq,
AuthTypeConstants, AuthTypeConstants,
type CardLoginReq,
type EmailLoginReq, type EmailLoginReq,
type PhoneLoginReq, type PhoneLoginReq,
type UserInfo, type UserInfo,
accountLogin as accountLoginApi, accountLogin as accountLoginApi,
cardLogin as cardLoginApi,
emailLogin as emailLoginApi, emailLogin as emailLoginApi,
getUserInfo as getUserInfoApi, getUserInfo as getUserInfoApi,
logout as logoutApi, logout as logoutApi,
@@ -70,6 +72,13 @@ const storeSetup = () => {
token.value = res.data.token token.value = res.data.token
} }
// 刷卡登录
const cardLogin = async (req: CardLoginReq) => {
const res = await cardLoginApi({ ...req, clientId: import.meta.env.VITE_CLIENT_ID, authType: AuthTypeConstants.CARD })
setToken(res.data.token)
token.value = res.data.token
}
// 三方账号登录 // 三方账号登录
const socialLogin = async (source: string, req: any) => { const socialLogin = async (source: string, req: any) => {
const res = await socialLoginApi({ ...req, source, clientId: import.meta.env.VITE_CLIENT_ID, authType: AuthTypeConstants.SOCIAL }) const res = await socialLoginApi({ ...req, source, clientId: import.meta.env.VITE_CLIENT_ID, authType: AuthTypeConstants.SOCIAL })
@@ -120,6 +129,7 @@ const storeSetup = () => {
accountLogin, accountLogin,
emailLogin, emailLogin,
phoneLogin, phoneLogin,
cardLogin,
socialLogin, socialLogin,
logout, logout,
logoutCallBack, logoutCallBack,

View File

@@ -13,11 +13,10 @@ declare module 'vue' {
ABreadcrumb: typeof import('@arco-design/web-vue')['Breadcrumb'] ABreadcrumb: typeof import('@arco-design/web-vue')['Breadcrumb']
ABreadcrumbItem: typeof import('@arco-design/web-vue')['BreadcrumbItem'] ABreadcrumbItem: typeof import('@arco-design/web-vue')['BreadcrumbItem']
AButton: typeof import('@arco-design/web-vue')['Button'] AButton: typeof import('@arco-design/web-vue')['Button']
ACard: typeof import('@arco-design/web-vue')['Card']
ACheckbox: typeof import('@arco-design/web-vue')['Checkbox'] ACheckbox: typeof import('@arco-design/web-vue')['Checkbox']
ACheckboxGroup: typeof import('@arco-design/web-vue')['CheckboxGroup']
ACol: typeof import('@arco-design/web-vue')['Col'] ACol: typeof import('@arco-design/web-vue')['Col']
AConfigProvider: typeof import('@arco-design/web-vue')['ConfigProvider'] AConfigProvider: typeof import('@arco-design/web-vue')['ConfigProvider']
ADatePicker: typeof import('@arco-design/web-vue')['DatePicker']
ADescriptions: typeof import('@arco-design/web-vue')['Descriptions'] ADescriptions: typeof import('@arco-design/web-vue')['Descriptions']
ADescriptionsItem: typeof import('@arco-design/web-vue')['DescriptionsItem'] ADescriptionsItem: typeof import('@arco-design/web-vue')['DescriptionsItem']
ADivider: typeof import('@arco-design/web-vue')['Divider'] ADivider: typeof import('@arco-design/web-vue')['Divider']
@@ -27,8 +26,6 @@ declare module 'vue' {
AEmpty: typeof import('@arco-design/web-vue')['Empty'] AEmpty: typeof import('@arco-design/web-vue')['Empty']
AForm: typeof import('@arco-design/web-vue')['Form'] AForm: typeof import('@arco-design/web-vue')['Form']
AFormItem: typeof import('@arco-design/web-vue')['FormItem'] AFormItem: typeof import('@arco-design/web-vue')['FormItem']
AGrid: typeof import('@arco-design/web-vue')['Grid']
AGridItem: typeof import('@arco-design/web-vue')['GridItem']
AIcon: typeof import('@arco-design/web-vue')['Icon'] AIcon: typeof import('@arco-design/web-vue')['Icon']
AImage: typeof import('@arco-design/web-vue')['Image'] AImage: typeof import('@arco-design/web-vue')['Image']
AInput: typeof import('@arco-design/web-vue')['Input'] AInput: typeof import('@arco-design/web-vue')['Input']
@@ -36,41 +33,29 @@ declare module 'vue' {
AInputPassword: typeof import('@arco-design/web-vue')['InputPassword'] AInputPassword: typeof import('@arco-design/web-vue')['InputPassword']
AInputSearch: typeof import('@arco-design/web-vue')['InputSearch'] AInputSearch: typeof import('@arco-design/web-vue')['InputSearch']
ALayout: typeof import('@arco-design/web-vue')['Layout'] ALayout: typeof import('@arco-design/web-vue')['Layout']
ALayoutContent: typeof import('@arco-design/web-vue')['LayoutContent']
ALayoutHeader: typeof import('@arco-design/web-vue')['LayoutHeader'] ALayoutHeader: typeof import('@arco-design/web-vue')['LayoutHeader']
ALayoutSider: typeof import('@arco-design/web-vue')['LayoutSider'] ALayoutSider: typeof import('@arco-design/web-vue')['LayoutSider']
ALink: typeof import('@arco-design/web-vue')['Link'] ALink: typeof import('@arco-design/web-vue')['Link']
AMenu: typeof import('@arco-design/web-vue')['Menu'] AMenu: typeof import('@arco-design/web-vue')['Menu']
AMenuItem: typeof import('@arco-design/web-vue')['MenuItem'] AMenuItem: typeof import('@arco-design/web-vue')['MenuItem']
AModal: typeof import('@arco-design/web-vue')['Modal'] AModal: typeof import('@arco-design/web-vue')['Modal']
AOption: typeof import('@arco-design/web-vue')['Option']
AOverflowList: typeof import('@arco-design/web-vue')['OverflowList']
APagination: typeof import('@arco-design/web-vue')['Pagination'] APagination: typeof import('@arco-design/web-vue')['Pagination']
APopconfirm: typeof import('@arco-design/web-vue')['Popconfirm']
APopover: typeof import('@arco-design/web-vue')['Popover'] APopover: typeof import('@arco-design/web-vue')['Popover']
AProgress: typeof import('@arco-design/web-vue')['Progress']
ARadio: typeof import('@arco-design/web-vue')['Radio'] ARadio: typeof import('@arco-design/web-vue')['Radio']
ARadioGroup: typeof import('@arco-design/web-vue')['RadioGroup'] ARadioGroup: typeof import('@arco-design/web-vue')['RadioGroup']
ARangePicker: typeof import('@arco-design/web-vue')['RangePicker']
ARow: typeof import('@arco-design/web-vue')['Row'] ARow: typeof import('@arco-design/web-vue')['Row']
AScrollbar: typeof import('@arco-design/web-vue')['Scrollbar'] AScrollbar: typeof import('@arco-design/web-vue')['Scrollbar']
ASelect: typeof import('@arco-design/web-vue')['Select'] ASelect: typeof import('@arco-design/web-vue')['Select']
ASpace: typeof import('@arco-design/web-vue')['Space'] ASpace: typeof import('@arco-design/web-vue')['Space']
AStatistic: typeof import('@arco-design/web-vue')['Statistic'] ASpin: typeof import('@arco-design/web-vue')['Spin']
ASubMenu: typeof import('@arco-design/web-vue')['SubMenu'] ASubMenu: typeof import('@arco-design/web-vue')['SubMenu']
ASwitch: typeof import('@arco-design/web-vue')['Switch'] ASwitch: typeof import('@arco-design/web-vue')['Switch']
ATable: typeof import('@arco-design/web-vue')['Table'] ATable: typeof import('@arco-design/web-vue')['Table']
ATabPane: typeof import('@arco-design/web-vue')['TabPane'] ATabPane: typeof import('@arco-design/web-vue')['TabPane']
ATabs: typeof import('@arco-design/web-vue')['Tabs'] ATabs: typeof import('@arco-design/web-vue')['Tabs']
ATag: typeof import('@arco-design/web-vue')['Tag'] ATag: typeof import('@arco-design/web-vue')['Tag']
ATextarea: typeof import('@arco-design/web-vue')['Textarea']
ATooltip: typeof import('@arco-design/web-vue')['Tooltip'] ATooltip: typeof import('@arco-design/web-vue')['Tooltip']
ATree: typeof import('@arco-design/web-vue')['Tree']
ATreeSelect: typeof import('@arco-design/web-vue')['TreeSelect'] ATreeSelect: typeof import('@arco-design/web-vue')['TreeSelect']
ATrigger: typeof import('@arco-design/web-vue')['Trigger']
ATypographyParagraph: typeof import('@arco-design/web-vue')['TypographyParagraph']
ATypographyTitle: typeof import('@arco-design/web-vue')['TypographyTitle']
AUpload: typeof import('@arco-design/web-vue')['Upload']
Avatar: typeof import('./../components/Avatar/index.vue')['default'] Avatar: typeof import('./../components/Avatar/index.vue')['default']
AWatermark: typeof import('@arco-design/web-vue')['Watermark'] AWatermark: typeof import('@arco-design/web-vue')['Watermark']
Breadcrumb: typeof import('./../components/Breadcrumb/index.vue')['default'] Breadcrumb: typeof import('./../components/Breadcrumb/index.vue')['default']

View File

@@ -0,0 +1,118 @@
<template>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col-style="{ display: 'none' }"
:wrapper-col-style="{ flex: 1 }"
size="large"
@submit="handleLogin"
>
<a-form-item field="cardNumber" hide-label>
<a-input v-model="form.cardNumber" placeholder="请刷卡" allow-clear @keyup.enter="handleLogin" />
</a-form-item>
<a-form-item>
<a-space direction="vertical" fill class="w-full">
<a-button type="primary" :loading="loading" html-type="submit" size="large" long>立即登录</a-button>
</a-space>
</a-form-item>
</a-form>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { type FormInstance, Message } from '@arco-design/web-vue'
import { useTabsStore, useUserStore } from '@/stores'
const formRef = ref<FormInstance>()
const form = reactive({
cardNumber: '',
})
const rules: FormInstance['rules'] = {
cardNumber: [
{ required: true, message: '请刷卡或输入卡号' },
],
}
const userStore = useUserStore()
const tabsStore = useTabsStore()
const router = useRouter()
const loading = ref(false)
// 登录
const handleLogin = async () => {
const isInvalid = await formRef.value?.validate()
if (isInvalid) return
try {
loading.value = true
// 调用后端接口校验卡号
await userStore.cardLogin(form)
tabsStore.reset()
const { redirect, ...othersQuery } = router.currentRoute.value.query
await router.push({
path: (redirect as string) || '/',
query: {
...othersQuery,
},
})
Message.success('欢迎使用')
} catch (error) {
// 登录失败处理
} finally {
loading.value = false
}
}
// 监听键盘输入,实现自动识别卡号
onMounted(() => {
// 监听全局键盘事件,用于识别刷卡输入
document.addEventListener('keydown', (e) => {
// 刷卡器通常会快速输入卡号并以Enter键结束
if (e.key === 'Enter') {
// 遇到Enter键尝试登录
if (form.cardNumber.trim()) {
handleLogin()
}
} else if (e.key.length === 1 && !e.ctrlKey && !e.altKey && !e.metaKey) {
// 只捕获单个字符的按键,忽略控制键和功能键
form.cardNumber += e.key
} else if (e.key === 'Backspace') {
// 处理退格键
form.cardNumber = form.cardNumber.slice(0, -1)
}
})
})
</script>
<style scoped lang="scss">
.arco-input-wrapper,
:deep(.arco-select-view-single) {
height: 40px;
border-radius: 4px;
font-size: 13px;
}
.arco-input-wrapper.arco-input-error {
background-color: rgb(var(--danger-1));
border-color: rgb(var(--danger-3));
}
.arco-input-wrapper.arco-input-error:hover {
background-color: rgb(var(--danger-1));
border-color: rgb(var(--danger-6));
}
.arco-input-wrapper :deep(.arco-input) {
font-size: 13px;
color: var(--color-text-1);
}
.arco-input-wrapper:hover {
border-color: rgb(var(--arcoblue-6));
}
.btn {
height: 40px;
}
</style>

View File

@@ -15,28 +15,14 @@
<a-col :xs="24" :sm="12" :md="11"> <a-col :xs="24" :sm="12" :md="11">
<div class="login-right"> <div class="login-right">
<h3 v-if="isEmailLogin" class="login-right__title">邮箱登录</h3> <h3 v-if="isEmailLogin" class="login-right__title">邮箱登录</h3>
<EmailLogin v-if="isEmailLogin" />
<a-tabs v-else v-model:activeKey="activeTab" class="login-right__form"> <a-tabs v-else v-model:activeKey="activeTab" class="login-right__form">
<a-tab-pane key="1" title="账号登录"> <a-tab-pane key="1" title="账号登录">
<component :is="AccountLogin" v-if="activeTab === '1'" /> <component :is="AccountLogin" v-if="activeTab === '1'" />
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="2" title="手机号登录"> <a-tab-pane key="2" title="刷卡">
<component :is="PhoneLogin" v-if="activeTab === '2'" /> <component :is="CardLogin" v-if="activeTab === '2'" />
</a-tab-pane> </a-tab-pane>
</a-tabs> </a-tabs>
<!-- <div class="login-right__oauth">
<a-divider orientation="center">其他登录方式</a-divider>
<div class="list">
<div v-if="isEmailLogin" class="mode item" @click="toggleLoginMode"><icon-user /> 账号/手机号登录</div>
<div v-else class="mode item" @click="toggleLoginMode"><icon-email /> 邮箱登录</div>
<a class="item" title="使用 Gitee 账号登录" @click="onOauth('gitee')">
<GiSvgIcon name="gitee" :size="24" />
</a>
<a class="item" title="使用 GitHub 账号登录" @click="onOauth('github')">
<GiSvgIcon name="github" :size="24" />
</a>
</div>
</div>-->
</div> </div>
</a-col> </a-col>
</a-row> </a-row>
@@ -51,51 +37,13 @@
<Background /> <Background />
</div> </div>
<div v-else class="login h5">
<div class="login-logo">
<img v-if="logo" :src="logo" alt="logo" />
<img v-else src="/logo.svg" alt="logo" />
<span>{{ title }}</span>
</div>
<a-row align="stretch" class="login-box">
<a-col :xs="24" :sm="12" :md="11">
<div class="login-right">
<h3 v-if="isEmailLogin" class="login-right__title">邮箱登录</h3>
<EmailLogin v-if="isEmailLogin" />
<a-tabs v-else v-model:activeKey="activeTab" class="login-right__form">
<a-tab-pane key="1" title="账号登录">
<component :is="AccountLogin" v-if="activeTab === '1'" />
</a-tab-pane>
<a-tab-pane key="2" title="手机号登录">
<component :is="PhoneLogin" v-if="activeTab === '2'" />
</a-tab-pane>
</a-tabs>
</div>
</a-col>
</a-row>
<div class="login-right__oauth">
<a-divider orientation="center">其他登录方式</a-divider>
<div class="list">
<div v-if="isEmailLogin" class="mode item" @click="toggleLoginMode"><icon-user /> 账号/手机号登录</div>
<div v-else class="mode item" @click="toggleLoginMode"><icon-email /> 邮箱登录</div>
<a class="item" title="使用 Gitee 账号登录" @click="onOauth('gitee')">
<GiSvgIcon name="gitee" :size="24" />
</a>
<a class="item" title="使用 GitHub 账号登录" @click="onOauth('github')">
<GiSvgIcon name="github" :size="24" />
</a>
</div>
</div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import Background from './components/background/index.vue' import Background from './components/background/index.vue'
import AccountLogin from './components/account/index.vue' import AccountLogin from './components/account/index.vue'
import PhoneLogin from './components/phone/index.vue' import CardLogin from './components/card/index.vue'
import EmailLogin from './components/email/index.vue'
import { socialAuth } from '@/apis/auth'
import { useAppStore } from '@/stores' import { useAppStore } from '@/stores'
import { useDevice } from '@/hooks' import { useDevice } from '@/hooks'
@@ -109,16 +57,6 @@ const logo = computed(() => appStore.getLogo())
const isEmailLogin = ref(false) const isEmailLogin = ref(false)
const activeTab = ref('1') const activeTab = ref('1')
// 切换登录模式
const toggleLoginMode = () => {
isEmailLogin.value = !isEmailLogin.value
}
// 第三方登录授权
const onOauth = async (source: string) => {
const { data } = await socialAuth(source)
window.location.href = data.authorizeUrl
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -12,6 +12,7 @@
:disabled-column-keys="['name']" :disabled-column-keys="['name']"
@refresh="search" @refresh="search"
> >
<!-- toolbar 部分保持不变 -->
<template #toolbar-left> <template #toolbar-left>
<a-input-search v-model="queryForm.materialName" placeholder="请输入物料名称" allow-clear @search="search" /> <a-input-search v-model="queryForm.materialName" placeholder="请输入物料名称" allow-clear @search="search" />
<a-input-search v-model="queryForm.encoding" placeholder="请输入物料编码" allow-clear @search="search" /> <a-input-search v-model="queryForm.encoding" placeholder="请输入物料编码" allow-clear @search="search" />
@@ -38,18 +39,31 @@
<template #default>照片批量导入</template> <template #default>照片批量导入</template>
</a-button> </a-button>
</template> </template>
<!-- 修改点照片列插槽 -->
<template #photoUrl="{ record }"> <template #photoUrl="{ record }">
<div class="photo-container"> <div class="photo-container">
<!-- 1. 正常显示图片有地址 未标记为错误 -->
<img <img
v-if="record.photoUrl" v-if="record.photoUrl && !record.photoLoadError"
:src="record.photoUrl" :src="record.photoUrl"
alt="物料照片" alt="物料照片"
class="material-photo" class="material-photo"
@error="handleImgError($event)" @load="handleImgLoad(record)"
@error="handleImgError(record, $event)"
/> />
<span v-else class="no-photo">暂无照片</span>
<!-- 2. 无地址 -->
<span v-else-if="!record.photoUrl" class="no-photo">暂无照片</span>
<!-- 3. 地址存在但加载失败 -->
<span v-else class="photo-error">
<icon-exclamation-circle-fill class="error-icon" />
照片异常
</span>
</div> </div>
</template> </template>
<template #action="{ record }"> <template #action="{ record }">
<a-space> <a-space>
<a-link v-permission="['admin:materialInfo:update']" title="修改" @click="onUpdate(record)">修改</a-link> <a-link v-permission="['admin:materialInfo:update']" title="修改" @click="onUpdate(record)">修改</a-link>
@@ -73,6 +87,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
// ... 保持原有的 import 不变
import MaterialInfoAddModal from './MaterialInfoAddModal.vue' import MaterialInfoAddModal from './MaterialInfoAddModal.vue'
import MaterialInfoImportDrawer from './MaterialInfoImportDrawer.vue' import MaterialInfoImportDrawer from './MaterialInfoImportDrawer.vue'
import PhotosImport from '@/views/material/PhotosImport.vue'; import PhotosImport from '@/views/material/PhotosImport.vue';
@@ -84,6 +99,12 @@ import has from '@/utils/has'
defineOptions({ name: 'MaterialInfo' }) defineOptions({ name: 'MaterialInfo' })
// 扩展类型定义,允许动态添加 photoLoadError 属性
// 如果 TS 报错,可以在 MaterialInfoResp 接口定义中添加 photoLoadError?: boolean;
interface MaterialInfoRespWithStatus extends MaterialInfoResp {
photoLoadError?: boolean;
}
const queryForm = reactive<MaterialInfoQuery>({ const queryForm = reactive<MaterialInfoQuery>({
materialName: undefined, materialName: undefined,
encoding: undefined, encoding: undefined,
@@ -97,6 +118,7 @@ const {
search, search,
handleDelete, handleDelete,
} = useTable((page) => listMaterialInfo({ ...queryForm, ...page }), { immediate: true }) } = useTable((page) => listMaterialInfo({ ...queryForm, ...page }), { immediate: true })
const columns = ref<TableInstanceColumns[]>([ const columns = ref<TableInstanceColumns[]>([
{ title: '物料名称', dataIndex: 'materialName', slotName: 'materialName' }, { title: '物料名称', dataIndex: 'materialName', slotName: 'materialName' },
{ title: '物料编码', dataIndex: 'encoding', slotName: 'encoding' }, { title: '物料编码', dataIndex: 'encoding', slotName: 'encoding' },
@@ -116,14 +138,12 @@ const columns = ref<TableInstanceColumns[]>([
}, },
]) ])
// 重置
const reset = () => { const reset = () => {
queryForm.materialName = undefined queryForm.materialName = undefined
queryForm.encoding = undefined queryForm.encoding = undefined
search() search()
} }
// 删除
const onDelete = (record: MaterialInfoResp) => { const onDelete = (record: MaterialInfoResp) => {
return handleDelete(() => deleteMaterialInfo(record.id), { return handleDelete(() => deleteMaterialInfo(record.id), {
content: `是否确定删除该条数据?`, content: `是否确定删除该条数据?`,
@@ -131,25 +151,26 @@ const onDelete = (record: MaterialInfoResp) => {
}) })
} }
// 导出
const onExport = () => { const onExport = () => {
useDownload(() => exportMaterialInfo(queryForm)) useDownload(() => exportMaterialInfo(queryForm))
} }
// 图片加载失败处理函数(新增) // 【修改点】图片加载处理逻辑
const handleImgError = (e: Event) => { const handleImgLoad = (record: MaterialInfoRespWithStatus) => {
// 替换为你的默认图片地址(建议放在/static目录下 record.photoLoadError = false
const target = e.target as HTMLImageElement }
target.src = '/static/images/default-material.png'
const handleImgError = (record: MaterialInfoRespWithStatus, e: Event) => {
// 标记为加载错误,触发模板渲染 "照片异常"
record.photoLoadError = true
console.warn(`物料照片加载失败: ${record.photoUrl}`)
} }
const MaterialInfoAddModalRef = ref<InstanceType<typeof MaterialInfoAddModal>>() const MaterialInfoAddModalRef = ref<InstanceType<typeof MaterialInfoAddModal>>()
// 新增
const onAdd = () => { const onAdd = () => {
MaterialInfoAddModalRef.value?.onAdd() MaterialInfoAddModalRef.value?.onAdd()
} }
// 修改
const onUpdate = (record: MaterialInfoResp) => { const onUpdate = (record: MaterialInfoResp) => {
MaterialInfoAddModalRef.value?.onUpdate(record.id) MaterialInfoAddModalRef.value?.onUpdate(record.id)
} }
@@ -166,23 +187,43 @@ const onPhotosImport = () => {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
// 物料照片样式(新增)
.photo-container { .photo-container {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 80px; height: 80px;
width: 100%;
overflow: hidden;
} }
.material-photo { .material-photo {
width: 80px; width: 100%;
height: 80px; height: 100%;
object-fit: cover; // 保持图片比例,避免拉伸 object-fit: cover;
border-radius: 4px; border-radius: 4px;
transition: opacity 0.3s;
} }
.no-photo { .no-photo {
color: #999; color: #999;
font-size: 12px; font-size: 12px;
text-align: center;
}
// 新增样式
.photo-error {
color: #ff4d4f; // 阿里红/危险色
font-size: 12px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
line-height: 1.4;
.error-icon {
font-size: 18px;
margin-bottom: 2px;
}
} }
</style> </style>

View File

@@ -0,0 +1,377 @@
<template>
<div class="gi_page">
<div class="container">
<h2 class="page-title">标签打印</h2>
<!-- 标签参数设置 -->
<div class="form-section">
<a-form :model="formData" layout="vertical">
<div class="form-grid">
<div class="form-grid-item">
<a-form-item label="物料名称">
<a-input v-model="formData.materialName" placeholder="请输入物料名称" :disabled="isFromWorkOrder" />
</a-form-item>
</div>
<div class="form-grid-item">
<a-form-item label="物料编码">
<a-input v-model="formData.encoding" placeholder="请输入物料编码" :disabled="isFromWorkOrder" />
</a-form-item>
</div>
<div class="form-grid-item">
<a-form-item label="工单编号">
<a-input v-model="formData.orderNo" placeholder="请输入工单编号" :disabled="isFromWorkOrder" />
</a-form-item>
</div>
<div class="form-grid-item">
<a-form-item label="生产批次" required>
<a-input v-model="formData.productionBatch" placeholder="请输入生产批次" />
</a-form-item>
</div>
</div>
<div class="form-actions">
<a-button type="primary" @click="generateLabel" :disabled="!formData.productionBatch">生成标签</a-button>
</div>
</a-form>
</div>
<!-- 标签预览 -->
<div v-if="labelData.partName" class="label-preview-section">
<h3>标签预览</h3>
<div class="label-container" ref="labelContainer">
<div class="label" v-for="index in 1" :key="index">
<table class="label-table">
<tr>
<td class="label-cell">
<div class="label-field">零件名称</div>
<div class="label-value">{{ labelData.partName }}</div>
</td>
<td class="label-cell">
<div class="label-field">生产日期</div>
<div class="label-value">{{ labelData.productionDate }}</div>
</td>
<td class="label-cell qr-cell" rowspan="4">
<div class="qr-code">
<img :src="`https://api.qrserver.com/v1/create-qr-code/?size=120x120&data=${encodeURIComponent(labelData.qrCodeData)}`" alt="QR Code" />
</div>
</td>
</tr>
<tr>
<td class="label-cell">
<div class="label-field">零件号</div>
<div class="label-value">{{ labelData.partNumber }}</div>
</td>
<td class="label-cell">
<div class="label-field">数量</div>
<div class="label-value">{{ labelData.quantity }}</div>
</td>
</tr>
<tr>
<td class="label-cell">
<div class="label-field">标重(kg)</div>
<div class="label-value">{{ labelData.standardWeight }}</div>
</td>
<td class="label-cell">
<div class="label-field">包装签字</div>
<div class="label-value">{{ labelData.packingSignature || '' }}</div>
</td>
</tr>
<tr>
<td class="label-cell">
<div class="label-field">实重(kg)</div>
<div class="label-value">{{ labelData.actualWeight || '' }}</div>
</td>
<td class="label-cell">
<div class="label-field">检验签字</div>
<div class="label-value">{{ labelData.inspectionSignature || '' }}</div>
</td>
</tr>
<!-- <tr>-->
<!-- <td class="label-cell" colspan="2"></td>-->
<!-- <td class="label-cell qr-cell">-->
<!-- <div class="serial-number">序号: {{ labelData.serialNumber }}</div>-->
<!-- </td>-->
<!-- </tr>-->
</table>
</div>
</div>
<div class="label-actions">
<a-button type="primary" @click="printLabel">打印标签</a-button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, nextTick, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import { useRoute } from 'vue-router'
const route = useRoute()
// 表单数据
const formData = reactive({
encoding: '',
materialName: '',
orderNo: '',
productionBatch: ''
})
// 是否是从工单页面跳转过来的
const isFromWorkOrder = ref(false)
// 标签数据
const labelData = reactive({
partName: '',
partNumber: '',
standardWeight: '',
actualWeight: '',
productionDate: '',
quantity: '',
packingSignature: '',
inspectionSignature: '',
serialNumber: '',
qrCodeData: '',
})
// 标签容器引用
const labelContainer = ref<HTMLElement | null>(null)
// 模拟获取标签数据的接口
const fetchLabelData = async (params: any) => {
// 模拟后端接口延迟
return new Promise((resolve) => {
setTimeout(() => {
// 格式化生产日期为 yyyyMMddHHmm 格式
const now = new Date()
const formattedDate = now.getFullYear().toString() +
String(now.getMonth() + 1).padStart(2, '0') +
String(now.getDate()).padStart(2, '0') +
String(now.getHours()).padStart(2, '0') +
String(now.getMinutes()).padStart(2, '0')
// 模拟返回数据
const mockData = {
partName: params.materialName || 'PP0449002护套',
partNumber: params.encoding || 'PP0449002',
standardWeight: '0.39500',
actualWeight: '',
productionDate: formattedDate,
quantity: '200',
packingSignature: '',
inspectionSignature: '',
serialNumber: '4',
qrCodeData: `PART:${params.encoding || 'PP0449002'},NAME:${params.materialName || 'PP0449002护套'},DATE:${formattedDate},QTY:200`,
}
resolve(mockData)
}, 500)
})
}
// 生成标签
const generateLabel = async () => {
if (!formData.productionBatch) {
Message.error('请输入生产批次')
return
}
try {
// 获取标签数据
const result = await fetchLabelData(formData)
Object.assign(labelData, result)
Message.success('标签生成成功')
} catch (error) {
console.error('生成标签失败:', error)
Message.error('生成标签失败')
}
}
// 组件挂载时,检查是否有参数传递
onMounted(() => {
// 从路由参数中获取数据
const materialName = route.query.materialName as string
const encoding = route.query.encoding as string
const orderNo = route.query.orderNo as string
// 如果有参数传递,设置表单数据并标记为不可修改
if (materialName && encoding && orderNo) {
formData.materialName = materialName
formData.encoding = encoding
formData.orderNo = orderNo
isFromWorkOrder.value = true
// 自动生成标签
generateLabel()
}
})
// 打印标签
const printLabel = async () => {
await nextTick()
if (labelContainer.value) {
// 克隆标签容器内容用于打印
const printContent = labelContainer.value.cloneNode(true) as HTMLElement
// 创建打印窗口
const printWindow = window.open('', '_blank')
if (printWindow) {
printWindow.document.write(`
<html>
<head>
<title>标签打印</title>
<style>
body { margin: 0; padding: 10mm; font-family: Arial, sans-serif; }
.label-container { display: flex; flex-wrap: wrap; gap: 10mm; justify-content: center; }
.label { width: 90mm; height: 36mm; border: 1px solid #000; padding: 0; box-sizing: border-box; page-break-inside: avoid; }
.label-table { width: 100%; height: 100%; border-collapse: collapse; }
.label-cell { border: 1px solid #000; padding: 1mm; vertical-align: top; }
.qr-cell { width: 24mm; text-align: center; vertical-align: middle; }
.label-field { font-size: 7pt; font-weight: bold; margin-bottom: 0.5mm; }
.label-value { font-size: 7pt; }
.qr-code img { width: 20mm; height: 20mm; margin: 1mm 0; }
.serial-number { font-size: 7pt; margin-top: 1mm; }
</style>
</head>
<body>
${printContent.innerHTML}
</body>
</html>
`)
printWindow.document.close()
printWindow.focus()
printWindow.print()
printWindow.close()
}
}
}
defineOptions({ name: 'BarcodePrint' })
</script>
<style scoped lang="scss">
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.page-title {
margin-bottom: 30px;
text-align: center;
color: #333;
}
/* 表单区域 */
.form-section {
background-color: var(--color-bg-2);
padding: 20px;
border-radius: 4px;
margin-bottom: 30px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 16px;
margin-bottom: 20px;
}
.form-grid-item {
width: 100%;
}
.form-actions {
margin-top: 20px;
display: flex;
justify-content: center;
}
/* 标签预览 */
.label-preview-section {
margin-top: 30px;
padding: 20px;
background-color: var(--color-bg-2);
border-radius: 4px;
}
.label-preview-section h3 {
margin: 0 0 20px 0;
font-size: 16px;
font-weight: bold;
text-align: center;
}
.label-container {
display: flex;
flex-wrap: wrap;
gap: 20px;
justify-content: center;
margin-bottom: 20px;
}
.label {
width: 450px;
height: 180px;
border: 1px solid #000;
padding: 0;
box-sizing: border-box;
font-family: Arial, sans-serif;
}
.label-table {
width: 100%;
height: 100%;
border-collapse: collapse;
}
.label-cell {
border: 1px solid #000;
padding: 5px;
vertical-align: top;
}
.qr-cell {
width: 120px;
text-align: center;
vertical-align: middle;
}
.label-field {
font-size: 12px;
font-weight: bold;
margin-bottom: 2px;
}
.label-value {
font-size: 12px;
}
.qr-code img {
width: 100px;
height: 100px;
margin: 5px 0;
}
.serial-number {
font-size: 12px;
margin-top: 5px;
}
.label-actions {
display: flex;
justify-content: center;
margin-top: 20px;
}
/* 确保输入框宽度一致 */
:deep(.arco-input-wrapper) {
width: 100%;
height: 35px;
}
:deep(.arco-input) {
font-size: 16px;
}
</style>

View File

@@ -102,7 +102,7 @@ import { DisEnableStatusList } from '@/constant/common'
import { useDownload, useResetReactive, useTable } from '@/hooks' import { useDownload, useResetReactive, useTable } from '@/hooks'
import { isMobile } from '@/utils' import { isMobile } from '@/utils'
import has from '@/utils/has' import has from '@/utils/has'
import type { ColumnItem } from '@/components/GiForm' import type {ColumnItem} from "@/components/GiForm";
defineOptions({ name: 'SystemUser' }) defineOptions({ name: 'SystemUser' })
@@ -168,7 +168,7 @@ const columns: TableInstanceColumns[] = [
fixed: !isMobile() ? 'left' : undefined, fixed: !isMobile() ? 'left' : undefined,
}, },
{ title: '用户名', dataIndex: 'username', slotName: 'username', minWidth: 100, ellipsis: true, tooltip: true }, { title: '用户名', dataIndex: 'username', slotName: 'username', minWidth: 100, ellipsis: true, tooltip: true },
{ title: '卡号', dataIndex: 'cardNo', slotName: 'cardNo' }, { title:'卡号', dataIndex: 'cardNo', slotName: 'cardNo' },
{ title: '状态', dataIndex: 'status', slotName: 'status', align: 'center' }, { title: '状态', dataIndex: 'status', slotName: 'status', align: 'center' },
{ title: '性别', dataIndex: 'gender', slotName: 'gender', align: 'center' }, { title: '性别', dataIndex: 'gender', slotName: 'gender', align: 'center' },
{ title: '角色', dataIndex: 'roleNames', slotName: 'roleNames', minWidth: 165 }, { title: '角色', dataIndex: 'roleNames', slotName: 'roleNames', minWidth: 165 },
@@ -204,7 +204,7 @@ const reset = () => {
const onDelete = (record: UserResp) => { const onDelete = (record: UserResp) => {
return handleDelete(() => deleteUser(record.id), { return handleDelete(() => deleteUser(record.id), {
content: `是否确定删除用户「${record.username}」?`, content: `是否确定删除用户「${record.nickname}(${record.username})」?`,
showModal: true, showModal: true,
}) })
} }

View File

@@ -25,8 +25,8 @@
<div class="main-content"> <div class="main-content">
<!-- 左侧表单 --> <!-- 左侧表单 -->
<div class="left-section"> <div class="left-section">
<a-image v-if="!formData.imageUrl" :src="formData.imageUrl"/> <a-image v-if="!formData.photoUrl" :src="formData.photoUrl" />
<img v-else :src="formData.imageUrl" class="sample-image square-image" alt="样图"> <img v-else :src="formData.photoUrl" class="sample-image square-image" alt="样图">
<a-form :model="formData" layout="vertical"> <a-form :model="formData" layout="vertical">
<div class="form-row"> <div class="form-row">
@@ -44,12 +44,12 @@
<div class="form-row"> <div class="form-row">
<div class="form-item"> <div class="form-item">
<a-form-item label="物料编码"> <a-form-item label="物料编码">
<a-input v-model="formData.materialCode" placeholder="物料编码" disabled /> <a-input v-model="formData.encoding" placeholder="物料编码" disabled />
</a-form-item> </a-form-item>
</div> </div>
<div class="form-item"> <div class="form-item">
<a-form-item label="重量"> <a-form-item label="重量(g)">
<a-input v-model="formData.weight" placeholder="Kg" disabled /> <a-input v-model="formData.unitWeight" placeholder="Kg" disabled />
</a-form-item> </a-form-item>
</div> </div>
</div> </div>
@@ -58,8 +58,9 @@
<a-form-item label="物料编码"> <a-form-item label="物料编码">
<a-input <a-input
v-model="formData.inputMaterialCode" v-model="formData.inputMaterialCode"
placeholder="请使用扫码枪扫描物料编码" placeholder="请点击此处确保光标闪烁,并使用扫码枪扫描物料编码"
@change="handleMaterialCodeChange" @keydown="handleKeyDown"
@input="handleMaterialCodeChange"
/> />
</a-form-item> </a-form-item>
</div> </div>
@@ -83,11 +84,10 @@
</div> </div>
<div class="info-item"> <div class="info-item">
<span class="label">物料编码:</span> <span class="label">物料编码:</span>
<span class="value">{{ formData.materialCode || '-' }}</span> <span class="value">{{ formData.encoding || '-' }}</span>
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card">
<h4>比对结果</h4> <h4>比对结果</h4>
<div class="info-item"> <div class="info-item">
@@ -109,7 +109,7 @@
</div> </div>
</div> </div>
<!-- 称重登记页面 --> <!-- 称重登记页面 - 这里放FLV播放器 -->
<div v-else-if="activeStep === 2" class="step-content"> <div v-else-if="activeStep === 2" class="step-content">
<div class="weighing-content"> <div class="weighing-content">
<!-- 左侧输入区域 --> <!-- 左侧输入区域 -->
@@ -123,22 +123,58 @@
<div class="form-row"> <div class="form-row">
<div class="form-item"> <div class="form-item">
<label>输入数量:</label> <label>输入数量:</label>
<a-input v-model="inputQuantity" placeholder="请输入数量" /> <a-input v-model="inputQuantity" placeholder="请输入数量" @change="calculateWeight" />
</div> </div>
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="form-item"> <div class="form-item">
<label>对应重量:</label> <label>计算重量(g):</label>
<a-input v-model="inputWeight" placeholder="请输入重量" /> <a-input v-model="calculatedWeight" placeholder="-" disabled />
</div> </div>
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="form-item"> <div class="form-item">
<label>称重次数:</label> <label>对应重量(g):</label>
<a-input v-model="weighingCount" placeholder="称重次数" disabled /> <a-input v-model="ahDeviceWeight" placeholder="-" disabled/>
</div> </div>
</div> </div>
<div class="image-placeholder large-image">实时画面</div>
<!-- 这里是摄像头画面 - 替换成FLV播放器 -->
<div class="video-container large-image">
<video
ref="cameraVideo"
autoplay
muted
playsinline
style="width: 100%; height: 100%; object-fit: cover;"
></video>
<!-- 加载状态 -->
<div v-if="cameraStatus === 'connecting'" class="video-overlay">
<a-spin />
<span style="margin-left: 8px;">加载中...</span>
</div>
<!-- 错误状态 -->
<div v-if="cameraStatus === 'error'" class="video-overlay error">
<icon-close-circle-fill style="color: #ff4d4f; font-size: 24px;" />
<span>连接失败</span>
<a-button size="small" type="primary" @click="reconnectCamera">重试</a-button>
</div>
<!-- 未连接状态 -->
<div v-if="cameraStatus === 'disconnected'" class="video-overlay">
<icon-pause-circle-fill style="color: #999; font-size: 24px;" />
<span>未连接</span>
<a-button size="small" type="primary" @click="initCamera">连接</a-button>
</div>
<!-- 摄像头状态标识 -->
<div v-if="cameraStatus === 'connected'" class="camera-badge">
<span class="live-badge">LIVE</span>
</div>
</div>
<div class="weighing-actions"> <div class="weighing-actions">
<a-button type="primary" @click="handleConfirm">确定</a-button> <a-button type="primary" @click="handleConfirm">确定</a-button>
<a-button style="margin-left: 12px;" @click="handleReset">重置</a-button> <a-button style="margin-left: 12px;" @click="handleReset">重置</a-button>
@@ -172,12 +208,12 @@
<div class="completion-icon"> <div class="completion-icon">
<a-icon type="check-circle" :size="64" style="color: #52c41a;" /> <a-icon type="check-circle" :size="64" style="color: #52c41a;" />
</div> </div>
<h2>创建完成</h2> <h2>{{ workOrderResp.matchResult === 'success' ? '创建成功' : '创建失败' }}</h2>
<p>任务已成功创建以下是任务详情</p> <p>{{ workOrderResp.matchResult === 'success' ? '任务创建成功' : '任务创建失败' }}以下是任务详情</p>
<div class="completion-info"> <div class="completion-info">
<div class="info-item"> <div class="info-item">
<span class="label">任务ID:</span> <span class="label">任务工单号:</span>
<span class="value">{{ taskId }}</span> <span class="value">{{ workOrderResp.orderNo }}</span>
</div> </div>
<div class="info-item"> <div class="info-item">
<span class="label">物料名称:</span> <span class="label">物料名称:</span>
@@ -185,15 +221,19 @@
</div> </div>
<div class="info-item"> <div class="info-item">
<span class="label">物料编码:</span> <span class="label">物料编码:</span>
<span class="value">{{ formData.materialCode }}</span> <span class="value">{{ formData.encoding }}</span>
</div> </div>
<div class="info-item"> <div class="info-item">
<span class="label">物料规格:</span> <span class="label">物料规格:</span>
<span class="value">{{ formData.materialSpec }}</span> <span class="value">{{ formData.materialSpec }}</span>
</div> </div>
<div class="info-item"> <div class="info-item">
<span class="label">重量:</span> <span class="label">物料总个数:</span>
<span class="value">{{ formData.weight }}</span> <span class="value">{{ workOrderResp.totalCount }}</span>
</div>
<div class="info-item">
<span class="label">物料总重量(g):</span>
<span class="value">{{ workOrderResp.totalWeight }}</span>
</div> </div>
</div> </div>
<div class="completion-actions"> <div class="completion-actions">
@@ -215,6 +255,7 @@
v-if="activeStep < 3" v-if="activeStep < 3"
type="primary" type="primary"
@click="handleNext" @click="handleNext"
:disabled="activeStep === 1 && formData.matchResult !== 'success'"
class="next-button" class="next-button"
> >
{{ activeStep === 2 ? '完成' : '下一步' }} {{ activeStep === 2 ? '完成' : '下一步' }}
@@ -225,9 +266,20 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed } from 'vue' import { nextTick, onBeforeUnmount, onMounted, ref, reactive, onUnmounted } from 'vue'
import { Modal, Icon } from '@arco-design/web-vue' import { Modal, Message } from '@arco-design/web-vue'
import {getMaterialDetail, type WeighManageQuery} from "@/apis/weightManage/weightManage"; import { getMaterialDetail } from "@/apis/weightManage/weightManage";
import {addWorkOrder, type WorkOrderResp} from "@/apis/workOrder/workOrder";
import { Notification } from "@arco-design/web-vue"
import { catchPhoto } from '@/apis/material/materialInfo'
defineOptions({ name: 'WeightManage' })
// 动态导入flv.js
const flvjs = ref<any>(null)
// 当前步骤 // 当前步骤
const activeStep = ref(1) const activeStep = ref(1)
@@ -235,37 +287,298 @@ const activeStep = ref(1)
// 表单数据 // 表单数据
const formData = reactive({ const formData = reactive({
inputMaterialCode: '', // 输入的物料编码 inputMaterialCode: '', // 输入的物料编码
materialCode: '', // 物料编码 id: '', // 物料ID
encoding: '', // 物料编码
materialName: '', // 物料名称 materialName: '', // 物料名称
materialSpec: '', // 物料规格 materialSpec: '', // 物料规格
weight: 0, // 重量 unitWeight: 0, // 重量
imageUrl: '', // 样图URL photoUrl: '', // 样图URL
matchResult: '' // 比对结果 matchResult: '', // 比对结果
}) })
// 摄像头状态
const cameraStatus = ref<'connected' | 'connecting' | 'disconnected' | 'error'>('disconnected')
// 视频元素引用
const cameraVideo = ref<HTMLVideoElement | null>(null)
// FLV播放器实例
let player: any = null
const reconnectCount = ref(0)
const maxReconnectAttempts = 5
const reconnectTimer = ref<any>(null)
// 直接写死FLV流地址根据你的实际地址修改
const FLV_URL = 'http://192.168.2.87:8866/live?url=rtsp://admin:admin%40123@192.168.2.59:554/media/video1' // 摄像头的FLV地址
// 加载flv.js
const loadFlvJs = async () => {
if (typeof window !== 'undefined') {
const module = await import('flv.js')
flvjs.value = module.default
}
}
// 初始化摄像头
const initCamera = () => {
if (!cameraVideo.value || !flvjs.value) {
return
}
// 检查浏览器支持
if (!flvjs.value.isSupported()) {
Message.error('当前浏览器不支持FLV播放')
cameraStatus.value = 'error'
return
}
cameraStatus.value = 'connecting'
// 清除之前的重连定时器
if (reconnectTimer.value) {
clearTimeout(reconnectTimer.value)
reconnectTimer.value = null
}
try {
// 销毁旧的播放器
if (player) {
player.destroy()
player = null
}
// 创建新的播放器
player = flvjs.value.createPlayer({
type: 'flv',
url: FLV_URL,
isLive: true,
hasAudio: false,
hasVideo: true,
enableStashBuffer: false,
stashInitialSize: 128,
enableWorker: true,
autoCleanupSourceBuffer: true,
})
// 绑定视频元素
player.attachMediaElement(cameraVideo.value)
// 加载并播放
player.load()
player.play().then(() => {
cameraStatus.value = 'connected'
reconnectCount.value = 0 // 连接成功,重置重连计数
}).catch((error: any) => {
cameraStatus.value = 'error'
Message.error('摄像头播放失败,正在重连')
// 触发自动重连
handleReconnect()
})
// 监听各种事件
player.on(flvjs.value.Events.ERROR, (err: any) => {
cameraStatus.value = 'error'
// 触发自动重连
handleReconnect()
})
player.on(flvjs.value.Events.STALLED, () => {
// 可以在这里处理卡顿,但不立即重连
})
player.on(flvjs.value.Events.RECOVERED_EARLY, () => {
if (cameraStatus.value !== 'connected') {
cameraStatus.value = 'connected'
}
})
} catch (error) {
cameraStatus.value = 'error'
handleReconnect()
}
}
// 处理自动重连
const handleReconnect = () => {
// 只有当前在称重页面才重连
if (activeStep.value !== 2) return
// 检查重连次数
if (reconnectCount.value >= maxReconnectAttempts) {
Message.error('摄像头连接失败,请手动重试')
return
}
// 增加重连计数
reconnectCount.value++
// 计算重连延迟(指数退避)
const delay = Math.min(1000 * 2 ** (reconnectCount.value - 1), 30000)
// 清除旧的定时器
if (reconnectTimer.value) {
clearTimeout(reconnectTimer.value)
}
// 设置新的重连定时器
reconnectTimer.value = setTimeout(() => {
initCamera()
}, delay)
}
// 重新连接摄像头(手动)
const reconnectCamera = () => {
reconnectCount.value = 0 // 手动重连时重置计数
initCamera()
}
// 添加一个定时检查状态的函数
const checkCameraStatus = () => {
if (!player || !cameraVideo.value) return
try {
// 检查播放器状态
if (player.isPlaying && !player.isPlaying()) {
if (cameraStatus.value === 'connected') {
cameraStatus.value = 'error'
handleReconnect()
}
}
} catch (error) {
}
}
// 启动状态检查定时器
let statusCheckTimer: any = null
// 监听步骤变化
watch(activeStep, (newVal) => {
if (newVal === 2) {
// 进入称重登记页面,启动摄像头
reconnectCount.value = 0 // 重置重连计数
nextTick(() => {
initCamera()
})
// 启动状态检查每30秒检查一次
statusCheckTimer = setInterval(checkCameraStatus, 30000)
} else {
// 离开称重登记页面,停止摄像头和定时器
stopCamera()
if (statusCheckTimer) {
clearInterval(statusCheckTimer)
statusCheckTimer = null
}
if (reconnectTimer.value) {
clearTimeout(reconnectTimer.value)
reconnectTimer.value = null
}
reconnectCount.value = 0
}
})
// 组件卸载时清理所有定时器
onBeforeUnmount(() => {
stopCamera()
if (statusCheckTimer) {
clearInterval(statusCheckTimer)
statusCheckTimer = null
}
if (reconnectTimer.value) {
clearTimeout(reconnectTimer.value)
reconnectTimer.value = null
}
})
// 重新连接摄像头
// const reconnectCamera = () => {
// if (player) {
// player.unload()
// player.detachMediaElement()
// player.destroy()
// player = null
// }
// setTimeout(() => initCamera(), 1000)
// }
// 停止摄像头
const stopCamera = () => {
if (player) {
player.pause()
player.unload()
player.detachMediaElement()
player.destroy()
player = null
cameraStatus.value = 'disconnected'
}
if (reconnectTimer.value) {
clearTimeout(reconnectTimer.value)
reconnectTimer.value = null
}
reconnectCount.value = 0
}
// 监听步骤变化
watch(activeStep, (newVal) => {
if (newVal === 2) {
// 进入称重登记页面,启动摄像头
nextTick(() => {
initCamera()
})
} else {
// 离开称重登记页面,停止摄像头
stopCamera()
}
})
// 组件挂载时
onMounted(async () => {
await loadFlvJs()
})
// 组件卸载时
onBeforeUnmount(() => {
stopCamera()
})
const workOrderResp = ref<WorkOrderResp>({
id: '',
title: '',
orderNo: '',
materialName: '',
encoding: '',
unitWeight: '',
materialSpec: '',
photoUrl: '',
totalWeight: '',
totalCount: '',
createUserString: '',
updateUserString: '',
matchResult: 'failed'
})
// 称重登记页面数据 // 称重登记页面数据
const inputQuantity = ref('') const inputQuantity = ref('')
const inputWeight = ref('') const ahDeviceWeight = ref('10')
const calculatedWeight = ref('')
const weighingCount = ref(1) const weighingCount = ref(1)
// WebSocket连接
const ws = ref<WebSocket | null>(null)
const wsConnected = ref(false)
// 称重列表数据 // 称重列表数据
const weighingList = ref([ const weighingList = ref([
// 初始数据可以在这里添加 // 初始数据可以在这里添加
]) ])
// 任务ID
const taskId = computed(() => {
const date = new Date().toISOString().slice(0, 10).replace(/-/g, '')
const random = Math.floor(1000 + Math.random() * 9000)
return `${date}${formData.materialCode}${random}`
})
// 称重表格列 // 称重表格列
const columns = [ const columns = [
{ {
title: '称重次数', title: '称重次数',
dataIndex: 'count', dataIndex: 'weightTime',
key: 'count' key: 'weightTime'
}, },
{ {
title: '数量', title: '数量',
@@ -274,16 +587,36 @@ const columns = [
className: 'green-bg' className: 'green-bg'
}, },
{ {
title: '重量', title: '重量(g)',
dataIndex: 'weight', dataIndex: 'weight',
key: 'weight', key: 'weight',
className: 'green-bg' className: 'green-bg'
}, },
{
title: '计算重量(g)',
dataIndex: 'calculatedWeight',
key: 'calculatedWeight',
className: 'green-bg'
},
{ {
title: '抓拍图片', title: '抓拍图片',
dataIndex: 'image', dataIndex: 'image',
key: 'image', key: 'image',
className: 'green-bg' className: 'green-bg',
// 使用自定义渲染
render: ({ record }) => {
if (record.image && record.image !== '未抓拍') {
return h('div', { class: 'image-preview' }, [
h('img', {
src: record.image,
alt: '抓拍图片',
style: 'width: 60px; height: 45px; object-fit: cover; border-radius: 4px; cursor: pointer;',
onClick: () => previewImage(record.image),
}),
])
}
return h('span', record.image || '-')
},
}, },
{ {
title: '操作', title: '操作',
@@ -293,62 +626,134 @@ const columns = [
} }
] ]
// 防抖函数
const debounce = <T extends (...args: any[]) => any>(func: T, delay: number): ((...args: Parameters<T>) => void) => {
let timer: ReturnType<typeof setTimeout> | null = null;
return function(...args: Parameters<T>) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
func(...args);
}, delay);
};
};
// 原始的物料编码变化处理函数
const originalHandleMaterialCodeChange = async () => {
// 确保输入框内容是完整的物料编码
const materialCode = formData.inputMaterialCode?.trim();
// 处理物料编码变化 // 无论是否有输入,先重置所有物料相关字段,确保新数据能完全覆盖旧数据
const handleMaterialCodeChange = async () => { formData.encoding = "";
if (formData.inputMaterialCode) {
try {
await fetchMaterialData(formData.inputMaterialCode)
} catch (error) {
console.error('获取物料数据失败:', error)
}
} else {
formData.materialCode = "";
formData.materialName = ""; formData.materialName = "";
formData.materialSpec = ""; formData.materialSpec = "";
formData.weight = 0; formData.unitWeight = 0;
formData.imageUrl = ""; formData.photoUrl = "";
formData.matchResult = ""; formData.matchResult = "";
// 如果有物料编码输入,获取物料数据
if (materialCode) {
try {
await fetchMaterialData(materialCode)
} catch (error) {
console.error('获取物料数据失败:', error)
// 即使获取失败,也保持字段为空,避免显示旧数据
}
} }
}; };
// 模拟调用后端接口 // 记录上次输入时间,用于判断是否是扫码枪输入
const fetchMaterialData = async (code: string) => { let lastInputTime = 0;
const query: WeighManageQuery = { // 记录输入内容,用于判断是否是新的扫码
materialCode: code let previousInput = '';
// 处理键盘按下事件
const handleKeyDown = (event: KeyboardEvent) => {
// 获取当前时间
const currentTime = Date.now();
// 如果输入速度非常快小于100ms认为是扫码枪输入
if (currentTime - lastInputTime < 100 && formData.inputMaterialCode) {
// 扫码枪通常会以回车键结束,这里不做处理
} else if (event.key.length === 1 && !event.ctrlKey && !event.altKey) {
// 如果是新的输入(不是快速连续输入),清空输入框
// 这样可以确保每次扫码都从空输入框开始
formData.inputMaterialCode = '';
} }
getMaterialDetail(query).then(res => {
// 更新上次输入时间
lastInputTime = currentTime;
};
// 带防抖的物料编码变化处理函数
const handleMaterialCodeChange = debounce(originalHandleMaterialCodeChange, 500); // 500ms防抖延迟
// 扫码核验获取物料信息
const fetchMaterialData = async (code: string) => {
getMaterialDetail(code).then(res => {
if (res.code == '0') { if (res.code == '0') {
// 更新表单数据 // 更新表单数据
formData.materialCode = res.data.materialCode formData.id = res.data?.id || "";
formData.materialName = res.data.materialName formData.encoding = res.data?.encoding || "";
formData.materialSpec = res.data.materialSpec formData.materialName = res.data?.materialName || "";
formData.weight = res.data.weight formData.materialSpec = res.data?.materialSpec || "";
// formData.imageUrl = res.data.imageUrl formData.unitWeight = res.data?.unitWeight || 0;
formData.imageUrl = "https://menjing.hzjj.cn/uploadPath/2024/01/20/XMT-Q3_20240120171349A257.jpg" formData.photoUrl = res.data?.photoUrl || "";
// 假设后端返回比对结果 // 假设后端返回比对结果
// formData.matchResult = res.data.matchResult || 'failed' // 这里根据实际接口返回调整 // formData.matchResult = res.data.matchResult || 'failed' // 这里根据实际接口返回调整
formData.matchResult = res.data.matchResult || 'success' // 这里根据实际接口返回调整 formData.matchResult = res.data?.matchResult || 'success' // 这里根据实际接口返回调整
return true return true
} }
}); });
} }
// 处理下一步 // 处理下一步
const handleNext = () => { const handleNext = async () => {
if (activeStep.value < 3) { if (activeStep.value < 3) {
if (activeStep.value === 2) { if (activeStep.value === 2) {
// 当在称重登记页面点击完成时,显示确认弹框 // 当在称重登记页面点击完成时,显示确认弹框
Modal.confirm({ Modal.confirm({
title: '确认完成', title: '确认完成',
content: '确定要完成称重登记吗?', content: '确定要完成称重登记吗?',
onOk: () => { onOk: async () => {
try {
// 准备工作订单数据
const workOrderData = {
materialId: formData.id,
materialName: formData.materialName,
workOrderInfos: weighingList.value
}
// 调用后端接口
addWorkOrder(workOrderData).then(res => {
if (res.code == '0') {
Notification.success({
title: '操作成功',
content: `工单创建成功!`
})
workOrderResp.value.matchResult = 'success';
workOrderResp.value.title = res.data?.title || "";
workOrderResp.value.orderNo = res.data?.orderNo || "";
workOrderResp.value.totalWeight = res.data?.totalWeight || "";
workOrderResp.value.totalCount = res.data?.totalCount || "";
// 关闭WebSocket连接
closeWebSocket()
// 跳转到完成页面
activeStep.value++ activeStep.value++
return true
}
});
} catch (error) {
console.error('创建工作订单失败:', error)
Message.error('创建工作订单失败')
}
} }
}) })
} else { } else {
activeStep.value++ activeStep.value++
// 进入称重页面时建立WebSocket连接
if (activeStep.value === 2) {
establishWebSocket()
}
} }
} }
} }
@@ -357,40 +762,131 @@ const handleNext = () => {
const handlePrevious = () => { const handlePrevious = () => {
if (activeStep.value > 1) { if (activeStep.value > 1) {
activeStep.value-- activeStep.value--
// 离开称重页面时关闭WebSocket连接
if (activeStep.value !== 2) {
closeWebSocket()
}
} }
} }
// 返回第一步 // 返回第一步
const handleBackToFirst = () => { const handleBackToFirst = () => {
activeStep.value = 1 activeStep.value = 1
// 离开称重页面时关闭WebSocket连接
closeWebSocket()
}
// 计算重量
const calculateWeight = () => {
if (inputQuantity.value && formData.unitWeight) {
const singleWeight = parseFloat(formData.unitWeight.toString())
const quantity = parseFloat(inputQuantity.value)
if (!isNaN(singleWeight) && !isNaN(quantity)) {
calculatedWeight.value = (singleWeight * quantity).toFixed(2)
}
} else {
calculatedWeight.value = ''
}
} }
// 处理确定 // 处理确定
const handleConfirm = () => { const handleConfirm = () => {
// 校验输入数量是否为空
if (!inputQuantity.value || inputQuantity.value.trim() === '') {
Message.error('请输入数量')
return
}
if (!ahDeviceWeight.value || ahDeviceWeight.value.trim() === '') {
Message.error('电子秤称重结果为空!')
return
}
// 生成新的列表数据 // 生成新的列表数据
const newItem = { const newItem = {
key: (weighingList.value.length + 1).toString(), key: (weighingList.value.length + 1).toString(),
count: weighingCount.value, weightTime: weighingCount.value,
name: formData.materialName, materialId: formData.id,
quantity: inputQuantity.value, quantity: inputQuantity.value,
weight: inputWeight.value, weight: ahDeviceWeight.value,
image: '图片' calculatedWeight: calculatedWeight.value,
image: captureUrl || '未抓拍',
} }
// 添加到列表 // 添加到列表
weighingList.value.push(newItem) weighingList.value.push(newItem)
// 重置输入 // 重置输入
inputQuantity.value = '' inputQuantity.value = ''
inputWeight.value = '' ahDeviceWeight.value = '10'
calculatedWeight.value = ''
// 称重次数累加 // 称重次数累加
weighingCount.value = weighingList.value.length + 1 weighingCount.value = weighingList.value.length + 1
Message.success('添加成功')
}
// 抓拍功能 - 内部方法,不暴露按钮
const capturePhoto = async (): Promise<string> => {
return new Promise(async (resolve, reject) => {
try {
const video = cameraVideo.value
if (!video) {
reject(new Error('视频未初始化'))
return
}
// 创建canvas抓取当前帧
const canvas = document.createElement('canvas')
canvas.width = video.videoWidth || 640
canvas.height = video.videoHeight || 480
const ctx = canvas.getContext('2d')
ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
// 转换为Blob
const blob = await new Promise<Blob>((resolve) => {
canvas.toBlob((b) => resolve(b), 'image/jpeg', 0.9)
})
// 创建FormData
const formData = new FormData()
formData.append('file', blob, `capture_${Date.now()}.jpg`)
// 调用后端接口
const response = await catchPhoto(formData)
if (!response.code === '200') {
reject(new Error('上传失败'))
return
}
const imageUrl = await response.data
resolve(imageUrl)
} catch (error) {
reject(error)
}
})
}
// 图片预览函数
const previewImage = (url: string) => {
// 使用Arco Design的图片预览组件
Modal.info({
title: '图片预览',
content: () => h('div', { style: 'text-align: center;' }, [
h('img', {
src: url,
style: 'max-width: 100%; max-height: 500px; object-fit: contain;',
}),
]),
okText: '关闭',
hideCancel: true,
width: 'auto',
})
} }
// 处理重置 // 处理重置
const handleReset = () => { const handleReset = () => {
inputQuantity.value = '' inputQuantity.value = ''
inputWeight.value = '' ahDeviceWeight.value = ''
calculatedWeight.value = ''
} }
// 处理删除 // 处理删除
@@ -399,13 +895,61 @@ const handleDelete = (key) => {
weighingList.value = weighingList.value.filter(item => item.key !== key) weighingList.value = weighingList.value.filter(item => item.key !== key)
// 重新排序称重次数 // 重新排序称重次数
weighingList.value.forEach((item, index) => { weighingList.value.forEach((item, index) => {
item.count = index + 1 item.weightTime = index + 1
}) })
// 更新当前称重次数 // 更新当前称重次数
weighingCount.value = weighingList.value.length + 1 weighingCount.value = weighingList.value.length + 1
} }
defineOptions({ name: 'WeightManage' }) // 建立WebSocket连接
const establishWebSocket = () => {
try {
// 这里替换为实际的WebSocket服务器地址
const wsUrl = 'ws://localhost:6609/ws/scale'
ws.value = new WebSocket(wsUrl)
ws.value.onopen = () => {
Message.success('已和电子秤建立连接')
}
ws.value.onmessage = (event) => {
try {
if (event.data) {
ahDeviceWeight.value = event.data
}
} catch (error) {
console.error('WebSocket消息解析失败:', error)
}
}
ws.value.onclose = () => {
wsConnected.value = false
}
ws.value.onerror = (error) => {
console.error('WebSocket错误:', error)
wsConnected.value = false
Message.error('称重连接失败')
}
} catch (error) {
console.error('建立WebSocket连接失败:', error)
Message.error('无法建立称重连接')
}
}
// 关闭WebSocket连接
const closeWebSocket = () => {
if (ws.value) {
ws.value.close()
ws.value = null
wsConnected.value = false
}
}
// 组件卸载时关闭WebSocket连接
onUnmounted(() => {
closeWebSocket()
})
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@@ -533,10 +1077,68 @@ defineOptions({ name: 'WeightManage' })
font-weight: bold; font-weight: bold;
} }
/* 视频容器 - 替换原来的image-placeholder */
.video-container {
position: relative;
width: 100%;
height: 240px;
background: #000;
border-radius: 4px;
overflow: hidden;
margin: 20px 0;
}
.video-container video {
width: 100%;
height: 100%;
object-fit: cover;
}
.video-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #fff;
gap: 10px;
}
.video-overlay.error {
background: rgba(255, 77, 79, 0.2);
}
.camera-badge {
position: absolute;
top: 10px;
right: 10px;
z-index: 10;
}
.live-badge {
background: #ff4d4f;
color: white;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.7; }
100% { opacity: 1; }
}
.large-image { .large-image {
width: 100%; width: 100%;
height: 200px; height: 240px;
margin: 20px 0;
} }
.input-placeholder { .input-placeholder {

View File

@@ -0,0 +1,284 @@
<template>
<div class="gi_table_page">
<GiTable
row-key="id"
:data="dataList"
:columns="columns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1600 }"
:pagination="pagination"
:disabled-tools="['size']"
:disabled-column-keys="['name']"
:selected-keys="selectedKeys"
:row-selection="{ type: 'checkbox', showCheckedAll: true }"
@select-all="selectAll"
@select="select"
@refresh="search"
>
<template #toolbar-left>
<a-input-search v-model="queryForm.userName" placeholder="请输入姓名" allow-clear @search="search" />
<a-input-search v-model="queryForm.carNo" placeholder="请输入人员卡号" allow-clear @search="search" />
<a-input-search v-model="queryForm.orderNo" placeholder="请输入任务工单号" allow-clear @search="search" />
<a-input-search v-model="queryForm.materialName" placeholder="请输入物料名称" allow-clear @search="search" />
<a-input-search v-model="queryForm.encoding" placeholder="请输入物料编码" allow-clear @search="search" />
<a-date-picker
v-model="queryForm.startDate"
placeholder="请选择开始时间"
show-time
format="YYYY-MM-DD HH:mm:ss"
style="height: 32px"
@change="search"
/>
<a-date-picker
v-model="queryForm.endDate"
placeholder="请选择结束时间"
show-time
format="YYYY-MM-DD HH:mm:ss"
style="height: 32px"
@change="search"
/>
<a-button @click="reset">
<template #icon><icon-refresh /></template>
<template #default>重置</template>
</a-button>
</template>
<template #toolbar-right>
<a-button v-permission="['workOrder:record:delete']" type="outline" status="danger" @click="onDelete">
<template #icon><icon-delete /></template>
<template #default>删除</template>
</a-button>
<a-button v-permission="['workOrder:record:export']" @click="onExport">
<template #icon><icon-download /></template>
<template #default>导出</template>
</a-button>
</template>
<template #photoUrl="{ record }">
<a-image :src="record.photoUrl" width="55" />
</template>
<template #unitWeight="{ record }">
{{ record.unitWeight + 'g' }}
</template>
<template #totalWeight="{ record }">
{{ record.totalWeight + 'g' }}
</template>
<template #action="{ record }">
<a-space>
<a-link v-permission="['workOrder:record:detail']" title="详情" @click="onDetail(record)">详情</a-link>
<a-link v-permission="['workOrder:record:print']" title="打印" @click="onPrint(record)">打印</a-link>
<a-link v-permission="['workOrder:record:delete']" status="danger" :title="'删除'" @click="onDeleteOne(record.id)">删除</a-link>
</a-space>
</template>
</GiTable>
<!-- 详情弹窗 -->
<a-modal
v-model:visible="detailModalVisible"
title="称重详情"
width="800px"
:loading="detailLoading"
>
<GiTable
v-if="detailData.length > 0"
row-key="id"
:data="detailData"
:columns="detailColumns"
:scroll="{ x: '100%', y: '100%', minWidth: 700 }"
>
<template #imgUrl="{ record }">
<a-image :src="record.imgUrl" width="50" />
</template>
<template #calculatedWeight="{ record }">
{{ record.calculatedWeight + 'g' }}
</template>
<template #weight="{ record }">
{{ record.weight + 'g' }}
</template>
</GiTable>
<div v-else class="no-data">
<icon-loading style="font-size: 48px; color: rgb(var(--arcoblue-6));" />
<p>暂无称重数据</p>
</div>
</a-modal>
</div>
</template>
<script setup lang="ts">
import type { TableInstanceColumns } from '@/components/GiTable/type'
import { useDownload, useTable } from '@/hooks'
import { isMobile } from '@/utils'
import has from '@/utils/has'
import type GiTable from "@/components/GiTable/index.vue";
import {ref, reactive} from "vue";
import {Message} from "@arco-design/web-vue";
import { useRouter } from 'vue-router';
import {
deleteWorkOrder,
exportWorkOrder, getWorkOrder,
listWorkOrder,
type WorkOrderQuery,
type WorkOrderResp
} from "@/apis/workOrder/workOrder";
defineOptions({ name: 'Record' })
const router = useRouter()
const queryForm = reactive<WorkOrderQuery>({
orderNo: undefined,
materialName: undefined,
encoding: undefined,
userName: undefined,
carNo: undefined,
startDate: undefined,
endDate: undefined,
sort: ['w.id,desc']
})
const {
tableData: dataList,
loading,
pagination,
selectedKeys,
select,
selectAll,
search,
handleDelete,
} = useTable((page) => listWorkOrder({ ...queryForm, ...page }), { immediate: true })
// 创建工具函数统一处理列配置
const processColumns = (columns: TableInstanceColumns[]): TableInstanceColumns[] => {
return columns.map(column => {
const defaultConfig = {
ellipsis: true,
tooltip: true
};
return { ...defaultConfig, ...column };
});
};
// 定义列配置,使用工具函数处理
const columns = ref<TableInstanceColumns[]>(processColumns([
{ title: '标题', dataIndex: 'title', width: 180 },
{ title: '创建人', dataIndex: 'createUserString' },
{ title: '人员卡号', dataIndex: 'cardNo' },
{ title: '任务工单号', dataIndex: 'orderNo' },
{ title: '物料图片', dataIndex: 'photoUrl', slotName: 'photoUrl', minWidth: 180, ellipsis: true, tooltip: true },
{ title: '物料名称', dataIndex: 'materialName' },
{ title: '物料编码', dataIndex: 'encoding' },
{ title: '单位克重', dataIndex: 'unitWeight' ,slotName: 'unitWeight'},
{ title: '总重量', dataIndex: 'totalWeight' ,slotName: 'totalWeight'},
{ title: '称重次数', dataIndex: 'totalCount' },
{ title: '创建时间', dataIndex: 'createTime', width: 180 },
{
title: '操作',
dataIndex: 'action',
slotName: 'action',
width: 160,
align: 'center',
fixed: !isMobile() ? 'right' : undefined,
show: has.hasPermOr(['workOrder:record:detail', 'workOrder:record:update', 'workOrder:record:delete'])
}
]))
// 重置
const reset = () => {
queryForm.orderNo = undefined
queryForm.materialName = undefined
queryForm.encoding = undefined
queryForm.userName = undefined
queryForm.carNo = undefined
queryForm.startDate = undefined
queryForm.endDate = undefined
search()
}
// 详情弹窗状态
const detailModalVisible = ref(false)
const detailLoading = ref(false)
const detailData = ref<any[]>([])
// 详情列配置
const detailColumns = ref<TableInstanceColumns[]>([
{ title: '称重次数', dataIndex: 'weightTime' },
{ title: '物料名称', dataIndex: 'materialName' },
{ title: '数量', dataIndex: 'quantity' },
{ title: '重量', dataIndex: 'weight', slotName: 'weight' },
{ title: '计算重量', dataIndex: 'calculatedWeight', slotName: 'calculatedWeight' },
{ title: '抓拍图片', dataIndex: 'imgUrl', slotName: 'imgUrl' }
])
// 详情
const onDetail = async (record: WorkOrderResp) => {
detailLoading.value = true
detailModalVisible.value = true
detailData.value = []
getWorkOrder(record.id).then(res => {
if (res.code == '0') {
detailData.value = res.data;
detailLoading.value = false
} else {
Message.error('获取详情失败')
}
});
};
// 删除
const onDeleteOne = (id) => {
return handleDelete(() => deleteWorkOrder(id), {
content: `是否确定删除该条数据?`,
showModal: true
})
}
// 删除
const onDelete = () => {
if (!selectedKeys.value.length) {
return Message.warning('请选择数据')
}
return handleDelete(() => deleteWorkOrder(selectedKeys.value), {
content: `是否确定删除选中的 ${selectedKeys.value.length} 条数据?`,
showModal: true
})
}
// 导出
const onExport = () => {
useDownload(() => exportWorkOrder(queryForm))
}
// 打印
const onPrint = (record: WorkOrderResp) => {
// 跳转到标签打印页面,并传递数据
router.push({
path: '/print',
query: {
materialName: record.materialName,
encoding: record.encoding,
orderNo: record.orderNo
}
})
}
</script>
<style scoped lang="scss">
.no-data {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 0;
color: var(--color-text-4);
p {
margin-top: 16px;
font-size: 14px;
}
}
</style>