更新配置文件,添加文件传输代理;修改样式文件,增加自定义滚动条样式;更新地图组件,调整地图样式和功能;新增路由,添加即将推出的页面;扩展工单存储,增加处理工单的功能;更新用户存储,支持用户名的存储;修复资产管理视图,优化数据获取逻辑;更新仪表板视图,添加地图背景和加载状态;优化维护视图,增加故障设备选择和处理功能。

main
zonawayne 10 months ago
parent c04d95f9ea
commit 4eb432c321

@ -0,0 +1,21 @@
import rpc from '../utils/rpc'
// 获取大屏基础数据
export const fetchBasicData = (params = {}) => {
return rpc.post('/dataBoard/basicData', params)
}
// 获取设备统计数据
export const fetchDeviceStats = (params = {}) => {
return rpc.post('/dataBoard/deviceStats', params)
}
// 获取能耗数据
export const fetchEnergyData = (params = {}) => {
return rpc.post('/dataBoard/energyData', params)
}
// 获取故障统计数据
export const fetchFaultStats = (params = {}) => {
return rpc.post('/dataBoard/faultStats', params)
}

@ -113,11 +113,25 @@ const initMap = async () => {
zoom: 15,
center: [currentLocation.value.lng || 121.4737, currentLocation.value.lat || 31.23037],
viewMode: '3D',
mapStyle: 'amap://styles/dark',
mapStyle: 'amap://styles/darkblue',
features: ['bg', 'building', 'point'],
buildingAnimation: true,
pitch: 50,
skyColor: '#1E1E1E',
pitch: 40,
resizeEnable: true,
buildingAnimation: true, // POI
showBuildingBlock: true,
showIndoorMap: false,
showLabel: true, //
showRoad: true, //
showTraffic: false, //
zoomEnable: true, //
dragEnable: true, //
scrollWheel: true, //
keyboardEnable: true, //
doubleClickZoom: true, //
touchZoom: true, //
jogEnable: true, //
logoVisible: false, //
})
//

@ -7,6 +7,7 @@ import DataScreenView from '../views/DataScreenView.vue'
import DefaultLayout from '../layouts/DefaultLayout.vue'
import { useUserStore } from '../stores/user'
import AssetTreeView from '../views/AssetTreeView.vue'
import ComingSoonView from '../views/ComingSoonView.vue'
const router = createRouter({
history: createWebHistory(),
@ -48,6 +49,11 @@ const router = createRouter({
path: '/asset-tree',
name: 'AssetTree',
component: AssetTreeView
},
{
path: '/coming-soon',
name: 'ComingSoon',
component: ComingSoonView
}
]
}

@ -71,6 +71,16 @@ export const useTicketStore = defineStore('ticket', () => {
}
}
// 处理接口
const processTicket = async (params: any) => {
loading.value = true
try {
await ticketApi.transitionTicket(params)
} finally {
loading.value = false
}
}
return {
ticketList,
currentTicket,
@ -80,6 +90,7 @@ export const useTicketStore = defineStore('ticket', () => {
fetchTicketDetail,
createTicket,
updateTicket,
deleteTicket
deleteTicket,
processTicket,
}
})
});

