add 地秤管理界面

This commit is contained in:
Asoka 2025-06-06 11:35:06 +08:00
parent 310af266f9
commit c302448bc2
7 changed files with 1742 additions and 17 deletions

View File

@ -0,0 +1,156 @@
/* eslint-disable */
/* tslint:disable */
/*
* ---------------------------------------------------------------
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
* ## ##
* ## AUTHOR: adademo / https://github.com/adademo/swagger-typescript-api ##
* ## SOURCE: https://github.com/adademo/swagger-typescript-api ##
* ---------------------------------------------------------------
*/
import {
UspscaleAddInput,
UspscaleGetOutput,
UspscaleUpdateInput,
PageInputUspscaleGetPageInput,
ResultOutputUspscaleGetOutput,
ResultOutputPageOutputUspscaleGetPageOutput,
ResultOutputInt64,
} from './data-contracts'
import { ContentType, HttpClient, RequestParams } from './http-client'
import { AxiosResponse } from 'axios'
export class UspscaleApi<SecurityDataType = unknown> extends HttpClient<SecurityDataType> {
/**
* No description
*
* @tags uspscale
* @name Get
* @summary
* @request GET:/api/admin/usp-scale/get
* @secure
*/
get = (
query?: {
/** @format int64 */
id?: number
},
params: RequestParams = {}
) =>
this.request<ResultOutputUspscaleGetOutput, any>({
path: `/api/admin/usp-scale/get`,
method: 'GET',
query: query,
secure: true,
format: 'json',
...params,
})
/**
* No description
*
* @tags uspscale
* @name GetPage
* @summary
* @request POST:/api/admin/usp-scale/get-page
* @secure
*/
getPage = (data: PageInputUspscaleGetPageInput, params: RequestParams = {}) =>
this.request<ResultOutputPageOutputUspscaleGetPageOutput, any>({
path: `/api/admin/usp-scale/get-page`,
method: 'POST',
body: data,
secure: true,
type: ContentType.Json,
format: 'json',
...params,
})
/**
* No description
*
* @tags uspscale
* @name Add
* @summary
* @request POST:/api/admin/usp-scale/add
* @secure
*/
add = (data: UspscaleAddInput, params: RequestParams = {}) =>
this.request<ResultOutputInt64, any>({
path: `/api/admin/usp-scale/add`,
method: 'POST',
body: data,
secure: true,
type: ContentType.Json,
format: 'json',
...params,
})
/**
* No description
*
* @tags uspscale
* @name Update
* @summary
* @request PUT:/api/admin/usp-scale/update
* @secure
*/
update = (data: UspscaleUpdateInput, params: RequestParams = {}) =>
this.request<AxiosResponse, any>({
path: `/api/admin/usp-scale/update`,
method: 'PUT',
body: data,
secure: true,
type: ContentType.Json,
...params,
})
/**
* No description
*
* @tags uspscale
* @name Delete
* @summary
* @request DELETE:/api/admin/usp-scale/delete
* @secure
*/
delete = (
query?: {
/** @format int64 */
id?: number
},
params: RequestParams = {}
) =>
this.request<AxiosResponse, any>({
path: `/api/admin/usp-scale/delete`,
method: 'DELETE',
query: query,
secure: true,
...params,
})
/**
* No description
*
* @tags uspscale
* @name SoftDelete
* @summary
* @request DELETE:/api/admin/usp-scale/soft-delete
* @secure
*/
softDelete = (
query?: {
/** @format int64 */
id?: number
},
params: RequestParams = {}
) =>
this.request<AxiosResponse, any>({
path: `/api/admin/usp-scale/soft-delete`,
method: 'DELETE',
query: query,
secure: true,
...params,
})
}

View File

