kkk-ops 4 weeks ago
commit e6bd9cc133

@ -65,6 +65,15 @@ export interface HistoryRecordParams {
collectionEndTime?: string
}
export interface DeviceContactModelVO {
id: number
attributeCode?: string
attributeName?: string
attributeType?: string
dataType?: string
dataUnit?: string
}
// 物联设备 API
export const DeviceApi = {
// 查询物联设备分页
@ -127,6 +136,10 @@ export const DeviceApi = {
getDeviceAttributePage: async (params) => {
return await request.get({ url: `/iot/device/device-attribute/page`, params })
},
getDeviceContactModelPage: async () => {
return await request.get({ url: `/iot/device-contact-model/page` })
},
// 新增设备属性
createDeviceAttribute: async (data) => {
return await request.post({ url: `/iot/device-contact-model/create`, data })

@ -1,23 +1,33 @@
import request from '@/config/axios'
export interface RecipeConfigVO {
id: number
recipeCode: string
recipeName: string
recipeType?: string
recipeName?: string
name?: string
recipeType?: string | number
productId?: number
productName?: string
machineName?: string
deviceId?: number
deviceName?: string
recipeDesc?: string
remark?: string
isEnable?: string | number
dataUnit?: string
createTime?: string
}
export interface RecipePointDetailVO {
id: number
recipeId: number
recipeId: string | number
attributeId?: number
pointName: string
pointType: string
dataType: string
dataUnit: string
pointName?: string
pointType?: string
dataType?: string
dataUnit?: string
attributeName?: string
attributeType?: string
}
export interface RecipePointCandidateVO {
@ -31,7 +41,6 @@ export interface RecipePointCandidateVO {
type PageResult<T> = { list: T[]; total: number }
const STORAGE_KEYS = {
recipeList: 'mock:recipeConfig:recipes',
pointCandidates: 'mock:recipeConfig:pointCandidates',
recipePointRelation: 'mock:recipeConfig:recipePointRelation'
} as const
@ -54,23 +63,6 @@ const saveToStorage = (key: string, value: any) => {
window.localStorage.setItem(key, JSON.stringify(value))
}
const buildMockRecipes = (): RecipeConfigVO[] => {
return Array.from({ length: 18 }).map((_, idx) => {
const no = String(idx + 1).padStart(3, '0')
return {
id: idx + 1,
recipeCode: `RCP-${no}`,
recipeName: `配方-${no}`,
recipeType: idx % 2 === 0 ? '标准' : '自定义',
productId: undefined,
productName: idx % 3 === 0 ? '产品A' : idx % 3 === 1 ? '产品B' : '产品C',
deviceId: undefined,
deviceName: idx % 2 === 0 ? '设备1' : '设备2',
remark: idx % 4 === 0 ? '示例数据' : ''
}
})
}
const buildMockPointCandidates = (): RecipePointCandidateVO[] => {
const pointTypes = ['模拟量', '开关量', '计算量']
const dataTypes = ['int', 'float', 'string', 'bool']
@ -88,9 +80,6 @@ const buildMockPointCandidates = (): RecipePointCandidateVO[] => {
}
const ensureMockSeeded = () => {
const recipeList = loadFromStorage<RecipeConfigVO[]>(STORAGE_KEYS.recipeList, [])
if (!recipeList.length) saveToStorage(STORAGE_KEYS.recipeList, buildMockRecipes())
const pointCandidates = loadFromStorage<RecipePointCandidateVO[]>(STORAGE_KEYS.pointCandidates, [])
if (!pointCandidates.length) saveToStorage(STORAGE_KEYS.pointCandidates, buildMockPointCandidates())
@ -119,103 +108,53 @@ const contains = (value: string | undefined, keyword: string | undefined) => {
export const RecipeConfigApi = {
getRecipeConfigPage: async (params: any) => {
ensureMockSeeded()
await sleep(120)
const recipeCode = params?.recipeCode
const recipeName = params?.recipeName
const productName = params?.productName
const pageNo = params?.pageNo
const pageSize = params?.pageSize
const list = loadFromStorage<RecipeConfigVO[]>(STORAGE_KEYS.recipeList, buildMockRecipes())
const filtered = list.filter((item) => {
return (
contains(item.recipeCode, recipeCode) &&
contains(item.recipeName, recipeName) &&
contains(item.productName, productName)
)
})
return paginate(filtered, pageNo, pageSize)
const finalParams = {
...(params ?? {}),
name: params?.name ?? params?.recipeName
}
return await request.get({ url: `/iot/recipe/page`, params: finalParams })
},
createRecipeConfig: async (data: Partial<RecipeConfigVO>) => {
ensureMockSeeded()
await sleep(120)
const list = loadFromStorage<RecipeConfigVO[]>(STORAGE_KEYS.recipeList, buildMockRecipes())
const maxId = list.reduce((acc, cur) => Math.max(acc, cur.id), 0)
const nextId = maxId + 1
const newItem: RecipeConfigVO = {
id: nextId,
recipeCode: data.recipeCode ?? '',
recipeName: data.recipeName ?? '',
const finalData = {
id: data.id,
name: data.name ?? data.recipeName,
recipeCode: data.recipeCode,
recipeType: data.recipeType,
productId: data.productId,
productName: data.productName,
deviceId: data.deviceId,
deviceName: data.deviceName,
remark: data.remark
machineName: data.machineName ?? data.deviceName,
recipeDesc: data.recipeDesc ?? data.remark,
isEnable: data.isEnable,
dataUnit: data.dataUnit
}
saveToStorage(STORAGE_KEYS.recipeList, [newItem, ...list])
return nextId
return await request.post({ url: `/iot/recipe/create`, data: finalData })
},
updateRecipeConfig: async (data: Partial<RecipeConfigVO>) => {
ensureMockSeeded()
await sleep(120)
if (!data.id) return true
const list = loadFromStorage<RecipeConfigVO[]>(STORAGE_KEYS.recipeList, buildMockRecipes())
const next = list.map((item) => {
if (item.id !== data.id) return item
return {
...item,
recipeCode: data.recipeCode ?? item.recipeCode,
recipeName: data.recipeName ?? item.recipeName,
recipeType: data.recipeType ?? item.recipeType,
productId: data.productId ?? item.productId,
productName: data.productName ?? item.productName,
deviceId: data.deviceId ?? item.deviceId,
deviceName: data.deviceName ?? item.deviceName,
remark: data.remark ?? item.remark
}
})
saveToStorage(STORAGE_KEYS.recipeList, next)
return true
const finalData = {
id: data.id,
name: data.name ?? data.recipeName,
recipeCode: data.recipeCode,
recipeType: data.recipeType,
productName: data.productName,
machineName: data.machineName ?? data.deviceName,
recipeDesc: data.recipeDesc ?? data.remark,
isEnable: data.isEnable,
dataUnit: data.dataUnit
}
return await request.put({ url: `/iot/recipe/update`, data: finalData })
},
deleteRecipeConfig: async (id: number) => {
ensureMockSeeded()
await sleep(120)
const list = loadFromStorage<RecipeConfigVO[]>(STORAGE_KEYS.recipeList, buildMockRecipes())
saveToStorage(
STORAGE_KEYS.recipeList,
list.filter((item) => item.id !== id)
)
const relation = loadFromStorage<Record<string, number[]>>(STORAGE_KEYS.recipePointRelation, {})
delete relation[String(id)]
saveToStorage(STORAGE_KEYS.recipePointRelation, relation)
return true
return await request.delete({ url: `/iot/recipe/delete?id=` + id })
},
exportRecipeConfig: async (params: any) => {
ensureMockSeeded()
await sleep(120)
const ids = String(params?.ids ?? '')
.split(',')
.map((v) => Number(v))
.filter((v) => !Number.isNaN(v))
const list = loadFromStorage<RecipeConfigVO[]>(STORAGE_KEYS.recipeList, buildMockRecipes())
const exportList = ids.length ? list.filter((item) => ids.includes(item.id)) : list
const header = ['配方编码', '配方名称', '配方类型', '关联产品', '关联设备', '备注']
const rows = exportList.map((item) => [
item.recipeCode,
item.recipeName,
item.recipeType ?? '',
item.productName ?? '',
item.deviceName ?? '',
item.remark ?? ''
])
const csv = [header, ...rows].map((r) => r.map((v) => `"${String(v).replace(/"/g, '""')}"`).join(',')).join('\n')
return new Blob([csv], { type: 'text/csv;charset=utf-8' })
const finalParams = {
...(params ?? {}),
name: params?.name ?? params?.recipeName
}
return await request.download({ url: `/iot/recipe/export-excel`, params: finalParams })
},
getPointCandidatePage: async (params: any): Promise<PageResult<RecipePointCandidateVO>> => {
@ -235,39 +174,10 @@ export const RecipeConfigApi = {
},
getRecipePointDetailPage: async (params: any) => {
ensureMockSeeded()
await sleep(120)
const recipeId = Number(params?.recipeId)
const pageNo = params?.pageNo
const pageSize = params?.pageSize
if (!recipeId) return { list: [], total: 0 }
const relation = loadFromStorage<Record<string, number[]>>(STORAGE_KEYS.recipePointRelation, {})
const selectedIds = relation[String(recipeId)] ?? []
const candidates = loadFromStorage<RecipePointCandidateVO[]>(
STORAGE_KEYS.pointCandidates,
buildMockPointCandidates()
)
const map = candidates.reduce((acc, cur) => {
acc[cur.id] = cur
return acc
}, {} as Record<number, RecipePointCandidateVO>)
const detailAll: RecipePointDetailVO[] = selectedIds
.map((id) => map[id])
.filter(Boolean)
.map((item) => ({
id: item.id,
recipeId,
attributeId: item.id,
pointName: item.pointName,
pointType: item.pointType,
dataType: item.dataType,
dataUnit: item.dataUnit
}))
return paginate(detailAll, pageNo, pageSize)
return await request.get({ url: `/iot/recipe-device-attribute/page`, params })
},
saveRecipePointConfig: async (data: { recipeId: number; attributeIds: number[] }) => {
saveRecipePointConfig: async (data: { recipeId: string | number; attributeIds: number[] }) => {
ensureMockSeeded()
await sleep(120)
const relation = loadFromStorage<Record<string, number[]>>(STORAGE_KEYS.recipePointRelation, {})

@ -0,0 +1,31 @@
import request from '@/config/axios'
export interface RecipePointVO {
id: number
recipeId: string | number
name?: string
max?: string | number
min?: string | number
dataType?: string
dataUnit?: string
remark?: string
createTime?: string
}
export const RecipePointApi = {
getRecipePointPage: async (params: any) => {
return await request.get({ url: `/iot/recipe-point/page`, params })
},
createRecipePoint: async (params: Partial<RecipePointVO>) => {
return await request.post({ url: `/iot/recipe-point/create`, data: params })
},
updateRecipePoint: async (params: Partial<RecipePointVO>) => {
return await request.put({ url: `/iot/recipe-point/update`, data: params })
},
deleteRecipePoint: async (id: number) => {
return await request.delete({ url: `/iot/recipe-point/delete`, params: { id } })
}
}

@ -0,0 +1,62 @@
import request from '@/config/axios'
// 设备类型 VO
export interface DeviceLedgerVO {
id: number // id
deviceCode: string // 设备编号
deviceName: string // 设备名称
deviceStatus: number // 设备状态 (0-正常, 1-停用, 2-维修, 3-报废)
deviceBrand: string // 设备品牌
deviceModel: string // 设备型号
deviceSpec: string // 设备规格
deviceType: string | number // 设备类型
deviceTypeName?: string // 设备类型名称
supplier: string // 供应商
workshop: string // 所属车间
systemOrg: string // 所属系统组织
deviceLocation: string // 设备位置
useDept?: string // 使用部门
deviceManager: string // 设备负责人
quantity?: number // 数量
productionDate: Date // 设备生产日期
factoryEntryDate: Date // 设备入厂日期
deviceRemark: string // 设备备注
remark: string // 备注
creator?: string // 创建人
createTime?: string | number | Date
updateTime?: string | number | Date
sort: number // 排序
}
// 设备类型 API
export const DeviceLedgerApi = {
// 查询设备类型分页
getDeviceLedgerPage: async (params: any) => {
return await request.get({ url: `/mes/device-ledger/page`, params })
},
// 查询设备类型详情
getDeviceLedger: async (id: number) => {
return await request.get({ url: `/mes/device-ledger/get?id=` + id })
},
// 新增设备类型
createDeviceLedger: async (data: DeviceLedgerVO) => {
return await request.post({ url: `/mes/device-ledger/create`, data })
},
// 修改设备类型
updateDeviceLedger: async (data: DeviceLedgerVO) => {
return await request.put({ url: `/mes/device-ledger/update`, data })
},
// 删除设备类型
deleteDeviceLedger: async (id: number) => {
return await request.delete({ url: `/mes/device-ledger/delete?id=` + id })
},
// 导出设备类型 Excel
exportDeviceLedger: async (params) => {
return await request.download({ url: `/mes/device-ledger/export-excel`, params })
}
}

@ -0,0 +1,58 @@
import request from '@/config/axios'
// 设备类型 VO
export interface DeviceTypeVO {
id: number // id
code: string // 编码
name: string // 名称
remark: string // 备注
sort: number // 排序
parentId?: number
parentChain?: string
createTime?: string
}
export interface DeviceTypeTreeVO extends DeviceTypeVO {
parentId?: number
parentChain?: string
createTime?: string
children?: DeviceTypeTreeVO[]
leaf?: boolean
}
// 设备类型 API
export const DeviceTypeApi = {
// 查询设备类型分页
getDeviceTypePage: async (params: any) => {
return await request.get({ url: `/mes/device-type/page`, params })
},
getDeviceTypeTree: async (params: any) => {
return await request.get({ url: `/mes/device-type/tree`, params })
},
// 查询设备类型详情
getDeviceType: async (id: number) => {
return await request.get({ url: `/mes/device-type/get?id=` + id })
},
// 新增设备类型
createDeviceType: async (data: DeviceTypeVO) => {
return await request.post({ url: `/mes/device-type/create`, data })
},
// 修改设备类型
updateDeviceType: async (data: DeviceTypeVO) => {
return await request.put({ url: `/mes/device-type/update`, data })
},
// 删除设备类型
deleteDeviceType: async (id: number) => {
return await request.delete({ url: `/mes/device-type/delete?id=` + id })
},
// 导出设备类型 Excel
exportDeviceType: async (params) => {
return await request.download({ url: `/mes/device-type/export-excel`, params })
}
}

@ -165,8 +165,8 @@ $toolbar-position: -55px;
width: 80px;
height: 25px;
font-size: 12px;
color: #6a6a6a;
line-height: 25px;
color: #6a6a6a;
text-align: center;
background: #fff;
box-shadow:

@ -94,9 +94,9 @@ const handleCloneComponent = (component: DiyComponent<any>) => {
<style scoped lang="scss">
.editor-left {
z-index: 1;
flex-shrink: 0;
user-select: none;
box-shadow: 8px 0 8px -8px rgb(0 0 0 / 12%);
user-select: none;
flex-shrink: 0;
:deep(.el-collapse) {
border-top: none;

@ -55,12 +55,12 @@ const handleToggleFab = () => {
/* 模态背景 */
.modal-bg {
position: absolute;
left: calc(50% - 375px / 2);
top: 0;
left: calc(50% - 375px / 2);
z-index: 11;
width: 375px;
height: 100%;
background-color: rgba(#000000, 0.4);
background-color: rgba(#000, 0.4);
}
.fab-icon {

@ -192,39 +192,39 @@ const handleAppLinkChange = (appLink: AppLink) => {
<style scoped lang="scss">
.hot-zone {
position: absolute;
z-index: 10;
display: flex;
font-size: 16px;
color: var(--el-color-primary);
cursor: move;
background: var(--el-color-primary-light-7);
opacity: 0.8;
border: 1px solid var(--el-color-primary);
color: var(--el-color-primary);
font-size: 16px;
display: flex;
opacity: 0.8;
align-items: center;
justify-content: center;
cursor: move;
z-index: 10;
/* 控制点 */
.ctrl-dot {
position: absolute;
z-index: 11;
width: 8px;
height: 8px;
border-radius: 50%;
border: inherit;
background-color: #fff;
z-index: 11;
border: inherit;
border-radius: 50%;
}
.delete {
display: none;
position: absolute;
top: 0;
right: 0;
display: none;
padding: 2px 2px 6px 6px;
background-color: var(--el-color-primary);
border-radius: 0 0 0 80%;
cursor: pointer;
color: #fff;
text-align: right;
cursor: pointer;
background-color: var(--el-color-primary);
border-radius: 0 0 0 80%;
}
&:hover {

@ -28,15 +28,15 @@ const props = defineProps<{ property: HotZoneProperty }>()
<style scoped lang="scss">
.hot-zone {
position: absolute;
z-index: 10;
display: flex;
font-size: 14px;
color: var(--el-color-primary);
cursor: move;
background: var(--el-color-primary-light-7);
opacity: 0.8;
border: 1px solid var(--el-color-primary);
color: var(--el-color-primary);
font-size: 14px;
display: flex;
opacity: 0.8;
align-items: center;
justify-content: center;
cursor: move;
z-index: 10;
}
</style>

@ -42,22 +42,22 @@ const handleOpenEditDialog = () => {
<style scoped lang="scss">
.hot-zone {
position: absolute;
display: flex;
font-size: 12px;
color: #fff;
cursor: move;
background: #409effbf;
border: 1px solid var(--el-color-primary);
color: #fff;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
cursor: move;
/* 控制点 */
.ctrl-dot {
position: absolute;
width: 4px;
height: 4px;
border-radius: 50%;
background-color: #fff;
border-radius: 50%;
}
}
</style>

@ -103,13 +103,16 @@ watch(
.el-carousel__indicator {
padding-top: 0;
padding-bottom: 0;
.el-carousel__button {
--el-carousel-indicator-height: 6px;
--el-carousel-indicator-width: 6px;
--el-carousel-indicator-out-color: #ff6000;
border-radius: 6px;
}
}
.el-carousel__indicator.is-active {
.el-carousel__button {
--el-carousel-indicator-width: 12px;

@ -64,10 +64,10 @@ const getSearchProp = (cell: NavigationBarCellProperty) => {
.navigation-bar {
display: flex;
height: 50px;
padding: 0 6px;
background: #fff;
justify-content: space-between;
align-items: center;
padding: 0 6px;
/* 左边 */
.left {

@ -545,11 +545,12 @@ $toolbar-height: 42px;
gap: 8px;
:deep(.el-tag) {
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.1);
border: none;
box-shadow: 0 2px 8px 0 rgb(0 0 0 / 10%);
.el-tag__content {
width: 100%;
display: flex;
width: 100%;
align-items: center;
justify-content: flex-start;

@ -50,6 +50,7 @@ watch(
<style scoped lang="scss">
:deep(.el-input-group__append) {
padding: 0;
.el-color-picker__trigger {
padding: 0;
border-left: none;

@ -225,15 +225,16 @@ const eachCube = (callback: (x: number, y: number, cube: Cube) => void) => {
<style lang="scss" scoped>
.cube-table {
position: relative;
border-spacing: 0;
border-collapse: collapse;
border-spacing: 0;
.cube {
border: 1px solid var(--el-border-color);
text-align: center;
color: var(--el-text-color-secondary);
text-align: center;
cursor: pointer;
border: 1px solid var(--el-border-color);
box-sizing: border-box;
&.active {
background: var(--el-color-primary-light-9);
}
@ -242,28 +243,28 @@ const eachCube = (callback: (x: number, y: number, cube: Cube) => void) => {
.hot-area {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--el-color-primary);
background: var(--el-color-primary-light-8);
color: var(--el-color-primary);
box-sizing: border-box;
border-spacing: 0;
border-collapse: collapse;
cursor: pointer;
background: var(--el-color-primary-light-8);
border: 1px solid var(--el-color-primary);
border-collapse: collapse;
border-spacing: 0;
box-sizing: border-box;
align-items: center;
justify-content: center;
.btn-delete {
z-index: 1;
position: absolute;
top: -8px;
right: -8px;
height: 16px;
width: 16px;
z-index: 1;
display: flex;
width: 16px;
height: 16px;
background-color: #fff;
border-radius: 50%;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: #fff;
}
}
}

@ -51,14 +51,14 @@ onMounted(async () => {
<style lang="scss">
.markdown-view {
font-family: PingFang SC;
max-width: 100%;
font-family: "PingFang SC";
font-size: 0.95rem;
font-weight: 400;
line-height: 1.6rem;
letter-spacing: 0em;
text-align: left;
letter-spacing: 0;
color: #3b3e55;
max-width: 100%;
text-align: left;
pre {
position: relative;
@ -69,22 +69,23 @@ onMounted(async () => {
}
code.hljs {
border-radius: 6px;
padding-top: 20px;
width: auto;
@media screen and (min-width: 1536px) {
padding-top: 20px;
border-radius: 6px;
@media screen and (width >= 1536px) {
width: 960px;
}
@media screen and (max-width: 1536px) and (min-width: 1024px) {
@media screen and (width <= 1536px) and (width >= 1024px) {
width: calc(100vw - 400px - 64px - 32px * 2);
}
@media screen and (max-width: 1024px) and (min-width: 768px) {
@media screen and (width <= 1024px) and (width >= 768px) {
width: calc(100vw - 32px * 2);
}
@media screen and (max-width: 768px) {
@media screen and (width <= 768px) {
width: calc(100vw - 16px * 2);
}
}
@ -107,9 +108,9 @@ onMounted(async () => {
h4,
h5,
h6 {
color: var(--color-G900);
margin: 24px 0 8px;
font-weight: 600;
color: var(--el-text-color-primary);
}
h1 {
@ -145,11 +146,11 @@ onMounted(async () => {
/* 列表(有序,无序) */
ul,
ol {
margin: 0 0 8px 0;
padding: 0;
margin: 0 0 8px;
font-size: 16px;
line-height: 24px;
color: #3b3e55; // var(--color-CG600);
color: #3b3e55; // var(--el-text-color-primary);
}
li {
@ -158,8 +159,8 @@ onMounted(async () => {
}
ol > li {
list-style-type: decimal;
margin-bottom: 1rem;
list-style-type: decimal;
// ,
// &:nth-child(n + 10) {
// margin-left: 30px;
@ -171,23 +172,23 @@ onMounted(async () => {
}
ul > li {
list-style-type: disc;
font-size: 16px;
line-height: 24px;
margin-right: 11px;
margin-bottom: 1rem;
font-size: 16px;
line-height: 24px;
color: #3b3e55; // var(--color-G900);
list-style-type: disc;
}
ol ul,
ol ul > li,
ul ul,
ul ul li {
margin-bottom: 1rem;
margin-left: 6px;
// list-style: circle;
font-size: 16px;
list-style: none;
margin-left: 6px;
margin-bottom: 1rem;
}
ul ul ul,

@ -139,11 +139,13 @@ defineExpose({ open }) // 提供 open 方法,用于打开弹窗
.el-transfer {
display: flex;
}
.el-transfer__buttons {
display: flex !important;
flex-direction: column-reverse;
justify-content: center;
gap: 20px;
.el-transfer__button:nth-child(2) {
margin: 0;
}

@ -466,9 +466,9 @@ onMounted(async () => {
<style lang="scss" scoped>
.button-setting-pane {
display: flex;
flex-direction: column;
font-size: 14px;
margin-top: 8px;
font-size: 14px;
flex-direction: column;
.button-setting-desc {
padding-right: 8px;

@ -296,7 +296,9 @@ const clear = () => {
$prefix-cls: #{$namespace}-setting;
.#{$prefix-cls} {
z-index: 1200;
border-radius: 6px 0 0 6px;
z-index: 1200;/*修正没有z-index会被表格层覆盖,值不要超过4000*/
/* 修正没有z-index会被表格层覆盖,值不要超过4000 */
}
</style>

@ -91,7 +91,7 @@ const handleLock = async () => {
<style lang="scss" scoped>
:global(.v-lock-dialog) {
@media (max-width: 767px) {
@media (width <= 767px) {
max-width: calc(100vw - 16px);
}
}

@ -205,6 +205,7 @@ $error-color: #ed6f6f;
font-size: 90px;
}
}
@media screen and (min-width: $screen-lg) {
span:not(.meridiem) {
font-size: 220px;
@ -216,6 +217,7 @@ $error-color: #ed6f6f;
font-size: 260px;
}
}
@media screen and (min-width: $screen-2xl) {
span:not(.meridiem) {
font-size: 320px;
@ -230,7 +232,7 @@ $error-color: #ed6f6f;
display: flex;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
background-color: rgb(0 0 0 / 50%);
backdrop-filter: blur(8px);
justify-content: center;
align-items: center;

@ -461,8 +461,8 @@ getAllApi()
</script>
<style scoped>
.demo-progress .el-progress--line {
margin-bottom: 15px;
max-width: 600px;
margin-bottom: 15px;
}
img{

@ -205,15 +205,18 @@ onMounted(() => {
<style lang="scss" scoped>
:deep() {
.el-table--fit .el-table__inner-wrapper:before {
.el-table--fit .el-table__inner-wrapper::before {
height: 0;
}
.el-card {
border-radius: 8px;
}
.el-form--inline .el-form-item {
margin-right: 10px;
}
.el-divider--horizontal {
margin-top: 6px;
}

@ -283,12 +283,13 @@ onMounted(() => {
<style lang="scss" scoped>
.process-definition-container::before {
content: '';
border-left: 1px solid #e6e6e6;
position: absolute;
left: 20.8%;
height: 100%;
border-left: 1px solid #e6e6e6;
content: '';
}
:deep() {
.definition-item-card {
.el-card__body {

@ -42,8 +42,8 @@ watch(
</script>
<style lang="scss" scoped>
.box-card {
height: 100%;
width: 100%;
height: 100%;
margin-bottom: 0;
:deep(.el-card__body) {
@ -52,9 +52,9 @@ watch(
}
:deep(.process-viewer) {
width: 100%;
height: 100% !important;
min-height: 100%;
width: 100%;
overflow: auto;
}
}

@ -155,13 +155,13 @@ const setSimpleModelNodeTaskStatus = (
<style lang="scss" scoped>
.process-viewer-container {
height: 100%;
width: 100%;
height: 100%;
:deep(.process-viewer) {
width: 100%;
height: 100% !important;
min-height: 100%;
width: 100%;
overflow: auto;
}
}

@ -0,0 +1,332 @@
<template>
<ContentWrap v-if="visible">
<el-tabs v-model="activeTab" class="mb-12px">
<template #extra>
<el-button link type="info" @click="handleClose"></el-button>
</template>
<el-tab-pane :label="`详情:${recipeCode} - ${name}`" name="detail">
<div class="mb-12px text-left">
<el-button type="primary" @click="handleConfig"></el-button>
</div>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true" row-key="id">
<el-table-column label="序号" align="center" width="80">
<template #default="scope">
{{ (queryParams.pageNo - 1) * queryParams.pageSize + scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column label="点位名称" align="center" prop="pointName" />
<el-table-column label="点位类型" align="center" prop="pointType" />
<el-table-column label="数据类型" align="center" prop="dataType" />
<el-table-column label="单位" align="center" prop="dataUnit" />
</el-table>
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="handlePagination"
/>
</el-tab-pane>
<el-tab-pane label="手动录入参数" name="manual">
<div class="mb-12px flex justify-start">
<el-button type="primary" @click="openManualDialog('create')"></el-button>
</div>
<el-table
v-loading="manualLoading"
:data="manualList"
:stripe="true"
:show-overflow-tooltip="true"
row-key="id"
>
<el-table-column label="名称" align="center" prop="name" />
<el-table-column label="上限" align="center" prop="max" />
<el-table-column label="下限" align="center" prop="min" />
<el-table-column label="单位" align="center" prop="dataUnit" />
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" width="150px" fixed="right">
<template #default="scope">
<el-button link type="primary" @click="openManualDialog('update', scope.row)">编辑</el-button>
<el-button link type="danger" @click="handleManualDelete(scope.row)"></el-button>
</template>
</el-table-column>
</el-table>
<Pagination
:total="manualTotal"
v-model:page="manualQueryParams.pageNo"
v-model:limit="manualQueryParams.pageSize"
@pagination="handleManualPagination"
/>
</el-tab-pane>
</el-tabs>
<Dialog
:title="manualDialogMode === 'create' ? '新增参数' : '编辑参数'"
v-model="manualDialogVisible"
width="640px"
>
<el-form
ref="manualFormRef"
:model="manualForm"
:rules="manualRules"
label-width="90px"
v-loading="manualDialogLoading"
>
<el-form-item label="名称" prop="name">
<el-input v-model="manualForm.name" placeholder="请输入名称" clearable />
</el-form-item>
<el-form-item label="上限" prop="max">
<el-input v-model="manualForm.max" placeholder="请输入上限" clearable />
</el-form-item>
<el-form-item label="下限" prop="min">
<el-input v-model="manualForm.min" placeholder="请输入下限" clearable />
</el-form-item>
<el-form-item label="数据类型" prop="dataType">
<el-select v-model="manualForm.dataType" placeholder="请选择数据类型" clearable filterable class="!w-full">
<el-option
v-for="dict in dataTypeDictOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="单位" prop="dataUnit">
<el-select v-model="manualForm.dataUnit" placeholder="请选择单位" clearable filterable class="!w-full">
<el-option
v-for="dict in unitDictOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="manualForm.remark" placeholder="请输入备注" clearable type="textarea" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="manualDialogVisible = false"> </el-button>
<el-button type="primary" :disabled="manualDialogLoading" @click="submitManualDialog"> </el-button>
</template>
</Dialog>
</ContentWrap>
</template>
<script setup lang="ts">
import { RecipeConfigApi, RecipePointDetailVO } from '@/api/iot/recipeConfig'
import { RecipePointApi, RecipePointVO } from '@/api/iot/recipePoint'
import { getStrDictOptions } from '@/utils/dict'
defineOptions({ name: 'RecipeDetailList' })
const message = useMessage()
const { t } = useI18n()
const props = defineProps<{
visible: boolean
recipeId?: string
manualRecipeId?: string | number
recipeCode: string
name: string
}>()
const emit = defineEmits<{
(e: 'close'): void
(e: 'config'): void
}>()
const activeTab = ref('detail')
const loading = ref(false)
const list = ref<RecipePointDetailVO[]>([])
const total = ref(0)
const queryParams = reactive({
pageNo: 1,
pageSize: 10
})
const manualLoading = ref(false)
const manualList = ref<RecipePointVO[]>([])
const manualTotal = ref(0)
const manualQueryParams = reactive({
pageNo: 1,
pageSize: 10
})
type ManualDialogMode = 'create' | 'update'
const manualDialogVisible = ref(false)
const manualDialogMode = ref<ManualDialogMode>('create')
const manualDialogLoading = ref(false)
const manualFormRef = ref()
const manualForm = reactive({
id: undefined as number | undefined,
name: '',
max: '' as string | undefined,
min: '' as string | undefined,
dataType: '' as string | undefined,
dataUnit: '' as string | undefined,
remark: '' as string | undefined
})
const manualRules = reactive({
name: [{ required: true, message: '名称不能为空', trigger: 'blur' }],
max: [{ required: true, message: '上限不能为空', trigger: 'blur' }],
min: [{ required: true, message: '下限不能为空', trigger: 'blur' }],
dataType: [{ required: true, message: '数据类型不能为空', trigger: 'change' }],
dataUnit: [{ required: true, message: '单位不能为空', trigger: 'change' }]
})
const dataTypeDictOptions = computed(() => getStrDictOptions('iot_device_data_type'))
const unitDictOptions = computed(() => getStrDictOptions('iot_device_attribute_unit'))
const normalizeDetail = (item: any): RecipePointDetailVO => {
return {
...item,
pointName: item.pointName ?? item.attributeName,
pointType: item.pointType ?? item.attributeType
}
}
const getList = async () => {
if (!props.recipeId) return
loading.value = true
try {
const pageSize = Math.min(100, Math.max(1, Number(queryParams.pageSize) || 10))
const data = await RecipeConfigApi.getRecipePointDetailPage({
pageNo: queryParams.pageNo,
pageSize,
recipeId: props.recipeId
})
list.value = (data.list ?? []).map(normalizeDetail)
total.value = data.total
} finally {
loading.value = false
}
}
const handlePagination = () => {
getList()
}
const getManualList = async () => {
if (!props.manualRecipeId) return
manualLoading.value = true
try {
const pageSize = Math.min(100, Math.max(1, Number(manualQueryParams.pageSize) || 10))
const data = await RecipePointApi.getRecipePointPage({
pageNo: manualQueryParams.pageNo,
pageSize,
recipeId: props.manualRecipeId
})
manualList.value = data.list ?? []
manualTotal.value = data.total
} finally {
manualLoading.value = false
}
}
const handleManualPagination = () => {
getManualList()
}
const openManualDialog = (mode: ManualDialogMode, row?: RecipePointVO) => {
manualDialogMode.value = mode
manualDialogVisible.value = true
nextTick(() => {
manualFormRef.value?.clearValidate?.()
})
if (mode === 'create') {
manualForm.id = undefined
manualForm.name = ''
manualForm.max = ''
manualForm.min = ''
manualForm.dataType = ''
manualForm.dataUnit = ''
manualForm.remark = ''
return
}
manualForm.id = row?.id
manualForm.name = row?.name ?? ''
manualForm.max = row?.max === undefined || row?.max === null ? '' : String(row.max)
manualForm.min = row?.min === undefined || row?.min === null ? '' : String(row.min)
manualForm.dataType = row?.dataType === undefined || row?.dataType === null ? '' : String(row.dataType)
manualForm.dataUnit = row?.dataUnit ?? ''
manualForm.remark = row?.remark ?? ''
}
const submitManualDialog = async () => {
if (!props.manualRecipeId) return
await manualFormRef.value?.validate?.()
manualDialogLoading.value = true
try {
const params = {
id: manualForm.id,
recipeId: props.manualRecipeId,
name: manualForm.name,
max: manualForm.max,
min: manualForm.min,
dataType: manualForm.dataType,
dataUnit: manualForm.dataUnit,
remark: manualForm.remark
}
if (manualDialogMode.value === 'create') {
await RecipePointApi.createRecipePoint(params)
message.success(t('common.createSuccess'))
} else {
await RecipePointApi.updateRecipePoint(params)
message.success(t('common.updateSuccess'))
}
manualDialogVisible.value = false
await getManualList()
} finally {
manualDialogLoading.value = false
}
}
const handleManualDelete = async (row: RecipePointVO) => {
try {
await message.delConfirm()
await RecipePointApi.deleteRecipePoint(row.id)
message.success(t('common.delSuccess'))
await getManualList()
} catch {}
}
const handleClose = () => {
emit('close')
}
const handleConfig = () => {
emit('config')
}
watch(
() => [props.visible, props.recipeId],
([visible]) => {
if (!visible) return
queryParams.pageNo = 1
manualQueryParams.pageNo = 1
activeTab.value = 'detail'
getList()
},
{ immediate: true }
)
watch(
() => activeTab.value,
(tab) => {
if (!props.visible) return
if (tab === 'manual') {
getManualList()
}
}
)
const refresh = async () => {
if (activeTab.value === 'manual') {
await getManualList()
return
}
await getList()
}
defineExpose({ refresh })
</script>

@ -1,42 +1,26 @@
<template>
<ContentWrap>
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
<el-form-item label="配方编码" prop="recipeCode">
<el-input
v-model="queryParams.recipeCode"
placeholder="请输入配方编码"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
v-model="queryParams.recipeCode" placeholder="请输入配方编码" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item label="配方名称" prop="recipeName">
<el-form-item label="配方名称" prop="name">
<el-input
v-model="queryParams.recipeName"
placeholder="请输入配方名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
v-model="queryParams.name" placeholder="请输入配方名称" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item label="产品名称" prop="productName">
<el-input
v-model="queryParams.productName"
placeholder="请输入产品名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
v-model="queryParams.productName" placeholder="请输入产品名称" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 查询</el-button>
<el-button @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" /> 查询
</el-button>
<el-button type="primary" plain @click="openDialog('create')">
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
@ -49,123 +33,74 @@
<ContentWrap>
<el-table
ref="tableRef"
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
row-key="id"
@selection-change="handleSelectionChange"
>
ref="tableRef" v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true" row-key="id"
highlight-current-row @selection-change="handleSelectionChange" @row-click="handleRowClick">
<el-table-column type="selection" width="55" reserve-selection />
<el-table-column label="配方编码" align="center" prop="recipeCode" />
<el-table-column label="配方名称" align="center" prop="recipeName" />
<el-table-column label="配方类型" align="center" prop="recipeType" />
<el-table-column label="关联产品" align="center" prop="productName" />
<el-table-column label="关联设备" align="center" prop="deviceName" />
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" width="240px" fixed="right">
<el-table-column label="配方名称" align="center" prop="name" />
<el-table-column label="配方类型" align="center" prop="recipeType">
<template #default="scope">
<el-button link type="primary" @click="openConfigDialog(scope.row)"></el-button>
<el-button link type="info" @click="openDetail(scope.row)"></el-button>
<el-button link type="warning" @click="openDialog('update', scope.row)">编辑</el-button>
<el-button link type="danger" @click="handleDelete(scope.row)"></el-button>
{{ getRecipeTypeLabel(scope.row.recipeType) }}
</template>
</el-table-column>
</el-table>
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="handlePagination"
/>
</ContentWrap>
<ContentWrap v-if="detailVisible">
<div class="flex items-center justify-between mb-12px">
<div class="text-14px">
详情{{ detailMeta.recipeCode }} - {{ detailMeta.recipeName }}
</div>
<el-button link type="info" @click="closeDetail"></el-button>
</div>
<el-table
v-loading="detailLoading"
:data="detailList"
:stripe="true"
:show-overflow-tooltip="true"
row-key="id"
>
<el-table-column label="序号" align="center" width="80">
<el-table-column label="关联产品" align="center" prop="productName" />
<el-table-column label="关联设备" align="center" prop="machineName" />
<el-table-column label="配方描述" align="center" prop="recipeDesc" />
<el-table-column label="操作" align="center" width="240px" fixed="right">
<template #default="scope">
{{ (detailQueryParams.pageNo - 1) * detailQueryParams.pageSize + scope.$index + 1 }}
<el-button link type="primary" @click.stop="openDetail(scope.row)">配置</el-button>
<el-button link type="warning" @click.stop="openDialog('update', scope.row)">编辑</el-button>
<el-button link type="danger" @click.stop="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
<el-table-column label="点位名称" align="center" prop="pointName" />
<el-table-column label="点位类型" align="center" prop="pointType" />
<el-table-column label="数据类型" align="center" prop="dataType" />
<el-table-column label="单位" align="center" prop="dataUnit" />
</el-table>
<Pagination
:total="detailTotal"
v-model:page="detailQueryParams.pageNo"
v-model:limit="detailQueryParams.pageSize"
@pagination="handleDetailPagination"
/>
:total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
@pagination="handlePagination" />
</ContentWrap>
<RecipeDetailList
ref="detailRef"
:visible="detailVisible"
:recipe-id="detailMeta.recipeCode"
:manual-recipe-id="detailMeta.id"
:recipe-code="detailMeta.recipeCode"
:name="detailMeta.name"
@config="handleDetailConfig"
@close="closeDetail"
/>
<Dialog :title="dialogTitle" v-model="dialogVisible" width="720px">
<el-form
ref="dialogFormRef"
:model="dialogForm"
:rules="dialogRules"
label-width="100px"
v-loading="dialogLoading"
>
<el-form ref="dialogFormRef" :model="dialogForm" :rules="dialogRules" label-width="100px" v-loading="dialogLoading">
<el-form-item label="配方编码" prop="recipeCode">
<el-input v-model="dialogForm.recipeCode" placeholder="请输入配方编码" clearable />
</el-form-item>
<el-form-item label="配方名称" prop="recipeName">
<el-input v-model="dialogForm.recipeName" placeholder="请输入配方名称" clearable />
<el-form-item label="配方名称" prop="name">
<el-input v-model="dialogForm.name" placeholder="请输入配方名称" clearable />
</el-form-item>
<el-form-item label="配方类型" prop="recipeType">
<el-input v-model="dialogForm.recipeType" placeholder="请输入配方类型" clearable />
<el-select
v-model="dialogForm.recipeType" placeholder="请选择配方类型" clearable filterable class="!w-full"
:loading="recipeTypeLoading">
<el-option v-for="item in recipeTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="关联产品" prop="productId">
<el-form-item label="关联产品" prop="productName">
<el-select
v-model="dialogForm.productId"
placeholder="请选择关联产品"
clearable
filterable
class="!w-full"
:loading="productLoading"
>
<el-option
v-for="item in productOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
v-model="dialogForm.productName" placeholder="请选择关联产品" clearable filterable class="!w-full"
:loading="productLoading">
<el-option v-for="item in productOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="关联设备" prop="deviceId">
<el-form-item label="关联设备" prop="machineName">
<el-select
v-model="dialogForm.deviceId"
placeholder="请选择关联设备"
clearable
filterable
class="!w-full"
:loading="deviceLoading"
>
<el-option
v-for="item in deviceOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
v-model="dialogForm.machineName" placeholder="请选择关联设备" clearable filterable class="!w-full"
:loading="deviceLoading">
<el-option v-for="item in deviceOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="dialogForm.remark" placeholder="请输入备注" clearable type="textarea" />
<el-form-item label="配方描述" prop="recipeDesc">
<el-input v-model="dialogForm.recipeDesc" placeholder="请输入配方描述" clearable type="textarea" />
</el-form-item>
</el-form>
<template #footer>
@ -177,11 +112,8 @@
<Dialog title="配置" v-model="configVisible" width="920px">
<div v-loading="configLoading">
<el-transfer
v-model="configSelectedKeys"
:data="configCandidates"
filterable
:titles="['候选点位', '已选点位']"
/>
class="formula-config-transfer" v-model="configSelectedKeys" :data="configCandidates" filterable
:titles="['来源', '目标']" />
</div>
<template #footer>
<el-button @click="configVisible = false"> </el-button>
@ -194,9 +126,11 @@
import download from '@/utils/download'
import { ProductApi } from '@/api/erp/product/product'
import { DeviceApi } from '@/api/iot/device'
import { RecipeConfigApi, RecipeConfigVO, RecipePointDetailVO } from '@/api/iot/recipeConfig'
import { RecipeApi } from '@/api/iot/recipe'
import { RecipeConfigApi, RecipeConfigVO } from '@/api/iot/recipeConfig'
import RecipeDetailList from './components/RecipeDetailList.vue'
type SelectOption = { label: string; value: number }
type SelectOption<T = any> = { label: string; value: T }
defineOptions({ name: 'FormulaConfig' })
@ -204,13 +138,11 @@ const message = useMessage()
const { t } = useI18n()
const loading = ref(false)
const tableRef = ref()
const queryFormRef = ref()
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
recipeCode: '',
recipeName: '',
name: '',
productName: ''
})
@ -225,13 +157,13 @@ const total = ref(0)
const buildQueryParams = () => {
const recipeCode = queryParams.recipeCode?.trim()
const recipeName = queryParams.recipeName?.trim()
const name = queryParams.name?.trim()
const productName = queryParams.productName?.trim()
return {
pageNo: queryParams.pageNo,
pageSize: queryParams.pageSize,
recipeCode: recipeCode ? recipeCode : undefined,
recipeName: recipeName ? recipeName : undefined,
name: name ? name : undefined,
productName: productName ? productName : undefined
}
}
@ -278,33 +210,25 @@ const handleDelete = async (row: RecipeConfigVO) => {
await RecipeConfigApi.deleteRecipeConfig(row.id)
message.success(t('common.delSuccess'))
await getList()
} catch {}
} catch { }
}
const productLoading = ref(false)
const productOptions = ref<SelectOption[]>([])
const productOptions = ref<SelectOption<string>[]>([])
const deviceLoading = ref(false)
const deviceOptions = ref<SelectOption[]>([])
const productLabelMap = computed<Record<number, string>>(() => {
return productOptions.value.reduce((acc, cur) => {
acc[cur.value] = cur.label
return acc
}, {} as Record<number, string>)
})
const deviceOptions = ref<SelectOption<string>[]>([])
const recipeTypeLoading = ref(false)
const recipeTypeOptions = ref<SelectOption<string>[]>([])
const deviceLabelMap = computed<Record<number, string>>(() => {
return deviceOptions.value.reduce((acc, cur) => {
acc[cur.value] = cur.label
return acc
}, {} as Record<number, string>)
})
const getRecipeTypeLabel = (value: unknown) => {
return value === undefined || value === null ? '' : String(value)
}
const getProductOptions = async () => {
productLoading.value = true
try {
const data = await ProductApi.getProductSimpleList()
productOptions.value = (data ?? []).map((item: any) => ({ label: item.name, value: item.id }))
productOptions.value = (data ?? []).map((item: any) => ({ label: item.name, value: item.name }))
} finally {
productLoading.value = false
}
@ -314,15 +238,26 @@ const getDeviceOptions = async () => {
deviceLoading.value = true
try {
const data = await DeviceApi.getDeviceList()
deviceOptions.value = (data ?? []).map((item: any) => ({ label: item.deviceName, value: item.id }))
deviceOptions.value = (data ?? []).map((item: any) => ({ label: item.deviceName, value: item.deviceName }))
} finally {
deviceLoading.value = false
}
}
const getRecipeTypeOptions = async () => {
recipeTypeLoading.value = true
try {
const data = await RecipeApi.getRecipePage({})
recipeTypeOptions.value = (data?.list ?? []).map((item: any) => ({ label: item.name, value: item.name }))
} finally {
recipeTypeLoading.value = false
}
}
const ensureOptionsLoaded = async () => {
if (!productOptions.value.length) await getProductOptions()
if (!deviceOptions.value.length) await getDeviceOptions()
if (!recipeTypeOptions.value.length) await getRecipeTypeOptions()
}
type DialogMode = 'create' | 'update'
@ -334,16 +269,16 @@ const dialogLoading = ref(false)
const dialogForm = reactive({
id: undefined as number | undefined,
recipeCode: '',
recipeName: '',
recipeType: '',
productId: undefined as number | undefined,
deviceId: undefined as number | undefined,
remark: ''
name: '',
recipeType: '' as string | undefined,
productName: '' as string | undefined,
machineName: '' as string | undefined,
recipeDesc: ''
})
const dialogRules = reactive({
recipeCode: [{ required: true, message: '配方编码不能为空', trigger: 'blur' }],
recipeName: [{ required: true, message: '配方名称不能为空', trigger: 'blur' }]
name: [{ required: true, message: '配方名称不能为空', trigger: 'blur' }]
})
const openDialog = async (mode: DialogMode, row?: RecipeConfigVO) => {
@ -357,21 +292,21 @@ const openDialog = async (mode: DialogMode, row?: RecipeConfigVO) => {
if (mode === 'create') {
dialogForm.id = undefined
dialogForm.recipeCode = ''
dialogForm.recipeName = ''
dialogForm.name = ''
dialogForm.recipeType = ''
dialogForm.productId = undefined
dialogForm.deviceId = undefined
dialogForm.remark = ''
dialogForm.productName = ''
dialogForm.machineName = ''
dialogForm.recipeDesc = ''
return
}
dialogForm.id = row?.id
dialogForm.recipeCode = row?.recipeCode ?? ''
dialogForm.recipeName = row?.recipeName ?? ''
dialogForm.recipeType = row?.recipeType ?? ''
dialogForm.productId = row?.productId
dialogForm.deviceId = row?.deviceId
dialogForm.remark = row?.remark ?? ''
dialogForm.name = row?.name ?? row?.recipeName ?? ''
dialogForm.recipeType = row?.recipeType === undefined || row?.recipeType === null ? '' : String(row.recipeType)
dialogForm.productName = row?.productName ?? ''
dialogForm.machineName = row?.machineName ?? row?.deviceName ?? ''
dialogForm.recipeDesc = row?.recipeDesc ?? row?.remark ?? ''
}
const submitDialog = async () => {
@ -382,13 +317,11 @@ const submitDialog = async () => {
const data = {
id: dialogForm.id,
recipeCode: dialogForm.recipeCode,
recipeName: dialogForm.recipeName,
name: dialogForm.name,
recipeType: dialogForm.recipeType,
productId: dialogForm.productId,
productName: dialogForm.productId ? productLabelMap.value[dialogForm.productId] : undefined,
deviceId: dialogForm.deviceId,
deviceName: dialogForm.deviceId ? deviceLabelMap.value[dialogForm.deviceId] : undefined,
remark: dialogForm.remark
productName: dialogForm.productName,
machineName: dialogForm.machineName,
recipeDesc: dialogForm.recipeDesc
}
if (dialogMode.value === 'create') {
await RecipeConfigApi.createRecipeConfig(data)
@ -404,86 +337,66 @@ const submitDialog = async () => {
}
}
const detailRef = ref()
const detailVisible = ref(false)
const detailLoading = ref(false)
const detailList = ref<RecipePointDetailVO[]>([])
const detailTotal = ref(0)
const detailMeta = reactive({
recipeId: undefined as number | undefined,
id: undefined as number | undefined,
recipeCode: '',
recipeName: ''
})
const detailQueryParams = reactive({
pageNo: 1,
pageSize: 10,
recipeId: undefined as number | undefined
name: ''
})
const getDetailList = async () => {
if (!detailQueryParams.recipeId) return
detailLoading.value = true
try {
const data = await RecipeConfigApi.getRecipePointDetailPage({
pageNo: detailQueryParams.pageNo,
pageSize: detailQueryParams.pageSize,
recipeId: detailQueryParams.recipeId
})
detailList.value = data.list
detailTotal.value = data.total
} finally {
detailLoading.value = false
}
}
const openDetail = async (row: RecipeConfigVO) => {
detailMeta.recipeId = row.id
detailMeta.id = row.id
detailMeta.recipeCode = row.recipeCode
detailMeta.recipeName = row.recipeName
detailQueryParams.recipeId = row.id
detailQueryParams.pageNo = 1
detailMeta.name = row.name ?? row.recipeName ?? ''
detailVisible.value = true
await getDetailList()
}
const handleDetailConfig = () => {
if (!detailMeta.recipeCode) return
openConfigDialog(detailMeta.recipeCode)
}
const handleRowClick = async (row: RecipeConfigVO, column: any) => {
if (column?.type === 'selection') return
if (column?.label === '操作') return
await openDetail(row)
}
const closeDetail = () => {
detailVisible.value = false
detailMeta.recipeId = undefined
detailMeta.id = undefined
detailMeta.recipeCode = ''
detailMeta.recipeName = ''
detailQueryParams.recipeId = undefined
detailQueryParams.pageNo = 1
detailQueryParams.pageSize = 10
detailList.value = []
detailTotal.value = 0
}
const handleDetailPagination = () => {
getDetailList()
detailMeta.name = ''
}
type TransferItem = { key: number; label: string; disabled?: boolean }
const configVisible = ref(false)
const configLoading = ref(false)
const configRecipeId = ref<number | undefined>(undefined)
const configRecipeId = ref<string | undefined>(undefined)
const configCandidates = ref<TransferItem[]>([])
const configSelectedKeys = ref<number[]>([])
const openConfigDialog = async (row: RecipeConfigVO) => {
const openConfigDialog = async (recipeCode: string) => {
configVisible.value = true
configRecipeId.value = row.id
configRecipeId.value = recipeCode
configSelectedKeys.value = []
configCandidates.value = []
configLoading.value = true
try {
const [candidateRes, selectedRes] = await Promise.all([
RecipeConfigApi.getPointCandidatePage({ pageNo: 1, pageSize: 1000 }),
RecipeConfigApi.getRecipePointDetailPage({ pageNo: 1, pageSize: 1000, recipeId: row.id })
DeviceApi.getDeviceContactModelPage(),
RecipeConfigApi.getRecipePointDetailPage({ pageNo: 1, pageSize: 100, recipeId: recipeCode })
])
configCandidates.value = (candidateRes.list ?? []).map((item: any) => ({
const candidateList = Array.isArray(candidateRes)
? candidateRes
: ((candidateRes as any)?.list ?? (candidateRes as any)?.data ?? [])
configCandidates.value = (candidateList ?? []).map((item: any) => ({
key: item.id,
label: `${item.pointName}${item.pointType ? '' + item.pointType : ''}${item.dataType ? '/' + item.dataType : ''}${item.dataUnit ? ' ' + item.dataUnit : ''}${item.pointType ? '' : ''}`
label: `${item.attributeName ?? item.pointName ?? ''}${item.attributeType || item.pointType ? '' + (item.attributeType ?? item.pointType) : ''}${item.dataType ? '/' + item.dataType : ''}${item.dataUnit ? ' ' + item.dataUnit : ''}${item.attributeType || item.pointType ? '' : ''}`
}))
configSelectedKeys.value = (selectedRes.list ?? [])
@ -504,8 +417,8 @@ const submitConfig = async () => {
})
message.success(t('common.updateSuccess'))
configVisible.value = false
if (detailVisible.value && detailQueryParams.recipeId === configRecipeId.value) {
await getDetailList()
if (detailVisible.value && detailMeta.recipeCode === configRecipeId.value) {
await detailRef.value?.refresh?.()
}
} finally {
configLoading.value = false
@ -518,4 +431,21 @@ onMounted(() => {
})
</script>
<style scoped></style>
<style scoped>
:deep(.formula-config-transfer.el-transfer) {
--el-transfer-panel-body-height: 440px;
display: flex;
width: 100%;
}
:deep(.formula-config-transfer .el-transfer-panel) {
width: calc((100% - 96px) / 2);
}
:deep(.el-transfer__buttons) {
display: flex;
text-align: center;
align-items: center;
}
</style>

@ -3,7 +3,8 @@
<div class="single-device-dialog">
<el-form class="-mb-15px" :inline="true" label-width="80px">
<el-form-item label="采集时间">
<el-date-picker v-model="collectionTimeRange" value-format="YYYY-MM-DD HH:mm:ss" type="datetimerange"
<el-date-picker
v-model="collectionTimeRange" value-format="YYYY-MM-DD HH:mm:ss" type="datetimerange"
start-placeholder="开始时间" end-placeholder="结束时间"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" class="!w-360px" />
</el-form-item>
@ -19,15 +20,18 @@
采集时间{{ group.collectTime || '-' }}
</div>
<div v-if="group.sections.length" class="single-device-dialog__table-grid">
<div v-for="section in group.sections" :key="`${group.key}-${section.key}`"
<div
v-for="section in group.sections" :key="`${group.key}-${section.key}`"
class="single-device-dialog__section">
<div class="single-device-dialog__section-title">
{{ section.title }}
</div>
<el-empty v-if="!section.columns.length" description="暂无数据" />
<el-table v-else :data="section.rows" :border="true" :header-cell-style="headerCellStyle"
<el-table
v-else :data="section.rows" :border="true" :header-cell-style="headerCellStyle"
:cell-style="bodyCellStyle" size="small">
<el-table-column v-for="col in section.columns" :key="col.prop" :prop="col.prop" :label="col.label"
<el-table-column
v-for="col in section.columns" :key="col.prop" :prop="col.prop" :label="col.label"
align="center">
<template #default="scope">
<span>{{ formatCell(scope.row[col.prop]) }}</span>
@ -228,16 +232,16 @@ watch(
.single-device-dialog__record {
padding: 12px;
background: var(--el-bg-color-overlay);
border: 1px solid var(--el-border-color);
border-radius: 6px;
background: var(--el-bg-color-overlay);
}
.single-device-dialog__section {
padding: 10px;
background: var(--el-fill-color-lighter);
border: 1px solid var(--el-border-color);
border-radius: 6px;
background: var(--el-fill-color-lighter);
}
.single-device-dialog__section :deep(.el-table) {
@ -245,14 +249,14 @@ watch(
}
.single-device-dialog__record-title {
font-size: 13px;
margin-bottom: 6px;
font-size: 13px;
color: var(--el-text-color-secondary);
}
.single-device-dialog__section-title {
font-size: 13px;
margin-bottom: 4px;
font-size: 13px;
color: var(--el-text-color-primary);
}

@ -204,8 +204,8 @@ watch(
}
.single-device-dialog__section-title {
font-size: 13px;
margin-bottom: 4px;
font-size: 13px;
color: var(--el-text-color-primary);
}

@ -136,11 +136,11 @@ const emitActivityChange = () => {
display: flex;
width: 60px;
height: 60px;
cursor: pointer;
border: 1px dashed var(--el-border-color-darker);
border-radius: 8px;
align-items: center;
justify-content: center;
cursor: pointer;
}
.spu-pic {

@ -208,8 +208,8 @@ onBeforeUnmount(() => {
//transition: border-left 0.05s ease-in-out; /* */
.username {
min-width: 0;
max-width: 60%;
min-width: 0;
}
.last-message {
@ -218,27 +218,27 @@ onBeforeUnmount(() => {
.last-message,
.username {
display: -webkit-box;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
}
}
.active {
background-color: rgba(128, 128, 128, 0.5); //
background-color: rgb(128 128 128 / 50%); //
}
.right-menu-ul {
position: absolute;
background-color: var(--app-content-bg-color);
width: 130px;
padding: 5px;
margin: 0;
list-style-type: none; /* 移除默认的项目符号 */
background-color: var(--app-content-bg-color);
border-radius: 12px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* 阴影效果 */
width: 130px;
box-shadow: 0 2px 4px rgb(0 0 0 / 10%); /* 阴影效果 */
li {
padding: 8px 16px;

@ -372,36 +372,36 @@ const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
<style lang="scss" scoped>
.kefu {
background-color: #f5f5f5;
position: relative;
width: calc(100% - 300px - 260px);
background-color: #f5f5f5;
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 1px; /* 实际宽度 */
height: 100%;
background-color: var(--el-border-color);
content: '';
transform: scaleX(0.3); /* 缩小宽度 */
}
.kefu-header {
background-color: #f5f5f5;
position: relative;
display: flex;
background-color: #f5f5f5;
align-items: center;
justify-content: space-between;
&::before {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 1px; /* 初始宽度 */
background-color: var(--el-border-color);
content: '';
transform: scaleY(0.3); /* 缩小视觉高度 */
}
@ -412,30 +412,30 @@ const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
}
&-content {
margin: 0;
padding: 10px;
position: relative;
height: 100%;
width: 100%;
height: 100%;
padding: 10px;
margin: 0;
.newMessageTip {
position: absolute;
bottom: 35px;
right: 35px;
background-color: var(--app-content-bg-color);
bottom: 35px;
padding: 10px;
border-radius: 30px;
font-size: 12px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* 阴影效果 */
background-color: var(--app-content-bg-color);
border-radius: 30px;
box-shadow: 0 2px 4px rgb(0 0 0 / 10%); /* 阴影效果 */
}
.ss-row-left {
justify-content: flex-start;
.kefu-message {
background-color: #fff;
margin-left: 10px;
margin-top: 3px;
margin-left: 10px;
background-color: #fff;
border-top-right-radius: 10px;
border-bottom-right-radius: 10px;
border-bottom-left-radius: 10px;
@ -446,22 +446,22 @@ const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
justify-content: flex-end;
.kefu-message {
background-color: rgb(206, 223, 255);
margin-right: 10px;
margin-top: 3px;
border-top-left-radius: 10px;
margin-right: 10px;
background-color: rgb(206 223 255);
border-bottom-right-radius: 10px;
border-bottom-left-radius: 10px;
border-top-left-radius: 10px;
}
}
//
.kefu-message {
color: #414141;
font-weight: 500;
padding: 5px 10px;
width: auto;
max-width: 50%;
padding: 5px 10px;
font-weight: 500;
color: #414141;
//text-align: left;
//display: inline-block !important;
//word-break: break-all;
@ -475,30 +475,30 @@ const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
.date-message,
.system-message {
width: fit-content;
background-color: rgba(0, 0, 0, 0.1);
border-radius: 8px;
padding: 0 5px;
color: #fff;
font-size: 10px;
color: #fff;
background-color: rgb(0 0 0 / 10%);
border-radius: 8px;
}
}
.kefu-footer {
position: relative;
display: flex;
flex-direction: column;
height: auto;
margin: 0;
padding: 0;
margin: 0;
flex-direction: column;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 1px; /* 初始宽度 */
background-color: var(--el-border-color);
content: '';
transform: scaleY(0.3); /* 缩小视觉高度 */
}
@ -509,13 +509,13 @@ const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
}
::v-deep(textarea) {
resize: none;
background-color: #f5f5f5;
resize: none;
}
:deep(.el-input__wrapper) {
box-shadow: none !important;
border-radius: 0;
box-shadow: none !important;
}
::v-deep(.el-textarea__inner) {

@ -170,31 +170,31 @@ const getUserData = async () => {
background-color: #f5f5f5;
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 1px; /* 实际宽度 */
height: 100%;
background-color: var(--el-border-color);
content: '';
transform: scaleX(0.3); /* 缩小宽度 */
}
&-header {
background-color: #f5f5f5;
position: relative;
display: flex;
background-color: #f5f5f5;
align-items: center;
justify-content: space-around;
&::before {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 1px; /* 初始宽度 */
background-color: var(--el-border-color);
content: '';
transform: scaleY(0.3); /* 缩小视觉高度 */
}
@ -204,45 +204,39 @@ const getUserData = async () => {
}
&-item {
height: 100%;
width: 100%;
position: relative;
width: 100%;
height: 100%;
&-activation::before {
content: '';
position: absolute; /* 绝对定位 */
top: 0;
left: 0;
right: 0;
bottom: 0; /* 覆盖整个元素 */
border-bottom: 2px solid rgba(128, 128, 128, 0.5); /* 边框样式 */
inset: 0; /* 覆盖整个元素 */
pointer-events: none; /* 确保点击事件不会被伪元素拦截 */
border-bottom: 2px solid rgb(128 128 128 / 50%); /* 边框样式 */
content: '';
}
&:hover::before {
content: '';
position: absolute; /* 绝对定位 */
top: 0;
left: 0;
right: 0;
bottom: 0; /* 覆盖整个元素 */
border-bottom: 2px solid rgba(128, 128, 128, 0.5); /* 边框样式 */
inset: 0; /* 覆盖整个元素 */
pointer-events: none; /* 确保点击事件不会被伪元素拦截 */
border-bottom: 2px solid rgb(128 128 128 / 50%); /* 边框样式 */
content: '';
}
}
}
&-content {
margin: 0;
padding: 0;
position: relative;
height: 100%;
width: 100%;
height: 100%;
padding: 0;
margin: 0;
}
&-tabs {
height: 100%;
width: 100%;
height: 100%;
}
}

@ -109,10 +109,10 @@ function formatOrderStatus(order: any) {
<style lang="scss" scoped>
.order-list-card-box {
border-radius: 10px;
padding: 10px;
border: 1px var(--el-border-color) solid;
background-color: #fff; //
border: 1px var(--el-border-color) solid;
border-radius: 10px;
.order-card-header {
height: 28px;
@ -123,8 +123,8 @@ function formatOrderStatus(order: any) {
span {
&:hover {
text-decoration: underline;
color: var(--left-menu-bg-active-color);
text-decoration: underline;
}
}
}
@ -144,9 +144,9 @@ function formatOrderStatus(order: any) {
}
.discounts-money {
font-family: OPPOSANS;
font-size: 16px;
line-height: normal;
font-family: OPPOSANS;
}
.pay-color {
@ -156,26 +156,26 @@ function formatOrderStatus(order: any) {
}
.warning-color {
color: #faad14;
font-size: 11px;
font-weight: bold;
color: #faad14;
}
.danger-color {
color: #ff3000;
font-size: 11px;
font-weight: bold;
color: #ff3000;
}
.success-color {
color: #52c41a;
font-size: 11px;
font-weight: bold;
color: #52c41a;
}
.info-color {
color: #999999;
font-size: 11px;
font-weight: bold;
color: #999;
}
</style>

@ -68,20 +68,20 @@ const openDetail = (spuId: number) => {
<style lang="scss" scoped>
.button {
background-color: #007bff;
color: white;
border: none;
padding: 5px 10px;
color: white;
cursor: pointer;
background-color: #007bff;
border: none;
}
.product-warp {
display: flex;
width: 100%;
padding: 10px;
background-color: #fff;
border-radius: 8px;
display: flex;
align-items: center;
padding: 10px;
&-left {
width: 70px;
@ -97,14 +97,14 @@ const openDetail = (spuId: number) => {
flex: 1;
.description {
display: -webkit-box;
width: 100%;
overflow: hidden;
font-size: 16px;
font-weight: bold;
display: -webkit-box;
-webkit-line-clamp: 1; /* 显示一行 */
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1; /* 显示一行 */
}
.price {

@ -102,11 +102,11 @@ onBeforeUnmount(() => {
<style lang="scss">
.kefu-layout {
position: absolute;
flex: 1;
top: 0;
left: 0;
height: 100%;
width: 100%;
height: 100%;
flex: 1;
}
/* 定义滚动条样式 */
@ -117,15 +117,15 @@ onBeforeUnmount(() => {
/* 定义滚动条轨道 内阴影+圆角 */
::-webkit-scrollbar-track {
box-shadow: inset 0 0 0 rgba(240, 240, 240, 0.5);
border-radius: 10px;
background-color: #fff;
border-radius: 10px;
box-shadow: inset 0 0 0 rgb(240 240 240 / 50%);
}
/* 定义滑块 内阴影+圆角 */
::-webkit-scrollbar-thumb {
background-color: rgb(240 240 240 / 50%);
border-radius: 10px;
box-shadow: inset 0 0 0 rgba(240, 240, 240, 0.5);
background-color: rgba(240, 240, 240, 0.5);
box-shadow: inset 0 0 0 rgb(240 240 240 / 50%);
}
</style>

@ -132,11 +132,11 @@ const emitActivityChange = () => {
display: flex;
width: 60px;
height: 60px;
cursor: pointer;
border: 1px dashed var(--el-border-color-darker);
border-radius: 8px;
align-items: center;
justify-content: center;
cursor: pointer;
}
.spu-pic {

@ -134,11 +134,11 @@ const emitActivityChange = () => {
display: flex;
width: 60px;
height: 60px;
cursor: pointer;
border: 1px dashed var(--el-border-color-darker);
border-radius: 8px;
align-items: center;
justify-content: center;
cursor: pointer;
}
.spu-pic {

@ -394,11 +394,11 @@ onMounted(async () => {
.el-timeline-right-content {
display: flex;
align-items: center;
min-height: 30px;
padding: 10px;
border-radius: var(--el-card-border-radius);
background-color: var(--app-content-bg-color);
border-radius: var(--el-card-border-radius);
align-items: center;
&::before {
position: absolute;

@ -70,8 +70,8 @@ withDefaults(defineProps<{ user: UserApi.UserVO; wallet: WalletApi.WalletVO; col
justify-content: space-between;
.el-descriptions__label {
width: 120px;
display: block;
width: 120px;
text-align: left;
}

@ -151,8 +151,8 @@ withDefaults(defineProps<{ user: UserApi.UserVO; mode?: string }>(), {
justify-content: space-between;
.el-descriptions__label {
width: 120px;
display: block;
width: 120px;
text-align: left;
}

@ -0,0 +1,346 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible" width="920px">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-row :gutter="16">
<el-col :span="24">
<el-form-item label="资产编号" prop="id">
<el-input v-model="formData.id" placeholder="系统自动生成" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="编码" prop="deviceCode" required>
<el-input v-model="formData.deviceCode" placeholder="请输入编码" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="名称" prop="deviceName" required>
<el-input v-model="formData.deviceName" placeholder="请输入名称" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="类型" prop="deviceType" required>
<el-tree-select
v-model="formData.deviceType"
:data="deviceTypeTree"
:props="treeSelectProps"
check-strictly
default-expand-all
value-key="id"
placeholder="请选择类型"
class="!w-full"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="品牌" prop="deviceBrand">
<el-input v-model="formData.deviceBrand" placeholder="请输入品牌" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="型号" prop="deviceModel">
<el-input v-model="formData.deviceModel" placeholder="请输入型号" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="规格" prop="deviceSpec">
<el-input v-model="formData.deviceSpec" placeholder="请输入规格" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="数量" prop="quantity">
<el-input-number v-model="formData.quantity" :min="1" controls-position="right" class="!w-full" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="供应商" prop="supplier">
<el-select v-model="formData.supplier" placeholder="请选择供应商" clearable filterable class="!w-full">
<el-option v-for="item in supplierOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="所属车间" prop="workshop">
<el-tree-select
v-model="formData.workshop"
:data="deptTree"
:props="treeSelectProps"
check-strictly
default-expand-all
value-key="id"
placeholder="请选择所属车间"
class="!w-full"
/>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="生产日期" prop="productionDate" required>
<el-date-picker v-model="formData.productionDate" type="date" value-format="x" placeholder="请选择生产日期" class="!w-full" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="入厂日期" prop="factoryEntryDate" required>
<el-date-picker v-model="formData.factoryEntryDate" type="date" value-format="x" placeholder="请选择入厂日期" class="!w-full" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="位置" prop="deviceLocation">
<el-input v-model="formData.deviceLocation" placeholder="请输入位置" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="部门" prop="useDept">
<el-tree-select
v-model="formData.useDept"
:data="deptTree"
:props="treeSelectProps"
check-strictly
default-expand-all
value-key="id"
placeholder="请选择部门"
class="!w-full"
/>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="责任人" prop="deviceManager">
<div class="device-ledger-manager">
<el-input v-model="formData.deviceManager" placeholder="请选择数据" readonly />
<el-button type="primary" plain @click="openUserPicker"></el-button>
</div>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" placeholder="请输入备注" type="textarea" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
<Dialog title="选择责任人" v-model="userPickerVisible" width="720px">
<el-input v-model="userKeyword" placeholder="搜索用户" clearable class="mb-12px" />
<el-table :data="filteredUsers" height="420">
<el-table-column label="账号" prop="username" />
<el-table-column label="姓名" prop="nickname" />
<el-table-column label="操作" width="120">
<template #default="scope">
<el-button link type="primary" @click="selectUser(scope.row)"></el-button>
</template>
</el-table-column>
</el-table>
</Dialog>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { DeviceLedgerApi, DeviceLedgerVO } from '@/api/mes/deviceledger'
import { DeviceTypeApi, DeviceTypeTreeVO } from '@/api/mes/devicetype'
import { SupplierApi } from '@/api/erp/purchase/supplier'
import { getSimpleDeptList } from '@/api/system/dept'
import { getSimpleUserList, UserVO } from '@/api/system/user'
import { handleTree } from '@/utils/tree'
import type { FormRules } from 'element-plus'
/** 设备类型 表单 */
defineOptions({ name: 'DeviceLedgerForm' })
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formData = ref({
id: undefined,
deviceCode: undefined,
deviceName: undefined,
deviceStatus: undefined,
deviceBrand: undefined,
deviceModel: undefined,
deviceSpec: undefined,
deviceType: undefined,
supplier: undefined,
workshop: undefined,
deviceLocation: undefined,
useDept: undefined,
deviceManager: undefined,
quantity: 1,
productionDate: undefined,
factoryEntryDate: undefined,
remark: undefined,
sort: undefined
})
const formRules = reactive<FormRules>({
deviceCode: [{ required: true, message: '编码不能为空', trigger: 'blur' }],
deviceName: [{ required: true, message: '名称不能为空', trigger: 'blur' }],
deviceType: [{ required: true, message: '类型不能为空', trigger: 'change' }],
productionDate: [{ required: true, message: '生产日期不能为空', trigger: 'change' }],
factoryEntryDate: [{ required: true, message: '入厂日期不能为空', trigger: 'change' }]
})
const formRef = ref() // Ref
const treeSelectProps = { label: 'name', children: 'children' }
const deviceTypeTree = ref<DeviceTypeTreeVO[]>([])
const deptTree = ref<any[]>([])
const supplierOptions = ref<string[]>([])
const deptNameMap = ref<Record<number, string>>({})
const userPickerVisible = ref(false)
const userKeyword = ref('')
const users = ref<UserVO[]>([])
const filteredUsers = computed(() => {
const keyword = userKeyword.value?.trim().toLowerCase()
if (!keyword) return users.value
return users.value.filter((u) =>
[u.username, u.nickname].some((v) => (v ?? '').toString().toLowerCase().includes(keyword))
)
})
const openUserPicker = () => {
userPickerVisible.value = true
}
const selectUser = (user: UserVO) => {
formData.value.deviceManager = user.nickname
userPickerVisible.value = false
}
const buildDeptNameMap = (nodes: any[]) => {
const map: Record<number, string> = {}
const stack = [...nodes]
while (stack.length) {
const node = stack.pop()!
if (typeof node.id === 'number') map[node.id] = node.name
if (Array.isArray(node.children) && node.children.length) stack.push(...node.children)
}
deptNameMap.value = map
}
const ensureOptionsLoaded = async () => {
const [deviceTypeRes, supplierRes, deptRes, userRes] = await Promise.all([
DeviceTypeApi.getDeviceTypeTree({ pageNo: 1, pageSize: 10 }),
SupplierApi.getSupplierSimpleList(),
getSimpleDeptList(),
getSimpleUserList()
])
deviceTypeTree.value = deviceTypeRes
supplierOptions.value = (supplierRes ?? []).map((item: any) => item.name).filter((v: any) => typeof v === 'string')
const tree = handleTree(deptRes, 'id', 'parentId')
deptTree.value = tree
buildDeptNameMap(tree)
users.value = userRes ?? []
}
/** 打开弹窗 */
const open = async (type: string, id?: number, defaultDeviceTypeId?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
await ensureOptionsLoaded()
if (type === 'create' && defaultDeviceTypeId) {
formData.value.deviceType = defaultDeviceTypeId
}
//
if (id) {
formLoading.value = true
try {
formData.value = await DeviceLedgerApi.getDeviceLedger(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
await formRef.value.validate()
//
formLoading.value = true
try {
const workshopId = formData.value.workshop
const useDeptId = formData.value.useDept
const data = {
...(formData.value as any),
workshop: typeof workshopId === 'number' ? deptNameMap.value[workshopId] : undefined,
useDept: typeof useDeptId === 'number' ? deptNameMap.value[useDeptId] : undefined
} as unknown as DeviceLedgerVO
if (formType.value === 'create') {
await DeviceLedgerApi.createDeviceLedger(data)
message.success(t('common.createSuccess'))
} else {
await DeviceLedgerApi.updateDeviceLedger(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
deviceCode: undefined,
deviceName: undefined,
deviceStatus: undefined,
deviceBrand: undefined,
deviceModel: undefined,
deviceSpec: undefined,
deviceType: undefined,
supplier: undefined,
workshop: undefined,
deviceLocation: undefined,
useDept: undefined,
deviceManager: undefined,
quantity: 1,
productionDate: undefined,
factoryEntryDate: undefined,
remark: undefined,
sort: undefined
}
formRef.value?.resetFields()
}
</script>
<style scoped>
.device-ledger-manager {
display: flex;
width: 100%;
gap: 12px;
}
.device-ledger-manager :deep(.el-input) {
flex: 1;
}
</style>

@ -0,0 +1,437 @@
<template>
<div class="device-ledger-layout">
<ContentWrap class="device-ledger-left">
<el-tree
v-loading="typeTreeLoading"
:data="typeTreeData"
node-key="id"
highlight-current
:props="typeTreeProps"
:default-expanded-keys="typeTreeExpandedKeys"
:expand-on-click-node="false"
@node-click="handleTypeNodeClick"
/>
</ContentWrap>
<div class="device-ledger-right">
<ContentWrap>
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="90px"
>
<el-form-item label="编码" prop="deviceCode">
<el-input
v-model="queryParams.deviceCode"
placeholder="请输入编码"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="名称" prop="deviceName">
<el-input
v-model="queryParams.deviceName"
placeholder="请输入名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="状态" prop="deviceStatus">
<el-select v-model="queryParams.deviceStatus" placeholder="请选择状态" clearable class="!w-240px">
<el-option
v-for="dict in tzStatusOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button type="primary" plain @click="openForm('create')" v-hasPermi="['mes:device-ledger:create']">
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['mes:device-ledger:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="序号" align="center" width="80" fixed="left">
<template #default="scope">
{{ (queryParams.pageNo - 1) * queryParams.pageSize + scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column label="编码" align="center" prop="deviceCode" />
<el-table-column label="名称" align="center" prop="deviceName" />
<el-table-column label="类型" align="center" prop="deviceType">
<template #default="scope">
<el-tag effect="light">
{{ getDeviceTypeName(scope.row.deviceTypeName ?? scope.row.deviceType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="deviceStatus">
<template #default="scope">
<el-tag
:type="getTzStatusTagType(scope.row.deviceStatus)"
:color="getTzStatusTagColor(scope.row.deviceStatus)"
:style="getTzStatusTagStyle(scope.row.deviceStatus)"
effect="light"
disable-transitions
>
{{ getTzStatusLabel(scope.row.deviceStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="规格" align="center" prop="deviceSpec" />
<el-table-column label="型号" align="center" prop="deviceModel" />
<el-table-column label="品牌" align="center" prop="deviceBrand" />
<el-table-column label="数量" align="center" prop="quantity" />
<el-table-column label="生产日期" align="center" prop="productionDate" :formatter="dateFormatter" width="180px" />
<el-table-column label="入厂日期" align="center" prop="factoryEntryDate" :formatter="dateFormatter" width="180px" />
<el-table-column label="供应商" align="center" prop="supplier" />
<el-table-column label="所属车间" align="center" prop="workshop" />
<el-table-column label="位置" align="center" prop="deviceLocation" />
<el-table-column label="使用部门" align="center" prop="useDept" />
<el-table-column label="责任人" align="center" prop="deviceManager" />
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="创建人" align="center" prop="creator" />
<el-table-column label="创建时间" align="center" prop="createTime" :formatter="dateFormatter" width="180px" />
<el-table-column label="更新时间" align="center" prop="updateTime" :formatter="dateFormatter" width="180px" />
<el-table-column label="操作" align="center" min-width="160px" fixed="right">
<template #default="scope">
<el-button link @click="handleDetail(scope.row.id)"></el-button>
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['mes:device-ledger:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['mes:device-ledger:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" @pagination="getList" />
</ContentWrap>
<ContentWrap v-if="detailVisible" v-loading="detailLoading">
<div class="device-ledger-detail-header">
<div class="device-ledger-detail-title">详情</div>
<el-button link @click="closeDetail">
<Icon icon="ep:close" />
</el-button>
</div>
<el-descriptions :column="3" class="device-ledger-detail-desc">
<el-descriptions-item label="设备编号">{{ detailData?.deviceCode ?? '' }}</el-descriptions-item>
<el-descriptions-item label="设备名称">{{ detailData?.deviceName ?? '' }}</el-descriptions-item>
<el-descriptions-item label="设备状态">
<dict-tag type="mes_tz_status" :value="detailData?.deviceStatus" />
</el-descriptions-item>
<el-descriptions-item label="设备品牌">{{ detailData?.deviceBrand ?? '' }}</el-descriptions-item>
<el-descriptions-item label="设备型号">{{ detailData?.deviceModel ?? '' }}</el-descriptions-item>
<el-descriptions-item label="设备规格">{{ detailData?.deviceSpec ?? '' }}</el-descriptions-item>
<el-descriptions-item label="设备类型">
<el-tag effect="light">{{ getDeviceTypeName(detailData?.deviceTypeName ?? detailData?.deviceType) }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="供应商">{{ detailData?.supplier ?? '' }}</el-descriptions-item>
<el-descriptions-item label="所属车间">{{ detailData?.workshop ?? '' }}</el-descriptions-item>
<el-descriptions-item label="所属系统组织">{{ detailData?.systemOrg ?? '' }}</el-descriptions-item>
<el-descriptions-item label="设备位置">{{ detailData?.deviceLocation ?? '' }}</el-descriptions-item>
<el-descriptions-item label="设备负责人">{{ detailData?.deviceManager ?? '' }}</el-descriptions-item>
<el-descriptions-item label="设备生产日期">{{ formatDetailDate(detailData?.productionDate) }}</el-descriptions-item>
<el-descriptions-item label="设备入厂日期">{{ formatDetailDate(detailData?.factoryEntryDate) }}</el-descriptions-item>
<el-descriptions-item label="备注">{{ detailData?.remark ?? detailData?.deviceRemark ?? '' }}</el-descriptions-item>
</el-descriptions>
<el-tabs v-model="detailActiveTab" class="mt-12px">
<el-tab-pane label="点检履历" name="check">
<el-empty />
</el-tab-pane>
<el-tab-pane label="保养履历" name="maintain">
<el-empty />
</el-tab-pane>
<el-tab-pane label="维修履历" name="repair">
<el-empty />
</el-tab-pane>
</el-tabs>
</ContentWrap>
</div>
</div>
<!-- 表单弹窗添加/修改 -->
<DeviceLedgerForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { dateFormatter, formatDate } from '@/utils/formatTime'
import download from '@/utils/download'
import { DeviceLedgerApi, DeviceLedgerVO } from '@/api/mes/deviceledger'
import { DeviceTypeApi, DeviceTypeTreeVO } from '@/api/mes/devicetype'
import DeviceLedgerForm from './DeviceLedgerForm.vue'
import { getIntDictOptions } from '@/utils/dict'
import { isHexColor } from '@/utils/color'
import { useDictStoreWithOut } from '@/store/modules/dict'
/** 设备类型 列表 */
defineOptions({ name: 'DeviceLedger' })
const message = useMessage() //
const { t } = useI18n() //
const loading = ref(true) //
const list = ref<DeviceLedgerVO[]>([]) //
const total = ref(0) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
deviceCode: undefined,
deviceName: undefined,
deviceStatus: undefined,
deviceType: undefined,
})
const queryFormRef = ref() //
const exportLoading = ref(false) //
const dictStore = useDictStoreWithOut()
const dictReady = ref(false)
const tzStatusOptions = computed(() => {
if (!dictReady.value) return []
const options = getIntDictOptions('mes_tz_status')
return options.filter((o) => o.value !== null && o.value !== undefined && !Number.isNaN(o.value))
})
const detailVisible = ref(false)
const detailLoading = ref(false)
const detailData = ref<DeviceLedgerVO | undefined>()
const detailActiveTab = ref('check')
const selectedDetailId = ref<number | undefined>()
const typeTreeLoading = ref(false)
const typeTreeData = ref<DeviceTypeTreeVO[]>([])
const typeTreeProps = { label: 'name', children: 'children' }
const typeTreeExpandedKeys = ref<number[]>([0])
const deviceTypeNameMap = ref<Record<number, string>>({})
const buildDeviceTypeNameMap = (nodes: DeviceTypeTreeVO[]) => {
const map: Record<number, string> = {}
const stack = [...nodes]
while (stack.length) {
const node = stack.pop()!
if (typeof node.id === 'number') map[node.id] = node.name
if (Array.isArray(node.children) && node.children.length) stack.push(...node.children)
}
deviceTypeNameMap.value = map
}
const getTypeTree = async () => {
typeTreeLoading.value = true
try {
const data = await DeviceTypeApi.getDeviceTypeTree({ pageNo: 1, pageSize: 10 })
const treeChildren = JSON.parse(JSON.stringify(data ?? []))
typeTreeData.value = [{ id: 0, code: '', name: '全部', remark: '', sort: 0, children: treeChildren } as any]
buildDeviceTypeNameMap(treeChildren)
typeTreeExpandedKeys.value = [0]
} finally {
typeTreeLoading.value = false
}
}
const handleTypeNodeClick = (node: any) => {
const id = node?.id
queryParams.deviceType = id === 0 ? undefined : id
queryParams.pageNo = 1
getList()
}
const getDeviceTypeName = (value: any) => {
const id = typeof value === 'number' ? value : Number(value)
if (!Number.isNaN(id) && deviceTypeNameMap.value[id]) return deviceTypeNameMap.value[id]
return value ?? ''
}
const getTzStatusLabel = (value: any) => {
const v = value === '' || value === null || value === undefined ? undefined : Number(value)
const found = tzStatusOptions.value.find((d) => d.value === v)
return found?.label ?? ''
}
const getTzStatusTagType = (value: any) => {
const v = value === '' || value === null || value === undefined ? undefined : Number(value)
const found = tzStatusOptions.value.find((d) => d.value === v)
const type = found?.colorType
if (type + '' === 'primary' || type + '' === 'default') return '' as any
return (type ?? '') as any
}
const getTzStatusTagColor = (value: any) => {
const v = value === '' || value === null || value === undefined ? undefined : Number(value)
const found = tzStatusOptions.value.find((d) => d.value === v)
if (found?.cssClass && isHexColor(found.cssClass)) return found.cssClass
return ''
}
const getTzStatusTagStyle = (value: any) => {
const color = getTzStatusTagColor(value)
if (!color) return ''
return 'color: #fff'
}
const formatDetailDate = (value: any) => {
if (!value) return ''
return formatDate(new Date(value), 'YYYY-MM-DD')
}
const handleDetail = async (id: number) => {
if (detailVisible.value && selectedDetailId.value === id) {
closeDetail()
return
}
selectedDetailId.value = id
detailVisible.value = true
detailActiveTab.value = 'check'
detailLoading.value = true
try {
detailData.value = await DeviceLedgerApi.getDeviceLedger(id)
} finally {
detailLoading.value = false
}
}
const closeDetail = () => {
detailVisible.value = false
selectedDetailId.value = undefined
detailData.value = undefined
}
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await DeviceLedgerApi.getDeviceLedgerPage({
pageNo: queryParams.pageNo,
pageSize: queryParams.pageSize,
deviceCode: queryParams.deviceCode,
deviceName: queryParams.deviceName,
deviceStatus: queryParams.deviceStatus,
deviceType: queryParams.deviceType
})
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id, queryParams.deviceType)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
//
await message.delConfirm()
//
await DeviceLedgerApi.deleteDeviceLedger(id)
message.success(t('common.delSuccess'))
//
await getList()
} catch {}
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
//
await message.exportConfirm()
//
exportLoading.value = true
const data = await DeviceLedgerApi.exportDeviceLedger({
deviceCode: queryParams.deviceCode,
deviceName: queryParams.deviceName,
deviceStatus: queryParams.deviceStatus,
deviceType: queryParams.deviceType
})
download.excel(data, '设备台账.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(async () => {
await dictStore.setDictMap()
dictReady.value = true
getTypeTree()
getList()
})
</script>
<style scoped>
.device-ledger-layout {
display: flex;
gap: 12px;
}
.device-ledger-left {
width: 280px;
flex: 0 0 auto;
}
.device-ledger-right {
flex: 1;
min-width: 0;
}
.device-ledger-detail-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.device-ledger-detail-title {
font-size: 14px;
font-weight: 600;
}
</style>

@ -0,0 +1,117 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="编码" prop="code">
<el-input v-model="formData.code" placeholder="请输入编码" />
</el-form-item>
<el-form-item label="名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入名称" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" placeholder="请输入备注" />
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input v-model="formData.sort" placeholder="请输入排序" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { DeviceTypeApi, DeviceTypeTreeVO, DeviceTypeVO } from '@/api/mes/devicetype'
/** 设备类型 表单 */
defineOptions({ name: 'DeviceTypeForm' })
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formData = ref({
id: undefined,
code: undefined,
name: undefined,
remark: undefined,
sort: undefined,
parentId: undefined
})
const formRules = reactive({
code: [{ required: true, message: '编码不能为空', trigger: 'blur' }],
name: [{ required: true, message: '名称不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
/** 打开弹窗 */
const open = async (type: string, id?: number, parentRow?: DeviceTypeTreeVO) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
if (type === 'create') {
formData.value.parentId = parentRow?.id ?? 0
}
//
if (id) {
formLoading.value = true
try {
formData.value = await DeviceTypeApi.getDeviceType(id)
if (formData.value.parentId === undefined) {
formData.value.parentId = parentRow?.parentId ?? 0
}
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
await formRef.value.validate()
//
formLoading.value = true
try {
const data = formData.value as unknown as DeviceTypeVO
if (formType.value === 'create') {
await DeviceTypeApi.createDeviceType(data)
message.success(t('common.createSuccess'))
} else {
await DeviceTypeApi.updateDeviceType(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
code: undefined,
name: undefined,
remark: undefined,
sort: undefined,
parentId: undefined
}
formRef.value?.resetFields()
}
</script>

@ -0,0 +1,233 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="编码" prop="code">
<el-input
v-model="queryParams.code"
placeholder="请输入编码"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="名称" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-220px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['mes:device-type:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
row-key="id"
default-expand-all
:tree-props="{ children: 'children' }"
>
<el-table-column label="id" align="center" prop="id" />
<el-table-column label="编码" align="center" prop="code" />
<el-table-column label="名称" align="center" prop="name" />
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="排序" align="center" prop="sort" />
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="操作" align="center" min-width="120px">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('create', undefined, scope.row)"
>
添加
</el-button>
<el-button
link
type="primary"
@click="openForm('update', scope.row.id, scope.row)"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
v-if="showPagination"
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<DeviceTypeForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import { DeviceTypeApi, DeviceTypeTreeVO } from '@/api/mes/devicetype'
import DeviceTypeForm from './DeviceTypeForm.vue'
/** 设备类型 列表 */
defineOptions({ name: 'DeviceType' })
const message = useMessage() //
const { t } = useI18n() //
const loading = ref(true) //
const list = ref<DeviceTypeTreeVO[]>([]) //
const total = ref(0) //
const showPagination = ref(true)
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
code: undefined,
name: undefined,
createTime: []
})
const queryFormRef = ref() //
const exportLoading = ref(false) //
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const [createStartTime, createEndTime] = Array.isArray(queryParams.createTime)
? queryParams.createTime
: []
const data = await DeviceTypeApi.getDeviceTypeTree({
pageNo: queryParams.pageNo,
pageSize: queryParams.pageSize,
code: queryParams.code,
name: queryParams.name,
createStartTime: createStartTime || undefined,
createEndTime: createEndTime || undefined
})
if (Array.isArray(data)) {
list.value = data
total.value = data.length
showPagination.value = false
return
}
list.value = data.list
total.value = data.total
showPagination.value = true
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number, parentRow?: DeviceTypeTreeVO) => {
formRef.value.open(type, id, parentRow)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
//
await message.delConfirm()
//
await DeviceTypeApi.deleteDeviceType(id)
message.success(t('common.delSuccess'))
//
await getList()
} catch {}
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
//
await message.exportConfirm()
//
exportLoading.value = true
const data = await DeviceTypeApi.exportDeviceType(queryParams)
download.excel(data, '设备类型.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

@ -62,7 +62,7 @@
<el-divider />
<el-descriptions :column="1" label-class-name="desc-label" direction="vertical" border>
<el-descriptions-item label="支付通道异步回调内容">
<el-text style="white-space: pre-wrap; word-break: break-word">
<el-text style=" word-break: break-word;white-space: pre-wrap">
{{ detailData.extension.channelNotifyData }}
</el-text>
</el-descriptions-item>

@ -62,7 +62,7 @@
</el-descriptions>
<el-descriptions :column="1" label-class-name="desc-label" direction="vertical" border>
<el-descriptions-item label="支付通道异步回调内容">
<el-text style="white-space: pre-wrap; word-break: break-word">
<el-text style=" word-break: break-word;white-space: pre-wrap">
{{ refundDetail.channelNotifyData }}
</el-text>
</el-descriptions-item>

Loading…
Cancel
Save