diff --git a/.gitignore b/.gitignore index a547bf3..fe7027e 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ dist-ssr # Editor directories and files .vscode/* +docs/* !.vscode/extensions.json .idea .DS_Store diff --git a/src/api/ops/asset.ts b/src/api/ops/asset.ts index 1f37b68..a15b398 100644 --- a/src/api/ops/asset.ts +++ b/src/api/ops/asset.ts @@ -86,6 +86,11 @@ export const fetchAssetList = (data?: AssetListParams) => { return request.post("/Assets/v1/asset/list", data || {}); }; +/** 获取资产列表(不分页,下拉) */ +export const fetchAssetAll = (params?: { keyword?: string }) => { + return request.get("/Assets/v1/asset/all", { params }); +}; + /** 获取资产详情 */ export const fetchAssetDetail = (id: number) => { return request.get(`/Assets/v1/asset/detail/${id}`); diff --git a/src/api/ops/datacenter.ts b/src/api/ops/datacenter.ts index dab87dd..bbc4a63 100644 --- a/src/api/ops/datacenter.ts +++ b/src/api/ops/datacenter.ts @@ -38,14 +38,14 @@ export const deleteDatacenter = (id: number) => { }; /** 获取省份列表(用于下拉选择) */ -export const fetchProvinceList = () => { - return request.get("/Assets/v1/province/all"); -}; +export const fetchProvinceList = (params?: { keyword?: string }) => { + return request.get('/Assets/v1/province/all', { params }) +} /** 获取城市列表(用于下拉选择) */ -export const fetchCityList = () => { - return request.get("/Assets/v1/city/all"); -}; +export const fetchCityList = (provinceId: number, params?: { keyword?: string }) => { + return request.get(`/Assets/v1/city/province/${provinceId}`, { params }) +} /** 根据城市获取数据中心列表 */ export const fetchDatacenterByCity = (cityId: number) => { diff --git a/src/api/ops/feedbackTicket.ts b/src/api/ops/feedbackTicket.ts index 5bc1e15..02b9e98 100644 --- a/src/api/ops/feedbackTicket.ts +++ b/src/api/ops/feedbackTicket.ts @@ -1,25 +1,91 @@ -import { request } from "@/api/request"; +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; +/** + * 后端要求 status 为字符串数组;统一为 string[](单选、逗号分隔写法也转成多项)。 + */ +function normalizeTicketListStatusParam(params: Record) { + const raw = params.status + if (raw === undefined || raw === null) return + let arr: string[] + if (Array.isArray(raw)) { + arr = raw.map((s) => String(s).trim()).filter(Boolean) + } else { + const s = String(raw).trim() + if (!s) { + delete params.status + return + } + arr = s.includes(',') ? s.split(',').map((x) => x.trim()).filter(Boolean) : [s] } - return request.get("/Feedback/v1/tickets", params ? { params } : undefined); -}; + if (arr.length === 0) delete params.status + else params.status = arr +} + +/** GET query:`status` 重复键 status=a&status=b(与后端 []string 绑定一致) */ +function serializeFeedbackTicketListParams(params: Record): string { + const usp = new URLSearchParams() + const keys = Object.keys(params).sort() + for (const key of keys) { + const value = params[key] + if (value === undefined || value === null || value === '') continue + if (key === 'status' && Array.isArray(value)) { + for (const item of value) { + const t = String(item).trim() + if (t) usp.append('status', t) + } + continue + } + usp.append(key, String(value)) + } + return usp.toString() +} + +/** 避免 undefined / null / 空串进入 query(部分运行时下发异常键名) */ +function stripEmptyQueryValues(params: Record) { + Object.keys(params).forEach((k) => { + const v = params[k] + if (v === undefined || v === null || v === '') { + delete params[k] + } + }) +} + +/** 获取工单列表 GET /Feedback/v1/tickets(分页与多条件筛选) */ +export const fetchFeedbackTickets = (data?: { + page?: number + page_size?: number + size?: number + /** 单状态、逗号分隔、或 string[](将统一为数组并以重复 query 键序列化) */ + status?: string | string[] + type?: string + priority?: string + creator_id?: number + assignee_id?: number + category_id?: number + keyword?: string + /** 格式 YYYY-MM-DD HH:MM:SS */ + start_time?: string + /** 格式 YYYY-MM-DD HH:MM:SS */ + end_time?: string +}) => { + const params: any = data ? { ...data } : {} + if (params.size !== undefined && params.page_size === undefined) { + params.page_size = params.size + delete params.size + } + normalizeTicketListStatusParam(params) + stripEmptyQueryValues(params) + const config = + Object.keys(params).length > 0 + ? { + params, + paramsSerializer: { + serialize: (p: Record) => serializeFeedbackTicketListParams(p), + }, + } + : undefined + return request.get('/Feedback/v1/tickets', config) +} /** 创建 工单 */ export const createFeedbackTicket = (data: any) => request.post("/Feedback/v1/tickets", data); diff --git a/src/api/ops/floor.ts b/src/api/ops/floor.ts index a9179ad..dec3140 100644 --- a/src/api/ops/floor.ts +++ b/src/api/ops/floor.ts @@ -16,6 +16,14 @@ export const fetchFloorDetail = (id: number) => { return request.get(`/Assets/v1/floor/detail/${id}`); }; +/** 根据数据中心获取楼层列表(下拉,不分页) */ +export const fetchFloorListByDatacenter = ( + datacenterId: number, + params?: { name?: string } +) => { + return request.get(`/Assets/v1/floor/datacenter/${datacenterId}`, { params }); +}; + /** 创建楼层 */ export const createFloor = (data: any) => { return request.post("/Assets/v1/floor/create", data); @@ -32,6 +40,9 @@ export const deleteFloor = (id: number) => { }; /** 获取数据中心列表(用于下拉选择) */ -export const fetchDatacenterList = () => { - return request.get("/Assets/v1/datacenter/all"); +export const fetchDatacenterList = (params?: { keyword?: string; name?: string }) => { + const normalizedParams = params?.keyword + ? { ...params, name: params.name ?? params.keyword } + : params + return request.get("/Assets/v1/datacenter/all", { params: normalizedParams }); }; diff --git a/src/api/ops/rack.ts b/src/api/ops/rack.ts index ce4cb7d..b598206 100644 --- a/src/api/ops/rack.ts +++ b/src/api/ops/rack.ts @@ -23,6 +23,14 @@ export const fetchRackListByDatacenter = ( return request.get(`/Assets/v1/rack/datacenter/${datacenterId}`, { params }); }; +/** 根据楼层获取机柜列表(下拉,不分页) */ +export const fetchRackListByFloor = ( + floorId: number, + params?: { name?: string } +) => { + return request.get(`/Assets/v1/rack/floor/${floorId}`, { params }); +}; + /** 获取机柜详情 */ export const fetchRackDetail = (id: number) => { return request.get(`/Assets/v1/rack/detail/${id}`); @@ -49,11 +57,17 @@ export const fetchSupplierList = () => { }; /** 获取数据中心列表(用于下拉选择) */ -export const fetchDatacenterList = () => { - return request.get("/Assets/v1/datacenter/all"); +export const fetchDatacenterList = (params?: { keyword?: string; name?: string }) => { + const normalizedParams = params?.keyword + ? { ...params, name: params.name ?? params.keyword } + : params + return request.get("/Assets/v1/datacenter/all", { params: normalizedParams }); }; /** 获取楼层列表(用于下拉选择) */ -export const fetchFloorListForSelect = (datacenterId?: number) => { - return request.get("/Assets/v1/floor/all", { params: { datacenter_id: datacenterId } }); +export const fetchFloorListForSelect = (params?: { datacenter_id?: number; keyword?: string; name?: string }) => { + const normalizedParams = params?.keyword + ? { ...params, name: params.name ?? params.keyword } + : params + return request.get("/Assets/v1/floor/all", { params: normalizedParams }); }; diff --git a/src/components/search-form/index.vue b/src/components/search-form/index.vue index d2ec127..d22298a 100644 --- a/src/components/search-form/index.vue +++ b/src/components/search-form/index.vue @@ -24,6 +24,8 @@ :options="item.options" :placeholder="item.placeholder || '请选择'" :disabled="item.disabled" + :allow-search="item.allowSearch" + @search="handleSelectSearch(item, $event)" allow-clear /> @@ -134,6 +136,10 @@ const handleSearch = () => { const handleReset = () => { emit('reset') } + +const handleSelectSearch = (item: FormItem, keyword: string) => { + item.onSearch?.(keyword) +} diff --git a/src/views/ops/pages/datacenter/rack/components/RackFormDialog.vue b/src/views/ops/pages/datacenter/rack/components/RackFormDialog.vue index b125160..1dee5a2 100644 --- a/src/views/ops/pages/datacenter/rack/components/RackFormDialog.vue +++ b/src/views/ops/pages/datacenter/rack/components/RackFormDialog.vue @@ -75,6 +75,7 @@ placeholder="请选择所属楼层" :loading="loadingFloors" allow-search + @search="handleFloorSearch" > ([]) const floorList = ref([]) const supplierList = ref([]) +let floorSearchTimer: number | undefined // 表单数据 const form = ref({ @@ -557,13 +559,28 @@ const loadDatacenterList = async () => { } } -// 加载楼层列表 -const loadFloorList = async (datacenterId?: number) => { +// 加载楼层列表(通过机柜下拉接口提取去重楼层) +const loadFloorList = async (datacenterId?: number, keyword?: string) => { + if (!datacenterId) { + floorList.value = [] + return + } loadingFloors.value = true try { - const res: any = await fetchFloorListForSelect(datacenterId) + const res: any = await fetchRackListByDatacenter(datacenterId, { name: keyword }) if (res.code === 0) { - floorList.value = res.details || [] + const list = res.details?.data ?? res.data ?? res.details ?? [] + const rows = Array.isArray(list) ? list : [] + const floorMap = new Map() + rows.forEach((rack: any) => { + const floor = rack?.floor + if (!floor?.id || floorMap.has(floor.id)) return + floorMap.set(floor.id, { + id: floor.id, + name: floor.name || String(floor.id), + }) + }) + floorList.value = Array.from(floorMap.values()) } } catch (error) { console.error('获取楼层列表失败:', error) @@ -593,6 +610,16 @@ const handleDatacenterChange = async (value: number) => { await loadFloorList(value) } +const handleFloorSearch = (keyword: string) => { + if (!form.value.datacenter_id) return + if (floorSearchTimer) { + window.clearTimeout(floorSearchTimer) + } + floorSearchTimer = window.setTimeout(() => { + loadFloorList(form.value.datacenter_id, keyword?.trim() || undefined) + }, 300) +} + // 监听对话框显示状态 watch( () => props.visible, diff --git a/src/views/ops/pages/datacenter/rack/index.vue b/src/views/ops/pages/datacenter/rack/index.vue index eef0551..0ea8806 100644 --- a/src/views/ops/pages/datacenter/rack/index.vue +++ b/src/views/ops/pages/datacenter/rack/index.vue @@ -57,7 +57,7 @@ + + + + {{ datacenter.label }} + + + + + + + {{ floor.label }} + + + @@ -179,17 +221,31 @@ import { releaseUnit, updateUnitStatus, } from '@/api/ops/unit' -import { fetchRackList } from '@/api/ops/rack' +import { + fetchDatacenterList, + fetchRackListByFloor, +} from '@/api/ops/rack' +import { fetchFloorListByDatacenter } from '@/api/ops/floor' +import { normalizeUnitList } from './utils/unitFallback' import AllocateUnitDialog from './components/AllocateUnitDialog.vue' import ReserveUnitDialog from './components/ReserveUnitDialog.vue' // 状态管理 const loading = ref(false) const rackListLoading = ref(false) +const datacenterListLoading = ref(false) +const floorListLoading = ref(false) +const selectedDatacenterId = ref(undefined) +const selectedFloorId = ref(undefined) const selectedRackId = ref(undefined) const rackInfo = ref({}) +const datacenterList = ref<{ label: string; value: number }[]>([]) +const floorList = ref<{ label: string; value: number }[]>([]) const rackList = ref([]) const unitList = ref([]) +let datacenterSearchTimer: number | undefined +let floorSearchTimer: number | undefined +let rackSearchTimer: number | undefined // 对话框可见性 const allocateVisible = ref(false) @@ -213,6 +269,16 @@ const usagePercentage = computed(() => { return Math.round((usedUnits.value / rackInfo.value.height) * 100) }) +const extractList = (res: any): any[] => { + const candidate = + res?.data?.data ?? + res?.details?.data ?? + res?.data ?? + res?.details ?? + [] + return Array.isArray(candidate) ? candidate : [] +} + // 获取U位状态颜色 const getUnitStatusColor = (status?: string) => { const colorMap: Record = { @@ -236,17 +302,16 @@ const getUnitStatusText = (status?: string) => { } // 获取机柜列表 -const fetchRacks = async () => { +const fetchRacks = async (keyword?: string) => { + if (!selectedFloorId.value) { + rackList.value = [] + return + } rackListLoading.value = true try { - const res = await fetchRackList({ - page: 1, - page_size: 1000, - status: 'in_use', // 只获取使用中的机柜 - }) - - rackList.value = res.details?.data || [] + const res: any = await fetchRackListByFloor(selectedFloorId.value, { name: keyword }) + rackList.value = extractList(res) } catch (error) { console.error('获取机柜列表失败:', error) Message.error('获取机柜列表失败') @@ -256,6 +321,101 @@ const fetchRacks = async () => { } } +const fetchDatacenters = async (keyword?: string) => { + datacenterListLoading.value = true + try { + const res: any = await fetchDatacenterList({ keyword }) + const rows = extractList(res) + datacenterList.value = rows.map((d: any) => ({ + label: d.name || d.code || String(d.id), + value: d.id, + })) + } catch (error) { + console.error('获取数据中心列表失败:', error) + Message.error('获取数据中心列表失败') + datacenterList.value = [] + } finally { + datacenterListLoading.value = false + } +} + +const fetchFloors = async (keyword?: string) => { + if (!selectedDatacenterId.value) { + floorList.value = [] + return + } + floorListLoading.value = true + try { + const res: any = await fetchFloorListByDatacenter(selectedDatacenterId.value, { + name: keyword || undefined, + }) + const rows = extractList(res) + floorList.value = rows.map((floor: any) => ({ + label: floor.name || floor.code || String(floor.id), + value: floor.id, + })) + } catch (error) { + console.error('获取楼层列表失败:', error) + Message.error('获取楼层列表失败') + floorList.value = [] + } finally { + floorListLoading.value = false + } +} + +const handleDatacenterSearch = (keyword: string) => { + if (datacenterSearchTimer) { + window.clearTimeout(datacenterSearchTimer) + } + datacenterSearchTimer = window.setTimeout(() => { + fetchDatacenters(keyword?.trim() || undefined) + }, 300) +} + +const handleFloorSearch = (keyword: string) => { + if (!selectedDatacenterId.value) return + if (floorSearchTimer) { + window.clearTimeout(floorSearchTimer) + } + floorSearchTimer = window.setTimeout(() => { + fetchFloors(keyword?.trim() || undefined) + }, 300) +} + +const handleRackSearch = (keyword: string) => { + if (!selectedFloorId.value) return + if (rackSearchTimer) { + window.clearTimeout(rackSearchTimer) + } + rackSearchTimer = window.setTimeout(() => { + fetchRacks(keyword?.trim() || undefined) + }, 300) +} + +const handleDatacenterChange = async (datacenterId?: number | string) => { + if (datacenterId !== undefined && datacenterId !== null && datacenterId !== '') { + selectedDatacenterId.value = Number(datacenterId) + } + selectedFloorId.value = undefined + selectedRackId.value = undefined + rackInfo.value = {} + rackList.value = [] + floorList.value = [] + unitList.value = [] + await fetchFloors() +} + +const handleFloorChange = async (floorId?: number | string) => { + if (floorId !== undefined && floorId !== null && floorId !== '') { + selectedFloorId.value = Number(floorId) + } + selectedRackId.value = undefined + rackInfo.value = {} + rackList.value = [] + unitList.value = [] + await fetchRacks() +} + // 机柜变化 const handleRackChange = (rackId: number) => { if (rackId) { @@ -278,8 +438,9 @@ const fetchUnits = async (rackId?: number) => { const res = await fetchUnitList(targetRackId) if (res.code === 0) { - rackInfo.value = res.data?.rack || {} - unitList.value = res.data?.units || [] + const payload = res?.details ?? res?.data ?? {} + rackInfo.value = payload?.rack || {} + unitList.value = normalizeUnitList(payload?.units, rackInfo.value?.height) } else { Message.error(res.message || '获取U位列表失败') } @@ -453,7 +614,7 @@ const handleCancelReservation = async (record: any) => { // 初始化 onMounted(() => { - fetchRacks() + fetchDatacenters() }) diff --git a/src/views/ops/pages/datacenter/u-position/utils/unitFallback.ts b/src/views/ops/pages/datacenter/u-position/utils/unitFallback.ts new file mode 100644 index 0000000..e8acaf2 --- /dev/null +++ b/src/views/ops/pages/datacenter/u-position/utils/unitFallback.ts @@ -0,0 +1,22 @@ +export const normalizeUnitList = (units: any, rackHeight?: number) => { + if (Array.isArray(units) && units.length > 0) { + return units + } + + const height = Number(rackHeight) || 0 + if (height <= 0) { + return [] + } + + return Array.from({ length: height }, (_, idx) => ({ + id: `virtual-${height - idx}`, + unit_number: height - idx, + status: 'available', + asset_name: '', + asset_code: '', + asset_type: '', + occupied_units: 1, + reserved_for: '', + power_consumption: 0, + })) +} diff --git a/src/views/ops/pages/feedback/all/index.vue b/src/views/ops/pages/feedback/all/index.vue index cba5fe3..e9bb27b 100644 --- a/src/views/ops/pages/feedback/all/index.vue +++ b/src/views/ops/pages/feedback/all/index.vue @@ -1,7 +1,7 @@