@ -6594,3 +6594,229 @@ export interface WebSocketPreConnectInput {
*/
websocketId?: number | null
}
/** 秤台设备 */
export interface UspscaleGetOutput {
/** 设备编号 */
deviceNo?: string | null
/** 资产编号 */
assetNo?: string | null
/** 型号 */
model?: string | null
/** 规格 */
specification?: string | null
/** 房间ID */
roomID?: number
/** 维护标记 */
maintenanceFlag?: boolean
/** 是否喂料秤 */
isFeedingScale?: boolean
/** 启用 */
enabled?: boolean
/**
* Id
* @format int64
*/
id: number
}
/** 结果输出 */
export interface ResultOutputUspscaleGetOutput {
/** 是否成功标记 */
success?: boolean
/** 编码 */
code?: string | null
/** 消息 */
msg?: string | null
/** 秤台设备 */
data?: UspscaleGetOutput
}
/** 添加 */
export interface UspscaleAddInput {
/** 设备编号 */
deviceNo?: string | null
/** 资产编号 */
assetNo?: string | null
/** 型号 */
model?: string | null
/** 规格 */
specification?: string | null
/** 房间ID */
roomID?: number
/** 维护标记 */
maintenanceFlag?: boolean
/** 是否喂料秤 */
isFeedingScale?: boolean
/** 启用 */
enabled?: boolean
}
/** 修改 */
export interface UspscaleUpdateInput {
/** 设备编号 */
deviceNo?: string | null
/** 资产编号 */
assetNo?: string | null
/** 型号 */
model?: string | null
/** 规格 */
specification?: string | null
/** 房间ID */
roomID?: number
/** 设备负责人ID */
principalId?: number
/** 维护标记 */
maintenanceFlag?: boolean
/** 报警下限值(g) */
warningLowerLimit?: number
/** 子系统IP */
subSystemIP?: string | null
/** 子系统IP端口 */
subSystemIPPort?: string | null
/** 设备IP */
deviceIP?: string | null
/** 设备IP端口 */
deviceIPPort?: string | null
/** 精度(g) */
scalePrecision?: number
/** 服务名称 */
serviceName?: string | null
/** 是否喂料秤 */
isFeedingScale?: boolean
/** 启用 */
enabled?: boolean
/**
* Id
* @format int64
*/
id: number
}
/** 结果输出 */
export interface ResultOutputListUspscaleGetListOutput {
/** 是否成功标记 */
success?: boolean
/** 编码 */
code?: string | null
/** 消息 */
msg?: string | null
/** 数据 */
data?: UspscaleGetListOutput[] | null
}
/** 秤台设备列表 */
export interface UspscaleGetListOutput {
/**
* Id
* @format int64
*/
id?: number
/** 设备编号 */
deviceNo?: string | null
/** 资产编号 */
assetNo?: string | null
/** 型号 */
model?: string | null
/** 规格 */
specification?: string | null
/** 房间ID */
roomID?: number
/** 维护标记 */
maintenanceFlag?: boolean
/** 是否喂料秤 */
isFeedingScale?: boolean
/** 启用 */
enabled?: boolean
/**
*
* @format date-time
*/
createdTime?: string | null
}
/** 分页信息输入 */
export interface PageInputUspscaleGetPageInput {
dynamicFilter?: DynamicFilterInfo
/** 排序列表 */
sortList?: SortInput[] | null
/**
*
* @format int32
*/
currentPage?: number
/**
*
* @format int32
*/
pageSize?: number
/** 分页请求 */
filter?: UspscaleGetPageInput
}
/** 分页请求 */
export interface UspscaleGetPageInput {
/** 关键词 */
keyWord?: string | null
/** 房间ID */
roomID?: number
/** 开始日期 */
stDate?: string | null
/** 结束日期 */
edDate?: string | null
}
/** 结果输出 */
export interface ResultOutputPageOutputUspscaleGetPageOutput {
/** 是否成功标记 */
success?: boolean
/** 编码 */
code?: string | null
/** 消息 */
msg?: string | null
/** 分页信息输出 */
data?: PageOutputUspscaleGetPageOutput
}
/** 分页信息输出 */
export interface PageOutputUspscaleGetPageOutput {
/**
*
* @format int64
*/
total?: number
/** 数据 */
list?: UspscaleGetPageOutput[] | null
}
/** 分页响应 */
export interface UspscaleGetPageOutput {
/**
*
* @format int64
*/
id?: number
/** 设备编号 */
deviceNo?: string | null
/** 资产编号 */
assetNo?: string | null
/** 型号 */
model?: string | null
/** 规格 */
specification?: string | null
/** 房间ID */
roomID?: number
/** 维护标记 */
maintenanceFlag?: boolean
/** 是否喂料秤 */
isFeedingScale?: boolean
/** 启用 */
enabled?: boolean
/** 创建者 */
createdUserName?: string | null
/**
*
* @format date-time
*/
createdTime?: string | null
}

View File

