Compare commits

...

36 Commits

Author SHA1 Message Date
df67f63137 优化 2026-03-13 09:41:00 +08:00
zc
0f7efec0e1 优化 2026-03-12 17:18:56 +08:00
zc
2171c83d55 优化 2026-03-12 16:30:30 +08:00
zc
db40b1733b 优化 2026-03-12 16:12:30 +08:00
zc
fe56422336 优化 2026-03-12 15:41:52 +08:00
zc
dc395406c3 优化 2026-03-11 18:38:45 +08:00
zc
41db731e40 优化视频样式 2026-03-11 17:40:24 +08:00
zc
35fcd40bd3 优化视频流 2026-03-11 11:09:19 +08:00
zc
e27e44a3c3 优化 2026-03-11 10:56:33 +08:00
zc
47463c9b14 优化 2026-03-09 20:35:02 +08:00
zc
3f8b6e4695 优化 2026-03-09 20:34:20 +08:00
2bf7b6872a 抓取优化 2026-03-09 16:22:17 +08:00
zc
a3cb2263fd 优化 2026-03-09 16:06:00 +08:00
zc
d8273464c5 优化 2026-03-09 15:24:02 +08:00
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
7058245cdf 物料新增批量导入照片按钮 2026-03-05 14:48:32 +08:00
7f5ab90f22 优化 2026-03-04 18:04:37 +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
124de2926e 优化 2026-03-03 15:05:20 +08:00
7785356347 物料新增+用户修改 2026-03-03 11:37:26 +08:00
zc
cbd73d4312 称重页面 2026-02-28 16:54:47 +08:00
zc
3f63ae1d50 删除 2026-02-27 15:53:39 +08:00
d00c90d557 Merge remote-tracking branch 'origin/master' 2026-02-27 11:38:52 +08:00
2b49a59174 删除 2026-02-27 11:38:33 +08:00
44 changed files with 3586 additions and 1666 deletions

View File

