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:
ether 2026-07-01 03:06:55 +08:00
parent d231aec472
commit f0507e9c0d
20 changed files with 799 additions and 342 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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 '用户'
})

View File

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

View File

@ -3,7 +3,7 @@
* tokenrefreshToken
*/
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,

View File

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

View File

@ -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
}
/** 楼栋树节点(楼栋→楼层→房间) */

View File

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

View File

@ -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
}
// ============================================

View File

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

View File

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

View File

@ -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[]>([])

View File

@ -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[]>([])

View File

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

View File

@ -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="请选择状态">

View File

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

View File

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

View File

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