This commit is contained in:
ygx
2026-03-15 23:16:00 +08:00
parent 18072d75a4
commit 2835695d78
36 changed files with 6902 additions and 17 deletions

26
src/api/common/fts.ts Normal file
View 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;

View 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
View 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
View 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");

View 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
View 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
View 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);
};

View File

@@ -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 }
)

View File

@@ -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)

View File

@@ -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 || '')

View File

@@ -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: ['*'],
},
},
],
}

View File

@@ -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>

View File

@@ -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>

View 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,
},
]

View 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' },
],
},
]

View 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>

View File

@@ -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>

View File

@@ -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>

View 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,
},
]

View 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: [], // 将在组件中动态加载
// },
]

View 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>

View 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,
},
]

View 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',
},
]

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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
}

View 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' },
],
},
]

View 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>

View File

@@ -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',
},
{

View File

@@ -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