feat
This commit is contained in:
26
src/api/common/fts.ts
Normal file
26
src/api/common/fts.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { AxiosProgressEvent } from "axios";
|
||||
import { request } from "@/api/request";
|
||||
|
||||
/** 上传文件 */
|
||||
const FtsUpload = (data: FormData, onUploadProgress?: (progress: number) => void) => {
|
||||
data.append('provider', 'local');
|
||||
data.append('bucket', 'visual');
|
||||
|
||||
return request.post(
|
||||
`/fts/v1/uploader`,
|
||||
data,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
onUploadProgress: onUploadProgress ? (progressEvent: AxiosProgressEvent) => {
|
||||
if (progressEvent.total) {
|
||||
const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
|
||||
onUploadProgress(percentCompleted);
|
||||
}
|
||||
} : undefined
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default FtsUpload;
|
||||
36
src/api/ops/alertHistory.ts
Normal file
36
src/api/ops/alertHistory.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { request } from "@/api/request";
|
||||
|
||||
/** 获取 告警记录列表 */
|
||||
export const fetchHistories = (data: {
|
||||
page?: number,
|
||||
page_size?: number,
|
||||
size?: number,
|
||||
policy_id?: number,
|
||||
rule_id?: number,
|
||||
status?: string,
|
||||
severity_id?: number,
|
||||
starts_at?: string,
|
||||
ends_at?: string,
|
||||
keyword?: string
|
||||
}) => {
|
||||
// 兼容 size 参数,转换为 page_size
|
||||
const params: any = { ...data };
|
||||
if (params.size !== undefined && params.page_size === undefined) {
|
||||
params.page_size = params.size;
|
||||
delete params.size;
|
||||
}
|
||||
return request.get("/Alert/v1/record/list", { params });
|
||||
};
|
||||
|
||||
/** 获取 告警记录详情 */
|
||||
export const fetchHistoryDetail = (id: number) => request.get(`/Alert/v1/record/get/${id}`);
|
||||
|
||||
/** 获取 告警统计 */
|
||||
export const fetchAlertStatistics = (data: {
|
||||
policy_id?: number,
|
||||
starts_at?: string,
|
||||
ends_at?: string
|
||||
}) => request.get("/Alert/v1/record/statistics", { params: data });
|
||||
|
||||
/** 获取 告警计数 */
|
||||
export const fetchAlertCount = () => request.get("/Alert/v1/record/count");
|
||||
53
src/api/ops/datacenter.ts
Normal file
53
src/api/ops/datacenter.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { request } from "@/api/request";
|
||||
|
||||
/** 获取数据中心列表(分页) */
|
||||
export const fetchDatacenterList = (data?: {
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
keyword?: string;
|
||||
province_id?: number;
|
||||
city_id?: number;
|
||||
status?: string;
|
||||
}) => {
|
||||
return request.post("/Assets/v1/datacenter/list", data || {});
|
||||
};
|
||||
|
||||
/** 获取数据中心详情 */
|
||||
export const fetchDatacenterDetail = (id: number) => {
|
||||
return request.get(`/Assets/v1/datacenter/detail/${id}`);
|
||||
};
|
||||
|
||||
/** 创建数据中心 */
|
||||
export const createDatacenter = (data: any) => {
|
||||
return request.post("/Assets/v1/datacenter/create", data);
|
||||
};
|
||||
|
||||
/** 更新数据中心 */
|
||||
export const updateDatacenter = (data: any) => {
|
||||
return request.put("/Assets/v1/datacenter/update", data);
|
||||
};
|
||||
|
||||
/** 删除数据中心 */
|
||||
export const deleteDatacenter = (id: number) => {
|
||||
return request.delete(`/Assets/v1/datacenter/delete/${id}`);
|
||||
};
|
||||
|
||||
/** 获取省份列表(用于下拉选择) */
|
||||
export const fetchProvinceList = () => {
|
||||
return request.get("/Assets/v1/province/all");
|
||||
};
|
||||
|
||||
/** 获取城市列表(用于下拉选择) */
|
||||
export const fetchCityList = () => {
|
||||
return request.get("/Assets/v1/city/all");
|
||||
};
|
||||
|
||||
/** 根据城市获取数据中心列表 */
|
||||
export const fetchDatacenterByCity = (cityId: number) => {
|
||||
return request.get(`/Assets/v1/datacenter/city/${cityId}`);
|
||||
};
|
||||
|
||||
/** 获取数据中心树形结构 */
|
||||
export const fetchDatacenterTree = () => {
|
||||
return request.get("/Assets/v1/datacenter/tree");
|
||||
};
|
||||
22
src/api/ops/dcControl.ts
Normal file
22
src/api/ops/dcControl.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { request } from "@/api/request";
|
||||
|
||||
/** 获取 采集器 */
|
||||
export const fetchCollectors = (data: { page: number, size: number, keyword?: string }) => request.get("/DC-Control/v1/collectors", { params: data });
|
||||
|
||||
/** 新增 采集器 */
|
||||
export const createCollector = (data: any) => request.post("/DC-Control/v1/collectors", data);
|
||||
|
||||
/** 删除 采集器 */
|
||||
export const deleteCollector = (id: number) => request.delete(`/DC-Control/v1/collectors/${id}`);
|
||||
|
||||
/** 获取 采集器详情 */
|
||||
export const fetchCollectorDetail = (id: number) => request.get(`/DC-Control/v1/collectors/${id}`);
|
||||
|
||||
/** 更新 采集器 */
|
||||
export const updateCollector = (data: any) => request.put(`/DC-Control/v1/collectors/${data.id}`, data);
|
||||
|
||||
/** 获取 采集器统计数据 */
|
||||
export const fetchCollectorStatistics = () => request.get("/DC-Control/v1/statistics");
|
||||
|
||||
/** 获取 许可证信息 */
|
||||
export const fetchLicenseInfo = () => request.get("/DC-Control/v1/license");
|
||||
76
src/api/ops/feedbackTicket.ts
Normal file
76
src/api/ops/feedbackTicket.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { request } from "@/api/request";
|
||||
|
||||
/** 获取 工单列表 */
|
||||
export const fetchFeedbackTickets = (data?: {
|
||||
page?: number,
|
||||
page_size?: number,
|
||||
size?: number,
|
||||
keyword?: string,
|
||||
type?: string,
|
||||
priority?: string,
|
||||
status?: string,
|
||||
creator_id?: number,
|
||||
assignee_id?: number
|
||||
}) => {
|
||||
// 兼容 size 参数,转换为 page_size
|
||||
const params: any = data ? { ...data } : {};
|
||||
if (params.size !== undefined && params.page_size === undefined) {
|
||||
params.page_size = params.size;
|
||||
delete params.size;
|
||||
}
|
||||
return request.get("/Feedback/v1/tickets", params ? { params } : undefined);
|
||||
};
|
||||
|
||||
/** 创建 工单 */
|
||||
export const createFeedbackTicket = (data: any) => request.post("/Feedback/v1/tickets", data);
|
||||
|
||||
/** 更新 工单 */
|
||||
export const updateFeedbackTicket = (id: number, data: any) => request.put(`/Feedback/v1/tickets/${id}`, data);
|
||||
|
||||
/** 删除 工单 */
|
||||
export const deleteFeedbackTicket = (id: number) => request.delete(`/Feedback/v1/tickets/${id}`);
|
||||
|
||||
/** 获取 工单详情 */
|
||||
export const fetchFeedbackTicketDetail = (id: number) => request.get(`/Feedback/v1/tickets/${id}`);
|
||||
|
||||
/** 根据工单编号获取详情 */
|
||||
export const fetchFeedbackTicketByNo = (no: string) => request.get(`/Feedback/v1/tickets/no/${no}`);
|
||||
|
||||
/** 接单 */
|
||||
export const acceptFeedbackTicket = (id: number) => request.post(`/Feedback/v1/tickets/${id}/accept`);
|
||||
|
||||
/** 转交 */
|
||||
export const transferFeedbackTicket = (id: number, data: { assignee_id: number, assignee_name: string, reason?: string }) => request.post(`/Feedback/v1/tickets/${id}/transfer`, data);
|
||||
|
||||
/** 撤回 */
|
||||
export const cancelFeedbackTicket = (id: number) => request.post(`/Feedback/v1/tickets/${id}/cancel`);
|
||||
|
||||
/** 挂起 */
|
||||
export const suspendFeedbackTicket = (id: number, data: { content: string }) => request.post(`/Feedback/v1/tickets/${id}/suspend`, data);
|
||||
|
||||
/** 重启 */
|
||||
export const resumeFeedbackTicket = (id: number) => request.post(`/Feedback/v1/tickets/${id}/resume`);
|
||||
|
||||
/** 解决 */
|
||||
export const resolveFeedbackTicket = (id: number, data: { content: string }) => request.post(`/Feedback/v1/tickets/${id}/resolve`, data);
|
||||
|
||||
/** 关闭 */
|
||||
export const closeFeedbackTicket = (id: number, data: { remarks?: string }) => request.post(`/Feedback/v1/tickets/${id}/close`, data);
|
||||
|
||||
/** 添加评论 */
|
||||
export const commentFeedbackTicket = (id: number, data: { content: string }) => request.post(`/Feedback/v1/tickets/${id}/comment`, data);
|
||||
|
||||
/** 获取操作日志 */
|
||||
export const fetchFeedbackTicketLogs = (id: number) => request.get(`/Feedback/v1/tickets/${id}/logs`);
|
||||
|
||||
/** 获取关联关系 */
|
||||
export const fetchFeedbackTicketRelations = (id: number) => request.get(`/Feedback/v1/tickets/${id}/relations`);
|
||||
|
||||
/** 创建关联 */
|
||||
export const createFeedbackTicketRelation = (id: number, data: { target_ticket_id: number, relation_type: string, description?: string }) => request.post(`/Feedback/v1/tickets/${id}/relations`, data);
|
||||
|
||||
/** 删除关联 */
|
||||
export const deleteFeedbackTicketRelation = (relationId: number) => request.delete(`/Feedback/v1/tickets/relations/${relationId}`);
|
||||
|
||||
/** 获取 工单统计数据 */
|
||||
export const fetchFeedbackTicketStatistics = () => request.get("/Feedback/v1/tickets/statistics");
|
||||
37
src/api/ops/floor.ts
Normal file
37
src/api/ops/floor.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { request } from "@/api/request";
|
||||
|
||||
/** 获取楼层列表(分页) */
|
||||
export const fetchFloorList = (data?: {
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
keyword?: string;
|
||||
datacenter_id?: number;
|
||||
status?: string;
|
||||
}) => {
|
||||
return request.post("/Assets/v1/floor/list", data || {});
|
||||
};
|
||||
|
||||
/** 获取楼层详情 */
|
||||
export const fetchFloorDetail = (id: number) => {
|
||||
return request.get(`/Assets/v1/floor/detail/${id}`);
|
||||
};
|
||||
|
||||
/** 创建楼层 */
|
||||
export const createFloor = (data: any) => {
|
||||
return request.post("/Assets/v1/floor/create", data);
|
||||
};
|
||||
|
||||
/** 更新楼层 */
|
||||
export const updateFloor = (data: any) => {
|
||||
return request.put("/Assets/v1/floor/update", data);
|
||||
};
|
||||
|
||||
/** 删除楼层 */
|
||||
export const deleteFloor = (id: number) => {
|
||||
return request.delete(`/Assets/v1/floor/delete/${id}`);
|
||||
};
|
||||
|
||||
/** 获取数据中心列表(用于下拉选择) */
|
||||
export const fetchDatacenterList = () => {
|
||||
return request.get("/Assets/v1/datacenter/all");
|
||||
};
|
||||
31
src/api/ops/rbac2.ts
Normal file
31
src/api/ops/rbac2.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { request } from '@/api/request'
|
||||
|
||||
/**
|
||||
* 用户列表项接口
|
||||
*/
|
||||
export interface UserItem {
|
||||
id: number;
|
||||
name: string;
|
||||
username?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
status?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户列表响应接口
|
||||
*/
|
||||
export interface UserListResponse {
|
||||
code: number;
|
||||
message: string;
|
||||
data: UserItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户列表
|
||||
*/
|
||||
export const fetchUserList = async (data: any) => {
|
||||
return request.post<UserListResponse>('/rbac2/v1/user/list', data);
|
||||
};
|
||||
@@ -57,7 +57,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { PropType, reactive, watch } from 'vue'
|
||||
import { PropType, reactive, watch, ref } from 'vue'
|
||||
import type { FormItem } from './types'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -91,6 +91,7 @@ const emit = defineEmits<{
|
||||
|
||||
// 使用本地响应式副本,避免直接修改 props
|
||||
const localModel = reactive<Record<string, any>>({})
|
||||
const isUpdating = ref(false)
|
||||
|
||||
// 初始化本地模型
|
||||
const initLocalModel = () => {
|
||||
@@ -98,10 +99,11 @@ const initLocalModel = () => {
|
||||
Object.assign(localModel, props.modelValue)
|
||||
}
|
||||
|
||||
// 监听外部值变化
|
||||
// 监听外部值变化(只在非本地更新时响应)
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
if (isUpdating.value) return
|
||||
Object.keys(localModel).forEach(key => delete localModel[key])
|
||||
Object.assign(localModel, val)
|
||||
},
|
||||
@@ -112,7 +114,12 @@ watch(
|
||||
watch(
|
||||
localModel,
|
||||
(val) => {
|
||||
isUpdating.value = true
|
||||
emit('update:modelValue', { ...val })
|
||||
// 使用 nextTick 避免循环更新
|
||||
setTimeout(() => {
|
||||
isUpdating.value = false
|
||||
}, 0)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
@@ -118,7 +118,7 @@ const props = defineProps({
|
||||
},
|
||||
showDownload: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
default: false,
|
||||
},
|
||||
showRefresh: {
|
||||
type: Boolean,
|
||||
@@ -166,6 +166,7 @@ const emit = defineEmits<{
|
||||
(e: 'column-change', columns: TableColumnData[]): void
|
||||
}>()
|
||||
|
||||
|
||||
// 计算需要插槽的列(动态插槽透传)
|
||||
const slotColumns = computed(() => {
|
||||
return props.columns.filter(col => col.slotName)
|
||||
|
||||
@@ -44,12 +44,10 @@ export function loadViewComponent(componentPath: string) {
|
||||
}
|
||||
// 构建完整的文件路径
|
||||
const filePath = `/src/views/${normalizedPath}.vue`
|
||||
console.log('filePath', filePath)
|
||||
|
||||
// 从预加载的模块中查找
|
||||
const modulePath = Object.keys(viewModules).find((path) => path.endsWith(filePath) || path === filePath)
|
||||
|
||||
console.log('modulePath', modulePath)
|
||||
|
||||
if (modulePath && viewModules[modulePath]) {
|
||||
return viewModules[modulePath]
|
||||
}
|
||||
@@ -98,7 +96,6 @@ export function transformMenuToRoutes(menuItems: ServerMenuItem[]): AppRouteReco
|
||||
// 传递父级的 component 和 path 给子路由处理函数
|
||||
route.children = transformChildRoutes(item.children, item.component, item.menu_path)
|
||||
} else if (item.component) {
|
||||
console.log('menu item component:', item.component)
|
||||
// 一级菜单没有 children 但有 component,创建一个空路径的子路由
|
||||
const routeName = route.name
|
||||
route.children = [
|
||||
@@ -171,7 +168,6 @@ function transformChildRoutes(
|
||||
return children.map((child) => {
|
||||
// 优先使用子菜单自己的 component,否则继承父级的 component
|
||||
const componentPath = child.component || parentComponent
|
||||
console.log('child component:', componentPath)
|
||||
// 计算子路由的相对路径
|
||||
const childFullPath = child.menu_path || child.path || ''
|
||||
const relativePath = extractRelativePath(childFullPath, parentPath || '')
|
||||
|
||||
@@ -64,6 +64,15 @@ const OPS: AppRouteRecordRaw = {
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'feedback/all',
|
||||
name: 'FeedbackAll',
|
||||
component: () => import('@/views/ops/pages/feedback/all/index.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
title="楼层详情"
|
||||
width="700px"
|
||||
@cancel="handleCancel"
|
||||
@update:visible="handleVisibleChange"
|
||||
:footer="false"
|
||||
>
|
||||
<a-spin :loading="loading" style="width: 100%">
|
||||
<a-descriptions :column="2" bordered v-if="floorDetail">
|
||||
<a-descriptions-item label="楼层名称" :span="2">
|
||||
{{ floorDetail.name }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="所属中心">
|
||||
{{ floorDetail.datacenter?.name || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="楼层号">
|
||||
{{ floorDetail.floor_number }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="面积">
|
||||
{{ floorDetail.area ? `${floorDetail.area} 平方米` : '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="楼高">
|
||||
{{ floorDetail.height ? `${floorDetail.height} 米` : '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="称重">
|
||||
{{ floorDetail.load_bearing ? `${floorDetail.load_bearing} kg/m²` : '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="可容纳机柜数">
|
||||
{{ floorDetail.max_rack_capacity || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="电力容量">
|
||||
{{ floorDetail.power_capacity ? `${floorDetail.power_capacity} KW` : '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="已用电力">
|
||||
{{ floorDetail.used_power ? `${floorDetail.used_power} KW` : '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="制冷容量">
|
||||
{{ floorDetail.cooling_capacity ? `${floorDetail.cooling_capacity} 吨` : '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="空调数量">
|
||||
{{ floorDetail.air_conditioners || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="机柜数量">
|
||||
{{ floorDetail.rack_count || 0 }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="已用机柜数">
|
||||
{{ floorDetail.used_rack_count || 0 }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="状态">
|
||||
<a-tag :color="statusMap[floorDetail.status]?.color || 'gray'">
|
||||
{{ statusMap[floorDetail.status]?.text || floorDetail.status }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="启用状态">
|
||||
<a-tag :color="floorDetail.enabled ? 'green' : 'red'">
|
||||
{{ floorDetail.enabled ? '启用' : '禁用' }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="楼层布局" :span="2">
|
||||
<a-image
|
||||
v-if="floorDetail.layout_plan"
|
||||
:src="floorDetail.layout_plan"
|
||||
width="200"
|
||||
fit="cover"
|
||||
style="cursor: pointer"
|
||||
@click="handlePreviewImage(floorDetail.layout_plan)"
|
||||
/>
|
||||
<span v-else>-</span>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="描述" :span="2">
|
||||
{{ floorDetail.description || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="创建时间">
|
||||
{{ formatDate(floorDetail.created_at) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="更新时间">
|
||||
{{ formatDate(floorDetail.updated_at) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="创建人">
|
||||
{{ floorDetail.created_by || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="更新人">
|
||||
{{ floorDetail.updated_by || '-' }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-spin>
|
||||
|
||||
<!-- 图片预览 -->
|
||||
<a-image-preview-group :visible="previewVisible" @update:visible="handlePreviewClose">
|
||||
<a-image :src="previewImage" />
|
||||
</a-image-preview-group>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { fetchFloorDetail } from '@/api/ops/floor'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
floorId?: number
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:visible', value: boolean): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const loading = ref(false)
|
||||
const floorDetail = ref<any>(null)
|
||||
const previewVisible = ref(false)
|
||||
const previewImage = ref('')
|
||||
|
||||
// 状态映射
|
||||
const statusMap: Record<string, { text: string; color: string }> = {
|
||||
planning: { text: '规划中', color: 'blue' },
|
||||
construction: { text: '建设中', color: 'orange' },
|
||||
operating: { text: '运营中', color: 'green' },
|
||||
maintenance: { text: '维护中', color: 'gold' },
|
||||
offline: { text: '已下线', color: 'red' },
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr: string) => {
|
||||
if (!dateStr) return '-'
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
}
|
||||
|
||||
// 加载楼层详情
|
||||
const loadFloorDetail = async () => {
|
||||
if (!props.floorId) return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await fetchFloorDetail(props.floorId)
|
||||
if (res.code === 0) {
|
||||
floorDetail.value = res.details
|
||||
} else {
|
||||
Message.error(res.message || '获取楼层详情失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取楼层详情失败:', error)
|
||||
Message.error('获取楼层详情失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 监听对话框显示状态
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newVal) => {
|
||||
if (newVal && props.floorId) {
|
||||
loadFloorDetail()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 预览图片
|
||||
const handlePreviewImage = (url: string) => {
|
||||
previewImage.value = url
|
||||
previewVisible.value = true
|
||||
}
|
||||
|
||||
// 关闭图片预览
|
||||
const handlePreviewClose = () => {
|
||||
previewVisible.value = false
|
||||
}
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
// 处理对话框可见性变化
|
||||
const handleVisibleChange = (visible: boolean) => {
|
||||
emit('update:visible', visible)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'FloorDetailDialog',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
// 样式可以根据需要添加
|
||||
</style>
|
||||
@@ -0,0 +1,373 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
:title="isEdit ? '编辑楼层' : '新建楼层'"
|
||||
width="700px"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
@update:visible="handleVisibleChange"
|
||||
:confirm-loading="submitting"
|
||||
>
|
||||
<a-form :model="form" layout="vertical" ref="formRef">
|
||||
<a-form-item
|
||||
label="楼层名称"
|
||||
field="name"
|
||||
:rules="[{ required: true, message: '请输入楼层名称' }]"
|
||||
>
|
||||
<a-input
|
||||
v-model="form.name"
|
||||
placeholder="请输入楼层名称"
|
||||
:max-length="200"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
label="所属中心"
|
||||
field="datacenter_id"
|
||||
:rules="[{ required: true, message: '请选择所属中心' }]"
|
||||
>
|
||||
<a-select
|
||||
v-model="form.datacenter_id"
|
||||
placeholder="请选择所属中心"
|
||||
:loading="loadingDatacenters"
|
||||
allow-search
|
||||
>
|
||||
<a-option
|
||||
v-for="item in datacenterList"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
>
|
||||
{{ item.name }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
label="楼层号"
|
||||
field="floor_number"
|
||||
:rules="[{ required: true, message: '请输入楼层号' }]"
|
||||
>
|
||||
<a-input-number
|
||||
v-model="form.floor_number"
|
||||
placeholder="请输入楼层号"
|
||||
:min="-10"
|
||||
:max="100"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
label="面积(平方米)"
|
||||
field="area"
|
||||
>
|
||||
<a-input-number
|
||||
v-model="form.area"
|
||||
placeholder="请输入面积"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
label="楼高(米)"
|
||||
field="height"
|
||||
>
|
||||
<a-input-number
|
||||
v-model="form.height"
|
||||
placeholder="请输入楼高"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
label="称重(kg/m²)"
|
||||
field="load_bearing"
|
||||
>
|
||||
<a-input-number
|
||||
v-model="form.load_bearing"
|
||||
placeholder="请输入称重"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
label="启用"
|
||||
field="enabled"
|
||||
>
|
||||
<a-switch v-model="form.enabled" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
label="楼层布局"
|
||||
field="layout_plan"
|
||||
>
|
||||
<a-upload
|
||||
:limit="1"
|
||||
:custom-request="handleUpload"
|
||||
@change="handleFileChange"
|
||||
:file-list="fileList"
|
||||
list-type="picture-card"
|
||||
accept="image/*"
|
||||
>
|
||||
<!-- <template #upload-button>
|
||||
<div class="upload-btn">
|
||||
<icon-plus />
|
||||
<div class="upload-text">上传图片</div>
|
||||
</div>
|
||||
</template> -->
|
||||
</a-upload>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
label="描述"
|
||||
field="description"
|
||||
>
|
||||
<a-textarea
|
||||
v-model="form.description"
|
||||
placeholder="请输入描述"
|
||||
:auto-size="{ minRows: 4, maxRows: 8 }"
|
||||
:max-length="500"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { IconPlus } from '@arco-design/web-vue/es/icon'
|
||||
import { createFloor, updateFloor } from '@/api/ops/floor'
|
||||
import { fetchDatacenterList } from '@/api/ops/floor'
|
||||
import FtsUpload from '@/api/common/fts'
|
||||
|
||||
interface Floor {
|
||||
id?: number
|
||||
name?: string
|
||||
datacenter_id?: number
|
||||
floor_number?: number
|
||||
area?: number
|
||||
height?: number
|
||||
load_bearing?: number
|
||||
enabled?: boolean
|
||||
layout_plan?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
floor: Floor | null
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:visible', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref()
|
||||
const loadingDatacenters = ref(false)
|
||||
const submitting = ref(false)
|
||||
const datacenterList = ref<any[]>([])
|
||||
const fileList = ref<any[]>([])
|
||||
|
||||
// 表单数据
|
||||
const form = ref({
|
||||
name: '',
|
||||
datacenter_id: undefined as number | undefined,
|
||||
floor_number: 1,
|
||||
area: undefined as number | undefined,
|
||||
height: undefined as number | undefined,
|
||||
load_bearing: undefined as number | undefined,
|
||||
enabled: true,
|
||||
layout_plan: '',
|
||||
description: '',
|
||||
})
|
||||
|
||||
// 是否为编辑模式
|
||||
const isEdit = computed(() => !!props.floor?.id)
|
||||
|
||||
// 加载数据中心列表
|
||||
const loadDatacenterList = async () => {
|
||||
loadingDatacenters.value = true
|
||||
try {
|
||||
const res: any = await fetchDatacenterList()
|
||||
if (res.code === 0) {
|
||||
datacenterList.value = res.details || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取数据中心列表失败:', error)
|
||||
} finally {
|
||||
loadingDatacenters.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理文件上传
|
||||
const handleUpload = async (option: any) => {
|
||||
try {
|
||||
const file = option.fileItem.file
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
// 使用fts接口上传文件
|
||||
const res: any = await FtsUpload(formData)
|
||||
|
||||
if (res.code === 0) {
|
||||
// 上传成功,设置文件URL
|
||||
form.value.layout_plan = res.details?.result_url || ''
|
||||
option.onSuccess(res)
|
||||
Message.success('上传成功')
|
||||
} else {
|
||||
option.onError(new Error('上传失败'))
|
||||
Message.error('上传失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('文件上传失败:', error)
|
||||
option.onError(error)
|
||||
Message.error('文件上传失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 处理文件变化
|
||||
const handleFileChange = (files: any[]) => {
|
||||
fileList.value = files
|
||||
}
|
||||
|
||||
// 监听对话框显示状态
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
if (props.floor && isEdit.value) {
|
||||
// 编辑模式:填充表单
|
||||
form.value = {
|
||||
name: props.floor.name || '',
|
||||
datacenter_id: props.floor.datacenter_id,
|
||||
floor_number: props.floor.floor_number || 1,
|
||||
area: props.floor.area,
|
||||
height: props.floor.height,
|
||||
load_bearing: props.floor.load_bearing,
|
||||
enabled: props.floor.enabled !== undefined ? props.floor.enabled : true,
|
||||
layout_plan: props.floor.layout_plan || '',
|
||||
description: props.floor.description || '',
|
||||
}
|
||||
// 如果有布局图,设置文件列表
|
||||
if (props.floor.layout_plan) {
|
||||
fileList.value = [
|
||||
{
|
||||
uid: '-1',
|
||||
name: 'layout_plan',
|
||||
url: props.floor.layout_plan,
|
||||
},
|
||||
]
|
||||
} else {
|
||||
fileList.value = []
|
||||
}
|
||||
} else {
|
||||
// 新建模式:重置表单
|
||||
form.value = {
|
||||
name: '',
|
||||
datacenter_id: undefined,
|
||||
floor_number: 1,
|
||||
area: undefined,
|
||||
height: undefined,
|
||||
load_bearing: undefined,
|
||||
enabled: true,
|
||||
layout_plan: '',
|
||||
description: '',
|
||||
}
|
||||
fileList.value = []
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 确认提交
|
||||
const handleOk = async () => {
|
||||
const valid = await formRef.value?.validate()
|
||||
if (valid) return
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
const data: any = {
|
||||
name: form.value.name,
|
||||
datacenter_id: form.value.datacenter_id,
|
||||
floor_number: form.value.floor_number,
|
||||
area: form.value.area,
|
||||
height: form.value.height,
|
||||
load_bearing: form.value.load_bearing,
|
||||
enabled: form.value.enabled,
|
||||
layout_plan: form.value.layout_plan,
|
||||
description: form.value.description,
|
||||
}
|
||||
|
||||
let res
|
||||
if (isEdit.value && props.floor?.id) {
|
||||
// 编辑楼层
|
||||
data.id = props.floor.id
|
||||
res = await updateFloor(data)
|
||||
} else {
|
||||
// 新建楼层
|
||||
res = await createFloor(data)
|
||||
}
|
||||
|
||||
if (res.code === 0) {
|
||||
Message.success(isEdit.value ? '编辑成功' : '创建成功')
|
||||
emit('success')
|
||||
emit('update:visible', false)
|
||||
} else {
|
||||
Message.error(res.message || (isEdit.value ? '编辑失败' : '创建失败'))
|
||||
}
|
||||
} catch (error) {
|
||||
Message.error(isEdit.value ? '编辑失败' : '创建失败')
|
||||
console.error(error)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
// 处理对话框可见性变化
|
||||
const handleVisibleChange = (visible: boolean) => {
|
||||
emit('update:visible', visible)
|
||||
}
|
||||
|
||||
// 组件挂载时加载数据中心列表
|
||||
onMounted(() => {
|
||||
loadDatacenterList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'FloorFormDialog',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.upload-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
</style>
|
||||
46
src/views/ops/pages/datacenter/floor/config/columns.ts
Normal file
46
src/views/ops/pages/datacenter/floor/config/columns.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||
|
||||
export const columns: TableColumnData[] = [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'index',
|
||||
slotName: 'index',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '楼层名称',
|
||||
dataIndex: 'name',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '所属中心',
|
||||
dataIndex: 'datacenter',
|
||||
slotName: 'datacenter',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '楼层号',
|
||||
dataIndex: 'floor_number',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'description',
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
slotName: 'status',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'actions',
|
||||
slotName: 'actions',
|
||||
width: 240,
|
||||
fixed: 'right' as const,
|
||||
},
|
||||
]
|
||||
23
src/views/ops/pages/datacenter/floor/config/search-form.ts
Normal file
23
src/views/ops/pages/datacenter/floor/config/search-form.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
|
||||
export const searchFormConfig: FormItem[] = [
|
||||
{
|
||||
field: 'keyword',
|
||||
label: '关键词',
|
||||
type: 'input',
|
||||
placeholder: '请输入楼层名称或编码',
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
label: '状态',
|
||||
type: 'select',
|
||||
placeholder: '请选择状态',
|
||||
options: [
|
||||
{ label: '规划中', value: 'planning' },
|
||||
{ label: '建设中', value: 'construction' },
|
||||
{ label: '运营中', value: 'operating' },
|
||||
{ label: '维护中', value: 'maintenance' },
|
||||
{ label: '已下线', value: 'offline' },
|
||||
],
|
||||
},
|
||||
]
|
||||
247
src/views/ops/pages/datacenter/floor/index.vue
Normal file
247
src/views/ops/pages/datacenter/floor/index.vue
Normal file
@@ -0,0 +1,247 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<search-table
|
||||
:form-model="formModel"
|
||||
:form-items="formItems"
|
||||
:data="tableData"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
title="楼层管理"
|
||||
search-button-text="查询"
|
||||
reset-button-text="重置"
|
||||
@update:form-model="handleFormModelUpdate"
|
||||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
@page-change="handlePageChange"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #toolbar-left>
|
||||
<a-button type="primary" @click="handleCreate">
|
||||
<template #icon>
|
||||
<icon-plus />
|
||||
</template>
|
||||
新建楼层
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<!-- 序号 -->
|
||||
<template #index="{ rowIndex }">
|
||||
{{ rowIndex + 1 + (pagination.current - 1) * pagination.pageSize }}
|
||||
</template>
|
||||
|
||||
<!-- 所属中心 -->
|
||||
<template #datacenter="{ record }">
|
||||
{{ record.datacenter?.name || '-' }}
|
||||
</template>
|
||||
|
||||
<!-- 状态 -->
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="statusMap[record.status]?.color || 'gray'">
|
||||
{{ statusMap[record.status]?.text || record.status }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 操作 -->
|
||||
<template #actions="{ record }">
|
||||
<a-button type="text" size="small" @click="handleDetail(record)">
|
||||
详情
|
||||
</a-button>
|
||||
<a-button type="text" size="small" @click="handleEdit(record)">
|
||||
编辑
|
||||
</a-button>
|
||||
<a-button type="text" size="small" status="danger" @click="handleDelete(record)">
|
||||
删除
|
||||
</a-button>
|
||||
</template>
|
||||
</search-table>
|
||||
|
||||
<!-- 楼层表单对话框(新建/编辑) -->
|
||||
<floor-form-dialog
|
||||
v-model:visible="formVisible"
|
||||
:floor="editingFloor"
|
||||
@success="handleFormSuccess"
|
||||
/>
|
||||
|
||||
<!-- 楼层详情对话框 -->
|
||||
<floor-detail-dialog
|
||||
v-model:visible="detailVisible"
|
||||
:floor-id="currentFloorId"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import { IconPlus } from '@arco-design/web-vue/es/icon'
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
import SearchTable from '@/components/search-table/index.vue'
|
||||
import { searchFormConfig } from './config/search-form'
|
||||
import { columns as columnsConfig } from './config/columns'
|
||||
import {
|
||||
fetchFloorList,
|
||||
deleteFloor,
|
||||
} from '@/api/ops/floor'
|
||||
import FloorDetailDialog from './components/FloorDetailDialog.vue'
|
||||
import FloorFormDialog from './components/FloorFormDialog.vue'
|
||||
|
||||
// 状态映射
|
||||
const statusMap: Record<string, { text: string; color: string }> = {
|
||||
planning: { text: '规划中', color: 'blue' },
|
||||
construction: { text: '建设中', color: 'orange' },
|
||||
operating: { text: '运营中', color: 'green' },
|
||||
maintenance: { text: '维护中', color: 'gold' },
|
||||
offline: { text: '已下线', color: 'red' },
|
||||
}
|
||||
|
||||
// 状态管理
|
||||
const loading = ref(false)
|
||||
const tableData = ref<any[]>([])
|
||||
const formModel = ref({
|
||||
keyword: '',
|
||||
status: '',
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
})
|
||||
|
||||
// 表单项配置
|
||||
const formItems = computed<FormItem[]>(() => searchFormConfig)
|
||||
|
||||
// 表格列配置
|
||||
const columns = computed(() => columnsConfig)
|
||||
|
||||
// 当前选中的楼层
|
||||
const currentFloorId = ref<number | undefined>(undefined)
|
||||
const editingFloor = ref<any>(null)
|
||||
|
||||
// 对话框可见性
|
||||
const formVisible = ref(false)
|
||||
const detailVisible = ref(false)
|
||||
|
||||
// 获取楼层列表
|
||||
const fetchFloors = async () => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const params: any = {
|
||||
page: pagination.current,
|
||||
page_size: pagination.pageSize,
|
||||
keyword: formModel.value.keyword || undefined,
|
||||
status: formModel.value.status || undefined,
|
||||
}
|
||||
|
||||
const res = await fetchFloorList(params)
|
||||
|
||||
tableData.value = res.details?.data || []
|
||||
pagination.total = res.details?.total || 0
|
||||
} catch (error) {
|
||||
console.error('获取楼层列表失败:', error)
|
||||
Message.error('获取楼层列表失败')
|
||||
tableData.value = []
|
||||
pagination.total = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchFloors()
|
||||
}
|
||||
|
||||
// 处理表单模型更新
|
||||
const handleFormModelUpdate = (value: any) => {
|
||||
formModel.value = value
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
formModel.value = {
|
||||
keyword: '',
|
||||
status: '',
|
||||
}
|
||||
pagination.current = 1
|
||||
fetchFloors()
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (current: number) => {
|
||||
pagination.current = current
|
||||
fetchFloors()
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const handleRefresh = () => {
|
||||
fetchFloors()
|
||||
Message.success('数据已刷新')
|
||||
}
|
||||
|
||||
// 新建楼层
|
||||
const handleCreate = () => {
|
||||
editingFloor.value = null
|
||||
formVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑楼层
|
||||
const handleEdit = (record: any) => {
|
||||
console.log('编辑楼层:', record)
|
||||
editingFloor.value = record
|
||||
formVisible.value = true
|
||||
}
|
||||
|
||||
// 详情
|
||||
const handleDetail = (record: any) => {
|
||||
console.log('查看详情:', record)
|
||||
currentFloorId.value = record.id
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
// 删除楼层
|
||||
const handleDelete = async (record: any) => {
|
||||
console.log('删除楼层:', record)
|
||||
try {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确认删除楼层 ${record.name} 吗?`,
|
||||
onOk: async () => {
|
||||
const res = await deleteFloor(record.id)
|
||||
if (res.code === 0) {
|
||||
Message.success('删除成功')
|
||||
fetchFloors()
|
||||
} else {
|
||||
Message.error(res.message || '删除失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('删除楼层失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 表单成功回调
|
||||
const handleFormSuccess = () => {
|
||||
formVisible.value = false
|
||||
fetchFloors()
|
||||
}
|
||||
|
||||
// 初始化加载数据
|
||||
fetchFloors()
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'DataCenterFloor',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,216 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
title="数据中心详情"
|
||||
width="900px"
|
||||
@cancel="handleCancel"
|
||||
@update:visible="handleVisibleChange"
|
||||
:footer="false"
|
||||
>
|
||||
<a-spin :loading="loading" style="width: 100%">
|
||||
<a-descriptions :column="2" bordered v-if="datacenterDetail">
|
||||
<!-- 基本信息 -->
|
||||
<a-descriptions-item label="数据中心名称" :span="2">
|
||||
{{ datacenterDetail.name }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="数据中心编码">
|
||||
{{ datacenterDetail.code }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="简称">
|
||||
{{ datacenterDetail.short_name || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="状态">
|
||||
<a-tag :color="statusMap[datacenterDetail.status]?.color || 'gray'">
|
||||
{{ statusMap[datacenterDetail.status]?.text || datacenterDetail.status }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="是否启用">
|
||||
<a-tag :color="datacenterDetail.enabled ? 'green' : 'red'">
|
||||
{{ datacenterDetail.enabled ? '是' : '否' }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
|
||||
<!-- 联系信息 -->
|
||||
<a-descriptions-item label="联系人">
|
||||
{{ datacenterDetail.contact_person || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="联系电话">
|
||||
{{ datacenterDetail.contact_phone || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="联系邮箱" :span="2">
|
||||
{{ datacenterDetail.contact_email || '-' }}
|
||||
</a-descriptions-item>
|
||||
|
||||
<!-- 地理信息 -->
|
||||
<a-descriptions-item label="所属省份">
|
||||
{{ datacenterDetail.province?.name || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="所属城市">
|
||||
{{ datacenterDetail.city?.name || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="详细地址" :span="2">
|
||||
{{ datacenterDetail.address || '-' }}
|
||||
</a-descriptions-item>
|
||||
|
||||
<!-- 面积信息 -->
|
||||
<a-descriptions-item label="总面积">
|
||||
{{ datacenterDetail.total_area ? `${datacenterDetail.total_area} 平方米` : '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="IT区域面积">
|
||||
{{ datacenterDetail.it_area ? `${datacenterDetail.it_area} 平方米` : '-' }}
|
||||
</a-descriptions-item>
|
||||
|
||||
<!-- 电力信息 -->
|
||||
<a-descriptions-item label="总电力">
|
||||
{{ datacenterDetail.total_power ? `${datacenterDetail.total_power} KW` : '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="已用电力">
|
||||
{{ datacenterDetail.used_power ? `${datacenterDetail.used_power} KW` : '-' }}
|
||||
</a-descriptions-item>
|
||||
|
||||
<!-- 容量信息 -->
|
||||
<a-descriptions-item label="可容纳机柜数">
|
||||
{{ datacenterDetail.max_rack_capacity || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="实际机柜数">
|
||||
{{ datacenterDetail.rack_count || 0 }}
|
||||
</a-descriptions-item>
|
||||
|
||||
<!-- 认证信息 -->
|
||||
<a-descriptions-item label="ISO27001认证">
|
||||
<a-tag :color="datacenterDetail.iso27001 ? 'green' : 'gray'">
|
||||
{{ datacenterDetail.iso27001 ? '是' : '否' }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="ISO9001认证">
|
||||
<a-tag :color="datacenterDetail.iso9001 ? 'green' : 'gray'">
|
||||
{{ datacenterDetail.iso9001 ? '是' : '否' }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<a-descriptions-item label="楼层数">
|
||||
{{ datacenterDetail.floor_count || 0 }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="机柜数">
|
||||
{{ datacenterDetail.rack_count || 0 }}
|
||||
</a-descriptions-item>
|
||||
|
||||
<!-- 其他信息 -->
|
||||
<a-descriptions-item label="描述" :span="2">
|
||||
{{ datacenterDetail.description || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="备注" :span="2">
|
||||
{{ datacenterDetail.remarks || '-' }}
|
||||
</a-descriptions-item>
|
||||
|
||||
<!-- 系统信息 -->
|
||||
<a-descriptions-item label="创建时间">
|
||||
{{ formatDate(datacenterDetail.created_at) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="更新时间">
|
||||
{{ formatDate(datacenterDetail.updated_at) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="创建人">
|
||||
{{ datacenterDetail.created_by || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="更新人">
|
||||
{{ datacenterDetail.updated_by || '-' }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { fetchDatacenterDetail } from '@/api/ops/datacenter'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
datacenterId?: number
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:visible', value: boolean): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const loading = ref(false)
|
||||
const datacenterDetail = ref<any>(null)
|
||||
|
||||
// 状态映射
|
||||
const statusMap: Record<string, { text: string; color: string }> = {
|
||||
planning: { text: '规划中', color: 'blue' },
|
||||
construction: { text: '建设中', color: 'orange' },
|
||||
operating: { text: '运营中', color: 'green' },
|
||||
maintenance: { text: '维护中', color: 'gold' },
|
||||
offline: { text: '已下线', color: 'red' },
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr: string) => {
|
||||
if (!dateStr) return '-'
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
}
|
||||
|
||||
// 加载数据中心详情
|
||||
const loadDatacenterDetail = async () => {
|
||||
if (!props.datacenterId) return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchDatacenterDetail(props.datacenterId)
|
||||
if (res.code === 0) {
|
||||
datacenterDetail.value = res.details
|
||||
} else {
|
||||
Message.error(res.message || '获取数据中心详情失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取数据中心详情失败:', error)
|
||||
Message.error('获取数据中心详情失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 监听对话框显示状态
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newVal) => {
|
||||
if (newVal && props.datacenterId) {
|
||||
loadDatacenterDetail()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
// 处理对话框可见性变化
|
||||
const handleVisibleChange = (visible: boolean) => {
|
||||
emit('update:visible', visible)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'DatacenterDetailDialog',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
// 样式可以根据需要添加
|
||||
</style>
|
||||
@@ -0,0 +1,461 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
:title="isEdit ? '编辑数据中心' : '新建数据中心'"
|
||||
width="800px"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
@update:visible="handleVisibleChange"
|
||||
:confirm-loading="submitting"
|
||||
>
|
||||
<a-form :model="form" layout="vertical" ref="formRef">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="数据中心名称"
|
||||
field="name"
|
||||
:rules="[{ required: true, message: '请输入数据中心名称' }]"
|
||||
>
|
||||
<a-input
|
||||
v-model="form.name"
|
||||
placeholder="请输入数据中心名称"
|
||||
:max-length="200"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="数据中心编码"
|
||||
field="code"
|
||||
:rules="[{ required: true, message: '请输入数据中心编码' }]"
|
||||
>
|
||||
<a-input
|
||||
v-model="form.code"
|
||||
placeholder="请输入数据中心编码"
|
||||
:max-length="100"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="简称" field="short_name">
|
||||
<a-input
|
||||
v-model="form.short_name"
|
||||
placeholder="请输入简称"
|
||||
:max-length="50"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="状态" field="status">
|
||||
<a-select
|
||||
v-model="form.status"
|
||||
placeholder="请选择状态"
|
||||
>
|
||||
<a-option value="planning">规划中</a-option>
|
||||
<a-option value="construction">建设中</a-option>
|
||||
<a-option value="operating">运营中</a-option>
|
||||
<a-option value="maintenance">维护中</a-option>
|
||||
<a-option value="offline">已下线</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="联系人" field="contact_person">
|
||||
<a-input
|
||||
v-model="form.contact_person"
|
||||
placeholder="请输入联系人"
|
||||
:max-length="50"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="联系电话" field="contact_phone">
|
||||
<a-input
|
||||
v-model="form.contact_phone"
|
||||
placeholder="请输入联系电话"
|
||||
:max-length="20"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="联系邮箱" field="contact_email">
|
||||
<a-input
|
||||
v-model="form.contact_email"
|
||||
placeholder="请输入联系邮箱"
|
||||
:max-length="100"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="所属省份"
|
||||
field="province_id"
|
||||
:rules="[{ required: true, message: '请选择所属省份' }]"
|
||||
>
|
||||
<a-select
|
||||
v-model="form.province_id"
|
||||
placeholder="请选择省份"
|
||||
@change="handleProvinceChange"
|
||||
>
|
||||
<a-option
|
||||
v-for="item in provinces"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
>
|
||||
{{ item.name }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="所属城市"
|
||||
field="city_id"
|
||||
:rules="[{ required: true, message: '请选择所属城市' }]"
|
||||
>
|
||||
<a-select
|
||||
v-model="form.city_id"
|
||||
placeholder="请选择城市"
|
||||
:disabled="!form.province_id"
|
||||
>
|
||||
<a-option
|
||||
v-for="item in filteredCities"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
>
|
||||
{{ item.name }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="详细地址" field="address">
|
||||
<a-input
|
||||
v-model="form.address"
|
||||
placeholder="请输入详细地址"
|
||||
:max-length="200"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="总面积(平方米)" field="total_area">
|
||||
<a-input-number
|
||||
v-model="form.total_area"
|
||||
placeholder="请输入总面积"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="IT区域面积(平方米)" field="it_area">
|
||||
<a-input-number
|
||||
v-model="form.it_area"
|
||||
placeholder="请输入IT区域面积"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="总电力(KW)" field="total_power">
|
||||
<a-input-number
|
||||
v-model="form.total_power"
|
||||
placeholder="请输入总电力"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="已用电力(KW)" field="used_power">
|
||||
<a-input-number
|
||||
v-model="form.used_power"
|
||||
placeholder="请输入已用电力"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="可容纳机柜数" field="max_rack_capacity">
|
||||
<a-input-number
|
||||
v-model="form.max_rack_capacity"
|
||||
placeholder="请输入可容纳机柜数"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="是否通过ISO27001认证" field="iso27001">
|
||||
<a-switch v-model="form.iso27001" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="是否通过ISO9001认证" field="iso9001">
|
||||
<a-switch v-model="form.iso9001" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="是否启用" field="enabled">
|
||||
<a-switch v-model="form.enabled" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="描述" field="description">
|
||||
<a-textarea
|
||||
v-model="form.description"
|
||||
placeholder="请输入描述"
|
||||
:auto-size="{ minRows: 3, maxRows: 6 }"
|
||||
:max-length="500"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="备注" field="remarks">
|
||||
<a-textarea
|
||||
v-model="form.remarks"
|
||||
placeholder="请输入备注"
|
||||
:auto-size="{ minRows: 3, maxRows: 6 }"
|
||||
:max-length="500"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { createDatacenter, updateDatacenter } from '@/api/ops/datacenter'
|
||||
|
||||
interface Datacenter {
|
||||
id?: number
|
||||
name?: string
|
||||
code?: string
|
||||
short_name?: string
|
||||
contact_person?: string
|
||||
contact_phone?: string
|
||||
contact_email?: string
|
||||
status?: string
|
||||
province_id?: number
|
||||
city_id?: number
|
||||
address?: string
|
||||
total_area?: number
|
||||
it_area?: number
|
||||
total_power?: number
|
||||
used_power?: number
|
||||
max_rack_capacity?: number
|
||||
iso27001?: boolean
|
||||
iso9001?: boolean
|
||||
enabled?: boolean
|
||||
description?: string
|
||||
remarks?: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
datacenter: Datacenter | null
|
||||
provinces: any[]
|
||||
cities: any[]
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:visible', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref()
|
||||
const submitting = ref(false)
|
||||
|
||||
// 表单数据
|
||||
const form = ref({
|
||||
name: '',
|
||||
code: '',
|
||||
short_name: '',
|
||||
contact_person: '',
|
||||
contact_phone: '',
|
||||
contact_email: '',
|
||||
status: 'planning',
|
||||
province_id: undefined as number | undefined,
|
||||
city_id: undefined as number | undefined,
|
||||
address: '',
|
||||
total_area: undefined as number | undefined,
|
||||
it_area: undefined as number | undefined,
|
||||
total_power: undefined as number | undefined,
|
||||
used_power: undefined as number | undefined,
|
||||
max_rack_capacity: undefined as number | undefined,
|
||||
iso27001: false,
|
||||
iso9001: false,
|
||||
enabled: true,
|
||||
description: '',
|
||||
remarks: '',
|
||||
})
|
||||
|
||||
// 是否为编辑模式
|
||||
const isEdit = computed(() => !!props.datacenter?.id)
|
||||
|
||||
// 根据选择的省份过滤城市列表
|
||||
const filteredCities = computed(() => {
|
||||
if (!form.value.province_id) return []
|
||||
return props.cities.filter((city) => city.province_id === form.value.province_id)
|
||||
})
|
||||
|
||||
// 省份变化时清空城市选择
|
||||
const handleProvinceChange = () => {
|
||||
form.value.city_id = undefined
|
||||
}
|
||||
|
||||
// 监听对话框显示状态
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
if (props.datacenter && isEdit.value) {
|
||||
// 编辑模式:填充表单
|
||||
form.value = {
|
||||
name: props.datacenter.name || '',
|
||||
code: props.datacenter.code || '',
|
||||
short_name: props.datacenter.short_name || '',
|
||||
contact_person: props.datacenter.contact_person || '',
|
||||
contact_phone: props.datacenter.contact_phone || '',
|
||||
contact_email: props.datacenter.contact_email || '',
|
||||
status: props.datacenter.status || 'planning',
|
||||
province_id: props.datacenter.province_id,
|
||||
city_id: props.datacenter.city_id,
|
||||
address: props.datacenter.address || '',
|
||||
total_area: props.datacenter.total_area,
|
||||
it_area: props.datacenter.it_area,
|
||||
total_power: props.datacenter.total_power,
|
||||
used_power: props.datacenter.used_power,
|
||||
max_rack_capacity: props.datacenter.max_rack_capacity,
|
||||
iso27001: props.datacenter.iso27001 !== undefined ? props.datacenter.iso27001 : false,
|
||||
iso9001: props.datacenter.iso9001 !== undefined ? props.datacenter.iso9001 : false,
|
||||
enabled: props.datacenter.enabled !== undefined ? props.datacenter.enabled : true,
|
||||
description: props.datacenter.description || '',
|
||||
remarks: props.datacenter.remarks || '',
|
||||
}
|
||||
} else {
|
||||
// 新建模式:重置表单
|
||||
form.value = {
|
||||
name: '',
|
||||
code: '',
|
||||
short_name: '',
|
||||
contact_person: '',
|
||||
contact_phone: '',
|
||||
contact_email: '',
|
||||
status: 'planning',
|
||||
province_id: undefined,
|
||||
city_id: undefined,
|
||||
address: '',
|
||||
total_area: undefined,
|
||||
it_area: undefined,
|
||||
total_power: undefined,
|
||||
used_power: undefined,
|
||||
max_rack_capacity: undefined,
|
||||
iso27001: false,
|
||||
iso9001: false,
|
||||
enabled: true,
|
||||
description: '',
|
||||
remarks: '',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 确认提交
|
||||
const handleOk = async () => {
|
||||
const valid = await formRef.value?.validate()
|
||||
if (valid) return
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
const data: any = {
|
||||
name: form.value.name,
|
||||
code: form.value.code,
|
||||
short_name: form.value.short_name,
|
||||
contact_person: form.value.contact_person,
|
||||
contact_phone: form.value.contact_phone,
|
||||
contact_email: form.value.contact_email,
|
||||
status: form.value.status,
|
||||
province_id: form.value.province_id,
|
||||
city_id: form.value.city_id,
|
||||
address: form.value.address,
|
||||
total_area: form.value.total_area,
|
||||
it_area: form.value.it_area,
|
||||
total_power: form.value.total_power,
|
||||
used_power: form.value.used_power,
|
||||
max_rack_capacity: form.value.max_rack_capacity,
|
||||
iso27001: form.value.iso27001,
|
||||
iso9001: form.value.iso9001,
|
||||
enabled: form.value.enabled,
|
||||
description: form.value.description,
|
||||
remarks: form.value.remarks,
|
||||
}
|
||||
|
||||
let res
|
||||
if (isEdit.value && props.datacenter?.id) {
|
||||
// 编辑数据中心
|
||||
data.id = props.datacenter.id
|
||||
res = await updateDatacenter(data)
|
||||
} else {
|
||||
// 新建数据中心
|
||||
res = await createDatacenter(data)
|
||||
}
|
||||
|
||||
if (res.code === 0) {
|
||||
Message.success(isEdit.value ? '编辑成功' : '创建成功')
|
||||
emit('success')
|
||||
emit('update:visible', false)
|
||||
} else {
|
||||
Message.error(res.message || (isEdit.value ? '编辑失败' : '创建失败'))
|
||||
}
|
||||
} catch (error) {
|
||||
Message.error(isEdit.value ? '编辑失败' : '创建失败')
|
||||
console.error(error)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
// 处理对话框可见性变化
|
||||
const handleVisibleChange = (visible: boolean) => {
|
||||
emit('update:visible', visible)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'DatacenterFormDialog',
|
||||
}
|
||||
</script>
|
||||
134
src/views/ops/pages/datacenter/house/config/columns.ts
Normal file
134
src/views/ops/pages/datacenter/house/config/columns.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||
|
||||
export const columns: TableColumnData[] = [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'index',
|
||||
slotName: 'index',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '数据中心名称',
|
||||
dataIndex: 'name',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '数据中心编码',
|
||||
dataIndex: 'code',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '简称',
|
||||
dataIndex: 'short_name',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
slotName: 'status',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '是否启用',
|
||||
dataIndex: 'enabled',
|
||||
slotName: 'enabled',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '所属省份',
|
||||
dataIndex: 'province',
|
||||
slotName: 'province',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '所属城市',
|
||||
dataIndex: 'city',
|
||||
slotName: 'city',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '联系人',
|
||||
dataIndex: 'contact_person',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '联系电话',
|
||||
dataIndex: 'contact_phone',
|
||||
width: 130,
|
||||
},
|
||||
{
|
||||
title: '详细地址',
|
||||
dataIndex: 'address',
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: '总面积(㎡)',
|
||||
dataIndex: 'total_area',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: 'IT区域面积(㎡)',
|
||||
dataIndex: 'it_area',
|
||||
width: 140,
|
||||
},
|
||||
{
|
||||
title: '总电力(KW)',
|
||||
dataIndex: 'total_power',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '已用电力(KW)',
|
||||
dataIndex: 'used_power',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '可容纳机柜数',
|
||||
dataIndex: 'max_rack_capacity',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: 'ISO27001',
|
||||
dataIndex: 'iso27001',
|
||||
slotName: 'iso27001',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: 'ISO9001',
|
||||
dataIndex: 'iso9001',
|
||||
slotName: 'iso9001',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '楼层数',
|
||||
dataIndex: 'floor_count',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '机柜数',
|
||||
dataIndex: 'rack_count',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'description',
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
dataIndex: 'remarks',
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'actions',
|
||||
slotName: 'actions',
|
||||
width: 240,
|
||||
fixed: 'right' as const,
|
||||
},
|
||||
]
|
||||
37
src/views/ops/pages/datacenter/house/config/search-form.ts
Normal file
37
src/views/ops/pages/datacenter/house/config/search-form.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
|
||||
export const searchFormConfig: FormItem[] = [
|
||||
{
|
||||
field: 'keyword',
|
||||
label: '关键词',
|
||||
type: 'input',
|
||||
placeholder: '请输入数据中心名称、编码、简称或地址',
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
label: '状态',
|
||||
type: 'select',
|
||||
placeholder: '请选择状态',
|
||||
options: [
|
||||
{ label: '规划中', value: 'planning' },
|
||||
{ label: '建设中', value: 'construction' },
|
||||
{ label: '运营中', value: 'operating' },
|
||||
{ label: '维护中', value: 'maintenance' },
|
||||
{ label: '已下线', value: 'offline' },
|
||||
],
|
||||
},
|
||||
// {
|
||||
// field: 'province_id',
|
||||
// label: '所属省份',
|
||||
// type: 'select',
|
||||
// placeholder: '请选择省份',
|
||||
// options: [], // 将在组件中动态加载
|
||||
// },
|
||||
// {
|
||||
// field: 'city_id',
|
||||
// label: '所属城市',
|
||||
// type: 'select',
|
||||
// placeholder: '请选择城市',
|
||||
// options: [], // 将在组件中动态加载
|
||||
// },
|
||||
]
|
||||
328
src/views/ops/pages/datacenter/house/index.vue
Normal file
328
src/views/ops/pages/datacenter/house/index.vue
Normal file
@@ -0,0 +1,328 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<search-table
|
||||
:form-model="formModel"
|
||||
:form-items="formItems"
|
||||
:data="tableData"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
title="数据中心管理"
|
||||
search-button-text="查询"
|
||||
reset-button-text="重置"
|
||||
@update:form-model="handleFormModelUpdate"
|
||||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
@page-change="handlePageChange"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #toolbar-left>
|
||||
<a-button type="primary" @click="handleCreate">
|
||||
<template #icon>
|
||||
<icon-plus />
|
||||
</template>
|
||||
新建数据中心
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<!-- 序号 -->
|
||||
<template #index="{ rowIndex }">
|
||||
{{ rowIndex + 1 + (pagination.current - 1) * pagination.pageSize }}
|
||||
</template>
|
||||
|
||||
<!-- 所属省份 -->
|
||||
<template #province="{ record }">
|
||||
{{ record.province?.name || '-' }}
|
||||
</template>
|
||||
|
||||
<!-- 所属城市 -->
|
||||
<template #city="{ record }">
|
||||
{{ record.city?.name || '-' }}
|
||||
</template>
|
||||
|
||||
<!-- 状态 -->
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="statusMap[record.status]?.color || 'gray'">
|
||||
{{ statusMap[record.status]?.text || record.status }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 是否启用 -->
|
||||
<template #enabled="{ record }">
|
||||
<a-tag :color="record.enabled ? 'green' : 'red'">
|
||||
{{ record.enabled ? '是' : '否' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- ISO27001 -->
|
||||
<template #iso27001="{ record }">
|
||||
<a-tag :color="record.iso27001 ? 'green' : 'gray'">
|
||||
{{ record.iso27001 ? '是' : '否' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- ISO9001 -->
|
||||
<template #iso9001="{ record }">
|
||||
<a-tag :color="record.iso9001 ? 'green' : 'gray'">
|
||||
{{ record.iso9001 ? '是' : '否' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 操作 -->
|
||||
<template #actions="{ record }">
|
||||
<a-button type="text" size="small" @click="handleDetail(record)">
|
||||
详情
|
||||
</a-button>
|
||||
<a-button type="text" size="small" @click="handleEdit(record)">
|
||||
编辑
|
||||
</a-button>
|
||||
<a-button type="text" size="small" status="danger" @click="handleDelete(record)">
|
||||
删除
|
||||
</a-button>
|
||||
</template>
|
||||
</search-table>
|
||||
|
||||
<!-- 数据中心表单对话框(新建/编辑) -->
|
||||
<datacenter-form-dialog
|
||||
v-model:visible="formVisible"
|
||||
:datacenter="editingDatacenter"
|
||||
:provinces="provinces"
|
||||
:cities="cities"
|
||||
@success="handleFormSuccess"
|
||||
/>
|
||||
|
||||
<!-- 数据中心详情对话框 -->
|
||||
<datacenter-detail-dialog
|
||||
v-model:visible="detailVisible"
|
||||
:datacenter-id="currentDatacenterId"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import { IconPlus } from '@arco-design/web-vue/es/icon'
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
import SearchTable from '@/components/search-table/index.vue'
|
||||
import { searchFormConfig } from './config/search-form'
|
||||
import { columns as columnsConfig } from './config/columns'
|
||||
import {
|
||||
fetchDatacenterList,
|
||||
deleteDatacenter,
|
||||
fetchProvinceList,
|
||||
fetchCityList,
|
||||
} from '@/api/ops/datacenter'
|
||||
import DatacenterDetailDialog from './components/DatacenterDetailDialog.vue'
|
||||
import DatacenterFormDialog from './components/DatacenterFormDialog.vue'
|
||||
|
||||
// 状态映射
|
||||
const statusMap: Record<string, { text: string; color: string }> = {
|
||||
planning: { text: '规划中', color: 'blue' },
|
||||
construction: { text: '建设中', color: 'orange' },
|
||||
operating: { text: '运营中', color: 'green' },
|
||||
maintenance: { text: '维护中', color: 'gold' },
|
||||
offline: { text: '已下线', color: 'red' },
|
||||
}
|
||||
|
||||
// 状态管理
|
||||
const loading = ref(false)
|
||||
const tableData = ref<any[]>([])
|
||||
const formModel = ref({
|
||||
keyword: '',
|
||||
status: '',
|
||||
province_id: '',
|
||||
city_id: '',
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
})
|
||||
|
||||
// 省份和城市列表
|
||||
const provinces = ref<any[]>([])
|
||||
const cities = ref<any[]>([])
|
||||
|
||||
// 表单项配置
|
||||
const formItems = computed<FormItem[]>(() => searchFormConfig)
|
||||
|
||||
// 表格列配置
|
||||
const columns = computed(() => columnsConfig)
|
||||
|
||||
// 当前选中的数据中心
|
||||
const currentDatacenterId = ref<number | undefined>(undefined)
|
||||
const editingDatacenter = ref<any>(null)
|
||||
|
||||
// 对话框可见性
|
||||
const formVisible = ref(false)
|
||||
const detailVisible = ref(false)
|
||||
|
||||
// 获取省份列表
|
||||
const fetchProvinces = async () => {
|
||||
try {
|
||||
const res = await fetchProvinceList()
|
||||
provinces.value = res.details || []
|
||||
// 更新表单配置中的省份选项
|
||||
const provinceFormItem = formItems.value.find((item) => item.field === 'province_id')
|
||||
if (provinceFormItem) {
|
||||
provinceFormItem.options = provinces.value.map((p) => ({
|
||||
label: p.name,
|
||||
value: p.id,
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取省份列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取城市列表
|
||||
const fetchCities = async () => {
|
||||
try {
|
||||
const res = await fetchCityList()
|
||||
console.log(res, '.res')
|
||||
cities.value = res.details || []
|
||||
// 更新表单配置中的城市选项
|
||||
const cityFormItem = formItems.value.find((item) => item.field === 'city_id')
|
||||
if (cityFormItem) {
|
||||
cityFormItem.options = cities.value.map((c) => ({
|
||||
label: c.name,
|
||||
value: c.id,
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取城市列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取数据中心列表
|
||||
const fetchDatacenters = async () => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const params: any = {
|
||||
page: pagination.current,
|
||||
page_size: pagination.pageSize,
|
||||
keyword: formModel.value.keyword || undefined,
|
||||
status: formModel.value.status || undefined,
|
||||
province_id: formModel.value.province_id || undefined,
|
||||
city_id: formModel.value.city_id || undefined,
|
||||
}
|
||||
|
||||
const res = await fetchDatacenterList(params)
|
||||
|
||||
tableData.value = res.details?.data || []
|
||||
pagination.total = res.details?.total || 0
|
||||
} catch (error) {
|
||||
console.error('获取数据中心列表失败:', error)
|
||||
Message.error('获取数据中心列表失败')
|
||||
tableData.value = []
|
||||
pagination.total = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchDatacenters()
|
||||
}
|
||||
|
||||
// 处理表单模型更新
|
||||
const handleFormModelUpdate = (value: any) => {
|
||||
formModel.value = value
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
formModel.value = {
|
||||
keyword: '',
|
||||
status: '',
|
||||
province_id: '',
|
||||
city_id: '',
|
||||
}
|
||||
pagination.current = 1
|
||||
fetchDatacenters()
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (current: number) => {
|
||||
pagination.current = current
|
||||
fetchDatacenters()
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const handleRefresh = () => {
|
||||
fetchDatacenters()
|
||||
Message.success('数据已刷新')
|
||||
}
|
||||
|
||||
// 新建数据中心
|
||||
const handleCreate = () => {
|
||||
editingDatacenter.value = null
|
||||
formVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑数据中心
|
||||
const handleEdit = (record: any) => {
|
||||
console.log('编辑数据中心:', record)
|
||||
editingDatacenter.value = record
|
||||
formVisible.value = true
|
||||
}
|
||||
|
||||
// 详情
|
||||
const handleDetail = (record: any) => {
|
||||
console.log('查看详情:', record)
|
||||
currentDatacenterId.value = record.id
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
// 删除数据中心
|
||||
const handleDelete = async (record: any) => {
|
||||
console.log('删除数据中心:', record)
|
||||
try {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确认删除数据中心 ${record.name} 吗?`,
|
||||
onOk: async () => {
|
||||
const res = await deleteDatacenter(record.id)
|
||||
if (res.code === 0) {
|
||||
Message.success('删除成功')
|
||||
fetchDatacenters()
|
||||
} else {
|
||||
Message.error(res.message || '删除失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('删除数据中心失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 表单成功回调
|
||||
const handleFormSuccess = () => {
|
||||
formVisible.value = false
|
||||
fetchDatacenters()
|
||||
}
|
||||
|
||||
// 初始化加载数据
|
||||
onMounted(() => {
|
||||
fetchProvinces()
|
||||
fetchCities()
|
||||
fetchDatacenters()
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'DataCenterHouse',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
62
src/views/ops/pages/feedback/all/config/columns.ts
Normal file
62
src/views/ops/pages/feedback/all/config/columns.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||
|
||||
export const columns: TableColumnData[] = [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'index',
|
||||
slotName: 'index',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '工单编号',
|
||||
dataIndex: 'ticket_no',
|
||||
width: 160,
|
||||
},
|
||||
{
|
||||
title: '工单标题',
|
||||
dataIndex: 'title',
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: '工单类型',
|
||||
dataIndex: 'type',
|
||||
slotName: 'type',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '工单状态',
|
||||
dataIndex: 'status',
|
||||
slotName: 'status',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '优先级',
|
||||
dataIndex: 'priority',
|
||||
slotName: 'priority',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '创建人',
|
||||
dataIndex: 'creator_name',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '处理人',
|
||||
dataIndex: 'assignee_name',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_at',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'actions',
|
||||
slotName: 'actions',
|
||||
width: 200,
|
||||
fixed: 'right' as const,
|
||||
},
|
||||
]
|
||||
54
src/views/ops/pages/feedback/all/config/search-form.ts
Normal file
54
src/views/ops/pages/feedback/all/config/search-form.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
|
||||
export const searchFormConfig: FormItem[] = [
|
||||
{
|
||||
field: 'keyword',
|
||||
label: '关键词',
|
||||
type: 'input',
|
||||
placeholder: '请输入工单编号或标题',
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
label: '工单类型',
|
||||
type: 'select',
|
||||
placeholder: '请选择工单类型',
|
||||
options: [
|
||||
{ label: '故障', value: 'incident' },
|
||||
{ label: '请求', value: 'request' },
|
||||
{ label: '问题', value: 'question' },
|
||||
{ label: '其他', value: 'other' },
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
label: '工单状态',
|
||||
type: 'select',
|
||||
placeholder: '请选择工单状态',
|
||||
options: [
|
||||
{ label: '待接单', value: 'pending' },
|
||||
{ label: '已接单', value: 'accepted' },
|
||||
{ label: '处理中', value: 'processing' },
|
||||
{ label: '已挂起', value: 'suspended' },
|
||||
{ label: '已解决', value: 'resolved' },
|
||||
{ label: '已关闭', value: 'closed' },
|
||||
{ label: '已撤回', value: 'cancelled' },
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'priority',
|
||||
label: '优先级',
|
||||
type: 'select',
|
||||
placeholder: '请选择优先级',
|
||||
options: [
|
||||
{ label: '低', value: 'low' },
|
||||
{ label: '中', value: 'medium' },
|
||||
{ label: '高', value: 'high' },
|
||||
{ label: '紧急', value: 'urgent' },
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'dateRange',
|
||||
label: '创建时间',
|
||||
type: 'dateRange',
|
||||
},
|
||||
]
|
||||
497
src/views/ops/pages/feedback/all/index.vue
Normal file
497
src/views/ops/pages/feedback/all/index.vue
Normal file
@@ -0,0 +1,497 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<search-table
|
||||
:form-model="formModel"
|
||||
:form-items="formItems"
|
||||
:data="tableData"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
title="所有工单"
|
||||
search-button-text="查询"
|
||||
reset-button-text="重置"
|
||||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
@page-change="handlePageChange"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #toolbar-left>
|
||||
<a-button type="primary" @click="handleCreate">
|
||||
<template #icon>
|
||||
<icon-plus />
|
||||
</template>
|
||||
新建工单
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<!-- 工单类型 -->
|
||||
<template #type="{ record }">
|
||||
<a-tag :color="typeMap[record.type]?.color || 'gray'">
|
||||
{{ typeMap[record.type]?.text || record.type }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 工单状态 -->
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="statusMap[record.status]?.color || 'gray'">
|
||||
{{ statusMap[record.status]?.text || record.status }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 优先级 -->
|
||||
<template #priority="{ record }">
|
||||
<a-tag :color="priorityMap[record.priority]?.color || 'gray'">
|
||||
{{ priorityMap[record.priority]?.text || record.priority }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 序号 -->
|
||||
<template #index="{ rowIndex }">
|
||||
{{ rowIndex + 1 + (pagination.current - 1) * pagination.pageSize }}
|
||||
</template>
|
||||
|
||||
<!-- 操作 -->
|
||||
<template #actions="{ record }">
|
||||
<!-- 详情按钮(所有状态都显示) -->
|
||||
<a-button type="text" size="small" @click="handleDetail(record)">
|
||||
详情
|
||||
</a-button>
|
||||
|
||||
<!-- 接单按钮(待接单状态) -->
|
||||
<a-button
|
||||
v-if="record.status === 'pending'"
|
||||
type="text"
|
||||
size="small"
|
||||
@click="handleAccept(record)"
|
||||
>
|
||||
接单
|
||||
</a-button>
|
||||
|
||||
<!-- 转交按钮(已接单、处理中、已挂起状态) -->
|
||||
<a-button
|
||||
v-if="['accepted', 'processing', 'suspended'].includes(record.status)"
|
||||
type="text"
|
||||
size="small"
|
||||
@click="handleTransfer(record)"
|
||||
>
|
||||
转交
|
||||
</a-button>
|
||||
|
||||
<!-- 挂起按钮(已接单、处理中状态) -->
|
||||
<a-button
|
||||
v-if="['accepted', 'processing'].includes(record.status)"
|
||||
type="text"
|
||||
size="small"
|
||||
@click="handleSuspend(record)"
|
||||
>
|
||||
挂起
|
||||
</a-button>
|
||||
|
||||
<!-- 重启按钮(已挂起状态) -->
|
||||
<a-button
|
||||
v-if="record.status === 'suspended'"
|
||||
type="text"
|
||||
size="small"
|
||||
@click="handleResume(record)"
|
||||
>
|
||||
重启
|
||||
</a-button>
|
||||
|
||||
<!-- 解决按钮(已接单、处理中状态) -->
|
||||
<a-button
|
||||
v-if="['accepted', 'processing'].includes(record.status)"
|
||||
type="text"
|
||||
size="small"
|
||||
@click="handleResolve(record)"
|
||||
>
|
||||
解决
|
||||
</a-button>
|
||||
|
||||
<!-- 关闭按钮(待接单、已接单、处理中、已解决状态) -->
|
||||
<a-button
|
||||
v-if="['pending', 'accepted', 'processing', 'resolved'].includes(record.status)"
|
||||
type="text"
|
||||
size="small"
|
||||
@click="handleClose(record)"
|
||||
>
|
||||
关闭
|
||||
</a-button>
|
||||
|
||||
<!-- 编辑按钮(非已关闭、已撤回状态) -->
|
||||
<a-button
|
||||
v-if="!['closed', 'cancelled'].includes(record.status)"
|
||||
type="text"
|
||||
size="small"
|
||||
@click="handleEdit(record)"
|
||||
>
|
||||
编辑
|
||||
</a-button>
|
||||
|
||||
<!-- 评论按钮(所有状态都显示) -->
|
||||
<a-button type="text" size="small" @click="handleComment(record)">
|
||||
评论
|
||||
</a-button>
|
||||
</template>
|
||||
</search-table>
|
||||
|
||||
<!-- 工单表单对话框(新建/编辑) -->
|
||||
<ticket-form-dialog
|
||||
v-model:visible="formVisible"
|
||||
:ticket="editingTicket"
|
||||
@success="handleFormSuccess"
|
||||
/>
|
||||
|
||||
<!-- 工单详情对话框 -->
|
||||
<ticket-detail-dialog
|
||||
v-model:visible="detailVisible"
|
||||
:ticket-id="currentTicket?.id"
|
||||
@refresh="handleDetailSuccess"
|
||||
/>
|
||||
|
||||
<!-- 转交对话框 -->
|
||||
<transfer-dialog
|
||||
v-model:visible="transferVisible"
|
||||
:ticket="currentTicket"
|
||||
@success="handleTransferSuccess"
|
||||
/>
|
||||
|
||||
<!-- 挂起对话框 -->
|
||||
<suspend-dialog
|
||||
v-model:visible="suspendVisible"
|
||||
:ticket="currentTicket"
|
||||
@success="handleSuspendSuccess"
|
||||
/>
|
||||
|
||||
<!-- 解决对话框 -->
|
||||
<resolve-dialog
|
||||
v-model:visible="resolveVisible"
|
||||
:ticket="currentTicket"
|
||||
@success="handleResolveSuccess"
|
||||
/>
|
||||
|
||||
<!-- 评论对话框 -->
|
||||
<comment-dialog
|
||||
v-model:visible="commentVisible"
|
||||
:ticket="currentTicket"
|
||||
@success="handleCommentSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import { IconPlus } from '@arco-design/web-vue/es/icon'
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
import SearchTable from '@/components/search-table/index.vue'
|
||||
import { searchFormConfig } from './config/search-form'
|
||||
import { columns as columnsConfig } from './config/columns'
|
||||
import {
|
||||
fetchFeedbackTickets,
|
||||
acceptFeedbackTicket,
|
||||
transferFeedbackTicket,
|
||||
suspendFeedbackTicket,
|
||||
resumeFeedbackTicket,
|
||||
resolveFeedbackTicket,
|
||||
closeFeedbackTicket,
|
||||
} from '@/api/ops/feedbackTicket'
|
||||
import TicketDetailDialog from '../components/TicketDetailDialog.vue'
|
||||
import TicketFormDialog from '../components/TicketFormDialog.vue'
|
||||
import CommentDialog from '../components/CommentDialog.vue'
|
||||
import TransferDialog from '../components/TransferDialog.vue'
|
||||
import SuspendDialog from '../components/SuspendDialog.vue'
|
||||
import ResolveDialog from '../components/ResolveDialog.vue'
|
||||
|
||||
// 工单状态映射
|
||||
const statusMap: Record<string, { text: string; color: string }> = {
|
||||
pending: { text: '待接单', color: 'blue' },
|
||||
accepted: { text: '已接单', color: 'cyan' },
|
||||
processing: { text: '处理中', color: 'orange' },
|
||||
suspended: { text: '已挂起', color: 'gray' },
|
||||
resolved: { text: '已解决', color: 'green' },
|
||||
closed: { text: '已关闭', color: 'red' },
|
||||
cancelled: { text: '已撤回', color: 'red' },
|
||||
}
|
||||
|
||||
// 工单类型映射
|
||||
const typeMap: Record<string, { text: string; color: string }> = {
|
||||
incident: { text: '故障', color: 'red' },
|
||||
request: { text: '请求', color: 'blue' },
|
||||
question: { text: '问题', color: 'orange' },
|
||||
other: { text: '其他', color: 'gray' },
|
||||
}
|
||||
|
||||
// 优先级映射
|
||||
const priorityMap: Record<string, { text: string; color: string }> = {
|
||||
low: { text: '低', color: 'gray' },
|
||||
medium: { text: '中', color: 'blue' },
|
||||
high: { text: '高', color: 'orange' },
|
||||
urgent: { text: '紧急', color: 'red' },
|
||||
}
|
||||
|
||||
// 状态管理
|
||||
const loading = ref(false)
|
||||
const tableData = ref<any[]>([])
|
||||
const formModel = ref({
|
||||
keyword: '',
|
||||
type: '',
|
||||
priority: '',
|
||||
status: '',
|
||||
dateRange: [],
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
})
|
||||
|
||||
// 表单项配置
|
||||
const formItems = computed<FormItem[]>(() => searchFormConfig)
|
||||
|
||||
// 表格列配置
|
||||
const columns = computed(() => columnsConfig)
|
||||
|
||||
// 当前选中的工单
|
||||
const currentTicket = ref<any>(null)
|
||||
const editingTicket = ref<any>(null)
|
||||
|
||||
// 对话框可见性
|
||||
const formVisible = ref(false)
|
||||
const detailVisible = ref(false)
|
||||
const transferVisible = ref(false)
|
||||
const suspendVisible = ref(false)
|
||||
const resolveVisible = ref(false)
|
||||
const commentVisible = ref(false)
|
||||
|
||||
// 获取工单列表
|
||||
const fetchTickets = async () => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const params: any = {
|
||||
page: pagination.current,
|
||||
page_size: pagination.pageSize,
|
||||
keyword: formModel.value.keyword || undefined,
|
||||
type: formModel.value.type || undefined,
|
||||
priority: formModel.value.priority || undefined,
|
||||
status: formModel.value.status || undefined,
|
||||
}
|
||||
|
||||
// 处理日期范围
|
||||
if (formModel.value.dateRange && formModel.value.dateRange.length === 2) {
|
||||
const [start, end] = formModel.value.dateRange
|
||||
params.start_time = start
|
||||
params.end_time = end
|
||||
}
|
||||
|
||||
const res = await fetchFeedbackTickets(params)
|
||||
|
||||
tableData.value = res.details?.data || []
|
||||
pagination.total = res.details?.total || 0
|
||||
} catch (error) {
|
||||
console.error('获取工单列表失败:', error)
|
||||
Message.error('获取工单列表失败')
|
||||
tableData.value = []
|
||||
pagination.total = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchTickets()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
formModel.value = {
|
||||
keyword: '',
|
||||
type: '',
|
||||
priority: '',
|
||||
status: '',
|
||||
dateRange: [],
|
||||
}
|
||||
pagination.current = 1
|
||||
fetchTickets()
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (current: number) => {
|
||||
pagination.current = current
|
||||
fetchTickets()
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const handleRefresh = () => {
|
||||
fetchTickets()
|
||||
Message.success('数据已刷新')
|
||||
}
|
||||
|
||||
// 新建工单
|
||||
const handleCreate = () => {
|
||||
editingTicket.value = null
|
||||
formVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑工单
|
||||
const handleEdit = (record: any) => {
|
||||
console.log('编辑工单:', record)
|
||||
editingTicket.value = record
|
||||
formVisible.value = true
|
||||
}
|
||||
|
||||
// 详情
|
||||
const handleDetail = (record: any) => {
|
||||
console.log('查看详情:', record)
|
||||
currentTicket.value = record
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
// 接单
|
||||
const handleAccept = async (record: any) => {
|
||||
console.log('接单:', record)
|
||||
try {
|
||||
Modal.confirm({
|
||||
title: '确认接单',
|
||||
content: `确认接单工单 ${record.ticket_no} 吗?`,
|
||||
onOk: async () => {
|
||||
const res = await acceptFeedbackTicket(record.id)
|
||||
if (res.code === 0) {
|
||||
Message.success('接单成功')
|
||||
fetchTickets()
|
||||
} else {
|
||||
Message.error(res.message || '接单失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('接单失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 转交
|
||||
const handleTransfer = (record: any) => {
|
||||
console.log('转交工单:', record)
|
||||
currentTicket.value = record
|
||||
transferVisible.value = true
|
||||
}
|
||||
|
||||
// 挂起
|
||||
const handleSuspend = (record: any) => {
|
||||
console.log('挂起工单:', record)
|
||||
currentTicket.value = record
|
||||
suspendVisible.value = true
|
||||
}
|
||||
|
||||
// 重启
|
||||
const handleResume = async (record: any) => {
|
||||
console.log('重启工单:', record)
|
||||
try {
|
||||
Modal.confirm({
|
||||
title: '确认重启',
|
||||
content: `确认重启工单 ${record.ticket_no} 吗?`,
|
||||
onOk: async () => {
|
||||
const res = await resumeFeedbackTicket(record.id)
|
||||
if (res.code === 0) {
|
||||
Message.success('重启成功')
|
||||
fetchTickets()
|
||||
} else {
|
||||
Message.error(res.message || '重启失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('重启工单失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 解决
|
||||
const handleResolve = (record: any) => {
|
||||
console.log('解决工单:', record)
|
||||
currentTicket.value = record
|
||||
resolveVisible.value = true
|
||||
}
|
||||
|
||||
// 关闭
|
||||
const handleClose = async (record: any) => {
|
||||
console.log('关闭工单:', record)
|
||||
try {
|
||||
Modal.confirm({
|
||||
title: '确认关闭',
|
||||
content: `确认关闭工单 ${record.ticket_no} 吗?`,
|
||||
onOk: async () => {
|
||||
const res = await closeFeedbackTicket(record.id, {})
|
||||
if (res.code === 0) {
|
||||
Message.success('关闭成功')
|
||||
fetchTickets()
|
||||
} else {
|
||||
Message.error(res.message || '关闭失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('关闭工单失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 评论
|
||||
const handleComment = (record: any) => {
|
||||
console.log('评论工单:', record)
|
||||
currentTicket.value = record
|
||||
commentVisible.value = true
|
||||
}
|
||||
|
||||
// 表单成功回调
|
||||
const handleFormSuccess = () => {
|
||||
formVisible.value = false
|
||||
fetchTickets()
|
||||
}
|
||||
|
||||
// 详情成功回调
|
||||
const handleDetailSuccess = () => {
|
||||
detailVisible.value = false
|
||||
fetchTickets()
|
||||
}
|
||||
|
||||
// 转交成功回调
|
||||
const handleTransferSuccess = () => {
|
||||
transferVisible.value = false
|
||||
fetchTickets()
|
||||
}
|
||||
|
||||
// 挂起成功回调
|
||||
const handleSuspendSuccess = () => {
|
||||
suspendVisible.value = false
|
||||
fetchTickets()
|
||||
}
|
||||
|
||||
// 解决成功回调
|
||||
const handleResolveSuccess = () => {
|
||||
resolveVisible.value = false
|
||||
fetchTickets()
|
||||
}
|
||||
|
||||
// 评论成功回调
|
||||
const handleCommentSuccess = () => {
|
||||
commentVisible.value = false
|
||||
fetchTickets()
|
||||
}
|
||||
|
||||
// 初始化加载数据
|
||||
fetchTickets()
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'FeedbackAll',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
165
src/views/ops/pages/feedback/components/CommentDialog.vue
Normal file
165
src/views/ops/pages/feedback/components/CommentDialog.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
title="工单评论"
|
||||
width="600px"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
@update:visible="handleVisibleChange"
|
||||
>
|
||||
<div class="ticket-info">
|
||||
<div class="info-item">
|
||||
<span class="label">工单编号:</span>
|
||||
<span class="value">{{ ticket?.ticket_no }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">工单标题:</span>
|
||||
<span class="value">{{ ticket?.title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-divider />
|
||||
|
||||
<a-form :model="form" layout="vertical">
|
||||
<a-form-item
|
||||
label="评论内容"
|
||||
field="content"
|
||||
:rules="[{ required: true, message: '请输入评论内容' }]"
|
||||
>
|
||||
<a-textarea
|
||||
v-model="form.content"
|
||||
placeholder="请输入评论内容"
|
||||
:auto-size="{ minRows: 3, maxRows: 5 }"
|
||||
:max-length="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { commentFeedbackTicket } from '@/api/ops/feedbackTicket'
|
||||
|
||||
interface Ticket {
|
||||
id: number
|
||||
ticket_no: string
|
||||
title: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
ticket: Ticket | null
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:visible', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
// 表单数据
|
||||
const form = ref({
|
||||
content: '',
|
||||
})
|
||||
|
||||
// 监听对话框显示状态
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newVal) => {
|
||||
if (newVal && props.ticket) {
|
||||
console.log('CommentDialog ticket:', props.ticket)
|
||||
}
|
||||
if (!newVal) {
|
||||
// 关闭时重置表单
|
||||
form.value = {
|
||||
content: '',
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 监听 ticket 变化
|
||||
watch(
|
||||
() => props.ticket,
|
||||
(newVal) => {
|
||||
console.log('Ticket changed:', newVal)
|
||||
}
|
||||
)
|
||||
|
||||
// 确认评论
|
||||
const handleOk = async () => {
|
||||
if (!props.ticket) {
|
||||
Message.error('工单信息不存在')
|
||||
return
|
||||
}
|
||||
|
||||
if (!form.value.content.trim()) {
|
||||
Message.warning('请输入评论内容')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await commentFeedbackTicket(props.ticket.id, {
|
||||
content: form.value.content,
|
||||
})
|
||||
if (res.code === 200) {
|
||||
Message.success('评论成功')
|
||||
emit('success')
|
||||
} else {
|
||||
Message.error(res.message || '评论失败')
|
||||
}
|
||||
} catch (error) {
|
||||
Message.error('评论失败')
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
// 处理对话框可见性变化
|
||||
const handleVisibleChange = (visible: boolean) => {
|
||||
emit('update:visible', visible)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'CommentDialog',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.ticket-info {
|
||||
padding: 12px 0;
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
|
||||
.label {
|
||||
font-weight: 500;
|
||||
color: var(--color-text-2);
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: var(--color-text-1);
|
||||
font-weight: 400;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.info-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
165
src/views/ops/pages/feedback/components/ResolveDialog.vue
Normal file
165
src/views/ops/pages/feedback/components/ResolveDialog.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
title="解决工单"
|
||||
width="600px"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
@update:visible="handleVisibleChange"
|
||||
>
|
||||
<div class="ticket-info">
|
||||
<div class="info-item">
|
||||
<span class="label">工单编号:</span>
|
||||
<span class="value">{{ ticket?.ticket_no }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">工单标题:</span>
|
||||
<span class="value">{{ ticket?.title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-divider />
|
||||
|
||||
<a-form :model="form" layout="vertical">
|
||||
<a-form-item
|
||||
label="解决方案"
|
||||
field="content"
|
||||
:rules="[{ required: true, message: '请输入解决方案' }]"
|
||||
>
|
||||
<a-textarea
|
||||
v-model="form.content"
|
||||
placeholder="请输入解决方案"
|
||||
:auto-size="{ minRows: 3, maxRows: 5 }"
|
||||
:max-length="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { resolveFeedbackTicket } from '@/api/ops/feedbackTicket'
|
||||
|
||||
interface Ticket {
|
||||
id: number
|
||||
ticket_no: string
|
||||
title: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
ticket: Ticket | null
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:visible', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
// 表单数据
|
||||
const form = ref({
|
||||
content: '',
|
||||
})
|
||||
|
||||
// 监听对话框显示状态
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newVal) => {
|
||||
if (newVal && props.ticket) {
|
||||
console.log('ResolveDialog ticket:', props.ticket)
|
||||
}
|
||||
if (!newVal) {
|
||||
// 关闭时重置表单
|
||||
form.value = {
|
||||
content: '',
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 监听 ticket 变化
|
||||
watch(
|
||||
() => props.ticket,
|
||||
(newVal) => {
|
||||
console.log('Ticket changed:', newVal)
|
||||
}
|
||||
)
|
||||
|
||||
// 确认解决
|
||||
const handleOk = async () => {
|
||||
if (!props.ticket) {
|
||||
Message.error('工单信息不存在')
|
||||
return
|
||||
}
|
||||
|
||||
if (!form.value.content.trim()) {
|
||||
Message.warning('请输入解决方案')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await resolveFeedbackTicket(props.ticket.id, {
|
||||
content: form.value.content,
|
||||
})
|
||||
if (res.code === 200) {
|
||||
Message.success('工单已解决')
|
||||
emit('success')
|
||||
} else {
|
||||
Message.error(res.message || '解决失败')
|
||||
}
|
||||
} catch (error) {
|
||||
Message.error('解决失败')
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
// 处理对话框可见性变化
|
||||
const handleVisibleChange = (visible: boolean) => {
|
||||
emit('update:visible', visible)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ResolveDialog',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.ticket-info {
|
||||
padding: 12px 0;
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
|
||||
.label {
|
||||
font-weight: 500;
|
||||
color: var(--color-text-2);
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: var(--color-text-1);
|
||||
font-weight: 400;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.info-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
165
src/views/ops/pages/feedback/components/SuspendDialog.vue
Normal file
165
src/views/ops/pages/feedback/components/SuspendDialog.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
title="挂起工单"
|
||||
width="600px"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
@update:visible="handleVisibleChange"
|
||||
>
|
||||
<div class="ticket-info">
|
||||
<div class="info-item">
|
||||
<span class="label">工单编号:</span>
|
||||
<span class="value">{{ ticket?.ticket_no }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">工单标题:</span>
|
||||
<span class="value">{{ ticket?.title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-divider />
|
||||
|
||||
<a-form :model="form" layout="vertical">
|
||||
<a-form-item
|
||||
label="挂起原因"
|
||||
field="content"
|
||||
:rules="[{ required: true, message: '请输入挂起原因' }]"
|
||||
>
|
||||
<a-textarea
|
||||
v-model="form.content"
|
||||
placeholder="请输入挂起原因"
|
||||
:auto-size="{ minRows: 3, maxRows: 5 }"
|
||||
:max-length="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { suspendFeedbackTicket } from '@/api/ops/feedbackTicket'
|
||||
|
||||
interface Ticket {
|
||||
id: number
|
||||
ticket_no: string
|
||||
title: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
ticket: Ticket | null
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:visible', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
// 表单数据
|
||||
const form = ref({
|
||||
content: '',
|
||||
})
|
||||
|
||||
// 监听对话框显示状态
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newVal) => {
|
||||
if (newVal && props.ticket) {
|
||||
console.log('SuspendDialog ticket:', props.ticket)
|
||||
}
|
||||
if (!newVal) {
|
||||
// 关闭时重置表单
|
||||
form.value = {
|
||||
content: '',
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 监听 ticket 变化
|
||||
watch(
|
||||
() => props.ticket,
|
||||
(newVal) => {
|
||||
console.log('Ticket changed:', newVal)
|
||||
}
|
||||
)
|
||||
|
||||
// 确认挂起
|
||||
const handleOk = async () => {
|
||||
if (!props.ticket) {
|
||||
Message.error('工单信息不存在')
|
||||
return
|
||||
}
|
||||
|
||||
if (!form.value.content.trim()) {
|
||||
Message.warning('请输入挂起原因')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await suspendFeedbackTicket(props.ticket.id, {
|
||||
content: form.value.content,
|
||||
})
|
||||
if (res.code === 0) {
|
||||
Message.success('挂起成功')
|
||||
emit('success')
|
||||
} else {
|
||||
Message.error(res.message || '挂起失败')
|
||||
}
|
||||
} catch (error) {
|
||||
Message.error('挂起失败')
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
// 处理对话框可见性变化
|
||||
const handleVisibleChange = (visible: boolean) => {
|
||||
emit('update:visible', visible)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'SuspendDialog',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.ticket-info {
|
||||
padding: 12px 0;
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
|
||||
.label {
|
||||
font-weight: 500;
|
||||
color: var(--color-text-2);
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: var(--color-text-1);
|
||||
font-weight: 400;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.info-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
420
src/views/ops/pages/feedback/components/TicketDetailDialog.vue
Normal file
420
src/views/ops/pages/feedback/components/TicketDetailDialog.vue
Normal file
@@ -0,0 +1,420 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
title="工单详情"
|
||||
width="900px"
|
||||
:footer="false"
|
||||
@cancel="handleCancel"
|
||||
@update:visible="handleVisibleChange"
|
||||
>
|
||||
<a-spin :loading="loading" style="width: 100%;">
|
||||
<div v-if="ticketDetail" class="ticket-detail">
|
||||
<!-- 工单基本信息 -->
|
||||
<a-descriptions title="基本信息" :column="2" bordered>
|
||||
<a-descriptions-item label="工单编号">
|
||||
{{ ticketDetail.ticket_no }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="工单状态">
|
||||
<a-tag :color="getStatusColor(ticketDetail.status)">
|
||||
{{ getStatusText(ticketDetail.status) }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="工单标题" :span="2">
|
||||
{{ ticketDetail.title }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="工单类型">
|
||||
<a-tag color="blue">
|
||||
{{ getTypeText(ticketDetail.type) }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="优先级">
|
||||
<a-tag :color="getPriorityColor(ticketDetail.priority)">
|
||||
{{ getPriorityText(ticketDetail.priority) }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="创建人">
|
||||
{{ ticketDetail.creator_name }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="处理人">
|
||||
{{ ticketDetail.assignee_name || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="分类">
|
||||
{{ ticketDetail.category_name || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="创建时间">
|
||||
{{ formatDateTime(ticketDetail.created_at) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="更新时间">
|
||||
{{ formatDateTime(ticketDetail.updated_at) }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<!-- 工单描述 -->
|
||||
<div class="section">
|
||||
<h3>工单描述</h3>
|
||||
<div class="description">
|
||||
{{ ticketDetail.description || '暂无描述' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 评论列表 -->
|
||||
<div class="section">
|
||||
<h3>评论列表</h3>
|
||||
<div class="comments">
|
||||
<a-empty v-if="!comments || comments.length === 0" description="暂无评论" />
|
||||
<div v-else class="comment-list">
|
||||
<div v-for="comment in comments" :key="comment.id" class="comment-item">
|
||||
<div class="comment-header">
|
||||
<span class="comment-operator">{{ comment.operator_name }}</span>
|
||||
<span class="comment-time">{{ formatDateTime(comment.created_at) }}</span>
|
||||
</div>
|
||||
<div class="comment-content">{{ comment.content }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加评论 -->
|
||||
<div class="section">
|
||||
<h3>添加评论</h3>
|
||||
<a-textarea
|
||||
v-model="commentContent"
|
||||
placeholder="请输入评论内容"
|
||||
:auto-size="{ minRows: 3, maxRows: 5 }"
|
||||
:max-length="500"
|
||||
show-word-limit
|
||||
/>
|
||||
<div class="comment-actions">
|
||||
<a-button type="primary" :loading="submitting" @click="handleSubmitComment">
|
||||
提交评论
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作日志 -->
|
||||
<div class="section">
|
||||
<h3>操作日志</h3>
|
||||
<a-timeline>
|
||||
<a-timeline-item v-for="log in logs" :key="log.id">
|
||||
<template #dot>
|
||||
<a-badge :status="getLogStatus(log.action)" />
|
||||
</template>
|
||||
<div class="log-content">
|
||||
<div class="log-header">
|
||||
<span class="log-action">{{ getActionText(log.action) }}</span>
|
||||
<span class="log-operator">{{ log.operator_name }}</span>
|
||||
<span class="log-time">{{ formatDateTime(log.created_at) }}</span>
|
||||
</div>
|
||||
<div class="log-detail">{{ log.content }}</div>
|
||||
</div>
|
||||
</a-timeline-item>
|
||||
<a-timeline-item v-if="!logs || logs.length === 0">
|
||||
暂无操作日志
|
||||
</a-timeline-item>
|
||||
</a-timeline>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import {
|
||||
fetchFeedbackTicketDetail,
|
||||
commentFeedbackTicket,
|
||||
fetchFeedbackTicketLogs,
|
||||
} from '@/api/ops/feedbackTicket'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
ticketId: number
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:visible', value: boolean): void
|
||||
(e: 'refresh'): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
// 状态管理
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const ticketDetail = ref<any>(null)
|
||||
const comments = ref<any[]>([])
|
||||
const logs = ref<any[]>([])
|
||||
const commentContent = ref('')
|
||||
|
||||
// 监听对话框显示状态
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newVal) => {
|
||||
if (newVal && props.ticketId) {
|
||||
fetchTicketDetail()
|
||||
fetchTicketLogs()
|
||||
} else {
|
||||
// 关闭时清空数据
|
||||
ticketDetail.value = null
|
||||
comments.value = []
|
||||
logs.value = []
|
||||
commentContent.value = ''
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 获取工单详情
|
||||
const fetchTicketDetail = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchFeedbackTicketDetail(props.ticketId)
|
||||
if (res.code === 0) {
|
||||
ticketDetail.value = res.details
|
||||
} else {
|
||||
Message.error(res.message || '获取工单详情失败')
|
||||
}
|
||||
} catch (error) {
|
||||
Message.error('获取工单详情失败')
|
||||
console.error(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取操作日志
|
||||
const fetchTicketLogs = async () => {
|
||||
try {
|
||||
const res = await fetchFeedbackTicketLogs(props.ticketId)
|
||||
if (res.code === 0) {
|
||||
logs.value = res.details || []
|
||||
// 提取评论
|
||||
comments.value = logs.value.filter((log: any) => log.action === 'comment')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取操作日志失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 提交评论
|
||||
const handleSubmitComment = async () => {
|
||||
if (!commentContent.value.trim()) {
|
||||
Message.warning('请输入评论内容')
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
const res = await commentFeedbackTicket(props.ticketId, {
|
||||
content: commentContent.value,
|
||||
})
|
||||
if (res.code === 0) {
|
||||
Message.success('评论成功')
|
||||
commentContent.value = ''
|
||||
// 刷新日志和评论
|
||||
await fetchTicketLogs()
|
||||
} else {
|
||||
Message.error(res.message || '评论失败')
|
||||
}
|
||||
} catch (error) {
|
||||
Message.error('评论失败')
|
||||
console.error(error)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
emit('refresh')
|
||||
}
|
||||
|
||||
// 处理对话框可见性变化
|
||||
const handleVisibleChange = (visible: boolean) => {
|
||||
emit('update:visible', visible)
|
||||
}
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
pending: 'blue',
|
||||
accepted: 'green',
|
||||
processing: 'cyan',
|
||||
suspended: 'orange',
|
||||
resolved: 'purple',
|
||||
closed: 'gray',
|
||||
cancelled: 'red',
|
||||
}
|
||||
return colorMap[status] || 'gray'
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
pending: '待接单',
|
||||
accepted: '已接单',
|
||||
processing: '处理中',
|
||||
suspended: '已挂起',
|
||||
resolved: '已解决',
|
||||
closed: '已关闭',
|
||||
cancelled: '已撤回',
|
||||
}
|
||||
return textMap[status] || status
|
||||
}
|
||||
|
||||
// 获取优先级颜色
|
||||
const getPriorityColor = (priority: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
low: 'gray',
|
||||
medium: 'blue',
|
||||
high: 'orange',
|
||||
urgent: 'red',
|
||||
}
|
||||
return colorMap[priority] || 'gray'
|
||||
}
|
||||
|
||||
// 获取优先级文本
|
||||
const getPriorityText = (priority: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
low: '低',
|
||||
medium: '中',
|
||||
high: '高',
|
||||
urgent: '紧急',
|
||||
}
|
||||
return textMap[priority] || priority
|
||||
}
|
||||
|
||||
// 获取类型文本
|
||||
const getTypeText = (type: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
incident: '故障',
|
||||
request: '请求',
|
||||
question: '问题',
|
||||
other: '其他',
|
||||
}
|
||||
return textMap[type] || type
|
||||
}
|
||||
|
||||
// 获取操作文本
|
||||
const getActionText = (action: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
create: '创建',
|
||||
accept: '接单',
|
||||
transfer: '转交',
|
||||
suspend: '挂起',
|
||||
resume: '重启',
|
||||
resolve: '解决',
|
||||
close: '关闭',
|
||||
cancel: '撤回',
|
||||
comment: '评论',
|
||||
update: '更新',
|
||||
}
|
||||
return textMap[action] || action
|
||||
}
|
||||
|
||||
// 获取日志状态
|
||||
const getLogStatus = (action: string) => {
|
||||
const statusMap: Record<string, string> = {
|
||||
create: 'processing',
|
||||
accept: 'success',
|
||||
transfer: 'warning',
|
||||
suspend: 'warning',
|
||||
resume: 'processing',
|
||||
resolve: 'success',
|
||||
close: 'default',
|
||||
cancel: 'error',
|
||||
comment: 'default',
|
||||
update: 'processing',
|
||||
}
|
||||
return statusMap[action] || 'default'
|
||||
}
|
||||
|
||||
// 格式化日期时间
|
||||
const formatDateTime = (dateTime: string) => {
|
||||
if (!dateTime) return '-'
|
||||
return new Date(dateTime).toLocaleString('zh-CN')
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'TicketDetailDialog',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.ticket-detail {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-top: 24px;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 12px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
padding: 12px;
|
||||
background-color: var(--color-fill-2);
|
||||
border-radius: 4px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.comment-list {
|
||||
.comment-item {
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
background-color: var(--color-fill-2);
|
||||
border-radius: 4px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
|
||||
.comment-operator {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-content {
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-actions {
|
||||
margin-top: 12px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.log-content {
|
||||
.log-header {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
|
||||
.log-action {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.log-detail {
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
304
src/views/ops/pages/feedback/components/TicketFormDialog.vue
Normal file
304
src/views/ops/pages/feedback/components/TicketFormDialog.vue
Normal file
@@ -0,0 +1,304 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
:title="isEdit ? '编辑工单' : '新建工单'"
|
||||
width="700px"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
@update:visible="handleVisibleChange"
|
||||
:confirm-loading="submitting"
|
||||
>
|
||||
<a-form :model="form" layout="vertical" ref="formRef">
|
||||
<a-form-item
|
||||
label="工单标题"
|
||||
field="title"
|
||||
:rules="[{ required: true, message: '请输入工单标题' }]"
|
||||
>
|
||||
<a-input
|
||||
v-model="form.title"
|
||||
placeholder="请输入工单标题"
|
||||
:max-length="100"
|
||||
show-word-limit
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
label="工单类型"
|
||||
field="type"
|
||||
:rules="[{ required: true, message: '请选择工单类型' }]"
|
||||
>
|
||||
<a-select v-model="form.type" placeholder="请选择工单类型">
|
||||
<a-option value="incident">故障</a-option>
|
||||
<a-option value="request">请求</a-option>
|
||||
<a-option value="question">问题</a-option>
|
||||
<a-option value="other">其他</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
label="优先级"
|
||||
field="priority"
|
||||
:rules="[{ required: true, message: '请选择优先级' }]"
|
||||
>
|
||||
<a-select v-model="form.priority" placeholder="请选择优先级">
|
||||
<a-option value="low">低</a-option>
|
||||
<a-option value="medium">中</a-option>
|
||||
<a-option value="high">高</a-option>
|
||||
<a-option value="urgent">紧急</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
label="工单描述"
|
||||
field="description"
|
||||
>
|
||||
<a-textarea
|
||||
v-model="form.description"
|
||||
placeholder="请输入工单描述"
|
||||
:auto-size="{ minRows: 4, maxRows: 8 }"
|
||||
:max-length="1000"
|
||||
show-word-limit
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- <a-form-item
|
||||
label="分类"
|
||||
field="category_name"
|
||||
>
|
||||
<a-input
|
||||
v-model="form.category_name"
|
||||
placeholder="请输入分类名称"
|
||||
:max-length="50"
|
||||
/>
|
||||
</a-form-item> -->
|
||||
|
||||
<a-form-item
|
||||
label="处理人"
|
||||
field="assignee_name"
|
||||
>
|
||||
<a-select
|
||||
v-model="form.assignee_name"
|
||||
placeholder="请选择处理人"
|
||||
:filter-option="true"
|
||||
show-search
|
||||
allow-clear
|
||||
:loading="loadingUsers"
|
||||
@change="handleAssigneeChange"
|
||||
>
|
||||
<a-option
|
||||
v-for="user in userList"
|
||||
:key="user.id"
|
||||
:value="user.name"
|
||||
>
|
||||
{{ user.name }} ({{ user.id }})
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
label="标签"
|
||||
field="tags"
|
||||
>
|
||||
<a-input
|
||||
v-model="form.tags"
|
||||
placeholder="多个标签用逗号分隔"
|
||||
:max-length="200"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import {
|
||||
createFeedbackTicket,
|
||||
updateFeedbackTicket,
|
||||
} from '@/api/ops/feedbackTicket'
|
||||
import { fetchUserList, type UserItem } from '@/api/ops/rbac2'
|
||||
|
||||
interface Ticket {
|
||||
id?: number
|
||||
title?: string
|
||||
description?: string
|
||||
type?: string
|
||||
priority?: string
|
||||
category_id?: number
|
||||
category_name?: string
|
||||
assignee_id?: number
|
||||
assignee_name?: string
|
||||
tags?: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
ticket: Ticket | null
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:visible', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref()
|
||||
const loadingUsers = ref(false)
|
||||
const submitting = ref(false)
|
||||
const userList = ref<UserItem[]>([])
|
||||
|
||||
// 表单数据
|
||||
const form = ref({
|
||||
title: '',
|
||||
description: '',
|
||||
type: '',
|
||||
priority: '',
|
||||
category_id: undefined as number | undefined,
|
||||
category_name: '',
|
||||
assignee_id: undefined as number | undefined,
|
||||
assignee_name: '',
|
||||
tags: '',
|
||||
})
|
||||
|
||||
// 是否为编辑模式
|
||||
const isEdit = computed(() => !!props.ticket?.id)
|
||||
|
||||
// 加载用户列表
|
||||
const loadUserList = async () => {
|
||||
loadingUsers.value = true
|
||||
try {
|
||||
const res: any = await fetchUserList({ workspace: import.meta.env.VITE_APP_WORKSPACE || '' })
|
||||
if (res.code === 0) {
|
||||
userList.value = res.details?.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error)
|
||||
} finally {
|
||||
loadingUsers.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理人选择变化
|
||||
const handleAssigneeChange = (value: string) => {
|
||||
const selectedUser = userList.value.find((user) => user.name === value)
|
||||
if (selectedUser) {
|
||||
form.value.assignee_id = selectedUser.id
|
||||
form.value.assignee_name = selectedUser.name
|
||||
} else {
|
||||
form.value.assignee_id = undefined
|
||||
}
|
||||
}
|
||||
|
||||
// 监听对话框显示状态
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
if (props.ticket && isEdit.value) {
|
||||
// 编辑模式:填充表单
|
||||
form.value = {
|
||||
title: props.ticket.title || '',
|
||||
description: props.ticket.description || '',
|
||||
type: props.ticket.type || '',
|
||||
priority: props.ticket.priority || '',
|
||||
category_id: props.ticket.category_id,
|
||||
category_name: props.ticket.category_name || '',
|
||||
assignee_id: props.ticket.assignee_id,
|
||||
assignee_name: props.ticket.assignee_name || '',
|
||||
tags: '',
|
||||
}
|
||||
} else {
|
||||
// 新建模式:重置表单
|
||||
form.value = {
|
||||
title: '',
|
||||
description: '',
|
||||
type: '',
|
||||
priority: '',
|
||||
category_id: undefined,
|
||||
category_name: '',
|
||||
assignee_id: undefined,
|
||||
assignee_name: '',
|
||||
tags: '',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 确认提交
|
||||
const handleOk = async () => {
|
||||
const valid = await formRef.value?.validate()
|
||||
console.log('valid', valid)
|
||||
if (valid) return
|
||||
|
||||
console.log('form', form)
|
||||
submitting.value = true
|
||||
try {
|
||||
const data = {
|
||||
title: form.value.title,
|
||||
description: form.value.description || '',
|
||||
type: form.value.type,
|
||||
priority: form.value.priority,
|
||||
category_id: form.value.category_id,
|
||||
category_name: form.value.category_name || '',
|
||||
assignee_id: form.value.assignee_id,
|
||||
assignee_name: form.value.assignee_name || '',
|
||||
tags: form.value.tags || '',
|
||||
// metadata 和 attachments 传空对象
|
||||
metadata: '{}',
|
||||
attachments: '{}',
|
||||
}
|
||||
|
||||
let res
|
||||
if (isEdit.value && props.ticket?.id) {
|
||||
// 编辑工单
|
||||
res = await updateFeedbackTicket(props.ticket.id, data)
|
||||
} else {
|
||||
// 新建工单
|
||||
res = await createFeedbackTicket(data)
|
||||
}
|
||||
|
||||
if (res.code === 0) {
|
||||
Message.success(isEdit.value ? '编辑成功' : '创建成功')
|
||||
emit('success')
|
||||
emit('update:visible', false)
|
||||
} else {
|
||||
Message.error(res.message || (isEdit.value ? '编辑失败' : '创建失败'))
|
||||
}
|
||||
} catch (error) {
|
||||
Message.error(isEdit.value ? '编辑失败' : '创建失败')
|
||||
console.error(error)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
// 处理对话框可见性变化
|
||||
const handleVisibleChange = (visible: boolean) => {
|
||||
emit('update:visible', visible)
|
||||
}
|
||||
|
||||
// 组件挂载时加载用户列表
|
||||
onMounted(() => {
|
||||
loadUserList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
export default {
|
||||
name: 'TicketFormDialog',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
// 样式可以根据需要添加
|
||||
</style>
|
||||
232
src/views/ops/pages/feedback/components/TransferDialog.vue
Normal file
232
src/views/ops/pages/feedback/components/TransferDialog.vue
Normal file
@@ -0,0 +1,232 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
title="转交工单"
|
||||
width="600px"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
@update:visible="handleVisibleChange"
|
||||
>
|
||||
<div class="ticket-info">
|
||||
<div class="info-item">
|
||||
<span class="label">工单编号:</span>
|
||||
<span class="value">{{ ticket?.ticket_no }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">工单标题:</span>
|
||||
<span class="value">{{ ticket?.title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-divider />
|
||||
|
||||
<a-form :model="form" layout="vertical">
|
||||
<a-form-item
|
||||
label="转交人员"
|
||||
field="assignee_name"
|
||||
:rules="[{ required: true, message: '请选择转交人员' }]"
|
||||
>
|
||||
<a-select
|
||||
v-model="form.assignee_name"
|
||||
placeholder="请输入或选择转交人员"
|
||||
:filter-option="true"
|
||||
show-search
|
||||
allow-clear
|
||||
:loading="loading"
|
||||
@change="handleUserChange"
|
||||
>
|
||||
<a-option
|
||||
v-for="user in userList"
|
||||
:key="user.id"
|
||||
:value="user.name"
|
||||
>
|
||||
{{ user.name }} ({{ user.phone }})
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="转交原因" field="reason">
|
||||
<a-textarea
|
||||
v-model="form.reason"
|
||||
placeholder="请输入转交原因"
|
||||
:auto-size="{ minRows: 3, maxRows: 5 }"
|
||||
:max-length="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { transferFeedbackTicket } from '@/api/ops/feedbackTicket'
|
||||
import { fetchUserList, type UserItem } from '@/api/ops/rbac2'
|
||||
|
||||
interface Ticket {
|
||||
id: number
|
||||
ticket_no: string
|
||||
title: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
ticket: Ticket | null
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:visible', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
// 用户列表
|
||||
const userList = ref<UserItem[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 加载用户列表
|
||||
const loadUserList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await fetchUserList({ workspace: import.meta.env.VITE_APP_WORKSPACE || '' })
|
||||
if (res.code === 0) {
|
||||
userList.value = res.details?.data
|
||||
} else {
|
||||
Message.error(res.message || '获取用户列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error)
|
||||
Message.error('获取用户列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时加载用户列表
|
||||
onMounted(() => {
|
||||
loadUserList()
|
||||
})
|
||||
|
||||
// 表单数据
|
||||
const form = ref({
|
||||
assignee_id: undefined as number | undefined,
|
||||
assignee_name: '',
|
||||
reason: '',
|
||||
})
|
||||
|
||||
// 处理用户选择变化
|
||||
const handleUserChange = (value: string) => {
|
||||
const selectedUser = userList.value.find((user) => user.name === value)
|
||||
if (selectedUser) {
|
||||
form.value.assignee_id = selectedUser.id
|
||||
form.value.assignee_name = selectedUser.name
|
||||
}
|
||||
}
|
||||
|
||||
// 监听对话框显示状态
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newVal) => {
|
||||
if (newVal && props.ticket) {
|
||||
console.log('TransferDialog ticket:', props.ticket)
|
||||
}
|
||||
if (!newVal) {
|
||||
// 关闭时重置表单
|
||||
form.value = {
|
||||
assignee_id: undefined,
|
||||
assignee_name: '',
|
||||
reason: '',
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 监听 ticket 变化
|
||||
watch(
|
||||
() => props.ticket,
|
||||
(newVal) => {
|
||||
console.log('Ticket changed:', newVal)
|
||||
}
|
||||
)
|
||||
|
||||
// 确认转交
|
||||
const handleOk = async () => {
|
||||
if (!props.ticket) {
|
||||
Message.error('工单信息不存在')
|
||||
return
|
||||
}
|
||||
|
||||
if (!form.value.assignee_id) {
|
||||
Message.warning('请输入新处理人ID')
|
||||
return
|
||||
}
|
||||
|
||||
if (!form.value.assignee_name.trim()) {
|
||||
Message.warning('请输入新处理人名称')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await transferFeedbackTicket(props.ticket.id, {
|
||||
assignee_id: form.value.assignee_id,
|
||||
assignee_name: form.value.assignee_name,
|
||||
reason: form.value.reason,
|
||||
})
|
||||
if (res.code === 200) {
|
||||
Message.success('转交成功')
|
||||
emit('success')
|
||||
} else {
|
||||
Message.error(res.message || '转交失败')
|
||||
}
|
||||
} catch (error) {
|
||||
Message.error('转交失败')
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
// 处理对话框可见性变化
|
||||
const handleVisibleChange = (visible: boolean) => {
|
||||
emit('update:visible', visible)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'TransferDialog',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.ticket-info {
|
||||
padding: 12px 0;
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
|
||||
.label {
|
||||
font-weight: 500;
|
||||
color: var(--color-text-2);
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: var(--color-text-1);
|
||||
font-weight: 400;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.info-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
139
src/views/ops/pages/feedback/undo/config/columns.ts
Normal file
139
src/views/ops/pages/feedback/undo/config/columns.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { h } from 'vue'
|
||||
import { Tag, Button, Dropdown, Message } from '@arco-design/web-vue'
|
||||
import { IconEdit, IconDelete, IconMore, IconCheckCircle } from '@arco-design/web-vue/es/icon'
|
||||
|
||||
// 工单状态映射
|
||||
const statusMap = {
|
||||
pending: { text: '待接单', color: 'blue' },
|
||||
accepted: { text: '已接单', color: 'cyan' },
|
||||
processing: { text: '处理中', color: 'orange' },
|
||||
suspended: { text: '已挂起', color: 'gray' },
|
||||
resolved: { text: '已解决', color: 'green' },
|
||||
closed: { text: '已关闭', color: 'red' },
|
||||
cancelled: { text: '已撤回', color: 'red' },
|
||||
}
|
||||
|
||||
// 工单类型映射
|
||||
const typeMap = {
|
||||
incident: { text: '故障', color: 'red' },
|
||||
request: { text: '请求', color: 'blue' },
|
||||
question: { text: '问题', color: 'orange' },
|
||||
other: { text: '其他', color: 'gray' },
|
||||
}
|
||||
|
||||
// 优先级映射
|
||||
const priorityMap = {
|
||||
low: { text: '低', color: 'gray' },
|
||||
medium: { text: '中', color: 'blue' },
|
||||
high: { text: '高', color: 'orange' },
|
||||
urgent: { text: '紧急', color: 'red' },
|
||||
}
|
||||
|
||||
// 生成表格列配置
|
||||
export const generateColumns = (
|
||||
tabType: 'my-created' | 'pending',
|
||||
handlers: {
|
||||
onDetail: (record: any) => void
|
||||
onEdit?: (record: any) => void
|
||||
onTransfer?: (record: any) => void
|
||||
onSuspend?: (record: any) => void
|
||||
onResume?: (record: any) => void
|
||||
onResolve?: (record: any) => void
|
||||
onCancel?: (record: any) => void
|
||||
onComment?: (record: any) => void
|
||||
onDelete?: (record: any) => void
|
||||
onClose?: (record: any) => void
|
||||
}
|
||||
) => {
|
||||
const baseColumns = [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'index',
|
||||
width: 80,
|
||||
render: ({ rowIndex }: { rowIndex: number }) => rowIndex + 1,
|
||||
},
|
||||
{
|
||||
title: '工单编号',
|
||||
dataIndex: 'ticket_no',
|
||||
width: 160,
|
||||
},
|
||||
{
|
||||
title: '工单标题',
|
||||
dataIndex: 'title',
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: '工单类型',
|
||||
dataIndex: 'type',
|
||||
width: 100,
|
||||
render: ({ record }: { record: any }) => {
|
||||
const type = typeMap[record.type as keyof typeof typeMap] || typeMap.other
|
||||
return h(Tag, { color: type.color }, { default: () => type.text })
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '工单状态',
|
||||
dataIndex: 'status',
|
||||
width: 100,
|
||||
render: ({ record }: { record: any }) => {
|
||||
const status = statusMap[record.status as keyof typeof statusMap] || { text: record.status, color: 'gray' }
|
||||
return h(Tag, { color: status.color }, { default: () => status.text })
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '优先级',
|
||||
dataIndex: 'priority',
|
||||
width: 100,
|
||||
render: ({ record }: { record: any }) => {
|
||||
const priority = priorityMap[record.priority as keyof typeof priorityMap] || { text: record.priority, color: 'gray' }
|
||||
return h(Tag, { color: priority.color }, { default: () => priority.text })
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
width: tabType === 'pending' ? 280 : 300,
|
||||
fixed: 'right' as const,
|
||||
render: ({ record }: { record: any }) => {
|
||||
if (tabType === 'pending') {
|
||||
// 待处理选项卡的操作按钮
|
||||
return h(
|
||||
'div',
|
||||
{ style: { display: 'flex', gap: '8px', flexWrap: 'wrap' } },
|
||||
[
|
||||
h(Button, { size: 'small', type: 'text', onClick: () => handlers.onDetail(record) }, { default: () => '详情' }),
|
||||
h(Button, { size: 'small', type: 'text', onClick: () => handlers.onTransfer?.(record) }, { default: () => '转交' }),
|
||||
h(Button, { size: 'small', type: 'text', onClick: () => handlers.onSuspend?.(record) }, { default: () => '挂起' }),
|
||||
record.status === 'suspended' &&
|
||||
h(Button, { size: 'small', type: 'text', onClick: () => handlers.onResume?.(record) }, { default: () => '重启' }),
|
||||
h(Button, { size: 'small', type: 'text', onClick: () => handlers.onResolve?.(record) }, { default: () => '解决' }),
|
||||
h(Button, { size: 'small', type: 'text', onClick: () => handlers.onClose?.(record) }, { default: () => '关闭' }),
|
||||
]
|
||||
)
|
||||
} else {
|
||||
// 我创建的选项卡的操作按钮
|
||||
const isCompleted = ['resolved', 'closed'].includes(record.status)
|
||||
const isResolved = record.status === 'resolved'
|
||||
|
||||
return h(
|
||||
'div',
|
||||
{ style: { display: 'flex', gap: '8px', flexWrap: 'wrap' } },
|
||||
[
|
||||
h(Button, { size: 'small', type: 'text', onClick: () => handlers.onDetail(record) }, { default: () => '详情' }),
|
||||
!isCompleted &&
|
||||
h(Button, { size: 'small', type: 'text', onClick: () => handlers.onEdit?.(record) }, { default: () => '编辑' }),
|
||||
h(Button, { size: 'small', type: 'text', onClick: () => handlers.onCancel?.(record) }, { default: () => '撤回' }),
|
||||
isResolved && h(Button, { size: 'small', type: 'text', onClick: () => handlers.onComment?.(record) }, { default: () => '评论' }),
|
||||
h(Button, { size: 'small', type: 'text', status: 'danger', onClick: () => handlers.onDelete?.(record) }, { default: () => '删除' }),
|
||||
h(Button, { size: 'small', type: 'text', onClick: () => handlers.onClose?.(record) }, { default: () => '关闭' }),
|
||||
]
|
||||
)
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
return baseColumns
|
||||
}
|
||||
49
src/views/ops/pages/feedback/undo/config/search-form.ts
Normal file
49
src/views/ops/pages/feedback/undo/config/search-form.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
|
||||
export const searchFormConfig: FormItem[] = [
|
||||
{
|
||||
field: 'keyword',
|
||||
label: '关键词',
|
||||
type: 'input',
|
||||
placeholder: '请输入工单编号或标题',
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
label: '工单类型',
|
||||
type: 'select',
|
||||
placeholder: '请选择工单类型',
|
||||
options: [
|
||||
{ label: '故障', value: 'incident' },
|
||||
{ label: '请求', value: 'request' },
|
||||
{ label: '问题', value: 'question' },
|
||||
{ label: '其他', value: 'other' },
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
label: '工单状态',
|
||||
type: 'select',
|
||||
placeholder: '请选择工单状态',
|
||||
options: [
|
||||
{ label: '待接单', value: 'pending' },
|
||||
{ label: '已接单', value: 'accepted' },
|
||||
{ label: '处理中', value: 'processing' },
|
||||
{ label: '已挂起', value: 'suspended' },
|
||||
{ label: '已解决', value: 'resolved' },
|
||||
{ label: '已关闭', value: 'closed' },
|
||||
{ label: '已撤回', value: 'cancelled' },
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'priority',
|
||||
label: '优先级',
|
||||
type: 'select',
|
||||
placeholder: '请选择优先级',
|
||||
options: [
|
||||
{ label: '低', value: 'low' },
|
||||
{ label: '中', value: 'medium' },
|
||||
{ label: '高', value: 'high' },
|
||||
{ label: '紧急', value: 'urgent' },
|
||||
],
|
||||
},
|
||||
]
|
||||
639
src/views/ops/pages/feedback/undo/index.vue
Normal file
639
src/views/ops/pages/feedback/undo/index.vue
Normal file
@@ -0,0 +1,639 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<a-tabs v-model:active-key="activeTab" @change="handleTabChange">
|
||||
<a-tab-pane key="pending" title="待处理">
|
||||
<search-table
|
||||
:form-model="formModel"
|
||||
:form-items="formItems"
|
||||
:data="tableData"
|
||||
:columns="pendingColumns"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
title="待处理工单"
|
||||
search-button-text="查询"
|
||||
reset-button-text="重置"
|
||||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
@page-change="handlePageChange"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #toolbar-left>
|
||||
<a-button type="primary" @click="handleCreate">
|
||||
<template #icon>
|
||||
<icon-plus />
|
||||
</template>
|
||||
新建工单
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<!-- 工单类型 -->
|
||||
<template #type="{ record }">
|
||||
<a-tag :color="typeMap[record.type]?.color || 'gray'">
|
||||
{{ typeMap[record.type]?.text || record.type }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 工单状态 -->
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="statusMap[record.status]?.color || 'gray'">
|
||||
{{ statusMap[record.status]?.text || record.status }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 优先级 -->
|
||||
<template #priority="{ record }">
|
||||
<a-tag :color="priorityMap[record.priority]?.color || 'gray'">
|
||||
{{ priorityMap[record.priority]?.text || record.priority }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 序号 -->
|
||||
<template #index="{ rowIndex }">
|
||||
{{ rowIndex + 1 + (pagination.current - 1) * pagination.pageSize }}
|
||||
</template>
|
||||
|
||||
<!-- 操作 -->
|
||||
<template #actions="{ record }">
|
||||
<a-button type="text" size="small" @click="handleDetail(record)">
|
||||
详情
|
||||
</a-button>
|
||||
<a-button type="text" size="small" @click="handleTransfer(record)">
|
||||
转交
|
||||
</a-button>
|
||||
<a-button type="text" size="small" @click="handleSuspend(record)">
|
||||
挂起
|
||||
</a-button>
|
||||
<a-button v-if="record.status === 'suspended'" type="text" size="small" @click="handleResume(record)">
|
||||
重启
|
||||
</a-button>
|
||||
<a-button type="text" size="small" @click="handleResolve(record)">
|
||||
解决
|
||||
</a-button>
|
||||
<a-button type="text" size="small" @click="handleClose(record)">
|
||||
关闭
|
||||
</a-button>
|
||||
</template>
|
||||
</search-table>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="my-created" title="我创建的">
|
||||
<search-table
|
||||
:form-model="formModel"
|
||||
:form-items="formItems"
|
||||
:data="tableData"
|
||||
:columns="myCreatedColumns"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
title="我创建的工单"
|
||||
search-button-text="查询"
|
||||
reset-button-text="重置"
|
||||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
@page-change="handlePageChange"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #toolbar-left>
|
||||
<a-button type="primary" @click="handleCreate">
|
||||
<template #icon>
|
||||
<icon-plus />
|
||||
</template>
|
||||
新建工单
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<!-- 工单类型 -->
|
||||
<template #type="{ record }">
|
||||
<a-tag :color="typeMap[record.type]?.color || 'gray'">
|
||||
{{ typeMap[record.type]?.text || record.type }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 工单状态 -->
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="statusMap[record.status]?.color || 'gray'">
|
||||
{{ statusMap[record.status]?.text || record.status }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 优先级 -->
|
||||
<template #priority="{ record }">
|
||||
<a-tag :color="priorityMap[record.priority]?.color || 'gray'">
|
||||
{{ priorityMap[record.priority]?.text || record.priority }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 序号 -->
|
||||
<template #index="{ rowIndex }">
|
||||
{{ rowIndex + 1 + (pagination.current - 1) * pagination.pageSize }}
|
||||
</template>
|
||||
|
||||
<!-- 操作 -->
|
||||
<template #actions="{ record }">
|
||||
<a-button type="text" size="small" @click="handleDetail(record)">
|
||||
详情
|
||||
</a-button>
|
||||
<a-button v-if="!['resolved', 'closed'].includes(record.status)" type="text" size="small" @click="handleEdit(record)">
|
||||
编辑
|
||||
</a-button>
|
||||
<a-button type="text" size="small" @click="handleCancel(record)">
|
||||
撤回
|
||||
</a-button>
|
||||
<a-button v-if="record.status === 'resolved'" type="text" size="small" @click="handleComment(record)">
|
||||
评论
|
||||
</a-button>
|
||||
<a-button type="text" size="small" @click="handleClose(record)">
|
||||
关闭
|
||||
</a-button>
|
||||
<a-button type="text" size="small" status="danger" @click="handleDelete(record)">
|
||||
删除
|
||||
</a-button>
|
||||
</template>
|
||||
</search-table>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
|
||||
<!-- 工单详情对话框 -->
|
||||
<ticket-detail-dialog
|
||||
v-model:visible="detailVisible"
|
||||
:ticket-id="currentTicket?.id"
|
||||
@refresh="handleDetailSuccess"
|
||||
/>
|
||||
|
||||
<!-- 转交对话框 -->
|
||||
<transfer-dialog
|
||||
v-model:visible="transferVisible"
|
||||
:ticket="currentTicket"
|
||||
@success="handleTransferSuccess"
|
||||
/>
|
||||
|
||||
<!-- 挂起对话框 -->
|
||||
<suspend-dialog
|
||||
v-model:visible="suspendVisible"
|
||||
:ticket="currentTicket"
|
||||
@success="handleSuspendSuccess"
|
||||
/>
|
||||
|
||||
<!-- 解决对话框 -->
|
||||
<resolve-dialog
|
||||
v-model:visible="resolveVisible"
|
||||
:ticket="currentTicket"
|
||||
@success="handleResolveSuccess"
|
||||
/>
|
||||
|
||||
<!-- 评论对话框 -->
|
||||
<comment-dialog
|
||||
v-model:visible="commentVisible"
|
||||
:ticket="currentTicket"
|
||||
@success="handleCommentSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import { IconPlus } from '@arco-design/web-vue/es/icon'
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
import SearchTable from '@/components/search-table/index.vue'
|
||||
import { searchFormConfig } from './config/search-form'
|
||||
import {
|
||||
fetchFeedbackTickets,
|
||||
transferFeedbackTicket,
|
||||
suspendFeedbackTicket,
|
||||
resumeFeedbackTicket,
|
||||
resolveFeedbackTicket,
|
||||
cancelFeedbackTicket,
|
||||
closeFeedbackTicket,
|
||||
deleteFeedbackTicket,
|
||||
} from '@/api/ops/feedbackTicket'
|
||||
import TicketDetailDialog from '../components/TicketDetailDialog.vue'
|
||||
import TransferDialog from '../components/TransferDialog.vue'
|
||||
import SuspendDialog from '../components/SuspendDialog.vue'
|
||||
import ResolveDialog from '../components/ResolveDialog.vue'
|
||||
import CommentDialog from '../components/CommentDialog.vue'
|
||||
|
||||
// 工单状态映射
|
||||
const statusMap: Record<string, { text: string; color: string }> = {
|
||||
pending: { text: '待接单', color: 'blue' },
|
||||
accepted: { text: '已接单', color: 'cyan' },
|
||||
processing: { text: '处理中', color: 'orange' },
|
||||
suspended: { text: '已挂起', color: 'gray' },
|
||||
resolved: { text: '已解决', color: 'green' },
|
||||
closed: { text: '已关闭', color: 'red' },
|
||||
cancelled: { text: '已撤回', color: 'red' },
|
||||
}
|
||||
|
||||
// 工单类型映射
|
||||
const typeMap: Record<string, { text: string; color: string }> = {
|
||||
incident: { text: '故障', color: 'red' },
|
||||
request: { text: '请求', color: 'blue' },
|
||||
question: { text: '问题', color: 'orange' },
|
||||
other: { text: '其他', color: 'gray' },
|
||||
}
|
||||
|
||||
// 优先级映射
|
||||
const priorityMap: Record<string, { text: string; color: string }> = {
|
||||
low: { text: '低', color: 'gray' },
|
||||
medium: { text: '中', color: 'blue' },
|
||||
high: { text: '高', color: 'orange' },
|
||||
urgent: { text: '紧急', color: 'red' },
|
||||
}
|
||||
|
||||
// 当前激活的选项卡
|
||||
const activeTab = ref<'pending' | 'my-created'>('pending')
|
||||
|
||||
// 状态管理
|
||||
const loading = ref(false)
|
||||
const tableData = ref<any[]>([])
|
||||
const formModel = ref({
|
||||
keyword: '',
|
||||
type: '',
|
||||
priority: '',
|
||||
status: '',
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
})
|
||||
|
||||
// 表单项配置
|
||||
const formItems = computed<FormItem[]>(() => searchFormConfig)
|
||||
|
||||
// 待处理选项卡的表格列配置
|
||||
const pendingColumns = computed<TableColumnData[]>(() => [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'index',
|
||||
slotName: 'index',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '工单编号',
|
||||
dataIndex: 'ticket_no',
|
||||
width: 160,
|
||||
},
|
||||
{
|
||||
title: '工单标题',
|
||||
dataIndex: 'title',
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: '工单类型',
|
||||
dataIndex: 'type',
|
||||
slotName: 'type',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '工单状态',
|
||||
dataIndex: 'status',
|
||||
slotName: 'status',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '优先级',
|
||||
dataIndex: 'priority',
|
||||
slotName: 'priority',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'actions',
|
||||
slotName: 'actions',
|
||||
width: 280,
|
||||
fixed: 'right' as const,
|
||||
},
|
||||
])
|
||||
|
||||
// 我创建的选项卡的表格列配置
|
||||
const myCreatedColumns = computed<TableColumnData[]>(() => [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'index',
|
||||
slotName: 'index',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '工单编号',
|
||||
dataIndex: 'ticket_no',
|
||||
width: 160,
|
||||
},
|
||||
{
|
||||
title: '工单标题',
|
||||
dataIndex: 'title',
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: '工单类型',
|
||||
dataIndex: 'type',
|
||||
slotName: 'type',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '工单状态',
|
||||
dataIndex: 'status',
|
||||
slotName: 'status',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '优先级',
|
||||
dataIndex: 'priority',
|
||||
slotName: 'priority',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'actions',
|
||||
slotName: 'actions',
|
||||
width: 300,
|
||||
fixed: 'right' as const,
|
||||
},
|
||||
])
|
||||
|
||||
// 当前选中的工单
|
||||
const currentTicket = ref<any>(null)
|
||||
|
||||
// 对话框可见性
|
||||
const detailVisible = ref(false)
|
||||
const transferVisible = ref(false)
|
||||
const suspendVisible = ref(false)
|
||||
const resolveVisible = ref(false)
|
||||
const commentVisible = ref(false)
|
||||
|
||||
// 获取工单列表
|
||||
const fetchTickets = async () => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const params: any = {
|
||||
page: pagination.current,
|
||||
page_size: pagination.pageSize,
|
||||
keyword: formModel.value.keyword || undefined,
|
||||
type: formModel.value.type || undefined,
|
||||
priority: formModel.value.priority || undefined,
|
||||
status: formModel.value.status || undefined,
|
||||
}
|
||||
|
||||
if (activeTab.value === 'pending') {
|
||||
// 待处理工单:分配给当前用户且状态为 pending/accepted/processing/suspended
|
||||
params.status = 'accepted,processing,suspended'
|
||||
} else {
|
||||
// 我创建的工单:由当前用户创建
|
||||
// 这里需要从用户信息中获取 creator_id
|
||||
// params.creator_id = userStore.userId
|
||||
}
|
||||
|
||||
const res = await fetchFeedbackTickets(params)
|
||||
|
||||
tableData.value = res.details?.data || []
|
||||
pagination.total = res.details?.total || 0
|
||||
} catch (error) {
|
||||
console.error('获取工单列表失败:', error)
|
||||
Message.error('获取工单列表失败')
|
||||
tableData.value = []
|
||||
pagination.total = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 选项卡切换
|
||||
const handleTabChange = (key: string) => {
|
||||
console.log('切换到选项卡:', key)
|
||||
activeTab.value = key as 'pending' | 'my-created'
|
||||
pagination.current = 1
|
||||
fetchTickets()
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchTickets()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
formModel.value = {
|
||||
keyword: '',
|
||||
type: '',
|
||||
priority: '',
|
||||
status: '',
|
||||
}
|
||||
pagination.current = 1
|
||||
fetchTickets()
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (current: number) => {
|
||||
pagination.current = current
|
||||
fetchTickets()
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const handleRefresh = () => {
|
||||
fetchTickets()
|
||||
Message.success('数据已刷新')
|
||||
}
|
||||
|
||||
// 新建工单
|
||||
const handleCreate = () => {
|
||||
// TODO: 打开新建工单对话框
|
||||
Message.info('新建工单功能待实现')
|
||||
}
|
||||
|
||||
// 详情
|
||||
const handleDetail = (record: any) => {
|
||||
console.log('查看详情:', record)
|
||||
currentTicket.value = record
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (record: any) => {
|
||||
console.log('编辑工单:', record)
|
||||
// TODO: 打开编辑对话框
|
||||
Message.info('编辑工单功能待实现')
|
||||
}
|
||||
|
||||
// 转交
|
||||
const handleTransfer = (record: any) => {
|
||||
console.log('转交工单:', record)
|
||||
currentTicket.value = record
|
||||
transferVisible.value = true
|
||||
}
|
||||
|
||||
// 挂起
|
||||
const handleSuspend = (record: any) => {
|
||||
console.log('挂起工单:', record)
|
||||
currentTicket.value = record
|
||||
suspendVisible.value = true
|
||||
}
|
||||
|
||||
// 重启
|
||||
const handleResume = async (record: any) => {
|
||||
console.log('重启工单:', record)
|
||||
try {
|
||||
Modal.confirm({
|
||||
title: '确认重启',
|
||||
content: `确认重启工单 ${record.ticket_no} 吗?`,
|
||||
onOk: async () => {
|
||||
const res = await resumeFeedbackTicket(record.id)
|
||||
if (res.code === 0) {
|
||||
Message.success('重启成功')
|
||||
fetchTickets()
|
||||
} else {
|
||||
Message.error(res.message || '重启失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('重启工单失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 解决
|
||||
const handleResolve = (record: any) => {
|
||||
console.log('解决工单:', record)
|
||||
currentTicket.value = record
|
||||
resolveVisible.value = true
|
||||
}
|
||||
|
||||
// 撤回
|
||||
const handleCancel = async (record: any) => {
|
||||
console.log('撤回工单:', record)
|
||||
try {
|
||||
Modal.confirm({
|
||||
title: '确认撤回',
|
||||
content: `确认撤回工单 ${record.ticket_no} 吗?`,
|
||||
onOk: async () => {
|
||||
const res = await cancelFeedbackTicket(record.id)
|
||||
if (res.code === 0) {
|
||||
Message.success('撤回成功')
|
||||
fetchTickets()
|
||||
} else {
|
||||
Message.error(res.message || '撤回失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('撤回工单失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 评论
|
||||
const handleComment = (record: any) => {
|
||||
console.log('评论工单:', record)
|
||||
currentTicket.value = record
|
||||
commentVisible.value = true
|
||||
}
|
||||
|
||||
// 关闭
|
||||
const handleClose = async (record: any) => {
|
||||
console.log('关闭工单:', record)
|
||||
try {
|
||||
Modal.confirm({
|
||||
title: '确认关闭',
|
||||
content: `确认关闭工单 ${record.ticket_no} 吗?`,
|
||||
onOk: async () => {
|
||||
const res = await closeFeedbackTicket(record.id, {})
|
||||
if (res.code === 0) {
|
||||
Message.success('关闭成功')
|
||||
fetchTickets()
|
||||
} else {
|
||||
Message.error(res.message || '关闭失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('关闭工单失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (record: any) => {
|
||||
console.log('删除工单:', record)
|
||||
try {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确认删除工单 ${record.ticket_no} 吗?此操作不可恢复。`,
|
||||
okText: '删除',
|
||||
okButtonProps: { status: 'danger' },
|
||||
onOk: async () => {
|
||||
const res = await deleteFeedbackTicket(record.id)
|
||||
if (res.code === 0) {
|
||||
Message.success('删除成功')
|
||||
fetchTickets()
|
||||
} else {
|
||||
Message.error(res.message || '删除失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('删除工单失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 详情成功回调
|
||||
const handleDetailSuccess = () => {
|
||||
detailVisible.value = false
|
||||
fetchTickets()
|
||||
}
|
||||
|
||||
// 转交成功回调
|
||||
const handleTransferSuccess = () => {
|
||||
transferVisible.value = false
|
||||
fetchTickets()
|
||||
}
|
||||
|
||||
// 挂起成功回调
|
||||
const handleSuspendSuccess = () => {
|
||||
suspendVisible.value = false
|
||||
fetchTickets()
|
||||
}
|
||||
|
||||
// 解决成功回调
|
||||
const handleResolveSuccess = () => {
|
||||
resolveVisible.value = false
|
||||
fetchTickets()
|
||||
}
|
||||
|
||||
// 评论成功回调
|
||||
const handleCommentSuccess = () => {
|
||||
commentVisible.value = false
|
||||
fetchTickets()
|
||||
}
|
||||
|
||||
// 初始化加载数据
|
||||
fetchTickets()
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'FeedbackUndo',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.feedback-undo-container {
|
||||
padding: 0 20px 20px 20px;
|
||||
background-color: var(--color-bg-2);
|
||||
min-height: 100%;
|
||||
|
||||
:deep(.arco-tabs) {
|
||||
.arco-tabs-nav {
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.arco-tabs-content {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -16,11 +16,12 @@ export const getColumns = (): TableColumnData[] => [
|
||||
dataIndex: 'index',
|
||||
width: 60,
|
||||
align: 'center',
|
||||
slotName: 'index',
|
||||
},
|
||||
{
|
||||
title: '分组名称',
|
||||
dataIndex: 'name',
|
||||
width: 250,
|
||||
width: 150,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
@@ -33,7 +34,7 @@ export const getColumns = (): TableColumnData[] => [
|
||||
{
|
||||
title: '分组路径',
|
||||
dataIndex: 'path',
|
||||
width: 300,
|
||||
width: 150,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
@@ -75,6 +76,7 @@ export const getColumns = (): TableColumnData[] => [
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'description',
|
||||
width: 180,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<Breadcrumb :items="['运维管理', '网络架构', '拓扑分组']" />
|
||||
|
||||
<SearchTable
|
||||
title="拓扑分组管理"
|
||||
:form-model="searchForm"
|
||||
:form-items="filters"
|
||||
@update:form-model="handleFormModelUpdate"
|
||||
:data="tableData"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
@@ -40,14 +40,14 @@
|
||||
添加子分组
|
||||
</a-button>
|
||||
<!-- 详情:只在有父分组时显示 (order: 100) -->
|
||||
<a-button
|
||||
<!-- <a-button
|
||||
v-if="!!record.parent_id && record.parent_id !== 0"
|
||||
type="text"
|
||||
size="small"
|
||||
@click="handleViewDetail(record)"
|
||||
>
|
||||
详情
|
||||
</a-button>
|
||||
</a-button> -->
|
||||
<!-- 拓扑:只在有父分组时显示 (order: 150) -->
|
||||
<a-button
|
||||
v-if="!!record.parent_id && record.parent_id !== 0"
|
||||
@@ -68,6 +68,11 @@
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<!-- 表格自定义列:序号 -->
|
||||
<template #index="{ rowIndex }">
|
||||
{{ rowIndex + 1 + (page - 1) * pageSize }}
|
||||
</template>
|
||||
|
||||
<!-- 层级列 -->
|
||||
<template #level="{ record }">
|
||||
<a-tag
|
||||
@@ -217,6 +222,12 @@ const searchForm = reactive({
|
||||
enable: '',
|
||||
})
|
||||
|
||||
// 处理表单模型更新
|
||||
const handleFormModelUpdate = (newFormModel: Record<string, any>) => {
|
||||
// 合并新的值到 searchForm 中,而不是替换整个对象
|
||||
Object.assign(searchForm, newFormModel)
|
||||
}
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<TopologyGroup[]>([])
|
||||
const loading = ref(false)
|
||||
@@ -235,7 +246,7 @@ const formData = reactive<Partial<TopologyGroup> & { parent_id: number }>({
|
||||
name: '',
|
||||
description: '',
|
||||
sort: 0,
|
||||
enable: true,
|
||||
enable: 2,
|
||||
parent_id: 0,
|
||||
})
|
||||
|
||||
@@ -265,6 +276,7 @@ const currentParentGroup = ref<{ id: number; name: string } | null>(null)
|
||||
|
||||
// 获取数据
|
||||
const fetchData = async () => {
|
||||
console.log('searchForm', searchForm)
|
||||
try {
|
||||
loading.value = true
|
||||
const res = await fetchTopologyGroups({
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user