fix
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -14,6 +14,7 @@ dist-ssr
|
|||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
|
docs/*
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
.idea
|
.idea
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -86,6 +86,11 @@ export const fetchAssetList = (data?: AssetListParams) => {
|
|||||||
return request.post("/Assets/v1/asset/list", data || {});
|
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) => {
|
export const fetchAssetDetail = (id: number) => {
|
||||||
return request.get(`/Assets/v1/asset/detail/${id}`);
|
return request.get(`/Assets/v1/asset/detail/${id}`);
|
||||||
|
|||||||
@@ -38,14 +38,14 @@ export const deleteDatacenter = (id: number) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/** 获取省份列表(用于下拉选择) */
|
/** 获取省份列表(用于下拉选择) */
|
||||||
export const fetchProvinceList = () => {
|
export const fetchProvinceList = (params?: { keyword?: string }) => {
|
||||||
return request.get("/Assets/v1/province/all");
|
return request.get('/Assets/v1/province/all', { params })
|
||||||
};
|
}
|
||||||
|
|
||||||
/** 获取城市列表(用于下拉选择) */
|
/** 获取城市列表(用于下拉选择) */
|
||||||
export const fetchCityList = () => {
|
export const fetchCityList = (provinceId: number, params?: { keyword?: string }) => {
|
||||||
return request.get("/Assets/v1/city/all");
|
return request.get(`/Assets/v1/city/province/${provinceId}`, { params })
|
||||||
};
|
}
|
||||||
|
|
||||||
/** 根据城市获取数据中心列表 */
|
/** 根据城市获取数据中心列表 */
|
||||||
export const fetchDatacenterByCity = (cityId: number) => {
|
export const fetchDatacenterByCity = (cityId: number) => {
|
||||||
|
|||||||
@@ -1,25 +1,91 @@
|
|||||||
import { request } from "@/api/request";
|
import { request } from '@/api/request'
|
||||||
|
|
||||||
/** 获取 工单列表 */
|
/**
|
||||||
export const fetchFeedbackTickets = (data?: {
|
* 后端要求 status 为字符串数组;统一为 string[](单选、逗号分隔写法也转成多项)。
|
||||||
page?: number,
|
*/
|
||||||
page_size?: number,
|
function normalizeTicketListStatusParam(params: Record<string, any>) {
|
||||||
size?: number,
|
const raw = params.status
|
||||||
keyword?: string,
|
if (raw === undefined || raw === null) return
|
||||||
type?: string,
|
let arr: string[]
|
||||||
priority?: string,
|
if (Array.isArray(raw)) {
|
||||||
status?: string,
|
arr = raw.map((s) => String(s).trim()).filter(Boolean)
|
||||||
creator_id?: number,
|
} else {
|
||||||
assignee_id?: number
|
const s = String(raw).trim()
|
||||||
}) => {
|
if (!s) {
|
||||||
// 兼容 size 参数,转换为 page_size
|
delete params.status
|
||||||
const params: any = data ? { ...data } : {};
|
return
|
||||||
if (params.size !== undefined && params.page_size === undefined) {
|
}
|
||||||
params.page_size = params.size;
|
arr = s.includes(',') ? s.split(',').map((x) => x.trim()).filter(Boolean) : [s]
|
||||||
delete params.size;
|
|
||||||
}
|
}
|
||||||
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, any>): 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<string, any>) {
|
||||||
|
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<string, any>) => serializeFeedbackTicketListParams(p),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
return request.get('/Feedback/v1/tickets', config)
|
||||||
|
}
|
||||||
|
|
||||||
/** 创建 工单 */
|
/** 创建 工单 */
|
||||||
export const createFeedbackTicket = (data: any) => request.post("/Feedback/v1/tickets", data);
|
export const createFeedbackTicket = (data: any) => request.post("/Feedback/v1/tickets", data);
|
||||||
|
|||||||
@@ -16,6 +16,14 @@ export const fetchFloorDetail = (id: number) => {
|
|||||||
return request.get(`/Assets/v1/floor/detail/${id}`);
|
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) => {
|
export const createFloor = (data: any) => {
|
||||||
return request.post("/Assets/v1/floor/create", data);
|
return request.post("/Assets/v1/floor/create", data);
|
||||||
@@ -32,6 +40,9 @@ export const deleteFloor = (id: number) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/** 获取数据中心列表(用于下拉选择) */
|
/** 获取数据中心列表(用于下拉选择) */
|
||||||
export const fetchDatacenterList = () => {
|
export const fetchDatacenterList = (params?: { keyword?: string; name?: string }) => {
|
||||||
return request.get("/Assets/v1/datacenter/all");
|
const normalizedParams = params?.keyword
|
||||||
|
? { ...params, name: params.name ?? params.keyword }
|
||||||
|
: params
|
||||||
|
return request.get("/Assets/v1/datacenter/all", { params: normalizedParams });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,6 +23,14 @@ export const fetchRackListByDatacenter = (
|
|||||||
return request.get(`/Assets/v1/rack/datacenter/${datacenterId}`, { params });
|
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) => {
|
export const fetchRackDetail = (id: number) => {
|
||||||
return request.get(`/Assets/v1/rack/detail/${id}`);
|
return request.get(`/Assets/v1/rack/detail/${id}`);
|
||||||
@@ -49,11 +57,17 @@ export const fetchSupplierList = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/** 获取数据中心列表(用于下拉选择) */
|
/** 获取数据中心列表(用于下拉选择) */
|
||||||
export const fetchDatacenterList = () => {
|
export const fetchDatacenterList = (params?: { keyword?: string; name?: string }) => {
|
||||||
return request.get("/Assets/v1/datacenter/all");
|
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) => {
|
export const fetchFloorListForSelect = (params?: { datacenter_id?: number; keyword?: string; name?: string }) => {
|
||||||
return request.get("/Assets/v1/floor/all", { params: { datacenter_id: datacenterId } });
|
const normalizedParams = params?.keyword
|
||||||
|
? { ...params, name: params.name ?? params.keyword }
|
||||||
|
: params
|
||||||
|
return request.get("/Assets/v1/floor/all", { params: normalizedParams });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,6 +24,8 @@
|
|||||||
:options="item.options"
|
:options="item.options"
|
||||||
:placeholder="item.placeholder || '请选择'"
|
:placeholder="item.placeholder || '请选择'"
|
||||||
:disabled="item.disabled"
|
:disabled="item.disabled"
|
||||||
|
:allow-search="item.allowSearch"
|
||||||
|
@search="handleSelectSearch(item, $event)"
|
||||||
allow-clear
|
allow-clear
|
||||||
/>
|
/>
|
||||||
<!-- 日期范围选择器 -->
|
<!-- 日期范围选择器 -->
|
||||||
@@ -134,6 +136,10 @@ const handleSearch = () => {
|
|||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
emit('reset')
|
emit('reset')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSelectSearch = (item: FormItem, keyword: string) => {
|
||||||
|
item.onSearch?.(keyword)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|||||||
@@ -9,4 +9,8 @@ export interface FormItem {
|
|||||||
options?: SelectOptionData[]
|
options?: SelectOptionData[]
|
||||||
/** 仅对 select 生效 */
|
/** 仅对 select 生效 */
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
/** 仅对 select 生效 */
|
||||||
|
allowSearch?: boolean
|
||||||
|
/** 仅对 select 生效 */
|
||||||
|
onSearch?: (value: string) => void
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,13 @@ export const searchFormConfig: FormItem[] = [
|
|||||||
type: 'input',
|
type: 'input',
|
||||||
placeholder: '请输入楼层名称或编码',
|
placeholder: '请输入楼层名称或编码',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
field: 'datacenter_id',
|
||||||
|
label: '数据中心',
|
||||||
|
type: 'select',
|
||||||
|
placeholder: '请选择数据中心',
|
||||||
|
options: [],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
field: 'status',
|
field: 'status',
|
||||||
label: '状态',
|
label: '状态',
|
||||||
|
|||||||
@@ -72,7 +72,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, reactive, computed } from 'vue'
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
import { Message, Modal } from '@arco-design/web-vue'
|
import { Message, Modal } from '@arco-design/web-vue'
|
||||||
import { IconPlus } from '@arco-design/web-vue/es/icon'
|
import { IconPlus } from '@arco-design/web-vue/es/icon'
|
||||||
import type { FormItem } from '@/components/search-form/types'
|
import type { FormItem } from '@/components/search-form/types'
|
||||||
@@ -81,6 +81,7 @@ import { searchFormConfig } from './config/search-form'
|
|||||||
import { columns as columnsConfig } from './config/columns'
|
import { columns as columnsConfig } from './config/columns'
|
||||||
import {
|
import {
|
||||||
fetchFloorList,
|
fetchFloorList,
|
||||||
|
fetchDatacenterList,
|
||||||
deleteFloor,
|
deleteFloor,
|
||||||
} from '@/api/ops/floor'
|
} from '@/api/ops/floor'
|
||||||
import FloorDetailDialog from './components/FloorDetailDialog.vue'
|
import FloorDetailDialog from './components/FloorDetailDialog.vue'
|
||||||
@@ -101,6 +102,7 @@ const tableData = ref<any[]>([])
|
|||||||
const formModel = ref({
|
const formModel = ref({
|
||||||
keyword: '',
|
keyword: '',
|
||||||
status: '',
|
status: '',
|
||||||
|
datacenter_id: undefined as number | undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
const pagination = reactive({
|
const pagination = reactive({
|
||||||
@@ -110,7 +112,22 @@ const pagination = reactive({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 表单项配置
|
// 表单项配置
|
||||||
const formItems = computed<FormItem[]>(() => searchFormConfig)
|
const datacenterSelectOptions = ref<{ label: string; value: number }[]>([])
|
||||||
|
let datacenterSearchTimer: number | undefined
|
||||||
|
|
||||||
|
const formItems = computed<FormItem[]>(() =>
|
||||||
|
searchFormConfig.map((item) => {
|
||||||
|
if (item.field === 'datacenter_id') {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
options: datacenterSelectOptions.value,
|
||||||
|
allowSearch: true,
|
||||||
|
onSearch: handleDatacenterSearch,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
// 表格列配置
|
// 表格列配置
|
||||||
const columns = computed(() => columnsConfig)
|
const columns = computed(() => columnsConfig)
|
||||||
@@ -123,6 +140,35 @@ const editingFloor = ref<any>(null)
|
|||||||
const formVisible = ref(false)
|
const formVisible = ref(false)
|
||||||
const detailVisible = ref(false)
|
const detailVisible = ref(false)
|
||||||
|
|
||||||
|
// 获取数据中心下拉选项
|
||||||
|
const loadDatacenterOptions = async (keyword?: string) => {
|
||||||
|
try {
|
||||||
|
const res: any = await fetchDatacenterList({ keyword })
|
||||||
|
if (res.code === 0) {
|
||||||
|
const list = res.details || []
|
||||||
|
datacenterSelectOptions.value = Array.isArray(list)
|
||||||
|
? list.map((d: any) => ({
|
||||||
|
label: d.name || d.code || String(d.id),
|
||||||
|
value: d.id,
|
||||||
|
}))
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取数据中心列表失败:', error)
|
||||||
|
Message.error('获取数据中心列表失败')
|
||||||
|
datacenterSelectOptions.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDatacenterSearch = (keyword: string) => {
|
||||||
|
if (datacenterSearchTimer) {
|
||||||
|
window.clearTimeout(datacenterSearchTimer)
|
||||||
|
}
|
||||||
|
datacenterSearchTimer = window.setTimeout(() => {
|
||||||
|
loadDatacenterOptions(keyword?.trim() || undefined)
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
// 获取楼层列表
|
// 获取楼层列表
|
||||||
const fetchFloors = async () => {
|
const fetchFloors = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@@ -132,6 +178,7 @@ const fetchFloors = async () => {
|
|||||||
page: pagination.current,
|
page: pagination.current,
|
||||||
page_size: pagination.pageSize,
|
page_size: pagination.pageSize,
|
||||||
keyword: formModel.value.keyword || undefined,
|
keyword: formModel.value.keyword || undefined,
|
||||||
|
datacenter_id: formModel.value.datacenter_id ?? undefined,
|
||||||
status: formModel.value.status || undefined,
|
status: formModel.value.status || undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,6 +212,7 @@ const handleReset = () => {
|
|||||||
formModel.value = {
|
formModel.value = {
|
||||||
keyword: '',
|
keyword: '',
|
||||||
status: '',
|
status: '',
|
||||||
|
datacenter_id: undefined,
|
||||||
}
|
}
|
||||||
pagination.current = 1
|
pagination.current = 1
|
||||||
fetchFloors()
|
fetchFloors()
|
||||||
@@ -231,7 +279,10 @@ const handleFormSuccess = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 初始化加载数据
|
// 初始化加载数据
|
||||||
fetchFloors()
|
onMounted(() => {
|
||||||
|
loadDatacenterOptions()
|
||||||
|
fetchFloors()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|||||||
@@ -103,10 +103,14 @@
|
|||||||
<a-select
|
<a-select
|
||||||
v-model="form.province_id"
|
v-model="form.province_id"
|
||||||
placeholder="请选择省份"
|
placeholder="请选择省份"
|
||||||
|
allow-search
|
||||||
|
:filter-option="false"
|
||||||
|
:loading="provinceLoading"
|
||||||
@change="handleProvinceChange"
|
@change="handleProvinceChange"
|
||||||
|
@search="handleProvinceSearch"
|
||||||
>
|
>
|
||||||
<a-option
|
<a-option
|
||||||
v-for="item in provinces"
|
v-for="item in provinceOptions"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
:value="item.id"
|
:value="item.id"
|
||||||
>
|
>
|
||||||
@@ -125,9 +129,13 @@
|
|||||||
v-model="form.city_id"
|
v-model="form.city_id"
|
||||||
placeholder="请选择城市"
|
placeholder="请选择城市"
|
||||||
:disabled="!form.province_id"
|
:disabled="!form.province_id"
|
||||||
|
allow-search
|
||||||
|
:filter-option="false"
|
||||||
|
:loading="cityLoading"
|
||||||
|
@search="handleCitySearch"
|
||||||
>
|
>
|
||||||
<a-option
|
<a-option
|
||||||
v-for="item in filteredCities"
|
v-for="item in cityOptions"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
:value="item.id"
|
:value="item.id"
|
||||||
>
|
>
|
||||||
@@ -248,7 +256,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, watch, computed } from 'vue'
|
import { ref, watch, computed } from 'vue'
|
||||||
import { Message } from '@arco-design/web-vue'
|
import { Message } from '@arco-design/web-vue'
|
||||||
import { createDatacenter, updateDatacenter } from '@/api/ops/datacenter'
|
import { createDatacenter, updateDatacenter, fetchProvinceList, fetchCityList } from '@/api/ops/datacenter'
|
||||||
|
|
||||||
interface Datacenter {
|
interface Datacenter {
|
||||||
id?: number
|
id?: number
|
||||||
@@ -277,8 +285,7 @@ interface Datacenter {
|
|||||||
interface Props {
|
interface Props {
|
||||||
visible: boolean
|
visible: boolean
|
||||||
datacenter: Datacenter | null
|
datacenter: Datacenter | null
|
||||||
provinces: any[]
|
provinces?: any[]
|
||||||
cities: any[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Emits {
|
interface Emits {
|
||||||
@@ -319,15 +326,72 @@ const form = ref({
|
|||||||
// 是否为编辑模式
|
// 是否为编辑模式
|
||||||
const isEdit = computed(() => !!props.datacenter?.id)
|
const isEdit = computed(() => !!props.datacenter?.id)
|
||||||
|
|
||||||
// 根据选择的省份过滤城市列表
|
const provinceOptions = ref<any[]>([])
|
||||||
const filteredCities = computed(() => {
|
const cityOptions = ref<any[]>([])
|
||||||
if (!form.value.province_id) return []
|
const provinceLoading = ref(false)
|
||||||
return props.cities.filter((city) => city.province_id === form.value.province_id)
|
const cityLoading = ref(false)
|
||||||
})
|
const provinceKeyword = ref('')
|
||||||
|
const cityKeyword = ref('')
|
||||||
|
let provinceSearchTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
let citySearchTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
// 省份变化时清空城市选择
|
// 省份变化时清空城市选择
|
||||||
const handleProvinceChange = () => {
|
const handleProvinceChange = (value?: number) => {
|
||||||
form.value.city_id = undefined
|
form.value.city_id = undefined
|
||||||
|
cityOptions.value = []
|
||||||
|
cityKeyword.value = ''
|
||||||
|
if (value) {
|
||||||
|
fetchCitiesByProvince(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchProvinces = async (keyword = '') => {
|
||||||
|
provinceLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await fetchProvinceList({ keyword: keyword || undefined })
|
||||||
|
provinceOptions.value = res.details || []
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取省份列表失败:', error)
|
||||||
|
provinceOptions.value = []
|
||||||
|
} finally {
|
||||||
|
provinceLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchCitiesByProvince = async (provinceId: number, keyword = '') => {
|
||||||
|
cityLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await fetchCityList(provinceId, { keyword: keyword || undefined })
|
||||||
|
cityOptions.value = res.details || []
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取城市列表失败:', error)
|
||||||
|
cityOptions.value = []
|
||||||
|
} finally {
|
||||||
|
cityLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleProvinceSearch = (keyword: string) => {
|
||||||
|
provinceKeyword.value = keyword
|
||||||
|
if (provinceSearchTimer) {
|
||||||
|
clearTimeout(provinceSearchTimer)
|
||||||
|
}
|
||||||
|
provinceSearchTimer = setTimeout(() => {
|
||||||
|
if (!keyword.trim()) return
|
||||||
|
fetchProvinces(keyword.trim())
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCitySearch = (keyword: string) => {
|
||||||
|
cityKeyword.value = keyword
|
||||||
|
if (!form.value.province_id) return
|
||||||
|
if (citySearchTimer) {
|
||||||
|
clearTimeout(citySearchTimer)
|
||||||
|
}
|
||||||
|
citySearchTimer = setTimeout(() => {
|
||||||
|
if (!keyword.trim()) return
|
||||||
|
fetchCitiesByProvince(form.value.province_id as number, keyword.trim())
|
||||||
|
}, 300)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听对话框显示状态
|
// 监听对话框显示状态
|
||||||
@@ -335,6 +399,9 @@ watch(
|
|||||||
() => props.visible,
|
() => props.visible,
|
||||||
(newVal) => {
|
(newVal) => {
|
||||||
if (newVal) {
|
if (newVal) {
|
||||||
|
provinceOptions.value = props.provinces || []
|
||||||
|
provinceKeyword.value = ''
|
||||||
|
cityKeyword.value = ''
|
||||||
if (props.datacenter && isEdit.value) {
|
if (props.datacenter && isEdit.value) {
|
||||||
// 编辑模式:填充表单
|
// 编辑模式:填充表单
|
||||||
form.value = {
|
form.value = {
|
||||||
@@ -359,6 +426,10 @@ watch(
|
|||||||
description: props.datacenter.description || '',
|
description: props.datacenter.description || '',
|
||||||
remarks: props.datacenter.remarks || '',
|
remarks: props.datacenter.remarks || '',
|
||||||
}
|
}
|
||||||
|
fetchProvinces()
|
||||||
|
if (form.value.province_id) {
|
||||||
|
fetchCitiesByProvince(form.value.province_id)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// 新建模式:重置表单
|
// 新建模式:重置表单
|
||||||
form.value = {
|
form.value = {
|
||||||
@@ -383,6 +454,8 @@ watch(
|
|||||||
description: '',
|
description: '',
|
||||||
remarks: '',
|
remarks: '',
|
||||||
}
|
}
|
||||||
|
fetchProvinces()
|
||||||
|
cityOptions.value = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,52 @@
|
|||||||
@page-change="handlePageChange"
|
@page-change="handlePageChange"
|
||||||
@refresh="handleRefresh"
|
@refresh="handleRefresh"
|
||||||
>
|
>
|
||||||
|
<template #form-items>
|
||||||
|
<a-col :span="8">
|
||||||
|
<a-form-item field="province_id" label="所属省份">
|
||||||
|
<a-select
|
||||||
|
v-model="formModel.province_id"
|
||||||
|
placeholder="请选择省份"
|
||||||
|
allow-search
|
||||||
|
allow-clear
|
||||||
|
:filter-option="false"
|
||||||
|
:loading="provinceSelectLoading"
|
||||||
|
@change="handleFilterProvinceChange"
|
||||||
|
@search="handleFilterProvinceSearch"
|
||||||
|
>
|
||||||
|
<a-option
|
||||||
|
v-for="item in provinceSelectOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:value="item.id"
|
||||||
|
>
|
||||||
|
{{ item.name }}
|
||||||
|
</a-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="8">
|
||||||
|
<a-form-item field="city_id" label="所属城市">
|
||||||
|
<a-select
|
||||||
|
v-model="formModel.city_id"
|
||||||
|
placeholder="请选择城市"
|
||||||
|
:disabled="!formModel.province_id"
|
||||||
|
allow-search
|
||||||
|
allow-clear
|
||||||
|
:filter-option="false"
|
||||||
|
:loading="citySelectLoading"
|
||||||
|
@search="handleFilterCitySearch"
|
||||||
|
>
|
||||||
|
<a-option
|
||||||
|
v-for="item in citySelectOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:value="item.id"
|
||||||
|
>
|
||||||
|
{{ item.name }}
|
||||||
|
</a-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</template>
|
||||||
<template #toolbar-left>
|
<template #toolbar-left>
|
||||||
<a-button type="primary" @click="handleCreate">
|
<a-button type="primary" @click="handleCreate">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
@@ -87,7 +133,6 @@
|
|||||||
v-model:visible="formVisible"
|
v-model:visible="formVisible"
|
||||||
:datacenter="editingDatacenter"
|
:datacenter="editingDatacenter"
|
||||||
:provinces="provinces"
|
:provinces="provinces"
|
||||||
:cities="cities"
|
|
||||||
@success="handleFormSuccess"
|
@success="handleFormSuccess"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -143,7 +188,12 @@ const pagination = reactive({
|
|||||||
|
|
||||||
// 省份和城市列表
|
// 省份和城市列表
|
||||||
const provinces = ref<any[]>([])
|
const provinces = ref<any[]>([])
|
||||||
const cities = ref<any[]>([])
|
const provinceSelectOptions = ref<any[]>([])
|
||||||
|
const citySelectOptions = ref<any[]>([])
|
||||||
|
const provinceSelectLoading = ref(false)
|
||||||
|
const citySelectLoading = ref(false)
|
||||||
|
let filterProvinceSearchTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
let filterCitySearchTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
// 表单项配置
|
// 表单项配置
|
||||||
const formItems = computed<FormItem[]>(() => searchFormConfig)
|
const formItems = computed<FormItem[]>(() => searchFormConfig)
|
||||||
@@ -160,42 +210,66 @@ const formVisible = ref(false)
|
|||||||
const detailVisible = ref(false)
|
const detailVisible = ref(false)
|
||||||
|
|
||||||
// 获取省份列表
|
// 获取省份列表
|
||||||
const fetchProvinces = async () => {
|
const fetchProvinces = async (keyword = '') => {
|
||||||
|
provinceSelectLoading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await fetchProvinceList()
|
const res = await fetchProvinceList({ keyword: keyword || undefined })
|
||||||
provinces.value = res.details || []
|
const list = res.details || []
|
||||||
// 更新表单配置中的省份选项
|
provinceSelectOptions.value = list
|
||||||
const provinceFormItem = formItems.value.find((item) => item.field === 'province_id')
|
if (!keyword) {
|
||||||
if (provinceFormItem) {
|
provinces.value = list
|
||||||
provinceFormItem.options = provinces.value.map((p) => ({
|
|
||||||
label: p.name,
|
|
||||||
value: p.id,
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取省份列表失败:', error)
|
console.error('获取省份列表失败:', error)
|
||||||
|
provinceSelectOptions.value = []
|
||||||
|
} finally {
|
||||||
|
provinceSelectLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取城市列表
|
// 根据省份获取城市列表
|
||||||
const fetchCities = async () => {
|
const fetchCitiesByProvince = async (provinceId: number, keyword = '') => {
|
||||||
|
citySelectLoading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await fetchCityList()
|
const res = await fetchCityList(provinceId, { keyword: keyword || undefined })
|
||||||
console.log(res, '.res')
|
citySelectOptions.value = res.details || []
|
||||||
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) {
|
} catch (error) {
|
||||||
console.error('获取城市列表失败:', error)
|
console.error('获取城市列表失败:', error)
|
||||||
|
citySelectOptions.value = []
|
||||||
|
} finally {
|
||||||
|
citySelectLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleFilterProvinceChange = (provinceId?: number) => {
|
||||||
|
formModel.value.city_id = ''
|
||||||
|
citySelectOptions.value = []
|
||||||
|
if (provinceId) {
|
||||||
|
fetchCitiesByProvince(provinceId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFilterProvinceSearch = (keyword: string) => {
|
||||||
|
if (filterProvinceSearchTimer) {
|
||||||
|
clearTimeout(filterProvinceSearchTimer)
|
||||||
|
}
|
||||||
|
filterProvinceSearchTimer = setTimeout(() => {
|
||||||
|
if (!keyword.trim()) return
|
||||||
|
fetchProvinces(keyword.trim())
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFilterCitySearch = (keyword: string) => {
|
||||||
|
if (!formModel.value.province_id) return
|
||||||
|
if (filterCitySearchTimer) {
|
||||||
|
clearTimeout(filterCitySearchTimer)
|
||||||
|
}
|
||||||
|
filterCitySearchTimer = setTimeout(() => {
|
||||||
|
if (!keyword.trim()) return
|
||||||
|
fetchCitiesByProvince(Number(formModel.value.province_id), keyword.trim())
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
// 获取数据中心列表
|
// 获取数据中心列表
|
||||||
const fetchDatacenters = async () => {
|
const fetchDatacenters = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@@ -243,6 +317,8 @@ const handleReset = () => {
|
|||||||
province_id: '',
|
province_id: '',
|
||||||
city_id: '',
|
city_id: '',
|
||||||
}
|
}
|
||||||
|
citySelectOptions.value = []
|
||||||
|
fetchProvinces()
|
||||||
pagination.current = 1
|
pagination.current = 1
|
||||||
fetchDatacenters()
|
fetchDatacenters()
|
||||||
}
|
}
|
||||||
@@ -310,7 +386,6 @@ const handleFormSuccess = () => {
|
|||||||
// 初始化加载数据
|
// 初始化加载数据
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchProvinces()
|
fetchProvinces()
|
||||||
fetchCities()
|
|
||||||
fetchDatacenters()
|
fetchDatacenters()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -75,6 +75,7 @@
|
|||||||
placeholder="请选择所属楼层"
|
placeholder="请选择所属楼层"
|
||||||
:loading="loadingFloors"
|
:loading="loadingFloors"
|
||||||
allow-search
|
allow-search
|
||||||
|
@search="handleFloorSearch"
|
||||||
>
|
>
|
||||||
<a-option
|
<a-option
|
||||||
v-for="item in floorList"
|
v-for="item in floorList"
|
||||||
@@ -437,7 +438,7 @@ import { Message } from '@arco-design/web-vue'
|
|||||||
import { createRack, updateRack } from '@/api/ops/rack'
|
import { createRack, updateRack } from '@/api/ops/rack'
|
||||||
import {
|
import {
|
||||||
fetchDatacenterList,
|
fetchDatacenterList,
|
||||||
fetchFloorListForSelect,
|
fetchRackListByDatacenter,
|
||||||
fetchSupplierList,
|
fetchSupplierList,
|
||||||
} from '@/api/ops/rack'
|
} from '@/api/ops/rack'
|
||||||
|
|
||||||
@@ -500,6 +501,7 @@ const submitting = ref(false)
|
|||||||
const datacenterList = ref<any[]>([])
|
const datacenterList = ref<any[]>([])
|
||||||
const floorList = ref<any[]>([])
|
const floorList = ref<any[]>([])
|
||||||
const supplierList = ref<any[]>([])
|
const supplierList = ref<any[]>([])
|
||||||
|
let floorSearchTimer: number | undefined
|
||||||
|
|
||||||
// 表单数据
|
// 表单数据
|
||||||
const form = ref({
|
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
|
loadingFloors.value = true
|
||||||
try {
|
try {
|
||||||
const res: any = await fetchFloorListForSelect(datacenterId)
|
const res: any = await fetchRackListByDatacenter(datacenterId, { name: keyword })
|
||||||
if (res.code === 0) {
|
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<number, { id: number; name: string }>()
|
||||||
|
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) {
|
} catch (error) {
|
||||||
console.error('获取楼层列表失败:', error)
|
console.error('获取楼层列表失败:', error)
|
||||||
@@ -593,6 +610,16 @@ const handleDatacenterChange = async (value: number) => {
|
|||||||
await loadFloorList(value)
|
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(
|
watch(
|
||||||
() => props.visible,
|
() => props.visible,
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
<!-- 使用率 -->
|
<!-- 使用率 -->
|
||||||
<template #utilization_rate="{ record }">
|
<template #utilization_rate="{ record }">
|
||||||
<a-progress
|
<a-progress
|
||||||
:percent="(record.utilization_rate || 0)"
|
:percent="Math.min(Math.max((Number(record.utilization_rate) || 0) / 100, 0), 1)"
|
||||||
:size="'small'"
|
:size="'small'"
|
||||||
:color="
|
:color="
|
||||||
(record.utilization_rate || 0) > 80
|
(record.utilization_rate || 0) > 80
|
||||||
@@ -102,7 +102,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, reactive, computed } from 'vue'
|
import { ref, reactive, computed, watch, onMounted } from 'vue'
|
||||||
import { Message, Modal } from '@arco-design/web-vue'
|
import { Message, Modal } from '@arco-design/web-vue'
|
||||||
import { IconPlus } from '@arco-design/web-vue/es/icon'
|
import { IconPlus } from '@arco-design/web-vue/es/icon'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
@@ -112,6 +112,8 @@ import { searchFormConfig } from './config/search-form'
|
|||||||
import { columns as columnsConfig } from './config/columns'
|
import { columns as columnsConfig } from './config/columns'
|
||||||
import {
|
import {
|
||||||
fetchRackList,
|
fetchRackList,
|
||||||
|
fetchDatacenterList,
|
||||||
|
fetchRackListByDatacenter,
|
||||||
deleteRack,
|
deleteRack,
|
||||||
} from '@/api/ops/rack'
|
} from '@/api/ops/rack'
|
||||||
import RackDetailDialog from './components/RackDetailDialog.vue'
|
import RackDetailDialog from './components/RackDetailDialog.vue'
|
||||||
@@ -137,7 +139,33 @@ const pagination = reactive({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 表单项配置
|
// 表单项配置
|
||||||
const formItems = computed<FormItem[]>(() => searchFormConfig)
|
const datacenterSelectOptions = ref<{ label: string; value: number }[]>([])
|
||||||
|
const floorSelectOptions = ref<{ label: string; value: number }[]>([])
|
||||||
|
let datacenterSearchTimer: number | undefined
|
||||||
|
let floorSearchTimer: number | undefined
|
||||||
|
|
||||||
|
const formItems = computed<FormItem[]>(() =>
|
||||||
|
searchFormConfig.map((item) => {
|
||||||
|
if (item.field === 'datacenter_id') {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
options: datacenterSelectOptions.value,
|
||||||
|
allowSearch: true,
|
||||||
|
onSearch: handleDatacenterSearch,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (item.field === 'floor_id') {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
options: floorSelectOptions.value,
|
||||||
|
allowSearch: true,
|
||||||
|
onSearch: handleFloorSearch,
|
||||||
|
disabled: !formModel.value.datacenter_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
// 表格列配置
|
// 表格列配置
|
||||||
const columns = computed(() => columnsConfig)
|
const columns = computed(() => columnsConfig)
|
||||||
@@ -150,6 +178,83 @@ const editingRack = ref<any>(null)
|
|||||||
const formVisible = ref(false)
|
const formVisible = ref(false)
|
||||||
const detailVisible = ref(false)
|
const detailVisible = ref(false)
|
||||||
|
|
||||||
|
const loadDatacenterOptions = async (keyword?: string) => {
|
||||||
|
try {
|
||||||
|
const res: any = await fetchDatacenterList({ keyword })
|
||||||
|
if (res.code === 0) {
|
||||||
|
const list = res.details?.data ?? res.data ?? res.details ?? []
|
||||||
|
const rows = Array.isArray(list) ? list : []
|
||||||
|
datacenterSelectOptions.value = rows.map((d: any) => ({
|
||||||
|
label: d.name || d.code || String(d.id),
|
||||||
|
value: d.id,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载数据中心列表失败:', error)
|
||||||
|
Message.error('加载数据中心列表失败')
|
||||||
|
datacenterSelectOptions.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadFloorOptions = async (datacenterId?: number, keyword?: string) => {
|
||||||
|
if (!datacenterId) {
|
||||||
|
floorSelectOptions.value = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res: any = await fetchRackListByDatacenter(datacenterId, {
|
||||||
|
name: keyword,
|
||||||
|
})
|
||||||
|
if (res.code === 0) {
|
||||||
|
const list = res.details?.data ?? res.data ?? res.details ?? []
|
||||||
|
const rows = Array.isArray(list) ? list : []
|
||||||
|
const floorMap = new Map<number, { label: string; value: number }>()
|
||||||
|
rows.forEach((rack: any) => {
|
||||||
|
const floor = rack?.floor
|
||||||
|
if (!floor?.id || floorMap.has(floor.id)) return
|
||||||
|
floorMap.set(floor.id, {
|
||||||
|
label: floor.name || String(floor.id),
|
||||||
|
value: floor.id,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
floorSelectOptions.value = Array.from(floorMap.values())
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载楼层列表失败:', error)
|
||||||
|
Message.error('加载楼层列表失败')
|
||||||
|
floorSelectOptions.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDatacenterSearch = (keyword: string) => {
|
||||||
|
if (datacenterSearchTimer) {
|
||||||
|
window.clearTimeout(datacenterSearchTimer)
|
||||||
|
}
|
||||||
|
datacenterSearchTimer = window.setTimeout(() => {
|
||||||
|
loadDatacenterOptions(keyword?.trim() || undefined)
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFloorSearch = (keyword: string) => {
|
||||||
|
if (!formModel.value.datacenter_id) return
|
||||||
|
if (floorSearchTimer) {
|
||||||
|
window.clearTimeout(floorSearchTimer)
|
||||||
|
}
|
||||||
|
floorSearchTimer = window.setTimeout(() => {
|
||||||
|
loadFloorOptions(formModel.value.datacenter_id, keyword?.trim() || undefined)
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => formModel.value.datacenter_id,
|
||||||
|
(newId, oldId) => {
|
||||||
|
if (newId !== oldId) {
|
||||||
|
formModel.value.floor_id = undefined
|
||||||
|
}
|
||||||
|
loadFloorOptions(newId)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
// 获取机柜类型颜色
|
// 获取机柜类型颜色
|
||||||
const getRackTypeColor = (type?: string) => {
|
const getRackTypeColor = (type?: string) => {
|
||||||
const colorMap: Record<string, string> = {
|
const colorMap: Record<string, string> = {
|
||||||
@@ -207,16 +312,17 @@ const fetchRacks = async () => {
|
|||||||
page: pagination.current,
|
page: pagination.current,
|
||||||
page_size: pagination.pageSize,
|
page_size: pagination.pageSize,
|
||||||
keyword: formModel.value.keyword || undefined,
|
keyword: formModel.value.keyword || undefined,
|
||||||
datacenter_id: formModel.value.datacenter_id || undefined,
|
datacenter_id: formModel.value.datacenter_id ?? undefined,
|
||||||
floor_id: formModel.value.floor_id || undefined,
|
floor_id: formModel.value.floor_id ?? undefined,
|
||||||
rack_type: formModel.value.rack_type || undefined,
|
rack_type: formModel.value.rack_type || undefined,
|
||||||
status: formModel.value.status || undefined,
|
status: formModel.value.status || undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetchRackList(params)
|
const res = await fetchRackList(params)
|
||||||
|
|
||||||
tableData.value = res.data?.data || []
|
const payload = res?.data ?? res?.details ?? {}
|
||||||
pagination.total = res.data?.total || 0
|
tableData.value = Array.isArray(payload?.data) ? payload.data : []
|
||||||
|
pagination.total = payload?.total ?? 0
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取机柜列表失败:', error)
|
console.error('获取机柜列表失败:', error)
|
||||||
Message.error('获取机柜列表失败')
|
Message.error('获取机柜列表失败')
|
||||||
@@ -317,7 +423,10 @@ const handleFormSuccess = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 初始化加载数据
|
// 初始化加载数据
|
||||||
fetchRacks()
|
onMounted(() => {
|
||||||
|
loadDatacenterOptions()
|
||||||
|
fetchRacks()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
v-model:visible="dialogVisible"
|
v-model:visible="dialogVisible"
|
||||||
title="分配U位"
|
title="分配U位"
|
||||||
:width="600"
|
:width="600"
|
||||||
@ok="handleOk"
|
:on-before-ok="handleBeforeOk"
|
||||||
@cancel="handleCancel"
|
@cancel="handleCancel"
|
||||||
>
|
>
|
||||||
<a-form ref="formRef" :model="formData" :rules="rules" layout="vertical">
|
<a-form ref="formRef" :model="formData" :rules="rules" layout="vertical">
|
||||||
@@ -31,19 +31,23 @@
|
|||||||
<a-select
|
<a-select
|
||||||
v-model="formData.asset_id"
|
v-model="formData.asset_id"
|
||||||
placeholder="请选择设备"
|
placeholder="请选择设备"
|
||||||
|
:loading="assetListLoading"
|
||||||
allow-search
|
allow-search
|
||||||
|
:filter-option="false"
|
||||||
|
@search="handleAssetSearch"
|
||||||
@change="handleAssetChange"
|
@change="handleAssetChange"
|
||||||
>
|
>
|
||||||
<a-option
|
<a-option
|
||||||
v-for="asset in assetList"
|
v-for="asset in assetList"
|
||||||
:key="asset.id"
|
:key="asset.id"
|
||||||
:value="asset.id"
|
:value="asset.id"
|
||||||
:label="asset.asset_name"
|
>
|
||||||
/>
|
{{ asset.asset_name }} ({{ asset.asset_code }})
|
||||||
|
</a-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item field="asset_type" label="设备类型" required>
|
<a-form-item field="asset_type" label="设备类型">
|
||||||
<a-input
|
<a-input
|
||||||
v-model="formData.asset_type"
|
v-model="formData.asset_type"
|
||||||
placeholder="设备类型(选择设备后自动填入)"
|
placeholder="设备类型(选择设备后自动填入)"
|
||||||
@@ -51,6 +55,14 @@
|
|||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item field="asset_type_code" label="分类标识">
|
||||||
|
<a-input
|
||||||
|
v-model="formData.asset_type_code"
|
||||||
|
placeholder="分类标识(选择设备后自动填入)"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item field="power_consumption" label="功耗 (W)">
|
<a-form-item field="power_consumption" label="功耗 (W)">
|
||||||
<a-input-number
|
<a-input-number
|
||||||
v-model="formData.power_consumption"
|
v-model="formData.power_consumption"
|
||||||
@@ -82,12 +94,15 @@ export default defineComponent({
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, computed, watch } from 'vue'
|
import { ref, reactive, computed, watch } from 'vue'
|
||||||
import { Message } from '@arco-design/web-vue'
|
import { Message } from '@arco-design/web-vue'
|
||||||
|
import { fetchAssetAll } from '@/api/ops/asset'
|
||||||
|
|
||||||
interface Asset {
|
interface Asset {
|
||||||
id: number
|
id: number
|
||||||
asset_code: string
|
asset_code: string
|
||||||
asset_name: string
|
asset_name: string
|
||||||
asset_type: string
|
asset_type_name: string
|
||||||
|
asset_type_code: string
|
||||||
|
category_id?: number
|
||||||
power_consumption?: number
|
power_consumption?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,69 +126,121 @@ const dialogVisible = computed({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const formRef = ref()
|
const formRef = ref()
|
||||||
|
const assetListLoading = ref(false)
|
||||||
|
let assetSearchTimer: number | undefined
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
start_unit: 1,
|
start_unit: 1,
|
||||||
occupied_units: 1,
|
occupied_units: 1,
|
||||||
asset_id: undefined as number | undefined,
|
asset_id: undefined as number | undefined,
|
||||||
asset_code: '',
|
asset_code: '',
|
||||||
asset_name: '',
|
asset_name: '',
|
||||||
asset_type: '',
|
asset_type: '', // 展示 category.name
|
||||||
|
asset_type_code: '', // 提交 category.code
|
||||||
power_consumption: undefined as number | undefined,
|
power_consumption: undefined as number | undefined,
|
||||||
description: '',
|
description: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
const assetList = ref<Asset[]>([
|
const assetList = ref<Asset[]>([])
|
||||||
// 模拟设备列表,实际应从API获取
|
|
||||||
{ id: 1, asset_code: 'SRV001', asset_name: '服务器-001', asset_type: '服务器设备', power_consumption: 500 },
|
|
||||||
{ id: 2, asset_code: 'SRV002', asset_name: '服务器-002', asset_type: '服务器设备', power_consumption: 600 },
|
|
||||||
{ id: 3, asset_code: 'NET001', asset_name: '交换机-001', asset_type: '网络设备', power_consumption: 200 },
|
|
||||||
{ id: 4, asset_code: 'STO001', asset_name: '存储-001', asset_type: '存储设备', power_consumption: 400 },
|
|
||||||
])
|
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
start_unit: [{ required: true, message: '请输入起始U位' }],
|
start_unit: [{ required: true, message: '请输入起始U位' }],
|
||||||
occupied_units: [{ required: true, message: '请输入占用U位数量' }],
|
occupied_units: [{ required: true, message: '请输入占用U位数量' }],
|
||||||
asset_type: [{ required: true, message: '设备类型不能为空' }],
|
asset_id: [{ required: true, message: '请选择设备' }],
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAssetChange = (value: number) => {
|
const syncAssetFields = (value: number | string | undefined) => {
|
||||||
const asset = assetList.value.find((a) => a.id === value)
|
if (value === undefined || value === null || value === '') return
|
||||||
|
const asset = assetList.value.find((a) => String(a.id) === String(value))
|
||||||
if (asset) {
|
if (asset) {
|
||||||
|
formData.asset_id = asset.id
|
||||||
formData.asset_code = asset.asset_code
|
formData.asset_code = asset.asset_code
|
||||||
formData.asset_name = asset.asset_name
|
formData.asset_name = asset.asset_name
|
||||||
formData.asset_type = asset.asset_type
|
formData.asset_type = asset.asset_type_name
|
||||||
|
formData.asset_type_code = asset.asset_type_code
|
||||||
formData.power_consumption = asset.power_consumption
|
formData.power_consumption = asset.power_consumption
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleOk = async () => {
|
const handleAssetChange = (value: number | string) => {
|
||||||
|
syncAssetFields(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadAssetList = async (keyword?: string) => {
|
||||||
|
assetListLoading.value = true
|
||||||
try {
|
try {
|
||||||
const valid = await formRef.value?.validate()
|
const res: any = await fetchAssetAll({ keyword: keyword || undefined })
|
||||||
if (!valid) {
|
if (res.code === 0) {
|
||||||
// 发送分配请求
|
const rowsCandidate =
|
||||||
const { allocateUnit } = await import('@/api/ops/unit')
|
res?.data?.data ??
|
||||||
const params = {
|
res?.details?.data ??
|
||||||
rack_id: props.rackId,
|
res?.data ??
|
||||||
start_unit: formData.start_unit,
|
res?.details ??
|
||||||
occupied_units: formData.occupied_units,
|
[]
|
||||||
asset_id: formData.asset_id,
|
const rows = Array.isArray(rowsCandidate) ? rowsCandidate : []
|
||||||
asset_code: formData.asset_code,
|
assetList.value = rows.map((item: any) => ({
|
||||||
asset_name: formData.asset_name || `未命名设备-${formData.start_unit}`,
|
id: item.id,
|
||||||
asset_type: formData.asset_type,
|
asset_code: item.asset_code || '',
|
||||||
power_consumption: formData.power_consumption,
|
asset_name: item.asset_name || '',
|
||||||
description: formData.description,
|
asset_type_name: item?.category?.name || item?.category_name || item?.asset_type_name || '',
|
||||||
}
|
asset_type_code: item?.category?.code ?? item?.category_code ?? item?.asset_type ?? '',
|
||||||
const res = await allocateUnit(params)
|
category_id: item?.category_id,
|
||||||
if (res.code === 0) {
|
power_consumption: item.power_consumption,
|
||||||
Message.success('分配成功')
|
}))
|
||||||
emit('success')
|
} else {
|
||||||
handleCancel()
|
Message.error(res.message || '获取设备列表失败')
|
||||||
} else {
|
assetList.value = []
|
||||||
Message.error(res.message || '分配失败')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取设备列表失败:', error)
|
||||||
|
Message.error('获取设备列表失败')
|
||||||
|
assetList.value = []
|
||||||
|
} finally {
|
||||||
|
assetListLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAssetSearch = (keyword: string) => {
|
||||||
|
if (assetSearchTimer) {
|
||||||
|
window.clearTimeout(assetSearchTimer)
|
||||||
|
}
|
||||||
|
assetSearchTimer = window.setTimeout(() => {
|
||||||
|
loadAssetList(keyword?.trim() || undefined)
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBeforeOk = async () => {
|
||||||
|
try {
|
||||||
|
await formRef.value?.validate()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('分配失败:', error)
|
console.error('分配失败:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 防止 change 事件未触发时字段未回写
|
||||||
|
syncAssetFields(formData.asset_id)
|
||||||
|
|
||||||
|
const { allocateUnit } = await import('@/api/ops/unit')
|
||||||
|
const params = {
|
||||||
|
rack_id: props.rackId,
|
||||||
|
start_unit: formData.start_unit,
|
||||||
|
occupied_units: formData.occupied_units,
|
||||||
|
asset_id: formData.asset_id,
|
||||||
|
asset_code: formData.asset_code,
|
||||||
|
asset_name: formData.asset_name || `未命名设备-${formData.start_unit}`,
|
||||||
|
// 后端字段 asset_type 使用分类标识(category.code)
|
||||||
|
asset_type: formData.asset_type_code,
|
||||||
|
power_consumption: formData.power_consumption,
|
||||||
|
description: formData.description,
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await allocateUnit(params)
|
||||||
|
if (res.code === 0) {
|
||||||
|
Message.success('分配成功')
|
||||||
|
emit('success')
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
Message.error(res.message || '分配失败')
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,6 +256,7 @@ const resetForm = () => {
|
|||||||
formData.asset_code = ''
|
formData.asset_code = ''
|
||||||
formData.asset_name = ''
|
formData.asset_name = ''
|
||||||
formData.asset_type = ''
|
formData.asset_type = ''
|
||||||
|
formData.asset_type_code = ''
|
||||||
formData.power_consumption = undefined
|
formData.power_consumption = undefined
|
||||||
formData.description = ''
|
formData.description = ''
|
||||||
formRef.value?.resetFields()
|
formRef.value?.resetFields()
|
||||||
@@ -197,9 +265,18 @@ const resetForm = () => {
|
|||||||
watch(
|
watch(
|
||||||
() => props.visible,
|
() => props.visible,
|
||||||
(val) => {
|
(val) => {
|
||||||
if (!val) {
|
if (val) {
|
||||||
|
loadAssetList()
|
||||||
|
} else {
|
||||||
resetForm()
|
resetForm()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => formData.asset_id,
|
||||||
|
(value) => {
|
||||||
|
syncAssetFields(value)
|
||||||
|
}
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -227,6 +227,7 @@ import {
|
|||||||
releaseUnit,
|
releaseUnit,
|
||||||
updateUnitStatus,
|
updateUnitStatus,
|
||||||
} from '@/api/ops/unit'
|
} from '@/api/ops/unit'
|
||||||
|
import { normalizeUnitList } from './utils/unitFallback'
|
||||||
import AllocateUnitDialog from './components/AllocateUnitDialog.vue'
|
import AllocateUnitDialog from './components/AllocateUnitDialog.vue'
|
||||||
import ReserveUnitDialog from './components/ReserveUnitDialog.vue'
|
import ReserveUnitDialog from './components/ReserveUnitDialog.vue'
|
||||||
|
|
||||||
@@ -334,8 +335,9 @@ const fetchUnits = async () => {
|
|||||||
const res = await fetchUnitList(rackId.value)
|
const res = await fetchUnitList(rackId.value)
|
||||||
|
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
rackInfo.value = res.data?.rack || {}
|
const payload = res?.details ?? res?.data ?? {}
|
||||||
unitList.value = res.data?.units || []
|
rackInfo.value = payload?.rack || {}
|
||||||
|
unitList.value = normalizeUnitList(payload?.units, rackInfo.value?.height)
|
||||||
} else {
|
} else {
|
||||||
Message.error(res.message || '获取U位列表失败')
|
Message.error(res.message || '获取U位列表失败')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,53 @@
|
|||||||
<icon-storage /> 选择机柜
|
<icon-storage /> 选择机柜
|
||||||
</template>
|
</template>
|
||||||
<a-row :gutter="16">
|
<a-row :gutter="16">
|
||||||
|
<a-col :span="8">
|
||||||
|
<a-select
|
||||||
|
v-model="selectedDatacenterId"
|
||||||
|
placeholder="请选择数据中心"
|
||||||
|
:loading="datacenterListLoading"
|
||||||
|
allow-search
|
||||||
|
@search="handleDatacenterSearch"
|
||||||
|
@change="handleDatacenterChange"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<a-option
|
||||||
|
v-for="datacenter in datacenterList"
|
||||||
|
:key="datacenter.value"
|
||||||
|
:value="datacenter.value"
|
||||||
|
>
|
||||||
|
{{ datacenter.label }}
|
||||||
|
</a-option>
|
||||||
|
</a-select>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="8">
|
||||||
|
<a-select
|
||||||
|
v-model="selectedFloorId"
|
||||||
|
placeholder="请选择楼层"
|
||||||
|
:loading="floorListLoading"
|
||||||
|
:disabled="!selectedDatacenterId"
|
||||||
|
allow-search
|
||||||
|
@search="handleFloorSearch"
|
||||||
|
@change="handleFloorChange"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<a-option
|
||||||
|
v-for="floor in floorList"
|
||||||
|
:key="floor.value"
|
||||||
|
:value="floor.value"
|
||||||
|
>
|
||||||
|
{{ floor.label }}
|
||||||
|
</a-option>
|
||||||
|
</a-select>
|
||||||
|
</a-col>
|
||||||
<a-col :span="8">
|
<a-col :span="8">
|
||||||
<a-select
|
<a-select
|
||||||
v-model="selectedRackId"
|
v-model="selectedRackId"
|
||||||
placeholder="请选择机柜"
|
placeholder="请选择机柜"
|
||||||
:loading="rackListLoading"
|
:loading="rackListLoading"
|
||||||
|
:disabled="!selectedFloorId"
|
||||||
|
allow-search
|
||||||
|
@search="handleRackSearch"
|
||||||
@change="handleRackChange"
|
@change="handleRackChange"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
>
|
>
|
||||||
@@ -179,17 +221,31 @@ import {
|
|||||||
releaseUnit,
|
releaseUnit,
|
||||||
updateUnitStatus,
|
updateUnitStatus,
|
||||||
} from '@/api/ops/unit'
|
} 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 AllocateUnitDialog from './components/AllocateUnitDialog.vue'
|
||||||
import ReserveUnitDialog from './components/ReserveUnitDialog.vue'
|
import ReserveUnitDialog from './components/ReserveUnitDialog.vue'
|
||||||
|
|
||||||
// 状态管理
|
// 状态管理
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const rackListLoading = ref(false)
|
const rackListLoading = ref(false)
|
||||||
|
const datacenterListLoading = ref(false)
|
||||||
|
const floorListLoading = ref(false)
|
||||||
|
const selectedDatacenterId = ref<number | undefined>(undefined)
|
||||||
|
const selectedFloorId = ref<number | undefined>(undefined)
|
||||||
const selectedRackId = ref<number | undefined>(undefined)
|
const selectedRackId = ref<number | undefined>(undefined)
|
||||||
const rackInfo = ref<any>({})
|
const rackInfo = ref<any>({})
|
||||||
|
const datacenterList = ref<{ label: string; value: number }[]>([])
|
||||||
|
const floorList = ref<{ label: string; value: number }[]>([])
|
||||||
const rackList = ref<any[]>([])
|
const rackList = ref<any[]>([])
|
||||||
const unitList = ref<any[]>([])
|
const unitList = ref<any[]>([])
|
||||||
|
let datacenterSearchTimer: number | undefined
|
||||||
|
let floorSearchTimer: number | undefined
|
||||||
|
let rackSearchTimer: number | undefined
|
||||||
|
|
||||||
// 对话框可见性
|
// 对话框可见性
|
||||||
const allocateVisible = ref(false)
|
const allocateVisible = ref(false)
|
||||||
@@ -213,6 +269,16 @@ const usagePercentage = computed(() => {
|
|||||||
return Math.round((usedUnits.value / rackInfo.value.height) * 100)
|
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位状态颜色
|
// 获取U位状态颜色
|
||||||
const getUnitStatusColor = (status?: string) => {
|
const getUnitStatusColor = (status?: string) => {
|
||||||
const colorMap: Record<string, string> = {
|
const colorMap: Record<string, string> = {
|
||||||
@@ -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
|
rackListLoading.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetchRackList({
|
const res: any = await fetchRackListByFloor(selectedFloorId.value, { name: keyword })
|
||||||
page: 1,
|
rackList.value = extractList(res)
|
||||||
page_size: 1000,
|
|
||||||
status: 'in_use', // 只获取使用中的机柜
|
|
||||||
})
|
|
||||||
|
|
||||||
rackList.value = res.details?.data || []
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取机柜列表失败:', error)
|
console.error('获取机柜列表失败:', error)
|
||||||
Message.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) => {
|
const handleRackChange = (rackId: number) => {
|
||||||
if (rackId) {
|
if (rackId) {
|
||||||
@@ -278,8 +438,9 @@ const fetchUnits = async (rackId?: number) => {
|
|||||||
const res = await fetchUnitList(targetRackId)
|
const res = await fetchUnitList(targetRackId)
|
||||||
|
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
rackInfo.value = res.data?.rack || {}
|
const payload = res?.details ?? res?.data ?? {}
|
||||||
unitList.value = res.data?.units || []
|
rackInfo.value = payload?.rack || {}
|
||||||
|
unitList.value = normalizeUnitList(payload?.units, rackInfo.value?.height)
|
||||||
} else {
|
} else {
|
||||||
Message.error(res.message || '获取U位列表失败')
|
Message.error(res.message || '获取U位列表失败')
|
||||||
}
|
}
|
||||||
@@ -453,7 +614,7 @@ const handleCancelReservation = async (record: any) => {
|
|||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchRacks()
|
fetchDatacenters()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}))
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<search-table
|
<search-table
|
||||||
:form-model="formModel"
|
v-model:form-model="formModel"
|
||||||
:form-items="formItems"
|
:form-items="formItems"
|
||||||
:data="tableData"
|
:data="tableData"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
@@ -50,84 +50,118 @@
|
|||||||
{{ rowIndex + 1 + (pagination.current - 1) * pagination.pageSize }}
|
{{ rowIndex + 1 + (pagination.current - 1) * pagination.pageSize }}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 操作 -->
|
<!-- 操作(禁用规则见 ticket-list-operation-buttons.md;已解决/已关闭仅保留详情与评论) -->
|
||||||
<template #actions="{ record }">
|
<template #actions="{ record }">
|
||||||
<!-- 详情按钮(所有状态都显示) -->
|
|
||||||
<a-button type="text" size="small" @click="handleDetail(record)">
|
<a-button type="text" size="small" @click="handleDetail(record)">
|
||||||
详情
|
详情
|
||||||
</a-button>
|
</a-button>
|
||||||
|
|
||||||
<!-- 接单按钮(待接单状态) -->
|
<template v-if="!isTicketResolvedOrClosed(record.status)">
|
||||||
<a-button
|
<a-tooltip
|
||||||
v-if="record.status === 'pending'"
|
v-if="record.status === 'pending'"
|
||||||
type="text"
|
:content="reasonAcceptTicket(record, me)"
|
||||||
size="small"
|
:disabled="canAcceptTicket(record, me)"
|
||||||
@click="handleAccept(record)"
|
|
||||||
>
|
>
|
||||||
接单
|
<a-button
|
||||||
</a-button>
|
type="text"
|
||||||
|
size="small"
|
||||||
<!-- 转交按钮(已接单、处理中、已挂起状态) -->
|
:disabled="!canAcceptTicket(record, me)"
|
||||||
<a-button
|
@click="handleAccept(record)"
|
||||||
v-if="['accepted', 'processing', 'suspended'].includes(record.status)"
|
>
|
||||||
type="text"
|
接单
|
||||||
size="small"
|
</a-button>
|
||||||
@click="handleTransfer(record)"
|
</a-tooltip>
|
||||||
|
|
||||||
|
<a-tooltip
|
||||||
|
v-if="record.status !== 'closed' && record.status !== 'cancelled'"
|
||||||
|
:content="reasonTransferTicket(record, me)"
|
||||||
|
:disabled="canTransferTicket(record, me)"
|
||||||
>
|
>
|
||||||
转交
|
<a-button
|
||||||
</a-button>
|
type="text"
|
||||||
|
size="small"
|
||||||
<!-- 挂起按钮(已接单、处理中状态) -->
|
:disabled="!canTransferTicket(record, me)"
|
||||||
<a-button
|
@click="handleTransfer(record)"
|
||||||
v-if="['accepted', 'processing'].includes(record.status)"
|
>
|
||||||
type="text"
|
转交
|
||||||
size="small"
|
</a-button>
|
||||||
@click="handleSuspend(record)"
|
</a-tooltip>
|
||||||
|
|
||||||
|
<a-tooltip
|
||||||
|
v-if="record.status !== 'suspended' && record.status !== 'closed' && record.status !== 'cancelled'"
|
||||||
|
:content="reasonSuspendTicket(record, me)"
|
||||||
|
:disabled="canSuspendTicket(record, me)"
|
||||||
>
|
>
|
||||||
挂起
|
<a-button
|
||||||
</a-button>
|
type="text"
|
||||||
|
size="small"
|
||||||
<!-- 重启按钮(已挂起状态) -->
|
:disabled="!canSuspendTicket(record, me)"
|
||||||
<a-button
|
@click="handleSuspend(record)"
|
||||||
v-if="record.status === 'suspended'"
|
>
|
||||||
type="text"
|
挂起
|
||||||
size="small"
|
</a-button>
|
||||||
@click="handleResume(record)"
|
</a-tooltip>
|
||||||
|
|
||||||
|
<a-tooltip
|
||||||
|
v-if="record.status === 'suspended'"
|
||||||
|
:content="reasonResumeTicket(record, me)"
|
||||||
|
:disabled="canResumeTicket(record, me)"
|
||||||
>
|
>
|
||||||
重启
|
<a-button
|
||||||
</a-button>
|
type="text"
|
||||||
|
size="small"
|
||||||
<!-- 解决按钮(已接单、处理中状态) -->
|
:disabled="!canResumeTicket(record, me)"
|
||||||
<a-button
|
@click="handleResume(record)"
|
||||||
v-if="['accepted', 'processing'].includes(record.status)"
|
>
|
||||||
type="text"
|
重启
|
||||||
size="small"
|
</a-button>
|
||||||
@click="handleResolve(record)"
|
</a-tooltip>
|
||||||
|
|
||||||
|
<a-tooltip
|
||||||
|
v-if="record.status !== 'closed' && record.status !== 'cancelled'"
|
||||||
|
:content="reasonResolveTicket(record, me)"
|
||||||
|
:disabled="canResolveTicket(record, me)"
|
||||||
>
|
>
|
||||||
解决
|
<a-button
|
||||||
</a-button>
|
type="text"
|
||||||
|
size="small"
|
||||||
<!-- 关闭按钮(待接单、已接单、处理中、已解决状态) -->
|
:disabled="!canResolveTicket(record, me)"
|
||||||
<a-button
|
@click="handleResolve(record)"
|
||||||
v-if="['pending', 'accepted', 'processing', 'resolved'].includes(record.status)"
|
>
|
||||||
type="text"
|
解决
|
||||||
size="small"
|
</a-button>
|
||||||
@click="handleClose(record)"
|
</a-tooltip>
|
||||||
|
|
||||||
|
<a-tooltip
|
||||||
|
v-if="record.status !== 'closed' && record.status !== 'cancelled'"
|
||||||
|
:content="reasonCloseTicket(record, me)"
|
||||||
|
:disabled="canCloseTicket(record, me)"
|
||||||
>
|
>
|
||||||
关闭
|
<a-button
|
||||||
</a-button>
|
type="text"
|
||||||
|
size="small"
|
||||||
<!-- 编辑按钮(非已关闭、已撤回状态) -->
|
:disabled="!canCloseTicket(record, me)"
|
||||||
<a-button
|
@click="handleClose(record)"
|
||||||
v-if="!['closed', 'cancelled'].includes(record.status)"
|
>
|
||||||
type="text"
|
关闭
|
||||||
size="small"
|
</a-button>
|
||||||
@click="handleEdit(record)"
|
</a-tooltip>
|
||||||
|
|
||||||
|
<a-tooltip
|
||||||
|
:content="reasonEditTicket(record, me)"
|
||||||
|
:disabled="canEditTicket(record, me)"
|
||||||
>
|
>
|
||||||
编辑
|
<a-button
|
||||||
</a-button>
|
type="text"
|
||||||
|
size="small"
|
||||||
<!-- 评论按钮(所有状态都显示) -->
|
:disabled="!canEditTicket(record, me)"
|
||||||
|
@click="handleEdit(record)"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</a-button>
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
|
|
||||||
<a-button type="text" size="small" @click="handleComment(record)">
|
<a-button type="text" size="small" @click="handleComment(record)">
|
||||||
评论
|
评论
|
||||||
</a-button>
|
</a-button>
|
||||||
@@ -186,6 +220,24 @@ import type { FormItem } from '@/components/search-form/types'
|
|||||||
import SearchTable from '@/components/search-table/index.vue'
|
import SearchTable from '@/components/search-table/index.vue'
|
||||||
import { searchFormConfig } from './config/search-form'
|
import { searchFormConfig } from './config/search-form'
|
||||||
import { columns as columnsConfig } from './config/columns'
|
import { columns as columnsConfig } from './config/columns'
|
||||||
|
import {
|
||||||
|
getFeedbackCurrentUserId,
|
||||||
|
canAcceptTicket,
|
||||||
|
canTransferTicket,
|
||||||
|
canSuspendTicket,
|
||||||
|
canResumeTicket,
|
||||||
|
canResolveTicket,
|
||||||
|
canCloseTicket,
|
||||||
|
canEditTicket,
|
||||||
|
reasonAcceptTicket,
|
||||||
|
reasonTransferTicket,
|
||||||
|
reasonSuspendTicket,
|
||||||
|
reasonResumeTicket,
|
||||||
|
reasonResolveTicket,
|
||||||
|
reasonCloseTicket,
|
||||||
|
reasonEditTicket,
|
||||||
|
isTicketResolvedOrClosed,
|
||||||
|
} from '../utils/ticketActionPermissions'
|
||||||
import {
|
import {
|
||||||
fetchFeedbackTickets,
|
fetchFeedbackTickets,
|
||||||
acceptFeedbackTicket,
|
acceptFeedbackTicket,
|
||||||
@@ -229,6 +281,9 @@ const priorityMap: Record<string, { text: string; color: string }> = {
|
|||||||
urgent: { text: '紧急', color: 'red' },
|
urgent: { text: '紧急', color: 'red' },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 当前账号 ID,与列表行 creator_id / assignee_id 同口径(登录 details.user_id) */
|
||||||
|
const me = computed(() => getFeedbackCurrentUserId())
|
||||||
|
|
||||||
// 状态管理
|
// 状态管理
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const tableData = ref<any[]>([])
|
const tableData = ref<any[]>([])
|
||||||
@@ -338,7 +393,10 @@ const handleCreate = () => {
|
|||||||
|
|
||||||
// 编辑工单
|
// 编辑工单
|
||||||
const handleEdit = (record: any) => {
|
const handleEdit = (record: any) => {
|
||||||
console.log('编辑工单:', record)
|
if (!canEditTicket(record, me.value)) {
|
||||||
|
Message.warning(reasonEditTicket(record, me.value) || '当前不可编辑')
|
||||||
|
return
|
||||||
|
}
|
||||||
editingTicket.value = record
|
editingTicket.value = record
|
||||||
formVisible.value = true
|
formVisible.value = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ const handleOk = async () => {
|
|||||||
const res = await commentFeedbackTicket(props.ticket.id, {
|
const res = await commentFeedbackTicket(props.ticket.id, {
|
||||||
content: form.value.content,
|
content: form.value.content,
|
||||||
})
|
})
|
||||||
if (res.code === 200) {
|
if (res.code === 0) {
|
||||||
Message.success('评论成功')
|
Message.success('评论成功')
|
||||||
emit('success')
|
emit('success')
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ const handleOk = async () => {
|
|||||||
const res = await resolveFeedbackTicket(props.ticket.id, {
|
const res = await resolveFeedbackTicket(props.ticket.id, {
|
||||||
content: form.value.content,
|
content: form.value.content,
|
||||||
})
|
})
|
||||||
if (res.code === 200) {
|
if (res.code === 0) {
|
||||||
Message.success('工单已解决')
|
Message.success('工单已解决')
|
||||||
emit('success')
|
emit('success')
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ const handleOk = async () => {
|
|||||||
assignee_name: form.value.assignee_name,
|
assignee_name: form.value.assignee_name,
|
||||||
reason: form.value.reason,
|
reason: form.value.reason,
|
||||||
})
|
})
|
||||||
if (res.code === 200) {
|
if (res.code === 0) {
|
||||||
Message.success('转交成功')
|
Message.success('转交成功')
|
||||||
emit('success')
|
emit('success')
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -46,4 +46,9 @@ export const searchFormConfig: FormItem[] = [
|
|||||||
{ label: '紧急', value: 'urgent' },
|
{ label: '紧急', value: 'urgent' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
field: 'dateRange',
|
||||||
|
label: '创建时间',
|
||||||
|
type: 'dateRange',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<a-tabs v-model:active-key="activeTab" @change="handleTabChange">
|
<a-tabs v-model:active-key="activeTab" @change="handleTabChange">
|
||||||
<a-tab-pane key="pending" title="待处理">
|
<a-tab-pane key="pending" title="待处理">
|
||||||
<search-table
|
<search-table
|
||||||
:form-model="formModel"
|
v-model:form-model="formModel"
|
||||||
:form-items="formItems"
|
:form-items="formItems"
|
||||||
:data="tableData"
|
:data="tableData"
|
||||||
:columns="pendingColumns"
|
:columns="pendingColumns"
|
||||||
@@ -17,15 +17,6 @@
|
|||||||
@page-change="handlePageChange"
|
@page-change="handlePageChange"
|
||||||
@refresh="handleRefresh"
|
@refresh="handleRefresh"
|
||||||
>
|
>
|
||||||
<template #toolbar-left>
|
|
||||||
<a-button type="primary" @click="handleCreate">
|
|
||||||
<template #icon>
|
|
||||||
<icon-plus />
|
|
||||||
</template>
|
|
||||||
新建工单
|
|
||||||
</a-button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- 工单类型 -->
|
<!-- 工单类型 -->
|
||||||
<template #type="{ record }">
|
<template #type="{ record }">
|
||||||
<a-tag :color="typeMap[record.type]?.color || 'gray'">
|
<a-tag :color="typeMap[record.type]?.color || 'gray'">
|
||||||
@@ -52,25 +43,111 @@
|
|||||||
{{ rowIndex + 1 + (pagination.current - 1) * pagination.pageSize }}
|
{{ rowIndex + 1 + (pagination.current - 1) * pagination.pageSize }}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 操作 -->
|
<!-- 操作(待处理;已解决/已关闭仅详情与评论) -->
|
||||||
<template #actions="{ record }">
|
<template #actions="{ record }">
|
||||||
<a-button type="text" size="small" @click="handleDetail(record)">
|
<a-button type="text" size="small" @click="handleDetail(record)">
|
||||||
详情
|
详情
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button type="text" size="small" @click="handleTransfer(record)">
|
|
||||||
转交
|
<template v-if="!isTicketResolvedOrClosed(record.status)">
|
||||||
</a-button>
|
<a-tooltip
|
||||||
<a-button type="text" size="small" @click="handleSuspend(record)">
|
v-if="record.status === 'pending'"
|
||||||
挂起
|
:content="reasonAcceptTicket(record, me)"
|
||||||
</a-button>
|
:disabled="canAcceptTicket(record, me)"
|
||||||
<a-button v-if="record.status === 'suspended'" type="text" size="small" @click="handleResume(record)">
|
>
|
||||||
重启
|
<a-button
|
||||||
</a-button>
|
type="text"
|
||||||
<a-button type="text" size="small" @click="handleResolve(record)">
|
size="small"
|
||||||
解决
|
:disabled="!canAcceptTicket(record, me)"
|
||||||
</a-button>
|
@click="handleAccept(record)"
|
||||||
<a-button type="text" size="small" @click="handleClose(record)">
|
>
|
||||||
关闭
|
接单
|
||||||
|
</a-button>
|
||||||
|
</a-tooltip>
|
||||||
|
|
||||||
|
<a-tooltip
|
||||||
|
v-if="record.status !== 'closed' && record.status !== 'cancelled'"
|
||||||
|
:content="reasonTransferTicket(record, me)"
|
||||||
|
:disabled="canTransferTicket(record, me)"
|
||||||
|
>
|
||||||
|
<a-button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
:disabled="!canTransferTicket(record, me)"
|
||||||
|
@click="handleTransfer(record)"
|
||||||
|
>
|
||||||
|
转交
|
||||||
|
</a-button>
|
||||||
|
</a-tooltip>
|
||||||
|
|
||||||
|
<a-tooltip
|
||||||
|
v-if="record.status !== 'suspended' && record.status !== 'closed' && record.status !== 'cancelled'"
|
||||||
|
:content="reasonSuspendTicket(record, me)"
|
||||||
|
:disabled="canSuspendTicket(record, me)"
|
||||||
|
>
|
||||||
|
<a-button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
:disabled="!canSuspendTicket(record, me)"
|
||||||
|
@click="handleSuspend(record)"
|
||||||
|
>
|
||||||
|
挂起
|
||||||
|
</a-button>
|
||||||
|
</a-tooltip>
|
||||||
|
|
||||||
|
<a-tooltip
|
||||||
|
v-if="record.status === 'suspended'"
|
||||||
|
:content="reasonResumeTicket(record, me)"
|
||||||
|
:disabled="canResumeTicket(record, me)"
|
||||||
|
>
|
||||||
|
<a-button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
:disabled="!canResumeTicket(record, me)"
|
||||||
|
@click="handleResume(record)"
|
||||||
|
>
|
||||||
|
重启
|
||||||
|
</a-button>
|
||||||
|
</a-tooltip>
|
||||||
|
|
||||||
|
<a-tooltip
|
||||||
|
v-if="record.status !== 'closed' && record.status !== 'cancelled'"
|
||||||
|
:content="reasonResolveTicket(record, me)"
|
||||||
|
:disabled="canResolveTicket(record, me)"
|
||||||
|
>
|
||||||
|
<a-button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
:disabled="!canResolveTicket(record, me)"
|
||||||
|
@click="handleResolve(record)"
|
||||||
|
>
|
||||||
|
解决
|
||||||
|
</a-button>
|
||||||
|
</a-tooltip>
|
||||||
|
|
||||||
|
<a-tooltip
|
||||||
|
v-if="record.status !== 'closed' && record.status !== 'cancelled'"
|
||||||
|
:content="reasonCloseTicket(record, me)"
|
||||||
|
:disabled="canCloseTicket(record, me)"
|
||||||
|
>
|
||||||
|
<a-button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
:disabled="!canCloseTicket(record, me)"
|
||||||
|
@click="handleClose(record)"
|
||||||
|
>
|
||||||
|
关闭
|
||||||
|
</a-button>
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<a-button
|
||||||
|
v-if="isTicketResolvedOrClosed(record.status)"
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
@click="handleComment(record)"
|
||||||
|
>
|
||||||
|
评论
|
||||||
</a-button>
|
</a-button>
|
||||||
</template>
|
</template>
|
||||||
</search-table>
|
</search-table>
|
||||||
@@ -78,7 +155,7 @@
|
|||||||
|
|
||||||
<a-tab-pane key="my-created" title="我创建的">
|
<a-tab-pane key="my-created" title="我创建的">
|
||||||
<search-table
|
<search-table
|
||||||
:form-model="formModel"
|
v-model:form-model="formModel"
|
||||||
:form-items="formItems"
|
:form-items="formItems"
|
||||||
:data="tableData"
|
:data="tableData"
|
||||||
:columns="myCreatedColumns"
|
:columns="myCreatedColumns"
|
||||||
@@ -92,15 +169,6 @@
|
|||||||
@page-change="handlePageChange"
|
@page-change="handlePageChange"
|
||||||
@refresh="handleRefresh"
|
@refresh="handleRefresh"
|
||||||
>
|
>
|
||||||
<template #toolbar-left>
|
|
||||||
<a-button type="primary" @click="handleCreate">
|
|
||||||
<template #icon>
|
|
||||||
<icon-plus />
|
|
||||||
</template>
|
|
||||||
新建工单
|
|
||||||
</a-button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- 工单类型 -->
|
<!-- 工单类型 -->
|
||||||
<template #type="{ record }">
|
<template #type="{ record }">
|
||||||
<a-tag :color="typeMap[record.type]?.color || 'gray'">
|
<a-tag :color="typeMap[record.type]?.color || 'gray'">
|
||||||
@@ -127,25 +195,77 @@
|
|||||||
{{ rowIndex + 1 + (pagination.current - 1) * pagination.pageSize }}
|
{{ rowIndex + 1 + (pagination.current - 1) * pagination.pageSize }}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 操作 -->
|
<!-- 操作(我创建的;已解决/已关闭仅详情与评论) -->
|
||||||
<template #actions="{ record }">
|
<template #actions="{ record }">
|
||||||
<a-button type="text" size="small" @click="handleDetail(record)">
|
<a-button type="text" size="small" @click="handleDetail(record)">
|
||||||
详情
|
详情
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button v-if="!['resolved', 'closed'].includes(record.status)" type="text" size="small" @click="handleEdit(record)">
|
|
||||||
编辑
|
<template v-if="!isTicketResolvedOrClosed(record.status)">
|
||||||
</a-button>
|
<a-tooltip
|
||||||
<a-button type="text" size="small" @click="handleCancel(record)">
|
:content="reasonEditTicket(record, me)"
|
||||||
撤回
|
:disabled="canEditTicket(record, me)"
|
||||||
</a-button>
|
>
|
||||||
<a-button v-if="record.status === 'resolved'" type="text" size="small" @click="handleComment(record)">
|
<a-button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
:disabled="!canEditTicket(record, me)"
|
||||||
|
@click="handleEdit(record)"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</a-button>
|
||||||
|
</a-tooltip>
|
||||||
|
<a-tooltip
|
||||||
|
v-if="record.status !== 'closed' && record.status !== 'cancelled'"
|
||||||
|
:content="reasonCancelTicket(record, me)"
|
||||||
|
:disabled="canCancelTicket(record, me)"
|
||||||
|
>
|
||||||
|
<a-button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
:disabled="!canCancelTicket(record, me)"
|
||||||
|
@click="handleCancel(record)"
|
||||||
|
>
|
||||||
|
撤回
|
||||||
|
</a-button>
|
||||||
|
</a-tooltip>
|
||||||
|
<a-button type="text" size="small" @click="handleComment(record)">
|
||||||
评论
|
评论
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button type="text" size="small" @click="handleClose(record)">
|
<a-tooltip
|
||||||
关闭
|
v-if="record.status !== 'closed' && record.status !== 'cancelled'"
|
||||||
</a-button>
|
:content="reasonCloseTicket(record, me)"
|
||||||
<a-button type="text" size="small" status="danger" @click="handleDelete(record)">
|
:disabled="canCloseTicket(record, me)"
|
||||||
删除
|
>
|
||||||
|
<a-button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
:disabled="!canCloseTicket(record, me)"
|
||||||
|
@click="handleClose(record)"
|
||||||
|
>
|
||||||
|
关闭
|
||||||
|
</a-button>
|
||||||
|
</a-tooltip>
|
||||||
|
<a-tooltip :content="reasonDeleteTicket(record, me)" :disabled="canDeleteTicket(record, me)">
|
||||||
|
<a-button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
status="danger"
|
||||||
|
:disabled="!canDeleteTicket(record, me)"
|
||||||
|
@click="handleDelete(record)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</a-button>
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<a-button
|
||||||
|
v-else
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
@click="handleComment(record)"
|
||||||
|
>
|
||||||
|
评论
|
||||||
</a-button>
|
</a-button>
|
||||||
</template>
|
</template>
|
||||||
</search-table>
|
</search-table>
|
||||||
@@ -192,14 +312,14 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, reactive, computed } from 'vue'
|
import { ref, reactive, computed } from 'vue'
|
||||||
import { Message, Modal } from '@arco-design/web-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 { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||||
import type { FormItem } from '@/components/search-form/types'
|
import type { FormItem } from '@/components/search-form/types'
|
||||||
import SearchTable from '@/components/search-table/index.vue'
|
import SearchTable from '@/components/search-table/index.vue'
|
||||||
import { searchFormConfig } from './config/search-form'
|
import { searchFormConfig } from './config/search-form'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
import {
|
import {
|
||||||
fetchFeedbackTickets,
|
fetchFeedbackTickets,
|
||||||
transferFeedbackTicket,
|
acceptFeedbackTicket,
|
||||||
suspendFeedbackTicket,
|
suspendFeedbackTicket,
|
||||||
resumeFeedbackTicket,
|
resumeFeedbackTicket,
|
||||||
resolveFeedbackTicket,
|
resolveFeedbackTicket,
|
||||||
@@ -207,6 +327,28 @@ import {
|
|||||||
closeFeedbackTicket,
|
closeFeedbackTicket,
|
||||||
deleteFeedbackTicket,
|
deleteFeedbackTicket,
|
||||||
} from '@/api/ops/feedbackTicket'
|
} from '@/api/ops/feedbackTicket'
|
||||||
|
import {
|
||||||
|
getFeedbackCurrentUserId,
|
||||||
|
canAcceptTicket,
|
||||||
|
canTransferTicket,
|
||||||
|
canSuspendTicket,
|
||||||
|
canResumeTicket,
|
||||||
|
canResolveTicket,
|
||||||
|
canCloseTicket,
|
||||||
|
canEditTicket,
|
||||||
|
canCancelTicket,
|
||||||
|
canDeleteTicket,
|
||||||
|
reasonAcceptTicket,
|
||||||
|
reasonTransferTicket,
|
||||||
|
reasonSuspendTicket,
|
||||||
|
reasonResumeTicket,
|
||||||
|
reasonResolveTicket,
|
||||||
|
reasonCloseTicket,
|
||||||
|
reasonCancelTicket,
|
||||||
|
reasonDeleteTicket,
|
||||||
|
reasonEditTicket,
|
||||||
|
isTicketResolvedOrClosed,
|
||||||
|
} from '../utils/ticketActionPermissions'
|
||||||
import TicketDetailDialog from '../components/TicketDetailDialog.vue'
|
import TicketDetailDialog from '../components/TicketDetailDialog.vue'
|
||||||
import TransferDialog from '../components/TransferDialog.vue'
|
import TransferDialog from '../components/TransferDialog.vue'
|
||||||
import SuspendDialog from '../components/SuspendDialog.vue'
|
import SuspendDialog from '../components/SuspendDialog.vue'
|
||||||
@@ -243,6 +385,18 @@ const priorityMap: Record<string, { text: string; color: string }> = {
|
|||||||
// 当前激活的选项卡
|
// 当前激活的选项卡
|
||||||
const activeTab = ref<'pending' | 'my-created'>('pending')
|
const activeTab = ref<'pending' | 'my-created'>('pending')
|
||||||
|
|
||||||
|
/** 待处理 Tab:已接单且未关闭、处理人为我(默认多状态,见 GET /tickets 的 status string[] 说明) */
|
||||||
|
const PENDING_TAB_DEFAULT_STATUS = ['accepted', 'processing', 'suspended', 'resolved'] as const
|
||||||
|
|
||||||
|
function formatTicketQueryDateTime(v: unknown): string | undefined {
|
||||||
|
if (v == null || v === '') return undefined
|
||||||
|
const d = dayjs(v as string | Date | dayjs.Dayjs)
|
||||||
|
return d.isValid() ? d.format('YYYY-MM-DD HH:mm:ss') : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当前账号 ID(登录 details.user_id,与工单 creator_id / assignee_id 比较) */
|
||||||
|
const me = computed(() => getFeedbackCurrentUserId())
|
||||||
|
|
||||||
// 状态管理
|
// 状态管理
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const tableData = ref<any[]>([])
|
const tableData = ref<any[]>([])
|
||||||
@@ -251,6 +405,7 @@ const formModel = ref({
|
|||||||
type: '',
|
type: '',
|
||||||
priority: '',
|
priority: '',
|
||||||
status: '',
|
status: '',
|
||||||
|
dateRange: [] as unknown[],
|
||||||
})
|
})
|
||||||
|
|
||||||
const pagination = reactive({
|
const pagination = reactive({
|
||||||
@@ -366,27 +521,43 @@ const suspendVisible = ref(false)
|
|||||||
const resolveVisible = ref(false)
|
const resolveVisible = ref(false)
|
||||||
const commentVisible = ref(false)
|
const commentVisible = ref(false)
|
||||||
|
|
||||||
// 获取工单列表
|
// 获取工单列表(GET /Feedback/v1/tickets)
|
||||||
const fetchTickets = async () => {
|
const fetchTickets = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 两个 Tab 均依赖当前用户 id(assignee_id / creator_id);缺失则不请求以免数据范围错误
|
||||||
|
if (me.value <= 0) {
|
||||||
|
tableData.value = []
|
||||||
|
pagination.total = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyword = typeof formModel.value.keyword === 'string' ? formModel.value.keyword.trim() : ''
|
||||||
const params: any = {
|
const params: any = {
|
||||||
page: pagination.current,
|
page: pagination.current,
|
||||||
page_size: pagination.pageSize,
|
page_size: pagination.pageSize,
|
||||||
keyword: formModel.value.keyword || undefined,
|
keyword: keyword || undefined,
|
||||||
type: formModel.value.type || undefined,
|
type: formModel.value.type || undefined,
|
||||||
priority: formModel.value.priority || undefined,
|
priority: formModel.value.priority || undefined,
|
||||||
status: formModel.value.status || undefined,
|
status: formModel.value.status || undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (formModel.value.dateRange && formModel.value.dateRange.length === 2) {
|
||||||
|
const [start, end] = formModel.value.dateRange
|
||||||
|
const st = formatTicketQueryDateTime(start)
|
||||||
|
const et = formatTicketQueryDateTime(end)
|
||||||
|
if (st) params.start_time = st
|
||||||
|
if (et) params.end_time = et
|
||||||
|
}
|
||||||
|
|
||||||
if (activeTab.value === 'pending') {
|
if (activeTab.value === 'pending') {
|
||||||
// 待处理工单:分配给当前用户且状态为 pending/accepted/processing/suspended
|
params.assignee_id = me.value
|
||||||
params.status = 'accepted,processing,suspended'
|
if (!formModel.value.status) {
|
||||||
|
params.status = [...PENDING_TAB_DEFAULT_STATUS]
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// 我创建的工单:由当前用户创建
|
params.creator_id = me.value
|
||||||
// 这里需要从用户信息中获取 creator_id
|
|
||||||
// params.creator_id = userStore.userId
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetchFeedbackTickets(params)
|
const res = await fetchFeedbackTickets(params)
|
||||||
@@ -424,6 +595,7 @@ const handleReset = () => {
|
|||||||
type: '',
|
type: '',
|
||||||
priority: '',
|
priority: '',
|
||||||
status: '',
|
status: '',
|
||||||
|
dateRange: [],
|
||||||
}
|
}
|
||||||
pagination.current = 1
|
pagination.current = 1
|
||||||
fetchTickets()
|
fetchTickets()
|
||||||
@@ -441,12 +613,6 @@ const handleRefresh = () => {
|
|||||||
Message.success('数据已刷新')
|
Message.success('数据已刷新')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新建工单
|
|
||||||
const handleCreate = () => {
|
|
||||||
// TODO: 打开新建工单对话框
|
|
||||||
Message.info('新建工单功能待实现')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 详情
|
// 详情
|
||||||
const handleDetail = (record: any) => {
|
const handleDetail = (record: any) => {
|
||||||
console.log('查看详情:', record)
|
console.log('查看详情:', record)
|
||||||
@@ -454,9 +620,33 @@ const handleDetail = (record: any) => {
|
|||||||
detailVisible.value = true
|
detailVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 接单(待处理列表若含待接单工单)
|
||||||
|
const handleAccept = async (record: any) => {
|
||||||
|
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 handleEdit = (record: any) => {
|
const handleEdit = (record: any) => {
|
||||||
console.log('编辑工单:', record)
|
if (!canEditTicket(record, me.value)) {
|
||||||
|
Message.warning(reasonEditTicket(record, me.value) || '当前不可编辑')
|
||||||
|
return
|
||||||
|
}
|
||||||
// TODO: 打开编辑对话框
|
// TODO: 打开编辑对话框
|
||||||
Message.info('编辑工单功能待实现')
|
Message.info('编辑工单功能待实现')
|
||||||
}
|
}
|
||||||
|
|||||||
167
src/views/ops/pages/feedback/utils/ticketActionPermissions.ts
Normal file
167
src/views/ops/pages/feedback/utils/ticketActionPermissions.ts
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
import useUserStore from '@/store/modules/user'
|
||||||
|
import SafeStorage, { AppStorageKey } from '@/utils/safeStorage'
|
||||||
|
|
||||||
|
/** 列表行与后端 FeedbackTicket 对齐:含 status / creator_id / assignee_id */
|
||||||
|
export interface FeedbackTicketRow {
|
||||||
|
status?: string
|
||||||
|
creator_id?: number
|
||||||
|
assignee_id?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
function numId(v: unknown): number {
|
||||||
|
if (v === null || v === undefined || v === '') return 0
|
||||||
|
const n = Number(v)
|
||||||
|
return Number.isFinite(n) ? n : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前登录用户 ID(与 JWT / GetUserInfo 一致)。
|
||||||
|
* 取自登录接口 details,写入 Pinia `user.userInfo` 与 SafeStorage USER_INFO(含 user_id)。
|
||||||
|
*/
|
||||||
|
export function getFeedbackCurrentUserId(): number {
|
||||||
|
const userStore = useUserStore()
|
||||||
|
let payload = userStore.$state.userInfo as Record<string, unknown> | undefined | null
|
||||||
|
if (!payload || typeof payload !== 'object') {
|
||||||
|
payload = SafeStorage.get(AppStorageKey.USER_INFO) as Record<string, unknown> | undefined | null
|
||||||
|
}
|
||||||
|
if (payload && typeof payload === 'object') {
|
||||||
|
return numId(payload.user_id ?? payload.id)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function assigneeId(row: FeedbackTicketRow): number {
|
||||||
|
return numId(row.assignee_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function creatorId(row: FeedbackTicketRow): number {
|
||||||
|
return numId(row.creator_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isClosedOrCancelled(status: string | undefined): boolean {
|
||||||
|
return status === 'closed' || status === 'cancelled'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 已解决或已关闭:列表操作仅保留「详情」「评论」 */
|
||||||
|
export function isTicketResolvedOrClosed(status: string | undefined): boolean {
|
||||||
|
return status === 'resolved' || status === 'closed'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canAcceptTicket(row: FeedbackTicketRow, me: number): boolean {
|
||||||
|
if (row.status !== 'pending') return false
|
||||||
|
const aid = assigneeId(row)
|
||||||
|
if (aid > 0) return me > 0 && aid === me
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canTransferTicket(row: FeedbackTicketRow, me: number): boolean {
|
||||||
|
if (isClosedOrCancelled(row.status)) return false
|
||||||
|
const aid = assigneeId(row)
|
||||||
|
return me > 0 && aid > 0 && aid === me
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canCancelTicket(row: FeedbackTicketRow, me: number): boolean {
|
||||||
|
if (isClosedOrCancelled(row.status)) return false
|
||||||
|
return me > 0 && creatorId(row) === me
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canSuspendTicket(row: FeedbackTicketRow, me: number): boolean {
|
||||||
|
if (isClosedOrCancelled(row.status)) return false
|
||||||
|
if (row.status === 'suspended') return false
|
||||||
|
const aid = assigneeId(row)
|
||||||
|
return me > 0 && aid > 0 && aid === me
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canResumeTicket(row: FeedbackTicketRow, me: number): boolean {
|
||||||
|
if (row.status !== 'suspended') return false
|
||||||
|
const aid = assigneeId(row)
|
||||||
|
return me > 0 && aid > 0 && aid === me
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canResolveTicket(row: FeedbackTicketRow, me: number): boolean {
|
||||||
|
if (isClosedOrCancelled(row.status)) return false
|
||||||
|
const aid = assigneeId(row)
|
||||||
|
return me > 0 && aid > 0 && aid === me
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canCloseTicket(row: FeedbackTicketRow, me: number): boolean {
|
||||||
|
if (row.status === 'closed') return false
|
||||||
|
if (row.status === 'cancelled') return false
|
||||||
|
if (me <= 0) return false
|
||||||
|
return creatorId(row) === me || assigneeId(row) === me
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 仅创建人、且待接单(pending)时可编辑(产品约定) */
|
||||||
|
export function canEditTicket(row: FeedbackTicketRow, me: number): boolean {
|
||||||
|
if (isClosedOrCancelled(row.status)) return false
|
||||||
|
if (row.status !== 'pending') return false
|
||||||
|
if (me <= 0) return false
|
||||||
|
return creatorId(row) === me
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reasonEditTicket(row: FeedbackTicketRow, me: number): string {
|
||||||
|
if (isClosedOrCancelled(row.status)) return '工单已关闭或撤回,不允许修改'
|
||||||
|
if (row.status !== 'pending') return '仅待接单状态下可由创建人编辑'
|
||||||
|
if (me <= 0) return '无法识别当前用户'
|
||||||
|
if (creatorId(row) !== me) return '只有创建人可在待接单时编辑工单'
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canDeleteTicket(row: FeedbackTicketRow, me: number): boolean {
|
||||||
|
return me > 0 && creatorId(row) === me
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 禁用时的原因文案(与后端错误信息对齐,便于用户理解) */
|
||||||
|
export function reasonAcceptTicket(row: FeedbackTicketRow, me: number): string {
|
||||||
|
if (row.status !== 'pending') return '工单状态不是待接单,无法接单'
|
||||||
|
const aid = assigneeId(row)
|
||||||
|
if (aid > 0 && aid !== me) return '该工单已指定其他处理人'
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reasonTransferTicket(row: FeedbackTicketRow, me: number): string {
|
||||||
|
if (isClosedOrCancelled(row.status)) return '工单已关闭或撤回,无法转交'
|
||||||
|
const aid = assigneeId(row)
|
||||||
|
if (aid <= 0 || aid !== me) return '只有当前处理人可以转交工单'
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reasonSuspendTicket(row: FeedbackTicketRow, me: number): string {
|
||||||
|
if (isClosedOrCancelled(row.status)) return '工单已关闭或撤回,无法挂起'
|
||||||
|
if (row.status === 'suspended') return '工单已经是挂起状态'
|
||||||
|
const aid = assigneeId(row)
|
||||||
|
if (aid <= 0 || aid !== me) return '只有处理人可以挂起工单'
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reasonResumeTicket(row: FeedbackTicketRow, me: number): string {
|
||||||
|
if (row.status !== 'suspended') return '工单不是挂起状态,无法重启'
|
||||||
|
const aid = assigneeId(row)
|
||||||
|
if (aid <= 0 || aid !== me) return '只有处理人可以重启工单'
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reasonResolveTicket(row: FeedbackTicketRow, me: number): string {
|
||||||
|
if (isClosedOrCancelled(row.status)) return '工单已关闭或撤回'
|
||||||
|
const aid = assigneeId(row)
|
||||||
|
if (aid <= 0 || aid !== me) return '只有处理人可以解决工单'
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reasonCloseTicket(row: FeedbackTicketRow, me: number): string {
|
||||||
|
if (row.status === 'closed') return '工单已关闭'
|
||||||
|
if (row.status === 'cancelled') return '工单已撤回,无法关闭'
|
||||||
|
if (creatorId(row) !== me && assigneeId(row) !== me) return '只有创建人或处理人可以关闭工单'
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reasonCancelTicket(row: FeedbackTicketRow, me: number): string {
|
||||||
|
if (isClosedOrCancelled(row.status)) return '工单已关闭或撤回'
|
||||||
|
if (creatorId(row) !== me) return '只有创建人可以撤回工单'
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reasonDeleteTicket(row: FeedbackTicketRow, me: number): string {
|
||||||
|
if (creatorId(row) !== me) return '只有创建人可以删除工单'
|
||||||
|
return ''
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user