@ -5,10 +5,15 @@ export const useUserStore = defineStore('user', () => {
// 从本地存储获取初始值
const nickname = ref(localStorage.getItem('nickname') || '')
const token = ref(localStorage.getItem('token') || '')
const username = ref(localStorage.getItem('username') || '')
const setUserInfo = (userInfo: { nickname: string; token: string }) => {
const setUserInfo = (userInfo: { nickname: string; token: string; username?: string }) => {
nickname.value = userInfo.nickname
token.value = userInfo.token
if (userInfo.username) {
username.value = userInfo.username
localStorage.setItem('username', userInfo.username)
}
// 保存到本地存储
localStorage.setItem('nickname', userInfo.nickname)
localStorage.setItem('token', userInfo.token)
@ -17,14 +22,17 @@ export const useUserStore = defineStore('user', () => {
const clearUserInfo = () => {
nickname.value = ''
token.value = ''
username.value = ''
// 清除本地存储
localStorage.removeItem('nickname')
localStorage.removeItem('token')
localStorage.removeItem('username')
}
return {
nickname,
token,
username,
setUserInfo,
clearUserInfo
}

@ -124,11 +124,11 @@ body {
}
/* 状态标识色 */
.el-tag {
/* .el-tag {
background-color: #001529 !important;
border-color: #13c2c2 !important;
color: #13c2c2 !important;
}
} */
/* 空数据提示 */
.el-empty__description p {
@ -202,3 +202,45 @@ body {
background: #002140 !important;
padding: 12px;
}
/* 自定义深蓝科技风滚动条 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
background-color: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(24, 144, 255, 0.3);
border-radius: 20px;
border: 2px solid transparent;
background-clip: content-box;
transition: all 0.3s ease;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(24, 144, 255, 0.5);
border: 2px solid transparent;
background-clip: content-box;
box-shadow: 0 0 8px rgba(24, 144, 255, 0.3);
}
::-webkit-scrollbar-track {
background: rgba(0, 21, 41, 0.1);
border-radius: 20px;
}
/* 滚动容器的边缘发光效果 */
.el-scrollbar__wrap,
.el-table__body-wrapper,
.el-select-dropdown__wrap,
.el-cascader-menu__wrap {
&:hover::-webkit-scrollbar-thumb {
background: rgba(24, 144, 255, 0.4);
border: 2px solid transparent;
background-clip: content-box;
}
scrollbar-width: thin;
scrollbar-color: rgba(24, 144, 255, 0.3) transparent;
}

@ -34,7 +34,7 @@
--el-color-primary-light-5: #41cfff;
--el-color-primary-light-7: #6eeaff;
--el-color-primary-light-8: #b3e5ff;
--el-color-primary-light-9: #e6f7ff;
--el-color-primary-light-9: #1a9fff;
--el-color-primary-dark-2: #0052cc;
--el-bg-color-page: #020b1f;
--el-bg-color-overlay: #0a1a3c;
@ -43,6 +43,8 @@
--el-box-shadow-light: 0 0 12px #00c3ff44;
--el-box-shadow-dark: 0 0 24px #00c3ff88;
--el-dropdown-menuItem-hover-fill: #00c3ff;
--el-button-hover-text-color: var(--el-text-color-primary);
--el-button-hover-border-color: #188fffd5;
@ -75,9 +77,52 @@ $--table-row-hover-background-color: rgba(24, 144, 255, 0.08); // 表格行悬
$--button-default-background-color: transparent; //
$--button-default-border-color: rgba(255,255,255,0.3); //
.el-tag,
.el-button--primary {
box-shadow: 0 0 12px #00c3ff88, 0 0 2px #6eeaff;
text-shadow: 0 0 6px #6eeaff88;
border-color: #00c3ff !important;
// .el-tag,
// .el-button--primary {
// box-shadow: 0 0 12px #00c3ff88, 0 0 2px #6eeaff;
// text-shadow: 0 0 6px #6eeaff88;
// border-color: #00c3ff !important;
// }
// Tag
.el-tag--dark.el-tag--info {
--el-tag-bg-color: #409EFF; //
--el-tag-border-color: #409EFF;
--el-tag-text-color: #fff;
--el-tag-hover-color: #1a9fff;
}
.el-tag--dark.el-tag--primary {
--el-tag-bg-color: #00c3ff; //
--el-tag-border-color: #00c3ff;
--el-tag-text-color: #fff;
--el-tag-hover-color: #41cfff;
}
.el-tag--dark.el-tag--success {
--el-tag-bg-color: #67c23a; // 绿
--el-tag-border-color: #67c23a;
--el-tag-text-color: #fff;
--el-tag-hover-color: #95d475;
}
.el-tag--dark.el-tag--warning {
--el-tag-bg-color: #e6a23c; //
--el-tag-border-color: #e6a23c;
--el-tag-text-color: #fff;
--el-tag-hover-color: #ffb955;
}
.el-tag--dark.el-tag--danger {
--el-tag-bg-color: #f56c6c; //
--el-tag-border-color: #f56c6c;
--el-tag-text-color: #fff;
--el-tag-hover-color: #ff9999;
}
.el-tag--dark.el-tag--default {
--el-tag-bg-color: #606266; //
--el-tag-border-color: #606266;
--el-tag-text-color: #fff;
--el-tag-hover-color: #909399;
}

@ -39,6 +39,11 @@ export interface TicketItem {
updated_at: string
contact_info: string
expected_completion_time?: string
process_time?: string
fault_time?: string
handler?: string
images?: string[]
dispatch_time?: string
}
// 工单列表响应接口

@ -72,15 +72,18 @@ const currentPage = ref(1)
const allData = ref<any[]>([])
const fetchList = async () => {
const res = await rpc.post('/markType/list', {})
const rows = (res as any)?.rows || []
allData.value = rows
let data = rows
const params: any = {
page: currentPage.value,
limit: pageSize.value,
orderby: [{ mark_type_id: 'desc' }]
}
if (filters.keyword) {
data = data.filter((item: any) => item.markTypeName?.includes(filters.keyword))
params.like = { mark_type_name: ['%' + filters.keyword + '%'] }
}
total.value = data.length
tableData.value = data.slice((currentPage.value-1)*pageSize.value, currentPage.value*pageSize.value)
const res = await rpc.post('/markType/list', params)
const rows = res?.rows || []
total.value = res?.count || rows.length
tableData.value = rows
}
const openAddDialog = () => {

@ -187,14 +187,20 @@ const fetchTypeTree = async () => {
//
const fetchList = async () => {
const res = await rpc.post('/asset/list', {
...filters,
pageSize: pageSize.value,
currentPage: currentPage.value
})
const data = res as any
tableData.value = data.rows || []
total.value = data.total || 0
const params: any = {
page: currentPage.value,
limit: pageSize.value,
orderby: [{ id: 'desc' }]
}
if (filters.keyword) {
params.like = { asset_name: ['%' + filters.keyword + '%'] }
}
if (filters.assetTypeId) {
params.and = { asset_type_id: [filters.assetTypeId] }
}
const res = await rpc.post('/asset/list', params)
tableData.value = res?.rows || []
total.value = res?.count || 0
}
//

@ -1,7 +1,7 @@
<template>
<div class="asset-type-page">
<!-- 顶部筛选区 -->
<!-- 主表格区 -->
<el-card class="table-card">
@ -16,16 +16,11 @@
<el-button type="success" @click="openAddDialog"></el-button>
</el-form-item>
</el-form>
<el-table
:data="tableData"
row-key="id"
border
style="width: 100%;"
>
<el-table :data="tableData" row-key="id" border style="width: 100%;">
<el-table-column prop="assetTypeName" label="类型名称" min-width="120" />
<el-table-column prop="parentId" label="父类型" min-width="120">
<template #default="{ row }">
{{ row.parentId ? parentOptions.find(item => item.id === row.parentId)?.assetTypeName || '-' : '-' }}
{{row.parentId ? parentOptions.find(item => item.id === row.parentId)?.assetTypeName || '-' : '-'}}
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="120" />
@ -36,34 +31,22 @@
</template>
</el-table-column>
</el-table>
<el-pagination
style="margin: 16px 0 0; text-align: right"
background
layout="total, sizes, prev, pager, next, jumper"
:total="total"
:page-size="pageSize"
:current-page="currentPage"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:page-sizes="[10, 20, 50, 100]"
/>
<el-pagination style="margin: 16px 0 0; text-align: right" background
layout="total, sizes, prev, pager, next, jumper" :total="total" :page-size="pageSize"
:current-page="currentPage" @size-change="handleSizeChange" @current-change="handleCurrentChange"
:page-sizes="[10, 20, 50, 100]" />
</el-card>
<!-- 新增/编辑弹窗 -->
<el-dialog v-model="showDialog" :title="dialogType==='add'?'新增资产类型':'编辑资产类型'" width="500px">
<el-dialog v-model="showDialog" :title="dialogType === 'add' ? '新增资产类型' : '编辑资产类型'" width="500px">
<el-form :model="form" label-width="100px">
<el-form-item label="类型名称" required>
<el-input v-model="form.assetTypeName" />
</el-form-item>
<el-form-item label="父类型">
<el-select v-model="form.parentId" placeholder="请选择父类型" clearable>
<el-option
v-for="item in parentOptions"
:key="item.id"
:label="item.assetTypeName"
:value="item.id"
:disabled="dialogType === 'edit' && item.id === form.id"
/>
<el-option v-for="item in parentOptions" :key="item.id" :label="item.assetTypeName" :value="item.id"
:disabled="dialogType === 'edit' && item.id === form.id" />
</el-select>
</el-form-item>
<el-form-item label="备注">
@ -72,7 +55,7 @@
<!-- 可根据接口补充attrs/assetAttrs等字段 -->
</el-form>
<template #footer>
<el-button @click="showDialog=false"></el-button>
<el-button @click="showDialog = false">取消</el-button>
<el-button type="primary" @click="submitForm"></el-button>
</template>
</el-dialog>
@ -87,7 +70,7 @@ import rpc from '../utils/rpc'
const tableData = ref<any[]>([])
const parentOptions = ref<any[]>([])
const showDialog = ref(false)
const dialogType = ref<'add'|'edit'>('add')
const dialogType = ref<'add' | 'edit'>('add')
const form = reactive<any>({})
const filters = reactive({ keyword: '' })
const total = ref(0)
@ -96,22 +79,23 @@ const currentPage = ref(1)
//
const fetchParentOptions = async () => {
const res = await rpc.post('/assetType/list', {})
const res = await rpc.post('/assetType/list', { limit: -1 })
parentOptions.value = (res as any)?.rows || []
}
const fetchList = async () => {
const res = await rpc.post('/assetType/list', {})
let rows = (res as any)?.rows || []
//
const params: any = {
page: currentPage.value,
limit: pageSize.value,
orderby: [{ id: 'asc' }]
}
if (filters.keyword) {
rows = rows.filter((item:any) =>
item.assetTypeName?.includes(filters.keyword) ||
parentOptions.value.find(p => p.id === item.parentId)?.assetTypeName?.includes(filters.keyword)
)
params.like = { asset_type_name: ['%' + filters.keyword + '%'] }
}
total.value = rows.length
tableData.value = rows.slice((currentPage.value-1)*pageSize.value, currentPage.value*pageSize.value)
const res = await rpc.post('/assetType/list', params)
let rows = res?.rows || []
total.value = res?.count || rows.length
tableData.value = rows
}
const openAddDialog = () => {
@ -133,11 +117,12 @@ const openEditDialog = (row: any) => {
const handleDelete = (row: any) => {
ElMessageBox.confirm('确定删除该资产类型吗?', '提示', { type: 'warning' })
.then(async () => {
await rpc.post('/assetType/delete', { id: row.id })
await rpc.post('/assetType/delete', { id: row.id }
)
ElMessage.success('删除成功')
fetchList()
})
.catch(() => {})
.catch(() => { })
}
const submitForm = async () => {
@ -146,10 +131,12 @@ const submitForm = async () => {
return
}
if (dialogType.value === 'add') {
await rpc.post('/assetType/create', form)
await rpc.post('/assetType/create', form
)
ElMessage.success('新增成功')
} else {
await rpc.post('/assetType/update', form)
await rpc.post('/assetType/update', form
)
ElMessage.success('编辑成功')
}
showDialog.value = false
@ -181,6 +168,7 @@ onMounted(() => {
padding: 20px;
box-sizing: border-box;
}
.filter-card {
background: transparent;
border: none;
@ -188,9 +176,11 @@ onMounted(() => {
margin-bottom: 16px;
padding: 0;
}
.filter-form {
padding: 0 0 8px 0;
}
.table-card {
flex: 1;
display: flex;
@ -200,23 +190,28 @@ onMounted(() => {
box-shadow: 0 0 20px #0003;
padding: 0 0 16px 0;
}
.el-table {
background: transparent;
color: #e6f7ff;
}
.el-table th {
background: #002140;
color: #6eeaff;
font-weight: bold;
font-size: 15px;
}
.el-table .el-button {
box-shadow: 0 0 8px #00c3ff88;
border-radius: 4px;
}
.el-pagination {
margin-top: 16px;
}
:deep(.el-select .el-input__wrapper) {
background: rgba(0, 33, 64, 0.5);
box-shadow: 0 0 0 1px rgba(0, 195, 255, 0.2);
@ -247,4 +242,4 @@ onMounted(() => {
color: #00c3ff;
background: rgba(0, 195, 255, 0.2);
}
</style>
</style>

@ -0,0 +1,149 @@
<template>
<div class="coming-soon-container">
<!-- 动画层 -->
<div class="bg-animate">
<div class="bg-circle"></div>
<div class="bg-circle bg-circle2"></div>
<div class="bg-light"></div>
</div>
<!-- Main content -->
<div class="main-content">
<div class="message-box">
<div class="icon">
<i class="building-icon"></i>
</div>
<h1>功能暂未开放</h1>
<div class="back-button" @click="router.push('/')">
返回首页
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router';
const router = useRouter();
</script>
<style scoped>
.coming-soon-container {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
background: linear-gradient(180deg, #041236 0%, #02081A 100%);
position: relative;
overflow: hidden;
}
/* 动画层样式 */
.bg-animate {
position: absolute;
left: 50%; top: 50%;
transform: translate(-50%, -50%);
width: 120vw; height: 120vh;
pointer-events: none;
z-index: 0;
}
.bg-circle {
position: absolute;
left: 50%; top: 50%;
transform: translate(-50%, -50%);
border-radius: 50%;
border: 2px solid #00c3ff55;
width: 60vw; height: 60vw;
animation: rotate 16s linear infinite;
box-shadow: 0 0 80px 20px #00c3ff33, 0 0 200px 40px #00c3ff22 inset;
}
.bg-circle2 {
width: 40vw; height: 40vw;
border: 2px dashed #00c3ff33;
animation: rotate-rev 24s linear infinite;
opacity: 0.7;
}
.bg-light {
position: absolute;
left: 50%; top: 50%;
transform: translate(-50%, -50%);
width: 100vw; height: 100vw;
background: conic-gradient(from 0deg, #00c3ff33 0deg 60deg, transparent 60deg 360deg);
border-radius: 50%;
filter: blur(40px);
animation: rotate 10s linear infinite;
opacity: 0.5;
}
@keyframes rotate {
0% { transform: translate(-50%, -50%) rotate(0deg);}
100% { transform: translate(-50%, -50%) rotate(360deg);}
}
@keyframes rotate-rev {
0% { transform: translate(-50%, -50%) rotate(0deg);}
100% { transform: translate(-50%, -50%) rotate(-360deg);}
}
.main-content {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
position: relative;
z-index: 1;
}
.message-box {
background: rgba(11, 37, 96, 0.7);
border: 1px solid #0066ff;
border-radius: 10px;
padding: 40px;
text-align: center;
box-shadow: 0 0 30px rgba(0, 102, 255, 0.3);
max-width: 500px;
width: 90%;
}
.icon {
margin-bottom: 20px;
}
.building-icon {
display: inline-block;
width: 80px;
height: 80px;
background: linear-gradient(135deg, #0066ff, #00c3ff);
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M17.5 4c-3.3 0-6 2.7-6 6 0 1.9.9 3.6 2.2 4.7-.6.6-1.4 1-2.2 1.3V14c0-1.1-.9-2-2-2H3c-1.1 0-2 .9-2 2v8h7v-2h4v2h9v-5h-3.5c.3-.8.5-1.6.5-2.5 0-3.3-2.7-6-6-6zm0 10c-2.2 0-4-1.8-4-4s1.8-4 4-4 4 1.8 4 4-1.8 4-4 4z'/%3E%3C/svg%3E") no-repeat center;
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M17.5 4c-3.3 0-6 2.7-6 6 0 1.9.9 3.6 2.2 4.7-.6.6-1.4 1-2.2 1.3V14c0-1.1-.9-2-2-2H3c-1.1 0-2 .9-2 2v8h7v-2h4v2h9v-5h-3.5c.3-.8.5-1.6.5-2.5 0-3.3-2.7-6-6-6zm0 10c-2.2 0-4-1.8-4-4s1.8-4 4-4 4 1.8 4 4-1.8 4-4 4z'/%3E%3C/svg%3E") no-repeat center;
}
h1 {
color: #ffffff;
font-size: 28px;
margin-bottom: 15px;
text-shadow: 0 0 10px rgba(0, 195, 255, 0.5);
}
p {
color: #a0c8ff;
font-size: 16px;
margin-bottom: 30px;
}
.back-button {
background: linear-gradient(180deg, #0A3677 0%, #041A47 100%);
color: white;
padding: 10px 30px;
border-radius: 4px;
font-size: 16px;
display: inline-block;
border: 1px solid #0066ff;
box-shadow: 0 0 15px rgba(0, 102, 255, 0.5);
cursor: pointer;
transition: box-shadow 0.3s;
}
.back-button:hover {
box-shadow: 0 0 20px rgba(0, 102, 255, 0.8);
}
</style>

@ -40,11 +40,11 @@ const activeModuleIndex = ref(3); // Data visualization screen is active by defa
const modules = [
{ title: '照明监控系统', icon: 'camera', route: '/lighting' },
{ title: '视频监控系统', icon: 'video', route: '/' },
{ title: '分析决策系统', icon: 'chart', route: '/' },
{ title: '视频监控系统', icon: 'video', route: '/coming-soon' },
{ title: '分析决策系统', icon: 'chart', route: '/coming-soon' },
{ title: '数据可视大屏', icon: 'screen', route: '/datascreen' },
{ title: '资产管理系统', icon: 'asset', route: '/asset-tree' },
{ title: '指挥调度系统', icon: 'command', route: '/' },
{ title: '指挥调度系统', icon: 'command', route: '/coming-soon' },
{ title: '维护工单系统', icon: 'maintenance', route: '/maintenance' }
];

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -128,7 +128,8 @@ const doLogin = async () => {
});
userStore.setUserInfo({
nickname: (res as any).nickname || username.value,
token: (res as any).token
token: (res as any).token,
username: username.value
});
ElMessage.success('登录成功');
await router.push('/dashboard');

@ -18,15 +18,16 @@
<div class="main-area">
<el-card>
<div style="margin-bottom: 16px;">
<el-form :inline="true" :model="filters" size="small">
<el-form-item label="关键词">
<el-input v-model="filters.keyword" size="small" placeholder="请输入关键词" clearable />
<el-form :inline="true" :model="filters" size="small" @submit.prevent="handleQuery">
<el-form-item label="标题">
<el-input v-model="filters.keyword" style="width: 200px;" placeholder="请输入标题" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="间区间">
<el-form-item label="故障时间">
<el-date-picker
v-model="filters.startTime"
type="datetime"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="开始时间"
style="width: 160px"
/>
@ -35,6 +36,7 @@
v-model="filters.endTime"
type="datetime"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="结束时间"
style="width: 160px"
/>
@ -58,12 +60,13 @@
</el-form>
</div>
<el-tabs v-model="currentTab" @tab-change="handleTabChange" style="margin-bottom: 16px;">
<el-tab-pane label="全部工单" name="all" />
<el-tab-pane label="我的工单" name="mine" />
<el-tab-pane label="全部工单" name="all" />
</el-tabs>
<el-table :data="ticketList" border style="width: 100%">
<el-table-column prop="title" label="工单标题" min-width="120" />
<el-table-column prop="description" label="工单描述" min-width="180" />
<!-- <el-table-column prop="description" label="工单描述" min-width="180" /> -->
<el-table-column prop="source" label="来源" min-width="80">
<template #default="{ row }">
{{ getSourceLabel(row.source) }}
@ -76,16 +79,31 @@
</el-tag>
</template>
</el-table-column>
<el-table-column label="故障设备" min-width="120">
<template #default="{ row }">
{{ deviceOptions.find(item => item.commUid === row.comm_uid)?.ilcName || row.comm_uid || '-' }}
</template>
</el-table-column>
<el-table-column prop="fault_time" label="故障时间" min-width="180" />
<el-table-column label="处理人" min-width="100">
<template #default="{ row }">
{{ handlerOptions.find(user => user.username === row.handler)?.nickname || row.handler }}
</template>
</el-table-column>
<el-table-column prop="contact" label="联系方式" min-width="120" />
<el-table-column prop="expected_completion_time" label="预计完成时间" min-width="140" />
<el-table-column prop="status" label="状态" min-width="100">
<el-table-column prop="expected_completion_time" label="预计完成时间" min-width="180" />
<el-table-column prop="status" label="状态" min-width="100" fixed="right">
<template #default="{ row }">
<el-tag :type="row.status === 'completed' ? 'success' : row.status === 'in_progress' ? 'warning' : row.status === 'closed' ? 'info' : 'default'">
<el-tag
:type="getStatusType(row.status)"
effect="dark"
disable-transitions
>
{{ getStatusLabel(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" min-width="180">
<el-table-column label="操作" min-width="180" fixed="right">
<template #default="{ row }">
<el-button-group>
<el-button size="small" @click="handleView(row)"></el-button>
@ -96,8 +114,8 @@
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="handleUpdateStatus(row)" v-if="canUpdateStatus(row.status)"></el-dropdown-item>
<el-dropdown-item @click="handleDispatch(row)"></el-dropdown-item>
<el-dropdown-item @click="handleProcess(row)" :disabled="!canProcess(row.status, row)">处理</el-dropdown-item>
<el-dropdown-item @click="handleDispatch(row)" :disabled="row.status !== 'new'">派工</el-dropdown-item>
<el-dropdown-item divided @click="handleDelete(row)" v-if="canDelete(row.status)">
<span style="color: #f56c6c;">删除</span>
</el-dropdown-item>
@ -134,6 +152,16 @@
<el-option v-for="item in sources" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="故障设备" required>
<el-select v-model="createForm.comm_uid" filterable placeholder="请选择故障设备">
<el-option
v-for="item in deviceOptions"
:key="item.commUid"
:label="item.ilcName"
:value="item.commUid"
/>
</el-select>
</el-form-item>
<el-form-item label="优先级" required>
<el-select v-model="createForm.priority" placeholder="请选择">
<el-option v-for="item in priorities" :key="item.value" :label="item.label" :value="item.value" />
@ -142,12 +170,9 @@
<el-form-item label="联系方式" required>
<el-input v-model="createForm.contact" />
</el-form-item>
<el-form-item label="预计完成时间">
<el-date-picker v-model="createForm.expected_completion_time" format="YYYY-MM-DD HH:mm:ss" type="datetime" style="width: 100%" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showCreateDialog = false">取消</el-button>
<el-button @click="() => { showCreateDialog = false; Object.assign(createForm, initialCreateForm()) }">取消</el-button>
<el-button type="primary" @click="handleCreate"></el-button>
</template>
</el-dialog>
@ -207,42 +232,64 @@
<el-skeleton v-if="viewLoading" rows="6" animated />
<div v-else>
<div v-if="currentTicket" class="ticket-detail">
<div class="detail-item">
<span class="label">工单标题</span>
<span class="value">{{ currentTicket.title }}</span>
</div>
<div class="detail-item">
<span class="label">工单描述</span>
<span class="value">{{ currentTicket.description }}</span>
</div>
<div class="detail-item">
<span class="label">工单来源</span>
<span class="value">{{ getSourceLabel(currentTicket.source) }}</span>
</div>
<div class="detail-item">
<span class="label">优先级</span>
<span class="value">{{ getPriorityLabel(currentTicket.priority) }}</span>
</div>
<div class="detail-item">
<span class="label">联系方式</span>
<span class="value">{{ currentTicket.contact }}</span>
</div>
<div class="detail-item">
<span class="label">预计完成时间</span>
<span class="value">{{ currentTicket.expected_completion_time }}</span>
</div>
<div class="detail-item">
<span class="label">工单状态</span>
<span class="value">{{ getStatusLabel(currentTicket.status) }}</span>
</div>
<div class="detail-item">
<span class="label">创建时间</span>
<span class="value">{{ currentTicket.created_at }}</span>
</div>
<div class="detail-item">
<span class="label">更新时间</span>
<span class="value">{{ currentTicket.updated_at }}</span>
<div class="detail-item"><span class="label">工单ID</span><span class="value">{{ currentTicket.id }}</span></div>
<div class="detail-item"><span class="label">工单标题</span><span class="value">{{ currentTicket.title }}</span></div>
<div class="detail-item"><span class="label">工单描述</span><span class="value">{{ currentTicket.description }}</span></div>
<div class="detail-item"><span class="label">工单来源</span><span class="value">{{ getSourceLabel(currentTicket.source) }}</span></div>
<div class="detail-item"><span class="label">故障设备</span><span class="value">
{{
(() => {
if (!currentTicket) return '-';
// comm_uid/commUid
const commUid = (currentTicket as any).comm_uid || (currentTicket as any).commUid;
if (!commUid) return '-';
return deviceOptions.find(item => item.commUid === commUid)?.ilcName || commUid || '-';
})()
}}
</span></div>
<div class="detail-item"><span class="label">优先级</span><span class="value">{{ getPriorityLabel(currentTicket.priority) }}</span></div>
<div class="detail-item"><span class="label">工单状态</span><span class="value">{{ getStatusLabel(currentTicket.status) }}</span></div>
<div class="detail-item"><span class="label">创建人姓名</span><span class="value">{{ currentTicket.created_name }}</span></div>
<div class="detail-item"><span class="label">联系方式</span><span class="value">
{{
(() => {
if (!currentTicket) return '-';
return (currentTicket as any).contact || (currentTicket as any).contact_info || '-';
})()
}}
</span></div>
<div class="detail-item"><span class="label">故障发生时间</span><span class="value">{{ currentTicket.fault_time }}</span></div>
<div class="detail-item"><span class="label">派单时间</span><span class="value">{{ currentTicket.dispatch_time }}</span></div>
<div class="detail-item"><span class="label">预计完成时间</span><span class="value">{{ currentTicket.expected_completion_time }}</span></div>
<div class="detail-item"><span class="label">处理人</span><span class="value">{{ currentTicket.handler }}</span></div>
<div class="detail-item"><span class="label">故障描述</span><span class="value">
{{
(() => {
if (!currentTicket) return '-';
const desc = (currentTicket as any).fault_description || (currentTicket as any).fault_description || '-';
return desc || '-';
})()
}}
</span></div>
<div class="detail-item"><span class="label">处理完成时间</span><span class="value">{{ getStatusLabel(currentTicket.status) === '已完成' ? currentTicket.updated_at : '-' }}</span></div>
<div class="detail-item"><span class="label">图片</span>
<span class="value">
<el-image
v-for="(img, idx) in currentTicket.images"
:key="idx"
:src="img"
style="width: 60px; margin-right: 8px;"
fit="cover"
:preview-src-list="currentTicket.images"
:initial-index="idx"
v-if="currentTicket.images && currentTicket.images.length"
/>
<span v-if="!currentTicket.images || !currentTicket.images.length">-</span>
</span>
</div>
</div>
</div>
</el-dialog>
@ -251,7 +298,22 @@
<el-dialog v-model="showDispatchDialog" title="派工" width="400px">
<el-form :model="dispatchForm" label-width="80px">
<el-form-item label="派工人">
<el-input v-model="dispatchForm.handler" />
<el-select v-model="dispatchForm.handler" filterable placeholder="请选择处理人">
<el-option
v-for="user in handlerOptions"
:key="user.username"
:label="user.nickname"
:value="user.username"
/>
</el-select>
</el-form-item>
<el-form-item label="优先级">
<el-select v-model="dispatchForm.priority" placeholder="请选择优先级">
<el-option v-for="item in priorities" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="预计完成时间" required>
<el-date-picker v-model="dispatchForm.expected_completion_time" format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss" type="datetime" style="width: 100%" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="dispatchForm.remark" type="textarea" />
@ -273,6 +335,34 @@
>
<fault-management-view />
</el-dialog>
<!-- 处理弹窗 -->
<el-dialog v-model="showProcessDialog" title="工单处理" width="500px">
<el-form :model="processForm" label-width="100px">
<el-form-item label="故障描述" required>
<el-input v-model="processForm.fault_description" type="textarea" placeholder="请输入故障描述" />
</el-form-item>
<el-form-item label="上传图片">
<el-upload
action="/filestransfer/upload"
list-type="picture-card"
:file-list="processForm.images"
:on-remove="handleRemoveImage"
:on-success="handleUploadSuccess"
:before-upload="beforeImageUpload"
accept="image/*"
:limit="5"
multiple
>
<i class="el-icon-plus"></i>
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showProcessDialog = false">取消</el-button>
<el-button type="primary" @click="submitProcess"></el-button>
</template>
</el-dialog>
</div>
</template>
@ -286,6 +376,8 @@ import type { TicketItem, TicketDetail } from '../types/ticket'
import FaultManagementView from './FaultManagementView.vue'
import { fetchTicketList } from '../api/ticket'
import { ArrowDown } from '@element-plus/icons-vue'
import dayjs from 'dayjs'
import rpc from '../utils/rpc'
const ticketStore = useTicketStore()
const userStore = useUserStore()
@ -298,21 +390,22 @@ const ticketList = ref<TicketItem[]>([])
//
const tabs = [
{ label: '我的工单', value: 'mine' },
{ label: '全部工单', value: 'all' },
{ label: '我的工单', value: 'my' }
]
const currentTab = ref('all')
const currentTab = ref('mine')
//
const statuses = [
{ label: '新建', value: 'new' },
{ label: '已审核', value: 'reviewed' },
// { label: '', value: 'reviewed' },
{ label: '已派发', value: 'dispatched' },
{ label: '已分配', value: 'assigned' },
{ label: '处理中', value: 'in_progress' },
// { label: '', value: 'assigned' },
// { label: '', value: 'in_progress' },
{ label: '已完成', value: 'completed' },
{ label: '已关闭', value: 'closed' },
{ label: '已驳回', value: 'rejected' }
// { label: '', value: 'closed' },
// { label: '', value: 'rejected' }
]
//
@ -324,9 +417,9 @@ const priorities = [
//
const sources = [
{ label: '市民举报', value: 'CITIZEN' },
{ label: '巡检发现', value: 'INSPECTION' },
{ label: '监控系统', value: 'MONITOR' }
{ label: '市民举报', value: '市民举报' },
{ label: '巡检发现', value: '巡检发现' },
{ label: '监控系统', value: '监控系统' }
]
interface Filters {
@ -358,19 +451,22 @@ const showHistory = ref(false)
const showFaultManagement = ref(false)
const viewLoading = ref(false)
const showDispatchDialog = ref(false)
const showProcessDialog = ref(false)
//
const currentTicket = ref<TicketDetail | null>(null)
//
const createForm = reactive({
const initialCreateForm = () => ({
title: '',
description: '',
source: '',
priority: '',
contact: '',
expected_completion_time: ''
expected_completion_time: '',
comm_uid: ''
})
const createForm = reactive(initialCreateForm())
//
const statusForm = ref({
@ -382,7 +478,16 @@ const statusForm = ref({
const dispatchForm = reactive({
handler: '',
remark: '',
ticketId: null as number | null
ticketId: null as number | null,
priority: '',
expected_completion_time: ''
})
//
const processForm = reactive({
id: null as number | null,
fault_description: '',
images: [] as any[]
})
//
@ -395,18 +500,18 @@ const fetchList = async () => {
and: {},
orderby: [{ created_at: 'desc' }]
}
if (filters.keyword) params.like.title = [filters.keyword]
if (filters.keyword) params.like.title = ['%' + filters.keyword + '%']
if (filters.status) params.and.status = [filters.status]
if (filters.priority) params.and.priority = [filters.priority]
if (filters.startTime && filters.endTime) {
params.where = [
{ field: 'created_at', operator: '>=', value: filters.startTime },
{ field: 'created_at', operator: '<=', value: filters.endTime }
{ field: 'fault_time', operator: '>=', value: dayjs(filters.startTime).format('YYYY-MM-DD HH:mm:ss') },
{ field: 'fault_time', operator: '<=', value: dayjs(filters.endTime).format('YYYY-MM-DD HH:mm:ss') }
]
}
//
if (currentTab.value === 'mine') {
params.and.handler = [userStore.nickname]
params.and.handler = [userStore.username]
}
const res: any = await fetchTicketList(params)
ticketList.value = res?.rows || []
@ -462,13 +567,14 @@ const handleCreate = async () => {
...createForm,
status: 'new',
creator_id: userStore.token || 1, // tokenID
fault_time: new Date().toISOString(),
fault_time: dayjs().format('YYYY-MM-DD HH:mm:ss'),
expected_completion_time: createForm.expected_completion_time
? new Date(createForm.expected_completion_time).toISOString()
? dayjs(createForm.expected_completion_time).format('YYYY-MM-DD HH:mm:ss')
: undefined
})
ElMessage.success('创建成功')
showCreateDialog.value = false
Object.assign(createForm, initialCreateForm())
fetchList()
} catch (e: any) {
console.error(e)
@ -584,20 +690,132 @@ const handleDispatch = (row: any) => {
dispatchForm.handler = ''
dispatchForm.remark = ''
dispatchForm.ticketId = row.id
dispatchForm.priority = row.priority
dispatchForm.expected_completion_time = row.expected_completion_time
showDispatchDialog.value = true
}
const submitDispatch = () => {
const submitDispatch = async () => {
if (!dispatchForm.handler) {
ElMessage.error('请填写派工人')
return
}
if (!dispatchForm.expected_completion_time) {
ElMessage.error('请填写预计完成时间')
return
}
await ticketStore.processTicket({
id: dispatchForm.ticketId,
operator_id: userStore.token,
handler: dispatchForm.handler,
expected_completion_time: dayjs(dispatchForm.expected_completion_time).format('YYYY-MM-DD HH:mm:ss'),
//
priority: dispatchForm.priority,
remark: dispatchForm.remark
})
// TODO: API
ElMessage.success(`工单${dispatchForm.ticketId}已派工给:${dispatchForm.handler}`)
showDispatchDialog.value = false
fetchList()
//
}
//
const handleProcess = (row: any) => {
processForm.id = row.id
processForm.fault_description = ''
processForm.images = []
showProcessDialog.value = true
}
const handleRemoveImage = (file: any, fileList: any[]) => {
processForm.images = fileList
}
const handleUploadSuccess = (response: any, file: any, fileList: any[]) => {
// url response.url
file.url = response.url || response?.result?.url || ''
processForm.images = fileList
}
const beforeImageUpload = (file: File) => {
const isImage = file.type.startsWith('image/')
if (!isImage) {
ElMessage.error('只能上传图片文件!')
}
return isImage
}
const submitProcess = async () => {
if (!processForm.fault_description) {
ElMessage.error('请输入故障描述')
return
}
// url
const imageUrls = processForm.images.map(f => f.url || f.response?.url).filter(Boolean)
await ticketStore.processTicket({
id: processForm.id,
operator_id: userStore.token,
fault_description: processForm.fault_description,
images: imageUrls
})
ElMessage.success('处理成功')
showProcessDialog.value = false
fetchList()
}
//
const canProcess = (status: string, row?: any) => {
//
const currentUser = userStore.username
const isMine = row && row.handler && row.handler === currentUser
return ['in_progress', 'dispatched', 'assigned'].includes(status) && isMine
}
const handlerOptions = ref<{username: string, nickname: string}[]>([])
const fetchHandlerOptions = async () => {
const res = await rpc.post('/account/list', { limit: -1 }) as any
handlerOptions.value = (res?.rows || []).map((item: any) => ({
username: item.username,
nickname: item.nickname || item.username
}))
}
const deviceOptions = ref<{ commUid: string, ilcName: string }[]>([])
const fetchDeviceOptions = async () => {
const res = await rpc.post('/lightm/ilc/list', { limit: -1 }) as any
deviceOptions.value = (res?.rows || []).map((item: any) => ({
commUid: item.commUid,
ilcName: item.ilcName || item.commUid
}))
}
//
onMounted(() => {
fetchList()
fetchHandlerOptions()
fetchDeviceOptions()
})
const getStatusType = (status: string) => {
switch (status) {
case 'new': return 'info';
case 'dispatched': return 'primary';
case 'assigned': return 'warning';
case 'in_progress': return 'warning';
case 'completed': return 'success';
case 'closed': return 'default';
case 'rejected': return 'danger';
default: return 'info';
}
}
</script>
<style scoped>
@ -1233,25 +1451,6 @@ input:checked + .slider:before {
z-index: 999;
}
/* 自定义滚动条 */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
}
::-webkit-scrollbar-track {
background: transparent;
}
.fault-management-dialog {
:deep(.el-dialog) {
margin: 0 !important;

@ -17,6 +17,14 @@ export default defineConfig({
target: 'https://lamp.ruixininfo.com',
changeOrigin: true
},
"/filestransfer": {
secure: false,
headers: {
Referer: 'https://lamp.ruixininfo.com'
},
target: 'https://lamp.ruixininfo.com',
changeOrigin: true
},
}
},
plugins: [vue()],

Loading…
Cancel
Save