@@ -1,4 +1,5 @@
import components from 'unplugin-vue-components/vite' import components from 'unplugin-vue-components/vite'
import { ArcoResolver } from 'unplugin-vue-components/resolvers'
export default function createComponents() { export default function createComponents() {
return components({ return components({
@@ -7,5 +8,11 @@ export default function createComponents() {
extensions: ['vue', 'tsx'], extensions: ['vue', 'tsx'],
// 配置文件生成位置 // 配置文件生成位置
dts: './src/types/components.d.ts', dts: './src/types/components.d.ts',
// 自动导入Arco Design组件
resolvers: [
ArcoResolver({
sideEffect: true,
}),
],
}) })
} }

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",
"dev": 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

@@ -0,0 +1,89 @@
import http from '@/utils/http'
const BASE_URL = '/admin/materialInfo'
export interface MaterialInfoResp {
materialName: string
encoding: string
unitWeight: string
materialSpec: string
photoUrl: string
createUser: string
createTime: string
createUserString: string
updateUserString: string
disabled: boolean
}
export interface MaterialInfoQuery {
materialName: string | undefined
encoding: string | undefined
sort: Array<string>
}
/* 物料信息导入结果类型 */
export interface MaterialImportResp {
importKey: string
totalRows: number
validRows: number
duplicateNameRows: number
duplicateCodeRows: number
}
export interface MaterialInfoPageQuery extends MaterialInfoQuery, PageQuery {}
/** @desc 查询物料信息列表 */
export function listMaterialInfo(query: MaterialInfoPageQuery) {
return http.get<PageRes<MaterialInfoResp[]>>(`${BASE_URL}`, query)
}
interface MaterialInfoDetailResp {
}
/** @desc 下载物料信息导入模板 */
export function downloadMaterialInfoImportTemplate() {
return http.download(`${BASE_URL}/import/template`)
}
/** @desc 查询物料信息详情 */
export function getMaterialInfo(id: string) {
return http.get<MaterialInfoDetailResp>(`${BASE_URL}/${id}`)
}
/** @desc 新增物料信息 */
export function addMaterialInfo(data: any) {
return http.post(`${BASE_URL}`, data)
}
/** @desc 修改物料信息 */
export function updateMaterialInfo(data: any, id: string) {
return http.put(`${BASE_URL}/${id}`, data)
}
/** @desc 删除物料信息 */
export function deleteMaterialInfo(id: string) {
return http.del(`${BASE_URL}/${id}`)
}
/** @desc 导出物料信息 */
export function exportMaterialInfo(query: MaterialInfoQuery) {
return http.download(`${BASE_URL}/export`, query)
}
/** @desc 解析物料信息导入数据 */
export function parseImportMaterial(data: FormData) {
return http.post(`${BASE_URL}/import/parse`, data)
}
/** @desc 导入物料信息 */
export function importMaterial(data: any) {
return http.post(`${BASE_URL}/import`, data)
}
/** @desc 物料照片批量导入数据 */
export function uploadMaterialPhotos(data: FormData) {
return http.post(`${BASE_URL}/import/uploadMaterialPhotos`, data)
}
/** @desc 物料照片抓取数据 */
export function catchPhoto(data: FormData) {
return http.post(`${BASE_URL}/import/catch`, data)
}

View File

@@ -2,10 +2,8 @@ export * from './user'
export * from './role' export * from './role'
export * from './menu' export * from './menu'
export * from './dept' export * from './dept'
export * from './notice'
export * from './dict' export * from './dict'
export * from './file' export * from './file'
export * from './storage' export * from './storage'
export * from './option' export * from './option'
export * from './user-center' export * from './user-center'
export * from './message'

View File

@@ -1,26 +0,0 @@
import type * as T from './type'
import http from '@/utils/http'
export type * from './type'
const BASE_URL = '/system/message'
/** @desc 查询消息列表 */
export function listMessage(query: T.MessagePageQuery) {
return http.get<PageRes<T.MessageResp[]>>(`${BASE_URL}`, query)
}
/** @desc 删除消息 */
export function deleteMessage(ids: string | Array<string>) {
return http.del(`${BASE_URL}/${ids}`)
}
/** @desc 标记已读 */
export function readMessage(ids?: string | Array<string>) {
return http.patch(`${BASE_URL}/read`, ids)
}
/** @desc 查询未读消息数量 */
export function getUnreadMessageCount() {
return http.get(`${BASE_URL}/unread`)
}

View File

@@ -1,31 +0,0 @@
import type * as T from './type'
import http from '@/utils/http'
export type * from './type'
const BASE_URL = '/system/notice'
/** @desc 查询公告列表 */
export function listNotice(query: T.NoticePageQuery) {
return http.get<PageRes<T.NoticeResp[]>>(`${BASE_URL}`, query)
}
/** @desc 查询公告详情 */
export function getNotice(id: string) {
return http.get<T.NoticeResp>(`${BASE_URL}/${id}`)
}
/** @desc 新增公告 */
export function addNotice(data: any) {
return http.post(BASE_URL, data)
}
/** @desc 修改公告 */
export function updateNotice(data: any, id: string) {
return http.put(`${BASE_URL}/${id}`, data)
}
/** @desc 删除公告 */
export function deleteNotice(ids: string | Array<number>) {
return http.del(`${BASE_URL}/${ids}`)
}

View File

@@ -0,0 +1,37 @@
import http from '@/utils/http'
const BASE_URL = '/weighManage/workOrder'
export interface WeighManageResp {
id: string
encoding: string
materialName: string
materialSpec: string
unitWeight: number
photoUrl: string
matchResult: string
}
export interface WeighManageQuery {
encoding: string
}
/** @desc 查询物料信息 */
export function getMaterialDetail(code: string) {
return http.get<WeighManageResp>(`/admin/materialInfo/code/${code}`)
}
/** @desc 校验称重信息 */
export function validateWeighing(data: any) {
return http.post(`${BASE_URL}/validateWeighing`, data)
}
/** @desc 校验称重信息 */
export function vmSend(code: string) {
return http.get<WeighManageResp>(`/vm/send?msg=${code}`)
}
/** @desc 获取图片 */
export function getImg() {
return http.get<string>(`/vm/latest-photo`)
}

View File

@@ -0,0 +1,75 @@
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
totalCalculatedWeight: string
totalCount: string
createUserString: string
updateUserString: string
matchResult: string
workOrderInfos: Array<WorkOrderInfoResp>
qrCodeData: 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 getWorkOrderInfos(id: string) {
return http.get<Array<WorkOrderInfoResp>>(`${BASE_URL}/info/${id}`)
}
/** @desc 查询工作订单详情 */
export function getWorkOrder(id: string) {
return http.get<WorkOrderResp>(`${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

@@ -1,23 +0,0 @@
import { ref } from 'vue'
import type { EquipmentQuery } from '@/apis/equipment/equipment'
import { getEquipmentNameList } from '@/apis/equipment/equipment'
import type { LabelValueState } from '@/types/global'
/** 设备名称列表 */
export function equipmentName(query: EquipmentQuery, options?: { onSuccess?: () => void }) {
const loading = ref(false)
const equipmentNameList = ref<LabelValueState[]>([])
const getEquipmentName = async () => {
try {
loading.value = true
const res = await getEquipmentNameList(query)
equipmentNameList.value = res.data
// eslint-disable-next-line ts/no-unused-expressions
options?.onSuccess && options.onSuccess()
} finally {
loading.value = false
}
}
return { equipmentNameList, getEquipmentName, loading }
}

View File

@@ -1,9 +1,4 @@
export * from './useMenu' export * from './useMenu'
export * from './useDept' export * from './useDept'
export * from './useBranch'
export * from './useRole' export * from './useRole'
export * from './useDict' export * from './useDict'
export * from './space'
export * from './point'
export * from './equipmentName'
export * from './productName'

View File

@@ -1,22 +0,0 @@
import { ref } from 'vue'
import type { LabelValueState } from '@/types/global'
import { type PeopleQuery, getPeopleNameList } from '@/apis/sysPeople/people'
/** 设备名称列表 */
export function peopleName(query: PeopleQuery, options?: { onSuccess?: () => void }) {
const loading = ref(false)
const peopleNameList = ref<LabelValueState[]>([])
const getPeopleName = async () => {
try {
loading.value = true
const res = await getPeopleNameList(query)
peopleNameList.value = res.data
// eslint-disable-next-line ts/no-unused-expressions
options?.onSuccess && options.onSuccess()
} finally {
loading.value = false
}
}
return { peopleNameList, getPeopleName, loading }
}

View File

@@ -1,22 +0,0 @@
import { ref } from 'vue'
import type { TreeNodeData } from '@arco-design/web-vue'
import { listPointTree } from '@/apis'
/** 部门模块 */
export function point(options?: { onSuccess?: () => void }) {
const loading = ref(false)
const pointList = ref<TreeNodeData[]>([])
const getPointList = async (name?: string) => {
try {
loading.value = true
const res = await listPointTree({ description: name })
pointList.value = res.data
// eslint-disable-next-line ts/no-unused-expressions
options?.onSuccess && options.onSuccess()
} finally {
loading.value = false
}
}
return { pointList, getPointList, loading }
}

View File

@@ -1,22 +0,0 @@
import { ref } from 'vue'
import { getProductNameList } from '@/apis/equipment/product'
import type { LabelValueState } from '@/types/global'
/** 设备名称列表 */
export function productName(options?: { onSuccess?: () => void }) {
const loading = ref(false)
const productNameList = ref<LabelValueState[]>([])
const getProductName = async () => {
try {
loading.value = true
const res = await getProductNameList()
productNameList.value = res.data
// eslint-disable-next-line ts/no-unused-expressions
options?.onSuccess && options.onSuccess()
} finally {
loading.value = false
}
}
return { productNameList, getProductName, loading }
}

View File

@@ -1,22 +0,0 @@
import { ref } from 'vue'
import type { LabelValueState } from '@/types/global'
import { getRuleNameList } from '@/apis/rule/rule'
/** 设备名称列表 */
export function ruleName(options?: { onSuccess?: () => void }) {
const loading = ref(false)
const ruleNameList = ref<LabelValueState[]>([])
const getRuleName = async () => {
try {
loading.value = true
const res = await getRuleNameList()
ruleNameList.value = res.data
// eslint-disable-next-line ts/no-unused-expressions
options?.onSuccess && options.onSuccess()
} finally {
loading.value = false
}
}
return { ruleNameList, getRuleName, loading }
}

View File

@@ -1,22 +0,0 @@
import { ref } from 'vue'
import type { TreeNodeData } from '@arco-design/web-vue'
import { listSpaceTree } from '@/apis'
/** 部门模块 */
export function space(options?: { onSuccess?: () => void }) {
const loading = ref(false)
const spaceList = ref<TreeNodeData[]>([])
const getSpaceList = async (name?: string) => {
try {
loading.value = true
const res = await listSpaceTree({ description: name })
spaceList.value = res.data
// eslint-disable-next-line ts/no-unused-expressions
options?.onSuccess && options.onSuccess()
} finally {
loading.value = false
}
}
return { spaceList, getSpaceList, loading }
}

View File

@@ -1,22 +0,0 @@
import { ref } from 'vue'
import type { TreeNodeData } from '@arco-design/web-vue'
import { listBranchTree } from '@/apis'
/** 部门模块 */
export function useBranch(options?: { onSuccess?: () => void }) {
const loading = ref(false)
const branchList = ref<TreeNodeData[]>([])
const getBranchList = async (name?: string) => {
try {
loading.value = true
const res = await listBranchTree({ description: name })
branchList.value = res.data
// eslint-disable-next-line ts/no-unused-expressions
options?.onSuccess && options.onSuccess()
} finally {
loading.value = false
}
}
return { branchList, getBranchList, loading }
}

View File

@@ -1,124 +0,0 @@
<template>
<div class="message">
<a-list :loading="loading">
<template #header>通知</template>
<a-list-item v-for="item in messageList" :key="item.id">
<div class="content-wrapper" @click="open">
<div class="content">{{ item.title }}</div>
<div class="date">{{ item.createTime }}</div>
</div>
</a-list-item>
<template #footer>
<a class="more-btn" @click="open">查看更多
<icon-right />
</a>
<a class="read-all-btn" @click="readAll">全部已读</a>
</template>
</a-list>
</div>
</template>
<script setup lang="ts">
import { onMounted } from 'vue'
import { type MessageResp, listMessage, readMessage } from '@/apis'
const emit = defineEmits<{
(e: 'readall-success'): void
}>()
const queryParam = reactive({
isRead: false,
sort: ['createTime,desc'],
page: 1,
size: 5,
})
const messageList = ref<MessageResp[]>()
const loading = ref(false)
// 查询消息数据
const getMessageData = async () => {
try {
loading.value = true
const { data } = await listMessage(queryParam)
messageList.value = data.list
} finally {
loading.value = false
}
}
// 打开消息中心
const open = () => {
window.open('/setting/message')
}
// 全部已读
const readAll = async () => {
await readMessage()
await getMessageData()
emit('readall-success')
}
onMounted(() => {
getMessageData()
})
</script>
<style scoped lang="scss">
.message {
height: auto;
max-height: calc(100% - 51px);
width: 300px;
.content-wrapper {
padding: 10px;
border-radius: var(--border-radius-medium);
cursor: pointer;
.content {
font-size: 12px;
height: 20px;
max-width: 265px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 265px;
}
.date {
color: var(--color-text-4);
font-size: 12px;
margin-top: 4px;
}
&:hover {
background-color: var(--color-bg-4);
color: rgb(var(--arcoblue-6));
}
}
:deep(.arco-list) {
border-radius: var(--border-radius-medium);
.arco-list-header {
font-size: 13px;
padding: 9px 12px;
}
.arco-list-content {
max-height: 184px;
.arco-list-item {
padding: 6px;
}
}
.arco-list-footer {
font-size: 12px;
color: rgb(var(--arcoblue-6));
padding: 9px 12px;
display: flex;
.more-btn {
margin-right: auto;
}
}
}
}
</style>

View File

@@ -26,9 +26,6 @@
</template> </template>
</a-button> </a-button>
</a-badge> </a-badge>
<template #content>
<Message @readall-success="getMessageCount" />
</template>
</a-popover> </a-popover>
<!-- 全屏切换组件 --> <!-- 全屏切换组件 -->
@@ -74,12 +71,9 @@
import { Modal } from '@arco-design/web-vue' import { Modal } from '@arco-design/web-vue'
import { useFullscreen } from '@vueuse/core' import { useFullscreen } from '@vueuse/core'
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
import Message from './Message.vue'
import SettingDrawer from './SettingDrawer.vue' import SettingDrawer from './SettingDrawer.vue'
import Search from './Search.vue' import Search from './Search.vue'
import { getUnreadMessageCount } from '@/apis'
import { useUserStore } from '@/stores' import { useUserStore } from '@/stores'
import { getToken } from '@/utils/auth'
import { useBreakpoint } from '@/hooks' import { useBreakpoint } from '@/hooks'
defineOptions({ name: 'HeaderRight' }) defineOptions({ name: 'HeaderRight' })
@@ -113,15 +107,6 @@ const initWebSocket = (token: string) => {
} }
} }
// 查询未读消息数量
const getMessageCount = async () => {
const { data } = await getUnreadMessageCount()
unreadMessageCount.value = data.total
const token = getToken()
if (token) {
initWebSocket(token)
}
}
const { isFullscreen, toggle } = useFullscreen() const { isFullscreen, toggle } = useFullscreen()
@@ -148,9 +133,6 @@ const logout = () => {
}) })
} }
onMounted(() => {
getMessageCount()
})
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -28,7 +28,7 @@ export const systemRoutes: RouteRecordRaw[] = [
path: '/dashboard/analysis', path: '/dashboard/analysis',
name: 'Analysis', name: 'Analysis',
component: () => import('@/views/dashboard/analysis/index.vue'), component: () => import('@/views/dashboard/analysis/index.vue'),
meta: { title: '分析页', icon: 'insert-chart', hidden: false, affix: true }, meta: { title: '页', icon: 'insert-chart', hidden: false, affix: true },
}, },
], ],
}, },
@@ -54,12 +54,6 @@ export const systemRoutes: RouteRecordRaw[] = [
component: () => import('@/views/setting/profile/index.vue'), component: () => import('@/views/setting/profile/index.vue'),
meta: { title: '个人中心', showInTabs: false }, meta: { title: '个人中心', showInTabs: false },
}, },
{
path: '/setting/message',
name: 'SettingMessage',
component: () => import('@/views/setting/message/index.vue'),
meta: { title: '消息中心', showInTabs: false },
},
], ],
}, },
] ]

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

@@ -7,7 +7,65 @@ export {}
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
AAlert: typeof import('@arco-design/web-vue')['Alert']
AAvatar: typeof import('@arco-design/web-vue')['Avatar']
ABadge: typeof import('@arco-design/web-vue')['Badge']
ABreadcrumb: typeof import('@arco-design/web-vue')['Breadcrumb']
ABreadcrumbItem: typeof import('@arco-design/web-vue')['BreadcrumbItem']
AButton: typeof import('@arco-design/web-vue')['Button']
ACheckbox: typeof import('@arco-design/web-vue')['Checkbox']
ACol: typeof import('@arco-design/web-vue')['Col']
AConfigProvider: typeof import('@arco-design/web-vue')['ConfigProvider']
ADatePicker: typeof import('@arco-design/web-vue')['DatePicker']
ADescriptions: typeof import('@arco-design/web-vue')['Descriptions']
ADescriptionsItem: typeof import('@arco-design/web-vue')['DescriptionsItem']
ADivider: typeof import('@arco-design/web-vue')['Divider']
ADoption: typeof import('@arco-design/web-vue')['Doption']
ADrawer: typeof import('@arco-design/web-vue')['Drawer']
ADropdown: typeof import('@arco-design/web-vue')['Dropdown']
AEmpty: typeof import('@arco-design/web-vue')['Empty']
AForm: typeof import('@arco-design/web-vue')['Form']
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']
AImage: typeof import('@arco-design/web-vue')['Image']
AInput: typeof import('@arco-design/web-vue')['Input']
AInputNumber: typeof import('@arco-design/web-vue')['InputNumber']
AInputPassword: typeof import('@arco-design/web-vue')['InputPassword']
AInputSearch: typeof import('@arco-design/web-vue')['InputSearch']
ALayout: typeof import('@arco-design/web-vue')['Layout']
ALayoutHeader: typeof import('@arco-design/web-vue')['LayoutHeader']
ALayoutSider: typeof import('@arco-design/web-vue')['LayoutSider']
ALink: typeof import('@arco-design/web-vue')['Link']
AMenu: typeof import('@arco-design/web-vue')['Menu']
AMenuItem: typeof import('@arco-design/web-vue')['MenuItem']
AModal: typeof import('@arco-design/web-vue')['Modal']
AOverflowList: typeof import('@arco-design/web-vue')['OverflowList']
APagination: typeof import('@arco-design/web-vue')['Pagination']
APopover: typeof import('@arco-design/web-vue')['Popover']
AProgress: typeof import('@arco-design/web-vue')['Progress']
ARadio: typeof import('@arco-design/web-vue')['Radio']
ARadioGroup: typeof import('@arco-design/web-vue')['RadioGroup']
ARangePicker: typeof import('@arco-design/web-vue')['RangePicker']
ARow: typeof import('@arco-design/web-vue')['Row']
AScrollbar: typeof import('@arco-design/web-vue')['Scrollbar']
ASelect: typeof import('@arco-design/web-vue')['Select']
ASpace: typeof import('@arco-design/web-vue')['Space']
ASpin: typeof import('@arco-design/web-vue')['Spin']
AStatistic: typeof import('@arco-design/web-vue')['Statistic']
ASubMenu: typeof import('@arco-design/web-vue')['SubMenu']
ASwitch: typeof import('@arco-design/web-vue')['Switch']
ATable: typeof import('@arco-design/web-vue')['Table']
ATabPane: typeof import('@arco-design/web-vue')['TabPane']
ATabs: typeof import('@arco-design/web-vue')['Tabs']
ATag: typeof import('@arco-design/web-vue')['Tag']
ATooltip: typeof import('@arco-design/web-vue')['Tooltip']
ATreeSelect: typeof import('@arco-design/web-vue')['TreeSelect']
ATypographyParagraph: typeof import('@arco-design/web-vue')['TypographyParagraph']
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']
Breadcrumb: typeof import('./../components/Breadcrumb/index.vue')['default'] Breadcrumb: typeof import('./../components/Breadcrumb/index.vue')['default']
CellCopy: typeof import('./../components/CellCopy/index.vue')['default'] CellCopy: typeof import('./../components/CellCopy/index.vue')['default']
Chart: typeof import('./../components/Chart/index.vue')['default'] Chart: typeof import('./../components/Chart/index.vue')['default']

View File

@@ -0,0 +1,484 @@
<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="true" />
</a-form-item>
</div>
<div class="form-grid-item">
<a-form-item label="物料编码">
<a-input v-model="formData.encoding" placeholder="未获取到物料编码" :disabled="true" />
</a-form-item>
</div>
<div class="form-grid-item">
<a-form-item label="工单编号">
<a-input v-model="formData.orderNo" placeholder="未获取到工单编号" :disabled="true" />
</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">
<!-- <div 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-row">
<div class="label-field">零件名称</div>
<div class="label-value">{{ labelData.partName }}</div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">生产日期</div>
<div class="label-value">{{ labelData.productionDate }}</div>
</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-row">
<div class="label-field">零件号</div>
<div class="label-value">{{ labelData.partNumber }}</div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">数量</div>
<div class="label-value">{{ labelData.totalCount }}</div>
</div>
</td>
</tr>
<tr>
<td class="label-cell">
<div class="label-row">
<div class="label-field">标重(kg)</div>
<div class="label-value">{{ labelData.totalCalculatedWeight }}</div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">包装签字</div>
<div class="label-value">{{ labelData.packingSignature || '' }}</div>
</div>
</td>
</tr>
<tr>
<td class="label-cell">
<div class="label-row">
<div class="label-field">实重(kg)</div>
<div class="label-value">{{ labelData.totalWeight || '' }}</div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">检验签字</div>
<div class="label-value">{{ labelData.inspectionSignature || '' }}</div>
</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'
import {getWorkOrder} from "@/apis/workOrder/workOrder";
const route = useRoute()
// 表单数据
const formData = reactive({
workerOrderId: '',
encoding: '',
materialName: '',
orderNo: '',
totalCalculatedWeight: '',
totalWeight: '',
totalCount: '',
productionBatch: '',
qrCodeData: '',
})
// 标签数据
const labelData = reactive({
partName: '',
partNumber: '',
totalCalculatedWeight: '',
totalWeight: '',
productionDate: '',
totalCount: '',
packingSignature: '',
inspectionSignature: '',
qrCodeData: '',
})
// 标签数据
// const labelData = reactive({
// partName: '物料3',
// partNumber: '1',
// totalCalculatedWeight: '100',
// totalWeight: '100',
// productionDate: '202401010000',
// totalCount: '100',
// packingSignature: '',
// inspectionSignature: '',
// qrCodeData: '10#$$DY',
// })
// 标签容器引用
const labelContainer = ref<HTMLElement | null>(null)
// 生成标签
const generateLabel = async () => {
if (!formData.productionBatch) {
Message.error('请输入生产批次')
return
}
if (!formData.materialName) {
Message.error('未获取到物料信息')
return
}
try {
// 格式化生产日期为 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 formattedDate2 = now.getFullYear().toString() +
String(now.getMonth() + 1).padStart(2, '0') +
String(now.getDate()).padStart(2, '0');
// 直接从 formData 中获取数据
Object.assign(labelData, {
partName: formData.materialName || '',
partNumber: formData.encoding || '',
totalCalculatedWeight: formData.totalCalculatedWeight || '',
totalWeight: formData.totalWeight || '',
productionDate: formattedDate,
totalCount: formData.totalCount || '',
packingSignature: '',
inspectionSignature: '',
//10#零件号$11#供应商代码$12#生产批次$17#数量$20#包装日期$31#唯一号$DY
qrCodeData: `10#${formData.materialName}$11#9DP$12#${formData.productionBatch}$17#${formData.totalCount}$20#${formattedDate2}$31#${formData.orderNo}$DY`
})
Message.success('标签生成成功')
} catch (error) {
console.error('生成标签失败:', error)
Message.error('生成标签失败')
}
}
// 组件挂载时,检查是否有参数传递
onMounted(() => {
// 从路由参数中获取数据
const workerOrderId = route.query.workerOrderId as string
// 如果有参数传递,设置表单数据并标记为不可修改
if (workerOrderId) {
formData.workerOrderId = workerOrderId
getWorkOrder(workerOrderId).then(res => {
if (res.code == '0') {
formData.encoding = res.data.encoding
formData.materialName = res.data.materialName
formData.orderNo = res.data.orderNo
formData.totalCalculatedWeight = res.data.totalCalculatedWeight
formData.totalWeight = res.data.totalWeight
formData.totalCount = res.data.totalCount
} else {
Message.error('获取详情失败')
}
});
// 自动生成标签
generateLabel()
}
})
// 打印标签
const printLabel = async () => {
await nextTick()
if (labelContainer.value) {
// 创建打印窗口
const printWindow = window.open('', '_blank')
if (printWindow) {
// 直接构建打印内容,确保二维码图片正确生成
const printHTML = `
<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: 38.5mm; 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; border: 1px solid #000; }
.label-row { display: flex; align-items: center; }
.label-field { font-size: 7pt; font-weight: bold; margin-right: 2mm; min-width: 25pt; }
.label-value { font-size: 7pt; flex: 1; }
.qr-code img { width: 20mm; height: 20mm; margin: 1mm 0; }
.serial-number { font-size: 7pt; margin-top: 1mm; }
</style>
</head>
<body>
<div class="label-container">
<div class="label">
<table class="label-table">
<tr>
<td class="label-cell">
<div class="label-row">
<div class="label-field">零件名称</div>
<div class="label-value">${labelData.partName}</div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">生产日期</div>
<div class="label-value">${labelData.productionDate}</div>
</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-row">
<div class="label-field">零件号</div>
<div class="label-value">${labelData.partNumber}</div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">数量</div>
<div class="label-value">${labelData.totalCount}</div>
</div>
</td>
</tr>
<tr>
<td class="label-cell">
<div class="label-row">
<div class="label-field">标重(kg)</div>
<div class="label-value">${labelData.totalCalculatedWeight}</div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">包装签字</div>
<div class="label-value">${labelData.packingSignature || ''}</div>
</div>
</td>
</tr>
<tr>
<td class="label-cell">
<div class="label-row">
<div class="label-field">实重(kg)</div>
<div class="label-value">${labelData.totalWeight || ''}</div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">检验签字</div>
<div class="label-value">${labelData.inspectionSignature || ''}</div>
</div>
</td>
</tr>
</table>
</div>
</div>
</body>
</html>
`
printWindow.document.write(printHTML)
printWindow.document.close()
// 等待图片加载完成后再打印
printWindow.onload = () => {
setTimeout(() => {
printWindow.focus()
printWindow.print()
printWindow.close()
}, 500)
}
}
}
}
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-row {
display: flex;
align-items: center;
}
.label-field {
font-size: 12px;
font-weight: bold;
margin-right: 10px;
min-width: 60px;
}
.label-value {
font-size: 12px;
flex: 1;
}
.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