@ -19,9 +19,9 @@
</el-menu>
<!-- 将四个模块合并为一个下拉菜单类似语言切换 -->
<el-dropdown
:show-timeout="70"
:hide-timeout="50"
<el-dropdown
:show-timeout="70"
:hide-timeout="50"
trigger="click"
@command="onModuleCommand"
class="module-dropdown"
@ -35,8 +35,8 @@
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="module in moduleList"
<el-dropdown-item
v-for="module in moduleList"
:key="module.path"
:command="module.path"
:class="{ 'is-active': isCurrentModule(module.path) }"
@ -104,17 +104,17 @@ const moduleList = computed(() => {
//
const currentModule = computed(() => {
const current = moduleList.value.find(module =>
state.currentModulePath === module.path ||
const current = moduleList.value.find(module =>
state.currentModulePath === module.path ||
route.path.startsWith(module.path)
)
console.log('state.currentModulePath', state.currentModulePath)
return current || moduleList.value[0] || {
path: '',
title: '选择模块',
icon: 'ele-Menu'
return current || moduleList.value[0] || {
path: '',
title: '选择模块',
icon: 'ele-Menu'
}
})
//
@ -180,11 +180,11 @@ const setCurrentRouterHighlight = (currentRoute: RouteToFrom) => {
const onModuleCommand = (path: string) => {
//
state.currentModulePath = path
//
const selectedModule = menuLists.value.find(item => item.path === path)
if (!selectedModule) return
//
if (selectedModule.children && selectedModule.children.length > 0) {
const firstChild = selectedModule.children.find((child: RouteRecordRaw) => !child.meta?.isHide)
@ -272,7 +272,7 @@ onBeforeMount(() => {
const initCurrentModule = () => {
//
const currentPath = route.path
const matchedModule = menuLists.value.find(module =>
const matchedModule = menuLists.value.find(module =>
currentPath.startsWith(module.path) && module.path !== '/'
)
if (matchedModule) {
@ -288,16 +288,16 @@ onBeforeRouteUpdate((to) => {
// https://gitee.com/lyt-top/vue-next-admin/issues/I3YX6G
setCurrentRouterHighlight(to)
//
const matchedModule = menuLists.value.find(module =>
const matchedModule = menuLists.value.find(module =>
to.path.startsWith(module.path) && module.path !== '/'
)
if (matchedModule) {
state.currentModulePath = matchedModule.path
}
console.log('onBeforeRouteUpdate', to.path, matchedModule.path)
console.log('onBeforeRouteUpdate', to.path, matchedModule.path)
mittBus.emit('setSendClassicChildren', setBeforeRouteUpdateClassicChildren(to.path))
// // tagsView
// let { layout, isClassicSplitMenu } = themeConfig.value
@ -339,6 +339,7 @@ onBeforeRouteUpdate((to) => {
font-size: 14px;
border-bottom: 2px solid transparent;
transition: color 0.3s, border-color 0.3s;
cursor: pointer;
.submenu-icon {
margin-top: 0;
@ -378,6 +379,7 @@ onBeforeRouteUpdate((to) => {
background: var(--el-bg-color);
font-size: 14px;
transition: background 0.3s, color 0.3s, border-color 0.3s;
cursor: pointer;
.module-title { font-weight: 500; }
&:hover {
@ -406,6 +408,7 @@ onBeforeRouteUpdate((to) => {
margin: 4px 6px;
border-radius: 6px;
transition: background 0.3s, color 0.3s;
cursor: pointer;
.dropdown-icon {
margin-right: 10px;

View File

@ -0,0 +1,106 @@
<template>
<div>
<el-dialog
v-model="state.showDialog"
destroy-on-close
title="通讯设置"
draggable
:close-on-click-modal="false"
:close-on-press-escape="false"
width="600px"
>
<el-form :model="form" ref="formRef" size="default" label-width="120px">
<el-row :gutter="35">
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<el-form-item label="子系统IP" prop="subSystemIP">
<el-input v-model="form.subSystemIP" clearable placeholder="请输入子系统IP地址" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<el-form-item label="子系统端口" prop="subSystemIPPort">
<el-input v-model="form.subSystemIPPort" clearable placeholder="请输入子系统端口" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<el-form-item label="设备IP" prop="deviceIP">
<el-input v-model="form.deviceIP" clearable placeholder="请输入设备IP地址" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<el-form-item label="设备端口" prop="deviceIPPort">
<el-input v-model="form.deviceIPPort" clearable placeholder="请输入设备端口" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
<el-form-item label="服务名称" prop="serviceName">
<el-input v-model="form.serviceName" clearable placeholder="请输入服务名称" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="onCancel" size="default"> </el-button>
<el-button type="primary" @click="onSure" size="default"> </el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup name="admin/Uspscale/communication-form">
import { reactive, toRefs, ref } from 'vue'
interface CommunicationData {
subSystemIP?: string | null
subSystemIPPort?: string | null
deviceIP?: string | null
deviceIPPort?: string | null
serviceName?: string | null
}
const formRef = ref()
const state = reactive({
showDialog: false,
form: {
subSystemIP: '',
subSystemIPPort: '',
deviceIP: '',
deviceIPPort: '',
serviceName: '',
} as CommunicationData,
})
const { form } = toRefs(state)
const emit = defineEmits(['confirm'])
//
const open = (data: CommunicationData = {}) => {
state.form = {
subSystemIP: data.subSystemIP || '',
subSystemIPPort: data.subSystemIPPort || '',
deviceIP: data.deviceIP || '',
deviceIPPort: data.deviceIPPort || '',
serviceName: data.serviceName || '',
}
state.showDialog = true
}
//
const onCancel = () => {
state.showDialog = false
}
//
const onSure = () => {
emit('confirm', { ...state.form })
state.showDialog = false
}
defineExpose({
open,
})
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,364 @@
<template>
<div>
<el-dialog
v-model="state.showDialog"
destroy-on-close
:title="title"
draggable
:close-on-click-modal="false"
:close-on-press-escape="false"
width="900px"
>
<el-form :model="form" ref="formRef" size="default" label-width="120px">
<!-- 基本信息 -->
<div class="form-section">
<div class="section-title">基本信息</div>
<el-row :gutter="25">
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<el-form-item label="设备编号" prop="deviceNo" :rules="[{ required: true, message: '请输入设备编号', trigger: ['blur', 'change'] }]">
<el-input v-model="form.deviceNo" clearable placeholder="请输入设备编号" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<el-form-item label="资产编号" prop="assetNo">
<el-input v-model="form.assetNo" clearable placeholder="请输入资产编号" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<el-form-item label="型号" prop="model">
<el-select v-model="form.model" placeholder="请选择设备型号" clearable style="width: 100%">
<el-option
v-for="model in state.modelOptions"
:key="model.value"
:label="model.label"
:value="model.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<el-form-item label="规格" prop="specification">
<el-input v-model="form.specification" clearable placeholder="请输入设备规格" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<el-form-item label="房间" prop="roomID" :rules="[{ required: true, message: '请选择房间', trigger: ['blur', 'change'] }]">
<el-select v-model="form.roomID" placeholder="请选择设备所在房间" style="width: 100%">
<el-option
v-for="room in state.roomOptions"
:key="room.id"
:label="room.roomName"
:value="room.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<el-form-item label="设备负责人" prop="principalId">
<el-select
v-model="form.principalId"
placeholder="请输入姓名搜索设备负责人"
clearable
filterable
style="width: 100%"
>
<el-option
v-for="user in state.userOptions"
:key="user.id"
:label="`${user.name} (${user.userName})`"
:value="user.id"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
</div>
<!-- 技术参数 -->
<div class="form-section">
<div class="section-title">技术参数</div>
<el-row :gutter="25">
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<el-form-item label="精度(g)" prop="scalePrecision">
<el-input-number v-model="form.scalePrecision" :min="0" :precision="1" style="width: 100%" placeholder="请输入设备精度" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<el-form-item label="报警下限值(g)" prop="warningLowerLimit">
<el-input-number v-model="form.warningLowerLimit" :min="0" :precision="2" style="width: 100%" placeholder="请输入报警下限值" />
</el-form-item>
</el-col>
</el-row>
</div>
<!-- 设备配置 -->
<div class="form-section">
<div class="section-title">设备配置</div>
<el-row :gutter="25">
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<el-form-item label="维护标记">
<el-switch
v-model="form.maintenanceFlag"
active-text="维护中"
inactive-text="正常"
/>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<el-form-item label="是否补料地秤">
<el-switch
v-model="form.isFeedingScale"
active-text="是"
inactive-text="否"
/>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<el-form-item label="启用状态">
<el-switch
v-model="form.enabled"
active-text="启用"
inactive-text="禁用"
/>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<el-form-item label="通讯设置">
<el-button type="primary" @click="openCommunicationForm" icon="ele-Setting">
配置通讯参数
</el-button>
</el-form-item>
</el-col>
</el-row>
</div>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="onCancel" size="default"> </el-button>
<el-button type="primary" @click="onSure" size="default" :loading="state.sureLoading"> </el-button>
</span>
</template>
</el-dialog>
<!-- 通讯设置表单 -->
<communication-form ref="communicationFormRef" @confirm="onCommunicationConfirm" />
</div>
</template>
<script lang="ts" setup name="admin/Uspscale/form">
import { reactive, toRefs, ref, getCurrentInstance, onMounted, defineAsyncComponent } from 'vue'
import { UspscaleUpdateInput, RoomGetPageOutput, PageInputRoomGetPageInput, UserGetPageOutput, PageInputUserGetPageInput } from '/@/api/admin/data-contracts'
import { UspscaleApi } from '/@/api/admin/UspscaleApi'
import { RoomApi } from '/@/api/admin/Room'
import { UserApi } from '/@/api/admin/User'
import eventBus from '/@/utils/mitt'
//
const CommunicationForm = defineAsyncComponent(() => import('./communication-form.vue'))
const { proxy } = getCurrentInstance() as any
defineProps({
title: {
type: String,
default: '',
},
})
const formRef = ref()
const communicationFormRef = ref()
const state = reactive({
showDialog: false,
sureLoading: false,
form: {
enabled: true,
maintenanceFlag: false,
isFeedingScale: false,
roomID: 0,
} as UspscaleUpdateInput,
roomOptions: [] as Array<RoomGetPageOutput>,
userOptions: [] as Array<UserGetPageOutput>,
modelOptions: [
{ label: 'CAIS2-U 1500kg', value: '001' },
{ label: 'METTLER', value: '3' },
{ label: 'OHAUS', value: '5' }
],
data: [],
})
const { form } = toRefs(state)
onMounted(() => {
getRoomOptions()
getUserOptions()
})
const getRoomOptions = async () => {
try {
const roomPageInput = {
currentPage: 1,
pageSize: 1000, //
filter: {
keyWord: '',
}
} as PageInputRoomGetPageInput
const res = await new RoomApi().getPage(roomPageInput)
if (res?.success) {
state.roomOptions = res.data?.list ?? []
}
} catch (error) {
console.error('获取房间列表失败:', error)
}
}
const getUserOptions = async () => {
try {
const userPageInput = {
currentPage: 1,
pageSize: 1000, //
filter: {
orgId: null,
}
} as PageInputUserGetPageInput
const res = await new UserApi().getPage(userPageInput)
if (res?.success) {
state.userOptions = res.data?.list ?? []
}
} catch (error) {
console.error('获取用户列表失败:', error)
}
}
//
const open = async (row: any = {}) => {
proxy.$modal.loading()
if (row.id > 0) {
const res = await new UspscaleApi().get({ id: row.id }, { loading: true })
if (res?.success) {
let formData = res.data as UspscaleUpdateInput
state.form = formData
}
} else {
state.form = {
enabled: true,
maintenanceFlag: false,
isFeedingScale: false,
roomID: undefined,
principalId: undefined,
warningLowerLimit: 0,
scalePrecision: 0,
deviceNo: '',
assetNo: '',
model: '',
specification: '',
subSystemIP: '',
subSystemIPPort: '',
deviceIP: '',
deviceIPPort: '',
serviceName: '',
} as UspscaleUpdateInput
}
proxy.$modal.closeLoading()
state.showDialog = true
}
//
const openCommunicationForm = () => {
const communicationData = {
subSystemIP: state.form.subSystemIP,
subSystemIPPort: state.form.subSystemIPPort,
deviceIP: state.form.deviceIP,
deviceIPPort: state.form.deviceIPPort,
serviceName: state.form.serviceName,
}
communicationFormRef.value.open(communicationData)
}
//
const onCommunicationConfirm = (data: any) => {
state.form.subSystemIP = data.subSystemIP
state.form.subSystemIPPort = data.subSystemIPPort
state.form.deviceIP = data.deviceIP
state.form.deviceIPPort = data.deviceIPPort
state.form.serviceName = data.serviceName
}
//
const onCancel = () => {
state.showDialog = false
}
//
const onSure = () => {
formRef.value.validate(async (valid: boolean) => {
if (!valid) return
state.sureLoading = true
let res = {} as any
if (state.form.id != undefined && state.form.id > 0) {
res = await new UspscaleApi().update(state.form, { showSuccessMessage: true }).catch(() => {
state.sureLoading = false
})
} else {
res = await new UspscaleApi().add(state.form, { showSuccessMessage: true }).catch(() => {
state.sureLoading = false
})
}
state.sureLoading = false
if (res?.success) {
eventBus.emit('refreshRoom')
state.showDialog = false
}
})
}
defineExpose({
open,
})
</script>
<style lang="scss" scoped>
.form-section {
margin-bottom: 24px;
&:last-child {
margin-bottom: 0;
}
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 2px solid #f0f0f0;
position: relative;
&:before {
content: '';
position: absolute;
left: 0;
bottom: -2px;
width: 40px;
height: 2px;
background: #409eff;
}
}
:deep(.el-form-item) {
margin-bottom: 18px;
}
:deep(.el-switch__label) {
font-size: 13px;
}
</style>

View File

@ -0,0 +1,300 @@
<template>
<MyLayout>
<el-card v-show="state.showQuery" class="my-query-box mt8" shadow="never" :body-style="{ paddingBottom: '0' }">
<el-form :inline="true" label-width="auto" @submit.stop.prevent>
<el-form-item label="关键词">
<el-input v-model="state.filter.keyWord" placeholder="设备编号、资产编号" @keyup.enter="onQuery" />
</el-form-item>
<el-form-item label="设备型号">
<el-select v-model="state.filter.model" placeholder="请选择设备型号" clearable style="width: 150px">
<el-option
v-for="model in state.modelOptions"
:key="model.value"
:label="model.label"
:value="model.value"
/>
</el-select>
</el-form-item>
<el-form-item label="房间">
<el-select v-model="state.filter.roomID" placeholder="请选择房间" clearable style="width: 150px">
<el-option
v-for="room in state.roomOptions"
:key="room.id"
:label="room.roomName"
:value="room.id"
/>
</el-select>
</el-form-item>
<el-form-item label="开始时间">
<el-date-picker
v-model="state.filter.stDate"
type="datetime"
placeholder="选择开始时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 180px"
/>
</el-form-item>
<el-form-item label="结束时间">
<el-date-picker
v-model="state.filter.edDate"
type="datetime"
placeholder="选择结束时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 180px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="ele-Search" @click="onQuery"> 查询 </el-button>
<el-button v-auth="'api:admin:usp-scale:add'" type="primary" icon="ele-Plus" @click="onAdd"> 新增 </el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="my-fill mt8" shadow="never">
<div class="my-tools-box mb8 my-flex my-flex-between">
<div>
</div>
</div>
<el-table
v-if="state.showUspscaleList"
:data="state.uspscaleListData"
style="width: 100%"
v-loading="state.loading"
row-key="id"
default-expand-all
border
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
>
<el-table-column prop="deviceNo" label="设备编号" min-width="120" show-overflow-tooltip />
<el-table-column prop="assetNo" label="资产编号" min-width="120" show-overflow-tooltip />
<el-table-column prop="model" label="型号" min-width="120" show-overflow-tooltip>
<template #default="{ row }">
{{ getModelName(row.model) }}
</template>
</el-table-column>
<el-table-column prop="specification" label="规格" min-width="120" show-overflow-tooltip />
<el-table-column prop="roomID" label="房间" width="120" align="center" show-overflow-tooltip>
<template #default="{ row }">
{{ getRoomName(row.roomID) }}
</template>
</el-table-column>
<el-table-column label="维护标记" width="80" align="center" show-overflow-tooltip>
<template #default="{ row }">
<el-tag type="warning" v-if="row.maintenanceFlag">维护中</el-tag>
<el-tag type="success" v-else>正常</el-tag>
</template>
</el-table-column>
<el-table-column label="补料地秤" width="100" align="center" show-overflow-tooltip>
<template #default="{ row }">
<el-tag type="primary" v-if="row.isFeedingScale"></el-tag>
<el-tag type="info" v-else></el-tag>
</template>
</el-table-column>
<el-table-column label="状态" width="80" align="center" show-overflow-tooltip>
<template #default="{ row }">
<el-tag type="success" v-if="row.enabled">启用</el-tag>
<el-tag type="danger" v-else>禁用</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right" header-align="center" align="center">
<template #default="{ row }">
<el-button
v-if="auth('api:admin:usp-scale:update')"
icon="ele-EditPen"
size="small"
text
type="primary"
@click="onEdit(row)"
>编辑</el-button
>
<el-button
v-if="auth('api:admin:usp-scale:soft-delete')"
icon="ele-Delete"
size="small"
text
type="danger"
@click="onDelete(row)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<div class="my-flex my-flex-end" style="margin-top: 10px">
<el-pagination
v-model:currentPage="state.pageInput.currentPage"
v-model:page-size="state.pageInput.pageSize"
:total="state.total"
:page-sizes="[10, 20, 50, 100]"
size="small"
background
@size-change="onSizeChange"
@current-change="onCurrentChange"
layout="total, sizes, prev, pager, next, jumper"
/>
</div>
</el-card>
<uspscale-form ref="uspscaleFormRef" :title="state.uspscaleFormTitle"></uspscale-form>
</MyLayout>
</template>
<script lang="ts" setup name="admin/uspscale">
import { ref, reactive, onMounted, getCurrentInstance, onBeforeMount, defineAsyncComponent } from 'vue'
import { UspscaleGetListOutput, PageInputUspscaleGetPageInput, UspscaleGetPageOutput, RoomGetPageOutput, PageInputRoomGetPageInput } from '/@/api/admin/data-contracts'
import { UspscaleApi } from '/@/api/admin/UspscaleApi'
import { RoomApi } from '/@/api/admin/Room'
import eventBus from '/@/utils/mitt'
import { auth } from '/@/utils/authFunction'
//
const UspscaleForm = defineAsyncComponent(() => import('./components/uspscale-form.vue'))
const { proxy } = getCurrentInstance() as any
const uspscaleFormRef = ref()
const state = reactive({
loading: false,
uspscaleFormTitle: '',
filter: {
keyWord: '',
model: '',
roomID: undefined as number | undefined,
stDate: '',
edDate: '',
},
total: 0,
pageInput: {
currentPage: 1,
pageSize: 20,
} as PageInputUspscaleGetPageInput,
uspscaleListData: [] as Array<UspscaleGetPageOutput>,
roomOptions: [] as Array<RoomGetPageOutput>,
modelOptions: [
{ label: 'CAIS2-U 1500kg', value: '001' },
{ label: 'METTLER', value: '3' },
{ label: 'OHAUS', value: '5' }
],
showQuery: true,
showUspscaleList: true,
})
onMounted(() => {
getRoomOptions()
Query()
eventBus.off('refreshRoom')
eventBus.on('refreshRoom', () => {
Query()
})
})
onBeforeMount(() => {
eventBus.off('refreshRoom')
})
const onChangeUspscaleList = () => {
state.showUspscaleList = !state.showUspscaleList
if (state.showUspscaleList) {
Query()
}
}
const onQuery = () => {
Query()
}
const getRoomOptions = async () => {
try {
const roomPageInput = {
currentPage: 1,
pageSize: 1000, //
filter: {
keyWord: '',
}
} as PageInputRoomGetPageInput
const res = await new RoomApi().getPage(roomPageInput)
if (res?.success) {
state.roomOptions = res.data?.list ?? []
}
} catch (error) {
console.error('获取房间列表失败:', error)
}
}
const getRoomName = (roomID: number) => {
const room = state.roomOptions.find(r => r.id === roomID)
return room ? room.roomName : `未知房间(${roomID})`
}
const getModelName = (modelValue: string) => {
const model = state.modelOptions.find(m => m.value === modelValue)
return model ? model.label : modelValue
}
const Query = async () => {
state.loading = true
state.pageInput.filter = state.filter
const res = await new UspscaleApi().getPage(state.pageInput).catch(() => {
state.loading = false
})
state.uspscaleListData = res?.data?.list ?? []
state.total = res?.data?.total ?? 0
state.loading = false
}
const onAdd = () => {
state.uspscaleFormTitle = '新增地秤'
uspscaleFormRef.value.open({
id: 0,
enabled: true,
deviceNo: '',
assetNo: '',
model: '',
specification: '',
roomID: undefined,
principalId: undefined,
maintenanceFlag: false,
warningLowerLimit: 0,
subSystemIP: '',
subSystemIPPort: '',
deviceIP: '',
deviceIPPort: '',
scalePrecision: 0,
serviceName: '',
isFeedingScale: false
})
}
const onEdit = (row: UspscaleGetListOutput) => {
state.uspscaleFormTitle = '编辑地秤'
uspscaleFormRef.value.open(row)
}
const onDelete = (row: UspscaleGetListOutput) => {
proxy.$modal
.confirmDelete(`确定要删除设备【${row.deviceNo || row.assetNo || '未知设备'}】?`)
.then(async () => {
await new UspscaleApi().softDelete({ id: row.id }, { loading: true })
Query()
})
.catch(() => {})
}
const onSizeChange = (val: number) => {
state.pageInput.currentPage = 1
state.pageInput.pageSize = val
onQuery()
}
const onCurrentChange = (val: number) => {
state.pageInput.currentPage = val
onQuery()
}
</script>
<style scoped lang="scss"></style>

570
页面开发数据模板.md Normal file
View File

@ -0,0 +1,570 @@
# 页面开发数据模板
> 基于 uspscale (秤台设备管理) 页面开发经验总结的完整开发模板
## 1. 基本信息配置
```typescript
{
// 页面基本信息
pageInfo: {
moduleName: "uspscale", // 模块名称(小写,用于路径)
displayName: "秤台设备管理", // 显示名称
baseRoute: "admin/uspscale", // 路由路径
referenceModule: "admin/room" // 参考的现有模块(用于复制结构)
}
}
```
## 2. API接口定义
```typescript
{
// API配置
apiConfig: {
baseEndpoint: "/api/admin/usp-scale", // API基础路径
endpoints: {
getPage: "get-page", // 分页查询
get: "get", // 单条查询
add: "add", // 新增
update: "update", // 更新
softDelete: "soft-delete" // 软删除(推荐)
// delete: "delete" // 硬删除(可选)
}
}
}
```
## 3. 数据字段定义
```typescript
{
// 字段定义
fields: {
// 主键字段
primaryKey: {
name: "id",
type: "number",
required: true
},
// 基本字段
basicFields: [
{
name: "deviceNo",
type: "string",
label: "设备编号",
required: true,
component: "input",
placeholder: "请输入设备编号"
},
{
name: "assetNo",
type: "string",
label: "资产编号",
required: false,
component: "input",
placeholder: "请输入资产编号"
}
],
// 下拉选择字段
selectFields: [
{
name: "roomID",
type: "number",
label: "房间",
required: true,
component: "select",
dataSource: "room", // 数据来源API
displayField: "roomName",
valueField: "id",
placeholder: "请选择设备所在房间"
},
{
name: "model",
type: "string",
label: "型号",
required: false,
component: "select",
dataSource: "static", // 静态数据
options: [
{ label: "CAIS2-U 1500kg", value: "001" },
{ label: "METTLER", value: "3" },
{ label: "OHAUS", value: "5" }
]
},
{
name: "principalId",
type: "number",
label: "设备负责人",
required: false,
component: "select",
dataSource: "user", // 用户数据源
displayField: "name (userName)", // 显示格式:姓名(用户名)
valueField: "id",
filterable: true, // 支持搜索
placeholder: "请输入姓名搜索设备负责人"
}
],
// 数字字段
numberFields: [
{
name: "scalePrecision",
type: "number",
label: "精度(g)",
required: false,
component: "input-number",
min: 0,
precision: 1,
placeholder: "请输入设备精度"
},
{
name: "warningLowerLimit",
type: "number",
label: "报警下限值(g)",
required: false,
component: "input-number",
min: 0,
precision: 2,
placeholder: "请输入报警下限值"
}
],
// 开关字段
switchFields: [
{
name: "enabled",
type: "boolean",
label: "启用状态",
required: false,
component: "switch",
activeText: "启用",
inactiveText: "禁用"
},
{
name: "maintenanceFlag",
type: "boolean",
label: "维护标记",
required: false,
component: "switch",
activeText: "维护中",
inactiveText: "正常"
},
{
name: "isFeedingScale",
type: "boolean",
label: "是否补料地秤",
required: false,
component: "switch",
activeText: "是",
inactiveText: "否"
}
],
// 搜索筛选字段
filterFields: [
{
name: "keyWord",
type: "string",
label: "关键词",
component: "input",
placeholder: "设备编号、资产编号"
},
{
name: "model",
type: "string",
label: "设备型号",
component: "select",
dataSource: "static",
placeholder: "请选择设备型号"
},
{
name: "roomID",
type: "number",
label: "房间",
component: "select",
dataSource: "room",
placeholder: "请选择房间"
},
{
name: "stDate",
type: "string",
label: "开始时间",
component: "datetime-picker",
format: "YYYY-MM-DD HH:mm:ss",
placeholder: "选择开始时间"
},
{
name: "edDate",
type: "string",
label: "结束时间",
component: "datetime-picker",
format: "YYYY-MM-DD HH:mm:ss",
placeholder: "选择结束时间"
}
]
}
}
```
## 4. 权限定义
```typescript
{
// 权限配置
permissions: [
"api:admin:usp-scale:get", // 查询单条
"api:admin:usp-scale:get-list", // 查询列表
"api:admin:usp-scale:get-page", // 分页查询
"api:admin:usp-scale:add", // 新增
"api:admin:usp-scale:update", // 更新
"api:admin:usp-scale:soft-delete" // 软删除
]
}
```
## 5. 表格列配置
```typescript
{
// 表格列定义
tableColumns: [
{
prop: "deviceNo",
label: "设备编号",
minWidth: 120,
showOverflowTooltip: true
},
{
prop: "assetNo",
label: "资产编号",
minWidth: 120,
showOverflowTooltip: true
},
{
prop: "model",
label: "型号",
minWidth: 120,
showOverflowTooltip: true,
formatter: "getModelName" // 需要转换显示的字段
},
{
prop: "specification",
label: "规格",
minWidth: 120,
showOverflowTooltip: true
},
{
prop: "roomID",
label: "房间",
width: 120,
align: "center",
formatter: "getRoomName" // 需要转换显示的字段
},
{
prop: "maintenanceFlag",
label: "维护标记",
width: 80,
align: "center",
component: "tag", // 使用标签显示
tagConfig: {
trueText: "维护中",
falseText: "正常",
trueType: "warning",
falseType: "success"
}
},
{
prop: "isFeedingScale",
label: "补料地秤",
width: 100,
align: "center",
component: "tag",
tagConfig: {
trueText: "是",
falseText: "否",
trueType: "primary",
falseType: "info"
}
},
{
prop: "enabled",
label: "状态",
width: 80,
align: "center",
component: "tag",
tagConfig: {
trueText: "启用",
falseText: "禁用",
trueType: "success",
falseType: "danger"
}
},
{
prop: "actions",
label: "操作",
width: 200,
fixed: "right",
headerAlign: "center",
align: "center",
actions: [
{
text: "编辑",
type: "primary",
permission: "api:admin:usp-scale:update",
handler: "onEdit"
},
{
text: "删除",
type: "danger",
permission: "api:admin:usp-scale:soft-delete",
handler: "onDelete"
}
]
}
]
}
```
## 6. 表单布局配置
```typescript
{
// 表单配置
formConfig: {
dialogWidth: "900px",
labelWidth: "120px",
// 表单分组
sections: [
{
title: "基本信息",
fields: [
"deviceNo",
"assetNo",
"model",
"specification",
"roomID",
"principalId"
]
},
{
title: "技术参数",
fields: [
"scalePrecision",
"warningLowerLimit"
]
},
{
title: "设备配置",
fields: [
"maintenanceFlag",
"isFeedingScale",
"enabled",
"communicationSettings"
]
}
],
// 特殊组件(如独立表单)
specialComponents: [
{
name: "communicationSettings",
type: "button",
label: "通讯设置",
buttonText: "配置通讯参数",
icon: "ele-Setting",
action: "openSubForm",
subFormConfig: {
title: "通讯设置",
width: "600px",
fields: [
"subSystemIP",
"subSystemIPPort",
"deviceIP",
"deviceIPPort",
"serviceName"
]
}
}
]
}
}
```
## 7. 完整示例配置
### 设备管理页面示例
```json
{
"pageInfo": {
"moduleName": "device",
"displayName": "设备管理",
"baseRoute": "admin/device",
"referenceModule": "admin/room"
},
"apiConfig": {
"baseEndpoint": "/api/admin/device",
"endpoints": {
"getPage": "get-page",
"get": "get",
"add": "add",
"update": "update",
"softDelete": "soft-delete"
}
},
"permissions": [
"api:admin:device:get",
"api:admin:device:get-page",
"api:admin:device:add",
"api:admin:device:update",
"api:admin:device:soft-delete"
],
"fields": {
"basicFields": [
{
"name": "deviceName",
"type": "string",
"label": "设备名称",
"required": true,
"component": "input",
"placeholder": "请输入设备名称"
},
{
"name": "deviceCode",
"type": "string",
"label": "设备编码",
"required": true,
"component": "input",
"placeholder": "请输入设备编码"
}
],
"selectFields": [
{
"name": "categoryId",
"type": "number",
"label": "设备分类",
"required": true,
"component": "select",
"dataSource": "category"
}
]
}
}
```
## 8. 开发步骤清单
### 8.1 准备阶段
- [ ] 确定页面基本信息(模块名、显示名等)
- [ ] 设计数据结构和字段定义
- [ ] 确定API接口规范
- [ ] 确定权限控制规则
- [ ] 分析数据关联关系(如外键引用)
### 8.2 后端准备
- [ ] 创建数据库表结构
- [ ] 实现API接口CRUD操作
- [ ] 配置权限控制
- [ ] 数据验证和业务逻辑
- [ ] API文档编写
### 8.3 前端开发
#### 8.3.1 基础文件创建
- [ ] 创建主页面文件 `src/views/admin/{module}/index.vue`
- [ ] 创建表单组件 `src/views/admin/{module}/components/{module}-form.vue`
- [ ] 创建API文件 `src/api/admin/{Module}Api.ts`
- [ ] 更新类型定义 `src/api/admin/data-contracts.ts`
#### 8.3.2 功能实现
- [ ] 实现分页查询功能
- [ ] 实现新增功能
- [ ] 实现编辑功能
- [ ] 实现删除功能
- [ ] 添加权限控制
#### 8.3.3 用户体验优化
- [ ] 添加搜索筛选功能
- [ ] 优化表单布局(分组、占位符)
- [ ] 添加数据验证
- [ ] 优化选择框(搜索、分页)
- [ ] 添加操作确认提示
### 8.4 测试阶段
- [ ] 功能测试CRUD操作
- [ ] 权限测试
- [ ] 数据验证测试
- [ ] 用户体验测试
- [ ] 性能测试
### 8.5 优化阶段
- [ ] 代码优化和重构
- [ ] 性能优化(懒加载、虚拟滚动等)
- [ ] 错误处理优化
- [ ] 用户反馈收集和改进
## 9. 开发最佳实践
### 9.1 命名规范
- **文件命名**:使用小写字母和连字符,如 `device-form.vue`
- **变量命名**:使用驼峰命名法,如 `deviceList`
- **常量命名**:使用大写字母和下划线,如 `MAX_PAGE_SIZE`
### 9.2 代码组织
- **组件拆分**:将复杂表单拆分为多个子组件
- **逻辑复用**:将通用逻辑提取为组合函数
- **类型安全**:全面使用 TypeScript 类型定义
### 9.3 用户体验
- **响应式设计**:适配不同屏幕尺寸
- **加载状态**:添加适当的加载提示
- **错误处理**:友好的错误提示信息
- **操作反馈**:成功/失败的操作提示
### 9.4 性能优化
- **懒加载**:大型组件使用异步加载
- **虚拟滚动**:大数据量列表使用虚拟滚动
- **防抖节流**:搜索等操作使用防抖
- **缓存策略**:合理使用数据缓存
## 10. 常见问题和解决方案
### 10.1 下拉选择优化
**问题**:数据量大时选择困难
**解决方案**
- 添加搜索功能filterable
- 显示更多信息(姓名+用户名)
- 支持远程搜索
- 添加分页加载
### 10.2 表单字段分组
**问题**:字段太多导致表单复杂
**解决方案**
- 按逻辑分组(基本信息、技术参数、配置)
- 使用独立表单处理复杂模块
- 添加分步表单
- 使用折叠面板
### 10.3 权限控制
**问题**:不同用户需要不同权限
**解决方案**
- 细化权限粒度
- 使用 v-auth 指令控制按钮显示
- 在API层面进行权限验证
- 提供权限管理界面
### 10.4 数据关联
**问题**:外键数据显示和选择
**解决方案**
- 提供格式化函数转换显示
- 在表单中使用下拉选择
- 支持级联选择
- 缓存关联数据
---
**注意**:此模板基于 Vue 3 + TypeScript + Element Plus + Pinia 技术栈,使用时请根据实际项目技术栈进行调整。