diff --git a/src/api/audit.ts b/src/api/audit.ts new file mode 100644 index 00000000..3f0c2055 --- /dev/null +++ b/src/api/audit.ts @@ -0,0 +1,57 @@ +import request from '@/utils/request' + +export interface AuditLog { + id: string + username: string + operation: string + module: string + action: string + targetType?: string + targetId?: string + content?: string + ipAddress: string + requestMethod: string + requestUrl: string + status: 'SUCCESS' | 'FAIL' + executionTimeMs?: number + createdAt: string +} + +export interface AuditLogQuery { + page?: number + size?: number + module?: string + action?: string + username?: string + startDate?: string + endDate?: string +} + +export function getAuditLogs(params: AuditLogQuery) { + return request({ + url: '/api/audit-logs', + method: 'get', + params + }) +} + +export function getAuditModules() { + return request({ + url: '/api/audit-logs/modules', + method: 'get' + }) +} + +export function getAuditActions() { + return request({ + url: '/api/audit-logs/actions', + method: 'get' + }) +} + +export function getAuditStats() { + return request({ + url: '/api/audit-logs/stats', + method: 'get' + }) +} diff --git a/src/api/equipment.ts b/src/api/equipment.ts new file mode 100644 index 00000000..edf66f9e --- /dev/null +++ b/src/api/equipment.ts @@ -0,0 +1,77 @@ +import request from '@/utils/request' + +// ==================== 设备相关类型 ==================== + +export interface EquipmentForm { + id?: string + code: string + name: string + isEquipment?: boolean + designLifeYears?: number + ratedPower?: number + ratedVoltage?: string + ratedCurrent?: number + maintenanceVendor?: string + maintenanceVendorPhone?: string + specialEquipmentType?: string + inspectionCycle?: number + nextInspectionDate?: string +} + +export interface Equipment { + id: string + code: string + name: string + isEquipment: boolean + designLifeYears?: number + ratedPower?: number + ratedVoltage?: string + ratedCurrent?: number + maintenanceVendor?: string + maintenanceVendorPhone?: string + specialEquipmentType?: string + inspectionCycle?: number + nextInspectionDate?: string + spaceNodeId?: string + spaceNodeName?: string + projectId?: string + projectName?: string + createdAt?: string + updatedAt?: string +} + +export interface PageResponse { + content: T[] + totalElements: number + totalPages: number + size: number + number: number +} + +// ==================== 设备 API ==================== + +// 获取设备列表 +export function getEquipmentList(projectId: string) { + return request.get>('/api/v1/mdm/space-nodes/equipment', { + params: { projectId } + }) +} + +// 获取设备详情 +export function getEquipmentDetail(id: string) { + return request.get(`/api/v1/mdm/space-nodes/${id}/equipment`) +} + +// 获取特种设备列表 +export function getSpecialEquipment(projectId: string) { + return request.get('/api/v1/mdm/space-nodes/special-equipment', { + params: { projectId } + }) +} + +// 获取即将年检设备 +export function getExpiringInspection(projectId: string, daysAhead?: number) { + return request.get('/api/v1/mdm/space-nodes/expiring-inspection', { + params: { projectId, daysAhead } + }) +} diff --git a/src/api/project.ts b/src/api/project.ts index 13fadf74..c925f508 100644 --- a/src/api/project.ts +++ b/src/api/project.ts @@ -1,26 +1,121 @@ import request from '@/utils/request' import type { Project } from '@/types' +import type { + ProjectQuery, + PageResponse, + ProjectStatistics, + ProjectMember, + ProjectConfig, + ProjectSelectorItem, + StatusChangeRequest, + AddMemberRequest +} from '@/types/project' +// ==================== 基础 CRUD ==================== + +// PM-001 分页查询项目列表 +export const queryProjects = (params: ProjectQuery) => { + return request.get>('/api/mdm/projects', { params }) +} + +// 获取所有项目(兼容旧接口) export const getProjects = () => { - return request.get('/projects') + return request.get('/api/mdm/projects/all') } +// 获取项目详情 export const getProject = (id: string) => { - return request.get(`/projects/${id}`) + return request.get(`/api/mdm/projects/${id}`) } +// 根据编码获取项目 export const getProjectByCode = (code: string) => { - return request.get(`/projects/code/${code}`) + return request.get(`/api/mdm/projects/code/${code}`) } +// 创建项目 export const createProject = (data: Partial) => { - return request.post('/projects', data) + return request.post('/api/mdm/projects', data) } +// 更新项目 export const updateProject = (id: string, data: Partial) => { - return request.put(`/projects/${id}`, data) + return request.put(`/api/mdm/projects/${id}`, data) } +// 删除项目 export const deleteProject = (id: string) => { - return request.delete(`/projects/${id}`) + return request.delete(`/api/mdm/projects/${id}`) +} + +// ==================== 统计数据 ==================== + +// PM-002 获取项目统计数据 +export const getProjectStatistics = (id: string) => { + return request.get(`/api/mdm/projects/${id}/statistics`) +} + +// ==================== 成员管理 ==================== + +// PM-003 获取项目成员列表 +export const getProjectMembers = (projectId: string, params?: { page?: number; size?: number }) => { + return request.get>(`/api/mdm/projects/${projectId}/members`, { params }) +} + +// 添加项目成员 +export const addProjectMembers = (projectId: string, data: AddMemberRequest) => { + return request.post(`/api/mdm/projects/${projectId}/members`, data) +} + +// 移除项目成员 +export const removeProjectMember = (projectId: string, memberId: string) => { + return request.delete(`/api/mdm/projects/${projectId}/members/${memberId}`) +} + +// 更新成员角色 +export const updateMemberRole = (projectId: string, memberId: string, roleInProject: string) => { + return request.put(`/api/mdm/projects/${projectId}/members/${memberId}/role`, { roleInProject }) +} + +// ==================== 编码生成 ==================== + +// PM-005 生成项目编码 +export const generateProjectCode = () => { + return request.get<{ code: string }>('/api/mdm/projects/generate-code') +} + +// ==================== 状态管理 ==================== + +// PM-006 变更项目状态 +export const changeProjectStatus = (id: string, data: StatusChangeRequest) => { + return request.put(`/api/mdm/projects/${id}/status`, data) +} + +// 启用项目 +export const enableProject = (id: string) => { + return changeProjectStatus(id, { status: 'ACTIVE' }) +} + +// 禁用项目 +export const disableProject = (id: string, reason?: string) => { + return changeProjectStatus(id, { status: 'DISABLED', reason }) +} + +// ==================== 配置管理 ==================== + +// PM-008 获取项目配置 +export const getProjectConfig = (id: string) => { + return request.get(`/api/mdm/projects/${id}/config`) +} + +// 更新项目配置 +export const updateProjectConfig = (id: string, data: Partial) => { + return request.put(`/api/mdm/projects/${id}/config`, data) +} + +// ==================== 选择器 ==================== + +// PM-010 获取项目选择器列表 +export const getProjectSelectorList = (params?: { keyword?: string }) => { + return request.get('/api/mdm/projects/selector', { params }) } diff --git a/src/api/role.ts b/src/api/role.ts index b8a77bea..db039b99 100644 --- a/src/api/role.ts +++ b/src/api/role.ts @@ -1,5 +1,5 @@ import request from '@/utils/request' -import type { Role } from '@/types' +import type { Role, Permission } from '@/types' export const getRoles = () => { return request.get('/api/roles') @@ -9,6 +9,10 @@ export const getRole = (id: string) => { return request.get(`/api/roles/${id}`) } +export const getRolePermissions = (id: string) => { + return request.get(`/api/roles/${id}/permissions`) +} + export const getRolesByProject = (projectId: string) => { return request.get(`/api/roles/project/${projectId}`) } @@ -36,3 +40,7 @@ export const getUserRoles = (userId: string) => { export const removeRoleFromUser = (userId: string, roleId: string) => { return request.delete(`/api/users/${userId}/roles/${roleId}`) } + +export const getRoleUsers = (roleId: string) => { + return request.get(`/api/roles/${roleId}/users`) +} diff --git a/src/api/space.ts b/src/api/space.ts new file mode 100644 index 00000000..ae146660 --- /dev/null +++ b/src/api/space.ts @@ -0,0 +1,38 @@ +import request from '@/utils/request' +import type { SpaceNode, SpaceNodeTree, SpaceNodeCreateForm, SpaceNodeUpdateForm } from '@/types/space' + +export const getSpaceNodes = (projectId: string) => { + return request.get(`/api/v1/mdm/space-nodes/project/${projectId}`) +} + +export const getSpaceTree = (projectId: string) => { + return request.get(`/api/v1/mdm/space-nodes/project/${projectId}/tree`) +} + +export const getSpaceRoots = (projectId: string) => { + return request.get(`/api/v1/mdm/space-nodes/project/${projectId}/roots`) +} + +export const getSpaceChildren = (parentId: string) => { + return request.get(`/api/v1/mdm/space-nodes/parent/${parentId}/children`) +} + +export const getSpaceNode = (id: string) => { + return request.get(`/api/v1/mdm/space-nodes/${id}`) +} + +export const getSpaceNodesByType = (projectId: string, nodeType: string) => { + return request.get(`/api/v1/mdm/space-nodes/project/${projectId}/type/${nodeType}`) +} + +export const createSpaceNode = (data: SpaceNodeCreateForm) => { + return request.post('/api/v1/mdm/space-nodes', data) +} + +export const updateSpaceNode = (id: string, data: SpaceNodeUpdateForm) => { + return request.put(`/api/v1/mdm/space-nodes/${id}`, data) +} + +export const deleteSpaceNode = (id: string) => { + return request.delete(`/api/v1/mdm/space-nodes/${id}`) +} diff --git a/src/api/system.ts b/src/api/system.ts new file mode 100644 index 00000000..88f99741 --- /dev/null +++ b/src/api/system.ts @@ -0,0 +1,16 @@ +import request from '@/utils/request' + +export function getConfig() { + return request({ + url: '/api/config', + method: 'get' + }) +} + +export function updateConfig(data: Record) { + return request({ + url: '/api/config', + method: 'put', + data + }) +} diff --git a/src/components/StatusTag/index.vue b/src/components/StatusTag/index.vue index 1e7908fc..ca7baf91 100644 --- a/src/components/StatusTag/index.vue +++ b/src/components/StatusTag/index.vue @@ -1,18 +1,20 @@ diff --git a/src/components/TableActions/index.vue b/src/components/TableActions/index.vue index 27d2e108..4690b03f 100644 --- a/src/components/TableActions/index.vue +++ b/src/components/TableActions/index.vue @@ -1,104 +1,201 @@ ``` -#### TableActions 行操作 +#### TableActions 行操作(统一组件) + +**使用方式:** ```vue - + + + + + + + + ``` +**属性说明:** + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| showView | boolean | false | 是否显示查看按钮 | +| showEdit | boolean | true | 是否显示编辑按钮 | +| showDelete | boolean | true | 是否显示删除按钮 | +| viewText | string | '查看' | 查看按钮文本 | +| editText | string | '编辑' | 编辑按钮文本 | +| deleteText | string | '删除' | 删除按钮文本 | +| deleteTitle | string | '确认删除' | 删除确认标题 | +| deleteDescription | string | '删除后不可恢复,是否继续?' | 删除确认描述 | +| actions | ActionItem[] | [] | 自定义操作按钮列表 | + +**ActionItem 类型:** +```typescript +interface ActionItem { + key: string // 按钮标识 + label: string // 按钮文本 + danger?: boolean // 是否危险操作(红色) +} +``` + +**事件说明:** + +| 事件 | 参数 | 说明 | +|------|------|------| +| view | - | 点击查看按钮 | +| edit | - | 点击编辑按钮 | +| delete | - | 点击删除按钮(确认后触发) | +| action | key: string | 点击自定义操作按钮 | + +**样式规范:** +- 使用 `Space` 组件包裹,间距为 0 +- 按钮使用 `type="link"` + `size="small"` +- 按钮内边距 `padding: 0 4px` +- 删除按钮使用 `danger` 属性 + --- ### 4. Detail 详情组件 @@ -804,10 +835,12 @@ const handleChange = (value: string) => { #### TableActions 表格行操作 | 状态 | 功能 | 说明 | |------|------|------| +| ✅ 已支持 | 查看按钮 | showView | | ✅ 已支持 | 编辑按钮 | showEdit | | ✅ 已支持 | 删除按钮 | showDelete | | ✅ 已支持 | 自定义操作 | actions prop | | ✅ 已支持 | 删除确认 | Popconfirm | +| ✅ 已支持 | 按钮文本配置 | viewText/editText/deleteText | | 🔲 待开发 | 更多操作 | more-actions dropdown | | 🔲 待开发 | 成功反馈 | success-message | | 🔲 待开发 | 二次确认配置 | confirm-title/description | diff --git a/src/types/index.ts b/src/types/index.ts index 43154a3c..86dd26b2 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -55,23 +55,17 @@ export interface Project { name: string description?: string address?: string + projectType?: 'RESIDENTIAL' | 'OFFICE' | 'INDUSTRIAL_PARK' province?: string city?: string district?: string - status: 'ACTIVE' | 'DISABLED' + status: 'ACTIVE' | 'DISABLED' | 'PENDING' | 'ARCHIVED' + createdAt?: string + updatedAt?: string } -export interface SpaceNode { - id: string - code: string - name: string - projectCode: string - nodeType: string - parentCode?: string - building?: string - unit?: string - floor?: string - roomNumber?: string - area?: number - status: 'ACTIVE' | 'DISABLED' -} +// 导出项目相关类型 +export * from './project' + +// 导出空间相关类型 +export * from './space' diff --git a/src/types/project.ts b/src/types/project.ts new file mode 100644 index 00000000..d6d0f64e --- /dev/null +++ b/src/types/project.ts @@ -0,0 +1,125 @@ +// 项目状态枚举 +export type ProjectStatus = 'ACTIVE' | 'DISABLED' | 'PENDING' | 'ARCHIVED' + +// 项目状态映射 +export const ProjectStatusMap: Record = { + ACTIVE: { label: '正常', color: 'success' }, + DISABLED: { label: '禁用', color: 'error' }, + PENDING: { label: '待审核', color: 'warning' }, + ARCHIVED: { label: '已归档', color: 'default' } +} + +// 项目类型枚举 +export type ProjectType = 'RESIDENTIAL' | 'OFFICE' | 'INDUSTRIAL_PARK' + +// 项目类型映射 +export const ProjectTypeMap: Record = { + RESIDENTIAL: { label: '住宅', color: 'green' }, + OFFICE: { label: '办公', color: 'blue' }, + INDUSTRIAL_PARK: { label: '产业园区', color: 'purple' } +} + +// 项目查询参数 +export interface ProjectQuery { + keyword?: string + status?: ProjectStatus + page?: number + size?: number + sort?: string +} + +// 分页响应 +export interface PageResponse { + content: T[] + totalElements: number + totalPages: number + size: number + number: number + first: boolean + last: boolean + empty: boolean +} + +// 项目统计信息 +export interface ProjectStatistics { + memberCount: number + buildingCount: number + roomCount: number + ownerCount: number + tenantCount: number + activeTaskCount: number + completedTaskCount: number +} + +// 项目成员 +export interface ProjectMember { + id: string + projectId: string + userId: string + userName: string + realName?: string + phone?: string + roleInProject: string + joinedAt: string + status: 'ACTIVE' | 'INACTIVE' +} + +// 项目成员角色 +export const ProjectMemberRoleMap: Record = { + PROJECT_MANAGER: { label: '项目经理', color: 'blue' }, + PROJECT_ADMIN: { label: '项目管理员', color: 'green' }, + OPERATION_STAFF: { label: '运营人员', color: 'orange' }, + FINANCE_STAFF: { label: '财务人员', color: 'purple' }, + VIEWER: { label: '查看者', color: 'default' } +} + +// 项目配置 +export interface ProjectConfig { + id: string + projectId: string + enableReservation: boolean + enableVisitor: boolean + enableComplaint: boolean + enablePayment: boolean + enableAnnouncement: boolean + enableSurvey: boolean + enableVote: boolean + enableMaintenance: boolean + enableAsset: boolean + customConfig?: string + updatedAt: string +} + +// 项目选择器项 +export interface ProjectSelectorItem { + id: string + code: string + name: string + status: ProjectStatus + address?: string +} + +// 项目表单数据 +export interface ProjectFormData { + id?: string + name?: string + description?: string + address?: string + projectType?: ProjectType + province?: string + city?: string + district?: string + status?: ProjectStatus +} + +// 状态变更请求 +export interface StatusChangeRequest { + status: ProjectStatus + reason?: string +} + +// 添加成员请求 +export interface AddMemberRequest { + userIds: string[] + roleInProject: string +} diff --git a/src/types/space.ts b/src/types/space.ts new file mode 100644 index 00000000..757867d4 --- /dev/null +++ b/src/types/space.ts @@ -0,0 +1,141 @@ +export type SpaceNodeCategory = 'BUILDING' | 'PARKING' | 'FACILITY' | 'AREA' + +export type SpaceNodeType = + | 'BUILDING' + | 'UNIT' + | 'FLOOR' + | 'ROOM' + | 'SHOP' + | 'GARAGE' + | 'PARKING_AREA' + | 'PARKING_SPACE' + | 'EQUIPMENT_ROOM' + | 'PROPERTY_OFFICE' + | 'SECURITY_ROOM' + | 'PUBLIC_AREA' + | 'GREEN_AREA' + | 'ROAD' + +export const SpaceNodeCategoryMap: Record = { + BUILDING: { label: '建筑空间' }, + PARKING: { label: '停车空间' }, + FACILITY: { label: '设施空间' }, + AREA: { label: '区域空间' } +} + +export const SpaceNodeTypeMap: Record = { + BUILDING: { label: '楼栋', category: 'BUILDING' }, + UNIT: { label: '单元', category: 'BUILDING' }, + FLOOR: { label: '楼层', category: 'BUILDING' }, + ROOM: { label: '房间', category: 'BUILDING' }, + SHOP: { label: '商铺', category: 'BUILDING' }, + GARAGE: { label: '车库', category: 'PARKING' }, + PARKING_AREA: { label: '停车区域', category: 'PARKING' }, + PARKING_SPACE: { label: '车位', category: 'PARKING' }, + EQUIPMENT_ROOM: { label: '设备房', category: 'FACILITY' }, + PROPERTY_OFFICE: { label: '物业用房', category: 'FACILITY' }, + SECURITY_ROOM: { label: '门岗', category: 'FACILITY' }, + PUBLIC_AREA: { label: '公共区域', category: 'AREA' }, + GREEN_AREA: { label: '绿化区域', category: 'AREA' }, + ROAD: { label: '道路', category: 'AREA' } +} + +export interface SpaceNode { + id: string + projectId: string + code: string + name: string + fullName?: string + shortName?: string + nodeCategory: SpaceNodeCategory + nodeType: SpaceNodeType + usageType?: string + parentId?: string + parentCode?: string + treePath?: string + treePathName?: string + level?: number + sortOrder?: number + status?: string + deliveryStatus?: string + decorationStatus?: string + buildingArea?: number + usableArea?: number + sharedArea?: number + landArea?: number + longitude?: number + latitude?: number + altitude?: number + floorNumber?: number + province?: string + city?: string + district?: string + street?: string + address?: string + attributes?: string + createdAt?: string + updatedAt?: string + createdBy?: string + updatedBy?: string + isDeleted?: boolean +} + +export interface SpaceNodeTree extends SpaceNode { + children: SpaceNodeTree[] +} + +export interface SpaceNodeCreateForm { + projectId: string + name: string + fullName?: string + shortName?: string + nodeCategory: SpaceNodeCategory + nodeType: SpaceNodeType + usageType?: string + parentId?: string + sortOrder?: number + status?: string + deliveryStatus?: string + decorationStatus?: string + buildingArea?: number + usableArea?: number + sharedArea?: number + landArea?: number + longitude?: number + latitude?: number + altitude?: number + floorNumber?: number + province?: string + city?: string + district?: string + street?: string + address?: string + attributes?: string +} + +export interface SpaceNodeUpdateForm { + name?: string + fullName?: string + shortName?: string + nodeCategory?: SpaceNodeCategory + nodeType?: SpaceNodeType + usageType?: string + sortOrder?: number + status?: string + deliveryStatus?: string + decorationStatus?: string + buildingArea?: number + usableArea?: number + sharedArea?: number + landArea?: number + longitude?: number + latitude?: number + altitude?: number + floorNumber?: number + province?: string + city?: string + district?: string + street?: string + address?: string + attributes?: string +} diff --git a/src/views/Dashboard.vue b/src/views/Dashboard.vue index a56a3b1a..37a1cf76 100644 --- a/src/views/Dashboard.vue +++ b/src/views/Dashboard.vue @@ -15,17 +15,55 @@ import { UserOutlined } from '@ant-design/icons-vue' import { Col, Row } from 'ant-design-vue' +import { onMounted, ref } from 'vue' import { useRouter } from 'vue-router' const router = useRouter() const stats = [ - { label: '用户总数', value: '1,286', change: '+12.5%', up: true, icon: UserOutlined }, - { label: '角色总数', value: '8', change: '-', up: true, icon: TeamOutlined }, - { label: '项目总数', value: '24', change: '+8.3%', up: true, icon: ProjectOutlined }, - { label: '空间节点', value: '156', change: '-2.1%', up: false, icon: ApartmentOutlined } + { label: '用户总数', value: 1286, change: '+12.5%', up: true, icon: UserOutlined }, + { label: '角色总数', value: 8, change: '-', up: true, icon: TeamOutlined }, + { label: '项目总数', value: 24, change: '+8.3%', up: true, icon: ProjectOutlined }, + { label: '空间节点', value: 156, change: '-2.1%', up: false, icon: ApartmentOutlined } ] +const displayValues = ref(stats.map(() => 0)) +const animationComplete = ref(stats.map(() => false)) + +const easeOutQuart = (t: number): number => { + return 1 - Math.pow(1 - t, 4) +} + +const animateValue = (index: number, endValue: number, duration: number = 1500) => { + const startTime = performance.now() + const startValue = 0 + + const tick = (currentTime: number) => { + const elapsed = currentTime - startTime + const progress = Math.min(elapsed / duration, 1) + const easedProgress = easeOutQuart(progress) + const currentValue = Math.round(startValue + (endValue - startValue) * easedProgress) + + displayValues.value[index] = currentValue + + if (progress < 1) { + requestAnimationFrame(tick) + } else { + animationComplete.value[index] = true + } + } + + requestAnimationFrame(tick) +} + +onMounted(() => { + stats.forEach((stat, index) => { + setTimeout(() => { + animateValue(index, stat.value, 1500) + }, index * 150) + }) +}) + const todos = [ { title: '待处理工单', count: 12 }, { title: '待审核报修', count: 5 }, @@ -48,6 +86,38 @@ const notices = [ ] const chartData = [65, 78, 52, 91, 68, 85, 73] +const displayHeights = ref(chartData.map(() => 0)) +const chartAnimationComplete = ref(false) + +const animateChart = () => { + const duration = 1200 + const startTime = performance.now() + + const tick = (currentTime: number) => { + const elapsed = currentTime - startTime + const progress = Math.min(elapsed / duration, 1) + const easedProgress = easeOutQuart(progress) + + displayHeights.value = chartData.map(v => v * easedProgress) + + if (progress < 1) { + requestAnimationFrame(tick) + } else { + chartAnimationComplete.value = true + } + } + + requestAnimationFrame(tick) +} + +onMounted(() => { + stats.forEach((stat, index) => { + setTimeout(() => { + animateValue(index, stat.value, 1500) + }, index * 150) + }) + setTimeout(animateChart, 600) +})