Merge branch 'main' of http://git.dibox.tech/parttime/smart_grid_iot
This commit is contained in:
commit
3596462eca
34 files changed
+2703
-93
No files matched your search
File renamed without changes.
File renamed without changes.
@@ -145,8 +145,11 @@
|
||||
<a-form-item label="建筑用途" name="purpose">
|
||||
<a-input v-model:value="formData.purpose" placeholder="请输入建筑用途" maxlength="128" />
|
||||
</a-form-item>
|
||||
<a-form-item label="楼层数" name="floorCount">
|
||||
<a-input-number v-model:value="formData.floorCount" :min="0" style="width: 100%" placeholder="请输入楼层数" />
|
||||
<a-form-item label="地上楼层数" name="floorCount">
|
||||
<a-input-number v-model:value="formData.floorCount" :min="0" style="width: 100%" placeholder="请输入地上楼层数" />
|
||||
</a-form-item>
|
||||
<a-form-item label="地下楼层数" name="floorDownCount">
|
||||
<a-input-number v-model:value="formData.floorDownCount" :min="0" style="width: 100%" placeholder="请输入地下楼层数" />
|
||||
</a-form-item>
|
||||
<a-form-item label="基本信息" name="basicInfo">
|
||||
<a-textarea v-model:value="formData.basicInfo" :rows="3" placeholder="请输入建筑基本信息" maxlength="2000" />
|
||||
@@ -159,6 +162,16 @@
|
||||
<a-form-item label="所属建筑" name="buildingId">
|
||||
<a-select v-model:value="formData.buildingId" :options="buildingOptions" :not-found-content="null" />
|
||||
</a-form-item>
|
||||
<a-form-item label="所属客户部门" name="deptId">
|
||||
<a-select
|
||||
v-model:value="formData.deptId"
|
||||
:options="departmentOptions"
|
||||
:loading="departmentLoading"
|
||||
allow-clear
|
||||
:not-found-content="departmentLoading ? null : (departmentOptions.length ? null : '暂无部门')"
|
||||
placeholder="请选择所属客户部门"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="房间编码" name="roomCode">
|
||||
<a-input v-model:value="formData.roomCode" placeholder="请输入房间编码" maxlength="64" />
|
||||
</a-form-item>
|
||||
@@ -166,7 +179,12 @@
|
||||
<a-input v-model:value="formData.roomName" placeholder="请输入房间名称" maxlength="128" />
|
||||
</a-form-item>
|
||||
<a-form-item label="所在楼层" name="floorNo">
|
||||
<a-input v-model:value="formData.floorNo" placeholder="请输入所在楼层" maxlength="16" />
|
||||
<a-select
|
||||
v-model:value="formData.floorNo"
|
||||
:options="floorOptions"
|
||||
placeholder="请选择所在楼层"
|
||||
:not-found-content="floorOptions.length ? null : '请先选择所属建筑'"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="房间用途" name="roomPurpose">
|
||||
<a-input v-model:value="formData.roomPurpose" placeholder="请输入房间用途" maxlength="128" />
|
||||
@@ -192,6 +210,7 @@ import {
|
||||
import { customerApi } from '@/api/customer';
|
||||
import { customerBuildingApi, type CustomerBuildingInfo } from '@/api/customerBuilding';
|
||||
import { customerRoomApi, type CustomerRoomInfo } from '@/api/customerRoom';
|
||||
import { customerDepartmentApi } from '@/api/customerDepartment';
|
||||
|
||||
type NodeType = 'customer' | 'building' | 'room';
|
||||
|
||||
@@ -226,11 +245,13 @@ const formData = reactive({
|
||||
buildingName: '',
|
||||
purpose: '',
|
||||
floorCount: undefined as number | undefined,
|
||||
floorDownCount: undefined as number | undefined,
|
||||
basicInfo: '',
|
||||
roomCode: '',
|
||||
roomName: '',
|
||||
floorNo: '',
|
||||
roomPurpose: '',
|
||||
deptId: undefined as number | undefined,
|
||||
});
|
||||
|
||||
const buildingCount = computed(() => {
|
||||
@@ -248,6 +269,24 @@ const buildingOptions = computed<{ label: string; value: number }[]>(() => {
|
||||
return root.children.map((node) => ({ label: node.title, value: node.data.id as number }));
|
||||
});
|
||||
|
||||
const departmentOptions = ref<{ label: string; value: number }[]>([]);
|
||||
const departmentLoading = ref(false);
|
||||
|
||||
const floorOptions = computed<{ label: string; value: string }[]>(() => {
|
||||
const root = treeData.value[0];
|
||||
const building = root?.children?.find((n) => n.type === 'building' && n.data.id === formData.buildingId);
|
||||
const up = building?.data?.floorCount ?? 0;
|
||||
const down = building?.data?.floorDownCount ?? 0;
|
||||
const opts: { label: string; value: string }[] = [];
|
||||
for (let i = down; i >= 1; i--) {
|
||||
opts.push({ label: `负${i}层`, value: String(-i) });
|
||||
}
|
||||
for (let i = 1; i <= up; i++) {
|
||||
opts.push({ label: `${i}层`, value: String(i) });
|
||||
}
|
||||
return opts;
|
||||
});
|
||||
|
||||
const formRules = computed(() => {
|
||||
if (modalType.value === 'building') {
|
||||
return {
|
||||
@@ -278,7 +317,8 @@ function descFor(data: TreeNode): string {
|
||||
if (data.type === 'building') {
|
||||
const b: CustomerBuildingInfo = data.data;
|
||||
if (b.purpose) parts.push(b.purpose);
|
||||
if (b.floorCount != null) parts.push(`共${b.floorCount}层`);
|
||||
if (b.floorCount != null) parts.push(`地上${b.floorCount}层`);
|
||||
if (b.floorDownCount != null && b.floorDownCount > 0) parts.push(`地下${b.floorDownCount}层`);
|
||||
if (b.basicInfo) parts.push(b.basicInfo);
|
||||
} else if (data.type === 'room') {
|
||||
const r: CustomerRoomInfo = data.data;
|
||||
@@ -358,6 +398,19 @@ async function loadTree() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDepartments(customerId: number) {
|
||||
departmentLoading.value = true;
|
||||
try {
|
||||
const res: any = await customerDepartmentApi.getDepartmentList({ customerId, page: 1, pageSize: 999 });
|
||||
departmentOptions.value =
|
||||
res.code === 200 ? res.data.list.map((d: any) => ({ label: d.deptName, value: d.id })) : [];
|
||||
} catch {
|
||||
departmentOptions.value = [];
|
||||
} finally {
|
||||
departmentLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleAddBuilding() {
|
||||
modalType.value = 'building';
|
||||
isEdit.value = false;
|
||||
@@ -379,6 +432,7 @@ function handleEditBuilding(node: TreeNode) {
|
||||
formData.buildingName = b.buildingName;
|
||||
formData.purpose = b.purpose || '';
|
||||
formData.floorCount = b.floorCount;
|
||||
formData.floorDownCount = b.floorDownCount;
|
||||
formData.basicInfo = b.basicInfo || '';
|
||||
modalVisible.value = true;
|
||||
}
|
||||
@@ -391,6 +445,9 @@ function handleAddRoom(node: TreeNode) {
|
||||
resetForm();
|
||||
formData.customerId = selectedCustomerId.value;
|
||||
formData.buildingId = node.data.id;
|
||||
if (selectedCustomerId.value !== undefined) {
|
||||
loadDepartments(selectedCustomerId.value);
|
||||
}
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
@@ -407,6 +464,10 @@ function handleEditRoom(node: TreeNode) {
|
||||
formData.roomName = r.roomName;
|
||||
formData.floorNo = r.floorNo || '';
|
||||
formData.roomPurpose = r.roomPurpose || '';
|
||||
formData.deptId = r.deptId;
|
||||
if (r.customerId !== undefined) {
|
||||
loadDepartments(r.customerId);
|
||||
}
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
@@ -416,11 +477,13 @@ function resetForm() {
|
||||
formData.buildingName = '';
|
||||
formData.purpose = '';
|
||||
formData.floorCount = undefined;
|
||||
formData.floorDownCount = undefined;
|
||||
formData.basicInfo = '';
|
||||
formData.roomCode = '';
|
||||
formData.roomName = '';
|
||||
formData.floorNo = '';
|
||||
formData.roomPurpose = '';
|
||||
formData.deptId = undefined;
|
||||
if (formRef.value) {
|
||||
formRef.value.clearValidate();
|
||||
}
|
||||
@@ -438,12 +501,13 @@ async function handleSubmit() {
|
||||
let res: any;
|
||||
if (modalType.value === 'building') {
|
||||
const payload = {
|
||||
customerId: formData.customerId,
|
||||
buildingName: formData.buildingName,
|
||||
purpose: formData.purpose,
|
||||
floorCount: formData.floorCount,
|
||||
basicInfo: formData.basicInfo,
|
||||
};
|
||||
customerId: formData.customerId,
|
||||
buildingName: formData.buildingName,
|
||||
purpose: formData.purpose,
|
||||
floorCount: formData.floorCount,
|
||||
floorDownCount: formData.floorDownCount,
|
||||
basicInfo: formData.basicInfo,
|
||||
};
|
||||
if (isEdit.value && editId.value != null) {
|
||||
res = await customerBuildingApi.update(editId.value, payload);
|
||||
} else {
|
||||
@@ -457,6 +521,7 @@ async function handleSubmit() {
|
||||
roomName: formData.roomName,
|
||||
floorNo: formData.floorNo,
|
||||
roomPurpose: formData.roomPurpose,
|
||||
deptId: formData.deptId,
|
||||
};
|
||||
if (isEdit.value && editId.value != null) {
|
||||
res = await customerRoomApi.update(editId.value, payload);
|
||||
|
||||
@@ -798,7 +798,7 @@ function handleFollow(record: CustomerVisitInfo) {
|
||||
// 一键发起勘察派单:跳转勘察派单页并预填客户(不创建、不回写状态)
|
||||
function handleSurvey(record: CustomerVisitInfo) {
|
||||
if (!record.customerId) return;
|
||||
router.push({ path: '/project/survey', query: { customerId: String(record.customerId) } });
|
||||
router.push({ path: '/dispatch/survey', query: { customerId: String(record.customerId) } });
|
||||
}
|
||||
|
||||
// 查看关联工程
|
||||
|
||||
@@ -0,0 +1,921 @@
|
||||
<template>
|
||||
<div class="device-equipment">
|
||||
<!-- 顶部标题 -->
|
||||
<div class="page-header">
|
||||
<div class="breadcrumb">
|
||||
<span>运营管控体系</span>
|
||||
<span class="sep">/</span>
|
||||
<span>设备监控</span>
|
||||
<span class="sep">/</span>
|
||||
<span class="cur">设备管理台账</span>
|
||||
</div>
|
||||
<div class="header-main">
|
||||
<div class="header-left">
|
||||
<div class="header-row">
|
||||
<span class="title-bar"></span>
|
||||
<span class="main-title">设备管理台账</span>
|
||||
<span class="sub-title">EQUIPMENT LEDGER</span>
|
||||
</div>
|
||||
<p class="desc">统一维护客户侧智电改造设备(配电间/井/箱)档案,支持按客户、建筑、类型快速检索。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 主体卡片 -->
|
||||
<div class="content-card">
|
||||
<!-- 筛选区 -->
|
||||
<div class="filter-card">
|
||||
<div class="filter-grid">
|
||||
<div class="filter-item">
|
||||
<label class="filter-label">所属客户</label>
|
||||
<a-select
|
||||
v-model:value="searchForm.customerId"
|
||||
placeholder="全部客户"
|
||||
allow-clear
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
:options="customerOptions"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<label class="filter-label">所属建筑</label>
|
||||
<a-input
|
||||
v-model:value="searchForm.building"
|
||||
placeholder="建筑名称"
|
||||
allow-clear
|
||||
@pressEnter="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<label class="filter-label">设备类型</label>
|
||||
<a-select v-model:value="searchForm.equipType" placeholder="全部类型" allow-clear>
|
||||
<a-select-option v-for="t in equipTypeOptions" :key="t" :value="t">{{ t }}</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<label class="filter-label">关键词检索</label>
|
||||
<a-input
|
||||
v-model:value="searchForm.keyword"
|
||||
placeholder="设备编码 / 名称 / 位置"
|
||||
allow-clear
|
||||
@pressEnter="handleSearch"
|
||||
>
|
||||
<template #prefix><SearchOutlined /></template>
|
||||
</a-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-bottom">
|
||||
<div class="filter-actions">
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
查询
|
||||
</a-button>
|
||||
<a-button @click="handleReset">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
重置
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 列表区 -->
|
||||
<div class="list-section">
|
||||
<div class="list-header">
|
||||
<div class="list-title">
|
||||
<span class="dot"></span>
|
||||
<span class="t">设备台账清单</span>
|
||||
<span class="count">{{ total }} 台</span>
|
||||
</div>
|
||||
<div class="list-actions">
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新增设备
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrapper" ref="wrapperRef">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="dataSource"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
row-key="id"
|
||||
:scroll="{ x: 'max-content', y: tableHeight }"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'equipmentNo'">
|
||||
<span class="mono">{{ record.equipmentNo || '-' }}</span>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'equipmentName'">
|
||||
<span class="equip-name">{{ record.equipmentName }}</span>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'equipType'">
|
||||
<span v-if="record.equipType" class="type-chip">{{ record.equipType }}</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'customerId'">
|
||||
{{ customerNameMap[record.customerId] || '-' }}
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'photo'">
|
||||
<a-image
|
||||
v-if="record.photo"
|
||||
:src="record.photo"
|
||||
:width="44"
|
||||
:height="44"
|
||||
style="border-radius: 6px"
|
||||
:preview="{ mask: false }"
|
||||
/>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">
|
||||
<template #icon><EditOutlined /></template>
|
||||
编辑
|
||||
</a-button>
|
||||
<a-popconfirm
|
||||
title="确定要删除该设备吗?"
|
||||
ok-text="确定"
|
||||
cancel-text="取消"
|
||||
ok-button-props="{ danger: true }"
|
||||
@confirm="handleDelete(record.id)"
|
||||
>
|
||||
<a-button type="link" size="small" danger>
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template #emptyText>
|
||||
<a-empty description="暂无设备数据" />
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
:confirm-loading="modalLoading"
|
||||
width="760px"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<!-- 设备编号信息条(编辑态) -->
|
||||
<div v-if="isEdit && editId" class="no-bar">
|
||||
<div class="no-item">
|
||||
<span class="no-label">设备编码</span>
|
||||
<span class="no-value mono">{{ editNo || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 17 }"
|
||||
>
|
||||
<!-- SECTION 01 设备主体信息 -->
|
||||
<div class="form-section">
|
||||
<div class="section-head">
|
||||
<span class="section-no">01</span>
|
||||
<div class="section-text">
|
||||
<span class="section-name">设备主体信息</span>
|
||||
<span class="section-sub">Equipment Identity</span>
|
||||
</div>
|
||||
</div>
|
||||
<a-form-item label="设备名称" name="equipmentName">
|
||||
<a-input v-model:value="formData.equipmentName" placeholder="请输入设备名称" maxlength="128" />
|
||||
</a-form-item>
|
||||
<a-form-item label="设备类型" name="equipType">
|
||||
<a-select
|
||||
v-model:value="formData.equipType"
|
||||
placeholder="请选择设备类型"
|
||||
allow-clear
|
||||
style="width: 100%"
|
||||
>
|
||||
<a-select-option v-for="t in equipTypeOptions" :key="t" :value="t">{{ t }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="所属客户" name="customerId">
|
||||
<a-select
|
||||
v-model:value="formData.customerId"
|
||||
placeholder="请选择所属客户"
|
||||
:options="customerOptions"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
/>
|
||||
</a-form-item>
|
||||
</div>
|
||||
|
||||
<!-- SECTION 02 位置信息 -->
|
||||
<div class="form-section">
|
||||
<div class="section-head">
|
||||
<span class="section-no">02</span>
|
||||
<div class="section-text">
|
||||
<span class="section-name">位置信息</span>
|
||||
<span class="section-sub">Location</span>
|
||||
</div>
|
||||
</div>
|
||||
<a-form-item label="所属建筑" name="building">
|
||||
<a-input v-model:value="formData.building" placeholder="请输入所属建筑" maxlength="128" />
|
||||
</a-form-item>
|
||||
<a-form-item label="所在楼层" name="floor">
|
||||
<a-input v-model:value="formData.floor" placeholder="请输入所在楼层" maxlength="64" />
|
||||
</a-form-item>
|
||||
<a-form-item label="具体位置" name="location">
|
||||
<a-input v-model:value="formData.location" placeholder="请输入具体位置" maxlength="128" />
|
||||
</a-form-item>
|
||||
</div>
|
||||
|
||||
<!-- SECTION 03 照片与备注 -->
|
||||
<div class="form-section">
|
||||
<div class="section-head">
|
||||
<span class="section-no">03</span>
|
||||
<div class="section-text">
|
||||
<span class="section-name">照片与备注</span>
|
||||
<span class="section-sub">Photo & Remark</span>
|
||||
</div>
|
||||
</div>
|
||||
<a-form-item label="设备照片" name="photo">
|
||||
<div class="photo-field">
|
||||
<div class="photo-preview">
|
||||
<img v-if="photoPreview" :src="photoPreview" class="photo-thumb" alt="设备照片" />
|
||||
<span v-else class="photo-empty">未上传</span>
|
||||
</div>
|
||||
<a-upload
|
||||
:show-upload-list="false"
|
||||
:custom-request="handlePhotoUpload"
|
||||
:before-upload="beforePhotoUpload"
|
||||
accept="image/*"
|
||||
:disabled="photoUploading"
|
||||
>
|
||||
<a-button :loading="photoUploading">
|
||||
{{ formData.photo ? '替换' : '上传' }}
|
||||
</a-button>
|
||||
</a-upload>
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item label="备注" name="remark">
|
||||
<a-textarea v-model:value="formData.remark" placeholder="请输入备注" :rows="3" maxlength="256" />
|
||||
</a-form-item>
|
||||
</div>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import {
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
PlusOutlined,
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import {
|
||||
customerEquipmentApi,
|
||||
type CustomerEquipment,
|
||||
type CustomerEquipmentQuery,
|
||||
} from '@/api/device';
|
||||
import { customerApi, type CustomerInfo } from '@/api/customer';
|
||||
import { uploadApi } from '@/api/upload';
|
||||
import { useTableScroll } from '@/composables/useTableScroll';
|
||||
|
||||
const { wrapperRef, tableHeight } = useTableScroll();
|
||||
|
||||
// 设备类型字典(配电间/配电井/配电箱)
|
||||
const equipTypeOptions = ['配电间', '配电井', '配电箱'];
|
||||
|
||||
const loading = ref(false);
|
||||
const dataSource = ref<CustomerEquipment[]>([]);
|
||||
const total = ref(0);
|
||||
const editNo = ref<string>('');
|
||||
|
||||
const searchForm = reactive<CustomerEquipmentQuery>({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
keyword: '',
|
||||
customerId: undefined as any,
|
||||
building: '',
|
||||
equipType: undefined as any,
|
||||
});
|
||||
|
||||
const modalVisible = ref(false);
|
||||
const modalTitle = ref('');
|
||||
const modalLoading = ref(false);
|
||||
const formRef = ref();
|
||||
const isEdit = ref(false);
|
||||
const editId = ref<number | null>(null);
|
||||
|
||||
const customerOptions = ref<{ label: string; value: number }[]>([]);
|
||||
const customerNameMap = reactive<Record<number, string>>({});
|
||||
|
||||
const photoUploading = ref(false);
|
||||
const photoPreview = ref<string>('');
|
||||
|
||||
const formData = reactive<Partial<CustomerEquipment>>({
|
||||
equipmentName: '',
|
||||
equipType: undefined,
|
||||
customerId: undefined,
|
||||
building: '',
|
||||
floor: '',
|
||||
location: '',
|
||||
photo: '',
|
||||
remark: '',
|
||||
});
|
||||
|
||||
const formRules = {
|
||||
equipmentName: [{ required: true, message: '请输入设备名称', trigger: 'blur' }],
|
||||
equipType: [{ required: true, message: '请选择设备类型', trigger: 'change' }],
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: '设备编码', dataIndex: 'equipmentNo', key: 'equipmentNo', ellipsis: true, width: 150 },
|
||||
{ title: '设备名称', dataIndex: 'equipmentName', key: 'equipmentName', ellipsis: true },
|
||||
{ title: '设备类型', dataIndex: 'equipType', key: 'equipType', ellipsis: true, width: 100 },
|
||||
{ title: '所属客户', dataIndex: 'customerId', key: 'customerId', ellipsis: true, width: 180 },
|
||||
{ title: '所属建筑', dataIndex: 'building', key: 'building', ellipsis: true, width: 140 },
|
||||
{ title: '所在楼层', dataIndex: 'floor', key: 'floor', ellipsis: true, width: 100 },
|
||||
{ title: '具体位置', dataIndex: 'location', key: 'location', ellipsis: true },
|
||||
{ title: '照片', dataIndex: 'photo', key: 'photo', align: 'center' as const, width: 70 },
|
||||
{ title: '操作', key: 'action', width: 140, fixed: 'right' as const },
|
||||
];
|
||||
|
||||
const pagination = computed(() => ({
|
||||
current: searchForm.page,
|
||||
pageSize: searchForm.pageSize,
|
||||
total: total.value,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (t: number) => `共 ${t} 条数据`,
|
||||
}));
|
||||
|
||||
onMounted(() => {
|
||||
fetchList();
|
||||
fetchCustomerOptions();
|
||||
});
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res: any = await customerEquipmentApi.getList(searchForm);
|
||||
if (res.code === 200) {
|
||||
dataSource.value = res.data.list;
|
||||
total.value = res.data.total;
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('获取设备列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCustomerOptions() {
|
||||
try {
|
||||
const res: any = await customerApi.getCustomerOptions();
|
||||
if (res.code === 200) {
|
||||
const list: CustomerInfo[] = res.data;
|
||||
customerOptions.value = list.map((item) => ({
|
||||
label: `${item.customerNo} ${item.customerName}`,
|
||||
value: item.id,
|
||||
}));
|
||||
list.forEach((item) => {
|
||||
customerNameMap[item.id] = `${item.customerNo} ${item.customerName}`;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取客户选项失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = '';
|
||||
searchForm.customerId = undefined as any;
|
||||
searchForm.building = '';
|
||||
searchForm.equipType = undefined as any;
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleTableChange(pag: any) {
|
||||
searchForm.page = pag.current;
|
||||
searchForm.pageSize = pag.pageSize;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
// ==================== 照片上传 ====================
|
||||
function beforePhotoUpload(file: File) {
|
||||
const isImage = file.type.startsWith('image/');
|
||||
if (!isImage) {
|
||||
message.error('请上传图片文件');
|
||||
return false;
|
||||
}
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
message.error('图片大小不能超过 10MB');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function handlePhotoUpload(options: any) {
|
||||
const { file, onSuccess, onError } = options;
|
||||
const f = file as File;
|
||||
if (!beforePhotoUpload(f)) {
|
||||
onError?.(new Error('文件校验失败'));
|
||||
return;
|
||||
}
|
||||
photoUploading.value = true;
|
||||
try {
|
||||
const res: any = await uploadApi.uploadImage(f);
|
||||
if (res.code === 200 && res.data) {
|
||||
formData.photo = res.data.key;
|
||||
photoPreview.value = res.data.url;
|
||||
onSuccess?.(res.data);
|
||||
message.success('上传成功');
|
||||
} else {
|
||||
throw new Error(res.message || '上传失败');
|
||||
}
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '图片上传失败');
|
||||
onError?.(e);
|
||||
} finally {
|
||||
photoUploading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 新增/编辑 ====================
|
||||
function handleAdd() {
|
||||
isEdit.value = false;
|
||||
editId.value = null;
|
||||
editNo.value = '';
|
||||
modalTitle.value = '新增设备';
|
||||
resetForm();
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function handleEdit(record: CustomerEquipment) {
|
||||
isEdit.value = true;
|
||||
editId.value = record.id;
|
||||
editNo.value = record.equipmentNo;
|
||||
modalTitle.value = '编辑设备';
|
||||
resetForm();
|
||||
|
||||
formData.equipmentName = record.equipmentName;
|
||||
formData.equipType = record.equipType || undefined;
|
||||
formData.customerId = record.customerId;
|
||||
formData.building = record.building || '';
|
||||
formData.floor = record.floor || '';
|
||||
formData.location = record.location || '';
|
||||
formData.photo = record.photo || '';
|
||||
formData.remark = record.remark || '';
|
||||
photoPreview.value = record.photo || '';
|
||||
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.equipmentName = '';
|
||||
formData.equipType = undefined;
|
||||
formData.customerId = undefined;
|
||||
formData.building = '';
|
||||
formData.floor = '';
|
||||
formData.location = '';
|
||||
formData.photo = '';
|
||||
formData.remark = '';
|
||||
photoPreview.value = '';
|
||||
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
modalLoading.value = true;
|
||||
try {
|
||||
const submitData: any = {
|
||||
equipmentName: formData.equipmentName,
|
||||
equipType: formData.equipType,
|
||||
customerId: formData.customerId,
|
||||
building: formData.building,
|
||||
floor: formData.floor,
|
||||
location: formData.location,
|
||||
photo: formData.photo,
|
||||
remark: formData.remark,
|
||||
};
|
||||
|
||||
let res: any;
|
||||
if (isEdit.value && editId.value) {
|
||||
res = await customerEquipmentApi.update(editId.value, submitData);
|
||||
} else {
|
||||
res = await customerEquipmentApi.create(submitData);
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
message.success(isEdit.value ? '更新成功' : '创建成功');
|
||||
modalVisible.value = false;
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '操作失败');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('操作失败');
|
||||
} finally {
|
||||
modalLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
modalVisible.value = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
try {
|
||||
const res: any = await customerEquipmentApi.delete(id);
|
||||
if (res.code === 200) {
|
||||
message.success('删除成功');
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '删除失败');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('删除失败');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.device-equipment {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(180deg, #eef3f8 0%, #e8eefb 100%);
|
||||
padding: 20px 24px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: #5b6b7e;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.breadcrumb .sep {
|
||||
color: #c2c6d8;
|
||||
}
|
||||
|
||||
.breadcrumb .cur {
|
||||
color: #0054cd;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.header-main {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.title-bar {
|
||||
width: 5px;
|
||||
height: 22px;
|
||||
background: #0054cd;
|
||||
border-radius: 3px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #0d1c2c;
|
||||
line-height: 1.4;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #0054cd;
|
||||
letter-spacing: 2px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin: 6px 0 0;
|
||||
font-size: 13px;
|
||||
color: #5b6b7e;
|
||||
}
|
||||
|
||||
.content-card {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
background: #fff;
|
||||
border: 1px solid #dbe4ee;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.07);
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.filter-card {
|
||||
flex-shrink: 0;
|
||||
background: #f8faff;
|
||||
border: 1px solid #dbe4ee;
|
||||
border-radius: 10px;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.filter-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 12px 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.filter-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.filter-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #5b6b7e;
|
||||
}
|
||||
|
||||
.filter-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 14px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #dbe4ee;
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.list-section {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 2px 4px 12px;
|
||||
}
|
||||
|
||||
.list-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.list-title .dot {
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background: #0054cd;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.list-title .t {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #0d1c2c;
|
||||
}
|
||||
|
||||
.list-title .count {
|
||||
padding: 1px 8px;
|
||||
border-radius: 6px;
|
||||
background: #eef4ff;
|
||||
color: #0054cd;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
font-family: Consolas, Menlo, monospace;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: Consolas, Menlo, monospace;
|
||||
font-size: 13px;
|
||||
color: #0d1c2c;
|
||||
}
|
||||
|
||||
.equip-name {
|
||||
font-weight: 600;
|
||||
color: #0d1c2c;
|
||||
}
|
||||
|
||||
.type-chip {
|
||||
display: inline-block;
|
||||
padding: 1px 8px;
|
||||
border-radius: 6px;
|
||||
background: #eef4ff;
|
||||
color: #0054cd;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 弹窗 */
|
||||
.no-bar {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 12px;
|
||||
border-radius: 8px;
|
||||
background: #f4f7ff;
|
||||
border: 1px solid #dbe4ee;
|
||||
}
|
||||
|
||||
.no-item {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.no-label {
|
||||
font-size: 12px;
|
||||
color: #5b6b7e;
|
||||
}
|
||||
|
||||
.no-value {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #0054cd;
|
||||
}
|
||||
|
||||
.photo-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.photo-preview {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #dbe4ee;
|
||||
background: #f8faff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.photo-thumb {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.photo-empty {
|
||||
font-size: 12px;
|
||||
color: #a4b0bf;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin: 4px 0 10px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #eef1f6;
|
||||
}
|
||||
|
||||
.section-no {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 7px;
|
||||
background: #0054cd;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: Consolas, Menlo, monospace;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.section-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.section-name {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #0d1c2c;
|
||||
}
|
||||
|
||||
.section-sub {
|
||||
font-size: 11px;
|
||||
color: #8a97a8;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
:deep(.ant-table) {
|
||||
font-size: 14px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.ant-table-thead > tr > th) {
|
||||
background: #f4f7ff;
|
||||
color: #5b6b7e;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:deep(.ant-btn-link) {
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
:deep(.ant-btn-primary) {
|
||||
background: #0054cd;
|
||||
border-color: #0054cd;
|
||||
}
|
||||
|
||||
:deep(.ant-btn-primary:not(:disabled):hover),
|
||||
:deep(.ant-btn-primary:not(:disabled):focus) {
|
||||
background: #136bfb;
|
||||
border-color: #136bfb;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,699 @@
|
||||
<template>
|
||||
<div class="smart-device">
|
||||
<!-- 顶部标题 -->
|
||||
<div class="page-header">
|
||||
<div class="breadcrumb">
|
||||
<span>运营管控体系</span>
|
||||
<span class="sep">/</span>
|
||||
<span>基础数据</span>
|
||||
<span class="sep">/</span>
|
||||
<span class="cur">智电设备型号</span>
|
||||
</div>
|
||||
<div class="header-main">
|
||||
<div class="header-left">
|
||||
<div class="header-row">
|
||||
<span class="title-bar"></span>
|
||||
<span class="main-title">智电设备型号</span>
|
||||
<span class="sub-title">SMART DEVICE MODEL</span>
|
||||
</div>
|
||||
<p class="desc">维护鑫潼智电设备产品目录(型号/规格/类型/供应商),作为基础数据供业务模块引用。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 主体卡片 -->
|
||||
<div class="content-card">
|
||||
<!-- 筛选区 -->
|
||||
<div class="filter-card">
|
||||
<div class="filter-grid">
|
||||
<div class="filter-item">
|
||||
<label class="filter-label">设备类型</label>
|
||||
<a-select v-model:value="searchForm.deviceType" placeholder="全部类型" allow-clear>
|
||||
<a-select-option v-for="t in deviceTypeOptions" :key="t" :value="t">{{ t }}</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<label class="filter-label">关键词检索</label>
|
||||
<a-input
|
||||
v-model:value="searchForm.keyword"
|
||||
placeholder="型号 / 名称 / 规格 / 供应商"
|
||||
allow-clear
|
||||
@pressEnter="handleSearch"
|
||||
>
|
||||
<template #prefix><SearchOutlined /></template>
|
||||
</a-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-bottom">
|
||||
<div class="filter-actions">
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
查询
|
||||
</a-button>
|
||||
<a-button @click="handleReset">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
重置
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 列表区 -->
|
||||
<div class="list-section">
|
||||
<div class="list-header">
|
||||
<div class="list-title">
|
||||
<span class="dot"></span>
|
||||
<span class="t">设备型号目录</span>
|
||||
<span class="count">{{ total }} 条</span>
|
||||
</div>
|
||||
<div class="list-actions">
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新增型号
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrapper" ref="wrapperRef">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="dataSource"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
row-key="id"
|
||||
:scroll="{ x: 'max-content', y: tableHeight }"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'modelNo'">
|
||||
<span class="mono">{{ record.modelNo || '-' }}</span>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'modelName'">
|
||||
<span class="model-name">{{ record.modelName }}</span>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'deviceType'">
|
||||
<span v-if="record.deviceType" class="type-chip">{{ record.deviceType }}</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">
|
||||
<template #icon><EditOutlined /></template>
|
||||
编辑
|
||||
</a-button>
|
||||
<a-popconfirm
|
||||
title="确定要删除该设备型号吗?"
|
||||
ok-text="确定"
|
||||
cancel-text="取消"
|
||||
ok-button-props="{ danger: true }"
|
||||
@confirm="handleDelete(record.id)"
|
||||
>
|
||||
<a-button type="link" size="small" danger>
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template #emptyText>
|
||||
<a-empty description="暂无型号数据" />
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
:confirm-loading="modalLoading"
|
||||
width="720px"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 16 }"
|
||||
>
|
||||
<div class="form-section">
|
||||
<div class="section-head">
|
||||
<span class="section-no">01</span>
|
||||
<div class="section-text">
|
||||
<span class="section-name">基本信息</span>
|
||||
<span class="section-sub">Basic Information</span>
|
||||
</div>
|
||||
</div>
|
||||
<a-form-item label="型号" name="modelNo">
|
||||
<a-input v-model:value="formData.modelNo" placeholder="请输入设备型号" maxlength="64" />
|
||||
</a-form-item>
|
||||
<a-form-item label="名称" name="modelName">
|
||||
<a-input v-model:value="formData.modelName" placeholder="请输入设备名称" maxlength="128" />
|
||||
</a-form-item>
|
||||
<a-form-item label="类型" name="deviceType">
|
||||
<a-select
|
||||
v-model:value="formData.deviceType"
|
||||
placeholder="请选择设备类型"
|
||||
allow-clear
|
||||
style="width: 100%"
|
||||
>
|
||||
<a-select-option v-for="t in deviceTypeOptions" :key="t" :value="t">{{ t }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="规格" name="spec">
|
||||
<a-input v-model:value="formData.spec" placeholder="请输入规格参数" maxlength="128" />
|
||||
</a-form-item>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<div class="section-head">
|
||||
<span class="section-no">02</span>
|
||||
<div class="section-text">
|
||||
<span class="section-name">功能与供应商</span>
|
||||
<span class="section-sub">Function & Supplier</span>
|
||||
</div>
|
||||
</div>
|
||||
<a-form-item label="功能说明" name="functionDesc">
|
||||
<a-textarea v-model:value="formData.functionDesc" placeholder="请输入功能说明" :rows="3" maxlength="256" />
|
||||
</a-form-item>
|
||||
<a-form-item label="供应商" name="supplier">
|
||||
<a-input v-model:value="formData.supplier" placeholder="请输入供应商" maxlength="64" />
|
||||
</a-form-item>
|
||||
<a-form-item label="备注" name="remark">
|
||||
<a-textarea v-model:value="formData.remark" placeholder="请输入备注" :rows="2" maxlength="256" />
|
||||
</a-form-item>
|
||||
</div>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import {
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
PlusOutlined,
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import {
|
||||
smartDeviceApi,
|
||||
type SmartDevice,
|
||||
type SmartDeviceQuery,
|
||||
} from '@/api/device';
|
||||
import { useTableScroll } from '@/composables/useTableScroll';
|
||||
|
||||
const { wrapperRef, tableHeight } = useTableScroll();
|
||||
|
||||
// 设备类型字典(网关/探测器/采集器)
|
||||
const deviceTypeOptions = ['网关', '探测器', '采集器'];
|
||||
|
||||
const loading = ref(false);
|
||||
const dataSource = ref<SmartDevice[]>([]);
|
||||
const total = ref(0);
|
||||
|
||||
const searchForm = reactive<SmartDeviceQuery>({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
keyword: '',
|
||||
deviceType: undefined as any,
|
||||
});
|
||||
|
||||
const modalVisible = ref(false);
|
||||
const modalTitle = ref('');
|
||||
const modalLoading = ref(false);
|
||||
const formRef = ref();
|
||||
const isEdit = ref(false);
|
||||
const editId = ref<number | null>(null);
|
||||
|
||||
const formData = reactive<Partial<SmartDevice>>({
|
||||
modelNo: '',
|
||||
modelName: '',
|
||||
spec: '',
|
||||
deviceType: undefined,
|
||||
functionDesc: '',
|
||||
supplier: '',
|
||||
remark: '',
|
||||
});
|
||||
|
||||
const formRules = {
|
||||
modelNo: [{ required: true, message: '请输入设备型号', trigger: 'blur' }],
|
||||
modelName: [{ required: true, message: '请输入设备名称', trigger: 'blur' }],
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: '型号', dataIndex: 'modelNo', key: 'modelNo', ellipsis: true, width: 150 },
|
||||
{ title: '名称', dataIndex: 'modelName', key: 'modelName', ellipsis: true },
|
||||
{ title: '类型', dataIndex: 'deviceType', key: 'deviceType', ellipsis: true, width: 110 },
|
||||
{ title: '规格', dataIndex: 'spec', key: 'spec', ellipsis: true },
|
||||
{ title: '功能说明', dataIndex: 'functionDesc', key: 'functionDesc', ellipsis: true },
|
||||
{ title: '供应商', dataIndex: 'supplier', key: 'supplier', ellipsis: true, width: 140 },
|
||||
{ title: '操作', key: 'action', width: 140, fixed: 'right' as const },
|
||||
];
|
||||
|
||||
const pagination = computed(() => ({
|
||||
current: searchForm.page,
|
||||
pageSize: searchForm.pageSize,
|
||||
total: total.value,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (t: number) => `共 ${t} 条数据`,
|
||||
}));
|
||||
|
||||
onMounted(() => {
|
||||
fetchList();
|
||||
});
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res: any = await smartDeviceApi.getList(searchForm);
|
||||
if (res.code === 200) {
|
||||
dataSource.value = res.data.list;
|
||||
total.value = res.data.total;
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('获取型号列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = '';
|
||||
searchForm.deviceType = undefined as any;
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleTableChange(pag: any) {
|
||||
searchForm.page = pag.current;
|
||||
searchForm.pageSize = pag.pageSize;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
// ==================== 新增/编辑 ====================
|
||||
function handleAdd() {
|
||||
isEdit.value = false;
|
||||
editId.value = null;
|
||||
modalTitle.value = '新增型号';
|
||||
resetForm();
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function handleEdit(record: SmartDevice) {
|
||||
isEdit.value = true;
|
||||
editId.value = record.id;
|
||||
modalTitle.value = '编辑型号';
|
||||
resetForm();
|
||||
|
||||
formData.modelNo = record.modelNo;
|
||||
formData.modelName = record.modelName;
|
||||
formData.spec = record.spec || '';
|
||||
formData.deviceType = record.deviceType || undefined;
|
||||
formData.functionDesc = record.functionDesc || '';
|
||||
formData.supplier = record.supplier || '';
|
||||
formData.remark = record.remark || '';
|
||||
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.modelNo = '';
|
||||
formData.modelName = '';
|
||||
formData.spec = '';
|
||||
formData.deviceType = undefined;
|
||||
formData.functionDesc = '';
|
||||
formData.supplier = '';
|
||||
formData.remark = '';
|
||||
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
modalLoading.value = true;
|
||||
try {
|
||||
const submitData: any = {
|
||||
modelNo: formData.modelNo,
|
||||
modelName: formData.modelName,
|
||||
spec: formData.spec,
|
||||
deviceType: formData.deviceType,
|
||||
functionDesc: formData.functionDesc,
|
||||
supplier: formData.supplier,
|
||||
remark: formData.remark,
|
||||
};
|
||||
|
||||
let res: any;
|
||||
if (isEdit.value && editId.value) {
|
||||
res = await smartDeviceApi.update(editId.value, submitData);
|
||||
} else {
|
||||
res = await smartDeviceApi.create(submitData);
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
message.success(isEdit.value ? '更新成功' : '创建成功');
|
||||
modalVisible.value = false;
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '操作失败');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('操作失败');
|
||||
} finally {
|
||||
modalLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
modalVisible.value = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
try {
|
||||
const res: any = await smartDeviceApi.delete(id);
|
||||
if (res.code === 200) {
|
||||
message.success('删除成功');
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '删除失败');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('删除失败');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.smart-device {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(180deg, #eef3f8 0%, #e8eefb 100%);
|
||||
padding: 20px 24px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: #5b6b7e;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.breadcrumb .sep {
|
||||
color: #c2c6d8;
|
||||
}
|
||||
|
||||
.breadcrumb .cur {
|
||||
color: #0054cd;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.header-main {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.title-bar {
|
||||
width: 5px;
|
||||
height: 22px;
|
||||
background: #0054cd;
|
||||
border-radius: 3px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #0d1c2c;
|
||||
line-height: 1.4;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #0054cd;
|
||||
letter-spacing: 2px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin: 6px 0 0;
|
||||
font-size: 13px;
|
||||
color: #5b6b7e;
|
||||
}
|
||||
|
||||
.content-card {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
background: #fff;
|
||||
border: 1px solid #dbe4ee;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.07);
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.filter-card {
|
||||
flex-shrink: 0;
|
||||
background: #f8faff;
|
||||
border: 1px solid #dbe4ee;
|
||||
border-radius: 10px;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.filter-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 12px 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.filter-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.filter-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #5b6b7e;
|
||||
}
|
||||
|
||||
.filter-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 14px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #dbe4ee;
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.list-section {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 2px 4px 12px;
|
||||
}
|
||||
|
||||
.list-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.list-title .dot {
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background: #0054cd;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.list-title .t {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #0d1c2c;
|
||||
}
|
||||
|
||||
.list-title .count {
|
||||
padding: 1px 8px;
|
||||
border-radius: 6px;
|
||||
background: #eef4ff;
|
||||
color: #0054cd;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
font-family: Consolas, Menlo, monospace;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: Consolas, Menlo, monospace;
|
||||
font-size: 13px;
|
||||
color: #0d1c2c;
|
||||
}
|
||||
|
||||
.model-name {
|
||||
font-weight: 600;
|
||||
color: #0d1c2c;
|
||||
}
|
||||
|
||||
.type-chip {
|
||||
display: inline-block;
|
||||
padding: 1px 8px;
|
||||
border-radius: 6px;
|
||||
background: #eef4ff;
|
||||
color: #0054cd;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin: 4px 0 6px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #eef1f6;
|
||||
}
|
||||
|
||||
.section-no {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 7px;
|
||||
background: #0054cd;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: Consolas, Menlo, monospace;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.section-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.section-name {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #0d1c2c;
|
||||
}
|
||||
|
||||
.section-sub {
|
||||
font-size: 11px;
|
||||
color: #8a97a8;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
:deep(.ant-table) {
|
||||
font-size: 14px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.ant-table-thead > tr > th) {
|
||||
background: #f4f7ff;
|
||||
color: #5b6b7e;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:deep(.ant-btn-link) {
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
:deep(.ant-btn-primary) {
|
||||
background: #0054cd;
|
||||
border-color: #0054cd;
|
||||
}
|
||||
|
||||
:deep(.ant-btn-primary:not(:disabled):hover),
|
||||
:deep(.ant-btn-primary:not(:disabled):focus) {
|
||||
background: #136bfb;
|
||||
border-color: #136bfb;
|
||||
}
|
||||
</style>
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
Reference in new issue
Block a user