fastbee/yack-web/src/views/iot/project/index.vue

552 lines
26 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="iot-project">
<el-card v-show="showSearch" style="margin-bottom: 10px">
<el-form ref="queryForm" :model="queryParams" :inline="true" size="mini" label-width="68px">
<el-form-item label="项目名称" prop="projectName">
<el-input v-model="queryParams.projectName" placeholder="请输入项目名称" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="项目状态" prop="projectStatus">
<el-select v-model="queryParams.projectStatus" placeholder="请选择项目状态" clearable style="width: 120px">
<el-option label="预警" :value="1" />
<el-option label="运行中" :value="2" />
<el-option label="完结" :value="3" />
</el-select>
</el-form-item>
<el-form-item label="负责人" prop="responsibleUserId">
<el-select v-model="queryParams.responsibleUserId" placeholder="请选择负责人" clearable filterable style="width: 150px">
<el-option v-for="user in userList" :key="user.userId" :label="user.nickName" :value="user.userId" />
</el-select>
</el-form-item>
<el-form-item style="float: right">
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card>
<el-row :gutter="10" style="margin-bottom: 15px">
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd" v-hasPermi="['iot:project:add']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete" v-hasPermi="['iot:project:remove']">删除</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="projectList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="项目名称" align="center" prop="projectName" min-width="150" />
<el-table-column label="项目编码" align="center" prop="projectCode" width="120" />
<el-table-column label="负责人" align="center" width="100">
<template slot-scope="scope">
<span>{{ getUserName(scope.row.responsibleUserId, scope.row.responsibleUserName) }}</span>
</template>
</el-table-column>
<el-table-column label="项目状态" align="center" prop="projectStatus" width="100">
<template slot-scope="scope">
<el-tag :type="getStatusTagType(scope.row.projectStatus)">{{ getStatusText(scope.row.projectStatus) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="坐标" align="center" width="150">
<template slot-scope="scope">
<span v-if="scope.row.longitude && scope.row.latitude">
{{ scope.row.longitude }}°, {{ scope.row.latitude }}°
</span>
<span v-else style="color: #999">未设置</span>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="160">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="250">
<template slot-scope="scope">
<el-button size="small" type="text" icon="el-icon-view" @click="handleView(scope.row)" v-hasPermi="['iot:project:query']">查看</el-button>
<el-button size="small" type="text" icon="el-icon-setting" @click="handleManageDevices(scope.row)" v-hasPermi="['iot:project:edit']">管理设备</el-button>
<el-button size="small" type="text" icon="el-icon-monitor" @click="handleViewDevices(scope.row)" v-hasPermi="['iot:project:query']">查看设备</el-button>
<el-button size="small" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['iot:project:edit']">编辑</el-button>
<el-button size="small" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['iot:project:remove']">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 添加或修改项目对话框 -->
<el-dialog :title="title" :visible.sync="open" width="600px" append-to-body :close-on-click-modal="false">
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="项目名称" prop="projectName">
<el-input v-model="form.projectName" placeholder="请输入项目名称" :disabled="isView" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="项目编码" prop="projectCode">
<el-input v-model="form.projectCode" placeholder="请输入项目编码" :disabled="isView" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="负责人" prop="responsibleUserId">
<el-select v-model="form.responsibleUserId" placeholder="请选择负责人" filterable style="width: 100%" :disabled="isView" @change="handleResponsibleUserChange">
<el-option v-for="user in userList" :key="user.userId" :label="user.nickName" :value="user.userId" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="项目状态" prop="projectStatus">
<el-select v-model="form.projectStatus" placeholder="请选择项目状态" style="width: 100%" :disabled="isView">
<el-option label="预警" :value="1" />
<el-option label="运行中" :value="2" />
<el-option label="完结" :value="3" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="开始时间" prop="startTime">
<el-date-picker v-model="form.startTime" type="datetime" placeholder="选择开始时间" value-format="yyyy-MM-dd HH:mm:ss" style="width: 100%" :disabled="isView" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="结束时间" prop="endTime">
<el-date-picker v-model="form.endTime" type="datetime" placeholder="选择结束时间" value-format="yyyy-MM-dd HH:mm:ss" style="width: 100%" :disabled="isView" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="项目坐标" prop="coordinates">
<el-row :gutter="10" type="flex" justify="start" style="flex-wrap: nowrap;">
<el-col :span="8">
<el-input v-model.number="form.longitude" placeholder="经度" type="number" :disabled="isView" />
</el-col>
<el-col :span="8">
<el-input v-model.number="form.latitude" placeholder="纬度" type="number" :disabled="isView" />
</el-col>
<el-col :span="8">
<el-button type="primary" size="small" @click="openMapPicker" :disabled="isView">地图选点</el-button>
</el-col>
</el-row>
</el-form-item>
<el-form-item label="项目地址" prop="address">
<el-input v-model="form.address" placeholder="请输入项目地址" :disabled="isView" />
</el-form-item>
<el-form-item label="项目描述" prop="projectDesc">
<el-input v-model="form.projectDesc" type="textarea" placeholder="请输入项目描述" :rows="3" :disabled="isView" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<template v-if="isView">
<el-button @click="open = false">关 闭</el-button>
</template>
<template v-else>
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</template>
</div>
</el-dialog>
<!-- 地图坐标选择对话框 -->
<el-dialog title="地图选点" :visible.sync="mapPickerOpen" width="800px" append-to-body @closed="disposeMap">
<div style="height: 400px; border: 1px solid #ddd; position: relative">
<div ref="mapContainer" style="width: 100%; height: 100%"></div>
<div v-if="geocoding" style="position: absolute; top: 8px; right: 8px; background: rgba(255,255,255,0.9); padding: 4px 10px; border-radius: 3px; font-size: 12px; z-index: 999">
<i class="el-icon-loading"></i> 地址解析中...
</div>
</div>
<div style="margin-top: 10px; display: flex; justify-content: space-between; align-items: center">
<span>经度: {{ pickerLongitude }}°, 纬度: {{ pickerLatitude }}°</span>
<span v-if="pickerAddress" style="color: #606266; font-size: 13px">{{ pickerAddress }}</span>
</div>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="confirmCoordinates">确 定</el-button>
<el-button @click="mapPickerOpen = false">取 消</el-button>
</div>
</el-dialog>
<project-device-select ref="deviceSelect" @refresh="getList" />
</div>
</template>
<script>
import { listProject, getProject, delProject, addProject, updateProject } from '@/api/iot/project';
import { listUserByScope } from '@/api/system/user';
import { DEFAULT_TIANDITU_AK, DEFAULT_TIANDITU_TILE_KEY, loadTiandituMap } from '@/utils/tdtMap';
import ProjectDeviceSelect from './device-select.vue';
export default {
name: 'Project',
components: {
ProjectDeviceSelect
},
data() {
return {
loading: true,
ids: [],
single: true,
multiple: true,
showSearch: true,
total: 0,
projectList: [],
userList: [],
title: '',
open: false,
isView: false,
mapPickerOpen: false,
pickerLongitude: null,
pickerLatitude: null,
pickerAddress: '',
geocoding: false,
queryParams: {
pageNum: 1,
pageSize: 10,
projectName: null,
projectStatus: null,
responsibleUserId: null,
},
form: {},
rules: {
projectName: [{ required: true, message: '项目名称不能为空', trigger: 'blur' }],
responsibleUserId: [{ required: true, message: '请选择负责人', trigger: 'change' }],
projectStatus: [{ required: true, message: '请选择项目状态', trigger: 'change' }],
},
// 天地图实例
tiandituMap: null,
tiandituMarker: null,
tiandituGeocoder: null,
};
},
created() {
this.getList();
this.getUserList();
},
beforeDestroy() {
this.disposeMap();
},
methods: {
/** 查询项目列表 */
getList() {
this.loading = true;
listProject(this.queryParams).then(response => {
this.projectList = response.rows;
this.total = response.total;
this.loading = false;
}).catch(() => {
this.loading = false;
});
},
/** 查询用户列表 */
getUserList() {
listUserByScope({ pageNum: 1, pageSize: 100 }).then(response => {
this.userList = response.rows || [];
});
},
/** 获取负责人名称 */
getUserName(userId, userName) {
if (userName) return userName;
const user = this.userList.find(item => Number(item.userId) === Number(userId));
return user ? user.nickName : '';
},
/** 管理项目设备 */
handleManageDevices(row) {
this.$refs.deviceSelect.show(row.projectId, row.projectName);
},
/** 负责人变更 */
handleResponsibleUserChange(userId) {
const user = this.userList.find(item => Number(item.userId) === Number(userId));
this.form.responsibleUserName = user ? user.nickName : null;
},
/** 规范化项目表单 */
normalizeProjectForm(data) {
if (!data) return data;
if (data.responsibleUserId != null) {
data.responsibleUserId = Number(data.responsibleUserId);
}
if (data.projectStatus != null) {
data.projectStatus = Number(data.projectStatus);
}
if (!data.responsibleUserName && data.responsibleUserId != null) {
data.responsibleUserName = this.getUserName(data.responsibleUserId, data.responsibleUserName);
}
return data;
},
/** 状态文本 */
getStatusText(status) {
const map = { 1: '预警', 2: '运行中', 3: '完结' };
return map[status] || '未知';
},
/** 状态标签颜色 */
getStatusTagType(status) {
const map = { 1: 'danger', 2: 'success', 3: 'info' };
return map[status] || '';
},
/** 取消 */
cancel() {
this.open = false;
this.isView = false;
this.reset();
},
/** 表单重置 */
reset() {
this.form = {
projectId: null,
projectName: null,
projectCode: null,
responsibleUserId: null,
responsibleUserName: null,
longitude: null,
latitude: null,
address: null,
projectStatus: 2,
startTime: null,
endTime: null,
projectDesc: null,
};
this.isView = false;
this.resetForm('form');
},
/** 搜索 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置搜索 */
resetQuery() {
this.resetForm('queryForm');
this.handleQuery();
},
/** 多选 */
handleSelectionChange(selection) {
this.ids = selection.map(item => item.projectId);
this.single = selection.length !== 1;
this.multiple = !selection.length;
},
/** 新增 */
handleAdd() {
this.reset();
this.open = true;
this.title = '添加项目';
},
/** 查看(只读) */
handleView(row) {
this.reset();
this.isView = true;
this.open = true;
this.title = '查看项目';
const projectId = row.projectId || this.ids[0];
getProject(projectId).then(response => {
this.form = this.normalizeProjectForm(response.data);
});
},
/** 查看项目设备 */
handleViewDevices(row) {
this.$router.push({
path: '/iot/device',
query: { projectId: row.projectId, t: Date.now() }
});
},
/** 编辑 */
handleUpdate(row) {
this.reset();
this.open = true;
this.title = '修改项目';
this.$nextTick(() => {
const projectId = row.projectId || this.ids[0];
getProject(projectId).then(response => {
this.form = this.normalizeProjectForm(response.data);
});
});
},
/** 提交 */
submitForm() {
this.$refs['form'].validate(valid => {
if (valid) {
this.handleResponsibleUserChange(this.form.responsibleUserId);
if (this.form.projectId != null) {
updateProject(this.form).then(() => {
this.$modal.msgSuccess('修改成功');
this.open = false;
this.getList();
});
} else {
addProject(this.form).then(() => {
this.$modal.msgSuccess('新增成功');
this.open = false;
this.getList();
});
}
}
});
},
/** 删除 */
handleDelete(row) {
const projectIds = row.projectId ? [row.projectId] : this.ids;
const names = row.projectName || '所选项目';
this.$modal.confirm('是否确认删除"' + names + '"?').then(() => {
return delProject(projectIds);
}).then(() => {
this.getList();
this.$modal.msgSuccess('删除成功');
}).catch(() => {});
},
// ==================== 地图选点 ====================
/** 打开地图选点 */
openMapPicker() {
this.pickerLongitude = this.form.longitude || null;
this.pickerLatitude = this.form.latitude || null;
this.pickerAddress = this.form.address || '';
this.mapPickerOpen = true;
this.$nextTick(() => {
this.initTiandituMap();
});
},
/** 初始化天地图 */
initTiandituMap() {
loadTiandituMap().then(T => {
const container = this.$refs.mapContainer;
if (!container) return;
const centerLng = this.pickerLongitude || 104.0;
const centerLat = this.pickerLatitude || 35.0;
const zoom = (this.pickerLongitude && this.pickerLatitude) ? 12 : 4;
// 天地图 v4 没有 destroy(),先清理旧实例再重建
this.resetMap();
const map = new T.Map(container);
map.centerAndZoom(new T.LngLat(centerLng, centerLat), zoom);
map.addControl(new T.Control.Zoom());
this.tiandituGeocoder = T.Geocoder ? new T.Geocoder() : null;
const protocol = window.location.protocol === 'http:' ? 'http:' : 'https:';
const tk = DEFAULT_TIANDITU_TILE_KEY;
// 底图
map.addLayer(new T.TileLayer(`${protocol}//t{s}.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=${tk}`, { subdomains: '01234567' }));
// 注记
map.addLayer(new T.TileLayer(`${protocol}//t{s}.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=${tk}`, { subdomains: '01234567' }));
// 已有坐标则添加标记
if (this.pickerLongitude && this.pickerLatitude) {
const point = new T.LngLat(this.pickerLongitude, this.pickerLatitude);
this.tiandituMarker = new T.Marker(point);
map.addOverLay(this.tiandituMarker);
}
// 点击选点
map.addEventListener('click', (e) => {
const lnglat = e.lnglat;
this.pickerLongitude = Number(lnglat.getLng().toFixed(6));
this.pickerLatitude = Number(lnglat.getLat().toFixed(6));
// 同步更新表单坐标字段
this.form.longitude = this.pickerLongitude;
this.form.latitude = this.pickerLatitude;
if (this.tiandituMarker) {
map.removeOverLay(this.tiandituMarker);
}
this.tiandituMarker = new T.Marker(new T.LngLat(this.pickerLongitude, this.pickerLatitude));
map.addOverLay(this.tiandituMarker);
this.reverseGeocode(this.pickerLongitude, this.pickerLatitude);
});
this.tiandituMap = map;
}).catch((err) => {
console.error('天地图加载失败:', err);
this.$message.error('天地图加载失败,请检查网络后重试');
});
},
/** 逆地理编码:经纬度 → 地址 */
reverseGeocode(lng, lat) {
this.geocoding = true;
if (this.tiandituGeocoder && window.T) {
this.tiandituGeocoder.getLocation(new window.T.LngLat(lng, lat), result => {
this.geocoding = false;
this.applyGeocodeAddress(result);
});
return;
}
const cbName = '_tianditu_geocoder_' + Date.now();
window[cbName] = (response) => {
this.geocoding = false;
this.applyGeocodeAddress(response);
delete window[cbName];
const script = document.getElementById(cbName + '_script');
if (script) script.remove();
};
const protocol = window.location.protocol === 'http:' ? 'http:' : 'https:';
const postStr = encodeURIComponent(JSON.stringify({ lon: lng, lat: lat, ver: 1 }));
const script = document.createElement('script');
script.id = cbName + '_script';
script.src = `${protocol}//api.tianditu.gov.cn/geocoder?postStr=${postStr}&type=geocode&tk=${DEFAULT_TIANDITU_AK}&callback=${cbName}`;
script.onerror = () => {
this.geocoding = false;
delete window[cbName];
};
document.head.appendChild(script);
},
/** 设置逆地理地址 */
applyGeocodeAddress(response) {
try {
const result = response && response.result ? response.result : response;
const data = typeof result === 'string' ? JSON.parse(result) : result;
const component = data && (data.addressComponent || data.address_component);
const addr = (data && (data.formatted_address || data.formattedAddress || data.address))
|| (component ? [component.province, component.city, component.county, component.road, component.poi].filter(Boolean).join('') : '');
if (addr) {
this.pickerAddress = addr;
this.form.address = addr;
}
} catch (e) {
this.pickerAddress = '';
}
},
/** 确认坐标 */
confirmCoordinates() {
this.form.longitude = this.pickerLongitude;
this.form.latitude = this.pickerLatitude;
if (this.pickerAddress) {
this.form.address = this.pickerAddress;
}
this.mapPickerOpen = false;
},
/** 清理地图实例(天地图 v4 无 destroy,手动清理覆盖物并清空容器) */
resetMap() {
if (this.tiandituMap) {
try {
if (typeof this.tiandituMap.clearOverLays === 'function') {
this.tiandituMap.clearOverLays();
}
} catch (e) {
console.warn('清理天地图覆盖物失败:', e);
}
this.tiandituMap = null;
}
const container = this.$refs.mapContainer;
if (container) {
container.innerHTML = '';
}
},
/** 销毁地图实例 */
disposeMap() {
this.tiandituMarker = null;
this.resetMap();
},
},
};
</script>
<style scoped>
.iot-project {
padding: 20px;
}
</style>