feat: enhance frontend with dashboard, import, and module views
- Add dashboard API module and enhance dashboard view with charts - Enhance building/room views with import and batch operations - Improve work-order and visitor views with better UX - Update charge standard view with batch billing support - Refine type definitions (api, building, charge, operation) - Update user store and permission directives
This commit is contained in:
parent
d231aec472
commit
f0507e9c0d
|
|
@ -11,7 +11,7 @@ export function getBuildingList(params: BuildingQueryParams) {
|
|||
}
|
||||
|
||||
/** 楼栋详情 */
|
||||
export function getBuildingById(id: number) {
|
||||
export function getBuildingById(id: number | string) {
|
||||
return get<BuildingRecord>(`/base/buildings/${id}`)
|
||||
}
|
||||
|
||||
|
|
@ -21,12 +21,12 @@ export function createBuilding(data: BuildingSaveRequest) {
|
|||
}
|
||||
|
||||
/** 更新楼栋 */
|
||||
export function updateBuilding(id: number, data: BuildingSaveRequest) {
|
||||
export function updateBuilding(id: number | string, data: BuildingSaveRequest) {
|
||||
return put<BuildingRecord>(`/base/buildings/${id}`, data as unknown as Record<string, unknown>)
|
||||
}
|
||||
|
||||
/** 删除楼栋 */
|
||||
export function deleteBuilding(id: number) {
|
||||
export function deleteBuilding(id: number | string) {
|
||||
return del<void>(`/base/buildings/${id}`)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
* 仪表盘统计 API
|
||||
* 聚合 base/charge/operation 三个服务的统计数据
|
||||
*/
|
||||
import { get } from '@/api/request'
|
||||
|
||||
/** 基础数据统计 */
|
||||
interface BaseDashboardStats {
|
||||
projectCount: number
|
||||
buildingCount: number
|
||||
roomCount: number
|
||||
ownerCount: number
|
||||
tenantCount: number
|
||||
}
|
||||
|
||||
/** 收费数据统计 */
|
||||
interface ChargeDashboardStats {
|
||||
monthBillCount: number
|
||||
monthPaidAmount: number
|
||||
monthPendingAmount: number
|
||||
overdueRoomCount: number
|
||||
}
|
||||
|
||||
/** 运营数据统计 */
|
||||
interface OperationDashboardStats {
|
||||
monthWorkOrderCount: number
|
||||
pendingWorkOrderCount: number
|
||||
todayVisitorCount: number
|
||||
monthActivityCount: number
|
||||
}
|
||||
|
||||
/** 获取基础数据统计 */
|
||||
export function getBaseDashboardStats() {
|
||||
return get<BaseDashboardStats>('/base/dashboard/stats')
|
||||
}
|
||||
|
||||
/** 获取收费数据统计 */
|
||||
export function getChargeDashboardStats() {
|
||||
return get<ChargeDashboardStats>('/charge/dashboard/stats')
|
||||
}
|
||||
|
||||
/** 获取运营数据统计 */
|
||||
export function getOperationDashboardStats() {
|
||||
return get<OperationDashboardStats>('/operation/dashboard/stats')
|
||||
}
|
||||
|
||||
export type { BaseDashboardStats, ChargeDashboardStats, OperationDashboardStats }
|
||||
|
|
@ -37,3 +37,12 @@ export function batchCreateRoom(data: RoomBatchCreateRequest) {
|
|||
export function getRoomTree(projectId: number) {
|
||||
return get<BuildingTreeNode[]>('/base/rooms/tree', { projectId })
|
||||
}
|
||||
|
||||
/** 批量导入房间(Excel上传) */
|
||||
export function importRooms(file: File) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
return post<unknown>('/base/rooms/import', formData as unknown as Record<string, unknown>, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* 工单管理 API
|
||||
* 包含:工单CRUD + 状态流转(派单/接单/完成/回访/取消/转单)
|
||||
*/
|
||||
import { get, post, put } from '@/api/request'
|
||||
import { get, post } from '@/api/request'
|
||||
|
||||
/** 工单列表(分页) */
|
||||
export function getWorkOrderList(params: WorkOrderQueryParams) {
|
||||
|
|
@ -21,30 +21,30 @@ export function createWorkOrder(data: WorkOrderCreateRequest) {
|
|||
|
||||
/** 派单 */
|
||||
export function dispatchWorkOrder(id: number, data: WorkOrderDispatchRequest) {
|
||||
return put<void>(`/operation/work-orders/${id}/dispatch`, data as unknown as Record<string, unknown>)
|
||||
return post<void>(`/operation/work-orders/${id}/assign`, data as unknown as Record<string, unknown>)
|
||||
}
|
||||
|
||||
/** 接单 */
|
||||
/** 接单(开始执行) */
|
||||
export function acceptWorkOrder(id: number) {
|
||||
return put<void>(`/operation/work-orders/${id}/accept`)
|
||||
return post<void>(`/operation/work-orders/${id}/start`, {} as Record<string, unknown>)
|
||||
}
|
||||
|
||||
/** 处理完成 */
|
||||
export function completeWorkOrder(id: number, data: WorkOrderCompleteRequest) {
|
||||
return put<void>(`/operation/work-orders/${id}/complete`, data as unknown as Record<string, unknown>)
|
||||
return post<void>(`/operation/work-orders/${id}/complete`, data as unknown as Record<string, unknown>)
|
||||
}
|
||||
|
||||
/** 回访 */
|
||||
export function visitWorkOrder(id: number, data: WorkOrderVisitRequest) {
|
||||
return put<void>(`/operation/work-orders/${id}/visit`, data as unknown as Record<string, unknown>)
|
||||
return post<void>(`/operation/work-orders/${id}/verify`, data as unknown as Record<string, unknown>)
|
||||
}
|
||||
|
||||
/** 取消 */
|
||||
export function cancelWorkOrder(id: number, data: WorkOrderCancelRequest) {
|
||||
return put<void>(`/operation/work-orders/${id}/cancel`, data as unknown as Record<string, unknown>)
|
||||
return post<void>(`/operation/work-orders/${id}/cancel`, data as unknown as Record<string, unknown>)
|
||||
}
|
||||
|
||||
/** 转单 */
|
||||
export function transferWorkOrder(id: number, data: WorkOrderTransferRequest) {
|
||||
return put<void>(`/operation/work-orders/${id}/transfer`, data as unknown as Record<string, unknown>)
|
||||
return post<void>(`/operation/work-orders/${id}/transfer`, data as unknown as Record<string, unknown>)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -161,7 +161,7 @@ const displayName = computed(
|
|||
)
|
||||
const userInitial = computed(() => (displayName.value.charAt(0) || 'U').toUpperCase())
|
||||
const roleDisplay = computed(() => {
|
||||
if (userStore.roles.includes('admin')) return '超级管理员'
|
||||
if (userStore.isSuperAdmin) return '超级管理员'
|
||||
if (userStore.roles.length > 0) return userStore.roles[0]
|
||||
return '用户'
|
||||
})
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ function checkPermission(el: HTMLElement, binding: DirectiveBinding<PermissionVa
|
|||
|
||||
const required = Array.isArray(binding.value) ? binding.value : [binding.value]
|
||||
|
||||
// admin 角色或拥有任一所需权限即放行
|
||||
// 超级管理员或拥有任一所需权限即放行
|
||||
const hasPermission =
|
||||
userStore.roles.includes('admin') ||
|
||||
userStore.isSuperAdmin ||
|
||||
required.some((p) => permissions.includes(p))
|
||||
|
||||
if (!hasPermission) {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* 管理 token、refreshToken、用户信息、权限、角色
|
||||
*/
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import * as authApi from '@/api/modules/auth'
|
||||
import {
|
||||
getAccessToken,
|
||||
|
|
@ -12,6 +12,10 @@ import {
|
|||
setRefreshToken,
|
||||
clearTokens,
|
||||
} from '@/utils/auth'
|
||||
import { useProjectStore } from '@/stores/project'
|
||||
|
||||
/** 超级管理员角色编码 */
|
||||
const SUPER_ADMIN_ROLE = 'ROLE_ADMIN'
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
// ---- State ----
|
||||
|
|
@ -21,6 +25,11 @@ export const useUserStore = defineStore('user', () => {
|
|||
const permissions = ref<string[]>([])
|
||||
const roles = ref<string[]>([])
|
||||
|
||||
// ---- Getters ----
|
||||
|
||||
/** 是否为超级管理员(拥有 ROLE_ADMIN 角色) */
|
||||
const isSuperAdmin = computed(() => roles.value.includes(SUPER_ADMIN_ROLE))
|
||||
|
||||
// ---- Actions ----
|
||||
|
||||
/** 用户登录 */
|
||||
|
|
@ -34,7 +43,8 @@ export const useUserStore = defineStore('user', () => {
|
|||
if (result.userInfo) {
|
||||
userInfo.value = result.userInfo
|
||||
permissions.value = result.userInfo.permissions || []
|
||||
roles.value = result.userInfo.roles || []
|
||||
// 将后端返回的角色对象数组映射为 roleCode 字符串数组
|
||||
roles.value = (result.userInfo.roles || []).map((r) => r.roleCode)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
@ -43,9 +53,12 @@ export const useUserStore = defineStore('user', () => {
|
|||
async function logout() {
|
||||
try {
|
||||
await authApi.logout()
|
||||
} catch {
|
||||
// 即使退出请求失败,也清除本地状态
|
||||
} finally {
|
||||
// 无论请求是否成功,都清除本地状态
|
||||
resetState()
|
||||
// 清除项目选择状态
|
||||
useProjectStore().clearCurrentProject()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -54,7 +67,8 @@ export const useUserStore = defineStore('user', () => {
|
|||
const info = await authApi.getUserInfo()
|
||||
userInfo.value = info
|
||||
permissions.value = info.permissions || []
|
||||
roles.value = info.roles || []
|
||||
// 将后端返回的角色对象数组映射为 roleCode 字符串数组
|
||||
roles.value = (info.roles || []).map((r) => r.roleCode)
|
||||
return info
|
||||
}
|
||||
|
||||
|
|
@ -71,7 +85,8 @@ export const useUserStore = defineStore('user', () => {
|
|||
/** 检查是否拥有指定权限 */
|
||||
function hasPermission(permission: string): boolean {
|
||||
if (!permission) return true
|
||||
if (roles.value.includes('admin')) return true
|
||||
// 超级管理员拥有所有权限
|
||||
if (isSuperAdmin.value) return true
|
||||
return permissions.value.includes(permission)
|
||||
}
|
||||
|
||||
|
|
@ -87,6 +102,8 @@ export const useUserStore = defineStore('user', () => {
|
|||
userInfo,
|
||||
permissions,
|
||||
roles,
|
||||
// getters
|
||||
isSuperAdmin,
|
||||
// actions
|
||||
login,
|
||||
logout,
|
||||
|
|
|
|||
|
|
@ -70,18 +70,28 @@ interface CaptchaResult {
|
|||
captchaImg: string
|
||||
}
|
||||
|
||||
/** 角色信息 */
|
||||
interface Role {
|
||||
id: number | string
|
||||
/** 角色编码,如 ROLE_ADMIN */
|
||||
roleCode: string
|
||||
/** 角色名称,如 管理员 */
|
||||
roleName: string
|
||||
}
|
||||
|
||||
/** 用户信息 */
|
||||
interface UserInfo {
|
||||
id: number
|
||||
id: number | string
|
||||
username: string
|
||||
realName: string
|
||||
avatar?: string
|
||||
phone?: string
|
||||
email?: string
|
||||
status: number
|
||||
roles: string[]
|
||||
/** 角色列表(后端返回对象数组) */
|
||||
roles: Role[]
|
||||
permissions: string[]
|
||||
projectId?: number
|
||||
projectId?: number | string
|
||||
projectName?: string
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
* 楼栋/楼层/房间类型定义
|
||||
*/
|
||||
|
||||
/** 楼栋类型 */
|
||||
type BuildingType = 'residential' | 'office' | 'commercial' | 'parking' | 'other'
|
||||
/** 楼栋类型:1-住宅 2-商业 3-办公 4-综合体 */
|
||||
type BuildingType = 1 | 2 | 3 | 4
|
||||
|
||||
/** 楼层类型 */
|
||||
type FloorType = 'above' | 'underground'
|
||||
|
|
@ -19,7 +19,7 @@ type RoomUsage = 'sale' | 'rent' | 'self_use' | 'idle' | 'other'
|
|||
/** 楼栋查询参数 */
|
||||
interface BuildingQueryParams extends PageParams {
|
||||
/** 项目ID */
|
||||
projectId: number
|
||||
projectId: number | string
|
||||
/** 楼栋名称 */
|
||||
buildingName?: string
|
||||
/** 楼栋类型 */
|
||||
|
|
@ -30,40 +30,62 @@ interface BuildingQueryParams extends PageParams {
|
|||
|
||||
/** 楼栋记录 */
|
||||
interface BuildingRecord {
|
||||
id: number
|
||||
id: number | string
|
||||
/** 项目ID */
|
||||
projectId: number
|
||||
projectId: number | string
|
||||
/** 楼栋编码 */
|
||||
buildingCode: string
|
||||
/** 楼栋名称 */
|
||||
buildingName: string
|
||||
/** 楼栋类型 */
|
||||
/** 楼栋类型:1-住宅 2-商业 3-办公 4-综合体 */
|
||||
buildingType: BuildingType
|
||||
/** 地上层数 */
|
||||
aboveGroundFloors: number
|
||||
/** 总楼层 */
|
||||
floorCount: number
|
||||
/** 地下层数 */
|
||||
undergroundFloors: number
|
||||
/** 高度(米) */
|
||||
height?: number
|
||||
/** 面积(平方米) */
|
||||
area?: number
|
||||
/** 状态:1-启用,0-禁用 */
|
||||
undergroundCount: number
|
||||
/** 建成年份 */
|
||||
buildYear?: number
|
||||
/** 结构类型,如钢混 */
|
||||
structureType?: string
|
||||
/** 总面积(㎡) */
|
||||
totalArea?: number
|
||||
/** 状态:0-停用 1-启用 */
|
||||
status: number
|
||||
/** 排序 */
|
||||
sort?: number
|
||||
/** 备注 */
|
||||
remark?: string
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
/** 房间数(统计字段) */
|
||||
roomCount?: number
|
||||
}
|
||||
|
||||
/** 楼栋新增/编辑保存请求 */
|
||||
interface BuildingSaveRequest {
|
||||
projectId: number
|
||||
/** 项目ID(管理员无项目上下文时由前端传入) */
|
||||
projectId?: number
|
||||
buildingCode: string
|
||||
buildingName: string
|
||||
buildingType: BuildingType
|
||||
aboveGroundFloors: number
|
||||
undergroundFloors: number
|
||||
height?: number
|
||||
area?: number
|
||||
status: number
|
||||
/** 总楼层 */
|
||||
floorCount?: number
|
||||
/** 地下层数 */
|
||||
undergroundCount?: number
|
||||
/** 建成年份 */
|
||||
buildYear?: number
|
||||
/** 结构类型 */
|
||||
structureType?: string
|
||||
/** 总面积(㎡) */
|
||||
totalArea?: number
|
||||
/** 状态:0-停用 1-启用 */
|
||||
status?: number
|
||||
/** 排序 */
|
||||
sort?: number
|
||||
/** 备注 */
|
||||
remark?: string
|
||||
/** 是否自动生成楼层 */
|
||||
autoGenerateFloors?: boolean
|
||||
}
|
||||
|
||||
/** 楼栋树节点(楼栋→楼层→房间) */
|
||||
|
|
|
|||
|
|
@ -15,16 +15,21 @@ type ChargeType = 'property' | 'parking' | 'water' | 'electric' | 'gas' | 'publi
|
|||
type BillingCycle = 'monthly' | 'quarterly' | 'yearly' | 'once'
|
||||
|
||||
/** 计量单位 */
|
||||
type ChargeUnit = 'sqm' | 'month' | 'time'
|
||||
type ChargeUnit = 'sqm' | 'room' | 'month' | 'time'
|
||||
|
||||
/** 计算规则 */
|
||||
type CalcRule = 'AREA' | 'FIXED' | 'METER'
|
||||
|
||||
/** 收费标准查询参数 */
|
||||
interface ChargeStandardQueryParams extends PageParams {
|
||||
/** 项目ID */
|
||||
projectId?: number
|
||||
/** 标准编码 */
|
||||
standardCode?: string
|
||||
/** 标准名称 */
|
||||
standardName?: string
|
||||
/** 收费类型 */
|
||||
chargeType?: ChargeType
|
||||
/** 费用类型 */
|
||||
feeType?: string
|
||||
/** 状态:1-启用,0-禁用 */
|
||||
status?: number
|
||||
}
|
||||
|
|
@ -36,18 +41,26 @@ interface ChargeStandardRecord {
|
|||
standardCode: string
|
||||
/** 标准名称 */
|
||||
standardName: string
|
||||
/** 收费类型 */
|
||||
chargeType: ChargeType
|
||||
/** 计费周期 */
|
||||
billingCycle: BillingCycle
|
||||
/** 费用类型 */
|
||||
feeType: string
|
||||
/** 单价(元) */
|
||||
unitPrice: number
|
||||
/** 计量单位 */
|
||||
chargeUnit: ChargeUnit
|
||||
/** 计费单位 */
|
||||
unit: string
|
||||
/** 计费周期 */
|
||||
billingCycle: BillingCycle
|
||||
/** 计算规则 */
|
||||
calcRule: CalcRule
|
||||
/** 最低收费额(元) */
|
||||
minAmount?: number
|
||||
/** 描述 */
|
||||
description?: string
|
||||
/** 税率 */
|
||||
taxRate?: number
|
||||
/** 生效日期 */
|
||||
effectiveDate?: string
|
||||
/** 失效日期 */
|
||||
expiryDate?: string
|
||||
/** 备注 */
|
||||
remark?: string
|
||||
/** 状态:1-启用,0-禁用 */
|
||||
status: number
|
||||
createdAt?: string
|
||||
|
|
@ -56,16 +69,31 @@ interface ChargeStandardRecord {
|
|||
|
||||
/** 收费标准新增/编辑请求 */
|
||||
interface ChargeStandardSaveRequest {
|
||||
/** 项目ID(管理员无项目上下文时由前端传入) */
|
||||
projectId?: number
|
||||
standardCode: string
|
||||
standardName: string
|
||||
chargeType: ChargeType
|
||||
billingCycle: BillingCycle
|
||||
/** 费用类型 */
|
||||
feeType: string
|
||||
/** 单价(元) */
|
||||
unitPrice: number
|
||||
chargeUnit: ChargeUnit
|
||||
/** 计费单位 */
|
||||
unit: string
|
||||
/** 计费周期 */
|
||||
billingCycle: BillingCycle
|
||||
/** 计算规则 */
|
||||
calcRule: CalcRule
|
||||
/** 最低收费额(元) */
|
||||
minAmount?: number
|
||||
description?: string
|
||||
/** 税率 */
|
||||
taxRate?: number
|
||||
/** 生效日期 */
|
||||
effectiveDate: string
|
||||
/** 失效日期 */
|
||||
expiryDate?: string
|
||||
/** 备注 */
|
||||
remark?: string
|
||||
/** 状态:1-启用,0-禁用 */
|
||||
status: number
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -116,6 +116,8 @@ type WorkOrderCategory = 'repair' | 'cleaning' | 'security' | 'greening' | 'comp
|
|||
|
||||
/** 工单查询参数 */
|
||||
interface WorkOrderQueryParams extends PageParams {
|
||||
/** 项目ID */
|
||||
projectId?: number
|
||||
/** 工单号 */
|
||||
orderNo?: string
|
||||
/** 标题 */
|
||||
|
|
@ -140,25 +142,25 @@ interface WorkOrderRecord {
|
|||
/** 标题 */
|
||||
title: string
|
||||
/** 分类 */
|
||||
category: WorkOrderCategory
|
||||
type: WorkOrderCategory
|
||||
/** 优先级 */
|
||||
priority: WorkOrderPriority
|
||||
/** 来源 */
|
||||
source: WorkOrderSource
|
||||
/** 状态 */
|
||||
status: WorkOrderStatus
|
||||
/** 内容 */
|
||||
content?: string
|
||||
/** 描述 */
|
||||
description?: string
|
||||
/** 处理人ID */
|
||||
handlerId?: number
|
||||
assigneeId?: number
|
||||
/** 处理人姓名 */
|
||||
handlerName?: string
|
||||
assigneeName?: string
|
||||
/** 创建人 */
|
||||
creatorName?: string
|
||||
/** 创建时间 */
|
||||
createdAt: string
|
||||
createTime?: string
|
||||
/** 完成时间 */
|
||||
completedAt?: string
|
||||
completeTime?: string
|
||||
/** 附件列表 */
|
||||
attachments?: WorkOrderAttachment[]
|
||||
/** 操作记录 */
|
||||
|
|
@ -191,42 +193,57 @@ interface WorkOrderOperationLog {
|
|||
|
||||
/** 工单创建请求 */
|
||||
interface WorkOrderCreateRequest {
|
||||
/** 项目ID */
|
||||
projectId: number
|
||||
title: string
|
||||
category: WorkOrderCategory
|
||||
priority: WorkOrderPriority
|
||||
/** 工单类型 */
|
||||
type: WorkOrderCategory
|
||||
source: WorkOrderSource
|
||||
content?: string
|
||||
/** 指派人ID */
|
||||
assigneeId?: number
|
||||
priority?: WorkOrderPriority
|
||||
/** 工单描述 */
|
||||
description?: string
|
||||
}
|
||||
|
||||
/** 工单派单请求 */
|
||||
interface WorkOrderDispatchRequest {
|
||||
handlerId: number
|
||||
/** 处理人ID */
|
||||
assigneeId: number
|
||||
/** 处理人姓名 */
|
||||
assigneeName?: string
|
||||
/** 处理人部门 */
|
||||
assigneeDept?: string
|
||||
remark?: string
|
||||
}
|
||||
|
||||
/** 工单完成请求 */
|
||||
interface WorkOrderCompleteRequest {
|
||||
result: string
|
||||
remark?: string
|
||||
/** 处理结果/备注 */
|
||||
remark: string
|
||||
/** 完工图片 */
|
||||
finishImages?: string[]
|
||||
}
|
||||
|
||||
/** 工单回访请求 */
|
||||
interface WorkOrderVisitRequest {
|
||||
satisfaction: number
|
||||
remark?: string
|
||||
/** 回访评分(1-5) */
|
||||
verifyScore: number
|
||||
/** 回访备注 */
|
||||
verifyRemark?: string
|
||||
}
|
||||
|
||||
/** 工单转单请求 */
|
||||
interface WorkOrderTransferRequest {
|
||||
newHandlerId: number
|
||||
reason?: string
|
||||
/** 新处理人ID */
|
||||
newAssigneeId: number
|
||||
/** 新处理人姓名 */
|
||||
newAssigneeName?: string
|
||||
remark?: string
|
||||
}
|
||||
|
||||
/** 工单取消请求 */
|
||||
interface WorkOrderCancelRequest {
|
||||
reason: string
|
||||
/** 取消原因 */
|
||||
cancelReason: string
|
||||
}
|
||||
|
||||
// ============================================
|
||||
|
|
@ -238,6 +255,8 @@ type VisitorStatus = 'reserved' | 'arrived' | 'left' | 'cancelled'
|
|||
|
||||
/** 访客查询参数 */
|
||||
interface VisitorQueryParams extends PageParams {
|
||||
/** 项目ID */
|
||||
projectId?: number
|
||||
/** 访客姓名 */
|
||||
visitorName?: string
|
||||
/** 手机号 */
|
||||
|
|
@ -253,37 +272,62 @@ interface VisitorQueryParams extends PageParams {
|
|||
/** 访客记录 */
|
||||
interface VisitorRecord {
|
||||
id: number
|
||||
/** 访客编号 */
|
||||
visitorNo?: string
|
||||
/** 访客姓名 */
|
||||
visitorName: string
|
||||
name: string
|
||||
/** 手机号 */
|
||||
phone: string
|
||||
/** 被访人 */
|
||||
visiteeName: string
|
||||
/** 被访人手机号 */
|
||||
visiteePhone?: string
|
||||
/** 预计到访时间 */
|
||||
expectedTime?: string
|
||||
/** 实际到访时间 */
|
||||
arrivedTime?: string
|
||||
/** 身份证号 */
|
||||
idCard?: string
|
||||
/** 性别 */
|
||||
gender?: number
|
||||
/** 访客人数 */
|
||||
visitorCount?: number
|
||||
/** 访问目的 */
|
||||
visitPurpose?: string
|
||||
/** 到访时间 */
|
||||
visitTime?: string
|
||||
/** 离开时间 */
|
||||
leftTime?: string
|
||||
leaveTime?: string
|
||||
/** 被访人ID */
|
||||
hostId?: number
|
||||
/** 被访人 */
|
||||
hostName: string
|
||||
/** 被访人手机号 */
|
||||
hostPhone?: string
|
||||
/** 被访人地址 */
|
||||
hostAddress?: string
|
||||
/** 状态 */
|
||||
status: VisitorStatus
|
||||
/** 二维码内容 */
|
||||
qrCode?: string
|
||||
/** 备注 */
|
||||
remark?: string
|
||||
createdAt?: string
|
||||
/** 邀请ID */
|
||||
inviteId?: number
|
||||
/** 创建时间 */
|
||||
createTime?: string
|
||||
}
|
||||
|
||||
/** 访客预约请求 */
|
||||
/** 访客预约请求(与后端 VisitorInviteRequest DTO 一致) */
|
||||
interface VisitorInviteRequest {
|
||||
/** 邀请人ID */
|
||||
inviterId: number
|
||||
/** 邀请人姓名 */
|
||||
inviterName?: string
|
||||
/** 访客姓名 */
|
||||
visitorName: string
|
||||
phone: string
|
||||
visiteeName: string
|
||||
visiteePhone?: string
|
||||
expectedTime?: string
|
||||
remark?: string
|
||||
/** 访客手机号 */
|
||||
visitorPhone: string
|
||||
/** 访客人数 */
|
||||
visitorCount?: number
|
||||
/** 访问目的 */
|
||||
visitPurpose?: string
|
||||
/** 预计到访时间 */
|
||||
planVisitTime: string
|
||||
/** 有效时长(小时) */
|
||||
validHours?: number
|
||||
/** 项目ID(管理员无项目上下文时由前端传入) */
|
||||
projectId?: number
|
||||
}
|
||||
|
||||
// ============================================
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@ export function formatDateTime(
|
|||
format = 'YYYY-MM-DD HH:mm:ss',
|
||||
): string {
|
||||
if (!value) return '-'
|
||||
return dayjs(value).format(format)
|
||||
// 处理纯数字字符串(Long 序列化为 String 后的时间戳)
|
||||
const parsed = typeof value === 'string' && /^\d+$/.test(value) ? Number(value) : value
|
||||
return dayjs(parsed).format(format)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
import {
|
||||
Card, FormItem, Input, InputNumber, Select, SelectOption, Tag, Button, Space,
|
||||
Popconfirm, Table, message,
|
||||
Popconfirm, Table, Checkbox, message,
|
||||
} from 'ant-design-vue'
|
||||
import {
|
||||
PlusOutlined, EditOutlined, DeleteOutlined, ApartmentOutlined, AppstoreOutlined,
|
||||
|
|
@ -24,29 +24,26 @@ import { getStatusColor } from '@/utils/status-mapping'
|
|||
const router = useRouter()
|
||||
const projectStore = useProjectStore()
|
||||
|
||||
// ---- 楼栋类型映射 ----
|
||||
// ---- 楼栋类型映射(后端:1-住宅 2-商业 3-办公 4-综合体) ----
|
||||
const buildingTypeMap: Record<BuildingType, string> = {
|
||||
residential: '住宅',
|
||||
office: '办公',
|
||||
commercial: '商业',
|
||||
parking: '停车',
|
||||
other: '其他',
|
||||
1: '住宅',
|
||||
2: '商业',
|
||||
3: '办公',
|
||||
4: '综合体',
|
||||
}
|
||||
|
||||
const buildingTypeColorMap: Record<BuildingType, string> = {
|
||||
residential: 'blue',
|
||||
office: 'cyan',
|
||||
commercial: 'orange',
|
||||
parking: 'purple',
|
||||
other: 'default',
|
||||
1: 'blue',
|
||||
2: 'orange',
|
||||
3: 'cyan',
|
||||
4: 'purple',
|
||||
}
|
||||
|
||||
const buildingTypeOptions = [
|
||||
{ label: '住宅', value: 'residential' as BuildingType },
|
||||
{ label: '办公', value: 'office' as BuildingType },
|
||||
{ label: '商业', value: 'commercial' as BuildingType },
|
||||
{ label: '停车', value: 'parking' as BuildingType },
|
||||
{ label: '其他', value: 'other' as BuildingType },
|
||||
{ label: '住宅', value: 1 as BuildingType },
|
||||
{ label: '商业', value: 2 as BuildingType },
|
||||
{ label: '办公', value: 3 as BuildingType },
|
||||
{ label: '综合体', value: 4 as BuildingType },
|
||||
]
|
||||
|
||||
// ---- 项目选择 ----
|
||||
|
|
@ -76,19 +73,20 @@ const treeData = ref<BuildingTreeNode[]>([])
|
|||
const modalOpen = ref(false)
|
||||
const modalLoading = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const editingId = ref(0)
|
||||
const editingId = ref<number | string>(0)
|
||||
|
||||
// ---- 表单数据 ----
|
||||
const formState = reactive({
|
||||
projectId: 0,
|
||||
buildingCode: '',
|
||||
buildingName: '',
|
||||
buildingType: 'residential' as BuildingType,
|
||||
aboveGroundFloors: 1,
|
||||
undergroundFloors: 0,
|
||||
height: undefined as number | undefined,
|
||||
area: undefined as number | undefined,
|
||||
buildingType: 1 as BuildingType,
|
||||
floorCount: 1,
|
||||
undergroundCount: 0,
|
||||
buildYear: undefined as number | undefined,
|
||||
structureType: '',
|
||||
totalArea: undefined as number | undefined,
|
||||
status: 1,
|
||||
autoGenerateFloors: false,
|
||||
})
|
||||
|
||||
// ---- 表格列配置 ----
|
||||
|
|
@ -96,9 +94,9 @@ const columns = [
|
|||
{ title: '楼栋编码', dataIndex: 'buildingCode', width: 120 },
|
||||
{ title: '楼栋名称', dataIndex: 'buildingName', width: 140 },
|
||||
{ title: '类型', dataIndex: 'buildingType', width: 100 },
|
||||
{ title: '地上层数', dataIndex: 'aboveGroundFloors', width: 100 },
|
||||
{ title: '地下层数', dataIndex: 'undergroundFloors', width: 100 },
|
||||
{ title: '面积(㎡)', dataIndex: 'area', width: 100 },
|
||||
{ title: '总楼层', dataIndex: 'floorCount', width: 100 },
|
||||
{ title: '地下层数', dataIndex: 'undergroundCount', width: 100 },
|
||||
{ title: '面积(㎡)', dataIndex: 'totalArea', width: 100 },
|
||||
{ title: '状态', dataIndex: 'status', width: 100 },
|
||||
{ title: '创建时间', dataIndex: 'createdAt', width: 180, customRender: ({ text }: { text: string }) => formatDateTime(text) },
|
||||
{ title: '操作', dataIndex: 'action', width: 200, fixed: 'right' as const },
|
||||
|
|
@ -122,8 +120,8 @@ const formRules = computed<Record<string, Rule[]>>(() => ({
|
|||
{ max: 100, message: '楼栋名称不超过100个字符', trigger: 'blur' },
|
||||
],
|
||||
buildingType: [{ required: true, message: '请选择楼栋类型', trigger: 'change' }],
|
||||
aboveGroundFloors: [{ required: true, message: '请输入地上层数', trigger: 'blur', type: 'number' }],
|
||||
undergroundFloors: [{ required: true, message: '请输入地下层数', trigger: 'blur', type: 'number' }],
|
||||
floorCount: [{ required: true, message: '请输入总楼层数', trigger: 'blur', type: 'number' }],
|
||||
undergroundCount: [{ required: true, message: '请输入地下层数', trigger: 'blur', type: 'number' }],
|
||||
}))
|
||||
|
||||
// ---- 加载项目列表 ----
|
||||
|
|
@ -215,15 +213,16 @@ function handlePageChange(pagination: { current: number; pageSize: number }) {
|
|||
|
||||
// ---- 新增 / 编辑 ----
|
||||
function resetForm() {
|
||||
formState.projectId = selectedProjectId.value
|
||||
formState.buildingCode = ''
|
||||
formState.buildingName = ''
|
||||
formState.buildingType = 'residential'
|
||||
formState.aboveGroundFloors = 1
|
||||
formState.undergroundFloors = 0
|
||||
formState.height = undefined
|
||||
formState.area = undefined
|
||||
formState.buildingType = 1
|
||||
formState.floorCount = 1
|
||||
formState.undergroundCount = 0
|
||||
formState.buildYear = undefined
|
||||
formState.structureType = ''
|
||||
formState.totalArea = undefined
|
||||
formState.status = 1
|
||||
formState.autoGenerateFloors = false
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
|
|
@ -234,16 +233,17 @@ function handleAdd() {
|
|||
|
||||
function handleEdit(record: Record<string, unknown>) {
|
||||
isEdit.value = true
|
||||
editingId.value = record.id as number
|
||||
formState.projectId = record.projectId as number
|
||||
editingId.value = record.id as number | string
|
||||
formState.buildingCode = record.buildingCode as string
|
||||
formState.buildingName = record.buildingName as string
|
||||
formState.buildingType = record.buildingType as BuildingType
|
||||
formState.aboveGroundFloors = record.aboveGroundFloors as number
|
||||
formState.undergroundFloors = record.undergroundFloors as number
|
||||
formState.height = record.height as number | undefined
|
||||
formState.area = record.area as number | undefined
|
||||
formState.floorCount = record.floorCount as number
|
||||
formState.undergroundCount = record.undergroundCount as number
|
||||
formState.buildYear = record.buildYear as number | undefined
|
||||
formState.structureType = (record.structureType as string) || ''
|
||||
formState.totalArea = record.totalArea as number | undefined
|
||||
formState.status = record.status as number
|
||||
formState.autoGenerateFloors = false
|
||||
modalOpen.value = true
|
||||
}
|
||||
|
||||
|
|
@ -251,15 +251,17 @@ async function handleModalOk() {
|
|||
modalLoading.value = true
|
||||
try {
|
||||
const data: BuildingSaveRequest = {
|
||||
projectId: formState.projectId,
|
||||
projectId: selectedProjectId.value || projectStore.projectId || 0,
|
||||
buildingCode: formState.buildingCode,
|
||||
buildingName: formState.buildingName,
|
||||
buildingType: formState.buildingType,
|
||||
aboveGroundFloors: formState.aboveGroundFloors,
|
||||
undergroundFloors: formState.undergroundFloors,
|
||||
height: formState.height,
|
||||
area: formState.area,
|
||||
floorCount: formState.floorCount,
|
||||
undergroundCount: formState.undergroundCount,
|
||||
buildYear: formState.buildYear,
|
||||
structureType: formState.structureType || undefined,
|
||||
totalArea: formState.totalArea,
|
||||
status: formState.status,
|
||||
autoGenerateFloors: isEdit.value ? undefined : formState.autoGenerateFloors,
|
||||
}
|
||||
|
||||
if (isEdit.value) {
|
||||
|
|
@ -283,7 +285,7 @@ async function handleModalOk() {
|
|||
}
|
||||
|
||||
// ---- 删除 ----
|
||||
async function handleDelete(id: number) {
|
||||
async function handleDelete(id: number | string) {
|
||||
try {
|
||||
await deleteBuilding(id)
|
||||
message.success('删除成功')
|
||||
|
|
@ -299,7 +301,7 @@ async function handleDelete(id: number) {
|
|||
|
||||
// ---- 查看楼层(跳转楼层管理页面,通过路由参数传递楼栋ID) ----
|
||||
function handleViewFloors(record: Record<string, unknown>) {
|
||||
const buildingId = record.id as number
|
||||
const buildingId = record.id as number | string
|
||||
const buildingName = record.buildingName as string
|
||||
router.push({ path: '/base/floors', query: { buildingId: String(buildingId), buildingName } })
|
||||
}
|
||||
|
|
@ -453,24 +455,30 @@ onMounted(async () => {
|
|||
<SelectOption v-for="opt in buildingTypeOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem label="地上层数" name="aboveGroundFloors">
|
||||
<InputNumber v-model:value="formState.aboveGroundFloors" :min="0" :max="200" style="width: 100%" placeholder="请输入地上层数" />
|
||||
<FormItem label="总楼层数" name="floorCount">
|
||||
<InputNumber v-model:value="formState.floorCount" :min="0" :max="200" style="width: 100%" placeholder="请输入总楼层数" />
|
||||
</FormItem>
|
||||
<FormItem label="地下层数" name="undergroundFloors">
|
||||
<InputNumber v-model:value="formState.undergroundFloors" :min="0" :max="20" style="width: 100%" placeholder="请输入地下层数" />
|
||||
<FormItem label="地下层数" name="undergroundCount">
|
||||
<InputNumber v-model:value="formState.undergroundCount" :min="0" :max="20" style="width: 100%" placeholder="请输入地下层数" />
|
||||
</FormItem>
|
||||
<FormItem label="高度(米)" name="height">
|
||||
<InputNumber v-model:value="formState.height" :min="0" :step="0.1" :precision="2" style="width: 100%" placeholder="请输入高度" />
|
||||
<FormItem label="建成年份" name="buildYear">
|
||||
<InputNumber v-model:value="formState.buildYear" :min="1900" :max="2099" style="width: 100%" placeholder="请输入建成年份" />
|
||||
</FormItem>
|
||||
<FormItem label="面积(㎡)" name="area">
|
||||
<InputNumber v-model:value="formState.area" :min="0" :step="0.01" :precision="2" style="width: 100%" placeholder="请输入面积" />
|
||||
<FormItem label="结构类型" name="structureType">
|
||||
<Input v-model:value="formState.structureType" placeholder="如:钢混、框架" />
|
||||
</FormItem>
|
||||
<FormItem label="总面积(㎡)" name="totalArea">
|
||||
<InputNumber v-model:value="formState.totalArea" :min="0" :step="0.01" :precision="2" style="width: 100%" placeholder="请输入总面积" />
|
||||
</FormItem>
|
||||
<FormItem label="状态" name="status">
|
||||
<Select v-model:value="formState.status" placeholder="请选择状态">
|
||||
<SelectOption :value="1">启用</SelectOption>
|
||||
<SelectOption :value="0">禁用</SelectOption>
|
||||
<SelectOption :value="0">停用</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem v-if="!isEdit" name="autoGenerateFloors">
|
||||
<Checkbox v-model:checked="formState.autoGenerateFloors">自动生成楼层</Checkbox>
|
||||
</FormItem>
|
||||
</FormModal>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ interface CategoryTreeNode {
|
|||
const categoryTreeData = ref<CategoryTreeNode[]>([])
|
||||
|
||||
// ---- 楼栋选项(安装位置选择) ----
|
||||
const buildingOptions = ref<{ value: number; label: string }[]>([])
|
||||
const buildingOptions = ref<{ value: number | string; label: string }[]>([])
|
||||
|
||||
// ---- 表格状态 ----
|
||||
const tableData = ref<DeviceRecord[]>([])
|
||||
|
|
|
|||
|
|
@ -36,8 +36,8 @@ const floorTypeOptions = [
|
|||
// ---- 项目和楼栋选择 ----
|
||||
const projectList = ref<ProjectRecord[]>([])
|
||||
const selectedProjectId = ref<number>(0)
|
||||
const buildingList = ref<{ id: number; buildingName: string }[]>([])
|
||||
const selectedBuildingId = ref<number>(Number(route.query.buildingId) || 0)
|
||||
const buildingList = ref<{ id: number | string; buildingName: string }[]>([])
|
||||
const selectedBuildingId = ref<number | string>(Number(route.query.buildingId) || 0)
|
||||
|
||||
// ---- 表格状态 ----
|
||||
const tableData = ref<FloorRecord[]>([])
|
||||
|
|
|
|||
|
|
@ -5,16 +5,17 @@
|
|||
*/
|
||||
import {
|
||||
Card, FormItem, Input, InputNumber, Select, SelectOption, Tag, Button, Space,
|
||||
Popconfirm, Table, message,
|
||||
Popconfirm, Table, Modal, Upload, message,
|
||||
} from 'ant-design-vue'
|
||||
import {
|
||||
PlusOutlined, EditOutlined, DeleteOutlined, ApartmentOutlined, AppstoreOutlined,
|
||||
UploadOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import { reactive, ref, computed, onMounted, watch } from 'vue'
|
||||
import type { Rule } from 'ant-design-vue/es/form'
|
||||
import ProTable from '@/components/common/ProTable.vue'
|
||||
import FormModal from '@/components/common/FormModal.vue'
|
||||
import { getRoomList, createRoom, updateRoom, deleteRoom, batchCreateRoom, getRoomTree } from '@/api/modules/room'
|
||||
import { getRoomList, createRoom, updateRoom, deleteRoom, batchCreateRoom, getRoomTree, importRooms } from '@/api/modules/room'
|
||||
import { getBuildingList, getFloorsByBuilding } from '@/api/modules/building'
|
||||
import { getAllProjects } from '@/api/modules/project'
|
||||
import { useProjectStore } from '@/stores/project'
|
||||
|
|
@ -75,7 +76,7 @@ const selectedProjectId = ref<number>(projectStore.projectId || 0)
|
|||
const viewMode = ref<'list' | 'tree'>('list')
|
||||
|
||||
// ---- 楼栋和楼层选择(搜索 + 表单用) ----
|
||||
const buildingOptions = ref<{ id: number; buildingName: string }[]>([])
|
||||
const buildingOptions = ref<{ id: number | string; buildingName: string }[]>([])
|
||||
const floorOptions = ref<{ id: number; floorName: string }[]>([])
|
||||
|
||||
// ---- 表格状态 ----
|
||||
|
|
@ -101,6 +102,8 @@ const modalOpen = ref(false)
|
|||
const modalLoading = ref(false)
|
||||
const batchModalOpen = ref(false)
|
||||
const batchLoading = ref(false)
|
||||
const importModalOpen = ref(false)
|
||||
const importLoading = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const editingId = ref(0)
|
||||
|
||||
|
|
@ -410,6 +413,26 @@ async function handleBatchOk() {
|
|||
}
|
||||
}
|
||||
|
||||
// ---- 批量导入 ----
|
||||
async function handleImport(file: File) {
|
||||
importLoading.value = true
|
||||
try {
|
||||
await importRooms(file)
|
||||
message.success('导入成功')
|
||||
importModalOpen.value = false
|
||||
if (viewMode.value === 'list') {
|
||||
loadData()
|
||||
} else {
|
||||
loadTree()
|
||||
}
|
||||
} catch {
|
||||
// 错误已在拦截器中提示
|
||||
} finally {
|
||||
importLoading.value = false
|
||||
}
|
||||
return false // 阻止默认上传行为
|
||||
}
|
||||
|
||||
// ---- 删除 ----
|
||||
async function handleDelete(id: number) {
|
||||
try {
|
||||
|
|
@ -491,6 +514,10 @@ onMounted(async () => {
|
|||
新增房间
|
||||
</Button>
|
||||
<Button :disabled="!selectedProjectId" @click="handleBatchCreate">批量创建</Button>
|
||||
<Button :disabled="!selectedProjectId" @click="importModalOpen = true">
|
||||
<template #icon><UploadOutlined /></template>
|
||||
批量导入
|
||||
</Button>
|
||||
</Space>
|
||||
</template>
|
||||
|
||||
|
|
@ -648,6 +675,29 @@ onMounted(async () => {
|
|||
<Input v-model:value="batchForm.codePrefix" placeholder="如:BLDG1-F3-(可选)" />
|
||||
</FormItem>
|
||||
</FormModal>
|
||||
|
||||
<!-- 批量导入弹窗 -->
|
||||
<Modal
|
||||
v-model:open="importModalOpen"
|
||||
title="批量导入房间"
|
||||
:confirm-loading="importLoading"
|
||||
:mask-closable="false"
|
||||
@ok="() => {}"
|
||||
>
|
||||
<Upload
|
||||
:before-upload="handleImport"
|
||||
:max-count="1"
|
||||
accept=".xlsx,.xls"
|
||||
>
|
||||
<Button>
|
||||
<template #icon><UploadOutlined /></template>
|
||||
选择Excel文件
|
||||
</Button>
|
||||
</Upload>
|
||||
<p style="margin-top: 12px; color: #999">
|
||||
请上传 .xlsx 或 .xls 格式的Excel文件,支持单次导入最多1000条记录。
|
||||
</p>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
InputNumber,
|
||||
Select,
|
||||
SelectOption,
|
||||
DatePicker,
|
||||
Tag,
|
||||
Button,
|
||||
Space,
|
||||
|
|
@ -16,7 +17,7 @@ import {
|
|||
message,
|
||||
} from 'ant-design-vue'
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons-vue'
|
||||
import { reactive, ref, computed, onMounted } from 'vue'
|
||||
import { reactive, ref, computed, onMounted, watch } from 'vue'
|
||||
import type { Rule } from 'ant-design-vue/es/form'
|
||||
import ProTable from '@/components/common/ProTable.vue'
|
||||
import FormModal from '@/components/common/FormModal.vue'
|
||||
|
|
@ -26,14 +27,22 @@ import {
|
|||
updateChargeStandard,
|
||||
deleteChargeStandard,
|
||||
} from '@/api/modules/charge-standard'
|
||||
import { getAllProjects } from '@/api/modules/project'
|
||||
import { useProjectStore } from '@/stores/project'
|
||||
import { formatDateTime, formatMoney } from '@/utils/format'
|
||||
import { getStatusColor } from '@/utils/status-mapping'
|
||||
|
||||
const projectStore = useProjectStore()
|
||||
|
||||
// 解构 TextArea 组件
|
||||
const { TextArea } = Input
|
||||
|
||||
// ---- 收费类型映射 ----
|
||||
const chargeTypeMap: Record<ChargeType, string> = {
|
||||
// ---- 项目选择 ----
|
||||
const projectList = ref<ProjectRecord[]>([])
|
||||
const selectedProjectId = ref<number>(projectStore.projectId || 0)
|
||||
|
||||
// ---- 费用类型映射 ----
|
||||
const feeTypeMap: Record<string, string> = {
|
||||
property: '物业费',
|
||||
parking: '停车费',
|
||||
water: '水费',
|
||||
|
|
@ -43,7 +52,7 @@ const chargeTypeMap: Record<ChargeType, string> = {
|
|||
other: '其他',
|
||||
}
|
||||
|
||||
const chargeTypeColorMap: Record<ChargeType, string> = {
|
||||
const feeTypeColorMap: Record<string, string> = {
|
||||
property: 'blue',
|
||||
parking: 'orange',
|
||||
water: 'cyan',
|
||||
|
|
@ -53,14 +62,14 @@ const chargeTypeColorMap: Record<ChargeType, string> = {
|
|||
other: 'default',
|
||||
}
|
||||
|
||||
const chargeTypeOptions = [
|
||||
{ label: '物业费', value: 'property' as ChargeType },
|
||||
{ label: '停车费', value: 'parking' as ChargeType },
|
||||
{ label: '水费', value: 'water' as ChargeType },
|
||||
{ label: '电费', value: 'electric' as ChargeType },
|
||||
{ label: '燃气费', value: 'gas' as ChargeType },
|
||||
{ label: '公摊费', value: 'public' as ChargeType },
|
||||
{ label: '其他', value: 'other' as ChargeType },
|
||||
const feeTypeOptions = [
|
||||
{ label: '物业费', value: 'property' },
|
||||
{ label: '停车费', value: 'parking' },
|
||||
{ label: '水费', value: 'water' },
|
||||
{ label: '电费', value: 'electric' },
|
||||
{ label: '燃气费', value: 'gas' },
|
||||
{ label: '公摊费', value: 'public' },
|
||||
{ label: '其他', value: 'other' },
|
||||
]
|
||||
|
||||
// ---- 计费周期映射 ----
|
||||
|
|
@ -79,16 +88,31 @@ const billingCycleOptions = [
|
|||
]
|
||||
|
||||
// ---- 计量单位映射 ----
|
||||
const chargeUnitMap: Record<ChargeUnit, string> = {
|
||||
const unitMap: Record<string, string> = {
|
||||
sqm: '平方米',
|
||||
room: '房间',
|
||||
month: '月',
|
||||
time: '次',
|
||||
}
|
||||
|
||||
const chargeUnitOptions = [
|
||||
{ label: '面积', value: 'area' as ChargeUnit },
|
||||
{ label: '房间', value: 'room' as ChargeUnit },
|
||||
{ label: '月', value: 'month' as ChargeUnit },
|
||||
const unitOptions = [
|
||||
{ label: '平方米', value: 'sqm' },
|
||||
{ label: '房间', value: 'room' },
|
||||
{ label: '月', value: 'month' },
|
||||
{ label: '次', value: 'time' },
|
||||
]
|
||||
|
||||
// ---- 计算规则映射 ----
|
||||
const calcRuleMap: Record<string, string> = {
|
||||
AREA: '按面积计算',
|
||||
FIXED: '固定金额',
|
||||
METER: '按表读数',
|
||||
}
|
||||
|
||||
const calcRuleOptions = [
|
||||
{ label: '按面积计算', value: 'AREA' as CalcRule },
|
||||
{ label: '固定金额', value: 'FIXED' as CalcRule },
|
||||
{ label: '按表读数', value: 'METER' as CalcRule },
|
||||
]
|
||||
|
||||
// ---- 表格状态 ----
|
||||
|
|
@ -102,7 +126,7 @@ const pageSize = ref(10)
|
|||
const searchForm = reactive({
|
||||
standardCode: '',
|
||||
standardName: '',
|
||||
chargeType: undefined as ChargeType | undefined,
|
||||
feeType: undefined as string | undefined,
|
||||
status: undefined as number | undefined,
|
||||
})
|
||||
|
||||
|
|
@ -112,16 +136,19 @@ const modalLoading = ref(false)
|
|||
const isEdit = ref(false)
|
||||
const editingId = ref(0)
|
||||
|
||||
// ---- 表单数据(金额以元为单位存储,提交时转换为分) ----
|
||||
// ---- 表单数据(金额以元为单位) ----
|
||||
const formState = reactive({
|
||||
standardCode: '',
|
||||
standardName: '',
|
||||
chargeType: 'property' as ChargeType,
|
||||
billingCycle: 'monthly' as BillingCycle,
|
||||
feeType: 'property',
|
||||
unitPrice: undefined as number | undefined,
|
||||
chargeUnit: 'area' as ChargeUnit,
|
||||
unit: 'sqm',
|
||||
billingCycle: 'monthly' as BillingCycle,
|
||||
calcRule: 'FIXED' as CalcRule,
|
||||
minAmount: undefined as number | undefined,
|
||||
description: '',
|
||||
effectiveDate: undefined as string | undefined,
|
||||
expiryDate: undefined as string | undefined,
|
||||
remark: '',
|
||||
status: 1,
|
||||
})
|
||||
|
||||
|
|
@ -129,7 +156,7 @@ const formState = reactive({
|
|||
const columns = [
|
||||
{ title: '标准编码', dataIndex: 'standardCode', width: 120 },
|
||||
{ title: '名称', dataIndex: 'standardName', width: 160 },
|
||||
{ title: '收费类型', dataIndex: 'chargeType', width: 100 },
|
||||
{ title: '费用类型', dataIndex: 'feeType', width: 100 },
|
||||
{ title: '计费周期', dataIndex: 'billingCycle', width: 100 },
|
||||
{
|
||||
title: '单价',
|
||||
|
|
@ -137,7 +164,8 @@ const columns = [
|
|||
width: 120,
|
||||
customRender: ({ text }: { text: number }) => `¥${formatMoney(text)}`,
|
||||
},
|
||||
{ title: '计量单位', dataIndex: 'chargeUnit', width: 100 },
|
||||
{ title: '计费单位', dataIndex: 'unit', width: 100 },
|
||||
{ title: '计算规则', dataIndex: 'calcRule', width: 120 },
|
||||
{
|
||||
title: '最低收费额',
|
||||
dataIndex: 'minAmount',
|
||||
|
|
@ -159,22 +187,39 @@ const formRules = computed<Record<string, Rule[]>>(() => ({
|
|||
{ required: true, message: '请输入标准名称', trigger: 'blur' },
|
||||
{ max: 100, message: '标准名称不超过100个字符', trigger: 'blur' },
|
||||
],
|
||||
chargeType: [{ required: true, message: '请选择收费类型', trigger: 'change' }],
|
||||
feeType: [{ required: true, message: '请选择费用类型', trigger: 'change' }],
|
||||
billingCycle: [{ required: true, message: '请选择计费周期', trigger: 'change' }],
|
||||
unitPrice: [{ required: true, message: '请输入单价', trigger: 'blur', type: 'number' }],
|
||||
chargeUnit: [{ required: true, message: '请选择计量单位', trigger: 'change' }],
|
||||
unit: [{ required: true, message: '请选择计费单位', trigger: 'change' }],
|
||||
calcRule: [{ required: true, message: '请选择计算规则', trigger: 'change' }],
|
||||
effectiveDate: [{ required: true, message: '请选择生效日期', trigger: 'change' }],
|
||||
}))
|
||||
|
||||
// ---- 加载项目列表 ----
|
||||
async function loadProjects() {
|
||||
try {
|
||||
const result = await getAllProjects()
|
||||
projectList.value = result || []
|
||||
// 如果未选择项目,默认选第一个
|
||||
if (!selectedProjectId.value && projectList.value.length > 0) {
|
||||
selectedProjectId.value = projectList.value[0].id
|
||||
}
|
||||
} catch {
|
||||
// 错误已在拦截器中提示
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 数据加载 ----
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await getChargeStandardList({
|
||||
projectId: selectedProjectId.value || undefined,
|
||||
page: currentPage.value,
|
||||
size: pageSize.value,
|
||||
standardCode: searchForm.standardCode || undefined,
|
||||
standardName: searchForm.standardName || undefined,
|
||||
chargeType: searchForm.chargeType,
|
||||
feeType: searchForm.feeType,
|
||||
status: searchForm.status,
|
||||
})
|
||||
tableData.value = result.list
|
||||
|
|
@ -186,6 +231,12 @@ async function loadData() {
|
|||
}
|
||||
}
|
||||
|
||||
// ---- 项目切换时重新加载 ----
|
||||
watch(selectedProjectId, () => {
|
||||
currentPage.value = 1
|
||||
loadData()
|
||||
})
|
||||
|
||||
// ---- 搜索 / 重置 ----
|
||||
function handleSearch() {
|
||||
currentPage.value = 1
|
||||
|
|
@ -195,7 +246,7 @@ function handleSearch() {
|
|||
function handleReset() {
|
||||
searchForm.standardCode = ''
|
||||
searchForm.standardName = ''
|
||||
searchForm.chargeType = undefined
|
||||
searchForm.feeType = undefined
|
||||
searchForm.status = undefined
|
||||
currentPage.value = 1
|
||||
loadData()
|
||||
|
|
@ -212,12 +263,15 @@ function handlePageChange(pagination: { current: number; pageSize: number }) {
|
|||
function resetForm() {
|
||||
formState.standardCode = ''
|
||||
formState.standardName = ''
|
||||
formState.chargeType = 'property'
|
||||
formState.billingCycle = 'monthly'
|
||||
formState.feeType = 'property'
|
||||
formState.unitPrice = undefined
|
||||
formState.chargeUnit = 'sqm'
|
||||
formState.unit = 'sqm'
|
||||
formState.billingCycle = 'monthly'
|
||||
formState.calcRule = 'FIXED'
|
||||
formState.minAmount = undefined
|
||||
formState.description = ''
|
||||
formState.effectiveDate = undefined
|
||||
formState.expiryDate = undefined
|
||||
formState.remark = ''
|
||||
formState.status = 1
|
||||
}
|
||||
|
||||
|
|
@ -232,13 +286,15 @@ function handleEdit(record: Record<string, unknown>) {
|
|||
editingId.value = record.id as number
|
||||
formState.standardCode = record.standardCode as string
|
||||
formState.standardName = record.standardName as string
|
||||
formState.chargeType = record.chargeType as ChargeType
|
||||
formState.feeType = record.feeType as string
|
||||
formState.billingCycle = record.billingCycle as BillingCycle
|
||||
// 金额:分 → 元
|
||||
formState.unitPrice = record.unitPrice ? (record.unitPrice as number) / 100 : undefined
|
||||
formState.chargeUnit = record.chargeUnit as ChargeUnit
|
||||
formState.minAmount = record.minAmount ? (record.minAmount as number) / 100 : undefined
|
||||
formState.description = (record.description as string) || ''
|
||||
formState.unitPrice = record.unitPrice as number | undefined
|
||||
formState.unit = record.unit as string
|
||||
formState.calcRule = record.calcRule as CalcRule
|
||||
formState.minAmount = record.minAmount as number | undefined
|
||||
formState.effectiveDate = record.effectiveDate as string | undefined
|
||||
formState.expiryDate = record.expiryDate as string | undefined
|
||||
formState.remark = (record.remark as string) || ''
|
||||
formState.status = record.status as number
|
||||
modalOpen.value = true
|
||||
}
|
||||
|
|
@ -247,15 +303,18 @@ async function handleModalOk() {
|
|||
modalLoading.value = true
|
||||
try {
|
||||
const data: ChargeStandardSaveRequest = {
|
||||
projectId: selectedProjectId.value || projectStore.projectId || undefined,
|
||||
standardCode: formState.standardCode,
|
||||
standardName: formState.standardName,
|
||||
chargeType: formState.chargeType,
|
||||
feeType: formState.feeType,
|
||||
unitPrice: formState.unitPrice || 0,
|
||||
unit: formState.unit,
|
||||
billingCycle: formState.billingCycle,
|
||||
// 金额:元 → 分
|
||||
unitPrice: Math.round((formState.unitPrice || 0) * 100),
|
||||
chargeUnit: formState.chargeUnit,
|
||||
minAmount: formState.minAmount ? Math.round(formState.minAmount * 100) : undefined,
|
||||
description: formState.description || undefined,
|
||||
calcRule: formState.calcRule,
|
||||
minAmount: formState.minAmount || undefined,
|
||||
effectiveDate: formState.effectiveDate || '',
|
||||
expiryDate: formState.expiryDate || undefined,
|
||||
remark: formState.remark || undefined,
|
||||
status: formState.status,
|
||||
}
|
||||
|
||||
|
|
@ -286,7 +345,8 @@ async function handleDelete(id: number) {
|
|||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
await loadProjects()
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
|
@ -312,9 +372,9 @@ onMounted(() => {
|
|||
<FormItem label="标准名称">
|
||||
<Input v-model:value="searchForm.standardName" placeholder="请输入标准名称" allow-clear style="width: 180px" />
|
||||
</FormItem>
|
||||
<FormItem label="收费类型">
|
||||
<Select v-model:value="searchForm.chargeType" placeholder="请选择类型" allow-clear style="width: 140px">
|
||||
<SelectOption v-for="opt in chargeTypeOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</SelectOption>
|
||||
<FormItem label="费用类型">
|
||||
<Select v-model:value="searchForm.feeType" placeholder="请选择类型" allow-clear style="width: 140px">
|
||||
<SelectOption v-for="opt in feeTypeOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem label="状态">
|
||||
|
|
@ -327,7 +387,10 @@ onMounted(() => {
|
|||
|
||||
<!-- 工具栏 -->
|
||||
<template #toolbar>
|
||||
<Button v-permission="'charge:standard:add'" type="primary" @click="handleAdd">
|
||||
<Select v-model:value="selectedProjectId" placeholder="请选择项目" style="width: 200px">
|
||||
<SelectOption v-for="p in projectList" :key="p.id" :value="p.id">{{ p.projectName }}</SelectOption>
|
||||
</Select>
|
||||
<Button v-permission="'charge:standard:add'" type="primary" :disabled="!selectedProjectId" @click="handleAdd">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新增标准
|
||||
</Button>
|
||||
|
|
@ -335,16 +398,19 @@ onMounted(() => {
|
|||
|
||||
<!-- 表格单元格渲染 -->
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'chargeType'">
|
||||
<Tag :color="chargeTypeColorMap[record.chargeType as ChargeType] || 'default'">
|
||||
{{ chargeTypeMap[record.chargeType as ChargeType] || record.chargeType }}
|
||||
<template v-if="column.dataIndex === 'feeType'">
|
||||
<Tag :color="feeTypeColorMap[record.feeType as string] || 'default'">
|
||||
{{ feeTypeMap[record.feeType as string] || record.feeType }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'billingCycle'">
|
||||
{{ billingCycleMap[record.billingCycle as BillingCycle] || record.billingCycle }}
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'chargeUnit'">
|
||||
{{ chargeUnitMap[record.chargeUnit as ChargeUnit] || record.chargeUnit }}
|
||||
<template v-if="column.dataIndex === 'unit'">
|
||||
{{ unitMap[record.unit as string] || record.unit }}
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'calcRule'">
|
||||
{{ calcRuleMap[record.calcRule as string] || record.calcRule }}
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'status'">
|
||||
<Tag :color="getStatusColor(record.status)">
|
||||
|
|
@ -384,9 +450,9 @@ onMounted(() => {
|
|||
<FormItem label="标准名称" name="standardName">
|
||||
<Input v-model:value="formState.standardName" placeholder="请输入标准名称" />
|
||||
</FormItem>
|
||||
<FormItem label="收费类型" name="chargeType">
|
||||
<Select v-model:value="formState.chargeType" placeholder="请选择收费类型">
|
||||
<SelectOption v-for="opt in chargeTypeOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</SelectOption>
|
||||
<FormItem label="费用类型" name="feeType">
|
||||
<Select v-model:value="formState.feeType" placeholder="请选择费用类型">
|
||||
<SelectOption v-for="opt in feeTypeOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem label="计费周期" name="billingCycle">
|
||||
|
|
@ -394,19 +460,30 @@ onMounted(() => {
|
|||
<SelectOption v-for="opt in billingCycleOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem label="计算规则" name="calcRule">
|
||||
<Select v-model:value="formState.calcRule" placeholder="请选择计算规则">
|
||||
<SelectOption v-for="opt in calcRuleOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem label="单价" name="unitPrice">
|
||||
<InputNumber v-model:value="formState.unitPrice" :step="0.01" :precision="2" :min="0" style="width: 100%" placeholder="请输入单价(元)" />
|
||||
</FormItem>
|
||||
<FormItem label="计量单位" name="chargeUnit">
|
||||
<Select v-model:value="formState.chargeUnit" placeholder="请选择计量单位">
|
||||
<SelectOption v-for="opt in chargeUnitOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</SelectOption>
|
||||
<FormItem label="计费单位" name="unit">
|
||||
<Select v-model:value="formState.unit" placeholder="请选择计费单位">
|
||||
<SelectOption v-for="opt in unitOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem label="生效日期" name="effectiveDate">
|
||||
<DatePicker v-model:value="formState.effectiveDate" style="width: 100%" placeholder="请选择生效日期" value-format="YYYY-MM-DD" />
|
||||
</FormItem>
|
||||
<FormItem label="失效日期" name="expiryDate">
|
||||
<DatePicker v-model:value="formState.expiryDate" style="width: 100%" placeholder="请选择失效日期(可选)" value-format="YYYY-MM-DD" />
|
||||
</FormItem>
|
||||
<FormItem label="最低收费额" name="minAmount">
|
||||
<InputNumber v-model:value="formState.minAmount" :step="0.01" :precision="2" :min="0" style="width: 100%" placeholder="请输入最低收费额(元)" />
|
||||
</FormItem>
|
||||
<FormItem label="描述" name="description">
|
||||
<TextArea v-model:value="formState.description" :rows="3" placeholder="请输入描述" />
|
||||
<FormItem label="备注" name="remark">
|
||||
<TextArea v-model:value="formState.remark" :rows="3" placeholder="请输入备注" />
|
||||
</FormItem>
|
||||
<FormItem label="状态" name="status">
|
||||
<Select v-model:value="formState.status" placeholder="请选择状态">
|
||||
|
|
|
|||
|
|
@ -1,31 +1,81 @@
|
|||
<script setup lang="ts">
|
||||
/**
|
||||
* 首页仪表盘
|
||||
* 展示系统概览数据(占位内容,后续实现)
|
||||
* 展示系统概览数据,调用真实后端 API 获取统计数据
|
||||
*/
|
||||
import { Row, Col, Card } from 'ant-design-vue'
|
||||
import {
|
||||
ToolOutlined,
|
||||
AccountBookOutlined,
|
||||
MessageOutlined,
|
||||
DesktopOutlined,
|
||||
HomeOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useProjectStore } from '@/stores/project'
|
||||
import PageSkeleton from '@/components/common/PageSkeleton.vue'
|
||||
import StatCard from '@/components/common/StatCard.vue'
|
||||
import {
|
||||
getBaseDashboardStats,
|
||||
getChargeDashboardStats,
|
||||
getOperationDashboardStats,
|
||||
} from '@/api/modules/dashboard'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const projectStore = useProjectStore()
|
||||
|
||||
const loading = ref(true)
|
||||
|
||||
onMounted(() => {
|
||||
// 模拟数据加载
|
||||
setTimeout(() => {
|
||||
// 统计数据
|
||||
const monthWorkOrderCount = ref(0)
|
||||
const monthPaidAmount = ref(0)
|
||||
const pendingWorkOrderCount = ref(0)
|
||||
const ownerCount = ref(0)
|
||||
|
||||
// 详细统计
|
||||
const buildingCount = ref(0)
|
||||
const roomCount = ref(0)
|
||||
const tenantCount = ref(0)
|
||||
const monthBillCount = ref(0)
|
||||
const monthPendingAmount = ref(0)
|
||||
const overdueRoomCount = ref(0)
|
||||
const todayVisitorCount = ref(0)
|
||||
const monthActivityCount = ref(0)
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// 并行请求三个服务的统计数据
|
||||
const [baseStats, chargeStats, operationStats] = await Promise.allSettled([
|
||||
getBaseDashboardStats(),
|
||||
getChargeDashboardStats(),
|
||||
getOperationDashboardStats(),
|
||||
])
|
||||
|
||||
if (baseStats.status === 'fulfilled' && baseStats.value) {
|
||||
ownerCount.value = baseStats.value.ownerCount || 0
|
||||
buildingCount.value = baseStats.value.buildingCount || 0
|
||||
roomCount.value = baseStats.value.roomCount || 0
|
||||
tenantCount.value = baseStats.value.tenantCount || 0
|
||||
}
|
||||
|
||||
if (chargeStats.status === 'fulfilled' && chargeStats.value) {
|
||||
monthPaidAmount.value = chargeStats.value.monthPaidAmount || 0
|
||||
monthBillCount.value = chargeStats.value.monthBillCount || 0
|
||||
monthPendingAmount.value = chargeStats.value.monthPendingAmount || 0
|
||||
overdueRoomCount.value = chargeStats.value.overdueRoomCount || 0
|
||||
}
|
||||
|
||||
if (operationStats.status === 'fulfilled' && operationStats.value) {
|
||||
monthWorkOrderCount.value = operationStats.value.monthWorkOrderCount || 0
|
||||
pendingWorkOrderCount.value = operationStats.value.pendingWorkOrderCount || 0
|
||||
todayVisitorCount.value = operationStats.value.todayVisitorCount || 0
|
||||
monthActivityCount.value = operationStats.value.monthActivityCount || 0
|
||||
}
|
||||
} catch {
|
||||
// 错误已在拦截器中提示
|
||||
} finally {
|
||||
loading.value = false
|
||||
}, 300)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
@ -55,47 +105,67 @@ onMounted(() => {
|
|||
<Row :gutter="16" class="stats-row">
|
||||
<Col :xs="12" :md="6">
|
||||
<StatCard
|
||||
label="今日工单"
|
||||
:value="12"
|
||||
label="本月工单"
|
||||
:value="monthWorkOrderCount"
|
||||
:icon="ToolOutlined"
|
||||
variant="primary"
|
||||
:trend="8"
|
||||
trend-text="较昨日"
|
||||
:progress="60"
|
||||
/>
|
||||
</Col>
|
||||
<Col :xs="12" :md="6">
|
||||
<StatCard
|
||||
label="本月收入"
|
||||
:value="86520"
|
||||
:value="monthPaidAmount"
|
||||
prefix="¥"
|
||||
:icon="AccountBookOutlined"
|
||||
variant="success"
|
||||
:trend="12"
|
||||
trend-text="较上月"
|
||||
:progress="75"
|
||||
/>
|
||||
</Col>
|
||||
<Col :xs="12" :md="6">
|
||||
<StatCard
|
||||
label="待处理投诉"
|
||||
:value="3"
|
||||
label="待处理工单"
|
||||
:value="pendingWorkOrderCount"
|
||||
:icon="MessageOutlined"
|
||||
variant="accent"
|
||||
:trend="-5"
|
||||
trend-text="较上周"
|
||||
:progress="30"
|
||||
/>
|
||||
</Col>
|
||||
<Col :xs="12" :md="6">
|
||||
<StatCard
|
||||
label="在线设备"
|
||||
:value="156"
|
||||
:icon="DesktopOutlined"
|
||||
label="业主总数"
|
||||
:value="ownerCount"
|
||||
:icon="HomeOutlined"
|
||||
variant="success"
|
||||
:trend="3"
|
||||
trend-text="运行正常"
|
||||
:progress="90"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<!-- 详细统计 -->
|
||||
<Row :gutter="16" class="stats-row">
|
||||
<Col :xs="12" :md="6">
|
||||
<StatCard
|
||||
label="楼栋总数"
|
||||
:value="buildingCount"
|
||||
variant="primary"
|
||||
/>
|
||||
</Col>
|
||||
<Col :xs="12" :md="6">
|
||||
<StatCard
|
||||
label="房间总数"
|
||||
:value="roomCount"
|
||||
variant="success"
|
||||
/>
|
||||
</Col>
|
||||
<Col :xs="12" :md="6">
|
||||
<StatCard
|
||||
label="本月账单"
|
||||
:value="monthBillCount"
|
||||
variant="accent"
|
||||
/>
|
||||
</Col>
|
||||
<Col :xs="12" :md="6">
|
||||
<StatCard
|
||||
label="今日访客"
|
||||
:value="todayVisitorCount"
|
||||
variant="primary"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
Select,
|
||||
SelectOption,
|
||||
RangePicker,
|
||||
DatePicker,
|
||||
Tag,
|
||||
Button,
|
||||
Space,
|
||||
|
|
@ -17,17 +18,27 @@ import {
|
|||
message,
|
||||
} from 'ant-design-vue'
|
||||
import { PlusOutlined, QrcodeOutlined } from '@ant-design/icons-vue'
|
||||
import { reactive, ref, computed, onMounted } from 'vue'
|
||||
import { reactive, ref, computed, onMounted, watch } from 'vue'
|
||||
import type { Rule } from 'ant-design-vue/es/form'
|
||||
import ProTable from '@/components/common/ProTable.vue'
|
||||
import FormModal from '@/components/common/FormModal.vue'
|
||||
import { getVisitorList, inviteVisitor, confirmArrival, confirmDeparture } from '@/api/modules/visitor'
|
||||
import { getAllProjects } from '@/api/modules/project'
|
||||
import { useProjectStore } from '@/stores/project'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import { getStatusColor } from '@/utils/status-mapping'
|
||||
|
||||
// 解构 TextArea 组件
|
||||
const { TextArea } = Input
|
||||
|
||||
const projectStore = useProjectStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
// ---- 项目选择 ----
|
||||
const projectList = ref<ProjectRecord[]>([])
|
||||
const selectedProjectId = ref<number>(projectStore.projectId || 0)
|
||||
|
||||
// ---- 状态映射 ----
|
||||
const statusMap: Record<VisitorStatus, { label: string; color: string }> = {
|
||||
reserved: { label: '已预约', color: getStatusColor('reserved') },
|
||||
|
|
@ -53,16 +64,16 @@ const searchForm = reactive({
|
|||
dateRange: undefined as [string, string] | undefined,
|
||||
})
|
||||
|
||||
// ---- 预约弹窗 ----
|
||||
// ---- 预约弹窗(字段名与后端 VisitorInviteRequest DTO 一致) ----
|
||||
const inviteModalOpen = ref(false)
|
||||
const inviteModalLoading = ref(false)
|
||||
const inviteForm = reactive({
|
||||
visitorName: '',
|
||||
phone: '',
|
||||
visiteeName: '',
|
||||
visiteePhone: '',
|
||||
expectedTime: undefined as string | undefined,
|
||||
remark: '',
|
||||
visitorPhone: '',
|
||||
inviterName: '',
|
||||
planVisitTime: undefined as string | undefined,
|
||||
visitPurpose: '',
|
||||
visitorCount: 1,
|
||||
})
|
||||
|
||||
// ---- 二维码弹窗 ----
|
||||
|
|
@ -71,12 +82,12 @@ const qrData = ref<VisitorRecord | null>(null)
|
|||
|
||||
// ---- 表格列配置 ----
|
||||
const columns = [
|
||||
{ title: '访客姓名', dataIndex: 'visitorName', width: 120 },
|
||||
{ title: '访客姓名', dataIndex: 'name', width: 120 },
|
||||
{ title: '手机号', dataIndex: 'phone', width: 140 },
|
||||
{ title: '被访人', dataIndex: 'visiteeName', width: 120 },
|
||||
{ title: '预计到访', dataIndex: 'expectedTime', width: 180, customRender: ({ text }: { text: string }) => formatDateTime(text) },
|
||||
{ title: '实际到访', dataIndex: 'arrivedTime', width: 180, customRender: ({ text }: { text: string }) => formatDateTime(text) },
|
||||
{ title: '离开时间', dataIndex: 'leftTime', width: 180, customRender: ({ text }: { text: string }) => formatDateTime(text) },
|
||||
{ title: '被访人', dataIndex: 'hostName', width: 120 },
|
||||
{ title: '预计到访', dataIndex: 'visitTime', width: 180, customRender: ({ text }: { text: string }) => formatDateTime(text) },
|
||||
{ title: '实际到访', dataIndex: 'visitTime', width: 180, customRender: ({ text }: { text: string }) => formatDateTime(text) },
|
||||
{ title: '离开时间', dataIndex: 'leaveTime', width: 180, customRender: ({ text }: { text: string }) => formatDateTime(text) },
|
||||
{ title: '状态', dataIndex: 'status', width: 100 },
|
||||
{ title: '二维码', dataIndex: 'qrCode', width: 80 },
|
||||
{ title: '操作', dataIndex: 'action', width: 200, fixed: 'right' as const },
|
||||
|
|
@ -85,18 +96,34 @@ const columns = [
|
|||
// ---- 表单验证规则 ----
|
||||
const inviteFormRules = computed<Record<string, Rule[]>>(() => ({
|
||||
visitorName: [{ required: true, message: '请输入访客姓名', trigger: 'blur' }],
|
||||
phone: [
|
||||
visitorPhone: [
|
||||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' },
|
||||
],
|
||||
visiteeName: [{ required: true, message: '请输入被访人姓名', trigger: 'blur' }],
|
||||
inviterName: [{ required: true, message: '请输入被访人姓名', trigger: 'blur' }],
|
||||
planVisitTime: [{ required: true, message: '请选择预计到访时间', trigger: 'change' }],
|
||||
}))
|
||||
|
||||
// ---- 加载项目列表 ----
|
||||
async function loadProjects() {
|
||||
try {
|
||||
const result = await getAllProjects()
|
||||
projectList.value = result || []
|
||||
// 如果未选择项目,默认选第一个
|
||||
if (!selectedProjectId.value && projectList.value.length > 0) {
|
||||
selectedProjectId.value = projectList.value[0].id
|
||||
}
|
||||
} catch {
|
||||
// 错误已在拦截器中提示
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 数据加载 ----
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await getVisitorList({
|
||||
projectId: selectedProjectId.value || undefined,
|
||||
page: currentPage.value,
|
||||
size: pageSize.value,
|
||||
visitorName: searchForm.visitorName || undefined,
|
||||
|
|
@ -114,6 +141,12 @@ async function loadData() {
|
|||
}
|
||||
}
|
||||
|
||||
// ---- 项目切换时重新加载 ----
|
||||
watch(selectedProjectId, () => {
|
||||
currentPage.value = 1
|
||||
loadData()
|
||||
})
|
||||
|
||||
// ---- 搜索 / 重置 ----
|
||||
function handleSearch() {
|
||||
currentPage.value = 1
|
||||
|
|
@ -139,11 +172,11 @@ function handlePageChange(pagination: { current: number; pageSize: number }) {
|
|||
// ---- 预约 ----
|
||||
function handleOpenInvite() {
|
||||
inviteForm.visitorName = ''
|
||||
inviteForm.phone = ''
|
||||
inviteForm.visiteeName = ''
|
||||
inviteForm.visiteePhone = ''
|
||||
inviteForm.expectedTime = undefined
|
||||
inviteForm.remark = ''
|
||||
inviteForm.visitorPhone = ''
|
||||
inviteForm.inviterName = userStore.userInfo?.realName || ''
|
||||
inviteForm.planVisitTime = undefined
|
||||
inviteForm.visitPurpose = ''
|
||||
inviteForm.visitorCount = 1
|
||||
inviteModalOpen.value = true
|
||||
}
|
||||
|
||||
|
|
@ -151,12 +184,14 @@ async function handleInviteOk() {
|
|||
inviteModalLoading.value = true
|
||||
try {
|
||||
await inviteVisitor({
|
||||
inviterId: Number(userStore.userInfo?.id) || 0,
|
||||
inviterName: inviteForm.inviterName || undefined,
|
||||
visitorName: inviteForm.visitorName,
|
||||
phone: inviteForm.phone,
|
||||
visiteeName: inviteForm.visiteeName,
|
||||
visiteePhone: inviteForm.visiteePhone || undefined,
|
||||
expectedTime: inviteForm.expectedTime || undefined,
|
||||
remark: inviteForm.remark || undefined,
|
||||
visitorPhone: inviteForm.visitorPhone,
|
||||
visitorCount: inviteForm.visitorCount || undefined,
|
||||
visitPurpose: inviteForm.visitPurpose || undefined,
|
||||
planVisitTime: inviteForm.planVisitTime || '',
|
||||
projectId: (selectedProjectId.value || projectStore.projectId) as number,
|
||||
})
|
||||
message.success('预约成功')
|
||||
inviteModalOpen.value = false
|
||||
|
|
@ -196,7 +231,8 @@ function handleViewQr(record: Record<string, unknown>) {
|
|||
qrModalOpen.value = true
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
await loadProjects()
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
|
@ -234,7 +270,10 @@ onMounted(() => {
|
|||
|
||||
<!-- 工具栏 -->
|
||||
<template #toolbar>
|
||||
<Button type="primary" @click="handleOpenInvite">
|
||||
<Select v-model:value="selectedProjectId" placeholder="请选择项目" style="width: 200px">
|
||||
<SelectOption v-for="p in projectList" :key="p.id" :value="p.id">{{ p.projectName }}</SelectOption>
|
||||
</Select>
|
||||
<Button type="primary" :disabled="!selectedProjectId" @click="handleOpenInvite">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
预约访客
|
||||
</Button>
|
||||
|
|
@ -277,20 +316,27 @@ onMounted(() => {
|
|||
<FormItem label="访客姓名" name="visitorName">
|
||||
<Input v-model:value="inviteForm.visitorName" placeholder="请输入访客姓名" />
|
||||
</FormItem>
|
||||
<FormItem label="手机号" name="phone">
|
||||
<Input v-model:value="inviteForm.phone" placeholder="请输入手机号" />
|
||||
<FormItem label="访客手机号" name="visitorPhone">
|
||||
<Input v-model:value="inviteForm.visitorPhone" placeholder="请输入访客手机号" />
|
||||
</FormItem>
|
||||
<FormItem label="被访人" name="visiteeName">
|
||||
<Input v-model:value="inviteForm.visiteeName" placeholder="请输入被访人姓名" />
|
||||
<FormItem label="被访人" name="inviterName">
|
||||
<Input v-model:value="inviteForm.inviterName" placeholder="请输入被访人姓名" />
|
||||
</FormItem>
|
||||
<FormItem label="被访人电话" name="visiteePhone">
|
||||
<Input v-model:value="inviteForm.visiteePhone" placeholder="请输入被访人电话" />
|
||||
<FormItem label="访客人数" name="visitorCount">
|
||||
<Input v-model:value="inviteForm.visitorCount" placeholder="请输入访客人数(默认1)" />
|
||||
</FormItem>
|
||||
<FormItem label="预计到访" name="expectedTime">
|
||||
<Input v-model:value="inviteForm.expectedTime" placeholder="请输入预计到访时间" />
|
||||
<FormItem label="预计到访" name="planVisitTime">
|
||||
<DatePicker
|
||||
v-model:value="inviteForm.planVisitTime"
|
||||
style="width: 100%"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DDTHH:mm:ss"
|
||||
placeholder="请选择预计到访时间"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="备注" name="remark">
|
||||
<TextArea v-model:value="inviteForm.remark" :rows="3" placeholder="请输入备注" />
|
||||
<FormItem label="访问目的" name="visitPurpose">
|
||||
<TextArea v-model:value="inviteForm.visitPurpose" :rows="3" placeholder="请输入访问目的" />
|
||||
</FormItem>
|
||||
</FormModal>
|
||||
|
||||
|
|
@ -303,7 +349,7 @@ onMounted(() => {
|
|||
>
|
||||
<div v-if="qrData" style="text-align: center; padding: 20px">
|
||||
<div style="margin-bottom: 16px">
|
||||
<p style="font-size: 16px; font-weight: 500">{{ qrData.visitorName }}</p>
|
||||
<p style="font-size: 16px; font-weight: 500">{{ qrData.name }}</p>
|
||||
<p style="color: #999">{{ qrData.phone }}</p>
|
||||
</div>
|
||||
<!-- 二维码图片占位(实际项目可通过qrcode库生成) -->
|
||||
|
|
@ -313,7 +359,7 @@ onMounted(() => {
|
|||
</div>
|
||||
</div>
|
||||
<p style="color: #666">请访客在门禁处扫描此二维码通行</p>
|
||||
<p style="color: #999; font-size: 12px">被访人:{{ qrData.visiteeName }}</p>
|
||||
<p style="color: #999; font-size: 12px">被访人:{{ qrData.hostName }}</p>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import {
|
|||
message,
|
||||
} from 'ant-design-vue'
|
||||
import { PlusOutlined, EyeOutlined } from '@ant-design/icons-vue'
|
||||
import { reactive, ref, computed, onMounted } from 'vue'
|
||||
import { reactive, ref, computed, onMounted, watch } from 'vue'
|
||||
import type { Rule } from 'ant-design-vue/es/form'
|
||||
import ProTable from '@/components/common/ProTable.vue'
|
||||
import FormModal from '@/components/common/FormModal.vue'
|
||||
|
|
@ -43,12 +43,20 @@ import {
|
|||
cancelWorkOrder,
|
||||
transferWorkOrder,
|
||||
} from '@/api/modules/work-order'
|
||||
import { getAllProjects } from '@/api/modules/project'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import { getStatusTagColor } from '@/utils/status-mapping'
|
||||
import { useProjectStore } from '@/stores/project'
|
||||
|
||||
// 解构 TextArea 组件
|
||||
const { TextArea } = Input
|
||||
|
||||
const projectStore = useProjectStore()
|
||||
|
||||
// ---- 项目选择 ----
|
||||
const projectList = ref<ProjectRecord[]>([])
|
||||
const selectedProjectId = ref<number>(projectStore.projectId || 0)
|
||||
|
||||
// ---- 状态映射 ----
|
||||
const statusMap: Record<WorkOrderStatus, { label: string; color: string }> = {
|
||||
pending_dispatch: { label: '待派单', color: getStatusTagColor('pending_dispatch') },
|
||||
|
|
@ -85,7 +93,7 @@ const sourceMap: Record<WorkOrderSource, string> = {
|
|||
|
||||
const statusOptions = Object.entries(statusMap).map(([value, item]) => ({ label: item.label, value: value as WorkOrderStatus }))
|
||||
const priorityOptions = Object.entries(priorityMap).map(([value, item]) => ({ label: item.label, value: value as WorkOrderPriority }))
|
||||
const categoryOptions = Object.entries(categoryMap).map(([value, label]) => ({ label, value: value as WorkOrderCategory }))
|
||||
const typeOptions = Object.entries(categoryMap).map(([value, label]) => ({ label, value: value as WorkOrderCategory }))
|
||||
const sourceOptions = Object.entries(sourceMap).map(([value, label]) => ({ label, value: value as WorkOrderSource }))
|
||||
|
||||
// ---- 表格状态 ----
|
||||
|
|
@ -110,11 +118,10 @@ const createModalOpen = ref(false)
|
|||
const createModalLoading = ref(false)
|
||||
const createForm = reactive({
|
||||
title: '',
|
||||
category: 'repair' as WorkOrderCategory,
|
||||
type: 'repair' as WorkOrderCategory,
|
||||
priority: 'medium' as WorkOrderPriority,
|
||||
source: 'app' as WorkOrderSource,
|
||||
content: '',
|
||||
assigneeId: undefined as number | undefined,
|
||||
description: '',
|
||||
})
|
||||
|
||||
// ---- 工单详情弹窗 ----
|
||||
|
|
@ -126,7 +133,7 @@ const detailLoading = ref(false)
|
|||
const dispatchModalOpen = ref(false)
|
||||
const dispatchModalLoading = ref(false)
|
||||
const dispatchForm = reactive({
|
||||
handlerId: undefined as number | undefined,
|
||||
assigneeId: undefined as number | undefined,
|
||||
remark: '',
|
||||
})
|
||||
let dispatchingId = 0
|
||||
|
|
@ -161,8 +168,8 @@ let cancellingId = 0
|
|||
const transferModalOpen = ref(false)
|
||||
const transferModalLoading = ref(false)
|
||||
const transferForm = reactive({
|
||||
newHandlerId: undefined as number | undefined,
|
||||
reason: '',
|
||||
newAssigneeId: undefined as number | undefined,
|
||||
remark: '',
|
||||
})
|
||||
let transferringId = 0
|
||||
|
||||
|
|
@ -170,26 +177,41 @@ let transferringId = 0
|
|||
const columns = [
|
||||
{ title: '工单号', dataIndex: 'orderNo', width: 140 },
|
||||
{ title: '标题', dataIndex: 'title', width: 160, ellipsis: true },
|
||||
{ title: '分类', dataIndex: 'category', width: 100 },
|
||||
{ title: '分类', dataIndex: 'type', width: 100 },
|
||||
{ title: '优先级', dataIndex: 'priority', width: 80 },
|
||||
{ title: '状态', dataIndex: 'status', width: 100 },
|
||||
{ title: '处理人', dataIndex: 'handlerName', width: 100 },
|
||||
{ title: '创建时间', dataIndex: 'createdAt', width: 180, customRender: ({ text }: { text: string }) => formatDateTime(text) },
|
||||
{ title: '处理人', dataIndex: 'assigneeName', width: 100 },
|
||||
{ title: '创建时间', dataIndex: 'createTime', width: 180, customRender: ({ text }: { text: string }) => formatDateTime(text) },
|
||||
{ title: '操作', dataIndex: 'action', width: 200, fixed: 'right' as const },
|
||||
]
|
||||
|
||||
// ---- 表单验证规则 ----
|
||||
const createFormRules = computed<Record<string, Rule[]>>(() => ({
|
||||
title: [{ required: true, message: '请输入工单标题', trigger: 'blur' }],
|
||||
category: [{ required: true, message: '请选择分类', trigger: 'change' }],
|
||||
type: [{ required: true, message: '请选择分类', trigger: 'change' }],
|
||||
priority: [{ required: true, message: '请选择优先级', trigger: 'change' }],
|
||||
}))
|
||||
|
||||
// ---- 加载项目列表 ----
|
||||
async function loadProjects() {
|
||||
try {
|
||||
const result = await getAllProjects()
|
||||
projectList.value = result || []
|
||||
// 如果未选择项目,默认选第一个
|
||||
if (!selectedProjectId.value && projectList.value.length > 0) {
|
||||
selectedProjectId.value = projectList.value[0].id
|
||||
}
|
||||
} catch {
|
||||
// 错误已在拦截器中提示
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 数据加载 ----
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await getWorkOrderList({
|
||||
projectId: selectedProjectId.value || undefined,
|
||||
page: currentPage.value,
|
||||
size: pageSize.value,
|
||||
orderNo: searchForm.orderNo || undefined,
|
||||
|
|
@ -209,6 +231,12 @@ async function loadData() {
|
|||
}
|
||||
}
|
||||
|
||||
// ---- 项目切换时重新加载 ----
|
||||
watch(selectedProjectId, () => {
|
||||
currentPage.value = 1
|
||||
loadData()
|
||||
})
|
||||
|
||||
// ---- 搜索 / 重置 ----
|
||||
function handleSearch() {
|
||||
currentPage.value = 1
|
||||
|
|
@ -236,11 +264,10 @@ function handlePageChange(pagination: { current: number; pageSize: number }) {
|
|||
// ---- 创建工单 ----
|
||||
function handleOpenCreate() {
|
||||
createForm.title = ''
|
||||
createForm.category = 'repair'
|
||||
createForm.type = 'repair'
|
||||
createForm.priority = 'medium'
|
||||
createForm.source = 'app'
|
||||
createForm.content = ''
|
||||
createForm.assigneeId = undefined
|
||||
createForm.description = ''
|
||||
createModalOpen.value = true
|
||||
}
|
||||
|
||||
|
|
@ -248,12 +275,12 @@ async function handleCreateOk() {
|
|||
createModalLoading.value = true
|
||||
try {
|
||||
await createWorkOrder({
|
||||
projectId: (selectedProjectId.value || projectStore.projectId) as number,
|
||||
title: createForm.title,
|
||||
category: createForm.category,
|
||||
priority: createForm.priority,
|
||||
type: createForm.type,
|
||||
source: createForm.source,
|
||||
content: createForm.content || undefined,
|
||||
assigneeId: createForm.assigneeId,
|
||||
priority: createForm.priority,
|
||||
description: createForm.description || undefined,
|
||||
})
|
||||
message.success('创建成功')
|
||||
createModalOpen.value = false
|
||||
|
|
@ -281,20 +308,20 @@ async function handleViewDetail(id: number) {
|
|||
// ---- 派单 ----
|
||||
function handleOpenDispatch(record: Record<string, unknown>) {
|
||||
dispatchingId = record.id as number
|
||||
dispatchForm.handlerId = undefined
|
||||
dispatchForm.assigneeId = undefined
|
||||
dispatchForm.remark = ''
|
||||
dispatchModalOpen.value = true
|
||||
}
|
||||
|
||||
async function handleDispatchOk() {
|
||||
if (!dispatchForm.handlerId) {
|
||||
if (!dispatchForm.assigneeId) {
|
||||
message.warning('请输入处理人ID')
|
||||
return
|
||||
}
|
||||
dispatchModalLoading.value = true
|
||||
try {
|
||||
await dispatchWorkOrder(dispatchingId, {
|
||||
handlerId: dispatchForm.handlerId,
|
||||
assigneeId: dispatchForm.assigneeId,
|
||||
remark: dispatchForm.remark || undefined,
|
||||
})
|
||||
message.success('派单成功')
|
||||
|
|
@ -330,8 +357,7 @@ async function handleCompleteOk() {
|
|||
completeModalLoading.value = true
|
||||
try {
|
||||
await completeWorkOrder(completingId, {
|
||||
result: completeForm.result,
|
||||
remark: completeForm.remark || undefined,
|
||||
remark: completeForm.result,
|
||||
})
|
||||
message.success('处理完成')
|
||||
completeModalOpen.value = false
|
||||
|
|
@ -355,8 +381,8 @@ async function handleVisitOk() {
|
|||
visitModalLoading.value = true
|
||||
try {
|
||||
await visitWorkOrder(visitingId, {
|
||||
satisfaction: visitForm.satisfaction,
|
||||
remark: visitForm.remark || undefined,
|
||||
verifyScore: visitForm.satisfaction,
|
||||
verifyRemark: visitForm.remark || undefined,
|
||||
})
|
||||
message.success('回访成功')
|
||||
visitModalOpen.value = false
|
||||
|
|
@ -378,7 +404,7 @@ function handleOpenCancel(record: Record<string, unknown>) {
|
|||
async function handleCancelOk() {
|
||||
cancelModalLoading.value = true
|
||||
try {
|
||||
await cancelWorkOrder(cancellingId, { reason: cancelForm.reason })
|
||||
await cancelWorkOrder(cancellingId, { cancelReason: cancelForm.reason })
|
||||
message.success('取消成功')
|
||||
cancelModalOpen.value = false
|
||||
loadData()
|
||||
|
|
@ -392,21 +418,21 @@ async function handleCancelOk() {
|
|||
// ---- 转单 ----
|
||||
function handleOpenTransfer(record: Record<string, unknown>) {
|
||||
transferringId = record.id as number
|
||||
transferForm.newHandlerId = undefined
|
||||
transferForm.reason = ''
|
||||
transferForm.newAssigneeId = undefined
|
||||
transferForm.remark = ''
|
||||
transferModalOpen.value = true
|
||||
}
|
||||
|
||||
async function handleTransferOk() {
|
||||
if (!transferForm.newHandlerId) {
|
||||
if (!transferForm.newAssigneeId) {
|
||||
message.warning('请输入新处理人ID')
|
||||
return
|
||||
}
|
||||
transferModalLoading.value = true
|
||||
try {
|
||||
await transferWorkOrder(transferringId, {
|
||||
newHandlerId: transferForm.newHandlerId,
|
||||
reason: transferForm.reason || undefined,
|
||||
newAssigneeId: transferForm.newAssigneeId,
|
||||
remark: transferForm.remark || undefined,
|
||||
})
|
||||
message.success('转单成功')
|
||||
transferModalOpen.value = false
|
||||
|
|
@ -418,7 +444,8 @@ async function handleTransferOk() {
|
|||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
await loadProjects()
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
|
@ -426,7 +453,10 @@ onMounted(() => {
|
|||
<template>
|
||||
<PageHeader title="工单管理" subtitle="管理物业工单与维修服务">
|
||||
<template #actions>
|
||||
<Button v-permission="'operation:workorder:add'" type="primary" @click="handleOpenCreate">
|
||||
<Select v-model:value="selectedProjectId" placeholder="请选择项目" style="width: 200px">
|
||||
<SelectOption v-for="p in projectList" :key="p.id" :value="p.id">{{ p.projectName }}</SelectOption>
|
||||
</Select>
|
||||
<Button v-permission="'operation:workorder:add'" type="primary" :disabled="!selectedProjectId" @click="handleOpenCreate">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
创建工单
|
||||
</Button>
|
||||
|
|
@ -472,8 +502,8 @@ onMounted(() => {
|
|||
|
||||
<!-- 表格单元格渲染 -->
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'category'">
|
||||
{{ categoryMap[record.category as WorkOrderCategory] || record.category }}
|
||||
<template v-if="column.dataIndex === 'type'">
|
||||
{{ categoryMap[record.type as WorkOrderCategory] || record.type }}
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'priority'">
|
||||
<StatusTag
|
||||
|
|
@ -517,9 +547,9 @@ onMounted(() => {
|
|||
<FormItem label="标题" name="title">
|
||||
<Input v-model:value="createForm.title" placeholder="请输入工单标题" />
|
||||
</FormItem>
|
||||
<FormItem label="分类" name="category">
|
||||
<Select v-model:value="createForm.category" placeholder="请选择分类">
|
||||
<SelectOption v-for="opt in categoryOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</SelectOption>
|
||||
<FormItem label="分类" name="type">
|
||||
<Select v-model:value="createForm.type" placeholder="请选择分类">
|
||||
<SelectOption v-for="opt in typeOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem label="优先级" name="priority">
|
||||
|
|
@ -532,11 +562,8 @@ onMounted(() => {
|
|||
<SelectOption v-for="opt in sourceOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem label="内容" name="content">
|
||||
<TextArea v-model:value="createForm.content" :rows="4" placeholder="请输入工单内容" />
|
||||
</FormItem>
|
||||
<FormItem label="指派人ID" name="assigneeId">
|
||||
<InputNumber v-model:value="createForm.assigneeId" :min="1" style="width: 100%" placeholder="请输入指派人ID" />
|
||||
<FormItem label="描述" name="description">
|
||||
<TextArea v-model:value="createForm.description" :rows="4" placeholder="请输入工单描述" />
|
||||
</FormItem>
|
||||
</FormModal>
|
||||
|
||||
|
|
@ -553,7 +580,7 @@ onMounted(() => {
|
|||
<Descriptions title="基本信息" :column="2" bordered size="small" style="margin-bottom: 24px">
|
||||
<DescriptionsItem label="工单号">{{ detailData.orderNo }}</DescriptionsItem>
|
||||
<DescriptionsItem label="标题">{{ detailData.title }}</DescriptionsItem>
|
||||
<DescriptionsItem label="分类">{{ categoryMap[detailData.category] }}</DescriptionsItem>
|
||||
<DescriptionsItem label="分类">{{ categoryMap[detailData.type] }}</DescriptionsItem>
|
||||
<DescriptionsItem label="优先级">
|
||||
<StatusTag :color="priorityMap[detailData.priority]?.color" :text="priorityMap[detailData.priority]?.label" />
|
||||
</DescriptionsItem>
|
||||
|
|
@ -561,11 +588,11 @@ onMounted(() => {
|
|||
<StatusTag :color="statusMap[detailData.status]?.color" :text="statusMap[detailData.status]?.label" />
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="来源">{{ sourceMap[detailData.source] }}</DescriptionsItem>
|
||||
<DescriptionsItem label="处理人">{{ detailData.handlerName || '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="处理人">{{ detailData.assigneeName || '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="创建人">{{ detailData.creatorName || '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="创建时间">{{ formatDateTime(detailData.createdAt) }}</DescriptionsItem>
|
||||
<DescriptionsItem label="完成时间">{{ detailData.completedAt ? formatDateTime(detailData.completedAt) : '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="内容" :span="2">{{ detailData.content || '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="创建时间">{{ formatDateTime(detailData.createTime) }}</DescriptionsItem>
|
||||
<DescriptionsItem label="完成时间">{{ detailData.completeTime ? formatDateTime(detailData.completeTime) : '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="描述" :span="2">{{ detailData.description || '-' }}</DescriptionsItem>
|
||||
</Descriptions>
|
||||
|
||||
<!-- 附件列表 -->
|
||||
|
|
@ -601,8 +628,8 @@ onMounted(() => {
|
|||
:width="480"
|
||||
@ok="handleDispatchOk"
|
||||
>
|
||||
<FormItem label="处理人ID" name="handlerId">
|
||||
<InputNumber v-model:value="dispatchForm.handlerId" :min="1" style="width: 100%" placeholder="请输入处理人ID" />
|
||||
<FormItem label="处理人ID" name="assigneeId">
|
||||
<InputNumber v-model:value="dispatchForm.assigneeId" :min="1" style="width: 100%" placeholder="请输入处理人ID" />
|
||||
</FormItem>
|
||||
<FormItem label="备注" name="remark">
|
||||
<TextArea v-model:value="dispatchForm.remark" :rows="3" placeholder="请输入备注" />
|
||||
|
|
@ -666,11 +693,11 @@ onMounted(() => {
|
|||
:width="480"
|
||||
@ok="handleTransferOk"
|
||||
>
|
||||
<FormItem label="新处理人ID" name="newHandlerId">
|
||||
<InputNumber v-model:value="transferForm.newHandlerId" :min="1" style="width: 100%" placeholder="请输入新处理人ID" />
|
||||
<FormItem label="新处理人ID" name="newAssigneeId">
|
||||
<InputNumber v-model:value="transferForm.newAssigneeId" :min="1" style="width: 100%" placeholder="请输入新处理人ID" />
|
||||
</FormItem>
|
||||
<FormItem label="转单原因" name="reason">
|
||||
<TextArea v-model:value="transferForm.reason" :rows="3" placeholder="请输入转单原因" />
|
||||
<FormItem label="转单原因" name="remark">
|
||||
<TextArea v-model:value="transferForm.remark" :rows="3" placeholder="请输入转单原因" />
|
||||
</FormItem>
|
||||
</FormModal>
|
||||
</template>
|
||||
|
|
|
|||
Loading…
Reference in New Issue