@@ -5,9 +5,10 @@
<div class="guide-section full-background"> <div class="guide-section full-background">
<div class="guide-content full-height"> <div class="guide-content full-height">
<!-- 标题 --> <!-- 标题 -->
<!-- 注意标题现在不再强制占满全宽而是适应内容 -->
<h1 class="page-title">物料领取流程</h1> <h1 class="page-title">物料领取流程</h1>
<!-- 图片和按钮卡片 - 确保在任何比例下完全显示 --> <!-- 图片和按钮卡片 -->
<div class="guide-card centered"> <div class="guide-card centered">
<!-- 图片展示区 --> <!-- 图片展示区 -->
<div class="image-wrapper"> <div class="image-wrapper">
@@ -36,7 +37,7 @@ defineOptions({ name: 'Analysis' })
const router = useRouter() const router = useRouter()
const handleStart = () => { const handleStart = () => {
router.push('') router.push('/weightManage')
} }
</script> </script>
@@ -66,10 +67,10 @@ const handleStart = () => {
height: 100%; height: 100%;
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
justify-content: center; justify-content: flex-start; // 整体靠左,由子元素 margin 控制位置
align-items: center; align-items: center;
background: #f5f7fa; background: #f5f7fa;
padding: 10px; // 减小内边距 padding: 10px;
box-sizing: border-box; box-sizing: border-box;
.guide-content.full-height { .guide-content.full-height {
@@ -79,10 +80,13 @@ const handleStart = () => {
margin: 0; margin: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-start; // 【关键】让子元素(标题和卡片)从左侧开始排列,而不是拉伸
.page-title { .page-title {
text-align: center; // 【修改】让标题宽度适应内容,以便通过 margin 控制其整体位置
font-size: 42px; // 稍微减小字体 width: fit-content;
text-align: center; // 文字本身在标题块内居中
font-size: 42px;
font-weight: 800; font-weight: 800;
color: #1e293b; color: #1e293b;
margin-bottom: 15px; margin-bottom: 15px;
@@ -92,13 +96,17 @@ const handleStart = () => {
flex-shrink: 0; flex-shrink: 0;
padding: 0 10px; padding: 0 10px;
// 【关键修改】应用与卡片相同的左侧偏移,确保标题在卡片正上方
margin-left: 30%; // 必须与 .guide-card 的 margin-left 一致
margin-right: auto;
&::after { &::after {
content: ''; content: '';
display: block; display: block;
width: 80px; // 减小下划线宽度 width: 80px;
height: 4px; height: 4px;
background: linear-gradient(90deg, #165dff, #6aa1ff); background: linear-gradient(90deg, #165dff, #6aa1ff);
margin: 10px auto 0; margin: 10px auto 0; // 下划线在标题块内居中
border-radius: 2px; border-radius: 2px;
} }
} }
@@ -108,22 +116,23 @@ const handleStart = () => {
.guide-card { .guide-card {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; gap: 40px;
gap: 40px; // 减小间距
background: white; background: white;
border-radius: 30px; // 减小圆角 border-radius: 30px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
padding: 20px 30px; // 减小内边距 padding: 20px 30px;
flex-wrap: wrap; flex-wrap: wrap;
border: 1px solid rgba(0, 0, 0, 0.05); border: 1px solid rgba(0, 0, 0, 0.05);
min-height: 400px; // 减小最小高度 min-height: 400px;
width: auto; width: auto;
max-width: 90vw; // 使用视口宽度的90%,确保有边距 max-width: 90vw;
margin: 0 auto;
// 【核心修改】设置左边距实现左偏移
margin-left: 10%; // 与标题保持一致
margin-right: auto;
&.centered { &.centered {
margin-left: auto; // 移除原本的居中逻辑
margin-right: auto;
} }
.image-wrapper { .image-wrapper {
@@ -131,16 +140,16 @@ const handleStart = () => {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
min-width: 300px; // 减小最小宽度 min-width: 300px;
max-width: 600px; // 减小最大宽度 max-width: 600px;
height: 100%; height: 100%;
.guide-image.enlarged { .guide-image.enlarged {
max-width: 100%; max-width: 100%;
width: auto; width: auto;
height: auto; height: auto;
max-height: 50vh; // 减小最大高度 max-height: 50vh;
min-height: 250px; // 减小最小高度 min-height: 250px;
object-fit: contain; object-fit: contain;
border-radius: 16px; border-radius: 16px;
filter: drop-shadow(0 10px 20px rgba(0, 0, 0, 0.08)); filter: drop-shadow(0 10px 20px rgba(0, 0, 0, 0.08));
@@ -159,13 +168,13 @@ const handleStart = () => {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 12px; gap: 12px;
min-width: 180px; // 减小最小宽度 min-width: 180px;
.button-hint { .button-hint {
font-size: 14px; // 减小字体 font-size: 14px;
color: #475569; color: #475569;
background: #f1f5f9; background: #f1f5f9;
padding: 6px 18px; // 减小内边距 padding: 6px 18px;
border-radius: 24px; border-radius: 24px;
white-space: nowrap; white-space: nowrap;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.02); box-shadow: 0 2px 6px rgba(0, 0, 0, 0.02);
@@ -180,9 +189,9 @@ const handleStart = () => {
} }
.start-button.enlarged { .start-button.enlarged {
min-width: 160px; // 减小按钮宽度 min-width: 160px;
height: 56px; // 减小按钮高度 height: 56px;
font-size: 22px; // 减小字体 font-size: 22px;
border-radius: 40px; border-radius: 40px;
box-shadow: 0 10px 20px rgba(22, 93, 255, 0.2); box-shadow: 0 10px 20px rgba(22, 93, 255, 0.2);
transition: all 0.3s ease; transition: all 0.3s ease;
@@ -209,12 +218,11 @@ const handleStart = () => {
// 针对不同屏幕尺寸的精细调整 // 针对不同屏幕尺寸的精细调整
@media (min-width: 1920px) { @media (min-width: 1920px) {
.guide-card { .guide-card {
max-width: 1600px; // 超大屏幕限制最大宽度 max-width: 1600px;
padding: 30px 50px; padding: 30px 50px;
.image-wrapper {
max-width: 800px;
} }
.guide-content.full-height .page-title {
// 大屏幕保持同步
} }
} }
@@ -222,15 +230,7 @@ const handleStart = () => {
.guide-card { .guide-card {
gap: 30px; gap: 30px;
padding: 20px 25px; padding: 20px 25px;
// 如果需要,可以在这里微调 margin-left记得同时微调 title
.image-wrapper {
min-width: 280px;
max-width: 500px;
.guide-image.enlarged {
max-height: 45vh;
}
}
} }
} }
@@ -242,43 +242,26 @@ const handleStart = () => {
.guide-card { .guide-card {
gap: 25px; gap: 25px;
padding: 15px 20px; padding: 15px 20px;
max-width: 95vw; // 在小屏幕上使用更宽的百分比 max-width: 95vw;
// 平板端如果需要调整偏移,请同时调整下方 title 的 margin
.image-wrapper {
min-width: 250px;
max-width: 450px;
.guide-image.enlarged {
max-height: 40vh;
min-height: 200px;
}
}
.button-wrapper {
min-width: 160px;
.button-hint {
font-size: 13px;
padding: 5px 15px;
}
.start-button.enlarged {
min-width: 150px;
height: 50px;
font-size: 20px;
}
}
} }
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.guide-section.full-background { .guide-section.full-background {
padding: 5px; padding: 5px;
justify-content: center; // 移动端恢复整体居中
.guide-content.full-height { .guide-content.full-height {
align-items: center; // 移动端子元素也居中
.page-title { .page-title {
font-size: 28px; font-size: 28px;
margin-bottom: 10px; margin-bottom: 10px;
// 【重要】移动端重置 margin恢复自动居中
margin-left: auto;
margin-right: auto;
width: fit-content; // 保持适应内容
&::after { &::after {
width: 60px; width: 60px;
@@ -294,7 +277,11 @@ const handleStart = () => {
padding: 20px 15px; padding: 20px 15px;
gap: 20px; gap: 20px;
min-height: auto; min-height: auto;
max-width: 98vw; // 移动端几乎占满宽度 max-width: 98vw;
// 【重要】移动端强制居中
margin-left: auto;
margin-right: auto;
.image-wrapper { .image-wrapper {
min-width: auto; min-width: auto;
@@ -334,14 +321,12 @@ const handleStart = () => {
.guide-section.full-background { .guide-section.full-background {
.guide-card { .guide-card {
@media (min-width: 1025px) { @media (min-width: 1025px) {
margin-left: 5%; // 有侧边栏时左移 // 如果有侧边栏,确保标题和卡片偏移量依然一致
margin-right: auto;
} }
} }
} }
} }
// 确保父容器占满且无滚动问题
html, body { html, body {
margin: 0; margin: 0;
padding: 0; padding: 0;
@@ -359,22 +344,22 @@ html, body {
overflow-y: auto; overflow-y: auto;
} }
// 针对100%缩放比例的特殊处理 // 针对特定分辨率的微调(可选)
@media screen and (min-width: 1280px) and (max-width: 1366px) { @media screen and (min-width: 1280px) and (max-width: 1366px) {
.guide-card { .guide-card {
max-width: 85vw; // 在常见笔记本屏幕上使用85%宽度 max-width: 85vw;
} }
} }
@media screen and (min-width: 1367px) and (max-width: 1440px) { @media screen and (min-width: 1367px) and (max-width: 1440px) {
.guide-card { .guide-card {
max-width: 80vw; // 在1440p屏幕上使用80%宽度 max-width: 80vw;
} }
} }
@media screen and (min-width: 1441px) and (max-width: 1680px) { @media screen and (min-width: 1441px) and (max-width: 1680px) {
.guide-card { .guide-card {
max-width: 75vw; // 在更大屏幕上使用75%宽度 max-width: 75vw;
} }
} }
</style> </style>

View File

@@ -0,0 +1,136 @@
<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, onUnmounted } 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 () => {
// 检查用户是否已经登录
if (userStore.token) {
return
}
const isInvalid = await formRef.value?.validate()
if (isInvalid) return
try {
loading.value = true
// 调用后端接口校验卡号
await userStore.cardLogin(form)
// 登录成功后移除事件监听器
if (keyDownListener) {
document.removeEventListener('keydown', keyDownListener)
keyDownListener = null
}
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
}
}
// 键盘事件监听器引用
let keyDownListener: ((e: KeyboardEvent) => void) | null = null
// 监听键盘输入,实现自动识别卡号
onMounted(() => {
// 只监听Enter键用于自动登录
keyDownListener = (e) => {
// 刷卡器通常会快速输入卡号并以Enter键结束
if (e.key === 'Enter') {
// 遇到Enter键尝试登录
if (form.cardNumber.trim()) {
handleLogin()
}
}
}
document.addEventListener('keydown', keyDownListener)
})
// 组件卸载时移除事件监听器
onUnmounted(() => {
if (keyDownListener) {
document.removeEventListener('keydown', keyDownListener)
}
})
</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

@@ -0,0 +1,119 @@
<template>
<a-modal
v-model:visible="visible"
:title="title"
:mask-closable="false"
:esc-to-close="false"
:width="width >= 600 ? 600 : '100%'"
draggable
@before-ok="save"
@close="reset"
>
<GiForm ref="formRef" v-model="form" :columns="columns" />
</a-modal>
</template>
<script setup lang="ts">
import { Message } from '@arco-design/web-vue'
import { useWindowSize } from '@vueuse/core'
import { addMaterialInfo, getMaterialInfo, updateMaterialInfo } from '@/apis/material/materialInfo'
import { type ColumnItem, GiForm } from '@/components/GiForm'
import { useResetReactive } from '@/hooks'
const emit = defineEmits<{
(e: 'save-success'): void
}>()
const { width } = useWindowSize()
const dataId = ref('')
const visible = ref(false)
const isUpdate = computed(() => !!dataId.value)
const title = computed(() => (isUpdate.value ? '修改物料管理' : '新增物料管理'))
const formRef = ref<InstanceType<typeof GiForm>>()
const [form, resetForm] = useResetReactive({
// todo 待补充
})
const columns: ColumnItem[] = reactive([
{
label: '物料名称',
field: 'materialName',
type: 'input',
span: 24,
required: true,
},
{
label: '物料编码',
field: 'encoding',
type: 'input',
span: 24,
required: true,
},
{
label: '物料单位重量,单位(g)',
field: 'unitWeight',
type: 'input-number',
span: 24,
},
{
label: '物料规格',
field: 'materialSpec',
type: 'input',
span: 24,
},
{
label: '物料照片上传',
field: 'photoUrl',
type: 'image',
savePath: 'material/',
span: 24,
},
])
// 重置
const reset = () => {
formRef.value?.formRef?.resetFields()
resetForm()
}
// 保存
const save = async () => {
try {
const isInvalid = await formRef.value?.formRef?.validate()
if (isInvalid) return false
if (isUpdate.value) {
await updateMaterialInfo(form, dataId.value)
Message.success('修改成功')
} else {
await addMaterialInfo(form)
Message.success('新增成功')
}
emit('save-success')
return true
} catch (error) {
return false
}
}
// 新增
const onAdd = async () => {
reset()
dataId.value = ''
visible.value = true
}
// 修改
const onUpdate = async (id: string) => {
reset()
dataId.value = id
const { data } = await getMaterialInfo(id)
Object.assign(form, data)
visible.value = true
}
defineExpose({ onAdd, onUpdate })
</script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,201 @@
<template>
<a-drawer
v-model:visible="visible"
title="导入物料信息"
:mask-closable="false"
:esc-to-close="false"
:width="width >= 600 ? 600 : '100%'"
ok-text="确认导入"
cancel-text="取消导入"
@before-ok="save"
@close="reset"
>
<a-form ref="formRef" :model="form" size="large" auto-label-width>
<a-alert v-if="!form.disabled" style="margin-bottom: 15px">
请按照模板要求填写数据填写完毕后请先上传并进行解析
<template #action>
<a-link @click="downloadTemplate">
<template #icon><GiSvgIcon name="file-excel" :size="16" /></template>
<template #default>下载模板</template>
</a-link>
</template>
</a-alert>
<fieldset>
<legend>1.解析数据</legend>
<div class="file-box">
<a-upload
draggable
:custom-request="handleUpload"
:limit="1"
:show-retry-butto="false"
:show-cancel-button="false" tip="仅支持xls、xlsx格式"
:file-list="uploadFile"
accept=".xls, .xlsx, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
/>
</div>
<div v-if="dataResult.importKey">
<div class="file-box">
<a-space size="large">
<a-statistic title="总计行数" :value="dataResult.totalRows" />
<a-statistic title="正常行数" :value="dataResult.validRows" />
</a-space>
</div>
<div class="file-box">
<a-space size="large">
<a-statistic title="已存在物料名" :value="dataResult.duplicateNameRows" />
<a-statistic title="已存在编码" :value="dataResult.duplicateCodeRows" />
</a-space>
</div>
</div>
</fieldset>
<fieldset>
<legend>2.导入策略</legend>
<a-form-item label="物料名已存在" field="duplicateName">
<a-radio-group v-model="form.duplicateName" type="button">
<a-radio :value="1">跳过该行</a-radio>
<a-radio :value="2">修改数据</a-radio>
<a-radio :value="3">停止导入</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="编码已存在" field="duplicateCode">
<a-radio-group v-model="form.duplicateCode" type="button">
<a-radio :value="1">跳过该行</a-radio>
<a-radio :value="2">修改数据</a-radio>
<a-radio :value="3">停止导入</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="默认状态" field="defaultStatus">
<a-switch
v-model="form.defaultStatus"
:checked-value="1"
:unchecked-value="2"
checked-text="启用"
unchecked-text="禁用"
type="round"
/>
</a-form-item>
</fieldset>
</a-form>
</a-drawer>
</template>
<script setup lang="ts">
import { type FormInstance, Message, type RequestOption } from '@arco-design/web-vue'
import { useWindowSize } from '@vueuse/core'
import {
type MaterialImportResp,
downloadMaterialInfoImportTemplate,
importMaterial,
parseImportMaterial,
} from '@/apis/material/materialInfo'
import { useDownload, useResetReactive } from '@/hooks'
const emit = defineEmits<{
(e: 'save-success'): void
}>()
const { width } = useWindowSize()
const visible = ref(false)
const formRef = ref<FormInstance>()
const uploadFile = ref([])
const [form, resetForm] = useResetReactive({
importKey: '',
errorPolicy: 1,
duplicateName: 1,
duplicateCode: 1,
defaultStatus: 1,
})
const dataResult = ref<MaterialImportResp>({
importKey: '',
totalRows: 0,
validRows: 0,
duplicateNameRows: 0,
duplicateCodeRows: 0,
})
// 重置
const reset = () => {
formRef.value?.resetFields()
dataResult.value.importKey = ''
uploadFile.value = []
resetForm()
}
// 下载模板
const downloadTemplate = () => {
useDownload(() => downloadMaterialInfoImportTemplate())
}
// 上传解析导入数据
const handleUpload = (options: RequestOption) => {
const controller = new AbortController();
(async function requestWrap() {
const { onProgress, onError, onSuccess, fileItem, name = 'file' } = options
onProgress(20)
const formData = new FormData()
formData.append(name as string, fileItem.file as Blob)
try {
const res = await parseImportMaterial(formData)
dataResult.value = res.data
Message.success('上传解析成功')
onSuccess(res)
} catch (error) {
onError(error)
}
})()
return {
abort() {
controller.abort()
},
}
}
// 执行导入
const save = async () => {
try {
if (!dataResult.value.importKey) {
Message.warning('请先上传文件,解析导入数据')
return false
}
form.importKey = dataResult.value.importKey
const res = await importMaterial(form)
Message.success(`导入成功! 新增${res.data.insertRows}, 修改${res.data.updateRows},总计处理${res.data.totalRows}`)
emit('save-success')
return true
} catch (error) {
return false
}
}
// 打开
const onOpen = () => {
reset()
visible.value = true
}
defineExpose({ onOpen })
</script>
<style scoped lang="scss">
fieldset {
padding: 15px 15px 0 15px;
margin-bottom: 15px;
border: 1px solid var(--color-neutral-3);
border-radius: 3px;
}
fieldset legend {
color: rgb(var(--gray-10));
padding: 2px 5px 2px 5px;
border: 1px solid var(--color-neutral-3);
border-radius: 3px;
}
.file-box {
margin-bottom: 20px;
margin-left: 10px;
}
</style>

View File

@@ -0,0 +1,134 @@
<template>
<a-drawer
v-model:visible="visible"
title="批量导入物料照片"
:mask-closable="false"
:esc-to-close="false"
:width="width >= 600 ? 600 : '100%'"
ok-text="确认导入"
cancel-text="取消导入"
@before-ok="save"
@close="reset"
>
<a-form ref="formRef" :model="form" size="large" auto-label-width>
<fieldset>
<legend>zip文件上传</legend>
<div class="file-box">
<a-upload
draggable
:custom-request="handleUpload"
:limit="1"
:show-retry-button="false"
:show-cancel-button="false"
tip="仅支持zip格式"
:file-list="uploadFile"
accept=".zip"
/>
</div>
</fieldset>
</a-form>
</a-drawer>
</template>
<script setup lang="ts">
import { type FormInstance, Message, type RequestOption } from '@arco-design/web-vue'
import { useWindowSize } from '@vueuse/core'
import { uploadMaterialPhotos } from '@/apis/material/materialInfo'
import { useResetReactive } from '@/hooks'
const emit = defineEmits<{
(e: 'save-success'): void
}>()
const { width } = useWindowSize()
const visible = ref(false)
const formRef = ref<FormInstance>()
const uploadFile = ref([])
// 新增:保存上传的文件对象
const uploadFileObj = ref<File | null>(null)
// 移除不必要的表单字段,只保留空对象即可
const [form, resetForm] = useResetReactive({})
// 重置
const reset = () => {
formRef.value?.resetFields()
uploadFile.value = []
uploadFileObj.value = null // 重置文件对象
resetForm()
}
// 修改上传逻辑:仅保存文件对象,不做解析
const handleUpload = (options: RequestOption) => {
const { onProgress, onError, onSuccess, fileItem } = options
onProgress(100) // 直接标记上传完成(仅前端显示)
// 保存文件对象供后续提交使用
uploadFileObj.value = fileItem.file as File
uploadFile.value = [fileItem] // 更新文件列表显示
Message.success('文件已选择,点击确认导入即可上传')
onSuccess({}) // 通知上传组件完成
return {
abort() {
// 空实现取消时会走reset逻辑
},
}
}
// 执行导入:直接上传文件到后端
const save = async () => {
try {
// 校验是否选择了文件
if (!uploadFileObj.value) {
Message.warning('请先选择要上传的ZIP文件')
return false
}
// 构建FormData
const formData = new FormData()
formData.append('zipFile', uploadFileObj.value) // 后端接收参数名保持为zipFile
// 调用后端照片批量导入接口
await uploadMaterialPhotos(formData)
Message.success('物料照片批量导入成功!')
emit('save-success') // 通知父组件刷新列表
visible.value = false // 关闭抽屉
return true
} catch (error) {
Message.error('物料照片导入失败,请重试!')
return false
}
}
// 打开抽屉
const onOpen = () => {
reset()
visible.value = true
}
defineExpose({ onOpen })
</script>
<style scoped lang="scss">
fieldset {
padding: 15px 15px 0 15px;
margin-bottom: 15px;
border: 1px solid var(--color-neutral-3);
border-radius: 3px;
}
fieldset legend {
color: rgb(var(--gray-10));
padding: 2px 5px 2px 5px;
border: 1px solid var(--color-neutral-3);
border-radius: 3px;
}
.file-box {
margin-bottom: 20px;
margin-left: 10px;
}
</style>

View File

@@ -0,0 +1,229 @@
<template>
<div class="gi_table_page">
<GiTable
title="物料管理管理"
row-key="id"
:data="dataList"
:columns="columns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1000 }"
:pagination="pagination"
:disabled-tools="['size']"
:disabled-column-keys="['name']"
@refresh="search"
>
<!-- toolbar 部分保持不变 -->
<template #toolbar-left>
<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-button @click="reset">
<template #icon><icon-refresh /></template>
<template #default>重置</template>
</a-button>
</template>
<template #toolbar-right>
<a-button v-permission="['admin:materialInfo:add']" type="primary" @click="onAdd">
<template #icon><icon-plus /></template>
<template #default>新增</template>
</a-button>
<a-button v-permission="['admin:materialInfo:import']" @click="onImport">
<template #icon><icon-upload /></template>
<template #default>导入</template>
</a-button>
<a-button v-permission="['admin:materialInfo:export']" @click="onExport">
<template #icon><icon-download /></template>
<template #default>导出</template>
</a-button>
<a-button v-permission="['admin:materialInfo:import']" @click="onPhotosImport">
<template #icon><icon-upload /></template>
<template #default>照片批量导入</template>
</a-button>
</template>
<!-- 修改点照片列插槽 -->
<template #photoUrl="{ record }">
<div class="photo-container">
<!-- 1. 正常显示图片有地址 未标记为错误 -->
<img
v-if="record.photoUrl && !record.photoLoadError"
:src="record.photoUrl"
alt="物料照片"
class="material-photo"
@load="handleImgLoad(record)"
@error="handleImgError(record, $event)"
/>
<!-- 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>
</template>
<template #action="{ record }">
<a-space>
<a-link v-permission="['admin:materialInfo:update']" title="修改" @click="onUpdate(record)">修改</a-link>
<a-link
v-permission="['admin:materialInfo:delete']"
status="danger"
:disabled="record.disabled"
:title="record.disabled ? '不可删除' : '删除'"
@click="onDelete(record)"
>
删除
</a-link>
</a-space>
</template>
</GiTable>
<MaterialInfoAddModal ref="MaterialInfoAddModalRef" @save-success="search" />
<MaterialInfoImportDrawer ref="MaterialInfoImportDrawerRef" @save-success="search" />
<PhotosImport ref="PhotosImportRef" @save-success="search"/>
</div>
</template>
<script setup lang="ts">
// ... 保持原有的 import 不变
import MaterialInfoAddModal from './MaterialInfoAddModal.vue'
import MaterialInfoImportDrawer from './MaterialInfoImportDrawer.vue'
import PhotosImport from '@/views/material/PhotosImport.vue';
import { type MaterialInfoQuery, type MaterialInfoResp, deleteMaterialInfo, exportMaterialInfo, listMaterialInfo } from '@/apis/material/materialInfo'
import type { TableInstanceColumns } from '@/components/GiTable/type'
import { useDownload, useTable } from '@/hooks'
import { isMobile } from '@/utils'
import has from '@/utils/has'
defineOptions({ name: 'MaterialInfo' })
// 扩展类型定义,允许动态添加 photoLoadError 属性
// 如果 TS 报错,可以在 MaterialInfoResp 接口定义中添加 photoLoadError?: boolean;
interface MaterialInfoRespWithStatus extends MaterialInfoResp {
photoLoadError?: boolean;
}
const queryForm = reactive<MaterialInfoQuery>({
materialName: undefined,
encoding: undefined,
sort: ['id,desc'],
})
const {
tableData: dataList,
loading,
pagination,
search,
handleDelete,
} = useTable((page) => listMaterialInfo({ ...queryForm, ...page }), { immediate: true })
const columns = ref<TableInstanceColumns[]>([
{ title: '物料名称', dataIndex: 'materialName', slotName: 'materialName' },
{ title: '物料编码', dataIndex: 'encoding', slotName: 'encoding' },
{ title: '物料单位重量(g)', dataIndex: 'unitWeight', slotName: 'unitWeight' },
{ title: '物料规格', dataIndex: 'materialSpec', slotName: 'materialSpec' },
{ title: '物料照片', dataIndex: 'photoUrl', slotName: 'photoUrl', width: 120, align: 'center' },
{ title: '创建人', dataIndex: 'createUserString', slotName: 'createUser' },
{ title: '创建时间', dataIndex: 'createTime', slotName: 'createTime' },
{
title: '操作',
dataIndex: 'action',
slotName: 'action',
width: 160,
align: 'center',
fixed: !isMobile() ? 'right' : undefined,
show: has.hasPermOr(['admin:materialInfo:detail', 'admin:materialInfo:update', 'admin:materialInfo:delete']),
},
])
const reset = () => {
queryForm.materialName = undefined
queryForm.encoding = undefined
search()
}
const onDelete = (record: MaterialInfoResp) => {
return handleDelete(() => deleteMaterialInfo(record.id), {
content: `是否确定删除该条数据?`,
showModal: true,
})
}
const onExport = () => {
useDownload(() => exportMaterialInfo(queryForm))
}
// 【修改点】图片加载处理逻辑
const handleImgLoad = (record: MaterialInfoRespWithStatus) => {
record.photoLoadError = false
}
const handleImgError = (record: MaterialInfoRespWithStatus, e: Event) => {
// 标记为加载错误,触发模板渲染 "照片异常"
record.photoLoadError = true
console.warn(`物料照片加载失败: ${record.photoUrl}`)
}
const MaterialInfoAddModalRef = ref<InstanceType<typeof MaterialInfoAddModal>>()
const onAdd = () => {
MaterialInfoAddModalRef.value?.onAdd()
}
const onUpdate = (record: MaterialInfoResp) => {
MaterialInfoAddModalRef.value?.onUpdate(record.id)
}
const MaterialInfoImportDrawerRef = ref<InstanceType<typeof MaterialInfoImportDrawer>>()
const onImport = () => {
MaterialInfoImportDrawerRef.value?.onOpen()
}
const PhotosImportRef = ref<InstanceType<typeof PhotosImport>>()
const onPhotosImport = () => {
PhotosImportRef.value?.onOpen()
}
</script>
<style scoped lang="scss">
.photo-container {
display: flex;
align-items: center;
justify-content: center;
height: 80px;
width: 100%;
overflow: hidden;
}
.material-photo {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 4px;
transition: opacity 0.3s;
}
.no-photo {
color: #999;
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>

View File

@@ -1,146 +0,0 @@
<template>
<div class="gi_table_page">
<GiTable
row-key="id"
title="消息中心"
:data="dataList"
:columns="columns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1000 }"
:pagination="pagination"
:disabled-tools="['size', 'setting']"
:disabled-column-keys="['name']"
:row-selection="{ type: 'checkbox', showCheckedAll: true }"
:selected-keys="selectedKeys"
@select-all="selectAll"
@select="select"
@refresh="search"
>
<template #toolbar-left>
<a-input v-model="queryForm.title" placeholder="请输入标题" allow-clear @change="search">
<template #prefix><icon-search /></template>
</a-input>
<a-select
v-model="queryForm.isRead"
placeholder="请选择状态"
allow-clear
style="width: 150px"
@change="search"
>
<a-option :value="false">未读</a-option>
<a-option :value="true">已读</a-option>
</a-select>
<a-button @click="reset">
<template #icon><icon-refresh /></template>
<template #default>重置</template>
</a-button>
</template>
<template #toolbar-right>
<a-button type="primary" status="danger" :disabled="!selectedKeys.length" :title="!selectedKeys.length ? '请选择' : ''" @click="onDelete">
<template #icon><icon-delete /></template>
<template #default>删除</template>
</a-button>
<a-button type="primary" :disabled="!selectedKeys.length" :title="!selectedKeys.length ? '请选择' : ''" @click="onRead">
<template #default>标记为已读</template>
</a-button>
<a-button type="primary" :disabled="selectedKeys.length" :title="!selectedKeys.length ? '请选择' : ''" @click="onReadAll">
<template #default>全部已读</template>
</a-button>
</template>
<template #title="{ record }">
<a-tooltip :content="record.content"><span>{{ record.title }}</span></a-tooltip>
</template>
<template #isRead="{ record }">
<a-tag :color="record.isRead ? '' : 'arcoblue'">
{{ record.isRead ? '已读' : '未读' }}
</a-tag>
</template>
<template #type="{ record }">
<GiCellTag :value="record.type" :dict="message_type" />
</template>
</GiTable>
</div>
</template>
<script setup lang="ts">
import { Message, Modal } from '@arco-design/web-vue'
import { type MessageQuery, deleteMessage, listMessage, readMessage } from '@/apis'
import type { TableInstanceColumns } from '@/components/GiTable/type'
import { useTable } from '@/hooks'
import { useDict } from '@/hooks/app'
defineOptions({ name: 'SystemMessage' })
const { message_type } = useDict('message_type')
const queryForm = reactive<MessageQuery>({
sort: ['createTime,desc'],
})
const {
tableData: dataList,
loading,
pagination,
selectedKeys,
select,
selectAll,
search,
handleDelete,
} = useTable((page) => listMessage({ ...queryForm, ...page }), { immediate: true })
const columns: TableInstanceColumns[] = [
{
title: '序号',
width: 66,
align: 'center',
render: ({ rowIndex }) => h('span', {}, rowIndex + 1 + (pagination.current - 1) * pagination.pageSize),
},
{ title: '标题', dataIndex: 'title', slotName: 'title', minWidth: 100, ellipsis: true, tooltip: true },
{ title: '状态', dataIndex: 'isRead', slotName: 'isRead', align: 'center' },
{ title: '时间', dataIndex: 'createTime', width: 180 },
{ title: '类型', dataIndex: 'type', slotName: 'type', width: 180, ellipsis: true, tooltip: true },
]
// 重置
const reset = () => {
queryForm.title = undefined
queryForm.type = undefined
queryForm.isRead = undefined
search()
}
// 删除
const onDelete = () => {
if (!selectedKeys.value.length) {
return Message.warning('请选择数据')
}
return handleDelete(() => deleteMessage(selectedKeys.value), { showModal: false })
}
// 标记为已读
const onRead = async () => {
if (!selectedKeys.value.length) {
return Message.warning('请选择数据')
}
await readMessage(selectedKeys.value)
Message.success('操作成功')
search()
}
// 全部已读
const onReadAll = async () => {
Modal.warning({
title: '全部已读',
content: '确定要标记全部消息为已读吗?',
hideCancel: false,
maskClosable: false,
onOk: async () => {
await readMessage([])
Message.success('操作成功')
search()
},
})
}
</script>
<style scoped lang="scss"></style>

View File

@@ -1,77 +0,0 @@
<template>
<a-modal v-model:visible="visible" :width="width >= 600 ? 'auto' : '100%'" :footer="false" draggable @close="reset">
<a-typography :style="{ marginTop: '-40px', textAlign: 'center' }">
<a-typography-title>
{{ dataDetail?.title }}
</a-typography-title>
<a-typography-paragraph>
<div class="meta-data">
<a-space>
<span>
<icon-user class="icon" />
<span class="label">发布人</span>
<span>{{ dataDetail?.createUserString }}</span>
</span>
<a-divider direction="vertical" />
<span>
<icon-history class="icon" />
<span class="label">发布时间</span>
<span>{{ dataDetail?.effectiveTime ? dataDetail?.effectiveTime : dataDetail?.createTime }}</span>
</span>
</a-space>
</div>
</a-typography-paragraph>
</a-typography>
<a-divider />
<AiEditor :model-value="dataDetail?.content" />
<a-divider />
<div v-if="dataDetail?.updateTime" class="update-time-row">
<span>
<icon-schedule class="icon" />
<span>最后更新于</span>
<span>{{ dataDetail?.updateTime }}</span>
</span>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { useWindowSize } from '@vueuse/core'
import AiEditor from './detail/components/index.vue'
import { type NoticeResp, getNotice } from '@/apis/system'
const { width } = useWindowSize()
const dataDetail = ref<NoticeResp>({
content: '',
})
const visible = ref(false)
// 详情
const onDetail = async (id: string) => {
const { data } = await getNotice(id)
dataDetail.value = data
visible.value = true
}
// 重置
const reset = () => {
dataDetail.value = {
content: '',
}
}
defineExpose({ onDetail })
</script>
<style scoped lang="scss">
.arco-link {
color: rgb(var(--gray-8));
}
.icon {
margin-right: 3px;
}
.update-time-row {
text-align: right;
}
</style>

View File

@@ -1,209 +0,0 @@
<!-- 未完善 -->
<template>
<div ref="divRef" class="container">
<div class="aie-container">
<div class="aie-header-panel">
<div class="aie-container-header"></div>
</div>
<div class="aie-main">
<div class="aie-directory-content">
<div class="aie-directory">
<h5>目录</h5>
<div id="outline">
</div>
</div>
</div>
<div class="aie-container-panel">
<div class="aie-container-main"></div>
</div>
</div>
<div class="aie-container-footer"></div>
</div>
</div>
</template>
<script setup lang="ts">
import { AiEditor, type AiEditorOptions } from 'aieditor'
import 'aieditor/dist/style.css'
import { useAppStore } from '@/stores'
defineOptions({ name: 'AiEditor' })
const props = defineProps<{
modelValue: string
options?: AiEditorOptions
}>()
const emit = defineEmits<(e: 'update:modelValue', value: string) => void>()
const appStore = useAppStore()
const divRef = ref<any>()
const aieditor = ref<AiEditor | null>(null)
const updateOutLine = (editor: AiEditor) => {
const outlineContainer = document.querySelector('#outline')
while (outlineContainer?.firstChild) {
outlineContainer.removeChild(outlineContainer.firstChild)
}
const outlines = editor.getOutline()
for (const outline of outlines) {
const child = document.createElement('div')
child.classList.add(`aie-title${outline.level}`)
child.style.marginLeft = `${14 * (outline.level - 1)}px`
child.style.padding = `4px 0`
child.innerHTML = `<a href="#${outline.id}">${outline.text}</a>`
child.addEventListener('click', (e) => {
e.preventDefault()
const el = editor.innerEditor.view.dom.querySelector(`#${outline.id}`) as HTMLElement
el.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' })
setTimeout(() => {
editor.focusPos(outline.pos + outline.size - 1)
}, 1000)
})
outlineContainer?.appendChild(child)
}
}
const editorConfig = reactive<AiEditorOptions>({
element: '',
theme: appStore.theme,
placeholder: '请输入内容',
content: '',
draggable: false,
onChange: (editor: AiEditor) => {
emit('update:modelValue', editor.getHtml())
updateOutLine(editor)
},
onCreated: (editor: AiEditor) => {
updateOutLine(editor)
},
})
watch(() => props.modelValue, (value) => {
if (value !== aieditor.value?.getHtml()) {
aieditor.value?.destroy()
editorConfig.content = value
aieditor.value = new AiEditor(editorConfig)
}
})
const init = () => {
editorConfig.element = divRef.value
aieditor.value = new AiEditor(editorConfig)
}
// 挂载阶段
onMounted(() => {
init()
})
// 销毁阶段
onUnmounted(() => {
aieditor.value?.destroy()
})
</script>
<style scoped lang="scss">
.container {
height: 100%;
width: 100%;
box-sizing: border-box;
}
.aie-header-panel {
position: sticky;
// top: 51px;
z-index: 1;
}
.aie-header-panel aie-header>div {
align-items: center;
justify-content: center;
padding: 10px 0;
}
.aie-container {
border: none !important;
}
.aie-container-panel {
width: calc(100% - 2rem - 2px);
max-width: 826.77px;
margin: 0rem auto;
border: 1px solid var(--color-border-1);
background-color: var() rgba($color: var(--color-bg-1), $alpha: 1.0);
height: 100%;
padding: 1rem;
z-index: 99;
overflow: auto;
box-sizing: border-box;
color: black;
}
.aie-main {
position: relative;
overflow: hidden;
flex: 1;
box-sizing: border-box;
padding: 1rem 0px;
background-color: var(--color-bg-2);
}
.aie-directory {
position: absolute;
top: 30px;
left: 10px;
width: 260px;
z-index: 0;
}
.aie-directory h5 {
// color: #000000c4;
font-size: 16px;
text-indent: 4px;
line-height: 32px;
}
.aie-directory a {
height: 30px;
font-size: 14px;
// color: #000000a3;
text-indent: 4px;
line-height: 30px;
text-decoration: none;
width: 100%;
display: inline-block;
margin: 0;
padding: 0;
white-space: nowrap;
overflow: hidden;
-o-text-overflow: ellipsis;
text-overflow: ellipsis;
}
.aie-directory a:hover {
cursor: pointer;
// background-color: #334d660f;
border-radius: 4px;
}
.aie-title1 {
font-size: 14px;
font-weight: 500;
}
#outline {
text-indent: 2rem;
}
.aie-directory-content {
position: sticky;
top: 0px
}
@media screen and (max-width: 1280px) {
.aie-directory {
display: none;
}
}
@media screen and (max-width: 1400px) {
.aie-directory {
width: 200px;
}
}
</style>

View File

@@ -1,212 +0,0 @@
<template>
<div ref="containerRef" class="detail">
<div class="detail_header">
<a-affix :target="(containerRef as HTMLElement)">
<a-page-header title="通知公告" :subtitle="title" @back="onBack">
<template #extra>
<a-button type="primary" @click="save">
<template #icon>
<icon-save v-if="isUpdate" />
<icon-send v-else />
</template>
<template #default>
{{ isUpdate ? '保存' : '发布' }}
</template>
</a-button>
</template>
</a-page-header>
</a-affix>
</div>
<div class="detail_content" style="display: flex; flex-direction: column;">
<GiForm ref="formRef" v-model="form" :columns="columns">
<template #noticeUsers>
<a-select
v-model="form.noticeUsers"
:options="userList"
multiple
:max-tag-count="4"
:allow-clear="true"
/>
<a-tooltip content="选择用户">
<a-button @click="onOpenSelectUser">
<template #icon>
<icon-plus />
</template>
</a-button>
</a-tooltip>
</template>
</GiForm>
<div style="flex: 1;">
<AiEditor v-model="form.content" />
</div>
</div>
<a-modal
v-model:visible="visible"
title="用户选择"
:mask-closable="false"
:esc-to-close="false"
:width="width >= 1100 ? 1100 : '100%'"
draggable
@close="reset"
>
<UserSelect v-if="visible" ref="UserSelectRef" v-model:value="form.noticeUsers" @select-user="onSelectUser" />
</a-modal>
</div>
</template>
<script setup lang="ts">
import { Message } from '@arco-design/web-vue'
import { useWindowSize } from '@vueuse/core'
import AiEditor from './components/index.vue'
import { addNotice, getNotice, updateNotice } from '@/apis/system/notice'
import { listUserDict } from '@/apis'
import { type ColumnItem, GiForm } from '@/components/GiForm'
import type { LabelValueState } from '@/types/global'
import { useTabsStore } from '@/stores'
import { useResetReactive } from '@/hooks'
import { useDict } from '@/hooks/app'
const { width } = useWindowSize()
const route = useRoute()
const router = useRouter()
const tabsStore = useTabsStore()
const { id, type } = route.query
const isUpdate = computed(() => type === 'update')
const title = computed(() => (isUpdate.value ? '修改' : '新增'))
const containerRef = ref<HTMLElement | null>()
const formRef = ref<InstanceType<typeof GiForm>>()
const { notice_type } = useDict('notice_type')
const [form, resetForm] = useResetReactive({
title: '',
type: '',
effectiveTime: '',
terminateTime: '',
content: '',
noticeScope: 1,
})
const columns: ColumnItem[] = reactive([
{
label: '标题',
field: 'title',
type: 'input',
props: {
maxLength: 150,
showWordLimit: true,
},
rules: [{ required: true, message: '请输入标题' }],
},
{
label: '类型',
field: 'type',
type: 'select',
props: {
options: notice_type,
},
rules: [{ required: true, message: '请输入类型' }],
},
{
label: '生效时间',
field: 'effectiveTime',
type: 'date-picker',
props: {
showTime: true,
},
},
{
label: '终止时间',
field: 'terminateTime',
type: 'date-picker',
props: {
showTime: true,
},
},
{
label: '通知范围',
field: 'noticeScope',
type: 'radio-group',
props: {
options: [{ label: '所有人', value: 1 }, { label: '指定用户', value: 2 }],
},
rules: [{ required: true, message: '请选择通知范围' }],
},
{
label: '指定用户',
field: 'noticeUsers',
type: 'select',
hide: () => {
return form.noticeScope === 1
},
rules: [{ required: true, message: '请选择指定用户' }],
},
])
// 修改
const onUpdate = async (id: string) => {
resetForm()
const res = await getNotice(id)
Object.assign(form, res.data)
}
// 返回
const onBack = () => {
router.back()
tabsStore.closeCurrent(route.path)
}
// 保存
const save = async () => {
const isInvalid = await formRef.value?.formRef?.validate()
if (isInvalid) return false
try {
// 通知范围 所有人 去除指定用户
form.noticeUsers = form.noticeScope === 1 ? null : form.noticeUsers
if (isUpdate.value) {
await updateNotice(form, id as string)
Message.success('修改成功')
} else {
await addNotice(form)
Message.success('新增成功')
}
onBack()
return true
} catch (error) {
console.error(error)
return false
}
}
// 打开用户选择窗口
const visible = ref(false)
const onOpenSelectUser = () => {
visible.value = true
}
const UserSelectRef = ref()
// 重置
const reset = () => {
UserSelectRef.value?.onClearSelected()
}
// 用户选择回调
const onSelectUser = (value: string[]) => {
form.noticeUsers = value
formRef.value?.formRef?.validateField('noticeUsers')
}
const userList = ref<LabelValueState[]>([])
onMounted(async () => {
// 当id存在与type为update时执行修改操作
if (id && isUpdate.value) {
await onUpdate(id as string)
}
// 获取所有用户
const { data } = await listUserDict()
userList.value = data.map((item) => ({ ...item, value: `${item.value}` }))
})
</script>
<style scoped lang="less"></style>

View File

@@ -1,123 +0,0 @@
<!-- 未完善 -->
<template>
<div ref="divRef" class="container">
<div class="aie-container">
<div class="aie-header-panel" style="display: none;">
<div class="aie-container-header"></div>
</div>
<div class="aie-main">
<div class="aie-container-panel">
<div class="aie-container-main"></div>
</div>
</div>
<div class="aie-container-footer" style="display: none;"></div>
</div>
</div>
</template>
<script setup lang="ts">
import { AiEditor, type AiEditorOptions } from 'aieditor'
import 'aieditor/dist/style.css'
import { useAppStore } from '@/stores'
defineOptions({ name: 'AiEditor' })
const props = defineProps<{
modelValue: string
options?: AiEditorOptions
}>()
const aieditor = ref<AiEditor | null>(null)
const appStore = useAppStore()
const divRef = ref<any>()
const editorConfig = reactive<AiEditorOptions>({
element: '',
theme: appStore.theme,
placeholder: '请输入内容',
content: '',
editable: false,
})
const init = () => {
aieditor.value?.destroy()
aieditor.value = new AiEditor(editorConfig)
}
watch(() => props.modelValue, (value) => {
if (value !== aieditor.value?.getHtml()) {
editorConfig.content = value
init()
}
})
watch(() => appStore.theme, (value) => {
editorConfig.theme = value
init()
})
// 挂载阶段
onMounted(() => {
editorConfig.element = divRef.value
init()
})
// 销毁阶段
onUnmounted(() => {
aieditor.value?.destroy()
})
</script>
<style scoped lang="scss">
.container {
height: 100%;
width: 100%;
box-sizing: border-box;
}
.aie-header-panel {
position: sticky;
// top: 51px;
z-index: 1;
}
.aie-header-panel aie-header>div {
align-items: center;
justify-content: center;
padding: 10px 0;
}
.aie-container {
border: none !important;
}
.aie-container-panel {
width: calc(100% - 2rem - 2px);
max-width: 826.77px;
margin: 0rem auto;
border: 1px solid var(--color-border-1);
background-color: var() rgba($color: var(--color-bg-1), $alpha: 1.0);
height: 100%;
padding: 1rem;
z-index: 99;
overflow: auto;
box-sizing: border-box;
}
.aie-main {
position: relative;
overflow: hidden;
flex: 1;
box-sizing: border-box;
padding: 1rem 0px;
background-color: var(--color-bg-1);
}
.aie-directory {
position: absolute;
top: 30px;
left: 10px;
width: 260px;
z-index: 0;
}
.aie-title1 {
font-size: 14px;
font-weight: 500;
}
</style>

View File

@@ -1,92 +0,0 @@
<template>
<div ref="containerRef" class="detail">
<div class="detail_header">
<a-affix :target="(containerRef as HTMLElement)">
<a-page-header title="通知公告" subtitle="查看" @back="onBack">
</a-page-header>
</a-affix>
</div>
<div class="detail_content">
<h1 class="title">{{ form?.title }}</h1>
<div class="info">
<a-space>
<span>
<icon-user class="icon" />
<span class="label">发布人</span>
<span>{{ form?.createUserString }}</span>
</span>
<a-divider direction="vertical" />
<span>
<icon-history class="icon" />
<span class="label">发布时间</span>
<span>{{ form?.effectiveTime ? form?.effectiveTime : form?.createTime
}}</span>
</span>
<a-divider v-if="form?.updateTime" direction="vertical" />
<span v-if="form?.updateTime">
<icon-schedule class="icon" />
<span>更新时间</span>
<span>{{ form?.updateTime }}</span>
</span>
</a-space>
</div>
<div style="flex: 1;">
<AiEditor v-model="form.content" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import AiEditor from './components/index.vue'
import { getNotice } from '@/apis/system/notice'
import { useTabsStore } from '@/stores'
import { useResetReactive } from '@/hooks'
const route = useRoute()
const router = useRouter()
const tabsStore = useTabsStore()
const { id } = route.query
const containerRef = ref<HTMLElement | null>()
const [form, resetForm] = useResetReactive({
title: '',
createUserString: '',
effectiveTime: '',
createTime: '',
content: '',
})
// 回退
const onBack = () => {
router.back()
tabsStore.closeCurrent(route.path)
}
// 打开
const onOpen = async (id: string) => {
resetForm()
const { data } = await getNotice(id)
Object.assign(form, data)
}
onMounted(() => {
onOpen(id as string)
})
</script>
<style scoped lang="scss">
.detail_content {
.title {
text-align: center;
}
.info {
text-align: center;
}
.icon {
margin-right: 3px;
}
}
</style>

View File

@@ -1,133 +0,0 @@
<template>
<div class="gi_table_page">
<GiTable
row-key="id"
title=""
:data="dataList"
:columns="columns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1200 }"
:pagination="pagination"
:disabled-tools="['size']"
:disabled-column-keys="['title']"
@refresh="search"
>
<template #toolbar-left>
<a-input-search v-model="queryForm.title" placeholder="搜索标题" allow-clear @search="search" />
<a-select
v-model="queryForm.type"
:options="notice_type"
placeholder="请选择类型"
allow-clear
style="width: 150px"
@change="search"
/>
<a-button @click="reset">
<template #icon><icon-refresh /></template>
<template #default>重置</template>
</a-button>
</template>
<template #toolbar-right>
<a-button v-permission="['system:notice:add']" type="primary" @click="onAdd">
<template #icon><icon-plus /></template>
<template #default>新增</template>
</a-button>
</template>
<template #type="{ record }">
<GiCellTag :value="record.type" :dict="notice_type" />
</template>
<template #status="{ record }">
<GiCellTag :value="record.status" :dict="notice_status_enum" />
</template>
<template #action="{ record }">
<a-space>
<a-link v-permission="['system:notice:detail']" title="详情" @click="onDetail(record)">详情</a-link>
<a-link v-permission="['system:notice:update']" title="修改" @click="onUpdate(record)">修改</a-link>
<a-link v-permission="['system:notice:delete']" status="danger" title="删除" @click="onDelete(record)"> 删除 </a-link>
</a-space>
</template>
</GiTable>
</div>
</template>
<script setup lang="ts">
import { type NoticeQuery, type NoticeResp, deleteNotice, listNotice } from '@/apis/system'
import type { TableInstanceColumns } from '@/components/GiTable/type'
import { useTable } from '@/hooks'
import { useDict } from '@/hooks/app'
import { isMobile } from '@/utils'
import has from '@/utils/has'
defineOptions({ name: 'SystemNotice' })
const { notice_type, notice_status_enum } = useDict('notice_type', 'notice_status_enum')
const router = useRouter()
const queryForm = reactive<NoticeQuery>({
sort: ['id,desc'],
})
const {
tableData: dataList,
loading,
pagination,
search,
handleDelete,
} = useTable((page) => listNotice({ ...queryForm, ...page }), { immediate: true })
const columns: TableInstanceColumns[] = [
{
title: '序号',
width: 66,
align: 'center',
render: ({ rowIndex }) => h('span', {}, rowIndex + 1 + (pagination.current - 1) * pagination.pageSize),
},
{ title: '标题', dataIndex: 'title', slotName: 'title', minWidth: 200, ellipsis: true, tooltip: true },
{ title: '类型', dataIndex: 'type', slotName: 'type', align: 'center' },
{ title: '状态', dataIndex: 'status', slotName: 'status', align: 'center' },
{ title: '生效时间', dataIndex: 'effectiveTime', width: 180 },
{ title: '终止时间', dataIndex: 'terminateTime', width: 180 },
{ title: '创建人', dataIndex: 'createUserString', show: false, ellipsis: true, tooltip: true },
{ title: '创建时间', dataIndex: 'createTime', width: 180 },
{
title: '操作',
dataIndex: 'action',
slotName: 'action',
width: 160,
align: 'center',
fixed: !isMobile() ? 'right' : undefined,
show: has.hasPermOr(['system:notice:detail', 'system:notice:update', 'system:notice:delete']),
},
]
// 重置
const reset = () => {
queryForm.title = undefined
queryForm.type = undefined
search()
}
// 删除
const onDelete = (record: NoticeResp) => {
return handleDelete(() => deleteNotice(record.id), {
content: `是否确定删除公告「${record.title}」?`,
showModal: true,
})
}
// 新增
const onAdd = () => {
router.push({ path: '/system/notice/add' })
}
// 修改
const onUpdate = (record: NoticeResp) => {
router.push({ path: '/system/notice/add', query: { id: record.id, type: 'update' } })
}
// 详情
const onDetail = (record: NoticeResp) => {
router.push({ path: '/system/notice/detail', query: { id: record.id } })
}
</script>
<style scoped lang="scss"></style>

View File

@@ -49,7 +49,7 @@ const columns: ColumnItem[] = reactive([
field: 'configKey', field: 'configKey',
type: 'input', type: 'input',
span: 24, span: 24,
disabled: true, disabled: isUpdate.value,
}, },
{ {
label: '参数键值', label: '参数键值',

View File

@@ -13,7 +13,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Message, type TreeNodeData } from '@arco-design/web-vue' import { Message } from '@arco-design/web-vue'
import { useWindowSize } from '@vueuse/core' import { useWindowSize } from '@vueuse/core'
import { addUser, getUser, updateUser } from '@/apis/system/user' import { addUser, getUser, updateUser } from '@/apis/system/user'
import { type ColumnItem, GiForm } from '@/components/GiForm' import { type ColumnItem, GiForm } from '@/components/GiForm'
@@ -53,16 +53,6 @@ const columns: ColumnItem[] = reactive([
maxLength: 64, maxLength: 64,
}, },
}, },
{
label: '昵称',
field: 'nickname',
type: 'input',
span: 24,
required: true,
props: {
maxLength: 30,
},
},
{ {
label: '密码', label: '密码',
field: 'password', field: 'password',
@@ -85,13 +75,10 @@ const columns: ColumnItem[] = reactive([
}, },
}, },
{ {
label: '邮箱', label: '卡号',
field: 'email', field: 'cardNo',
type: 'input', type: 'input',
span: 24, span: 24,
props: {
maxLength: 255,
},
}, },
{ {
label: '性别', label: '性别',
@@ -102,25 +89,6 @@ const columns: ColumnItem[] = reactive([
options: GenderList, options: GenderList,
}, },
}, },
{
label: '所属部门',
field: 'deptId',
type: 'tree-select',
span: 24,
required: true,
props: {
data: deptList,
allowClear: true,
allowSearch: true,
fallbackOption: false,
filterTreeNode(searchKey: string, nodeData: TreeNodeData) {
if (nodeData.title) {
return nodeData.title.toLowerCase().includes(searchKey.toLowerCase())
}
return false
},
},
},
{ {
label: '角色', label: '角色',
field: 'roleIds', field: 'roleIds',

View File

@@ -1,11 +1,7 @@
<template> <template>
<div class="gi_page"> <div class="gi_table_page">
<SplitPanel size="20%">
<template #left>
<DeptTree @node-click="handleSelectDept" />
</template>
<template #main>
<GiTable <GiTable
title="用户管理"
row-key="id" row-key="id"
:data="dataList" :data="dataList"
:columns="columns" :columns="columns"
@@ -17,24 +13,32 @@
@refresh="search" @refresh="search"
> >
<template #top> <template #top>
<GiForm v-model="queryForm" search :columns="queryFormColumns" size="medium" @search="search" @reset="reset"></GiForm> <GiForm
v-model="queryForm"
search :columns="queryFormColumns"
size="medium"
@search="search"
@reset="reset"
></GiForm>
</template> </template>
<template #toolbar-left> <template #toolbar-left>
<a-button v-permission="['system:user:add']" type="primary" @click="onAdd"> <a-button v-permission="['system:user:add']" type="primary" style="margin-right: 8px;" @click="onAdd">
<template #icon><icon-plus /></template> <template #icon><icon-plus /></template>
<template #default>新增</template> <template #default>新增</template>
</a-button> </a-button>
<a-button v-permission="['system:user:import']" @click="onImport"> <a-button v-permission="['system:user:import']" style="margin-right: 8px;" @click="onImport">
<template #icon><icon-upload /></template> <template #icon><icon-upload /></template>
<template #default>导入</template> <template #default>导入</template>
</a-button> </a-button>
</template>
<template #toolbar-right>
<a-button v-permission="['system:user:export']" @click="onExport"> <a-button v-permission="['system:user:export']" @click="onExport">
<template #icon><icon-download /></template> <template #icon><icon-download /></template>
<template #default>导出</template> <template #default>导出</template>
</a-button> </a-button>
</template> </template>
<template #toolbar-right>
</template>
<!-- 表格插槽内容 -->
<template #nickname="{ record }"> <template #nickname="{ record }">
<GiCellAvatar :avatar="record.avatar" :name="record.nickname" /> <GiCellAvatar :avatar="record.avatar" :name="record.nickname" />
</template> </template>
@@ -53,7 +57,6 @@
</template> </template>
<template #action="{ record }"> <template #action="{ record }">
<a-space> <a-space>
<a-link v-permission="['system:user:detail']" title="详情" @click="onDetail(record)">详情</a-link>
<a-link v-permission="['system:user:update']" title="修改" @click="onUpdate(record)">修改</a-link> <a-link v-permission="['system:user:update']" title="修改" @click="onUpdate(record)">修改</a-link>
<a-link <a-link
v-permission="['system:user:delete']" v-permission="['system:user:delete']"
@@ -78,22 +81,19 @@
</a-space> </a-space>
</template> </template>
</GiTable> </GiTable>
</template>
</SplitPanel>
<!-- 弹窗/抽屉组件 -->
<UserAddDrawer ref="UserAddDrawerRef" @save-success="search" /> <UserAddDrawer ref="UserAddDrawerRef" @save-success="search" />
<UserImportDrawer ref="UserImportDrawerRef" @save-success="search" /> <UserImportDrawer ref="UserImportDrawerRef" @save-success="search" />
<UserDetailDrawer ref="UserDetailDrawerRef" />
<UserResetPwdModal ref="UserResetPwdModalRef" /> <UserResetPwdModal ref="UserResetPwdModalRef" />
<UserUpdateRoleModal ref="UserUpdateRoleModalRef" @save-success="search" /> <UserUpdateRoleModal ref="UserUpdateRoleModalRef" @save-success="search" />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import DeptTree from './dept/index.vue' import { onMounted } from 'vue'
import UserAddDrawer from './UserAddDrawer.vue' import UserAddDrawer from './UserAddDrawer.vue'
import UserImportDrawer from './UserImportDrawer.vue' import UserImportDrawer from './UserImportDrawer.vue'
import UserDetailDrawer from './UserDetailDrawer.vue'
import UserResetPwdModal from './UserResetPwdModal.vue' import UserResetPwdModal from './UserResetPwdModal.vue'
import UserUpdateRoleModal from './UserUpdateRoleModal.vue' import UserUpdateRoleModal from './UserUpdateRoleModal.vue'
import { type UserResp, deleteUser, exportUser, listUser } from '@/apis/system/user' import { type UserResp, deleteUser, exportUser, listUser } from '@/apis/system/user'
@@ -102,30 +102,35 @@ 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' })
// 查询表单
const [queryForm, resetForm] = useResetReactive({ const [queryForm, resetForm] = useResetReactive({
description: undefined,
status: undefined,
createTime: undefined,
sort: ['t1.id,desc'], sort: ['t1.id,desc'],
}) })
const queryFormColumns: ColumnItem[] = reactive([ const queryFormColumns: ColumnItem[] = reactive([
{ {
type: 'input', type: 'input',
field: 'description', field: 'description',
span: { xs: 24, sm: 8, xxl: 8 }, span: { xs: 24, sm: 4, xxl: 3 },
formItemProps: { formItemProps: {
hideLabel: true, hideLabel: true,
}, },
props: { props: {
placeholder: '搜索用户名/昵称/描述', placeholder: '搜索用户名',
showWordLimit: false, showWordLimit: false,
}, },
}, },
{ {
type: 'select', type: 'select',
field: 'status', field: 'status',
span: { xs: 24, sm: 6, xxl: 8 }, span: { xs: 24, sm: 4, xxl: 4 },
formItemProps: { formItemProps: {
hideLabel: true, hideLabel: true,
}, },
@@ -137,13 +142,14 @@ const queryFormColumns: ColumnItem[] = reactive([
{ {
type: 'range-picker', type: 'range-picker',
field: 'createTime', field: 'createTime',
span: { xs: 24, sm: 10, xxl: 8 }, span: { xs: 24, sm: 8, xxl: 5 },
formItemProps: { formItemProps: {
hideLabel: true, hideLabel: true,
}, },
}, },
]) ])
// 表格数据
const { const {
tableData: dataList, tableData: dataList,
loading, loading,
@@ -151,6 +157,8 @@ const {
search, search,
handleDelete, handleDelete,
} = useTable((page) => listUser({ ...queryForm, ...page }), { immediate: false }) } = useTable((page) => listUser({ ...queryForm, ...page }), { immediate: false })
// 表格列配置
const columns: TableInstanceColumns[] = [ const columns: TableInstanceColumns[] = [
{ {
title: '序号', title: '序号',
@@ -159,22 +167,12 @@ const columns: TableInstanceColumns[] = [
render: ({ rowIndex }) => h('span', {}, rowIndex + 1 + (pagination.current - 1) * pagination.pageSize), render: ({ rowIndex }) => h('span', {}, rowIndex + 1 + (pagination.current - 1) * pagination.pageSize),
fixed: !isMobile() ? 'left' : undefined, fixed: !isMobile() ? 'left' : undefined,
}, },
{ { title: '用户名', dataIndex: 'username', slotName: 'username', minWidth: 100, ellipsis: true, tooltip: true },
title: '昵称', { title:'卡号', dataIndex: 'cardNo', slotName: 'cardNo' },
dataIndex: 'nickname',
slotName: 'nickname',
minWidth: 140,
ellipsis: true,
tooltip: true,
fixed: !isMobile() ? 'left' : undefined,
},
{ title: '用户名', dataIndex: 'username', slotName: 'username', minWidth: 140, ellipsis: true, tooltip: true },
{ 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: 'deptName', minWidth: 180, ellipsis: true, tooltip: true },
{ title: '角色', dataIndex: 'roleNames', slotName: 'roleNames', minWidth: 165 }, { title: '角色', dataIndex: 'roleNames', slotName: 'roleNames', minWidth: 165 },
{ title: '手机号', dataIndex: 'phone', minWidth: 170, ellipsis: true, tooltip: true }, { title: '手机号', dataIndex: 'phone', minWidth: 170, ellipsis: true, tooltip: true },
{ title: '邮箱', dataIndex: 'email', minWidth: 170, ellipsis: true, tooltip: true },
{ title: '系统内置', dataIndex: 'isSystem', slotName: 'isSystem', width: 100, align: 'center', show: false }, { title: '系统内置', dataIndex: 'isSystem', slotName: 'isSystem', width: 100, align: 'center', show: false },
{ title: '描述', dataIndex: 'description', minWidth: 130, ellipsis: true, tooltip: true }, { title: '描述', dataIndex: 'description', minWidth: 130, ellipsis: true, tooltip: true },
{ title: '创建人', dataIndex: 'createUserString', width: 140, ellipsis: true, tooltip: true, show: false }, { title: '创建人', dataIndex: 'createUserString', width: 140, ellipsis: true, tooltip: true, show: false },
@@ -198,13 +196,12 @@ const columns: TableInstanceColumns[] = [
}, },
] ]
// 重置 // 方法定义
const reset = () => { const reset = () => {
resetForm() resetForm()
search() search()
} }
// 删除
const onDelete = (record: UserResp) => { const onDelete = (record: UserResp) => {
return handleDelete(() => deleteUser(record.id), { return handleDelete(() => deleteUser(record.id), {
content: `是否确定删除用户「${record.nickname}(${record.username})」?`, content: `是否确定删除用户「${record.nickname}(${record.username})」?`,
@@ -212,60 +209,64 @@ const onDelete = (record: UserResp) => {
}) })
} }
// 导出
const onExport = () => { const onExport = () => {
useDownload(() => exportUser(queryForm)) useDownload(() => exportUser(queryForm))
} }
// 根据选中部门查询
const handleSelectDept = (keys: Array<any>) => {
queryForm.deptId = keys.length === 1 ? keys[0] : undefined
search()
}
const UserImportDrawerRef = ref<InstanceType<typeof UserImportDrawer>>() const UserImportDrawerRef = ref<InstanceType<typeof UserImportDrawer>>()
// 导入
const onImport = () => { const onImport = () => {
UserImportDrawerRef.value?.onOpen() UserImportDrawerRef.value?.onOpen()
} }
const UserAddDrawerRef = ref<InstanceType<typeof UserAddDrawer>>() const UserAddDrawerRef = ref<InstanceType<typeof UserAddDrawer>>()
// 新增
const onAdd = () => { const onAdd = () => {
UserAddDrawerRef.value?.onAdd() UserAddDrawerRef.value?.onAdd()
} }
// 修改
const onUpdate = (record: UserResp) => { const onUpdate = (record: UserResp) => {
UserAddDrawerRef.value?.onUpdate(record.id) UserAddDrawerRef.value?.onUpdate(record.id)
} }
const UserDetailDrawerRef = ref<InstanceType<typeof UserDetailDrawer>>()
// 详情
const onDetail = (record: UserResp) => {
UserDetailDrawerRef.value?.onOpen(record.id)
}
const UserResetPwdModalRef = ref<InstanceType<typeof UserResetPwdModal>>() const UserResetPwdModalRef = ref<InstanceType<typeof UserResetPwdModal>>()
// 重置密码
const onResetPwd = (record: UserResp) => { const onResetPwd = (record: UserResp) => {
UserResetPwdModalRef.value?.onOpen(record.id) UserResetPwdModalRef.value?.onOpen(record.id)
} }
const UserUpdateRoleModalRef = ref<InstanceType<typeof UserUpdateRoleModal>>() const UserUpdateRoleModalRef = ref<InstanceType<typeof UserUpdateRoleModal>>()
// 分配角色
const onUpdateRole = (record: UserResp) => { const onUpdateRole = (record: UserResp) => {
UserUpdateRoleModalRef.value?.onOpen(record.id) UserUpdateRoleModalRef.value?.onOpen(record.id)
} }
onMounted(() => {
search()
})
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.page_header { .gi_table_page {
flex: 0 0 auto; padding: 16px;
height: 100%;
box-sizing: border-box;
} }
.page_content { :deep(.gi-table) {
flex: 1; background: #ffffff;
overflow: auto; }
:deep(.arco-table-toolbar-left) {
display: flex;
align-items: center;
flex-wrap: wrap; // 允许在小屏幕换行
gap: 8px; // 统一间距
.arco-input-search,
.arco-select,
.arco-range-picker {
}
}
:deep(.arco-table-toolbar-right) {
display: flex;
align-items: center;
gap: 8px;
} }
</style> </style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,282 @@
<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, getWorkOrderInfos,
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 = []
getWorkOrderInfos(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: {
workerOrderId: record.id,
}
})
}
</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>