增加菜单

This commit is contained in:
游离 2025-06-30 09:14:46 +08:00
parent edb8aad10f
commit 41b9474300
47 changed files with 11077 additions and 260 deletions

View File

@ -36,6 +36,11 @@ export function listSiteOptionDict() {
return http.get<LabelValueState[]>(`${BASE_URL}/dict/option/site`)
}
/** @desc 查询菜单类型列表 */
export function listMenuType() {
return http.get<Record<string, string>[]>(`${BASE_URL}/list/menu-type`)
}
/** @desc 上传文件 */
export function uploadFile(data: FormData) {
return http.post(`${BASE_URL}/file`, data)

116
src/apis/project/budget.ts Normal file
View File

@ -0,0 +1,116 @@
import http from '@/utils/http'
const BASE_URL = '/project/budget'
/** 预算记录响应类型 */
export interface BudgetRecordResp {
id: string
projectId: string
projectName: string
projectCode: string
projectAmount: number
projectManager: string
establishDate: string
deliveryDate: string
client: string
remark: string
applyBudgetAmount: number
applicant: string
applyTime: string
auditStatus: string // 'pending' | 'approved' | 'rejected'
auditStatusLabel: string
auditRemark: string
auditor: string
auditTime: string
budgetProgress: number
usedBudgetAmount: number
remainingBudgetAmount: number
}
/** 预算项类型 */
export interface BudgetItemResp {
id?: string
budgetName: string
budgetType: string
budgetAmount: number
budgetDescription: string
attachments?: Array<{
id: string
name: string
url: string
}>
}
/** 预算申请请求类型 */
export interface BudgetApplyReq {
projectId: string
budgetItems: BudgetItemResp[]
totalAmount: number
applyReason?: string
}
/** 预算审核请求类型 */
export interface BudgetAuditReq {
auditStatus: 'approved' | 'rejected'
auditRemark: string
}
/** 预算查询参数 */
export interface BudgetQuery {
projectName?: string
projectCode?: string
auditStatus?: string
applicant?: string
applyTimeStart?: string
applyTimeEnd?: string
}
export interface BudgetPageQuery extends BudgetQuery, PageQuery {}
/** @desc 查询预算记录列表 */
export function listBudgetRecord(query: BudgetPageQuery) {
return http.get<PageRes<BudgetRecordResp[]>>(`${BASE_URL}/record`, query)
}
/** @desc 获取预算记录详情 */
export function getBudgetRecord(id: string) {
return http.get<BudgetRecordResp>(`${BASE_URL}/record/${id}`)
}
/** @desc 申请预算 */
export function applyBudget(data: BudgetApplyReq) {
return http.post<boolean>(`${BASE_URL}/apply`, data)
}
/** @desc 审核预算 */
export function auditBudget(id: string, data: BudgetAuditReq) {
return http.put<boolean>(`${BASE_URL}/audit/${id}`, data)
}
/** @desc 获取预算类型选项 */
export function getBudgetTypes() {
return http.get<Array<{ label: string; value: string }>>(`${BASE_URL}/types`)
}
/** @desc 上传预算附件 */
export function uploadBudgetAttachment(file: File) {
const formData = new FormData()
formData.append('file', file)
return http.post<{ id: string; name: string; url: string }>(`${BASE_URL}/upload`, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
/** @desc 删除预算附件 */
export function deleteBudgetAttachment(id: string) {
return http.del(`${BASE_URL}/attachment/${id}`)
}
/** @desc 导出预算记录 */
export function exportBudgetRecord(query: BudgetQuery) {
return http.get(`${BASE_URL}/export`, query, {
responseType: 'blob'
})
}

View File

@ -7,11 +7,11 @@ const BASE_URL = '/project'
/** @desc 查询项目列表 */
export function listProject(query: T.ProjectPageQuery) {
return http.get<PageRes<T.ProjectResp[]>>(`${BASE_URL}`, query)
return http.get<PageRes<T.ProjectResp[]>>(`${BASE_URL}/list`, query)
}
/** @desc 获取项目详情 */
export function getProject(id: number) {
export function getProject(id: string | number) {
return http.get<T.ProjectResp>(`${BASE_URL}/${id}`)
}
@ -21,17 +21,17 @@ export function addProject(data: any) {
}
/** @desc 修改项目 */
export function updateProject(data: any, id: number) {
export function updateProject(data: any, id: string | number) {
return http.put(`${BASE_URL}/${id}`, data)
}
/** @desc 修改项目状态 */
export function updateProjectStatus(data: any, id: number) {
export function updateProjectStatus(data: any, id: string | number) {
return http.patch(`${BASE_URL}/${id}/status`, data)
}
/** @desc 删除项目 */
export function deleteProject(id: number) {
export function deleteProject(id: string | number) {
return http.del(`${BASE_URL}/${id}`)
}

View File

@ -1,26 +1,44 @@
/** 项目类型 */
export interface ProjectResp {
id: number
projectCode: string // 项目编号
projectId: string // 项目ID (API返回的是字符串)
projectCode?: string // 项目编号
projectName: string // 项目名称
projectIntro?: string // 项目简介
fieldName: string // 风场名称
fieldLocation: string // 风场地址
commissionUnit: string // 委托单位
commissionContact: string // 委托单位联系人
commissionPhone: string // 委托单位联系电话
inspectionUnit: string // 检查单位
inspectionContact: string // 检查单位联系人
inspectionPhone: string // 检查单位联系电话
projectScale: string // 项目规模
orgNumber: string // 机组型号
projectCategory: string // 项目类型/服务
projectManager: string // 项目经理
projectStaff: string[] // 施工人员
projectPeriod: [string, string] // 项目周期
status: string // 状态
farmName: string // 风场名称 (API字段名是farmName)
farmAddress?: string // 风场地址 (API字段名是farmAddress)
client?: string // 委托单位
clientContact?: string // 委托单位联系人
clientPhone?: string // 委托单位联系电话
inspectionContact?: string // 检查单位联系人
inspectionPhone?: string // 检查单位联系电话
inspectionUnit?: string // 检查单位
projectScale?: string // 项目规模
scale?: string // 项目规模 (API可能使用scale字段)
turbineModel?: string // 机组型号 (API字段名是turbineModel)
projectCategory?: string // 项目类型/服务
projectManagerId?: string // 项目经理ID
projectManagerName?: string // 项目经理姓名
projectStaff?: string[] // 施工人员
startDate?: string // 开始日期
endDate?: string // 结束日期
status: number // 状态 (API返回数字类型)
statusLabel?: string // 状态标签 (API返回的状态文本)
constructorIds?: string // 施工人员ID
constructorName?: string // 施工人员姓名
coverUrl?: string // 封面URL
createDt?: Date
updateDt?: Date
// 为了保持向后兼容,添加一些别名字段
id?: string // projectId的别名
fieldName?: string // farmName的别名
fieldLocation?: string // farmAddress的别名
commissionUnit?: string // client的别名
commissionContact?: string // clientContact的别名
commissionPhone?: string // clientPhone的别名
orgNumber?: string // turbineModel的别名
projectManager?: string // projectManagerName的别名
projectPeriod?: [string, string] // 项目周期
}
export interface ProjectQuery {

View File

@ -16,6 +16,7 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { RouteLocationMatched } from 'vue-router'
import { findTree } from 'xe-utils'
import { useRouteStore } from '@/stores'
@ -25,31 +26,32 @@ const router = useRouter()
const { routes } = useRouteStore()
const attrs = useAttrs()
let home: RouteLocationMatched | null = null
const getHome = () => {
if (!home) {
const cloneRoutes = JSON.parse(JSON.stringify(routes)) as RouteLocationMatched[]
const obj = findTree(cloneRoutes, (i) => i.path === '/dashboard/workplace')
home = obj.item
}
}
// home
const home = computed(() => {
const cloneRoutes = JSON.parse(JSON.stringify(routes)) as RouteLocationMatched[]
const obj = findTree(cloneRoutes, (i) => i.path === '/dashboard/workplace')
return obj?.item || null
})
// 使computed
const breadcrumbList = computed(() => {
//
if (route.path.startsWith('/redirect/')) {
return []
}
const breadcrumbList = ref<RouteLocationMatched[]>([])
function getBreadcrumbList() {
getHome()
const cloneRoutes = JSON.parse(JSON.stringify(routes)) as RouteLocationMatched[]
const obj = findTree(cloneRoutes, (i) => i.path === route.path)
//
const arr = obj ? obj.nodes.filter((item) => item.meta && item.meta.title && item.meta.breadcrumb !== false) : []
if (home) {
breadcrumbList.value = [home, ...arr]
}
}
getBreadcrumbList()
watchEffect(() => {
if (route.path.startsWith('/redirect/')) return
getBreadcrumbList()
// home
if (home.value && !arr.some(item => item.path === home.value?.path)) {
return [home.value, ...arr]
}
return arr
})
//

View File

@ -76,7 +76,7 @@
<script setup lang="ts">
import { Modal } from '@arco-design/web-vue'
import { useFullscreen } from '@vueuse/core'
import { onMounted, ref } from 'vue'
import { onMounted, ref, nextTick } from 'vue'
import Message from './Message.vue'
import SettingDrawer from './SettingDrawer.vue'
import Search from './Search.vue'
@ -89,41 +89,73 @@ defineOptions({ name: 'HeaderRight' })
const { isDesktop } = useDevice()
const { breakpoint } = useBreakpoint()
let socket: WebSocket
let socket: WebSocket | null = null
//
onBeforeUnmount(() => {
if (socket) {
socket.close()
socket = null
}
})
const unreadMessageCount = ref(0)
// WebSocket
// WebSocket - 使
let initTimer: NodeJS.Timeout | null = null
const initWebSocket = (token: string) => {
socket = new WebSocket(`${import.meta.env.VITE_API_WS_URL}/websocket?token=${token}`)
socket.onopen = () => {
// console.log('WebSocket connection opened')
if (initTimer) {
clearTimeout(initTimer)
}
socket.onmessage = (event) => {
unreadMessageCount.value = Number.parseInt(event.data)
}
initTimer = setTimeout(() => {
//
if (socket) {
socket.close()
socket = null
}
socket.onerror = () => {
// console.error('WebSocket error:', error)
}
try {
socket = new WebSocket(`${import.meta.env.VITE_API_WS_URL}/websocket?token=${token}`)
socket.onclose = () => {
// console.log('WebSocket connection closed')
}
socket.onopen = () => {
// console.log('WebSocket connection opened')
}
socket.onmessage = (event) => {
const count = Number.parseInt(event.data)
if (!isNaN(count)) {
unreadMessageCount.value = count
}
}
socket.onerror = () => {
// console.error('WebSocket error:', error)
}
socket.onclose = () => {
// console.log('WebSocket connection closed')
socket = null
}
} catch (error) {
console.error('Failed to create WebSocket connection:', error)
}
initTimer = null
}, 100) // 100ms
}
//
const getMessageCount = async () => {
const { data } = await getUnreadMessageCount()
unreadMessageCount.value = data.total
const token = getToken()
if (token) {
initWebSocket(token)
try {
const token = getToken()
if (token && !socket) {
nextTick(() => {
initWebSocket(token)
})
}
} catch (error) {
console.error('Failed to get message count:', error)
}
}
@ -153,7 +185,9 @@ const logout = () => {
}
onMounted(() => {
getMessageCount()
nextTick(() => {
// getMessageCount()
})
})
</script>

View File

@ -45,13 +45,17 @@ interface Props {
const onlyOneChild = ref<RouteRecordRaw | null>(null)
const isOneShowingChild = ref(false)
const handleFunction = () => {
// 使watchEffectpropssetup
watchEffect(() => {
const children = props.item?.children?.length ? props.item.children : []
//
const showingChildren = children.filter((i) => i.meta?.hidden === false)
if (showingChildren.length) {
// hidden: false
onlyOneChild.value = showingChildren[showingChildren.length - 1]
} else {
onlyOneChild.value = null
}
// ,
@ -60,11 +64,11 @@ const handleFunction = () => {
}
// ,
if (showingChildren.length === 0) {
else if (showingChildren.length === 0) {
onlyOneChild.value = { ...props.item, meta: { ...props.item.meta, noShowingChildren: true } } as any
isOneShowingChild.value = true
} else {
isOneShowingChild.value = false
}
}
handleFunction()
})
</script>

View File

@ -11,6 +11,12 @@
@menu-item-click="onMenuItemClick"
@collapse="onCollapse"
>
<template #expand-icon-down>
<icon-down />
</template>
<template #expand-icon-right>
<icon-right />
</template>
<MenuItem v-for="(item, index) in sidebarRoutes" :key="item.path + index" :item="item"></MenuItem>
</a-menu>
</template>

View File

@ -5,10 +5,10 @@ import { useAppStore } from '@/stores'
const appStore = useAppStore()
onMounted(() => {
const s = document.createElement('script')
s.async = true
s.src = `https://cdn.wwads.cn/js/makemoney.js`
document.querySelector('.wwads-container')!.appendChild(s)
// const s = document.createElement('script')
// s.async = true
// s.src = `https://cdn.wwads.cn/js/makemoney.js`
// document.querySelector('.wwads-container')!.appendChild(s)
})
</script>

View File

@ -12,31 +12,350 @@ export const systemRoutes: RouteRecordRaw[] = [
meta: { hidden: true },
},
{
path: '/',
name: 'Dashboard',
path: '/company',
name: 'Company',
component: Layout,
redirect: '/dashboard/workplace',
meta: { title: '仪表盘', icon: 'dashboard', hidden: false },
redirect: '/company/overview',
meta: { title: '企业概览', icon: 'company', hidden: false, sort: 1 },
children: [
{
path: '/dashboard/workplace',
name: 'Workplace',
component: () => import('@/views/dashboard/workplace/index.vue'),
meta: { title: '工作台', icon: 'desktop', hidden: false, affix: true },
},
{
path: '/dashboard/analysis',
name: 'Analysis',
component: () => import('@/views/dashboard/analysis/index.vue'),
meta: { title: '分析页', icon: 'insert-chart', hidden: false },
},
path: '/company/overview',
name: 'CompanyOverview',
component: () => import('@/views/company/overview/index.vue'),
meta: { title: '企业概览', icon: 'overview', hidden: false },
}
],
},
{
path: '/project',
path: '/organization',
name: 'Organization',
component: Layout,
redirect: '/organization/hr/member',
meta: { title: '组织架构', icon: 'organization', hidden: false, sort: 2 },
children: [
{
path: '/organization/hr',
name: 'HRManagement',
component: () => import('@/components/ParentView/index.vue'),
redirect: '/organization/hr/member',
meta: { title: '人员管理', icon: 'user', hidden: false },
children: [
{
path: '/organization/hr/member',
name: 'HRMember',
component: () => import('@/views/system/user/index.vue'),
meta: { title: '成员', icon: 'user', hidden: false },
},
{
path: '/organization/hr/dept',
name: 'HRDept',
component: () => import('@/views/system/dept/index.vue'),
meta: { title: '部门', icon: 'dept', hidden: false },
},
{
path: '/organization/hr/workload',
name: 'HRWorkload',
component: () => import('@/views/hr/workload/index.vue'),
meta: { title: '工作量', icon: 'workload', hidden: false },
},
{
path: '/organization/hr/attendance',
name: 'HRAttendance',
component: () => import('@/views/hr/attendance/index.vue'),
meta: { title: '考勤', icon: 'attendance', hidden: false },
},
{
path: '/organization/hr/performance',
name: 'HRPerformance',
component: () => import('@/views/hr/performance/index.vue'),
meta: { title: '绩效', icon: 'performance', hidden: false },
},
{
path: '/organization/hr/salary',
name: 'HRSalary',
component: () => import('@/views/hr/salary/index.vue'),
meta: { title: '工资', icon: 'salary', hidden: false },
},
{
path: '/organization/hr/contribution',
name: 'HRContribution',
component: () => import('@/views/hr/contribution/index.vue'),
meta: { title: '责献积分制度、与企业共同发展', icon: 'contribution', hidden: false },
}
]
},
{
path: '/organization/role',
name: 'OrganizationRole',
component: () => import('@/views/system/role/index.vue'),
meta: { title: '角色管理', icon: 'role', hidden: false },
},
{
path: '/organization/log',
name: 'OrganizationLog',
component: () => import('@/views/monitor/log/index.vue'),
meta: { title: '操作日志', icon: 'log', hidden: false },
}
],
},
{
path: '/products-services',
name: 'ProductsServices',
component: Layout,
redirect: '/products-services/products/hardware/tower-monitoring',
meta: { title: '产品与服务', icon: 'products', hidden: false, sort: 3 },
children: [
{
path: '/products-services/products',
name: 'Products',
component: () => import('@/components/ParentView/index.vue'),
redirect: '/products-services/products/hardware/tower-monitoring',
meta: { title: '产品', icon: 'product', hidden: false },
children: [
{
path: '/products-services/products/hardware',
name: 'SmartHardware',
component: () => import('@/components/ParentView/index.vue'),
redirect: '/products-services/products/hardware/tower-monitoring',
meta: { title: '智能硬件', icon: 'hardware', hidden: false },
children: [
{
path: '/products-services/products/hardware/tower-monitoring',
name: 'TowerMonitoring',
component: () => import('@/views/product/hardware/tower-monitoring/index.vue'),
meta: { title: '风电塔下监测系统', icon: 'monitor', hidden: false },
},
{
path: '/products-services/products/hardware/custom-drone',
name: 'CustomDrone',
component: () => import('@/views/product/hardware/custom-drone/index.vue'),
meta: { title: '定制无人机', icon: 'drone', hidden: false },
},
{
path: '/products-services/products/hardware/drone-basket',
name: 'DroneBasket',
component: () => import('@/views/service/lightning-detection/index.vue'),
meta: { title: '无人吊篮', icon: 'basket', hidden: false },
},
{
path: '/products-services/products/hardware/blade-robot',
name: 'BladeRobot',
component: () => import('@/views/service/lightning-detection/index.vue'),
meta: { title: '叶片维修机器人', icon: 'robot', hidden: false },
}
]
},
{
path: '/products-services/products/software',
name: 'SmartSoftware',
component: () => import('@/components/ParentView/index.vue'),
redirect: '/products-services/products/software/field-assistant',
meta: { title: '智能软件', icon: 'software', hidden: false },
children: [
{
path: '/products-services/products/software/field-assistant',
name: 'FieldAssistant',
component: () => import('@/views/service/lightning-detection/index.vue'),
meta: { title: '风电外业智能助手(外业数据实时处理)', icon: 'assistant', hidden: false },
},
{
path: '/products-services/products/software/blade-report',
name: 'BladeReportSystem',
component: () => import('@/views/service/lightning-detection/index.vue'),
meta: { title: '叶片检查报告生成系统', icon: 'report', hidden: false },
},
{
path: '/products-services/products/software/ground-station',
name: 'GroundStation',
component: () => import('@/views/service/lightning-detection/index.vue'),
meta: { title: '无人机地面站软件', icon: 'station', hidden: false },
}
]
}
]
},
{
path: '/products-services/services',
name: 'Services',
component: () => import('@/components/ParentView/index.vue'),
redirect: '/products-services/services/lightning-detection',
meta: { title: '服务', icon: 'service', hidden: false },
children: [
{
path: '/products-services/services/lightning-detection',
name: 'LightningDetection',
component: () => import('@/views/service/lightning-detection/index.vue'),
meta: { title: '防雷检测', icon: 'lightning', hidden: false },
},
{
path: '/products-services/services/blade-internal-detection',
name: 'BladeInternalDetection',
component: () => import('@/views/service/blade-internal-detection/index.vue'),
meta: { title: '叶片内部检测', icon: 'internal', hidden: false },
},
{
path: '/products-services/services/blade-external-detection',
name: 'BladeExternalDetection',
component: () => import('@/views/service/blade-internal-detection/index.vue'),
meta: { title: '叶片外部检测', icon: 'external', hidden: false },
},
{
path: '/products-services/services/solar-inspection',
name: 'SolarInspection',
component: () => import('@/views/service/lightning-detection/index.vue'),
meta: { title: '太阳能光伏巡检', icon: 'solar', hidden: false },
},
{
path: '/products-services/services/cleaning',
name: 'CleaningService',
component: () => import('@/views/service/lightning-detection/index.vue'),
meta: { title: '清洗', icon: 'cleaning', hidden: false },
},
{
path: '/products-services/services/coating',
name: 'CoatingService',
component: () => import('@/views/service/lightning-detection/index.vue'),
meta: { title: '喷涂', icon: 'coating', hidden: false },
},
{
path: '/products-services/services/dam-crack-detection',
name: 'DamCrackDetection',
component: () => import('@/views/service/lightning-detection/index.vue'),
meta: { title: '大坝裂缝检测', icon: 'dam', hidden: false },
},
{
path: '/products-services/services/bridge-crack-detection',
name: 'BridgeCrackDetection',
component: () => import('@/views/service/lightning-detection/index.vue'),
meta: { title: '桥梁裂缝检测', icon: 'bridge', hidden: false },
},
{
path: '/products-services/services/blade-maintenance',
name: 'BladeMaintenance',
component: () => import('@/views/service/lightning-detection/index.vue'),
meta: { title: '叶片维修', icon: 'maintenance', hidden: false },
}
]
}
],
},
{
path: '/project-management',
name: 'ProjectManagement',
component: Layout,
redirect: '/project-management/bidding/tender-documents',
meta: { title: '项目管理', icon: 'project', hidden: false, sort: 4 },
children: [
{
path: '/project-management/bidding',
name: 'ProjectBidding',
meta: {
title: '项目投标',
icon: 'gavel'
},
children: [
{
path: '/project-management/bidding/tender-documents',
name: 'TenderDocuments',
component: () => import('@/views/project-management/bidding/tender-documents/index.vue'),
meta: {
title: '招标文件管理',
icon: 'file-text'
}
},
{
path: '/project-management/bidding/bid-documents',
name: 'BidDocuments',
component: () => import('@/views/project-management/bidding/bid-documents/index.vue'),
meta: {
title: '投标文件管理',
icon: 'file-text'
}
},
{
path: '/project-management/bidding/award-notice',
name: 'AwardNotice',
component: () => import('@/views/project-management/bidding/award-notice/index.vue'),
meta: {
title: '中标通知书管理',
icon: 'trophy'
}
}
]
},
{
path: '/project-management/contract',
name: 'ProjectContract',
meta: {
title: '合同管理',
icon: 'file-text'
},
children: [
{
path: '/project-management/contract/revenue-contract',
name: 'RevenueContract',
component: () => import('@/views/project-management/contract/revenue-contract/index.vue'),
meta: {
title: '收入合同管理',
icon: 'dollar'
}
},
{
path: '/project-management/contract/expense-contract',
name: 'ExpenseContract',
component: () => import('@/views/project-management/contract/expense-contract/index.vue'),
meta: {
title: '支出合同管理',
icon: 'credit-card'
}
},
{
path: '/project-management/contract/cost-management',
name: 'CostManagement',
component: () => import('@/views/project-management/contract/cost-management/index.vue'),
meta: {
title: '成本管理',
icon: 'bar-chart'
}
}
]
},
{
path: '/project-management/projects',
name: 'ProjectsManagement',
meta: {
title: '项目管理',
icon: 'briefcase'
},
children: [
{
path: '/project-management/projects/initiation',
name: 'ProjectInitiation',
component: () => import('@/views/project-management/projects/initiation/index.vue'),
meta: {
title: '立项管理',
icon: 'plus-circle'
}
},
{
path: '/project-management/projects/management',
name: 'ProjectDetailManagement',
component: () => import('@/views/project-management/projects/management/index.vue'),
meta: {
title: '项目详细管理',
icon: 'settings'
}
}
]
}
],
},
{
path: '/',
name: 'Project',
component: Layout,
meta: { title: '项目管理', icon: 'project', hidden: false, sort: 2 },
redirect: '/project',
meta: { title: '项目管理(旧)', icon: 'project-old', hidden: true, sort: 5 },
children: [
{
path: '/project',
@ -55,6 +374,18 @@ export const systemRoutes: RouteRecordRaw[] = [
name: 'TaskBoard',
component: () => import('@/views/project/task/index.vue'),
meta: { title: '任务看板', icon: 'table', hidden: false },
},
{
path: '/project/kanban',
name: 'ProjectKanban',
component: () => import('@/views/project/kanban/index.vue'),
meta: { title: '项目进度', icon: 'kanban', hidden: false },
},
{
path: '/project/budget',
name: 'ProjectBudget',
component: () => import('@/views/project/budget/index.vue'),
meta: { title: '项目预算', icon: 'budget', hidden: false },
}
],
},
@ -69,72 +400,98 @@ export const systemRoutes: RouteRecordRaw[] = [
meta: { hidden: true },
},
{
path: '/user',
name: 'User',
component: Layout,
meta: { hidden: true },
path: '/enterprise-settings',
name: 'EnterpriseSettings',
meta: {
title: '企业设置',
icon: 'building'
},
children: [
{
path: '/user/profile',
name: 'UserProfile',
component: () => import('@/views/user/profile/index.vue'),
meta: { title: '个人中心', showInTabs: false },
path: '/enterprise-settings/company-info',
name: 'CompanyInfo',
component: () => import('@/views/enterprise-settings/company-info/index.vue'),
meta: {
title: '企业信息',
icon: 'info-circle'
}
},
{
path: '/user/message',
name: 'UserMessage',
component: () => import('@/views/user/message/index.vue'),
meta: { title: '消息中心', showInTabs: false },
path: '/enterprise-settings/admin-permissions',
name: 'AdminPermissions',
component: () => import('@/views/enterprise-settings/admin-permissions/index.vue'),
meta: {
title: '管理员权限',
icon: 'user-switch'
}
},
{
path: '/user/notice',
name: 'UserNotice',
component: () => import('@/views/user/message/components/view/index.vue'),
meta: { title: '查看公告' },
path: '/enterprise-settings/data-migration',
name: 'DataMigration',
component: () => import('@/views/enterprise-settings/data-migration/index.vue'),
meta: {
title: '数据迁移',
icon: 'swap'
}
},
],
{
path: '/enterprise-settings/version-upgrade',
name: 'VersionUpgrade',
component: () => import('@/views/enterprise-settings/version-upgrade/index.vue'),
meta: {
title: '版本升级提醒',
icon: 'arrow-up'
}
}
]
},
{
path: '/about',
name: 'About',
component: Layout,
meta: { title: '关于项目', icon: 'apps', hidden: false, sort: 999 },
redirect: '/about/document/api',
path: '/system-resource',
name: 'SystemResource',
meta: {
title: '系统资源管理',
icon: 'cluster'
},
children: [
{
path: '/about/document/api',
component: () => import('@/views/about/document/api/index.vue'),
meta: { title: '接口文档', icon: 'swagger', hidden: false, keepAlive: true },
path: '/system-resource/device-management',
name: 'DeviceManagement',
component: () => import('@/views/system-resource/device-management/index.vue'),
meta: {
title: '设备管理',
icon: 'desktop'
}
},
{
path: 'https://continew.top',
meta: { title: '在线文档', icon: 'continew', hidden: false },
},
{
path: 'https://arco.design/vue/component/button',
meta: { title: 'Arco Design文档', icon: 'arco', hidden: false },
},
{
path: '/about/source',
name: 'AboutSource',
meta: { title: '开源地址', icon: 'github', hidden: false },
path: '/system-resource/information-system',
name: 'InformationSystem',
meta: {
title: '信息化系统管理',
icon: 'code'
},
children: [
{
path: 'https://gitee.com/continew/continew-admin',
meta: { title: 'Gitee', icon: 'gitee', hidden: false },
path: '/system-resource/information-system/software-management',
name: 'SoftwareManagement',
component: () => import('@/views/system-resource/information-system/software-management/index.vue'),
meta: {
title: '软件管理',
icon: 'appstore'
}
},
{
path: 'https://gitcode.com/continew/continew-admin',
meta: { title: 'GitCode', icon: 'gitcode', hidden: false },
},
{
path: 'https://github.com/continew-org/continew-admin',
meta: { title: 'GitHub', icon: 'github', hidden: false },
},
],
},
],
},
path: '/system-resource/information-system/system-backup',
name: 'SystemBackup',
component: () => import('@/views/system-resource/information-system/system-backup/index.vue'),
meta: {
title: '系统备份管理',
icon: 'save'
}
}
]
}
]
}
]
// 固定路由(默认路由)
@ -161,3 +518,4 @@ export const constantRoutes: RouteRecordRaw[] = [
meta: { hidden: true },
},
]

View File

@ -1,5 +1,5 @@
import { defineStore } from 'pinia'
import { computed, reactive, toRefs } from 'vue'
import { computed, reactive, toRefs, watch, watchEffect } from 'vue'
import { generate, getRgbStr } from '@arco-design/color'
import { type BasicConfig, listSiteOptionDict } from '@/apis'
import { getSettings } from '@/config/setting'
@ -58,21 +58,21 @@ const storeSetup = () => {
const siteConfig = reactive({}) as BasicConfig
// 初始化系统配置
const initSiteConfig = () => {
listSiteOptionDict().then((res) => {
const resMap = new Map()
res.data.forEach((item) => {
resMap.set(item.label, item.value)
})
siteConfig.SITE_FAVICON = resMap.get('SITE_FAVICON')
siteConfig.SITE_LOGO = resMap.get('SITE_LOGO')
siteConfig.SITE_TITLE = resMap.get('SITE_TITLE')
siteConfig.SITE_COPYRIGHT = resMap.get('SITE_COPYRIGHT')
siteConfig.SITE_BEIAN = resMap.get('SITE_BEIAN')
document.title = resMap.get('SITE_TITLE')
document
.querySelector('link[rel="shortcut icon"]')
?.setAttribute('href', resMap.get('SITE_FAVICON') || '/favicon.ico')
})
// listSiteOptionDict().then((res) => {
// const resMap = new Map()
// res.data.forEach((item) => {
// resMap.set(item.label, item.value)
// })
// siteConfig.SITE_FAVICON = resMap.get('SITE_FAVICON')
// siteConfig.SITE_LOGO = resMap.get('SITE_LOGO')
// siteConfig.SITE_TITLE = resMap.get('SITE_TITLE')
// siteConfig.SITE_COPYRIGHT = resMap.get('SITE_COPYRIGHT')
// siteConfig.SITE_BEIAN = resMap.get('SITE_BEIAN')
// document.title = resMap.get('SITE_TITLE')
// document
// .querySelector('link[rel="shortcut icon"]')
// ?.setAttribute('href', resMap.get('SITE_FAVICON') || '/favicon.ico')
// })
}
// 设置系统配置
@ -81,26 +81,24 @@ const storeSetup = () => {
document.title = config.SITE_TITLE || ''
document.querySelector('link[rel="shortcut icon"]')?.setAttribute('href', config.SITE_FAVICON || '/favicon.ico')
}
// 监听 色弱模式 和 哀悼模式
watch([
() => settingConfig.enableMourningMode,
() => settingConfig.enableColorWeaknessMode,
], ([mourningMode, colorWeaknessMode]) => {
// 使用watchEffect优化监听避免递归更新
watchEffect(() => {
const filters = [] as string[]
if (mourningMode) {
if (settingConfig.enableMourningMode) {
filters.push('grayscale(100%)')
}
if (colorWeaknessMode) {
if (settingConfig.enableColorWeaknessMode) {
filters.push('invert(80%)')
}
// 如果没有任何滤镜条件,移除 `filter` 样式
if (filters.length === 0) {
document.documentElement.style.removeProperty('filter')
} else {
document.documentElement.style.setProperty('filter', filters.join(' '))
}
}, {
immediate: true,
})
const getFavicon = () => {

View File

@ -0,0 +1,76 @@
<template>
<GiPageLayout>
<div class="company-overview">
<a-card title="企业概览" :bordered="false">
<a-row :gutter="24">
<!-- 基本信息 -->
<a-col :span="8">
<a-card title="基本信息" size="small">
<a-descriptions :column="1" size="small">
<a-descriptions-item label="企业名称">科技创新有限公司</a-descriptions-item>
<a-descriptions-item label="成立时间">2010-01-01</a-descriptions-item>
<a-descriptions-item label="注册资本">1000万元</a-descriptions-item>
<a-descriptions-item label="法人代表">张三</a-descriptions-item>
<a-descriptions-item label="员工总数">156</a-descriptions-item>
</a-descriptions>
</a-card>
</a-col>
<!-- 经营状况 -->
<a-col :span="8">
<a-card title="经营状况" size="small">
<a-descriptions :column="1" size="small">
<a-descriptions-item label="年营业额">5000万元</a-descriptions-item>
<a-descriptions-item label="年利润">800万元</a-descriptions-item>
<a-descriptions-item label="在建项目">12</a-descriptions-item>
<a-descriptions-item label="已完成项目">108</a-descriptions-item>
<a-descriptions-item label="客户满意度">98.5%</a-descriptions-item>
</a-descriptions>
</a-card>
</a-col>
<!-- 发展目标 -->
<a-col :span="8">
<a-card title="发展目标" size="small">
<a-descriptions :column="1" size="small">
<a-descriptions-item label="三年目标">成为行业领先企业</a-descriptions-item>
<a-descriptions-item label="五年目标">上市公司</a-descriptions-item>
<a-descriptions-item label="核心价值">创新诚信共赢</a-descriptions-item>
<a-descriptions-item label="企业愿景">让科技改变生活</a-descriptions-item>
<a-descriptions-item label="企业使命">为客户创造价值</a-descriptions-item>
</a-descriptions>
</a-card>
</a-col>
</a-row>
<!-- 图表展示 -->
<a-row :gutter="24" class="mt-6">
<a-col :span="12">
<a-card title="月度营收趋势" size="small">
<div style="height: 300px; display: flex; align-items: center; justify-content: center; color: #999;">
图表展示区域
</div>
</a-card>
</a-col>
<a-col :span="12">
<a-card title="部门人员分布" size="small">
<div style="height: 300px; display: flex; align-items: center; justify-content: center; color: #999;">
图表展示区域
</div>
</a-card>
</a-col>
</a-row>
</a-card>
</div>
</GiPageLayout>
</template>
<script setup lang="ts">
//
</script>
<style scoped>
.company-overview {
padding: 16px;
}
</style>

View File

@ -0,0 +1,363 @@
<template>
<GiPageLayout>
<GiTable
row-key="id"
title="管理员权限管理"
:data="dataList"
:columns="tableColumns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1400 }"
:pagination="pagination"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
@refresh="search"
>
<template #top>
<GiForm
v-model="searchForm"
search
:columns="queryFormColumns"
size="medium"
@search="search"
@reset="reset"
/>
</template>
<template #toolbar-left>
<a-space>
<a-button type="primary" @click="openAddModal">
<template #icon><icon-plus /></template>
<template #default>新增管理员</template>
</a-button>
<a-button @click="batchOperation">
<template #icon><icon-settings /></template>
<template #default>批量操作</template>
</a-button>
</a-space>
</template>
<!-- 管理员类型 -->
<template #adminType="{ record }">
<a-tag :color="getAdminTypeColor(record.adminType)">
{{ record.adminType }}
</a-tag>
</template>
<!-- 状态显示 -->
<template #status="{ record }">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<!-- 权限范围 -->
<template #permissions="{ record }">
<a-space>
<a-tag
v-for="permission in record.permissions.slice(0, 3)"
:key="permission"
size="small"
>
{{ permission }}
</a-tag>
<a-tag v-if="record.permissions.length > 3" size="small">
+{{ record.permissions.length - 3 }}
</a-tag>
</a-space>
</template>
<!-- 在线状态 -->
<template #onlineStatus="{ record }">
<a-space>
<div
:class="record.isOnline ? 'online-dot' : 'offline-dot'"
class="status-dot"
></div>
{{ record.isOnline ? '在线' : '离线' }}
</a-space>
</template>
<!-- 操作列 -->
<template #action="{ record }">
<a-space>
<a-link @click="viewDetail(record)">详情</a-link>
<a-link @click="editRecord(record)">编辑</a-link>
<a-link @click="resetPassword(record)">重置密码</a-link>
<a-link
@click="toggleStatus(record)"
:class="record.status === 'active' ? 'text-red-500' : 'text-green-500'"
>
{{ record.status === 'active' ? '禁用' : '启用' }}
</a-link>
</a-space>
</template>
</GiTable>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
//
const searchForm = reactive({
username: '',
adminType: '',
status: '',
createTime: '',
page: 1,
size: 10
})
//
const queryFormColumns = [
{
field: 'username',
label: '用户名',
type: 'input' as const,
props: {
placeholder: '请输入用户名'
}
},
{
field: 'adminType',
label: '管理员类型',
type: 'select' as const,
props: {
placeholder: '请选择管理员类型',
options: [
{ label: '超级管理员', value: '超级管理员' },
{ label: '系统管理员', value: '系统管理员' },
{ label: '业务管理员', value: '业务管理员' },
{ label: '财务管理员', value: '财务管理员' }
]
}
},
{
field: 'status',
label: '状态',
type: 'select' as const,
props: {
placeholder: '请选择状态',
options: [
{ label: '正常', value: 'active' },
{ label: '禁用', value: 'disabled' },
{ label: '锁定', value: 'locked' }
]
}
}
]
//
const tableColumns: TableColumnData[] = [
{ title: '用户名', dataIndex: 'username', width: 120 },
{ title: '真实姓名', dataIndex: 'realName', width: 100 },
{ title: '管理员类型', dataIndex: 'adminType', slotName: 'adminType', width: 120 },
{ title: '联系电话', dataIndex: 'phone', width: 120 },
{ title: '邮箱', dataIndex: 'email', width: 200 },
{ title: '权限范围', dataIndex: 'permissions', slotName: 'permissions', width: 250 },
{ title: '创建时间', dataIndex: 'createTime', width: 160 },
{ title: '最后登录', dataIndex: 'lastLoginTime', width: 160 },
{ title: '登录次数', dataIndex: 'loginCount', width: 100 },
{ title: '在线状态', dataIndex: 'onlineStatus', slotName: 'onlineStatus', width: 100 },
{ title: '状态', dataIndex: 'status', slotName: 'status', width: 80 },
{ title: '备注', dataIndex: 'remark', width: 200, ellipsis: true, tooltip: true },
{ title: '操作', slotName: 'action', width: 250, fixed: 'right' }
]
//
const loading = ref(false)
const dataList = ref([
{
id: 1,
username: 'superadmin',
realName: '系统管理员',
adminType: '超级管理员',
phone: '13800138001',
email: 'admin@windtech.com',
permissions: ['用户管理', '系统设置', '数据管理', '权限管理', '日志查看'],
createTime: '2023-01-01 10:00:00',
lastLoginTime: '2024-03-15 09:30:00',
loginCount: 1256,
isOnline: true,
status: 'active',
remark: '系统超级管理员,拥有所有权限'
},
{
id: 2,
username: 'projectadmin',
realName: '张项目经理',
adminType: '业务管理员',
phone: '13800138002',
email: 'zhang@windtech.com',
permissions: ['项目管理', '合同管理', '团队管理'],
createTime: '2023-02-15 14:20:00',
lastLoginTime: '2024-03-14 16:45:00',
loginCount: 892,
isOnline: false,
status: 'active',
remark: '负责项目相关业务管理'
},
{
id: 3,
username: 'financeadmin',
realName: '李财务经理',
adminType: '财务管理员',
phone: '13800138003',
email: 'li@windtech.com',
permissions: ['财务管理', '预算管理', '成本控制'],
createTime: '2023-03-01 11:10:00',
lastLoginTime: '2024-03-13 10:20:00',
loginCount: 654,
isOnline: true,
status: 'active',
remark: '负责财务相关业务管理'
},
{
id: 4,
username: 'techsupport',
realName: '王技术支持',
adminType: '系统管理员',
phone: '13800138004',
email: 'wang@windtech.com',
permissions: ['系统维护', '数据备份', '技术支持'],
createTime: '2023-04-10 15:30:00',
lastLoginTime: '2024-03-10 14:15:00',
loginCount: 432,
isOnline: false,
status: 'disabled',
remark: '技术支持人员,目前停用中'
}
])
const pagination = reactive({
current: 1,
pageSize: 10,
total: 4,
showTotal: true,
showPageSize: true
})
//
const getAdminTypeColor = (type: string) => {
const colorMap: Record<string, string> = {
'超级管理员': 'red',
'系统管理员': 'blue',
'业务管理员': 'green',
'财务管理员': 'orange'
}
return colorMap[type] || 'gray'
}
//
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
'active': 'green',
'disabled': 'red',
'locked': 'orange'
}
return colorMap[status] || 'gray'
}
//
const getStatusText = (status: string) => {
const textMap: Record<string, string> = {
'active': '正常',
'disabled': '禁用',
'locked': '锁定'
}
return textMap[status] || status
}
//
const search = async () => {
loading.value = true
setTimeout(() => {
loading.value = false
}, 1000)
}
const reset = () => {
Object.assign(searchForm, {
username: '',
adminType: '',
status: '',
createTime: '',
page: 1,
size: 10
})
pagination.current = 1
search()
}
//
const onPageChange = (page: number) => {
searchForm.page = page
pagination.current = page
search()
}
const onPageSizeChange = (size: number) => {
searchForm.size = size
searchForm.page = 1
pagination.pageSize = size
pagination.current = 1
search()
}
//
const openAddModal = () => {
Message.info('新增管理员功能开发中...')
}
const batchOperation = () => {
Message.info('批量操作功能开发中...')
}
const viewDetail = (record: any) => {
Message.info(`查看管理员详情: ${record.realName}`)
}
const editRecord = (record: any) => {
Message.info(`编辑管理员: ${record.realName}`)
}
const resetPassword = (record: any) => {
Message.info(`重置密码: ${record.realName}`)
}
const toggleStatus = (record: any) => {
const action = record.status === 'active' ? '禁用' : '启用'
Message.info(`${action}管理员: ${record.realName}`)
}
onMounted(() => {
search()
})
</script>
<style scoped>
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
}
.online-dot {
background-color: #52c41a;
}
.offline-dot {
background-color: #d9d9d9;
}
.text-red-500 {
color: #ef4444;
}
.text-green-500 {
color: #22c55e;
}
</style>

View File

@ -0,0 +1,271 @@
<template>
<GiPageLayout>
<div class="enterprise-info-container">
<!-- 企业基本信息卡片 -->
<a-card title="企业基本信息" class="mb-6">
<template #extra>
<a-button type="primary" @click="editCompanyInfo">
<template #icon><icon-edit /></template>
编辑信息
</a-button>
</template>
<a-descriptions :column="2" bordered>
<a-descriptions-item label="企业名称">{{ companyInfo.name }}</a-descriptions-item>
<a-descriptions-item label="统一社会信用代码">{{ companyInfo.creditCode }}</a-descriptions-item>
<a-descriptions-item label="法定代表人">{{ companyInfo.legalPerson }}</a-descriptions-item>
<a-descriptions-item label="注册资本">{{ companyInfo.registeredCapital }}</a-descriptions-item>
<a-descriptions-item label="成立日期">{{ companyInfo.establishDate }}</a-descriptions-item>
<a-descriptions-item label="营业期限">{{ companyInfo.businessTerm }}</a-descriptions-item>
<a-descriptions-item label="注册地址" :span="2">{{ companyInfo.registeredAddress }}</a-descriptions-item>
<a-descriptions-item label="经营范围" :span="2">{{ companyInfo.businessScope }}</a-descriptions-item>
<a-descriptions-item label="联系电话">{{ companyInfo.phone }}</a-descriptions-item>
<a-descriptions-item label="企业邮箱">{{ companyInfo.email }}</a-descriptions-item>
<a-descriptions-item label="官方网站">{{ companyInfo.website }}</a-descriptions-item>
<a-descriptions-item label="员工人数">{{ companyInfo.employeeCount }}</a-descriptions-item>
</a-descriptions>
</a-card>
<!-- 企业资质证书 -->
<a-card title="企业资质证书" class="mb-6">
<template #extra>
<a-button @click="addCertificate">
<template #icon><icon-plus /></template>
新增证书
</a-button>
</template>
<a-row :gutter="16">
<a-col
v-for="cert in certificates"
:key="cert.id"
:span="8"
class="mb-4"
>
<a-card hoverable>
<template #cover>
<img
:src="cert.image"
:alt="cert.name"
style="height: 200px; object-fit: cover;"
/>
</template>
<a-card-meta
:title="cert.name"
:description="cert.description"
/>
<template #actions>
<a-link @click="viewCertificate(cert)">查看</a-link>
<a-link @click="editCertificate(cert)">编辑</a-link>
<a-link @click="deleteCertificate(cert)">删除</a-link>
</template>
</a-card>
</a-col>
</a-row>
</a-card>
<!-- 银行信息 -->
<a-card title="银行账户信息" class="mb-6">
<template #extra>
<a-button @click="addBankAccount">
<template #icon><icon-plus /></template>
新增账户
</a-button>
</template>
<GiTable
row-key="id"
:data="bankAccounts"
:columns="bankColumns"
:pagination="false"
>
<template #accountType="{ record }">
<a-tag :color="getBankTypeColor(record.accountType)">
{{ record.accountType }}
</a-tag>
</template>
<template #isDefault="{ record }">
<a-tag v-if="record.isDefault" color="green">默认账户</a-tag>
<span v-else>-</span>
</template>
<template #action="{ record }">
<a-space>
<a-link @click="editBankAccount(record)">编辑</a-link>
<a-link @click="setDefaultAccount(record)" v-if="!record.isDefault">设为默认</a-link>
<a-link @click="deleteBankAccount(record)">删除</a-link>
</a-space>
</template>
</GiTable>
</a-card>
<!-- 企业统计信息 -->
<a-card title="企业统计信息">
<a-row :gutter="16">
<a-col :span="6">
<a-statistic title="年营业额" :value="statistics.annualRevenue" suffix="万元" />
</a-col>
<a-col :span="6">
<a-statistic title="项目总数" :value="statistics.totalProjects" />
</a-col>
<a-col :span="6">
<a-statistic title="在建项目" :value="statistics.ongoingProjects" />
</a-col>
<a-col :span="6">
<a-statistic title="客户数量" :value="statistics.clientCount" />
</a-col>
</a-row>
</a-card>
</div>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
//
const companyInfo = reactive({
name: '风电智能检测技术有限公司',
creditCode: '91110000123456789X',
legalPerson: '张三',
registeredCapital: '5000万元',
establishDate: '2018-03-15',
businessTerm: '2018-03-15至无固定期限',
registeredAddress: '北京市朝阳区科技园区创新大厦8层',
businessScope: '风力发电设备检测技术开发;技术咨询;技术服务;智能检测设备销售;工程技术咨询;专业技术服务',
phone: '010-12345678',
email: 'info@windtech.com',
website: 'https://www.windtech.com',
employeeCount: '158人'
})
//
const certificates = ref([
{
id: 1,
name: '高新技术企业证书',
description: '有效期2023-12-31',
image: '/api/placeholder/300/200',
issueDate: '2021-01-01',
expiryDate: '2023-12-31'
},
{
id: 2,
name: 'ISO9001质量管理体系认证',
description: '有效期2025-06-30',
image: '/api/placeholder/300/200',
issueDate: '2022-07-01',
expiryDate: '2025-06-30'
},
{
id: 3,
name: '安全生产许可证',
description: '有效期2024-12-31',
image: '/api/placeholder/300/200',
issueDate: '2021-01-01',
expiryDate: '2024-12-31'
}
])
//
const bankAccounts = ref([
{
id: 1,
bankName: '中国工商银行',
accountNumber: '1234567890123456789',
accountName: '风电智能检测技术有限公司',
accountType: '基本户',
isDefault: true
},
{
id: 2,
bankName: '中国建设银行',
accountNumber: '9876543210987654321',
accountName: '风电智能检测技术有限公司',
accountType: '一般户',
isDefault: false
}
])
//
const bankColumns: TableColumnData[] = [
{ title: '开户银行', dataIndex: 'bankName', width: 200 },
{ title: '银行账号', dataIndex: 'accountNumber', width: 200 },
{ title: '账户名称', dataIndex: 'accountName', width: 250 },
{ title: '账户类型', dataIndex: 'accountType', slotName: 'accountType', width: 120 },
{ title: '默认账户', dataIndex: 'isDefault', slotName: 'isDefault', width: 100 },
{ title: '操作', slotName: 'action', width: 200 }
]
//
const statistics = reactive({
annualRevenue: 12580,
totalProjects: 156,
ongoingProjects: 23,
clientCount: 68
})
//
const getBankTypeColor = (type: string) => {
const colorMap: Record<string, string> = {
'基本户': 'blue',
'一般户': 'green',
'专用户': 'orange'
}
return colorMap[type] || 'gray'
}
//
const editCompanyInfo = () => {
Message.info('编辑企业信息功能开发中...')
}
const addCertificate = () => {
Message.info('新增资质证书功能开发中...')
}
const viewCertificate = (cert: any) => {
Message.info(`查看证书: ${cert.name}`)
}
const editCertificate = (cert: any) => {
Message.info(`编辑证书: ${cert.name}`)
}
const deleteCertificate = (cert: any) => {
Message.info(`删除证书: ${cert.name}`)
}
const addBankAccount = () => {
Message.info('新增银行账户功能开发中...')
}
const editBankAccount = (account: any) => {
Message.info(`编辑银行账户: ${account.bankName}`)
}
const setDefaultAccount = (account: any) => {
Message.info(`设置默认账户: ${account.bankName}`)
}
const deleteBankAccount = (account: any) => {
Message.info(`删除银行账户: ${account.bankName}`)
}
</script>
<style scoped>
.enterprise-info-container {
padding: 16px;
}
.mb-6 {
margin-bottom: 24px;
}
.mb-4 {
margin-bottom: 16px;
}
</style>

View File

@ -0,0 +1,382 @@
<template>
<GiPageLayout>
<div class="data-migration-container">
<!-- 数据迁移概览 -->
<a-card title="数据迁移概览" class="mb-6">
<a-row :gutter="16">
<a-col :span="6">
<a-statistic title="已迁移数据量" :value="migrationStats.migratedData" suffix="GB" />
</a-col>
<a-col :span="6">
<a-statistic title="待迁移数据量" :value="migrationStats.pendingData" suffix="GB" />
</a-col>
<a-col :span="6">
<a-statistic title="迁移任务数" :value="migrationStats.totalTasks" />
</a-col>
<a-col :span="6">
<a-statistic title="成功率" :value="migrationStats.successRate" suffix="%" />
</a-col>
</a-row>
</a-card>
<!-- 数据迁移任务 -->
<a-card title="数据迁移任务" class="mb-6">
<template #extra>
<a-space>
<a-button type="primary" @click="createMigrationTask">
<template #icon><icon-plus /></template>
创建迁移任务
</a-button>
<a-button @click="importMigrationPlan">
<template #icon><icon-import /></template>
导入迁移计划
</a-button>
</a-space>
</template>
<GiTable
row-key="id"
:data="migrationTasks"
:columns="taskColumns"
:pagination="taskPagination"
:loading="tasksLoading"
>
<!-- 任务状态 -->
<template #status="{ record }">
<a-tag :color="getTaskStatusColor(record.status)">
{{ getTaskStatusText(record.status) }}
</a-tag>
</template>
<!-- 进度条 -->
<template #progress="{ record }">
<a-progress
:percent="record.progress"
:color="getProgressColor(record.progress)"
size="small"
/>
</template>
<!-- 数据量 -->
<template #dataSize="{ record }">
<span class="font-medium">{{ record.dataSize }} GB</span>
</template>
<!-- 操作 -->
<template #action="{ record }">
<a-space>
<a-link @click="viewTaskDetail(record)">详情</a-link>
<a-link
@click="startTask(record)"
v-if="record.status === 'pending'"
>
开始
</a-link>
<a-link
@click="pauseTask(record)"
v-if="record.status === 'running'"
>
暂停
</a-link>
<a-link
@click="resumeTask(record)"
v-if="record.status === 'paused'"
>
继续
</a-link>
<a-link @click="deleteTask(record)">删除</a-link>
</a-space>
</template>
</GiTable>
</a-card>
<!-- 数据源配置 -->
<a-card title="数据源配置">
<template #extra>
<a-button @click="addDataSource">
<template #icon><icon-plus /></template>
新增数据源
</a-button>
</template>
<GiTable
row-key="id"
:data="dataSources"
:columns="sourceColumns"
:pagination="false"
>
<!-- 数据源类型 -->
<template #sourceType="{ record }">
<a-tag :color="getSourceTypeColor(record.sourceType)">
{{ record.sourceType }}
</a-tag>
</template>
<!-- 连接状态 -->
<template #connectionStatus="{ record }">
<a-space>
<div
:class="record.isConnected ? 'online-dot' : 'offline-dot'"
class="status-dot"
></div>
{{ record.isConnected ? '已连接' : '未连接' }}
</a-space>
</template>
<!-- 操作 -->
<template #sourceAction="{ record }">
<a-space>
<a-link @click="testConnection(record)">测试连接</a-link>
<a-link @click="editDataSource(record)">编辑</a-link>
<a-link @click="deleteDataSource(record)">删除</a-link>
</a-space>
</template>
</GiTable>
</a-card>
</div>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
//
const migrationStats = reactive({
migratedData: 1256.8,
pendingData: 324.2,
totalTasks: 28,
successRate: 95.2
})
//
const migrationTasks = ref([
{
id: 1,
taskName: '用户数据迁移',
sourceSystem: '旧版CRM系统',
targetSystem: '新版管理系统',
dataSize: 125.6,
progress: 100,
status: 'completed',
createTime: '2024-03-01 10:00:00',
startTime: '2024-03-01 10:30:00',
endTime: '2024-03-01 12:45:00',
operator: '张管理员'
},
{
id: 2,
taskName: '项目数据迁移',
sourceSystem: '旧版项目系统',
targetSystem: '新版管理系统',
dataSize: 256.3,
progress: 75,
status: 'running',
createTime: '2024-03-10 09:00:00',
startTime: '2024-03-10 09:30:00',
endTime: '',
operator: '李管理员'
},
{
id: 3,
taskName: '财务数据迁移',
sourceSystem: '旧版财务系统',
targetSystem: '新版管理系统',
dataSize: 189.4,
progress: 0,
status: 'pending',
createTime: '2024-03-15 14:00:00',
startTime: '',
endTime: '',
operator: '王管理员'
}
])
//
const dataSources = ref([
{
id: 1,
sourceName: '旧版CRM数据库',
sourceType: 'MySQL',
host: '192.168.1.100',
port: 3306,
database: 'old_crm',
username: 'admin',
isConnected: true,
createTime: '2024-02-15 10:00:00'
},
{
id: 2,
sourceName: '旧版项目数据库',
sourceType: 'PostgreSQL',
host: '192.168.1.101',
port: 5432,
database: 'old_project',
username: 'admin',
isConnected: true,
createTime: '2024-02-20 11:30:00'
},
{
id: 3,
sourceName: '文件存储系统',
sourceType: 'FTP',
host: '192.168.1.102',
port: 21,
database: '/data/files',
username: 'fileuser',
isConnected: false,
createTime: '2024-03-01 09:15:00'
}
])
//
const taskColumns: TableColumnData[] = [
{ title: '任务名称', dataIndex: 'taskName', width: 200 },
{ title: '源系统', dataIndex: 'sourceSystem', width: 150 },
{ title: '目标系统', dataIndex: 'targetSystem', width: 150 },
{ title: '数据量', dataIndex: 'dataSize', slotName: 'dataSize', width: 100 },
{ title: '进度', dataIndex: 'progress', slotName: 'progress', width: 150 },
{ title: '状态', dataIndex: 'status', slotName: 'status', width: 100 },
{ title: '创建时间', dataIndex: 'createTime', width: 160 },
{ title: '操作人员', dataIndex: 'operator', width: 100 },
{ title: '操作', slotName: 'action', width: 200 }
]
//
const sourceColumns: TableColumnData[] = [
{ title: '数据源名称', dataIndex: 'sourceName', width: 200 },
{ title: '类型', dataIndex: 'sourceType', slotName: 'sourceType', width: 120 },
{ title: '主机地址', dataIndex: 'host', width: 150 },
{ title: '端口', dataIndex: 'port', width: 80 },
{ title: '数据库/路径', dataIndex: 'database', width: 200 },
{ title: '用户名', dataIndex: 'username', width: 120 },
{ title: '连接状态', dataIndex: 'connectionStatus', slotName: 'connectionStatus', width: 120 },
{ title: '创建时间', dataIndex: 'createTime', width: 160 },
{ title: '操作', slotName: 'sourceAction', width: 200 }
]
//
const taskPagination = reactive({
current: 1,
pageSize: 10,
total: 3,
showTotal: true
})
const tasksLoading = ref(false)
//
const getTaskStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
'pending': 'gray',
'running': 'blue',
'paused': 'orange',
'completed': 'green',
'failed': 'red'
}
return colorMap[status] || 'gray'
}
//
const getTaskStatusText = (status: string) => {
const textMap: Record<string, string> = {
'pending': '待开始',
'running': '运行中',
'paused': '已暂停',
'completed': '已完成',
'failed': '失败'
}
return textMap[status] || status
}
//
const getProgressColor = (progress: number) => {
if (progress >= 80) return '#52c41a'
if (progress >= 60) return '#1890ff'
if (progress >= 40) return '#faad14'
return '#ff4d4f'
}
//
const getSourceTypeColor = (type: string) => {
const colorMap: Record<string, string> = {
'MySQL': 'blue',
'PostgreSQL': 'green',
'Oracle': 'orange',
'FTP': 'purple',
'SFTP': 'cyan'
}
return colorMap[type] || 'gray'
}
//
const createMigrationTask = () => {
Message.info('创建迁移任务功能开发中...')
}
const importMigrationPlan = () => {
Message.info('导入迁移计划功能开发中...')
}
const viewTaskDetail = (task: any) => {
Message.info(`查看任务详情: ${task.taskName}`)
}
const startTask = (task: any) => {
Message.info(`开始任务: ${task.taskName}`)
}
const pauseTask = (task: any) => {
Message.info(`暂停任务: ${task.taskName}`)
}
const resumeTask = (task: any) => {
Message.info(`继续任务: ${task.taskName}`)
}
const deleteTask = (task: any) => {
Message.info(`删除任务: ${task.taskName}`)
}
const addDataSource = () => {
Message.info('新增数据源功能开发中...')
}
const testConnection = (source: any) => {
Message.info(`测试连接: ${source.sourceName}`)
}
const editDataSource = (source: any) => {
Message.info(`编辑数据源: ${source.sourceName}`)
}
const deleteDataSource = (source: any) => {
Message.info(`删除数据源: ${source.sourceName}`)
}
</script>
<style scoped>
.data-migration-container {
padding: 16px;
}
.mb-6 {
margin-bottom: 24px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
}
.online-dot {
background-color: #52c41a;
}
.offline-dot {
background-color: #d9d9d9;
}
</style>

View File

@ -0,0 +1,336 @@
<template>
<GiPageLayout>
<div class="version-upgrade-container">
<!-- 当前系统版本信息 -->
<a-card title="当前系统版本信息" class="mb-6">
<a-descriptions :column="2" bordered>
<a-descriptions-item label="系统名称">风电智能管理系统</a-descriptions-item>
<a-descriptions-item label="当前版本">v2.3.1</a-descriptions-item>
<a-descriptions-item label="发布日期">2024-02-15</a-descriptions-item>
<a-descriptions-item label="系统环境">生产环境</a-descriptions-item>
<a-descriptions-item label="数据库版本">MySQL 8.0.35</a-descriptions-item>
<a-descriptions-item label="运行时间">45</a-descriptions-item>
<a-descriptions-item label="许可证状态">
<a-tag color="green">已激活</a-tag>
</a-descriptions-item>
<a-descriptions-item label="技术支持">
<a-tag color="blue">有效至 2024-12-31</a-tag>
</a-descriptions-item>
</a-descriptions>
</a-card>
<!-- 版本更新提醒 -->
<a-card title="版本更新提醒" class="mb-6">
<template #extra>
<a-space>
<a-button @click="checkForUpdates">
<template #icon><icon-refresh /></template>
检查更新
</a-button>
<a-button type="primary" @click="configureAlerts">
<template #icon><icon-settings /></template>
配置提醒
</a-button>
</a-space>
</template>
<div v-if="latestVersion">
<a-alert
type="info"
:title="`发现新版本 ${latestVersion.version}`"
:description="latestVersion.description"
show-icon
closable
class="mb-4"
>
<template #action>
<a-space>
<a-button size="small" @click="viewChangelog">查看更新日志</a-button>
<a-button size="small" type="primary" @click="startUpgrade">立即升级</a-button>
</a-space>
</template>
</a-alert>
</div>
<div v-else>
<a-empty description="当前已是最新版本" />
</div>
</a-card>
<!-- 版本历史记录 -->
<a-card title="版本历史记录" class="mb-6">
<GiTable
row-key="id"
:data="versionHistory"
:columns="versionColumns"
:pagination="false"
>
<!-- 版本状态 -->
<template #status="{ record }">
<a-tag :color="getVersionStatusColor(record.status)">
{{ getVersionStatusText(record.status) }}
</a-tag>
</template>
<!-- 版本类型 -->
<template #versionType="{ record }">
<a-tag :color="getVersionTypeColor(record.versionType)">
{{ record.versionType }}
</a-tag>
</template>
<!-- 操作 -->
<template #action="{ record }">
<a-space>
<a-link @click="viewVersionDetail(record)">详情</a-link>
<a-link @click="downloadVersion(record)">下载</a-link>
<a-link
@click="rollbackVersion(record)"
v-if="record.status === 'installed' && record.id !== currentVersionId"
>
回滚
</a-link>
</a-space>
</template>
</GiTable>
</a-card>
<!-- 升级配置 -->
<a-card title="升级配置">
<a-form :model="upgradeConfig" layout="vertical">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="自动检查更新">
<a-switch v-model="upgradeConfig.autoCheck" />
<div class="text-gray-500 text-sm mt-1">启用后系统将定期自动检查版本更新</div>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="检查频率">
<a-select v-model="upgradeConfig.checkFrequency" :disabled="!upgradeConfig.autoCheck">
<a-option value="daily">每日</a-option>
<a-option value="weekly">每周</a-option>
<a-option value="monthly">每月</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="升级提醒方式">
<a-checkbox-group v-model="upgradeConfig.notificationMethods">
<a-checkbox value="email">邮件通知</a-checkbox>
<a-checkbox value="sms">短信通知</a-checkbox>
<a-checkbox value="system">系统通知</a-checkbox>
</a-checkbox-group>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="维护窗口">
<a-time-range-picker
v-model="upgradeConfig.maintenanceWindow"
format="HH:mm"
/>
<div class="text-gray-500 text-sm mt-1">系统升级的推荐时间窗口</div>
</a-form-item>
</a-col>
</a-row>
<a-form-item>
<a-space>
<a-button type="primary" @click="saveUpgradeConfig">保存配置</a-button>
<a-button @click="resetUpgradeConfig">重置</a-button>
</a-space>
</a-form-item>
</a-form>
</a-card>
</div>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
// ID
const currentVersionId = ref(3)
//
const latestVersion = ref({
version: 'v2.4.0',
description: '新增项目管理模块优化系统性能修复已知bug',
releaseDate: '2024-03-20',
size: '156.8 MB'
})
//
const versionHistory = ref([
{
id: 1,
version: 'v2.1.0',
versionType: '功能版本',
releaseDate: '2023-12-15',
installDate: '2023-12-20',
status: 'archived',
description: '初始版本发布,包含基础功能模块',
size: '128.5 MB'
},
{
id: 2,
version: 'v2.2.0',
versionType: '功能版本',
releaseDate: '2024-01-15',
installDate: '2024-01-20',
status: 'archived',
description: '新增用户管理和权限控制功能',
size: '142.3 MB'
},
{
id: 3,
version: 'v2.3.1',
versionType: '补丁版本',
releaseDate: '2024-02-15',
installDate: '2024-02-20',
status: 'current',
description: '修复安全漏洞,优化界面交互',
size: '145.7 MB'
},
{
id: 4,
version: 'v2.4.0',
versionType: '功能版本',
releaseDate: '2024-03-20',
installDate: '',
status: 'available',
description: '新增项目管理模块,优化系统性能',
size: '156.8 MB'
}
])
//
const versionColumns: TableColumnData[] = [
{ title: '版本号', dataIndex: 'version', width: 120 },
{ title: '版本类型', dataIndex: 'versionType', slotName: 'versionType', width: 120 },
{ title: '发布日期', dataIndex: 'releaseDate', width: 120 },
{ title: '安装日期', dataIndex: 'installDate', width: 120 },
{ title: '状态', dataIndex: 'status', slotName: 'status', width: 100 },
{ title: '文件大小', dataIndex: 'size', width: 100 },
{ title: '描述', dataIndex: 'description', width: 300, ellipsis: true, tooltip: true },
{ title: '操作', slotName: 'action', width: 200 }
]
//
const upgradeConfig = reactive({
autoCheck: true,
checkFrequency: 'weekly',
notificationMethods: ['email', 'system'],
maintenanceWindow: ['02:00', '06:00']
})
//
const getVersionStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
'current': 'green',
'available': 'blue',
'archived': 'gray',
'deprecated': 'red'
}
return colorMap[status] || 'gray'
}
//
const getVersionStatusText = (status: string) => {
const textMap: Record<string, string> = {
'current': '当前版本',
'available': '可升级',
'archived': '已归档',
'deprecated': '已弃用'
}
return textMap[status] || status
}
//
const getVersionTypeColor = (type: string) => {
const colorMap: Record<string, string> = {
'主要版本': 'red',
'功能版本': 'blue',
'补丁版本': 'green',
'热修复': 'orange'
}
return colorMap[type] || 'gray'
}
//
const checkForUpdates = () => {
Message.info('正在检查更新...')
setTimeout(() => {
Message.success('检查完成,发现新版本')
}, 2000)
}
const configureAlerts = () => {
Message.info('配置提醒功能开发中...')
}
const viewChangelog = () => {
Message.info('查看更新日志功能开发中...')
}
const startUpgrade = () => {
Message.info('立即升级功能开发中...')
}
const viewVersionDetail = (version: any) => {
Message.info(`查看版本详情: ${version.version}`)
}
const downloadVersion = (version: any) => {
Message.info(`下载版本: ${version.version}`)
}
const rollbackVersion = (version: any) => {
Message.info(`回滚到版本: ${version.version}`)
}
const saveUpgradeConfig = () => {
Message.success('升级配置已保存')
}
const resetUpgradeConfig = () => {
Object.assign(upgradeConfig, {
autoCheck: true,
checkFrequency: 'weekly',
notificationMethods: ['email', 'system'],
maintenanceWindow: ['02:00', '06:00']
})
Message.success('升级配置已重置')
}
</script>
<style scoped>
.version-upgrade-container {
padding: 16px;
}
.mb-6 {
margin-bottom: 24px;
}
.mb-4 {
margin-bottom: 16px;
}
.text-gray-500 {
color: #6b7280;
}
.text-sm {
font-size: 14px;
}
.mt-1 {
margin-top: 4px;
}
</style>

View File

@ -0,0 +1,241 @@
<template>
<GiPageLayout>
<GiTable
row-key="id"
title="考勤管理"
:data="dataList"
:columns="tableColumns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1200 }"
:pagination="pagination"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
@refresh="search"
>
<template #top>
<GiForm
v-model="searchForm"
search
:columns="queryFormColumns"
size="medium"
@search="search"
@reset="reset"
/>
</template>
<template #toolbar-left>
<a-button type="primary" @click="openAddModal">
<template #icon><icon-plus /></template>
<template #default>考勤统计</template>
</a-button>
</template>
<!-- 考勤状态 -->
<template #status="{ record }">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<!-- 操作列 -->
<template #action="{ record }">
<a-space>
<a-link @click="viewDetail(record)">详情</a-link>
<a-link @click="editRecord(record)">修改</a-link>
</a-space>
</template>
</GiTable>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
//
const searchForm = reactive({
userName: '',
deptName: '',
status: '',
attendanceDate: '',
page: 1,
size: 10
})
//
const queryFormColumns = [
{
field: 'userName',
label: '员工姓名',
type: 'input' as const,
props: {
placeholder: '请输入员工姓名'
}
},
{
field: 'deptName',
label: '部门',
type: 'input' as const,
props: {
placeholder: '请输入部门名称'
}
},
{
field: 'status',
label: '考勤状态',
type: 'select' as const,
props: {
placeholder: '请选择考勤状态',
options: [
{ label: '正常', value: 'normal' },
{ label: '迟到', value: 'late' },
{ label: '早退', value: 'early' },
{ label: '缺勤', value: 'absent' },
{ label: '请假', value: 'leave' }
]
}
}
]
//
const tableColumns: TableColumnData[] = [
{ title: '员工姓名', dataIndex: 'userName', width: 120 },
{ title: '员工工号', dataIndex: 'userCode', width: 120 },
{ title: '部门', dataIndex: 'deptName', width: 120 },
{ title: '考勤日期', dataIndex: 'attendanceDate', width: 120 },
{ title: '上班时间', dataIndex: 'startTime', width: 120 },
{ title: '下班时间', dataIndex: 'endTime', width: 120 },
{ title: '工作时长', dataIndex: 'workHours', width: 100 },
{ title: '考勤状态', dataIndex: 'status', slotName: 'status', width: 100 },
{ title: '备注', dataIndex: 'remark', width: 200, ellipsis: true, tooltip: true },
{ title: '操作', slotName: 'action', width: 120, fixed: 'right' }
]
//
const loading = ref(false)
const dataList = ref([
{
id: 1,
userName: '张三',
userCode: 'EMP001',
deptName: '技术部',
attendanceDate: '2024-01-15',
startTime: '09:00',
endTime: '18:00',
workHours: '8.0小时',
status: 'normal',
remark: ''
},
{
id: 2,
userName: '李四',
userCode: 'EMP002',
deptName: '技术部',
attendanceDate: '2024-01-15',
startTime: '09:15',
endTime: '18:00',
workHours: '7.75小时',
status: 'late',
remark: '迟到15分钟'
},
{
id: 3,
userName: '王五',
userCode: 'EMP003',
deptName: '市场部',
attendanceDate: '2024-01-15',
startTime: '09:00',
endTime: '17:30',
workHours: '7.5小时',
status: 'early',
remark: '早退30分钟'
}
])
const pagination = reactive({
current: 1,
pageSize: 10,
total: 3,
showTotal: true,
showPageSize: true
})
//
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
'normal': 'green',
'late': 'orange',
'early': 'blue',
'absent': 'red',
'leave': 'gray'
}
return colorMap[status] || 'gray'
}
//
const getStatusText = (status: string) => {
const textMap: Record<string, string> = {
'normal': '正常',
'late': '迟到',
'early': '早退',
'absent': '缺勤',
'leave': '请假'
}
return textMap[status] || status
}
//
const search = async () => {
loading.value = true
// API
setTimeout(() => {
loading.value = false
}, 1000)
}
const reset = () => {
Object.assign(searchForm, {
userName: '',
deptName: '',
status: '',
attendanceDate: '',
page: 1,
size: 10
})
pagination.current = 1
search()
}
//
const onPageChange = (page: number) => {
searchForm.page = page
pagination.current = page
search()
}
const onPageSizeChange = (size: number) => {
searchForm.size = size
searchForm.page = 1
pagination.pageSize = size
pagination.current = 1
search()
}
//
const openAddModal = () => {
Message.info('考勤统计功能开发中...')
}
const viewDetail = (record: any) => {
Message.info(`查看考勤详情: ${record.userName}`)
}
const editRecord = (record: any) => {
Message.info(`修改考勤记录: ${record.userName}`)
}
onMounted(() => {
search()
})
</script>

View File

@ -0,0 +1,309 @@
<template>
<GiPageLayout>
<GiTable
row-key="id"
title="责献积分制度、与企业共同发展"
:data="dataList"
:columns="tableColumns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1400 }"
:pagination="pagination"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
@refresh="search"
>
<template #top>
<GiForm
v-model="searchForm"
search
:columns="queryFormColumns"
size="medium"
@search="search"
@reset="reset"
/>
</template>
<template #toolbar-left>
<a-space>
<a-button type="primary" @click="openAddModal">
<template #icon><icon-plus /></template>
<template #default>积分记录</template>
</a-button>
<a-button @click="openRuleModal">
<template #icon><icon-settings /></template>
<template #default>积分规则</template>
</a-button>
</a-space>
</template>
<!-- 积分显示 -->
<template #currentPoints="{ record }">
<span class="font-medium text-blue-600">{{ record.currentPoints }}</span>
</template>
<!-- 积分等级 -->
<template #pointLevel="{ record }">
<a-tag :color="getLevelColor(record.pointLevel)">
{{ record.pointLevel }}
</a-tag>
</template>
<!-- 操作列 -->
<template #action="{ record }">
<a-space>
<a-link @click="viewDetail(record)">详情</a-link>
<a-link @click="viewHistory(record)">积分记录</a-link>
</a-space>
</template>
</GiTable>
<!-- 积分规则说明 -->
<a-card title="积分规则说明" class="mt-4" size="small">
<a-row :gutter="24">
<a-col :span="8">
<a-card title="获得积分" size="small">
<a-list size="small">
<a-list-item>项目按期完成+10</a-list-item>
<a-list-item>客户满意度优秀+8</a-list-item>
<a-list-item>技术创新贡献+15</a-list-item>
<a-list-item>团队协作优秀+5</a-list-item>
<a-list-item>培训新人+6</a-list-item>
</a-list>
</a-card>
</a-col>
<a-col :span="8">
<a-card title="扣除积分" size="small">
<a-list size="small">
<a-list-item>项目延期-5</a-list-item>
<a-list-item>客户投诉-10</a-list-item>
<a-list-item>违反规章制度-8</a-list-item>
<a-list-item>迟到早退-2</a-list-item>
<a-list-item>工作失误-3</a-list-item>
</a-list>
</a-card>
</a-col>
<a-col :span="8">
<a-card title="积分等级" size="small">
<a-list size="small">
<a-list-item><a-tag color="red">新手</a-tag> 0-50</a-list-item>
<a-list-item><a-tag color="orange">熟练</a-tag> 51-100</a-list-item>
<a-list-item><a-tag color="blue">专家</a-tag> 101-200</a-list-item>
<a-list-item><a-tag color="green">大师</a-tag> 201-300</a-list-item>
<a-list-item><a-tag color="purple">传奇</a-tag> 300</a-list-item>
</a-list>
</a-card>
</a-col>
</a-row>
</a-card>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
//
const searchForm = reactive({
userName: '',
deptName: '',
pointLevel: '',
page: 1,
size: 10
})
//
const queryFormColumns = [
{
field: 'userName',
label: '员工姓名',
type: 'input' as const,
props: {
placeholder: '请输入员工姓名'
}
},
{
field: 'deptName',
label: '部门',
type: 'input' as const,
props: {
placeholder: '请输入部门名称'
}
},
{
field: 'pointLevel',
label: '积分等级',
type: 'select' as const,
props: {
placeholder: '请选择积分等级',
options: [
{ label: '新手', value: '新手' },
{ label: '熟练', value: '熟练' },
{ label: '专家', value: '专家' },
{ label: '大师', value: '大师' },
{ label: '传奇', value: '传奇' }
]
}
}
]
//
const tableColumns: TableColumnData[] = [
{ title: '员工姓名', dataIndex: 'userName', width: 120 },
{ title: '员工工号', dataIndex: 'userCode', width: 120 },
{ title: '部门', dataIndex: 'deptName', width: 120 },
{ title: '岗位', dataIndex: 'position', width: 120 },
{ title: '当前积分', dataIndex: 'currentPoints', slotName: 'currentPoints', width: 120 },
{ title: '积分等级', dataIndex: 'pointLevel', slotName: 'pointLevel', width: 100 },
{ title: '本月获得', dataIndex: 'monthGain', width: 100 },
{ title: '本月扣除', dataIndex: 'monthDeduct', width: 100 },
{ title: '累计获得', dataIndex: 'totalGain', width: 100 },
{ title: '累计扣除', dataIndex: 'totalDeduct', width: 100 },
{ title: '最后更新', dataIndex: 'lastUpdate', width: 160 },
{ title: '操作', slotName: 'action', width: 150, fixed: 'right' }
]
//
const loading = ref(false)
const dataList = ref([
{
id: 1,
userName: '张三',
userCode: 'EMP001',
deptName: '技术部',
position: '前端工程师',
currentPoints: 285,
pointLevel: '大师',
monthGain: 25,
monthDeduct: 2,
totalGain: 320,
totalDeduct: 35,
lastUpdate: '2024-01-15 16:30:00'
},
{
id: 2,
userName: '李四',
userCode: 'EMP002',
deptName: '技术部',
position: '后端工程师',
currentPoints: 156,
pointLevel: '专家',
monthGain: 18,
monthDeduct: 0,
totalGain: 156,
totalDeduct: 0,
lastUpdate: '2024-01-14 14:20:00'
},
{
id: 3,
userName: '王五',
userCode: 'EMP003',
deptName: '市场部',
position: '市场专员',
currentPoints: 78,
pointLevel: '熟练',
monthGain: 12,
monthDeduct: 5,
totalGain: 95,
totalDeduct: 17,
lastUpdate: '2024-01-13 10:15:00'
},
{
id: 4,
userName: '赵六',
userCode: 'EMP004',
deptName: '技术部',
position: '架构师',
currentPoints: 350,
pointLevel: '传奇',
monthGain: 30,
monthDeduct: 0,
totalGain: 350,
totalDeduct: 0,
lastUpdate: '2024-01-15 18:00:00'
}
])
const pagination = reactive({
current: 1,
pageSize: 10,
total: 4,
showTotal: true,
showPageSize: true
})
//
const getLevelColor = (level: string) => {
const colorMap: Record<string, string> = {
'新手': 'red',
'熟练': 'orange',
'专家': 'blue',
'大师': 'green',
'传奇': 'purple'
}
return colorMap[level] || 'gray'
}
//
const search = async () => {
loading.value = true
// API
setTimeout(() => {
loading.value = false
}, 1000)
}
const reset = () => {
Object.assign(searchForm, {
userName: '',
deptName: '',
pointLevel: '',
page: 1,
size: 10
})
pagination.current = 1
search()
}
//
const onPageChange = (page: number) => {
searchForm.page = page
pagination.current = page
search()
}
const onPageSizeChange = (size: number) => {
searchForm.size = size
searchForm.page = 1
pagination.pageSize = size
pagination.current = 1
search()
}
//
const openAddModal = () => {
Message.info('积分记录功能开发中...')
}
const openRuleModal = () => {
Message.info('积分规则配置功能开发中...')
}
const viewDetail = (record: any) => {
Message.info(`查看积分详情: ${record.userName}`)
}
const viewHistory = (record: any) => {
Message.info(`查看积分记录: ${record.userName}`)
}
onMounted(() => {
search()
})
</script>
<style scoped>
.mt-4 {
margin-top: 16px;
}
</style>

View File

@ -0,0 +1,254 @@
<template>
<GiPageLayout>
<GiTable
row-key="id"
title="绩效管理"
:data="dataList"
:columns="tableColumns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1400 }"
:pagination="pagination"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
@refresh="search"
>
<template #top>
<GiForm
v-model="searchForm"
search
:columns="queryFormColumns"
size="medium"
@search="search"
@reset="reset"
/>
</template>
<template #toolbar-left>
<a-button type="primary" @click="openAddModal">
<template #icon><icon-plus /></template>
<template #default>绩效评估</template>
</a-button>
</template>
<!-- 绩效等级 -->
<template #performanceLevel="{ record }">
<a-tag :color="getLevelColor(record.performanceLevel)">
{{ record.performanceLevel }}
</a-tag>
</template>
<!-- 绩效分数 -->
<template #performanceScore="{ record }">
<span class="font-medium" :class="getScoreClass(record.performanceScore)">
{{ record.performanceScore }}
</span>
</template>
<!-- 操作列 -->
<template #action="{ record }">
<a-space>
<a-link @click="viewDetail(record)">详情</a-link>
<a-link @click="editRecord(record)">评估</a-link>
</a-space>
</template>
</GiTable>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
//
const searchForm = reactive({
userName: '',
deptName: '',
performanceLevel: '',
assessmentPeriod: '',
page: 1,
size: 10
})
//
const queryFormColumns = [
{
field: 'userName',
label: '员工姓名',
type: 'input' as const,
props: {
placeholder: '请输入员工姓名'
}
},
{
field: 'deptName',
label: '部门',
type: 'input' as const,
props: {
placeholder: '请输入部门名称'
}
},
{
field: 'performanceLevel',
label: '绩效等级',
type: 'select' as const,
props: {
placeholder: '请选择绩效等级',
options: [
{ label: '优秀', value: '优秀' },
{ label: '良好', value: '良好' },
{ label: '一般', value: '一般' },
{ label: '待改进', value: '待改进' }
]
}
}
]
//
const tableColumns: TableColumnData[] = [
{ title: '员工姓名', dataIndex: 'userName', width: 120 },
{ title: '员工工号', dataIndex: 'userCode', width: 120 },
{ title: '部门', dataIndex: 'deptName', width: 120 },
{ title: '岗位', dataIndex: 'position', width: 120 },
{ title: '考核周期', dataIndex: 'assessmentPeriod', width: 120 },
{ title: '绩效分数', dataIndex: 'performanceScore', slotName: 'performanceScore', width: 100 },
{ title: '绩效等级', dataIndex: 'performanceLevel', slotName: 'performanceLevel', width: 100 },
{ title: '工作目标完成度', dataIndex: 'goalCompletion', width: 130 },
{ title: '团队协作', dataIndex: 'teamwork', width: 100 },
{ title: '创新能力', dataIndex: 'innovation', width: 100 },
{ title: '评估人', dataIndex: 'assessor', width: 120 },
{ title: '评估时间', dataIndex: 'assessmentTime', width: 160 },
{ title: '操作', slotName: 'action', width: 120, fixed: 'right' }
]
//
const loading = ref(false)
const dataList = ref([
{
id: 1,
userName: '张三',
userCode: 'EMP001',
deptName: '技术部',
position: '前端工程师',
assessmentPeriod: '2024年第一季度',
performanceScore: 92,
performanceLevel: '优秀',
goalCompletion: '95%',
teamwork: '90分',
innovation: '88分',
assessor: '李经理',
assessmentTime: '2024-04-01 14:30:00'
},
{
id: 2,
userName: '李四',
userCode: 'EMP002',
deptName: '技术部',
position: '后端工程师',
assessmentPeriod: '2024年第一季度',
performanceScore: 85,
performanceLevel: '良好',
goalCompletion: '88%',
teamwork: '85分',
innovation: '82分',
assessor: '李经理',
assessmentTime: '2024-04-01 15:00:00'
},
{
id: 3,
userName: '王五',
userCode: 'EMP003',
deptName: '市场部',
position: '市场专员',
assessmentPeriod: '2024年第一季度',
performanceScore: 78,
performanceLevel: '一般',
goalCompletion: '75%',
teamwork: '80分',
innovation: '76分',
assessor: '张经理',
assessmentTime: '2024-04-02 10:00:00'
}
])
const pagination = reactive({
current: 1,
pageSize: 10,
total: 3,
showTotal: true,
showPageSize: true
})
//
const getLevelColor = (level: string) => {
const colorMap: Record<string, string> = {
'优秀': 'green',
'良好': 'blue',
'一般': 'orange',
'待改进': 'red'
}
return colorMap[level] || 'gray'
}
//
const getScoreClass = (score: number) => {
if (score >= 90) return 'text-green-600'
if (score >= 80) return 'text-blue-600'
if (score >= 70) return 'text-orange-600'
return 'text-red-600'
}
//
const search = async () => {
loading.value = true
// API
setTimeout(() => {
loading.value = false
}, 1000)
}
const reset = () => {
Object.assign(searchForm, {
userName: '',
deptName: '',
performanceLevel: '',
assessmentPeriod: '',
page: 1,
size: 10
})
pagination.current = 1
search()
}
//
const onPageChange = (page: number) => {
searchForm.page = page
pagination.current = page
search()
}
const onPageSizeChange = (size: number) => {
searchForm.size = size
searchForm.page = 1
pagination.pageSize = size
pagination.current = 1
search()
}
//
const openAddModal = () => {
Message.info('绩效评估功能开发中...')
}
const viewDetail = (record: any) => {
Message.info(`查看绩效详情: ${record.userName}`)
}
const editRecord = (record: any) => {
Message.info(`绩效评估: ${record.userName}`)
}
onMounted(() => {
search()
})
</script>

View File

@ -0,0 +1,292 @@
<template>
<GiPageLayout>
<GiTable
row-key="id"
title="工资管理"
:data="dataList"
:columns="tableColumns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1600 }"
:pagination="pagination"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
@refresh="search"
>
<template #top>
<GiForm
v-model="searchForm"
search
:columns="queryFormColumns"
size="medium"
@search="search"
@reset="reset"
/>
</template>
<template #toolbar-left>
<a-space>
<a-button type="primary" @click="openAddModal">
<template #icon><icon-plus /></template>
<template #default>工资核算</template>
</a-button>
<a-button @click="exportSalary">
<template #icon><icon-download /></template>
<template #default>导出工资单</template>
</a-button>
</a-space>
</template>
<!-- 基本工资 -->
<template #baseSalary="{ record }">
<span class="font-medium text-blue-600">{{ record.baseSalary.toLocaleString() }}</span>
</template>
<!-- 实发工资 -->
<template #netSalary="{ record }">
<span class="font-medium text-green-600">{{ record.netSalary.toLocaleString() }}</span>
</template>
<!-- 状态 -->
<template #status="{ record }">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<!-- 操作列 -->
<template #action="{ record }">
<a-space>
<a-link @click="viewDetail(record)">详情</a-link>
<a-link @click="editRecord(record)">编辑</a-link>
<a-link @click="confirmSalary(record)" v-if="record.status === 'draft'">确认</a-link>
</a-space>
</template>
</GiTable>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
//
const searchForm = reactive({
userName: '',
deptName: '',
salaryMonth: '',
status: '',
page: 1,
size: 10
})
//
const queryFormColumns = [
{
field: 'userName',
label: '员工姓名',
type: 'input' as const,
props: {
placeholder: '请输入员工姓名'
}
},
{
field: 'deptName',
label: '部门',
type: 'input' as const,
props: {
placeholder: '请输入部门名称'
}
},
{
field: 'status',
label: '状态',
type: 'select' as const,
props: {
placeholder: '请选择状态',
options: [
{ label: '草稿', value: 'draft' },
{ label: '已确认', value: 'confirmed' },
{ label: '已发放', value: 'paid' }
]
}
}
]
//
const tableColumns: TableColumnData[] = [
{ title: '员工姓名', dataIndex: 'userName', width: 120, fixed: 'left' },
{ title: '员工工号', dataIndex: 'userCode', width: 120 },
{ title: '部门', dataIndex: 'deptName', width: 120 },
{ title: '岗位', dataIndex: 'position', width: 120 },
{ title: '工资月份', dataIndex: 'salaryMonth', width: 100 },
{ title: '基本工资', dataIndex: 'baseSalary', slotName: 'baseSalary', width: 120 },
{ title: '岗位津贴', dataIndex: 'positionAllowance', width: 100 },
{ title: '绩效奖金', dataIndex: 'performanceBonus', width: 100 },
{ title: '加班费', dataIndex: 'overtimePay', width: 100 },
{ title: '其他补贴', dataIndex: 'otherAllowance', width: 100 },
{ title: '应发合计', dataIndex: 'grossSalary', width: 120 },
{ title: '个人所得税', dataIndex: 'personalTax', width: 100 },
{ title: '社保扣除', dataIndex: 'socialInsurance', width: 100 },
{ title: '公积金扣除', dataIndex: 'housingFund', width: 100 },
{ title: '其他扣除', dataIndex: 'otherDeduction', width: 100 },
{ title: '实发工资', dataIndex: 'netSalary', slotName: 'netSalary', width: 120 },
{ title: '状态', dataIndex: 'status', slotName: 'status', width: 100 },
{ title: '操作', slotName: 'action', width: 150, fixed: 'right' }
]
//
const loading = ref(false)
const dataList = ref([
{
id: 1,
userName: '张三',
userCode: 'EMP001',
deptName: '技术部',
position: '前端工程师',
salaryMonth: '2024-01',
baseSalary: 12000,
positionAllowance: 2000,
performanceBonus: 3000,
overtimePay: 800,
otherAllowance: 500,
grossSalary: 18300,
personalTax: 1245,
socialInsurance: 1200,
housingFund: 960,
otherDeduction: 0,
netSalary: 14895,
status: 'confirmed'
},
{
id: 2,
userName: '李四',
userCode: 'EMP002',
deptName: '技术部',
position: '后端工程师',
salaryMonth: '2024-01',
baseSalary: 11000,
positionAllowance: 1800,
performanceBonus: 2500,
overtimePay: 600,
otherAllowance: 300,
grossSalary: 16200,
personalTax: 975,
socialInsurance: 1100,
housingFund: 880,
otherDeduction: 0,
netSalary: 13245,
status: 'paid'
},
{
id: 3,
userName: '王五',
userCode: 'EMP003',
deptName: '市场部',
position: '市场专员',
salaryMonth: '2024-01',
baseSalary: 8000,
positionAllowance: 1000,
performanceBonus: 1500,
overtimePay: 400,
otherAllowance: 200,
grossSalary: 11100,
personalTax: 445,
socialInsurance: 800,
housingFund: 640,
otherDeduction: 0,
netSalary: 9215,
status: 'draft'
}
])
const pagination = reactive({
current: 1,
pageSize: 10,
total: 3,
showTotal: true,
showPageSize: true
})
//
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
'draft': 'orange',
'confirmed': 'blue',
'paid': 'green'
}
return colorMap[status] || 'gray'
}
//
const getStatusText = (status: string) => {
const textMap: Record<string, string> = {
'draft': '草稿',
'confirmed': '已确认',
'paid': '已发放'
}
return textMap[status] || status
}
//
const search = async () => {
loading.value = true
// API
setTimeout(() => {
loading.value = false
}, 1000)
}
const reset = () => {
Object.assign(searchForm, {
userName: '',
deptName: '',
salaryMonth: '',
status: '',
page: 1,
size: 10
})
pagination.current = 1
search()
}
//
const onPageChange = (page: number) => {
searchForm.page = page
pagination.current = page
search()
}
const onPageSizeChange = (size: number) => {
searchForm.size = size
searchForm.page = 1
pagination.pageSize = size
pagination.current = 1
search()
}
//
const openAddModal = () => {
Message.info('工资核算功能开发中...')
}
const exportSalary = () => {
Message.info('导出工资单功能开发中...')
}
const viewDetail = (record: any) => {
Message.info(`查看工资详情: ${record.userName}`)
}
const editRecord = (record: any) => {
Message.info(`编辑工资: ${record.userName}`)
}
const confirmSalary = (record: any) => {
Message.info(`确认工资: ${record.userName}`)
}
onMounted(() => {
search()
})
</script>

View File

@ -0,0 +1,182 @@
<template>
<GiPageLayout>
<GiTable
row-key="id"
title="工作量管理"
:data="dataList"
:columns="tableColumns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1200 }"
:pagination="pagination"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
@refresh="search"
>
<template #top>
<GiForm
v-model="searchForm"
search
:columns="queryFormColumns"
size="medium"
@search="search"
@reset="reset"
/>
</template>
<template #toolbar-left>
<a-button type="primary" @click="openAddModal">
<template #icon><icon-plus /></template>
<template #default>新增工作量记录</template>
</a-button>
</template>
<!-- 工作量显示 -->
<template #workload="{ record }">
<span class="font-medium text-blue-600">{{ record.workload }}小时</span>
</template>
<!-- 操作列 -->
<template #action="{ record }">
<a-space>
<a-link @click="editRecord(record)">编辑</a-link>
<a-link status="danger" @click="deleteRecord(record)">删除</a-link>
</a-space>
</template>
</GiTable>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
//
const searchForm = reactive({
userName: '',
projectName: '',
startDate: '',
endDate: '',
page: 1,
size: 10
})
//
const queryFormColumns = [
{
field: 'userName',
label: '员工姓名',
type: 'input' as const,
props: {
placeholder: '请输入员工姓名'
}
},
{
field: 'projectName',
label: '项目名称',
type: 'input' as const,
props: {
placeholder: '请输入项目名称'
}
}
]
//
const tableColumns: TableColumnData[] = [
{ title: '员工姓名', dataIndex: 'userName', width: 120 },
{ title: '部门', dataIndex: 'deptName', width: 120 },
{ title: '项目名称', dataIndex: 'projectName', width: 200, ellipsis: true, tooltip: true },
{ title: '工作内容', dataIndex: 'workContent', width: 250, ellipsis: true, tooltip: true },
{ title: '工作量(小时)', dataIndex: 'workload', slotName: 'workload', width: 120 },
{ title: '工作日期', dataIndex: 'workDate', width: 120 },
{ title: '创建时间', dataIndex: 'createTime', width: 160 },
{ title: '操作', slotName: 'action', width: 120, fixed: 'right' }
]
//
const loading = ref(false)
const dataList = ref([
{
id: 1,
userName: '张三',
deptName: '技术部',
projectName: '企业管理系统',
workContent: '前端开发',
workload: 8,
workDate: '2024-01-15',
createTime: '2024-01-15 10:30:00'
},
{
id: 2,
userName: '李四',
deptName: '技术部',
projectName: '移动端应用',
workContent: '后端接口开发',
workload: 6,
workDate: '2024-01-15',
createTime: '2024-01-15 11:20:00'
}
])
const pagination = reactive({
current: 1,
pageSize: 10,
total: 2,
showTotal: true,
showPageSize: true
})
//
const search = async () => {
loading.value = true
// API
setTimeout(() => {
loading.value = false
}, 1000)
}
const reset = () => {
Object.assign(searchForm, {
userName: '',
projectName: '',
startDate: '',
endDate: '',
page: 1,
size: 10
})
pagination.current = 1
search()
}
//
const onPageChange = (page: number) => {
searchForm.page = page
pagination.current = page
search()
}
const onPageSizeChange = (size: number) => {
searchForm.size = size
searchForm.page = 1
pagination.pageSize = size
pagination.current = 1
search()
}
//
const openAddModal = () => {
Message.info('新增工作量记录功能开发中...')
}
const editRecord = (record: any) => {
Message.info(`编辑工作量记录: ${record.userName}`)
}
const deleteRecord = (record: any) => {
Message.info(`删除工作量记录: ${record.userName}`)
}
onMounted(() => {
search()
})
</script>

View File

@ -0,0 +1,274 @@
<template>
<GiPageLayout>
<GiTable
row-key="id"
title="定制无人机"
:data="dataList"
:columns="tableColumns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1300 }"
:pagination="pagination"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
@refresh="search"
>
<template #top>
<GiForm
v-model="searchForm"
search
:columns="queryFormColumns"
size="medium"
@search="search"
@reset="reset"
/>
</template>
<template #toolbar-left>
<a-space>
<a-button type="primary" @click="openAddModal">
<template #icon><icon-plus /></template>
<template #default>新增无人机</template>
</a-button>
<a-button @click="openCustomModal">
<template #icon><icon-settings /></template>
<template #default>定制配置</template>
</a-button>
</a-space>
</template>
<!-- 无人机状态 -->
<template #status="{ record }">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<!-- 飞行时长 -->
<template #flightTime="{ record }">
<span class="font-medium text-blue-600">{{ record.flightTime }}小时</span>
</template>
<!-- 操作列 -->
<template #action="{ record }">
<a-space>
<a-link @click="viewDetail(record)">详情</a-link>
<a-link @click="editRecord(record)">编辑</a-link>
<a-link @click="flightRecord(record)">飞行记录</a-link>
<a-link @click="maintenance(record)">维护</a-link>
</a-space>
</template>
</GiTable>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
//
const searchForm = reactive({
droneName: '',
model: '',
status: '',
owner: '',
page: 1,
size: 10
})
//
const queryFormColumns = [
{
field: 'droneName',
label: '无人机名称',
type: 'input' as const,
props: {
placeholder: '请输入无人机名称'
}
},
{
field: 'model',
label: '型号',
type: 'input' as const,
props: {
placeholder: '请输入型号'
}
},
{
field: 'status',
label: '状态',
type: 'select' as const,
props: {
placeholder: '请选择状态',
options: [
{ label: '可用', value: 'available' },
{ label: '使用中', value: 'in_use' },
{ label: '维护中', value: 'maintenance' },
{ label: '故障', value: 'fault' }
]
}
}
]
//
const tableColumns: TableColumnData[] = [
{ title: '无人机编号', dataIndex: 'droneCode', width: 120 },
{ title: '无人机名称', dataIndex: 'droneName', width: 180, ellipsis: true, tooltip: true },
{ title: '型号规格', dataIndex: 'model', width: 120 },
{ title: '载重能力', dataIndex: 'payload', width: 100 },
{ title: '续航时间', dataIndex: 'endurance', width: 100 },
{ title: '最大飞行高度', dataIndex: 'maxAltitude', width: 120 },
{ title: '购买日期', dataIndex: 'purchaseDate', width: 120 },
{ title: '累计飞行时长', dataIndex: 'flightTime', slotName: 'flightTime', width: 120 },
{ title: '状态', dataIndex: 'status', slotName: 'status', width: 100 },
{ title: '负责人', dataIndex: 'owner', width: 100 },
{ title: '存放位置', dataIndex: 'location', width: 150, ellipsis: true, tooltip: true },
{ title: '备注', dataIndex: 'remark', width: 200, ellipsis: true, tooltip: true },
{ title: '操作', slotName: 'action', width: 200, fixed: 'right' }
]
//
const loading = ref(false)
const dataList = ref([
{
id: 1,
droneCode: 'DR001',
droneName: '风电叶片检测无人机',
model: 'M300RTK',
payload: '2.7kg',
endurance: '55分钟',
maxAltitude: '7000m',
purchaseDate: '2023-05-20',
flightTime: 156.5,
status: 'available',
owner: '李飞行员',
location: '设备仓库A区',
remark: '配备高清相机和热成像设备'
},
{
id: 2,
droneCode: 'DR002',
droneName: '塔筒巡检无人机',
model: 'M350RTK',
payload: '2.9kg',
endurance: '42分钟',
maxAltitude: '7000m',
purchaseDate: '2023-07-15',
flightTime: 89.2,
status: 'in_use',
owner: '王飞行员',
location: '现场作业点',
remark: '正在执行塔筒巡检任务'
},
{
id: 3,
droneCode: 'DR003',
droneName: '定制运输无人机',
model: 'Custom-T1000',
payload: '50kg',
endurance: '35分钟',
maxAltitude: '500m',
purchaseDate: '2023-09-10',
flightTime: 45.8,
status: 'maintenance',
owner: '张工程师',
location: '维修车间',
remark: '螺旋桨需要更换'
}
])
const pagination = reactive({
current: 1,
pageSize: 10,
total: 3,
showTotal: true,
showPageSize: true
})
//
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
'available': 'green',
'in_use': 'blue',
'maintenance': 'orange',
'fault': 'red'
}
return colorMap[status] || 'gray'
}
//
const getStatusText = (status: string) => {
const textMap: Record<string, string> = {
'available': '可用',
'in_use': '使用中',
'maintenance': '维护中',
'fault': '故障'
}
return textMap[status] || status
}
//
const search = async () => {
loading.value = true
setTimeout(() => {
loading.value = false
}, 1000)
}
const reset = () => {
Object.assign(searchForm, {
droneName: '',
model: '',
status: '',
owner: '',
page: 1,
size: 10
})
pagination.current = 1
search()
}
//
const onPageChange = (page: number) => {
searchForm.page = page
pagination.current = page
search()
}
const onPageSizeChange = (size: number) => {
searchForm.size = size
searchForm.page = 1
pagination.pageSize = size
pagination.current = 1
search()
}
//
const openAddModal = () => {
Message.info('新增无人机功能开发中...')
}
const openCustomModal = () => {
Message.info('定制配置功能开发中...')
}
const viewDetail = (record: any) => {
Message.info(`查看无人机详情: ${record.droneName}`)
}
const editRecord = (record: any) => {
Message.info(`编辑无人机: ${record.droneName}`)
}
const flightRecord = (record: any) => {
Message.info(`查看飞行记录: ${record.droneName}`)
}
const maintenance = (record: any) => {
Message.info(`维护记录: ${record.droneName}`)
}
onMounted(() => {
search()
})
</script>

View File

@ -0,0 +1,244 @@
<template>
<GiPageLayout>
<GiTable
row-key="id"
title="风电塔下监测系统"
:data="dataList"
:columns="tableColumns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1200 }"
:pagination="pagination"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
@refresh="search"
>
<template #top>
<GiForm
v-model="searchForm"
search
:columns="queryFormColumns"
size="medium"
@search="search"
@reset="reset"
/>
</template>
<template #toolbar-left>
<a-button type="primary" @click="openAddModal">
<template #icon><icon-plus /></template>
<template #default>添加设备</template>
</a-button>
</template>
<!-- 设备状态 -->
<template #status="{ record }">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<!-- 操作列 -->
<template #action="{ record }">
<a-space>
<a-link @click="viewDetail(record)">详情</a-link>
<a-link @click="editRecord(record)">编辑</a-link>
<a-link @click="viewData(record)">监测数据</a-link>
</a-space>
</template>
</GiTable>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
//
const searchForm = reactive({
deviceName: '',
location: '',
status: '',
page: 1,
size: 10
})
//
const queryFormColumns = [
{
field: 'deviceName',
label: '设备名称',
type: 'input' as const,
props: {
placeholder: '请输入设备名称'
}
},
{
field: 'location',
label: '安装位置',
type: 'input' as const,
props: {
placeholder: '请输入安装位置'
}
},
{
field: 'status',
label: '设备状态',
type: 'select' as const,
props: {
placeholder: '请选择设备状态',
options: [
{ label: '正常运行', value: 'normal' },
{ label: '故障', value: 'fault' },
{ label: '维护中', value: 'maintenance' },
{ label: '离线', value: 'offline' }
]
}
}
]
//
const tableColumns: TableColumnData[] = [
{ title: '设备编号', dataIndex: 'deviceCode', width: 120 },
{ title: '设备名称', dataIndex: 'deviceName', width: 200, ellipsis: true, tooltip: true },
{ title: '型号规格', dataIndex: 'model', width: 150 },
{ title: '安装位置', dataIndex: 'location', width: 200, ellipsis: true, tooltip: true },
{ title: '安装日期', dataIndex: 'installDate', width: 120 },
{ title: '设备状态', dataIndex: 'status', slotName: 'status', width: 100 },
{ title: '最后检测时间', dataIndex: 'lastCheckTime', width: 160 },
{ title: '负责人', dataIndex: 'manager', width: 100 },
{ title: '联系电话', dataIndex: 'phone', width: 120 },
{ title: '备注', dataIndex: 'remark', width: 200, ellipsis: true, tooltip: true },
{ title: '操作', slotName: 'action', width: 180, fixed: 'right' }
]
//
const loading = ref(false)
const dataList = ref([
{
id: 1,
deviceCode: 'TM001',
deviceName: '风电塔振动监测系统',
model: 'VM-2000',
location: '1号风机塔筒基础',
installDate: '2023-06-15',
status: 'normal',
lastCheckTime: '2024-01-15 10:30:00',
manager: '张工程师',
phone: '13800138001',
remark: '运行正常,数据传输稳定'
},
{
id: 2,
deviceCode: 'TM002',
deviceName: '塔筒应力监测设备',
model: 'SM-1500',
location: '2号风机塔筒中部',
installDate: '2023-07-10',
status: 'fault',
lastCheckTime: '2024-01-14 15:20:00',
manager: '李工程师',
phone: '13800138002',
remark: '传感器故障,需要更换'
},
{
id: 3,
deviceCode: 'TM003',
deviceName: '基础沉降监测系统',
model: 'FM-800',
location: '3号风机基础',
installDate: '2023-08-05',
status: 'maintenance',
lastCheckTime: '2024-01-13 09:15:00',
manager: '王工程师',
phone: '13800138003',
remark: '定期维护中'
}
])
const pagination = reactive({
current: 1,
pageSize: 10,
total: 3,
showTotal: true,
showPageSize: true
})
//
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
'normal': 'green',
'fault': 'red',
'maintenance': 'orange',
'offline': 'gray'
}
return colorMap[status] || 'gray'
}
//
const getStatusText = (status: string) => {
const textMap: Record<string, string> = {
'normal': '正常运行',
'fault': '故障',
'maintenance': '维护中',
'offline': '离线'
}
return textMap[status] || status
}
//
const search = async () => {
loading.value = true
setTimeout(() => {
loading.value = false
}, 1000)
}
const reset = () => {
Object.assign(searchForm, {
deviceName: '',
location: '',
status: '',
page: 1,
size: 10
})
pagination.current = 1
search()
}
//
const onPageChange = (page: number) => {
searchForm.page = page
pagination.current = page
search()
}
const onPageSizeChange = (size: number) => {
searchForm.size = size
searchForm.page = 1
pagination.pageSize = size
pagination.current = 1
search()
}
//
const openAddModal = () => {
Message.info('添加设备功能开发中...')
}
const viewDetail = (record: any) => {
Message.info(`查看设备详情: ${record.deviceName}`)
}
const editRecord = (record: any) => {
Message.info(`编辑设备: ${record.deviceName}`)
}
const viewData = (record: any) => {
Message.info(`查看监测数据: ${record.deviceName}`)
}
onMounted(() => {
search()
})
</script>

View File

@ -0,0 +1,282 @@
<template>
<GiPageLayout>
<GiTable
row-key="id"
title="中标通知书管理"
:data="dataList"
:columns="tableColumns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1500 }"
:pagination="pagination"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
@refresh="search"
>
<template #top>
<GiForm
v-model="searchForm"
search
:columns="queryFormColumns"
size="medium"
@search="search"
@reset="reset"
/>
</template>
<template #toolbar-left>
<a-space>
<a-button type="primary" @click="generateNotice">
<template #icon><icon-plus /></template>
<template #default>生成通知书</template>
</a-button>
<a-button @click="batchSend">
<template #icon><icon-send /></template>
<template #default>批量发送</template>
</a-button>
</a-space>
</template>
<!-- 发送状态 -->
<template #sendStatus="{ record }">
<a-tag :color="getSendStatusColor(record.sendStatus)">
{{ getSendStatusText(record.sendStatus) }}
</a-tag>
</template>
<!-- 中标金额 -->
<template #winningAmount="{ record }">
<span class="font-medium text-green-600">{{ record.winningAmount.toLocaleString() }}</span>
</template>
<!-- 操作列 -->
<template #action="{ record }">
<a-space>
<a-link @click="viewDetail(record)">查看</a-link>
<a-link @click="downloadNotice(record)">下载</a-link>
<a-link @click="sendNotice(record)" v-if="record.sendStatus === 'not_sent'">发送</a-link>
<a-link @click="signContract(record)" v-if="record.sendStatus === 'sent'">签署合同</a-link>
</a-space>
</template>
</GiTable>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
//
const searchForm = reactive({
projectName: '',
noticeCode: '',
sendStatus: '',
noticeDate: '',
page: 1,
size: 10
})
//
const queryFormColumns = [
{
field: 'projectName',
label: '项目名称',
type: 'input' as const,
props: {
placeholder: '请输入项目名称'
}
},
{
field: 'noticeCode',
label: '通知书编号',
type: 'input' as const,
props: {
placeholder: '请输入通知书编号'
}
},
{
field: 'sendStatus',
label: '发送状态',
type: 'select' as const,
props: {
placeholder: '请选择发送状态',
options: [
{ label: '未发送', value: 'not_sent' },
{ label: '已发送', value: 'sent' },
{ label: '已确认', value: 'confirmed' },
{ label: '已签约', value: 'contracted' }
]
}
}
]
//
const tableColumns: TableColumnData[] = [
{ title: '通知书编号', dataIndex: 'noticeCode', width: 150 },
{ title: '项目名称', dataIndex: 'projectName', width: 300, ellipsis: true, tooltip: true },
{ title: '中标单位', dataIndex: 'winningCompany', width: 200, ellipsis: true, tooltip: true },
{ title: '招标单位', dataIndex: 'tenderUnit', width: 200, ellipsis: true, tooltip: true },
{ title: '中标金额', dataIndex: 'winningAmount', slotName: 'winningAmount', width: 120 },
{ title: '工期要求', dataIndex: 'duration', width: 100 },
{ title: '通知日期', dataIndex: 'noticeDate', width: 120 },
{ title: '有效期', dataIndex: 'validUntil', width: 120 },
{ title: '发送状态', dataIndex: 'sendStatus', slotName: 'sendStatus', width: 100 },
{ title: '发送时间', dataIndex: 'sendTime', width: 160 },
{ title: '确认时间', dataIndex: 'confirmTime', width: 160 },
{ title: '联系人', dataIndex: 'contactPerson', width: 100 },
{ title: '联系电话', dataIndex: 'contactPhone', width: 120 },
{ title: '备注', dataIndex: 'remark', width: 200, ellipsis: true, tooltip: true },
{ title: '操作', slotName: 'action', width: 200, fixed: 'right' }
]
//
const loading = ref(false)
const dataList = ref([
{
id: 1,
noticeCode: 'WN2024001',
projectName: '华能新能源风电场叶片检测服务项目',
winningCompany: '我公司',
tenderUnit: '华能新能源股份有限公司',
winningAmount: 320,
duration: '60天',
noticeDate: '2024-02-16',
validUntil: '2024-02-26',
sendStatus: 'contracted',
sendTime: '2024-02-16 14:30:00',
confirmTime: '2024-02-17 10:15:00',
contactPerson: '李经理',
contactPhone: '13800138001',
remark: '已成功签署合同'
},
{
id: 2,
noticeCode: 'WN2024002',
projectName: '大唐风电场防雷检测项目',
winningCompany: '我公司',
tenderUnit: '大唐新能源股份有限公司',
winningAmount: 268,
duration: '45天',
noticeDate: '2024-02-21',
validUntil: '2024-03-03',
sendStatus: 'confirmed',
sendTime: '2024-02-21 09:00:00',
confirmTime: '2024-02-22 11:30:00',
contactPerson: '王经理',
contactPhone: '13800138002',
remark: '等待签署正式合同'
},
{
id: 3,
noticeCode: 'WN2024003',
projectName: '国电投风电场设备检修项目',
winningCompany: '我公司',
tenderUnit: '国家电力投资集团',
winningAmount: 450,
duration: '30天',
noticeDate: '2024-02-28',
validUntil: '2024-03-10',
sendStatus: 'sent',
sendTime: '2024-02-28 16:45:00',
confirmTime: '',
contactPerson: '张经理',
contactPhone: '13800138003',
remark: '已发送,等待对方确认'
}
])
const pagination = reactive({
current: 1,
pageSize: 10,
total: 3,
showTotal: true,
showPageSize: true
})
//
const getSendStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
'not_sent': 'gray',
'sent': 'blue',
'confirmed': 'orange',
'contracted': 'green'
}
return colorMap[status] || 'gray'
}
//
const getSendStatusText = (status: string) => {
const textMap: Record<string, string> = {
'not_sent': '未发送',
'sent': '已发送',
'confirmed': '已确认',
'contracted': '已签约'
}
return textMap[status] || status
}
//
const search = async () => {
loading.value = true
setTimeout(() => {
loading.value = false
}, 1000)
}
const reset = () => {
Object.assign(searchForm, {
projectName: '',
noticeCode: '',
sendStatus: '',
noticeDate: '',
page: 1,
size: 10
})
pagination.current = 1
search()
}
//
const onPageChange = (page: number) => {
searchForm.page = page
pagination.current = page
search()
}
const onPageSizeChange = (size: number) => {
searchForm.size = size
searchForm.page = 1
pagination.pageSize = size
pagination.current = 1
search()
}
//
const generateNotice = () => {
Message.info('生成通知书功能开发中...')
}
const batchSend = () => {
Message.info('批量发送功能开发中...')
}
const viewDetail = (record: any) => {
Message.info(`查看中标通知书: ${record.projectName}`)
}
const downloadNotice = (record: any) => {
Message.info(`下载中标通知书: ${record.projectName}`)
}
const sendNotice = (record: any) => {
Message.info(`发送中标通知书: ${record.projectName}`)
}
const signContract = (record: any) => {
Message.info(`签署合同: ${record.projectName}`)
}
onMounted(() => {
search()
})
</script>

View File

@ -0,0 +1,285 @@
<template>
<GiPageLayout>
<GiTable
row-key="id"
title="投标文件管理"
:data="dataList"
:columns="tableColumns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1500 }"
:pagination="pagination"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
@refresh="search"
>
<template #top>
<GiForm
v-model="searchForm"
search
:columns="queryFormColumns"
size="medium"
@search="search"
@reset="reset"
/>
</template>
<template #toolbar-left>
<a-space>
<a-button type="primary" @click="openAddModal">
<template #icon><icon-plus /></template>
<template #default>新建投标</template>
</a-button>
<a-button @click="generateProposal">
<template #icon><icon-file-text /></template>
<template #default>生成标书</template>
</a-button>
</a-space>
</template>
<!-- 投标状态 -->
<template #status="{ record }">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<!-- 投标金额 -->
<template #bidAmount="{ record }">
<span class="font-medium text-blue-600">{{ record.bidAmount.toLocaleString() }}</span>
</template>
<!-- 操作列 -->
<template #action="{ record }">
<a-space>
<a-link @click="viewDetail(record)">详情</a-link>
<a-link @click="editRecord(record)" v-if="record.status === 'draft'">编辑</a-link>
<a-link @click="submitBid(record)" v-if="record.status === 'draft'">提交</a-link>
<a-link @click="downloadBid(record)">下载</a-link>
</a-space>
</template>
</GiTable>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
//
const searchForm = reactive({
projectName: '',
bidCode: '',
status: '',
submitDate: '',
page: 1,
size: 10
})
//
const queryFormColumns = [
{
field: 'projectName',
label: '项目名称',
type: 'input' as const,
props: {
placeholder: '请输入项目名称'
}
},
{
field: 'bidCode',
label: '投标编号',
type: 'input' as const,
props: {
placeholder: '请输入投标编号'
}
},
{
field: 'status',
label: '投标状态',
type: 'select' as const,
props: {
placeholder: '请选择投标状态',
options: [
{ label: '草稿', value: 'draft' },
{ label: '已提交', value: 'submitted' },
{ label: '中标', value: 'won' },
{ label: '未中标', value: 'lost' },
{ label: '已撤回', value: 'withdrawn' }
]
}
}
]
//
const tableColumns: TableColumnData[] = [
{ title: '投标编号', dataIndex: 'bidCode', width: 150 },
{ title: '招标编号', dataIndex: 'tenderCode', width: 150 },
{ title: '项目名称', dataIndex: 'projectName', width: 250, ellipsis: true, tooltip: true },
{ title: '招标单位', dataIndex: 'tenderUnit', width: 200, ellipsis: true, tooltip: true },
{ title: '投标金额', dataIndex: 'bidAmount', slotName: 'bidAmount', width: 120 },
{ title: '工期(天)', dataIndex: 'duration', width: 100 },
{ title: '技术评分', dataIndex: 'techScore', width: 100 },
{ title: '商务评分', dataIndex: 'commercialScore', width: 100 },
{ title: '提交时间', dataIndex: 'submitTime', width: 160 },
{ title: '开标时间', dataIndex: 'openTime', width: 160 },
{ title: '投标状态', dataIndex: 'status', slotName: 'status', width: 100 },
{ title: '排名', dataIndex: 'ranking', width: 80 },
{ title: '负责人', dataIndex: 'manager', width: 100 },
{ title: '备注', dataIndex: 'remark', width: 200, ellipsis: true, tooltip: true },
{ title: '操作', slotName: 'action', width: 200, fixed: 'right' }
]
//
const loading = ref(false)
const dataList = ref([
{
id: 1,
bidCode: 'BD2024001',
tenderCode: 'TD2024001',
projectName: '华能新能源风电场叶片检测服务项目',
tenderUnit: '华能新能源股份有限公司',
bidAmount: 320,
duration: 60,
techScore: 92,
commercialScore: 89,
submitTime: '2024-02-09 17:30:00',
openTime: '2024-02-15 09:00:00',
status: 'won',
ranking: 1,
manager: '张项目经理',
remark: '成功中标,技术方案优秀'
},
{
id: 2,
bidCode: 'BD2024002',
tenderCode: 'TD2024002',
projectName: '大唐风电场防雷检测项目',
tenderUnit: '大唐新能源股份有限公司',
bidAmount: 268,
duration: 45,
techScore: 85,
commercialScore: 88,
submitTime: '2024-02-14 16:45:00',
openTime: '2024-02-20 10:00:00',
status: 'submitted',
ranking: 0,
manager: '李项目经理',
remark: '已提交投标文件,等待开标'
},
{
id: 3,
bidCode: 'BD2024003',
tenderCode: 'TD2024003',
projectName: '国电投海上风电智能监测系统',
tenderUnit: '国家电力投资集团',
bidAmount: 1150,
duration: 120,
techScore: 88,
commercialScore: 85,
submitTime: '2024-01-24 18:00:00',
openTime: '2024-01-30 14:00:00',
status: 'lost',
ranking: 3,
manager: '王项目经理',
remark: '未中标,技术方案需要完善'
}
])
const pagination = reactive({
current: 1,
pageSize: 10,
total: 3,
showTotal: true,
showPageSize: true
})
//
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
'draft': 'gray',
'submitted': 'blue',
'won': 'green',
'lost': 'red',
'withdrawn': 'orange'
}
return colorMap[status] || 'gray'
}
//
const getStatusText = (status: string) => {
const textMap: Record<string, string> = {
'draft': '草稿',
'submitted': '已提交',
'won': '中标',
'lost': '未中标',
'withdrawn': '已撤回'
}
return textMap[status] || status
}
//
const search = async () => {
loading.value = true
setTimeout(() => {
loading.value = false
}, 1000)
}
const reset = () => {
Object.assign(searchForm, {
projectName: '',
bidCode: '',
status: '',
submitDate: '',
page: 1,
size: 10
})
pagination.current = 1
search()
}
//
const onPageChange = (page: number) => {
searchForm.page = page
pagination.current = page
search()
}
const onPageSizeChange = (size: number) => {
searchForm.size = size
searchForm.page = 1
pagination.pageSize = size
pagination.current = 1
search()
}
//
const openAddModal = () => {
Message.info('新建投标功能开发中...')
}
const generateProposal = () => {
Message.info('生成标书功能开发中...')
}
const viewDetail = (record: any) => {
Message.info(`查看投标详情: ${record.projectName}`)
}
const editRecord = (record: any) => {
Message.info(`编辑投标文件: ${record.projectName}`)
}
const submitBid = (record: any) => {
Message.info(`提交投标: ${record.projectName}`)
}
const downloadBid = (record: any) => {
Message.info(`下载投标文件: ${record.projectName}`)
}
onMounted(() => {
search()
})
</script>

View File

@ -0,0 +1,277 @@
<template>
<GiPageLayout>
<GiTable
row-key="id"
title="招标文件管理"
:data="dataList"
:columns="tableColumns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1400 }"
:pagination="pagination"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
@refresh="search"
>
<template #top>
<GiForm
v-model="searchForm"
search
:columns="queryFormColumns"
size="medium"
@search="search"
@reset="reset"
/>
</template>
<template #toolbar-left>
<a-space>
<a-button type="primary" @click="openAddModal">
<template #icon><icon-plus /></template>
<template #default>新增招标文件</template>
</a-button>
<a-button @click="batchDownload">
<template #icon><icon-download /></template>
<template #default>批量下载</template>
</a-button>
</a-space>
</template>
<!-- 状态显示 -->
<template #status="{ record }">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<!-- 预算金额 -->
<template #budget="{ record }">
<span class="font-medium text-green-600">{{ record.budget.toLocaleString() }}</span>
</template>
<!-- 操作列 -->
<template #action="{ record }">
<a-space>
<a-link @click="viewDetail(record)">查看</a-link>
<a-link @click="downloadFile(record)">下载</a-link>
<a-link @click="editRecord(record)" v-if="record.status === 'draft'">编辑</a-link>
<a-link @click="publishTender(record)" v-if="record.status === 'draft'">发布</a-link>
</a-space>
</template>
</GiTable>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
//
const searchForm = reactive({
projectName: '',
tenderCode: '',
status: '',
publishDate: '',
page: 1,
size: 10
})
//
const queryFormColumns = [
{
field: 'projectName',
label: '项目名称',
type: 'input' as const,
props: {
placeholder: '请输入项目名称'
}
},
{
field: 'tenderCode',
label: '招标编号',
type: 'input' as const,
props: {
placeholder: '请输入招标编号'
}
},
{
field: 'status',
label: '状态',
type: 'select' as const,
props: {
placeholder: '请选择状态',
options: [
{ label: '草稿', value: 'draft' },
{ label: '已发布', value: 'published' },
{ label: '投标中', value: 'bidding' },
{ label: '已截止', value: 'closed' },
{ label: '已废标', value: 'cancelled' }
]
}
}
]
//
const tableColumns: TableColumnData[] = [
{ title: '招标编号', dataIndex: 'tenderCode', width: 150 },
{ title: '项目名称', dataIndex: 'projectName', width: 250, ellipsis: true, tooltip: true },
{ title: '招标单位', dataIndex: 'tenderUnit', width: 200, ellipsis: true, tooltip: true },
{ title: '项目类型', dataIndex: 'projectType', width: 120 },
{ title: '预算金额', dataIndex: 'budget', slotName: 'budget', width: 120 },
{ title: '发布日期', dataIndex: 'publishDate', width: 120 },
{ title: '截止日期', dataIndex: 'deadline', width: 120 },
{ title: '开标日期', dataIndex: 'openDate', width: 120 },
{ title: '状态', dataIndex: 'status', slotName: 'status', width: 100 },
{ title: '报名企业', dataIndex: 'registeredCompanies', width: 100 },
{ title: '联系人', dataIndex: 'contactPerson', width: 100 },
{ title: '联系电话', dataIndex: 'contactPhone', width: 120 },
{ title: '操作', slotName: 'action', width: 200, fixed: 'right' }
]
//
const loading = ref(false)
const dataList = ref([
{
id: 1,
tenderCode: 'TD2024001',
projectName: '华能新能源风电场叶片检测服务项目',
tenderUnit: '华能新能源股份有限公司',
projectType: '技术服务',
budget: 350,
publishDate: '2024-01-10',
deadline: '2024-02-10',
openDate: '2024-02-15',
status: 'bidding',
registeredCompanies: 15,
contactPerson: '李经理',
contactPhone: '13800138001'
},
{
id: 2,
tenderCode: 'TD2024002',
projectName: '大唐风电场防雷检测项目',
tenderUnit: '大唐新能源股份有限公司',
projectType: '检测服务',
budget: 280,
publishDate: '2024-01-15',
deadline: '2024-02-15',
openDate: '2024-02-20',
status: 'published',
registeredCompanies: 8,
contactPerson: '王经理',
contactPhone: '13800138002'
},
{
id: 3,
tenderCode: 'TD2024003',
projectName: '国电投海上风电智能监测系统',
tenderUnit: '国家电力投资集团',
projectType: '设备采购',
budget: 1200,
publishDate: '2024-01-05',
deadline: '2024-01-25',
openDate: '2024-01-30',
status: 'closed',
registeredCompanies: 22,
contactPerson: '张经理',
contactPhone: '13800138003'
}
])
const pagination = reactive({
current: 1,
pageSize: 10,
total: 3,
showTotal: true,
showPageSize: true
})
//
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
'draft': 'gray',
'published': 'blue',
'bidding': 'green',
'closed': 'orange',
'cancelled': 'red'
}
return colorMap[status] || 'gray'
}
//
const getStatusText = (status: string) => {
const textMap: Record<string, string> = {
'draft': '草稿',
'published': '已发布',
'bidding': '投标中',
'closed': '已截止',
'cancelled': '已废标'
}
return textMap[status] || status
}
//
const search = async () => {
loading.value = true
setTimeout(() => {
loading.value = false
}, 1000)
}
const reset = () => {
Object.assign(searchForm, {
projectName: '',
tenderCode: '',
status: '',
publishDate: '',
page: 1,
size: 10
})
pagination.current = 1
search()
}
//
const onPageChange = (page: number) => {
searchForm.page = page
pagination.current = page
search()
}
const onPageSizeChange = (size: number) => {
searchForm.size = size
searchForm.page = 1
pagination.pageSize = size
pagination.current = 1
search()
}
//
const openAddModal = () => {
Message.info('新增招标文件功能开发中...')
}
const batchDownload = () => {
Message.info('批量下载功能开发中...')
}
const viewDetail = (record: any) => {
Message.info(`查看招标详情: ${record.projectName}`)
}
const downloadFile = (record: any) => {
Message.info(`下载招标文件: ${record.projectName}`)
}
const editRecord = (record: any) => {
Message.info(`编辑招标文件: ${record.projectName}`)
}
const publishTender = (record: any) => {
Message.info(`发布招标: ${record.projectName}`)
}
onMounted(() => {
search()
})
</script>

View File

@ -0,0 +1,328 @@
<template>
<GiPageLayout>
<GiTable
row-key="id"
title="成本管理"
:data="dataList"
:columns="tableColumns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1700 }"
:pagination="pagination"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
@refresh="search"
>
<template #top>
<GiForm
v-model="searchForm"
search
:columns="queryFormColumns"
size="medium"
@search="search"
@reset="reset"
/>
</template>
<template #toolbar-left>
<a-space>
<a-button type="primary" @click="openCostModal">
<template #icon><icon-plus /></template>
<template #default>新增成本</template>
</a-button>
<a-button @click="analyseCost">
<template #icon><icon-line-chart /></template>
<template #default>成本分析</template>
</a-button>
<a-button @click="exportCost">
<template #icon><icon-download /></template>
<template #default>导出数据</template>
</a-button>
</a-space>
</template>
<!-- 成本类型 -->
<template #costType="{ record }">
<a-tag :color="getCostTypeColor(record.costType)">
{{ record.costType }}
</a-tag>
</template>
<!-- 实际成本 -->
<template #actualCost="{ record }">
<span class="font-medium text-red-600">{{ record.actualCost.toLocaleString() }}</span>
</template>
<!-- 预算成本 -->
<template #budgetCost="{ record }">
<span class="font-medium text-blue-600">{{ record.budgetCost.toLocaleString() }}</span>
</template>
<!-- 成本差异 -->
<template #costVariance="{ record }">
<span :class="getCostVarianceClass(record.costVariance)">
{{ record.costVariance > 0 ? '+' : '' }}{{ record.costVariance.toLocaleString() }}
</span>
</template>
<!-- 操作列 -->
<template #action="{ record }">
<a-space>
<a-link @click="viewDetail(record)">详情</a-link>
<a-link @click="editRecord(record)">编辑</a-link>
<a-link @click="viewBill(record)">单据</a-link>
<a-link @click="auditCost(record)">审核</a-link>
</a-space>
</template>
</GiTable>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
//
const searchForm = reactive({
projectName: '',
costType: '',
costCategory: '',
costPeriod: '',
page: 1,
size: 10
})
//
const queryFormColumns = [
{
field: 'projectName',
label: '项目名称',
type: 'input' as const,
props: {
placeholder: '请输入项目名称'
}
},
{
field: 'costType',
label: '成本类型',
type: 'select' as const,
props: {
placeholder: '请选择成本类型',
options: [
{ label: '人工成本', value: '人工成本' },
{ label: '材料成本', value: '材料成本' },
{ label: '设备成本', value: '设备成本' },
{ label: '差旅成本', value: '差旅成本' },
{ label: '管理成本', value: '管理成本' },
{ label: '其他成本', value: '其他成本' }
]
}
},
{
field: 'costCategory',
label: '费用类别',
type: 'select' as const,
props: {
placeholder: '请选择费用类别',
options: [
{ label: '直接成本', value: '直接成本' },
{ label: '间接成本', value: '间接成本' }
]
}
}
]
//
const tableColumns: TableColumnData[] = [
{ title: '项目名称', dataIndex: 'projectName', width: 250, ellipsis: true, tooltip: true },
{ title: '成本类型', dataIndex: 'costType', slotName: 'costType', width: 100 },
{ title: '费用类别', dataIndex: 'costCategory', width: 100 },
{ title: '成本科目', dataIndex: 'costSubject', width: 150 },
{ title: '预算成本', dataIndex: 'budgetCost', slotName: 'budgetCost', width: 120 },
{ title: '实际成本', dataIndex: 'actualCost', slotName: 'actualCost', width: 120 },
{ title: '成本差异', dataIndex: 'costVariance', slotName: 'costVariance', width: 120 },
{ title: '完成进度', dataIndex: 'progress', width: 100 },
{ title: '成本期间', dataIndex: 'costPeriod', width: 120 },
{ title: '负责部门', dataIndex: 'department', width: 120 },
{ title: '项目经理', dataIndex: 'projectManager', width: 100 },
{ title: '财务审核', dataIndex: 'financeAudit', width: 100 },
{ title: '录入时间', dataIndex: 'createTime', width: 160 },
{ title: '更新时间', dataIndex: 'updateTime', width: 160 },
{ title: '备注', dataIndex: 'remark', width: 200, ellipsis: true, tooltip: true },
{ title: '操作', slotName: 'action', width: 200, fixed: 'right' }
]
//
const loading = ref(false)
const dataList = ref([
{
id: 1,
projectName: '华能新能源风电场叶片检测服务项目',
costType: '人工成本',
costCategory: '直接成本',
costSubject: '技术人员工资',
budgetCost: 80,
actualCost: 85,
costVariance: 5,
progress: '65%',
costPeriod: '2024年3月',
department: '技术部',
projectManager: '张项目经理',
financeAudit: '已审核',
createTime: '2024-03-01 09:00:00',
updateTime: '2024-03-15 16:30:00',
remark: '包含项目经理和技术员工资'
},
{
id: 2,
projectName: '华能新能源风电场叶片检测服务项目',
costType: '设备成本',
costCategory: '直接成本',
costSubject: '检测设备租赁',
budgetCost: 40,
actualCost: 35,
costVariance: -5,
progress: '65%',
costPeriod: '2024年3月',
department: '设备部',
projectManager: '张项目经理',
financeAudit: '已审核',
createTime: '2024-03-01 10:30:00',
updateTime: '2024-03-10 14:20:00',
remark: '设备租赁费用节省'
},
{
id: 3,
projectName: '大唐风电场防雷检测项目',
costType: '差旅成本',
costCategory: '直接成本',
costSubject: '出差交通住宿',
budgetCost: 15,
actualCost: 18,
costVariance: 3,
progress: '45%',
costPeriod: '2024年3月',
department: '行政部',
projectManager: '王项目经理',
financeAudit: '待审核',
createTime: '2024-03-05 11:15:00',
updateTime: '2024-03-12 09:45:00',
remark: '差旅费用略超预算'
},
{
id: 4,
projectName: '大唐风电场防雷检测项目',
costType: '材料成本',
costCategory: '直接成本',
costSubject: '检测耗材',
budgetCost: 25,
actualCost: 22,
costVariance: -3,
progress: '45%',
costPeriod: '2024年3月',
department: '采购部',
projectManager: '王项目经理',
financeAudit: '已审核',
createTime: '2024-03-06 14:00:00',
updateTime: '2024-03-08 17:30:00',
remark: '材料采购成本控制良好'
}
])
const pagination = reactive({
current: 1,
pageSize: 10,
total: 4,
showTotal: true,
showPageSize: true
})
//
const getCostTypeColor = (type: string) => {
const colorMap: Record<string, string> = {
'人工成本': 'blue',
'材料成本': 'green',
'设备成本': 'orange',
'差旅成本': 'purple',
'管理成本': 'cyan',
'其他成本': 'gray'
}
return colorMap[type] || 'gray'
}
//
const getCostVarianceClass = (variance: number) => {
if (variance > 0) return 'font-medium text-red-600'
if (variance < 0) return 'font-medium text-green-600'
return 'font-medium text-gray-600'
}
//
const search = async () => {
loading.value = true
setTimeout(() => {
loading.value = false
}, 1000)
}
const reset = () => {
Object.assign(searchForm, {
projectName: '',
costType: '',
costCategory: '',
costPeriod: '',
page: 1,
size: 10
})
pagination.current = 1
search()
}
//
const onPageChange = (page: number) => {
searchForm.page = page
pagination.current = page
search()
}
const onPageSizeChange = (size: number) => {
searchForm.size = size
searchForm.page = 1
pagination.pageSize = size
pagination.current = 1
search()
}
//
const openCostModal = () => {
Message.info('新增成本功能开发中...')
}
const analyseCost = () => {
Message.info('成本分析功能开发中...')
}
const exportCost = () => {
Message.info('导出数据功能开发中...')
}
const viewDetail = (record: any) => {
Message.info(`查看成本详情: ${record.projectName} - ${record.costSubject}`)
}
const editRecord = (record: any) => {
Message.info(`编辑成本记录: ${record.costSubject}`)
}
const viewBill = (record: any) => {
Message.info(`查看相关单据: ${record.costSubject}`)
}
const auditCost = (record: any) => {
Message.info(`审核成本: ${record.costSubject}`)
}
onMounted(() => {
search()
})
</script>

View File

@ -0,0 +1,299 @@
<template>
<GiPageLayout>
<GiTable
row-key="id"
title="支出合同管理"
:data="dataList"
:columns="tableColumns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1600 }"
:pagination="pagination"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
@refresh="search"
>
<template #top>
<GiForm
v-model="searchForm"
search
:columns="queryFormColumns"
size="medium"
@search="search"
@reset="reset"
/>
</template>
<template #toolbar-left>
<a-space>
<a-button type="primary" @click="openAddModal">
<template #icon><icon-plus /></template>
<template #default>新建合同</template>
</a-button>
<a-button @click="exportContract">
<template #icon><icon-download /></template>
<template #default>导出合同</template>
</a-button>
</a-space>
</template>
<!-- 合同状态 -->
<template #status="{ record }">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<!-- 合同金额 -->
<template #contractAmount="{ record }">
<span class="font-medium text-red-600">{{ record.contractAmount.toLocaleString() }}</span>
</template>
<!-- 已付款金额 -->
<template #paidAmount="{ record }">
<span class="font-medium text-orange-600">{{ record.paidAmount.toLocaleString() }}</span>
</template>
<!-- 操作列 -->
<template #action="{ record }">
<a-space>
<a-link @click="viewDetail(record)">详情</a-link>
<a-link @click="editRecord(record)" v-if="record.status === 'draft'">编辑</a-link>
<a-link @click="approveContract(record)" v-if="record.status === 'pending'">审批</a-link>
<a-link @click="viewPayment(record)">付款记录</a-link>
</a-space>
</template>
</GiTable>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
//
const searchForm = reactive({
contractName: '',
contractCode: '',
supplier: '',
status: '',
signDate: '',
page: 1,
size: 10
})
//
const queryFormColumns = [
{
field: 'contractName',
label: '合同名称',
type: 'input' as const,
props: {
placeholder: '请输入合同名称'
}
},
{
field: 'supplier',
label: '供应商',
type: 'input' as const,
props: {
placeholder: '请输入供应商名称'
}
},
{
field: 'status',
label: '合同状态',
type: 'select' as const,
props: {
placeholder: '请选择合同状态',
options: [
{ label: '草稿', value: 'draft' },
{ label: '待审批', value: 'pending' },
{ label: '已签署', value: 'signed' },
{ label: '执行中', value: 'executing' },
{ label: '已完成', value: 'completed' },
{ label: '已终止', value: 'terminated' }
]
}
}
]
//
const tableColumns: TableColumnData[] = [
{ title: '合同编号', dataIndex: 'contractCode', width: 150 },
{ title: '合同名称', dataIndex: 'contractName', width: 250, ellipsis: true, tooltip: true },
{ title: '供应商名称', dataIndex: 'supplier', width: 200, ellipsis: true, tooltip: true },
{ title: '合同类型', dataIndex: 'contractType', width: 120 },
{ title: '合同金额', dataIndex: 'contractAmount', slotName: 'contractAmount', width: 120 },
{ title: '已付款金额', dataIndex: 'paidAmount', slotName: 'paidAmount', width: 120 },
{ title: '未付款金额', dataIndex: 'unpaidAmount', width: 120 },
{ title: '签署日期', dataIndex: 'signDate', width: 120 },
{ title: '开始日期', dataIndex: 'startDate', width: 120 },
{ title: '结束日期', dataIndex: 'endDate', width: 120 },
{ title: '合同状态', dataIndex: 'status', slotName: 'status', width: 100 },
{ title: '项目关联', dataIndex: 'relatedProject', width: 200, ellipsis: true, tooltip: true },
{ title: '采购负责人', dataIndex: 'purchaseManager', width: 100 },
{ title: '付款方式', dataIndex: 'paymentMethod', width: 100 },
{ title: '备注', dataIndex: 'remark', width: 200, ellipsis: true, tooltip: true },
{ title: '操作', slotName: 'action', width: 200, fixed: 'right' }
]
//
const loading = ref(false)
const dataList = ref([
{
id: 1,
contractCode: 'EC2024001',
contractName: '风电检测设备采购合同',
supplier: '深圳市智能检测设备有限公司',
contractType: '设备采购',
contractAmount: 120,
paidAmount: 60,
unpaidAmount: 60,
signDate: '2024-02-25',
startDate: '2024-03-01',
endDate: '2024-03-31',
status: 'executing',
relatedProject: '华能新能源风电场叶片检测服务项目',
purchaseManager: '李采购经理',
paymentMethod: '银行转账',
remark: '按合同约定分期付款'
},
{
id: 2,
contractCode: 'EC2024002',
contractName: '无人机检测服务外包合同',
supplier: '北京航天无人机技术有限公司',
contractType: '服务外包',
contractAmount: 85,
paidAmount: 25.5,
unpaidAmount: 59.5,
signDate: '2024-03-02',
startDate: '2024-03-05',
endDate: '2024-04-05',
status: 'executing',
relatedProject: '大唐风电场防雷检测项目',
purchaseManager: '王采购经理',
paymentMethod: '分期付款',
remark: '服务外包,按进度付款'
},
{
id: 3,
contractCode: 'EC2024003',
contractName: '检测车辆租赁合同',
supplier: '上海专业车辆租赁有限公司',
contractType: '车辆租赁',
contractAmount: 15,
paidAmount: 15,
unpaidAmount: 0,
signDate: '2024-01-20',
startDate: '2024-01-25',
endDate: '2024-02-25',
status: 'completed',
relatedProject: '中广核风电场设备维护服务项目',
purchaseManager: '刘采购经理',
paymentMethod: '月付',
remark: '租赁合同已完成'
}
])
const pagination = reactive({
current: 1,
pageSize: 10,
total: 3,
showTotal: true,
showPageSize: true
})
//
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
'draft': 'gray',
'pending': 'orange',
'signed': 'blue',
'executing': 'cyan',
'completed': 'green',
'terminated': 'red'
}
return colorMap[status] || 'gray'
}
//
const getStatusText = (status: string) => {
const textMap: Record<string, string> = {
'draft': '草稿',
'pending': '待审批',
'signed': '已签署',
'executing': '执行中',
'completed': '已完成',
'terminated': '已终止'
}
return textMap[status] || status
}
//
const search = async () => {
loading.value = true
setTimeout(() => {
loading.value = false
}, 1000)
}
const reset = () => {
Object.assign(searchForm, {
contractName: '',
contractCode: '',
supplier: '',
status: '',
signDate: '',
page: 1,
size: 10
})
pagination.current = 1
search()
}
//
const onPageChange = (page: number) => {
searchForm.page = page
pagination.current = page
search()
}
const onPageSizeChange = (size: number) => {
searchForm.size = size
searchForm.page = 1
pagination.pageSize = size
pagination.current = 1
search()
}
//
const openAddModal = () => {
Message.info('新建合同功能开发中...')
}
const exportContract = () => {
Message.info('导出合同功能开发中...')
}
const viewDetail = (record: any) => {
Message.info(`查看合同详情: ${record.contractName}`)
}
const editRecord = (record: any) => {
Message.info(`编辑合同: ${record.contractName}`)
}
const approveContract = (record: any) => {
Message.info(`审批合同: ${record.contractName}`)
}
const viewPayment = (record: any) => {
Message.info(`查看付款记录: ${record.contractName}`)
}
onMounted(() => {
search()
})
</script>

View File

@ -0,0 +1,295 @@
<template>
<GiPageLayout>
<GiTable
row-key="id"
title="收入合同管理"
:data="dataList"
:columns="tableColumns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1600 }"
:pagination="pagination"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
@refresh="search"
>
<template #top>
<GiForm
v-model="searchForm"
search
:columns="queryFormColumns"
size="medium"
@search="search"
@reset="reset"
/>
</template>
<template #toolbar-left>
<a-space>
<a-button type="primary" @click="openAddModal">
<template #icon><icon-plus /></template>
<template #default>新建合同</template>
</a-button>
<a-button @click="exportContract">
<template #icon><icon-download /></template>
<template #default>导出合同</template>
</a-button>
</a-space>
</template>
<!-- 合同状态 -->
<template #status="{ record }">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<!-- 合同金额 -->
<template #contractAmount="{ record }">
<span class="font-medium text-green-600">{{ record.contractAmount.toLocaleString() }}</span>
</template>
<!-- 已收款金额 -->
<template #receivedAmount="{ record }">
<span class="font-medium text-blue-600">{{ record.receivedAmount.toLocaleString() }}</span>
</template>
<!-- 操作列 -->
<template #action="{ record }">
<a-space>
<a-link @click="viewDetail(record)">详情</a-link>
<a-link @click="editRecord(record)" v-if="record.status === 'draft'">编辑</a-link>
<a-link @click="approveContract(record)" v-if="record.status === 'pending'">审批</a-link>
<a-link @click="viewPayment(record)">收款记录</a-link>
</a-space>
</template>
</GiTable>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
//
const searchForm = reactive({
contractName: '',
contractCode: '',
client: '',
status: '',
signDate: '',
page: 1,
size: 10
})
//
const queryFormColumns = [
{
field: 'contractName',
label: '合同名称',
type: 'input' as const,
props: {
placeholder: '请输入合同名称'
}
},
{
field: 'client',
label: '客户',
type: 'input' as const,
props: {
placeholder: '请输入客户名称'
}
},
{
field: 'status',
label: '合同状态',
type: 'select' as const,
props: {
placeholder: '请选择合同状态',
options: [
{ label: '草稿', value: 'draft' },
{ label: '待审批', value: 'pending' },
{ label: '已签署', value: 'signed' },
{ label: '执行中', value: 'executing' },
{ label: '已完成', value: 'completed' },
{ label: '已终止', value: 'terminated' }
]
}
}
]
//
const tableColumns: TableColumnData[] = [
{ title: '合同编号', dataIndex: 'contractCode', width: 150 },
{ title: '合同名称', dataIndex: 'contractName', width: 250, ellipsis: true, tooltip: true },
{ title: '客户名称', dataIndex: 'client', width: 200, ellipsis: true, tooltip: true },
{ title: '合同金额', dataIndex: 'contractAmount', slotName: 'contractAmount', width: 120 },
{ title: '已收款金额', dataIndex: 'receivedAmount', slotName: 'receivedAmount', width: 120 },
{ title: '未收款金额', dataIndex: 'pendingAmount', width: 120 },
{ title: '签署日期', dataIndex: 'signDate', width: 120 },
{ title: '开始日期', dataIndex: 'startDate', width: 120 },
{ title: '结束日期', dataIndex: 'endDate', width: 120 },
{ title: '合同状态', dataIndex: 'status', slotName: 'status', width: 100 },
{ title: '项目经理', dataIndex: 'projectManager', width: 100 },
{ title: '销售经理', dataIndex: 'salesManager', width: 100 },
{ title: '完成进度', dataIndex: 'progress', width: 100 },
{ title: '备注', dataIndex: 'remark', width: 200, ellipsis: true, tooltip: true },
{ title: '操作', slotName: 'action', width: 200, fixed: 'right' }
]
//
const loading = ref(false)
const dataList = ref([
{
id: 1,
contractCode: 'RC2024001',
contractName: '华能新能源风电场叶片检测服务合同',
client: '华能新能源股份有限公司',
contractAmount: 320,
receivedAmount: 192,
pendingAmount: 128,
signDate: '2024-02-20',
startDate: '2024-03-01',
endDate: '2024-04-30',
status: 'executing',
projectManager: '张项目经理',
salesManager: '李销售经理',
progress: '60%',
remark: '项目进展顺利,客户满意度高'
},
{
id: 2,
contractCode: 'RC2024002',
contractName: '大唐风电场防雷检测项目合同',
client: '大唐新能源股份有限公司',
contractAmount: 268,
receivedAmount: 134,
pendingAmount: 134,
signDate: '2024-02-25',
startDate: '2024-03-05',
endDate: '2024-04-20',
status: 'executing',
projectManager: '王项目经理',
salesManager: '赵销售经理',
progress: '45%',
remark: '按计划执行中'
},
{
id: 3,
contractCode: 'RC2024003',
contractName: '中广核风电场设备维护服务合同',
client: '中广核新能源投资有限公司',
contractAmount: 450,
receivedAmount: 450,
pendingAmount: 0,
signDate: '2024-01-15',
startDate: '2024-01-20',
endDate: '2024-01-31',
status: 'completed',
projectManager: '刘项目经理',
salesManager: '孙销售经理',
progress: '100%',
remark: '项目已完成,客户验收通过'
}
])
const pagination = reactive({
current: 1,
pageSize: 10,
total: 3,
showTotal: true,
showPageSize: true
})
//
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
'draft': 'gray',
'pending': 'orange',
'signed': 'blue',
'executing': 'cyan',
'completed': 'green',
'terminated': 'red'
}
return colorMap[status] || 'gray'
}
//
const getStatusText = (status: string) => {
const textMap: Record<string, string> = {
'draft': '草稿',
'pending': '待审批',
'signed': '已签署',
'executing': '执行中',
'completed': '已完成',
'terminated': '已终止'
}
return textMap[status] || status
}
//
const search = async () => {
loading.value = true
setTimeout(() => {
loading.value = false
}, 1000)
}
const reset = () => {
Object.assign(searchForm, {
contractName: '',
contractCode: '',
client: '',
status: '',
signDate: '',
page: 1,
size: 10
})
pagination.current = 1
search()
}
//
const onPageChange = (page: number) => {
searchForm.page = page
pagination.current = page
search()
}
const onPageSizeChange = (size: number) => {
searchForm.size = size
searchForm.page = 1
pagination.pageSize = size
pagination.current = 1
search()
}
//
const openAddModal = () => {
Message.info('新建合同功能开发中...')
}
const exportContract = () => {
Message.info('导出合同功能开发中...')
}
const viewDetail = (record: any) => {
Message.info(`查看合同详情: ${record.contractName}`)
}
const editRecord = (record: any) => {
Message.info(`编辑合同: ${record.contractName}`)
}
const approveContract = (record: any) => {
Message.info(`审批合同: ${record.contractName}`)
}
const viewPayment = (record: any) => {
Message.info(`查看收款记录: ${record.contractName}`)
}
onMounted(() => {
search()
})
</script>

View File

@ -0,0 +1,330 @@
<template>
<GiPageLayout>
<GiTable
row-key="id"
title="立项管理"
:data="dataList"
:columns="tableColumns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1800 }"
:pagination="pagination"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
@refresh="search"
>
<template #top>
<GiForm
v-model="searchForm"
search
:columns="queryFormColumns"
size="medium"
@search="search"
@reset="reset"
/>
</template>
<template #toolbar-left>
<a-space>
<a-button type="primary" @click="openAddModal">
<template #icon><icon-plus /></template>
<template #default>新建立项</template>
</a-button>
<a-button @click="batchApprove">
<template #icon><icon-check /></template>
<template #default>批量审批</template>
</a-button>
</a-space>
</template>
<!-- 项目类型标签 -->
<template #projectType="{ record }">
<a-tag :color="getTypeColor(record.projectType)">
{{ record.projectType }}
</a-tag>
</template>
<!-- 审批状态 -->
<template #approvalStatus="{ record }">
<a-tag :color="getStatusColor(record.approvalStatus)">
{{ getStatusText(record.approvalStatus) }}
</a-tag>
</template>
<!-- 预估收益 -->
<template #estimatedRevenue="{ record }">
<span class="font-medium text-green-600">{{ record.estimatedRevenue.toLocaleString() }}</span>
</template>
<!-- 风险等级 -->
<template #riskLevel="{ record }">
<a-tag :color="getRiskColor(record.riskLevel)">
{{ record.riskLevel }}
</a-tag>
</template>
<!-- 操作列 -->
<template #action="{ record }">
<a-space>
<a-link @click="viewDetail(record)">详情</a-link>
<a-link @click="editRecord(record)" v-if="record.approvalStatus === 'draft'">编辑</a-link>
<a-link @click="approveProject(record)" v-if="record.approvalStatus === 'pending'">审批</a-link>
<a-link @click="startProject(record)" v-if="record.approvalStatus === 'approved'">启动项目</a-link>
</a-space>
</template>
</GiTable>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
//
const searchForm = reactive({
projectName: '',
projectCode: '',
projectType: '',
approvalStatus: '',
submitDate: '',
page: 1,
size: 10
})
//
const queryFormColumns = [
{
field: 'projectName',
label: '项目名称',
type: 'input' as const,
props: {
placeholder: '请输入项目名称'
}
},
{
field: 'projectType',
label: '项目类型',
type: 'select' as const,
props: {
placeholder: '请选择项目类型',
options: [
{ label: '风电检测', value: '风电检测' },
{ label: '设备采购', value: '设备采购' },
{ label: '技术服务', value: '技术服务' },
{ label: '产品研发', value: '产品研发' },
{ label: '系统集成', value: '系统集成' }
]
}
},
{
field: 'approvalStatus',
label: '审批状态',
type: 'select' as const,
props: {
placeholder: '请选择审批状态',
options: [
{ label: '草稿', value: 'draft' },
{ label: '待审批', value: 'pending' },
{ label: '已通过', value: 'approved' },
{ label: '已拒绝', value: 'rejected' },
{ label: '已启动', value: 'started' }
]
}
}
]
//
const tableColumns: TableColumnData[] = [
{ title: '项目编号', dataIndex: 'projectCode', width: 150 },
{ title: '项目名称', dataIndex: 'projectName', width: 300, ellipsis: true, tooltip: true },
{ title: '项目类型', dataIndex: 'projectType', slotName: 'projectType', width: 120 },
{ title: '申请人', dataIndex: 'applicant', width: 100 },
{ title: '预估收益', dataIndex: 'estimatedRevenue', slotName: 'estimatedRevenue', width: 120 },
{ title: '预估成本', dataIndex: 'estimatedCost', width: 120 },
{ title: '预计工期', dataIndex: 'estimatedDuration', width: 120 },
{ title: '风险等级', dataIndex: 'riskLevel', slotName: 'riskLevel', width: 100 },
{ title: '提交时间', dataIndex: 'submitTime', width: 160 },
{ title: '审批状态', dataIndex: 'approvalStatus', slotName: 'approvalStatus', width: 100 },
{ title: '审批人', dataIndex: 'approver', width: 100 },
{ title: '审批时间', dataIndex: 'approvalTime', width: 160 },
{ title: '项目经理', dataIndex: 'projectManager', width: 100 },
{ title: '优先级', dataIndex: 'priority', width: 80 },
{ title: '操作', slotName: 'action', width: 200, fixed: 'right' }
]
//
const loading = ref(false)
const dataList = ref([
{
id: 1,
projectCode: 'PI2024001',
projectName: '华能新能源风电场智能监测系统集成项目',
projectType: '系统集成',
applicant: '李技术总监',
estimatedRevenue: 850,
estimatedCost: 520,
estimatedDuration: '90天',
riskLevel: '中风险',
submitTime: '2024-02-01 10:30:00',
approvalStatus: 'approved',
approver: '王总经理',
approvalTime: '2024-02-05 14:20:00',
projectManager: '张项目经理',
priority: 'A'
},
{
id: 2,
projectCode: 'PI2024002',
projectName: '大唐风电场叶片维修机器人研发项目',
projectType: '产品研发',
applicant: '刘研发总监',
estimatedRevenue: 1200,
estimatedCost: 800,
estimatedDuration: '180天',
riskLevel: '高风险',
submitTime: '2024-02-10 09:15:00',
approvalStatus: 'pending',
approver: '',
approvalTime: '',
projectManager: '赵项目经理',
priority: 'A'
},
{
id: 3,
projectCode: 'PI2024003',
projectName: '海上风电场防雷检测服务项目',
projectType: '风电检测',
applicant: '孙业务经理',
estimatedRevenue: 380,
estimatedCost: 190,
estimatedDuration: '45天',
riskLevel: '低风险',
submitTime: '2024-02-15 16:45:00',
approvalStatus: 'started',
approver: '王总经理',
approvalTime: '2024-02-18 11:00:00',
projectManager: '陈项目经理',
priority: 'B'
}
])
const pagination = reactive({
current: 1,
pageSize: 10,
total: 3,
showTotal: true,
showPageSize: true
})
//
const getTypeColor = (type: string) => {
const colorMap: Record<string, string> = {
'风电检测': 'blue',
'设备采购': 'green',
'技术服务': 'orange',
'产品研发': 'purple',
'系统集成': 'cyan'
}
return colorMap[type] || 'gray'
}
//
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
'draft': 'gray',
'pending': 'orange',
'approved': 'green',
'rejected': 'red',
'started': 'blue'
}
return colorMap[status] || 'gray'
}
//
const getStatusText = (status: string) => {
const textMap: Record<string, string> = {
'draft': '草稿',
'pending': '待审批',
'approved': '已通过',
'rejected': '已拒绝',
'started': '已启动'
}
return textMap[status] || status
}
//
const getRiskColor = (level: string) => {
const colorMap: Record<string, string> = {
'低风险': 'green',
'中风险': 'orange',
'高风险': 'red'
}
return colorMap[level] || 'gray'
}
//
const search = async () => {
loading.value = true
setTimeout(() => {
loading.value = false
}, 1000)
}
const reset = () => {
Object.assign(searchForm, {
projectName: '',
projectCode: '',
projectType: '',
approvalStatus: '',
submitDate: '',
page: 1,
size: 10
})
pagination.current = 1
search()
}
//
const onPageChange = (page: number) => {
searchForm.page = page
pagination.current = page
search()
}
const onPageSizeChange = (size: number) => {
searchForm.size = size
searchForm.page = 1
pagination.pageSize = size
pagination.current = 1
search()
}
//
const openAddModal = () => {
Message.info('新建立项功能开发中...')
}
const batchApprove = () => {
Message.info('批量审批功能开发中...')
}
const viewDetail = (record: any) => {
Message.info(`查看立项详情: ${record.projectName}`)
}
const editRecord = (record: any) => {
Message.info(`编辑立项: ${record.projectName}`)
}
const approveProject = (record: any) => {
Message.info(`审批项目: ${record.projectName}`)
}
const startProject = (record: any) => {
Message.info(`启动项目: ${record.projectName}`)
}
onMounted(() => {
search()
})
</script>

View File

@ -0,0 +1,372 @@
<template>
<GiPageLayout>
<GiTable
row-key="id"
title="项目管理"
:data="dataList"
:columns="tableColumns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1900 }"
:pagination="pagination"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
@refresh="search"
>
<template #top>
<GiForm
v-model="searchForm"
search
:columns="queryFormColumns"
size="medium"
@search="search"
@reset="reset"
/>
</template>
<template #toolbar-left>
<a-space>
<a-button type="primary" @click="openProjectModal">
<template #icon><icon-plus /></template>
<template #default>新建项目</template>
</a-button>
<a-button @click="batchAssign">
<template #icon><icon-user-add /></template>
<template #default>批量分配</template>
</a-button>
<a-button @click="exportProjects">
<template #icon><icon-download /></template>
<template #default>导出项目</template>
</a-button>
</a-space>
</template>
<!-- 项目状态 -->
<template #status="{ record }">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<!-- 项目进度 -->
<template #progress="{ record }">
<a-progress
:percent="record.progress"
:color="getProgressColor(record.progress)"
size="small"
:show-text="false"
/>
<span class="ml-2">{{ record.progress }}%</span>
</template>
<!-- 预算执行情况 -->
<template #budgetExecution="{ record }">
<div class="text-sm">
<div class="font-medium text-blue-600">预算{{ record.totalBudget.toLocaleString() }}</div>
<div class="text-green-600">已用{{ record.usedBudget.toLocaleString() }}</div>
<div class="text-gray-500">余额{{ (record.totalBudget - record.usedBudget).toLocaleString() }}</div>
</div>
</template>
<!-- 团队成员 -->
<template #team="{ record }">
<a-avatar-group size="small" :max-count="3">
<a-avatar v-for="member in record.team" :key="member.id" :title="member.name">
{{ member.name.substring(0, 1) }}
</a-avatar>
</a-avatar-group>
</template>
<!-- 优先级 -->
<template #priority="{ record }">
<a-tag :color="getPriorityColor(record.priority)">
{{ record.priority }}
</a-tag>
</template>
<!-- 操作列 -->
<template #action="{ record }">
<a-space>
<a-link @click="viewDetail(record)">详情</a-link>
<a-link @click="editProject(record)">编辑</a-link>
<a-link @click="viewProgress(record)">进度</a-link>
<a-link @click="viewBudget(record)">预算</a-link>
<a-link @click="viewTeam(record)">团队</a-link>
</a-space>
</template>
</GiTable>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
//
const searchForm = reactive({
projectName: '',
projectCode: '',
status: '',
projectManager: '',
priority: '',
page: 1,
size: 10
})
//
const queryFormColumns = [
{
field: 'projectName',
label: '项目名称',
type: 'input' as const,
props: {
placeholder: '请输入项目名称'
}
},
{
field: 'status',
label: '项目状态',
type: 'select' as const,
props: {
placeholder: '请选择项目状态',
options: [
{ label: '未开始', value: 'not_started' },
{ label: '进行中', value: 'in_progress' },
{ label: '已暂停', value: 'paused' },
{ label: '已完成', value: 'completed' },
{ label: '已取消', value: 'cancelled' }
]
}
},
{
field: 'priority',
label: '优先级',
type: 'select' as const,
props: {
placeholder: '请选择优先级',
options: [
{ label: '高', value: '高' },
{ label: '中', value: '中' },
{ label: '低', value: '低' }
]
}
}
]
//
const tableColumns: TableColumnData[] = [
{ title: '项目编号', dataIndex: 'projectCode', width: 150 },
{ title: '项目名称', dataIndex: 'projectName', width: 300, ellipsis: true, tooltip: true },
{ title: '客户', dataIndex: 'client', width: 200, ellipsis: true, tooltip: true },
{ title: '项目经理', dataIndex: 'projectManager', width: 100 },
{ title: '开始时间', dataIndex: 'startDate', width: 120 },
{ title: '计划完成', dataIndex: 'plannedEndDate', width: 120 },
{ title: '实际完成', dataIndex: 'actualEndDate', width: 120 },
{ title: '项目状态', dataIndex: 'status', slotName: 'status', width: 100 },
{ title: '进度', dataIndex: 'progress', slotName: 'progress', width: 150 },
{ title: '预算执行', dataIndex: 'budgetExecution', slotName: 'budgetExecution', width: 180 },
{ title: '团队成员', dataIndex: 'team', slotName: 'team', width: 120 },
{ title: '优先级', dataIndex: 'priority', slotName: 'priority', width: 80 },
{ title: '风险等级', dataIndex: 'riskLevel', width: 100 },
{ title: '质量得分', dataIndex: 'qualityScore', width: 100 },
{ title: '操作', slotName: 'action', width: 300, fixed: 'right' }
]
//
const loading = ref(false)
const dataList = ref([
{
id: 1,
projectCode: 'PM2024001',
projectName: '华能新能源风电场叶片检测服务项目',
client: '华能新能源股份有限公司',
projectManager: '张项目经理',
startDate: '2024-03-01',
plannedEndDate: '2024-04-30',
actualEndDate: '',
status: 'in_progress',
progress: 65,
totalBudget: 320,
usedBudget: 192,
team: [
{ id: 1, name: '张项目经理' },
{ id: 2, name: '李技术员' },
{ id: 3, name: '王检测员' },
{ id: 4, name: '赵质量员' }
],
priority: '高',
riskLevel: '中风险',
qualityScore: 92
},
{
id: 2,
projectCode: 'PM2024002',
projectName: '大唐风电场防雷检测项目',
client: '大唐新能源股份有限公司',
projectManager: '王项目经理',
startDate: '2024-03-05',
plannedEndDate: '2024-04-20',
actualEndDate: '',
status: 'in_progress',
progress: 45,
totalBudget: 268,
usedBudget: 107,
team: [
{ id: 1, name: '王项目经理' },
{ id: 2, name: '陈技术员' },
{ id: 3, name: '刘检测员' }
],
priority: '中',
riskLevel: '低风险',
qualityScore: 88
},
{
id: 3,
projectCode: 'PM2024003',
projectName: '中广核风电场设备维护服务项目',
client: '中广核新能源投资有限公司',
projectManager: '刘项目经理',
startDate: '2024-01-20',
plannedEndDate: '2024-01-31',
actualEndDate: '2024-01-31',
status: 'completed',
progress: 100,
totalBudget: 450,
usedBudget: 445,
team: [
{ id: 1, name: '刘项目经理' },
{ id: 2, name: '孙技术员' },
{ id: 3, name: '周检测员' },
{ id: 4, name: '吴质量员' },
{ id: 5, name: '郑安全员' }
],
priority: '高',
riskLevel: '低风险',
qualityScore: 95
}
])
const pagination = reactive({
current: 1,
pageSize: 10,
total: 3,
showTotal: true,
showPageSize: true
})
//
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
'not_started': 'gray',
'in_progress': 'blue',
'paused': 'orange',
'completed': 'green',
'cancelled': 'red'
}
return colorMap[status] || 'gray'
}
//
const getStatusText = (status: string) => {
const textMap: Record<string, string> = {
'not_started': '未开始',
'in_progress': '进行中',
'paused': '已暂停',
'completed': '已完成',
'cancelled': '已取消'
}
return textMap[status] || status
}
//
const getProgressColor = (progress: number) => {
if (progress >= 80) return '#52c41a'
if (progress >= 60) return '#1890ff'
if (progress >= 40) return '#faad14'
return '#ff4d4f'
}
//
const getPriorityColor = (priority: string) => {
const colorMap: Record<string, string> = {
'高': 'red',
'中': 'orange',
'低': 'blue'
}
return colorMap[priority] || 'gray'
}
//
const search = async () => {
loading.value = true
setTimeout(() => {
loading.value = false
}, 1000)
}
const reset = () => {
Object.assign(searchForm, {
projectName: '',
projectCode: '',
status: '',
projectManager: '',
priority: '',
page: 1,
size: 10
})
pagination.current = 1
search()
}
//
const onPageChange = (page: number) => {
searchForm.page = page
pagination.current = page
search()
}
const onPageSizeChange = (size: number) => {
searchForm.size = size
searchForm.page = 1
pagination.pageSize = size
pagination.current = 1
search()
}
//
const openProjectModal = () => {
Message.info('新建项目功能开发中...')
}
const batchAssign = () => {
Message.info('批量分配功能开发中...')
}
const exportProjects = () => {
Message.info('导出项目功能开发中...')
}
const viewDetail = (record: any) => {
Message.info(`查看项目详情: ${record.projectName}`)
}
const editProject = (record: any) => {
Message.info(`编辑项目: ${record.projectName}`)
}
const viewProgress = (record: any) => {
Message.info(`查看项目进度: ${record.projectName}`)
}
const viewBudget = (record: any) => {
Message.info(`查看项目预算: ${record.projectName}`)
}
const viewTeam = (record: any) => {
Message.info(`查看项目团队: ${record.projectName}`)
}
onMounted(() => {
search()
})
</script>

View File

@ -0,0 +1,422 @@
<!--
预算申请弹窗组件
-->
<template>
<a-modal
v-model:visible="visible"
title="预算申请"
width="1200px"
:mask-closable="false"
@before-ok="handleSubmit"
@cancel="handleCancel"
>
<!-- 项目基本信息 -->
<div class="project-info-section">
<a-descriptions
:column="5"
bordered
size="small"
class="project-info"
>
<a-descriptions-item label="项目名称">{{ projectInfo?.projectName || '项目一' }}</a-descriptions-item>
<a-descriptions-item label="项目编号">{{ projectInfo?.projectCode || 'HWPC0001' }}</a-descriptions-item>
<a-descriptions-item label="项目金额(万)">{{ (projectInfo as any)?.projectAmount || '126.00' }}</a-descriptions-item>
<a-descriptions-item label="项目负责人">{{ projectInfo?.projectManager || projectInfo?.projectManagerName || '丁曼容' }}</a-descriptions-item>
<a-descriptions-item label="客户">{{ projectInfo?.client || '義和大学' }}</a-descriptions-item>
<a-descriptions-item label="交付日期">{{ projectInfo?.endDate || '2025-05-01' }}</a-descriptions-item>
<a-descriptions-item label="预算总金额(万)">{{ totalBudgetAmount.toFixed(2) }}</a-descriptions-item>
</a-descriptions>
</div>
<!-- 申请预算项 -->
<div class="budget-items-section">
<h3 class="section-title">申请预算项</h3>
<a-table
:data="budgetItems"
:columns="budgetColumns"
:pagination="false"
size="small"
class="budget-table"
>
<template #budgetName="{ record, rowIndex }">
<a-input
v-model="record.budgetName"
placeholder="请输入预算名称"
:style="{ width: '100%' }"
/>
</template>
<template #budgetType="{ record, rowIndex }">
<a-select
v-model="record.budgetType"
placeholder="请选择"
:style="{ width: '100%' }"
:options="budgetTypeOptions"
/>
</template>
<template #budgetAmount="{ record, rowIndex }">
<a-input-number
v-model="record.budgetAmount"
placeholder="0.00"
:precision="2"
:min="0"
:style="{ width: '100%' }"
/>
</template>
<template #budgetDescription="{ record, rowIndex }">
<a-input
v-model="record.budgetDescription"
placeholder="--"
:style="{ width: '100%' }"
/>
</template>
<template #attachments="{ record, rowIndex }">
<a-upload
:file-list="record.attachments || []"
:show-file-list="false"
:custom-request="(option) => handleUpload(option, rowIndex)"
>
<a-button type="primary" size="small">上传</a-button>
</a-upload>
<div v-if="record.attachments?.length" class="attachment-list">
<a-tag
v-for="file in record.attachments"
:key="file.id"
closable
size="small"
@close="removeAttachment(rowIndex, file.id)"
>
{{ file.name }}
</a-tag>
</div>
</template>
<template #action="{ record, rowIndex }">
<a-button
type="text"
status="danger"
size="small"
@click="removeItem(rowIndex)"
>
删除
</a-button>
</template>
</a-table>
<!-- 新增预算项按钮 -->
<div class="add-item-btn">
<a-button type="dashed" @click="addBudgetItem">
<template #icon><icon-plus /></template>
新增预算项
</a-button>
</div>
</div>
<!-- 温馨提示 -->
<div class="tips-section">
<a-alert
type="info"
show-icon
message="手动新填、删除预算项及金额。"
banner
/>
</div>
<template #footer>
<a-space>
<a-button @click="handleCancel">取消</a-button>
<a-button type="primary" @click="handleSubmit">提交申请</a-button>
</a-space>
</template>
</a-modal>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch, onMounted } from 'vue'
import { Message, type TableColumnData } from '@arco-design/web-vue'
import {
applyBudget,
getBudgetTypes,
uploadBudgetAttachment,
type BudgetItemResp,
type BudgetApplyReq
} from '@/apis/project/budget'
import { getProject, type ProjectResp } from '@/apis/project'
interface Props {
visible: boolean
projectId?: string
}
interface Emits {
(e: 'update:visible', value: boolean): void
(e: 'success'): void
}
const props = withDefaults(defineProps<Props>(), {
visible: false,
projectId: ''
})
const emit = defineEmits<Emits>()
const visible = ref(props.visible)
const projectInfo = ref<ProjectResp | null>(null)
const budgetTypeOptions = ref<Array<{ label: string; value: string }>>([])
//
const budgetItems = ref<BudgetItemResp[]>([
{
budgetName: '设备采购',
budgetType: '采购',
budgetAmount: 50.00,
budgetDescription: '--',
attachments: []
},
{
budgetName: '人工费用',
budgetType: '人工',
budgetAmount: 16.00,
budgetDescription: '--',
attachments: []
},
{
budgetName: '管理费用',
budgetType: '管理费',
budgetAmount: 8.00,
budgetDescription: '--',
attachments: []
},
{
budgetName: '',
budgetType: '',
budgetAmount: 0,
budgetDescription: '',
attachments: []
}
])
//
const budgetColumns: TableColumnData[] = [
{
title: '预算名称',
dataIndex: 'budgetName',
slotName: 'budgetName',
width: 150
},
{
title: '预算类型',
dataIndex: 'budgetType',
slotName: 'budgetType',
width: 120
},
{
title: '预算金额(万)',
dataIndex: 'budgetAmount',
slotName: 'budgetAmount',
width: 130
},
{
title: '预算说明',
dataIndex: 'budgetDescription',
slotName: 'budgetDescription',
width: 150
},
{
title: '附件',
dataIndex: 'attachments',
slotName: 'attachments',
width: 200
},
{
title: '操作',
dataIndex: 'action',
slotName: 'action',
width: 80
}
]
//
const totalBudgetAmount = computed(() => {
return budgetItems.value.reduce((sum, item) => sum + (item.budgetAmount || 0), 0)
})
// visible
watch(() => props.visible, (newVal) => {
visible.value = newVal
if (newVal) {
loadProjectInfo()
loadBudgetTypes()
}
})
watch(visible, (newVal) => {
emit('update:visible', newVal)
})
//
const loadProjectInfo = async () => {
if (!props.projectId) return
try {
const { data } = await getProject(props.projectId)
projectInfo.value = data
} catch (error) {
console.error('加载项目信息失败:', error)
}
}
//
const loadBudgetTypes = async () => {
try {
const { data } = await getBudgetTypes()
budgetTypeOptions.value = data
} catch (error) {
console.error('加载预算类型失败:', error)
// 使
budgetTypeOptions.value = [
{ label: '采购', value: '采购' },
{ label: '人工', value: '人工' },
{ label: '管理费', value: '管理费' },
{ label: '设备', value: '设备' },
{ label: '运输', value: '运输' }
]
}
}
//
const addBudgetItem = () => {
budgetItems.value.push({
budgetName: '',
budgetType: '',
budgetAmount: 0,
budgetDescription: '',
attachments: []
})
}
//
const removeItem = (index: number) => {
if (budgetItems.value.length > 1) {
budgetItems.value.splice(index, 1)
} else {
Message.warning('至少保留一个预算项')
}
}
//
const handleUpload = async (option: any, rowIndex: number) => {
try {
const response = await uploadBudgetAttachment(option.file)
if (!budgetItems.value[rowIndex].attachments) {
budgetItems.value[rowIndex].attachments = []
}
budgetItems.value[rowIndex].attachments!.push(response.data)
Message.success('文件上传成功')
} catch (error) {
Message.error('文件上传失败')
}
}
//
const removeAttachment = (rowIndex: number, fileId: string) => {
const attachments = budgetItems.value[rowIndex].attachments || []
const index = attachments.findIndex(file => file.id === fileId)
if (index > -1) {
attachments.splice(index, 1)
}
}
//
const handleSubmit = async () => {
try {
//
const validItems = budgetItems.value.filter(item =>
item.budgetName && item.budgetType && item.budgetAmount > 0
)
if (validItems.length === 0) {
Message.warning('请至少填写一个完整的预算项')
return false
}
const applyData: BudgetApplyReq = {
projectId: props.projectId || '',
budgetItems: validItems,
totalAmount: totalBudgetAmount.value
}
await applyBudget(applyData)
Message.success('预算申请提交成功')
emit('success')
return true
} catch (error) {
console.error('提交申请失败:', error)
Message.error('提交申请失败')
return false
}
}
//
const handleCancel = () => {
//
budgetItems.value = [
{
budgetName: '',
budgetType: '',
budgetAmount: 0,
budgetDescription: '',
attachments: []
}
]
}
</script>
<style scoped lang="scss">
.project-info-section {
margin-bottom: 24px;
.project-info {
background: #fafafa;
}
}
.budget-items-section {
margin-bottom: 24px;
.section-title {
color: #1890ff;
font-size: 16px;
font-weight: 500;
margin-bottom: 16px;
padding-left: 8px;
border-left: 3px solid #1890ff;
}
.budget-table {
margin-bottom: 16px;
}
.add-item-btn {
text-align: center;
margin-top: 16px;
}
.attachment-list {
margin-top: 8px;
.arco-tag {
margin-right: 4px;
margin-bottom: 4px;
}
}
}
.tips-section {
margin-bottom: 16px;
}
</style>

View File

@ -0,0 +1,303 @@
<!--
预算审核弹窗组件
-->
<template>
<a-modal
v-model:visible="visible"
title="项目预算审核"
width="1000px"
:mask-closable="false"
:footer="false"
>
<!-- 项目基本信息 -->
<div class="project-info-section">
<a-descriptions
:column="5"
bordered
size="small"
class="project-info"
>
<a-descriptions-item label="项目名称">{{ budgetRecord?.projectName }}</a-descriptions-item>
<a-descriptions-item label="项目编号">{{ budgetRecord?.projectCode }}</a-descriptions-item>
<a-descriptions-item label="项目金额(万)">{{ budgetRecord?.projectAmount }}</a-descriptions-item>
<a-descriptions-item label="项目负责人">{{ budgetRecord?.projectManager }}</a-descriptions-item>
<a-descriptions-item label="客户">{{ budgetRecord?.client }}</a-descriptions-item>
<a-descriptions-item label="已有预算金额(万)" class="budget-highlight">{{ budgetRecord?.usedBudgetAmount || '75.00' }}</a-descriptions-item>
<a-descriptions-item label="本次预算金额(万)" class="budget-highlight">{{ budgetRecord?.applyBudgetAmount || '74.00' }}</a-descriptions-item>
</a-descriptions>
</div>
<!-- 申请预算项 -->
<div class="budget-items-section">
<h3 class="section-title">申请预算项</h3>
<a-table
:data="budgetItemsData"
:columns="budgetColumns"
:pagination="false"
size="small"
class="budget-table"
>
<template #budgetAmount="{ record }">
<span class="amount-text">{{ record.budgetAmount.toFixed(2) }}</span>
</template>
<template #attachments="{ record }">
<a-space v-if="record.attachments?.length">
<a-button
v-for="file in record.attachments"
:key="file.id"
type="primary"
size="small"
@click="viewAttachment(file)"
>
查看
</a-button>
</a-space>
<span v-else>--</span>
</template>
</a-table>
<!-- 合计 -->
<div class="total-amount">
<span class="total-label">合计</span>
<span class="total-value">{{ totalAmount.toFixed(2) }}</span>
</div>
</div>
<!-- 审核意见 -->
<div class="audit-section">
<h3 class="section-title">审核意见</h3>
<a-textarea
v-model="auditRemark"
placeholder="同意。"
:rows="4"
:max-length="500"
show-word-limit
/>
</div>
<!-- 操作按钮 -->
<div class="action-section">
<a-space>
<a-button @click="handleCancel">取消</a-button>
<a-button type="outline" status="danger" @click="handleReject">审核拒绝</a-button>
<a-button type="primary" @click="handleApprove">审核通过</a-button>
</a-space>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { Message, type TableColumnData } from '@arco-design/web-vue'
import { auditBudget, type BudgetRecordResp } from '@/apis/project/budget'
interface Props {
visible: boolean
budgetRecord?: BudgetRecordResp | null
}
interface Emits {
(e: 'update:visible', value: boolean): void
(e: 'success'): void
}
const props = withDefaults(defineProps<Props>(), {
visible: false,
budgetRecord: null
})
const emit = defineEmits<Emits>()
const visible = ref(props.visible)
const auditRemark = ref('同意。')
// props.budgetRecord
const budgetItemsData = ref([
{
budgetName: '设备采购',
budgetType: '采购',
budgetAmount: 50.00,
budgetDescription: '--',
attachments: []
},
{
budgetName: '管理费用',
budgetType: '人工',
budgetAmount: 16.00,
budgetDescription: '--',
attachments: []
},
{
budgetName: '人工费用',
budgetType: '管理费',
budgetAmount: 8.00,
budgetDescription: '--',
attachments: []
}
])
//
const budgetColumns: TableColumnData[] = [
{ title: '预算名称', dataIndex: 'budgetName', width: 150 },
{ title: '预算类型', dataIndex: 'budgetType', width: 120 },
{ title: '预算金额(万)', dataIndex: 'budgetAmount', slotName: 'budgetAmount', width: 130 },
{ title: '预算说明', dataIndex: 'budgetDescription', width: 150 },
{ title: '附件', dataIndex: 'attachments', slotName: 'attachments', width: 100 }
]
//
const totalAmount = computed(() => {
return budgetItemsData.value.reduce((sum, item) => sum + item.budgetAmount, 0)
})
// visible
watch(() => props.visible, (newVal) => {
visible.value = newVal
if (newVal && props.budgetRecord) {
//
auditRemark.value = '同意。'
}
})
watch(visible, (newVal) => {
emit('update:visible', newVal)
})
//
const viewAttachment = (file: any) => {
//
window.open(file.url, '_blank')
}
//
const handleApprove = async () => {
try {
if (!props.budgetRecord?.id) {
Message.error('预算记录ID不存在')
return
}
await auditBudget(props.budgetRecord.id, {
auditStatus: 'approved',
auditRemark: auditRemark.value
})
Message.success('审核通过成功')
emit('success')
visible.value = false
} catch (error) {
console.error('审核失败:', error)
Message.error('审核失败')
}
}
//
const handleReject = async () => {
try {
if (!props.budgetRecord?.id) {
Message.error('预算记录ID不存在')
return
}
if (!auditRemark.value.trim()) {
Message.warning('请填写审核意见')
return
}
await auditBudget(props.budgetRecord.id, {
auditStatus: 'rejected',
auditRemark: auditRemark.value
})
Message.success('审核拒绝成功')
emit('success')
visible.value = false
} catch (error) {
console.error('审核失败:', error)
Message.error('审核失败')
}
}
//
const handleCancel = () => {
visible.value = false
}
</script>
<style scoped lang="scss">
.project-info-section {
margin-bottom: 24px;
.project-info {
background: #fafafa;
}
.budget-highlight {
color: #f56500;
font-weight: 500;
}
}
.budget-items-section {
margin-bottom: 24px;
.section-title {
color: #1890ff;
font-size: 16px;
font-weight: 500;
margin-bottom: 16px;
padding-left: 8px;
border-left: 3px solid #1890ff;
}
.budget-table {
margin-bottom: 16px;
}
.amount-text {
font-weight: 500;
color: #f56500;
}
.total-amount {
text-align: right;
padding: 12px 16px;
background: #f7f8fa;
border-radius: 4px;
.total-label {
font-size: 16px;
font-weight: 500;
color: #333;
}
.total-value {
font-size: 18px;
font-weight: 600;
color: #f56500;
margin-left: 8px;
}
}
}
.audit-section {
margin-bottom: 24px;
.section-title {
color: #1890ff;
font-size: 16px;
font-weight: 500;
margin-bottom: 16px;
padding-left: 8px;
border-left: 3px solid #1890ff;
}
}
.action-section {
text-align: right;
padding-top: 16px;
border-top: 1px solid #e5e6eb;
}
</style>

View File

@ -0,0 +1,348 @@
<!--
预算详情弹窗组件
-->
<template>
<a-modal
v-model:visible="visible"
:title="budgetRecord?.projectName || '项目一'"
width="1200px"
:mask-closable="false"
:footer="false"
>
<!-- 项目概览卡片 -->
<div class="project-overview">
<a-descriptions
:column="8"
bordered
size="small"
class="overview-info"
>
<a-descriptions-item label="项目编号">{{ budgetRecord?.projectCode }}</a-descriptions-item>
<a-descriptions-item label="项目金额(万)">{{ budgetRecord?.projectAmount }}</a-descriptions-item>
<a-descriptions-item label="项目负责人">{{ budgetRecord?.projectManager }}</a-descriptions-item>
<a-descriptions-item label="立项日期">{{ budgetRecord?.establishDate }}</a-descriptions-item>
<a-descriptions-item label="客户">{{ budgetRecord?.client }}</a-descriptions-item>
<a-descriptions-item label="状态">
<a-tag color="orange">进行中</a-tag>
</a-descriptions-item>
<a-descriptions-item label="整体进度">
<span>25%</span>
</a-descriptions-item>
<a-descriptions-item label="预算总金额(万)">{{ budgetRecord?.usedBudgetAmount }}</a-descriptions-item>
<a-descriptions-item label="已用预算金额(万)">{{ budgetRecord?.usedBudgetAmount }}</a-descriptions-item>
<a-descriptions-item label="剩余预算金额(万)">{{ budgetRecord?.remainingBudgetAmount }}</a-descriptions-item>
</a-descriptions>
</div>
<!-- 选项卡内容 -->
<div class="detail-tabs">
<a-tabs v-model:active-key="activeTab" type="line">
<!-- 基础信息 -->
<a-tab-pane key="basic" title="基础信息">
<div class="tab-content">
<a-card title="基础信息" class="info-card">
<a-descriptions :column="2" bordered>
<a-descriptions-item label="项目名称">{{ projectDetailInfo.projectName }}</a-descriptions-item>
<a-descriptions-item label="项目编号">{{ projectDetailInfo.projectCode }}</a-descriptions-item>
<a-descriptions-item label="项目金额(万)">{{ projectDetailInfo.projectAmount }}</a-descriptions-item>
<a-descriptions-item label="项目负责人">{{ projectDetailInfo.projectManager }}</a-descriptions-item>
<a-descriptions-item label="负责人电话">{{ projectDetailInfo.managerPhone }}</a-descriptions-item>
<a-descriptions-item label="立项日期">{{ projectDetailInfo.establishDate }}</a-descriptions-item>
<a-descriptions-item label="交付日期">{{ projectDetailInfo.deliveryDate }}</a-descriptions-item>
<a-descriptions-item label="联系人">{{ projectDetailInfo.contactPerson }}</a-descriptions-item>
<a-descriptions-item label="备注">{{ projectDetailInfo.remark }}</a-descriptions-item>
<a-descriptions-item label="预算总金额(万)">{{ projectDetailInfo.totalBudget }}</a-descriptions-item>
<a-descriptions-item label="剩余预算金额(万)">{{ projectDetailInfo.remainingBudget }}</a-descriptions-item>
</a-descriptions>
</a-card>
<a-card title="系统信息" class="info-card">
<a-descriptions :column="2" bordered>
<a-descriptions-item label="创建人">{{ systemInfo.creator }}</a-descriptions-item>
<a-descriptions-item label="创建时间">{{ systemInfo.createTime }}</a-descriptions-item>
<a-descriptions-item label="修改人">{{ systemInfo.modifier }}</a-descriptions-item>
<a-descriptions-item label="修改时间">{{ systemInfo.modifyTime }}</a-descriptions-item>
<a-descriptions-item label="审核人">{{ systemInfo.auditor }}</a-descriptions-item>
<a-descriptions-item label="审核时间">{{ systemInfo.auditTime }}</a-descriptions-item>
</a-descriptions>
</a-card>
</div>
</a-tab-pane>
<!-- 项目预算 -->
<a-tab-pane key="budget" title="项目预算">
<div class="tab-content">
<a-table
:data="budgetHistoryData"
:columns="budgetHistoryColumns"
:pagination="false"
size="small"
>
<template #status="{ record }">
<a-tag :color="getBudgetStatusColor(record.status)">
{{ record.statusLabel }}
</a-tag>
</template>
<template #amount="{ record }">
<span class="amount-text">{{ record.amount.toFixed(2) }}</span>
</template>
</a-table>
</div>
</a-tab-pane>
<!-- 预算附件 -->
<a-tab-pane key="attachments" title="预算附件">
<div class="tab-content">
<a-table
:data="attachmentData"
:columns="attachmentColumns"
:pagination="false"
size="small"
>
<template #action="{ record }">
<a-space>
<a-button type="text" size="small" @click="downloadAttachment(record)">
下载
</a-button>
<a-button type="text" size="small" @click="previewAttachment(record)">
预览
</a-button>
</a-space>
</template>
</a-table>
</div>
</a-tab-pane>
<!-- 操作记录 -->
<a-tab-pane key="logs" title="操作记录">
<div class="tab-content">
<a-table
:data="operationLogs"
:columns="logColumns"
:pagination="false"
size="small"
>
<template #operation="{ record }">
<a-tag :color="getOperationColor(record.operation)">
{{ record.operation }}
</a-tag>
</template>
</a-table>
</div>
</a-tab-pane>
</a-tabs>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from 'vue'
import type { TableColumnData } from '@arco-design/web-vue'
import type { BudgetRecordResp } from '@/apis/project/budget'
interface Props {
visible: boolean
budgetRecord?: BudgetRecordResp | null
}
interface Emits {
(e: 'update:visible', value: boolean): void
}
const props = withDefaults(defineProps<Props>(), {
visible: false,
budgetRecord: null
})
const emit = defineEmits<Emits>()
const visible = ref(props.visible)
const activeTab = ref('basic')
//
const projectDetailInfo = reactive({
projectName: '广州洪湖路一期项目',
projectCode: 'HWPC0001',
projectAmount: 126.00,
projectManager: '纪广',
managerPhone: '13888888888',
establishDate: '2024-05-02',
deliveryDate: '交付日期',
contactPerson: '李四',
remark: '请输入',
totalBudget: 75.00,
remainingBudget: 25.00
})
//
const systemInfo = reactive({
creator: '张三',
createTime: '2023-12-01 13:32',
modifier: '傅彩瀞',
modifyTime: '2024-02-02 11:16',
auditor: '傅彩瀞',
auditTime: '2024-02-02 11:16'
})
//
const budgetHistoryData = ref([
{
applyDate: '2024-05-02',
applicant: '张三',
amount: 75.00,
status: 'approved',
statusLabel: '已批准',
auditDate: '2024-05-03',
auditor: '李四'
},
{
applyDate: '2024-06-01',
applicant: '王五',
amount: 50.00,
status: 'pending',
statusLabel: '待审核',
auditDate: '',
auditor: ''
}
])
//
const attachmentData = ref([
{
id: '1',
fileName: '设备采购清单.xlsx',
fileSize: '2.5MB',
uploadTime: '2024-05-02 10:30',
uploader: '张三'
},
{
id: '2',
fileName: '预算说明文档.pdf',
fileSize: '1.2MB',
uploadTime: '2024-05-02 14:20',
uploader: '李四'
}
])
//
const operationLogs = ref([
{
time: '2024-05-03 09:15',
operator: '李四',
operation: '审核通过',
remark: '预算合理,同意申请'
},
{
time: '2024-05-02 16:30',
operator: '张三',
operation: '提交申请',
remark: '申请设备采购预算'
},
{
time: '2024-05-02 14:20',
operator: '张三',
operation: '上传附件',
remark: '上传预算说明文档'
}
])
//
const budgetHistoryColumns: TableColumnData[] = [
{ title: '申请日期', dataIndex: 'applyDate', width: 120 },
{ title: '申请人', dataIndex: 'applicant', width: 100 },
{ title: '申请金额(万)', dataIndex: 'amount', slotName: 'amount', width: 120 },
{ title: '状态', dataIndex: 'status', slotName: 'status', width: 100 },
{ title: '审核日期', dataIndex: 'auditDate', width: 120 },
{ title: '审核人', dataIndex: 'auditor', width: 100 }
]
const attachmentColumns: TableColumnData[] = [
{ title: '文件名', dataIndex: 'fileName', width: 200 },
{ title: '文件大小', dataIndex: 'fileSize', width: 100 },
{ title: '上传时间', dataIndex: 'uploadTime', width: 160 },
{ title: '上传人', dataIndex: 'uploader', width: 100 },
{ title: '操作', slotName: 'action', width: 120 }
]
const logColumns: TableColumnData[] = [
{ title: '操作时间', dataIndex: 'time', width: 160 },
{ title: '操作人', dataIndex: 'operator', width: 100 },
{ title: '操作类型', dataIndex: 'operation', slotName: 'operation', width: 120 },
{ title: '备注', dataIndex: 'remark', ellipsis: true, tooltip: true }
]
// visible
watch(() => props.visible, (newVal) => {
visible.value = newVal
if (newVal) {
activeTab.value = 'basic'
}
})
watch(visible, (newVal) => {
emit('update:visible', newVal)
})
//
const getBudgetStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
'approved': 'green',
'pending': 'orange',
'rejected': 'red'
}
return colorMap[status] || 'gray'
}
//
const getOperationColor = (operation: string) => {
const colorMap: Record<string, string> = {
'审核通过': 'green',
'审核拒绝': 'red',
'提交申请': 'blue',
'上传附件': 'purple'
}
return colorMap[operation] || 'gray'
}
//
const downloadAttachment = (record: any) => {
//
console.log('下载附件:', record.fileName)
}
//
const previewAttachment = (record: any) => {
//
console.log('预览附件:', record.fileName)
}
</script>
<style scoped lang="scss">
.project-overview {
margin-bottom: 24px;
.overview-info {
background: #fafafa;
}
}
.detail-tabs {
.tab-content {
padding: 20px 0;
}
.info-card {
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
}
.amount-text {
font-weight: 500;
color: #f56500;
}
}
</style>

View File

@ -0,0 +1,332 @@
<!--
项目预算记录页面
包含预算记录列表预算申请预算审核等功能
-->
<template>
<GiPageLayout>
<GiTable
row-key="id"
:data="dataList"
:columns="tableColumns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 2000 }"
:pagination="pagination"
:disabled-tools="['size']"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
@refresh="search"
>
<template #top>
<GiForm
v-model="searchForm"
search
:columns="queryFormColumns"
size="medium"
@search="search"
@reset="reset"
/>
</template>
<template #toolbar-left>
<a-button v-permission="['budget:apply']" type="primary" @click="() => openApplyModal()">
<template #icon><icon-plus /></template>
<template #default>预算申请</template>
</a-button>
</template>
<template #toolbar-right>
<a-button v-permission="['budget:export']" @click="exportData">
<template #icon><icon-download /></template>
<template #default>导出</template>
</a-button>
</template>
<!-- 项目金额 -->
<template #projectAmount="{ record }">
<span class="font-medium text-blue-600">{{ record.projectAmount }}</span>
</template>
<!-- 申请预算金额 -->
<template #applyBudgetAmount="{ record }">
<span class="font-medium text-orange-600">{{ record.applyBudgetAmount }}</span>
</template>
<!-- 审核状态 -->
<template #auditStatus="{ record }">
<a-tag :color="getStatusColor(record.auditStatus)">
{{ record.auditStatusLabel }}
</a-tag>
</template>
<!-- 操作列 -->
<template #action="{ record }">
<a-space>
<a-link @click="viewDetail(record)">详情</a-link>
<a-link
v-if="record.auditStatus === 'pending'"
v-permission="['budget:audit']"
@click="openAuditModal(record)"
>
审核
</a-link>
<a-link
v-if="record.auditStatus === 'approved'"
v-permission="['budget:apply']"
@click="openApplyModal(record.projectId)"
>
追加预算
</a-link>
</a-space>
</template>
</GiTable>
<!-- 预算申请弹窗 -->
<BudgetApplyModal
v-model:visible="applyModalVisible"
:project-id="selectedProjectId"
@success="handleApplySuccess"
/>
<!-- 预算审核弹窗 -->
<BudgetAuditModal
v-model:visible="auditModalVisible"
:budget-record="selectedRecord"
@success="handleAuditSuccess"
/>
<!-- 详情弹窗 -->
<BudgetDetailModal
v-model:visible="detailModalVisible"
:budget-record="selectedRecord"
/>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
import {
listBudgetRecord,
exportBudgetRecord,
type BudgetRecordResp,
type BudgetPageQuery
} from '@/apis/project/budget'
import BudgetApplyModal from './components/BudgetApplyModal.vue'
import BudgetAuditModal from './components/BudgetAuditModal.vue'
import BudgetDetailModal from './components/BudgetDetailModal.vue'
//
const searchForm = reactive<BudgetPageQuery>({
projectName: '',
projectCode: '',
auditStatus: '',
applicant: '',
applyTimeStart: '',
applyTimeEnd: '',
page: 1,
size: 10
})
//
const queryFormColumns = [
{
field: 'projectName',
label: '项目名称',
type: 'input' as const,
props: {
placeholder: '请输入项目名称'
}
},
{
field: 'projectCode',
label: '项目编号',
type: 'input' as const,
props: {
placeholder: '请输入项目编号'
}
},
{
field: 'auditStatus',
label: '审核状态',
type: 'select' as const,
props: {
placeholder: '请选择审核状态',
options: [
{ label: '待审核', value: 'pending' },
{ label: '审核通过', value: 'approved' },
{ label: '审核拒绝', value: 'rejected' }
]
}
},
{
field: 'applicant',
label: '申请人',
type: 'input' as const,
props: {
placeholder: '请输入申请人'
}
}
]
//
const tableColumns: TableColumnData[] = [
{ title: '项目名称', dataIndex: 'projectName', width: 150, ellipsis: true, tooltip: true },
{ title: '项目编号', dataIndex: 'projectCode', width: 120 },
{ title: '项目金额(万)', dataIndex: 'projectAmount', slotName: 'projectAmount', width: 120 },
{ title: '项目负责人', dataIndex: 'projectManager', width: 100 },
{ title: '立项日期', dataIndex: 'establishDate', width: 110 },
{ title: '交付日期', dataIndex: 'deliveryDate', width: 110 },
{ title: '客户', dataIndex: 'client', width: 120, ellipsis: true, tooltip: true },
{ title: '备注说明', dataIndex: 'remark', width: 120, ellipsis: true, tooltip: true },
{ title: '申请预算金额(万)', dataIndex: 'applyBudgetAmount', slotName: 'applyBudgetAmount', width: 140 },
{ title: '申请人', dataIndex: 'applicant', width: 100 },
{ title: '申请时间', dataIndex: 'applyTime', width: 160 },
{ title: '审核状态', dataIndex: 'auditStatus', slotName: 'auditStatus', width: 100 },
{ title: '审核说明', dataIndex: 'auditRemark', width: 120, ellipsis: true, tooltip: true },
{ title: '审核人', dataIndex: 'auditor', width: 100 },
{ title: '审核时间', dataIndex: 'auditTime', width: 160 },
{ title: '操作', slotName: 'action', width: 150, fixed: 'right' }
]
//
const loading = ref(false)
const dataList = ref<BudgetRecordResp[]>([])
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showTotal: true,
showPageSize: true
})
//
const search = async () => {
try {
loading.value = true
const { data } = await listBudgetRecord(searchForm)
dataList.value = data.list || []
pagination.total = data.total || 0
} catch (error) {
console.error('查询预算记录失败:', error)
Message.error('查询预算记录失败')
} finally {
loading.value = false
}
}
const reset = () => {
Object.assign(searchForm, {
projectName: '',
projectCode: '',
auditStatus: '',
applicant: '',
applyTimeStart: '',
applyTimeEnd: '',
page: 1,
size: 10
})
pagination.current = 1
search()
}
//
const onPageChange = (page: number) => {
searchForm.page = page
pagination.current = page
search()
}
const onPageSizeChange = (size: number) => {
searchForm.size = size
searchForm.page = 1
pagination.pageSize = size
pagination.current = 1
search()
}
//
const applyModalVisible = ref(false)
const auditModalVisible = ref(false)
const detailModalVisible = ref(false)
const selectedProjectId = ref('')
const selectedRecord = ref<BudgetRecordResp | null>(null)
//
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
'pending': 'orange',
'approved': 'green',
'rejected': 'red'
}
return colorMap[status] || 'gray'
}
//
const openApplyModal = (projectId?: string) => {
selectedProjectId.value = projectId || ''
applyModalVisible.value = true
}
//
const openAuditModal = (record: BudgetRecordResp) => {
selectedRecord.value = record
auditModalVisible.value = true
}
//
const viewDetail = (record: BudgetRecordResp) => {
selectedRecord.value = record
detailModalVisible.value = true
}
//
const handleApplySuccess = () => {
Message.success('预算申请提交成功')
search()
}
//
const handleAuditSuccess = () => {
Message.success('预算审核完成')
search()
}
//
const exportData = async () => {
try {
const response = await exportBudgetRecord(searchForm)
//
const url = window.URL.createObjectURL(new Blob([response.data]))
const link = document.createElement('a')
link.href = url
link.download = `项目预算记录_${new Date().toLocaleDateString()}.xlsx`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
Message.success('导出成功')
} catch (error) {
Message.error('导出失败')
}
}
onMounted(() => {
search()
})
</script>
<style scoped lang="scss">
.font-medium {
font-weight: 500;
}
.text-blue-600 {
color: rgb(37, 99, 235);
}
.text-orange-600 {
color: rgb(234, 88, 12);
}
</style>

View File

@ -1,3 +1,15 @@
<!--
项目管理页面
已完成接口对接:
1. 项目列表查询 (listProject) - 支持分页和条件查询
2. 项目新增 (addProject)
3. 项目修改 (updateProject)
4. 项目删除 (deleteProject)
5. 项目导出 (exportProject)
6. 项目导入 (importProject)
所有API调用都已添加错误处理和类型安全检查
-->
<template>
<GiPageLayout>
<GiTable
@ -154,9 +166,9 @@
<a-col :span="12">
<a-form-item field="projectCategory" label="项目类型/服务" required>
<a-select v-model="form.projectCategory" placeholder="请选择">
<a-option value="外部工作">外部工作</a-option>
<a-option value="内部项目">内部项目</a-option>
<a-option value="技术服务">技术服务</a-option>
<a-option v-for="option in PROJECT_CATEGORY_OPTIONS" :key="option.value" :value="option.value">
{{ option.label }}
</a-option>
</a-select>
</a-form-item>
</a-col>
@ -173,9 +185,9 @@
<a-col :span="12">
<a-form-item field="status" label="状态" required>
<a-select v-model="form.status" placeholder="请选择">
<a-option value="施工中">施工中</a-option>
<a-option value="已完成">已完成</a-option>
<a-option value="未开始">未开始</a-option>
<a-option v-for="option in PROJECT_STATUS_OPTIONS" :key="option.value" :value="option.value">
{{ option.label }}
</a-option>
</a-select>
</a-form-item>
</a-col>
@ -240,26 +252,61 @@
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { Message, Modal } from '@arco-design/web-vue'
import { addProject, deleteProject, listProject, updateProject, exportProject } from '@/apis/project'
import { addProject, deleteProject, listProject, updateProject, exportProject, importProject } from '@/apis/project'
import { isMobile } from '@/utils'
import has from '@/utils/has'
import type { ColumnItem } from '@/components/GiForm'
import type { TableColumnData } from '@arco-design/web-vue'
import type { ProjectResp } from '@/apis/project/type'
import type { ProjectResp, ProjectPageQuery } from '@/apis/project/type'
defineOptions({ name: 'ProjectManagement' })
// (API)
const PROJECT_STATUS = {
NOT_STARTED: 0, // /
IN_PROGRESS: 1, //
COMPLETED: 2, //
} as const
//
const PROJECT_STATUS_MAP = {
0: '待施工',
1: '施工中',
2: '已完成'
} as const
//
const PROJECT_STATUS_OPTIONS = [
{ label: '待施工', value: 0 },
{ label: '施工中', value: 1 },
{ label: '已完成', value: 2 }
]
//
const PROJECT_CATEGORY = {
EXTERNAL_WORK: '外部工作',
INTERNAL_PROJECT: '内部项目',
TECHNICAL_SERVICE: '技术服务'
} as const
//
const PROJECT_CATEGORY_OPTIONS = [
{ label: PROJECT_CATEGORY.EXTERNAL_WORK, value: PROJECT_CATEGORY.EXTERNAL_WORK },
{ label: PROJECT_CATEGORY.INTERNAL_PROJECT, value: PROJECT_CATEGORY.INTERNAL_PROJECT },
{ label: PROJECT_CATEGORY.TECHNICAL_SERVICE, value: PROJECT_CATEGORY.TECHNICAL_SERVICE }
]
const router = useRouter()
const formRef = ref()
const loading = ref(false)
const addModalVisible = ref(false)
const importModalVisible = ref(false)
const isEdit = ref(false)
const currentId = ref<number | null>(null)
const currentId = ref<string | null>(null)
const fileList = ref([])
const dataList = ref<ProjectResp[]>([])
const searchForm = reactive({
const searchForm = reactive<Partial<ProjectPageQuery>>({
projectName: '',
status: undefined,
fieldName: '',
@ -290,11 +337,7 @@ const queryFormColumns: ColumnItem[] = reactive([
field: 'status',
span: { xs: 24, sm: 8, xxl: 8 },
props: {
options: [
{ label: '施工中', value: '施工中' },
{ label: '已完成', value: '已完成' },
{ label: '未开始', value: '未开始' }
],
options: PROJECT_STATUS_OPTIONS,
placeholder: '请选择状态',
},
},
@ -317,7 +360,7 @@ const form = reactive({
projectManager: '',
projectStaff: [] as string[],
projectPeriod: [] as string[],
status: '施工中'
status: PROJECT_STATUS.IN_PROGRESS //
})
const pagination = reactive({
@ -337,6 +380,14 @@ const tableColumns = ref<TableColumnData[]>([
render: ({ rowIndex }) => rowIndex + 1 + (pagination.current - 1) * pagination.pageSize,
fixed: !isMobile() ? 'left' : undefined,
},
{
title: '项目编号',
dataIndex: 'projectCode',
width: 120,
ellipsis: true,
tooltip: true,
fixed: !isMobile() ? 'left' : undefined,
},
{
title: '项目名称',
dataIndex: 'projectName',
@ -428,11 +479,11 @@ const modalTitle = computed(() => isEdit.value ? '编辑项目' : '新增项目'
const getStatusColor = (status: string) => {
switch (status) {
case '施工中':
case PROJECT_STATUS.IN_PROGRESS:
return 'blue'
case '已完成':
case PROJECT_STATUS.COMPLETED:
return 'green'
case '未开始':
case PROJECT_STATUS.NOT_STARTED:
return 'orange'
default:
return 'gray'
@ -442,16 +493,52 @@ const getStatusColor = (status: string) => {
const fetchData = async () => {
loading.value = true
try {
const res = await listProject({
const params: ProjectPageQuery = {
...searchForm,
page: pagination.current,
size: pagination.pageSize
})
dataList.value = res.data?.list || []
pagination.total = res.data?.total || 0
}
const res = await listProject(params)
if (res.success && res.data) {
// API
const projects = Array.isArray(res.data) ? res.data : []
//
dataList.value = projects.map((item: any) => ({
...item,
//
id: item.projectId,
fieldName: item.farmName,
fieldLocation: item.farmAddress,
commissionUnit: item.client,
commissionContact: item.clientContact,
commissionPhone: item.clientPhone,
orgNumber: item.turbineModel,
projectManager: item.projectManagerName,
//
projectPeriod: item.startDate && item.endDate ? [item.startDate, item.endDate] : [],
}))
// APItotal使
// total
pagination.total = projects.length
//
if (projects.length < pagination.pageSize) {
pagination.total = (pagination.current - 1) * pagination.pageSize + projects.length
}
} else {
Message.error(res.msg || '获取数据失败')
dataList.value = []
pagination.total = 0
}
} catch (error) {
console.error(error)
console.error('获取项目列表失败:', error)
Message.error('获取数据失败')
dataList.value = []
pagination.total = 0
} finally {
loading.value = false
}
@ -463,9 +550,15 @@ const search = () => {
}
const reset = () => {
searchForm.projectName = ''
searchForm.fieldName = ''
searchForm.status = undefined
//
Object.assign(searchForm, {
projectName: '',
fieldName: '',
status: undefined,
})
//
pagination.current = 1
search()
}
@ -491,14 +584,18 @@ const openAddModal = () => {
addModalVisible.value = true
}
const openEditModal = (record: any) => {
const openEditModal = (record: ProjectResp) => {
isEdit.value = true
currentId.value = record.id
currentId.value = record.id || record.projectId || null
//
Object.keys(form).forEach(key => {
if (key in record) {
form[key] = record[key]
if (key in record && record[key as keyof ProjectResp] !== undefined) {
// @ts-ignore -
form[key] = record[key as keyof ProjectResp]
}
})
addModalVisible.value = true
}
@ -507,23 +604,31 @@ const handleSubmit = async () => {
if (!valid) return false
try {
let res
if (isEdit.value && currentId.value) {
await updateProject(form, currentId.value)
res = await updateProject(form, currentId.value)
Message.success('更新成功')
} else {
await addProject(form)
res = await addProject(form)
Message.success('添加成功')
}
// APIsuccess
if (res && res.success === false) {
Message.error(res.msg || '操作失败')
return false
}
fetchData()
return true
} catch (error) {
console.error(error)
console.error('项目操作失败:', error)
Message.error('操作失败')
return false
}
}
const confirmDelete = (record: any) => {
const confirmDelete = (record: ProjectResp) => {
Modal.warning({
title: '确认删除',
content: `确定要删除项目"${record.projectName}"吗?`,
@ -531,22 +636,41 @@ const confirmDelete = (record: any) => {
})
}
const deleteItem = async (record: any) => {
const deleteItem = async (record: ProjectResp) => {
const projectId = record.id || record.projectId
if (!projectId) {
Message.error('项目ID不存在')
return
}
try {
await deleteProject(record.id)
const res = await deleteProject(projectId)
//
if (res && res.success === false) {
Message.error(res.msg || '删除失败')
return
}
Message.success('删除成功')
fetchData()
} catch (error) {
console.error(error)
console.error('删除项目失败:', error)
Message.error('删除失败')
}
}
const viewDetail = (record: any) => {
const viewDetail = (record: ProjectResp) => {
const projectId = record.id || record.projectId
if (!projectId) {
Message.error('项目ID不存在')
return
}
router.push({
name: 'ProjectDetail',
params: {
id: record.id
id: projectId.toString()
}
})
}
@ -565,21 +689,54 @@ const handleCancelImport = () => {
importModalVisible.value = false
}
const handleImport = () => {
const handleImport = async () => {
if (fileList.value.length === 0) {
Message.warning('请选择文件')
return false
}
//
Message.success('导入成功')
handleCancelImport()
return true
try {
const fileItem = fileList.value[0] as any
const file = fileItem?.file || fileItem
if (!file) {
Message.warning('请选择有效的文件')
return false
}
// API
const res = await importProject(file)
if (res && res.success === false) {
Message.error(res.msg || '导入失败')
return false
}
Message.success('导入成功')
handleCancelImport()
fetchData() //
return true
} catch (error) {
console.error('导入项目失败:', error)
Message.error('导入失败')
return false
}
}
const exportData = () => {
exportProject(searchForm)
Message.success('导出成功')
const exportData = async () => {
try {
const params = {
projectName: searchForm.projectName,
status: searchForm.status,
fieldName: searchForm.fieldName,
}
await exportProject(params)
Message.success('导出成功')
} catch (error) {
console.error('导出项目失败:', error)
Message.error('导出失败')
}
}
onMounted(() => {

View File

@ -0,0 +1,262 @@
<!--
任务新增/编辑弹窗组件
-->
<template>
<a-modal
v-model:visible="visible"
:title="isEdit ? '编辑任务' : '新增任务'"
width="800px"
@before-ok="handleSubmit"
@cancel="handleCancel"
>
<a-form ref="formRef" :model="form" :rules="rules" layout="vertical">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="taskName" label="任务名称" required>
<a-input v-model="form.taskName" placeholder="请输入任务名称" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="taskCode" label="任务编码" required>
<a-input v-model="form.taskCode" placeholder="请输入任务编码" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="responsiblePerson" label="负责人" required>
<a-input v-model="form.responsiblePerson" placeholder="请输入负责人" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="status" label="任务状态" required>
<a-select v-model="form.status" placeholder="请选择状态">
<a-option value="planning">计划中</a-option>
<a-option value="inProgress">正在做</a-option>
<a-option value="review">待复核</a-option>
<a-option value="completed">已完成</a-option>
<a-option value="other">其他</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="progress" label="任务进度">
<a-input-number
v-model="form.progress"
placeholder="请输入进度"
:min="0"
:max="100"
:precision="0"
style="width: 100%"
>
<template #suffix>%</template>
</a-input-number>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="plannedHours" label="计划工时">
<a-input-number
v-model="form.plannedHours"
placeholder="请输入计划工时"
:min="0"
:precision="1"
style="width: 100%"
>
<template #suffix>小时</template>
</a-input-number>
</a-form-item>
</a-col>
</a-row>
<a-form-item field="taskPeriod" label="任务周期">
<a-range-picker
v-model="form.taskPeriod"
style="width: 100%"
format="YYYY-MM-DD"
/>
</a-form-item>
<a-form-item field="participants" label="参与人员">
<a-select
v-model="form.participants"
placeholder="请选择参与人员"
multiple
allow-create
style="width: 100%"
>
<a-option v-for="user in userOptions" :key="user" :value="user">
{{ user }}
</a-option>
</a-select>
</a-form-item>
<a-form-item field="description" label="任务描述">
<a-textarea
v-model="form.description"
placeholder="请输入任务描述"
:rows="4"
/>
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup lang="ts">
import { ref, reactive, watch, nextTick } from 'vue'
import { Message, type FormInstance } from '@arco-design/web-vue'
import { addTask, updateTask, getTask } from '@/apis/project/task'
import type { TaskResp } from '@/apis/project/type'
interface Props {
visible: boolean
projectId?: string
initialStatus?: string
taskData?: TaskResp | null
}
interface Emits {
(e: 'update:visible', value: boolean): void
(e: 'success'): void
}
const props = withDefaults(defineProps<Props>(), {
visible: false,
initialStatus: 'planning',
taskData: null
})
const emit = defineEmits<Emits>()
const visible = ref(props.visible)
const formRef = ref<FormInstance>()
const isEdit = ref(false)
//
const form = reactive({
taskName: '',
taskCode: '',
responsiblePerson: '',
participants: [] as string[],
taskPeriod: [] as string[],
status: 'planning',
progress: 0,
plannedHours: 0,
description: '',
projectId: 0,
groupId: undefined as number | undefined,
superior: undefined as number | undefined
})
//
const rules = {
taskName: [{ required: true, message: '请输入任务名称' }],
taskCode: [{ required: true, message: '请输入任务编码' }],
responsiblePerson: [{ required: true, message: '请输入负责人' }],
status: [{ required: true, message: '请选择任务状态' }]
}
//
const userOptions = ref([
'张三', '李四', '王五', '赵六', '钱七', '孙八', '周九', '吴十'
])
// visible
watch(() => props.visible, (newVal) => {
visible.value = newVal
if (newVal) {
nextTick(() => {
initForm()
})
}
})
watch(visible, (newVal) => {
emit('update:visible', newVal)
})
//
const initForm = () => {
if (props.taskData) {
//
isEdit.value = true
form.taskName = props.taskData.taskName
form.taskCode = props.taskData.taskCode
form.responsiblePerson = props.taskData.responsiblePerson
form.participants = props.taskData.participants || []
form.taskPeriod = props.taskData.taskPeriod || []
form.status = props.taskData.status
form.progress = props.taskData.progress || 0
form.plannedHours = props.taskData.plannedHours || 0
form.description = props.taskData.description || ''
form.projectId = props.taskData.projectId
form.groupId = props.taskData.groupId
form.superior = props.taskData.superior
} else {
//
isEdit.value = false
resetForm()
form.status = props.initialStatus || 'planning'
form.projectId = Number(props.projectId) || 0
}
}
//
const resetForm = () => {
form.taskName = ''
form.taskCode = ''
form.responsiblePerson = ''
form.participants = []
form.taskPeriod = []
form.status = 'planning'
form.progress = 0
form.plannedHours = 0
form.description = ''
form.projectId = 0
form.groupId = undefined
form.superior = undefined
}
//
const handleSubmit = async () => {
try {
const isValid = await formRef.value?.validate()
if (!isValid) {
return false
}
const formData = {
...form,
taskPeriod: form.taskPeriod.length === 2 ? form.taskPeriod as [string, string] : undefined
}
if (isEdit.value && props.taskData) {
await updateTask(formData, props.taskData.id)
Message.success('任务更新成功')
} else {
await addTask(formData)
Message.success('任务创建成功')
}
emit('success')
return true
} catch (error) {
console.error('提交任务失败:', error)
Message.error(isEdit.value ? '更新任务失败' : '创建任务失败')
return false
}
}
//
const handleCancel = () => {
formRef.value?.clearValidate()
resetForm()
}
</script>
<style scoped lang="scss">
//
</style>

View File

@ -0,0 +1,674 @@
<!--
项目进度看板页面
实现Kanban风格的项目管理界面
-->
<template>
<div class="kanban-container">
<!-- 顶部工具栏 -->
<div class="kanban-header">
<div class="header-left">
<!-- 项目选择器 -->
<div class="project-selector">
<a-select
v-model="selectedProject"
placeholder="选择项目"
style="width: 200px"
@change="handleProjectChange"
>
<a-option
v-for="project in projectList"
:key="project.projectId"
:value="project.projectId"
>
{{ project.projectName }}
</a-option>
</a-select>
<a-button type="text" size="small">
<template #icon><icon-search /></template>
</a-button>
</div>
<!-- 项目进度 -->
<div v-if="currentProject" class="project-progress">
<span class="progress-label">项目进度</span>
<a-progress
:percent="projectProgress"
:width="200"
:stroke-width="8"
color="rgb(var(--success-6))"
/>
<span class="progress-percent">{{ projectProgress }}%</span>
</div>
</div>
<div class="header-right">
<a-input-search
v-model="searchKeyword"
placeholder="任务名称/负责人"
style="width: 200px"
@search="handleSearch"
/>
<a-button>查看进度</a-button>
<a-button @click="importTasks">导入任务</a-button>
<a-button type="primary" @click="showAddTaskGroupModal">
<template #icon><icon-plus /></template>
新增任务组
</a-button>
</div>
</div>
<!-- 看板主体 -->
<div class="kanban-board" v-if="currentProject">
<div class="board-columns">
<!-- 任务状态列 -->
<div
v-for="column in columns"
:key="column.status"
class="board-column"
@drop="handleDrop($event, column.status)"
@dragover="handleDragOver"
>
<div class="column-header">
<div class="column-title">
<span class="status-dot" :style="{ backgroundColor: column.color }"></span>
<span class="title-text">{{ column.title }}</span>
<span class="task-count">({{ getTaskCountByStatus(column.status) }})</span>
</div>
<a-dropdown>
<a-button type="text" size="small">
<template #icon><icon-more /></template>
</a-button>
<template #content>
<a-doption @click="addTask(column.status)">
<template #icon><icon-plus /></template>
新增任务
</a-doption>
</template>
</a-dropdown>
</div>
<div class="column-content">
<!-- 新增任务按钮 -->
<div class="add-task-btn" @click="addTask(column.status)">
<icon-plus />
新增任务
</div>
<!-- 任务卡片列表 -->
<div
v-for="task in getTasksByStatus(column.status)"
:key="task.id"
class="task-card"
draggable="true"
@dragstart="handleDragStart($event, task)"
@click="viewTaskDetail(task)"
>
<div class="task-header">
<h3 class="task-title">{{ task.taskName }}</h3>
<a-dropdown>
<a-button type="text" size="mini">
<template #icon><icon-more /></template>
</a-button>
<template #content>
<a-doption @click="editTask(task)">
<template #icon><icon-edit /></template>
编辑
</a-doption>
<a-doption @click="deleteTask(task)" class="danger">
<template #icon><icon-delete /></template>
删除
</a-doption>
</template>
</a-dropdown>
</div>
<div class="task-description">
{{ task.description || '该任务暂无描述信息,项目进度跟踪管理任务。' }}
</div>
<div class="task-dates">
<span v-if="task.taskPeriod">
{{ formatDate(task.taskPeriod[0]) }} - {{ formatDate(task.taskPeriod[1]) }}
</span>
</div>
<div class="task-priority">
<a-tag
:color="getPriorityColor((task as any).priority)"
size="small"
>
{{ getPriorityText((task as any).priority) }}
</a-tag>
</div>
<div class="task-footer">
<div class="task-progress">
<span class="progress-text">{{ task.progress || 0 }}%</span>
<div class="progress-icons">
<icon-message />
<span>2</span>
<icon-thumb-up />
<span>16</span>
<icon-eye />
<span>7</span>
</div>
</div>
<div class="task-avatars">
<a-avatar-group :size="24" :max-count="3">
<a-avatar
v-for="member in getTaskMembers(task)"
:key="member.id"
:style="{ backgroundColor: member.color }"
>
{{ member.name.charAt(0) }}
</a-avatar>
</a-avatar-group>
<span v-if="getTaskMembers(task).length > 3" class="more-count">
+{{ getTaskMembers(task).length - 3 }}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-else class="empty-state">
<a-empty description="请选择项目查看任务进度" />
</div>
<!-- 添加任务组弹窗 -->
<a-modal
v-model:visible="addTaskGroupVisible"
title="新增任务组"
@ok="handleAddTaskGroup"
@cancel="resetTaskGroupForm"
>
<a-form ref="taskGroupFormRef" :model="taskGroupForm">
<a-form-item field="groupName" label="任务组名称" required>
<a-input v-model="taskGroupForm.groupName" placeholder="请输入任务组名称" />
</a-form-item>
<a-form-item field="description" label="描述">
<a-textarea v-model="taskGroupForm.description" placeholder="请输入任务组描述" />
</a-form-item>
</a-form>
</a-modal>
<!-- 添加/编辑任务弹窗 -->
<TaskModal
v-model:visible="taskModalVisible"
:project-id="selectedProject"
:initial-status="initialTaskStatus"
:task-data="editingTask"
@success="refreshTasks"
/>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import { listProject, type ProjectResp } from '@/apis/project'
import { listTask, addTaskGroup, deleteTask as deleteTaskApi, updateTaskStatus } from '@/apis/project/task'
import type { TaskResp } from '@/apis/project/type'
import TaskModal from './components/TaskModal.vue'
//
const projectList = ref<ProjectResp[]>([])
const selectedProject = ref<string>('')
const currentProject = computed(() =>
projectList.value.find(p => p.projectId === selectedProject.value)
)
//
const taskList = ref<TaskResp[]>([])
const searchKeyword = ref('')
//
const columns = [
{ status: 'planning', title: '计划中', color: '#ff7d00' },
{ status: 'inProgress', title: '正在做', color: '#165dff' },
{ status: 'review', title: '待复核', color: '#ffb400' },
{ status: 'completed', title: '已完成', color: '#00b42a' },
{ status: 'other', title: '其他', color: '#86909c' },
]
//
const addTaskGroupVisible = ref(false)
const taskModalVisible = ref(false)
const initialTaskStatus = ref('')
const editingTask = ref<TaskResp | null>(null)
//
const taskGroupForm = reactive({
groupName: '',
description: ''
})
//
const projectProgress = computed(() => {
if (!taskList.value.length) return 0
const totalTasks = taskList.value.length
const completedTasks = taskList.value.filter(task => task.status === 'completed').length
return Math.round((completedTasks / totalTasks) * 100)
})
//
const getTasksByStatus = (status: string) => {
return taskList.value.filter(task => task.status === status)
}
//
const getTaskCountByStatus = (status: string) => {
return getTasksByStatus(status).length
}
//
const getTaskMembers = (task: TaskResp) => {
//
const colors = ['#165dff', '#00b42a', '#ff7d00', '#f53f3f', '#722ed1']
const members = task.participants || [task.responsiblePerson]
return members.map((name, index) => ({
id: index,
name: name,
color: colors[index % colors.length]
}))
}
//
const getPriorityColor = (priority: string) => {
const priorityMap: Record<string, string> = {
'high': 'red',
'medium': 'orange',
'low': 'green'
}
return priorityMap[priority] || 'blue'
}
//
const getPriorityText = (priority: string) => {
const priorityMap: Record<string, string> = {
'high': '高优先级',
'medium': '普通',
'low': '低优先级'
}
return priorityMap[priority] || '普通'
}
//
const draggedTask = ref<TaskResp | null>(null)
const handleDragStart = (e: DragEvent, task: TaskResp) => {
draggedTask.value = task
e.dataTransfer!.effectAllowed = 'move'
}
const handleDragOver = (e: DragEvent) => {
e.preventDefault()
e.dataTransfer!.dropEffect = 'move'
}
const handleDrop = async (e: DragEvent, newStatus: string) => {
e.preventDefault()
if (!draggedTask.value || draggedTask.value.status === newStatus) return
try {
//
await updateTaskStatus({ status: newStatus }, draggedTask.value.id)
draggedTask.value.status = newStatus
Message.success('任务状态更新成功')
} catch (error) {
Message.error('更新任务状态失败')
}
draggedTask.value = null
}
//
const loadProjects = async () => {
try {
const { data } = await listProject({ page: 1, size: 100 })
projectList.value = data.list || []
} catch (error) {
Message.error('加载项目列表失败')
}
}
//
const loadTasks = async () => {
if (!selectedProject.value) return
try {
const { data } = await listTask({
projectId: Number(selectedProject.value),
page: 1,
size: 100
})
taskList.value = data.list || []
} catch (error) {
Message.error('加载任务列表失败')
}
}
//
const handleProjectChange = () => {
loadTasks()
}
//
const handleSearch = () => {
loadTasks()
}
//
const showAddTaskGroupModal = () => {
if (!selectedProject.value) {
Message.warning('请先选择项目')
return
}
addTaskGroupVisible.value = true
}
//
const handleAddTaskGroup = async () => {
try {
await addTaskGroup({
...taskGroupForm,
projectId: Number(selectedProject.value)
})
Message.success('任务组添加成功')
addTaskGroupVisible.value = false
resetTaskGroupForm()
} catch (error) {
Message.error('添加任务组失败')
}
}
//
const resetTaskGroupForm = () => {
taskGroupForm.groupName = ''
taskGroupForm.description = ''
}
//
const addTask = (status: string) => {
if (!selectedProject.value) {
Message.warning('请先选择项目')
return
}
initialTaskStatus.value = status
editingTask.value = null
taskModalVisible.value = true
}
//
const editTask = (task: TaskResp) => {
editingTask.value = task
taskModalVisible.value = true
}
//
const viewTaskDetail = (task: TaskResp) => {
editTask(task)
}
//
const deleteTask = async (task: TaskResp) => {
try {
await deleteTaskApi(task.id)
Message.success('任务删除成功')
loadTasks()
} catch (error) {
Message.error('删除任务失败')
}
}
//
const importTasks = () => {
Message.info('导入功能开发中...')
}
//
const refreshTasks = () => {
loadTasks()
}
//
const formatDate = (date: string) => {
return date ? new Date(date).toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' }) : ''
}
onMounted(() => {
loadProjects()
})
</script>
<style scoped lang="scss">
.kanban-container {
height: 100vh;
background: #f7f8fa;
display: flex;
flex-direction: column;
}
.kanban-header {
background: white;
padding: 16px 24px;
border-bottom: 1px solid var(--color-border-2);
display: flex;
justify-content: space-between;
align-items: center;
.header-left {
display: flex;
align-items: center;
gap: 24px;
.project-selector {
display: flex;
align-items: center;
gap: 8px;
}
.project-progress {
display: flex;
align-items: center;
gap: 12px;
.progress-label {
font-size: 14px;
color: var(--color-text-2);
}
.progress-percent {
font-size: 14px;
font-weight: 500;
color: var(--color-text-1);
}
}
}
.header-right {
display: flex;
align-items: center;
gap: 12px;
}
}
.kanban-board {
flex: 1;
padding: 16px 24px;
overflow: hidden;
}
.board-columns {
display: flex;
gap: 16px;
height: 100%;
}
.board-column {
flex: 1;
background: white;
border-radius: 8px;
border: 1px solid var(--color-border-2);
display: flex;
flex-direction: column;
min-height: 0;
.column-header {
padding: 16px;
border-bottom: 1px solid var(--color-border-2);
display: flex;
justify-content: space-between;
align-items: center;
.column-title {
display: flex;
align-items: center;
gap: 8px;
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.title-text {
font-size: 14px;
font-weight: 500;
color: var(--color-text-1);
}
.task-count {
font-size: 12px;
color: var(--color-text-3);
}
}
}
.column-content {
flex: 1;
padding: 16px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 12px;
}
}
.add-task-btn {
padding: 12px;
border: 2px dashed var(--color-border-2);
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
color: var(--color-text-3);
cursor: pointer;
transition: all 0.2s;
&:hover {
border-color: var(--color-primary-6);
color: var(--color-primary-6);
}
}
.task-card {
background: white;
border: 1px solid var(--color-border-2);
border-radius: 6px;
padding: 12px;
cursor: pointer;
transition: all 0.2s;
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transform: translateY(-1px);
}
.task-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 8px;
.task-title {
font-size: 14px;
font-weight: 500;
color: var(--color-text-1);
margin: 0;
line-height: 1.4;
}
}
.task-description {
font-size: 12px;
color: var(--color-text-3);
line-height: 1.4;
margin-bottom: 12px;
}
.task-dates {
font-size: 12px;
color: var(--color-text-3);
margin-bottom: 8px;
}
.task-priority {
margin-bottom: 12px;
}
.task-footer {
display: flex;
justify-content: space-between;
align-items: center;
.task-progress {
display: flex;
align-items: center;
gap: 8px;
.progress-text {
font-size: 12px;
font-weight: 500;
color: var(--color-text-2);
}
.progress-icons {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: var(--color-text-3);
}
}
.task-avatars {
display: flex;
align-items: center;
gap: 4px;
.more-count {
font-size: 12px;
color: var(--color-text-3);
}
}
}
}
.empty-state {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
.danger {
color: var(--color-danger-6) !important;
}
</style>

View File

@ -0,0 +1,297 @@
<template>
<GiPageLayout>
<GiTable
row-key="id"
title="叶片内部检测"
:data="dataList"
:columns="tableColumns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1500 }"
:pagination="pagination"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
@refresh="search"
>
<template #top>
<GiForm
v-model="searchForm"
search
:columns="queryFormColumns"
size="medium"
@search="search"
@reset="reset"
/>
</template>
<template #toolbar-left>
<a-space>
<a-button type="primary" @click="openAddModal">
<template #icon><icon-plus /></template>
<template #default>新建检测任务</template>
</a-button>
<a-button @click="batchImport">
<template #icon><icon-upload /></template>
<template #default>批量导入</template>
</a-button>
</a-space>
</template>
<!-- 检测状态 -->
<template #status="{ record }">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<!-- 缺陷等级 -->
<template #defectLevel="{ record }">
<a-tag :color="getDefectColor(record.defectLevel)" v-if="record.defectLevel">
{{ record.defectLevel }}
</a-tag>
<span v-else>-</span>
</template>
<!-- 操作列 -->
<template #action="{ record }">
<a-space>
<a-link @click="viewDetail(record)">详情</a-link>
<a-link @click="viewImages(record)">检测图片</a-link>
<a-link @click="generateReport(record)">生成报告</a-link>
</a-space>
</template>
</GiTable>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
//
const searchForm = reactive({
projectName: '',
turbineCode: '',
bladePosition: '',
status: '',
page: 1,
size: 10
})
//
const queryFormColumns = [
{
field: 'projectName',
label: '项目名称',
type: 'input' as const,
props: {
placeholder: '请输入项目名称'
}
},
{
field: 'turbineCode',
label: '风机编号',
type: 'input' as const,
props: {
placeholder: '请输入风机编号'
}
},
{
field: 'bladePosition',
label: '叶片位置',
type: 'select' as const,
props: {
placeholder: '请选择叶片位置',
options: [
{ label: 'A叶片', value: 'A' },
{ label: 'B叶片', value: 'B' },
{ label: 'C叶片', value: 'C' }
]
}
},
{
field: 'status',
label: '检测状态',
type: 'select' as const,
props: {
placeholder: '请选择检测状态',
options: [
{ label: '待检测', value: 'pending' },
{ label: '检测中', value: 'in_progress' },
{ label: '已完成', value: 'completed' }
]
}
}
]
//
const tableColumns: TableColumnData[] = [
{ title: '检测编号', dataIndex: 'detectionCode', width: 120 },
{ title: '项目名称', dataIndex: 'projectName', width: 180, ellipsis: true, tooltip: true },
{ title: '风机编号', dataIndex: 'turbineCode', width: 100 },
{ title: '叶片位置', dataIndex: 'bladePosition', width: 100 },
{ title: '叶片型号', dataIndex: 'bladeModel', width: 120 },
{ title: '检测方法', dataIndex: 'detectionMethod', width: 120 },
{ title: '检测日期', dataIndex: 'detectionDate', width: 120 },
{ title: '检测状态', dataIndex: 'status', slotName: 'status', width: 100 },
{ title: '缺陷数量', dataIndex: 'defectCount', width: 100 },
{ title: '缺陷等级', dataIndex: 'defectLevel', slotName: 'defectLevel', width: 100 },
{ title: '检测人员', dataIndex: 'inspector', width: 120 },
{ title: '完成进度', dataIndex: 'progress', width: 100 },
{ title: '备注', dataIndex: 'remark', width: 200, ellipsis: true, tooltip: true },
{ title: '操作', slotName: 'action', width: 180, fixed: 'right' }
]
//
const loading = ref(false)
const dataList = ref([
{
id: 1,
detectionCode: 'BID2024001',
projectName: '华润风电场叶片内部检测',
turbineCode: 'WT001',
bladePosition: 'A',
bladeModel: 'LM82.5',
detectionMethod: '内窥镜检测',
detectionDate: '2024-01-18',
status: 'completed',
defectCount: 3,
defectLevel: '轻微',
inspector: '张检测师',
progress: '100%',
remark: '发现几处轻微粘接缺陷'
},
{
id: 2,
detectionCode: 'BID2024002',
projectName: '大唐风电场叶片内部检测',
turbineCode: 'WT002',
bladePosition: 'B',
bladeModel: 'GE87.0',
detectionMethod: '超声波检测',
detectionDate: '2024-01-20',
status: 'in_progress',
defectCount: 0,
defectLevel: '',
inspector: '李检测师',
progress: '65%',
remark: '正在进行根部检测'
},
{
id: 3,
detectionCode: 'BID2024003',
projectName: '国电投海上风电叶片检测',
turbineCode: 'WT003',
bladePosition: 'C',
bladeModel: 'V164-9.5',
detectionMethod: '红外热成像',
detectionDate: '2024-01-22',
status: 'pending',
defectCount: 0,
defectLevel: '',
inspector: '王检测师',
progress: '0%',
remark: '等待天气条件合适'
}
])
const pagination = reactive({
current: 1,
pageSize: 10,
total: 3,
showTotal: true,
showPageSize: true
})
//
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
'pending': 'orange',
'in_progress': 'blue',
'completed': 'green'
}
return colorMap[status] || 'gray'
}
//
const getStatusText = (status: string) => {
const textMap: Record<string, string> = {
'pending': '待检测',
'in_progress': '检测中',
'completed': '已完成'
}
return textMap[status] || status
}
//
const getDefectColor = (level: string) => {
const colorMap: Record<string, string> = {
'无缺陷': 'green',
'轻微': 'blue',
'中等': 'orange',
'严重': 'red'
}
return colorMap[level] || 'gray'
}
//
const search = async () => {
loading.value = true
setTimeout(() => {
loading.value = false
}, 1000)
}
const reset = () => {
Object.assign(searchForm, {
projectName: '',
turbineCode: '',
bladePosition: '',
status: '',
page: 1,
size: 10
})
pagination.current = 1
search()
}
//
const onPageChange = (page: number) => {
searchForm.page = page
pagination.current = page
search()
}
const onPageSizeChange = (size: number) => {
searchForm.size = size
searchForm.page = 1
pagination.pageSize = size
pagination.current = 1
search()
}
//
const openAddModal = () => {
Message.info('新建检测任务功能开发中...')
}
const batchImport = () => {
Message.info('批量导入功能开发中...')
}
const viewDetail = (record: any) => {
Message.info(`查看检测详情: ${record.projectName} - ${record.turbineCode}${record.bladePosition}叶片`)
}
const viewImages = (record: any) => {
Message.info(`查看检测图片: ${record.projectName} - ${record.turbineCode}${record.bladePosition}叶片`)
}
const generateReport = (record: any) => {
Message.info(`生成检测报告: ${record.projectName} - ${record.turbineCode}${record.bladePosition}叶片`)
}
onMounted(() => {
search()
})
</script>

View File

@ -0,0 +1,283 @@
<template>
<GiPageLayout>
<GiTable
row-key="id"
title="防雷检测服务"
:data="dataList"
:columns="tableColumns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1400 }"
:pagination="pagination"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
@refresh="search"
>
<template #top>
<GiForm
v-model="searchForm"
search
:columns="queryFormColumns"
size="medium"
@search="search"
@reset="reset"
/>
</template>
<template #toolbar-left>
<a-space>
<a-button type="primary" @click="openAddModal">
<template #icon><icon-plus /></template>
<template #default>新建检测任务</template>
</a-button>
<a-button @click="exportReport">
<template #icon><icon-download /></template>
<template #default>导出报告</template>
</a-button>
</a-space>
</template>
<!-- 检测状态 -->
<template #status="{ record }">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<!-- 检测结果 -->
<template #result="{ record }">
<a-tag :color="getResultColor(record.result)">
{{ record.result }}
</a-tag>
</template>
<!-- 操作列 -->
<template #action="{ record }">
<a-space>
<a-link @click="viewDetail(record)">详情</a-link>
<a-link @click="viewReport(record)">检测报告</a-link>
<a-link @click="editRecord(record)" v-if="record.status !== 'completed'">编辑</a-link>
</a-space>
</template>
</GiTable>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
//
const searchForm = reactive({
projectName: '',
client: '',
status: '',
detectionDate: '',
page: 1,
size: 10
})
//
const queryFormColumns = [
{
field: 'projectName',
label: '项目名称',
type: 'input' as const,
props: {
placeholder: '请输入项目名称'
}
},
{
field: 'client',
label: '客户',
type: 'input' as const,
props: {
placeholder: '请输入客户名称'
}
},
{
field: 'status',
label: '检测状态',
type: 'select' as const,
props: {
placeholder: '请选择检测状态',
options: [
{ label: '待检测', value: 'pending' },
{ label: '检测中', value: 'in_progress' },
{ label: '已完成', value: 'completed' },
{ label: '已取消', value: 'cancelled' }
]
}
}
]
//
const tableColumns: TableColumnData[] = [
{ title: '任务编号', dataIndex: 'taskCode', width: 120 },
{ title: '项目名称', dataIndex: 'projectName', width: 200, ellipsis: true, tooltip: true },
{ title: '客户', dataIndex: 'client', width: 150, ellipsis: true, tooltip: true },
{ title: '风机数量', dataIndex: 'turbineCount', width: 100 },
{ title: '检测类型', dataIndex: 'detectionType', width: 120 },
{ title: '计划检测时间', dataIndex: 'plannedDate', width: 120 },
{ title: '实际检测时间', dataIndex: 'actualDate', width: 120 },
{ title: '检测状态', dataIndex: 'status', slotName: 'status', width: 100 },
{ title: '检测结果', dataIndex: 'result', slotName: 'result', width: 100 },
{ title: '检测人员', dataIndex: 'inspector', width: 120 },
{ title: '合格率', dataIndex: 'passRate', width: 100 },
{ title: '备注', dataIndex: 'remark', width: 200, ellipsis: true, tooltip: true },
{ title: '操作', slotName: 'action', width: 180, fixed: 'right' }
]
//
const loading = ref(false)
const dataList = ref([
{
id: 1,
taskCode: 'LD2024001',
projectName: '华能新能源风电场防雷检测',
client: '华能新能源股份有限公司',
turbineCount: 25,
detectionType: '接地电阻检测',
plannedDate: '2024-01-20',
actualDate: '2024-01-20',
status: 'completed',
result: '合格',
inspector: '李检测师',
passRate: '96%',
remark: '检测完成,整体防雷系统良好'
},
{
id: 2,
taskCode: 'LD2024002',
projectName: '大唐风电场避雷器检测',
client: '大唐新能源股份有限公司',
turbineCount: 30,
detectionType: '避雷器性能检测',
plannedDate: '2024-01-25',
actualDate: '2024-01-25',
status: 'in_progress',
result: '检测中',
inspector: '王检测师',
passRate: '待确定',
remark: '正在进行第15台风机检测'
},
{
id: 3,
taskCode: 'LD2024003',
projectName: '国电投海上风电防雷检测',
client: '国家电力投资集团',
turbineCount: 40,
detectionType: '综合防雷检测',
plannedDate: '2024-02-01',
actualDate: '',
status: 'pending',
result: '待检测',
inspector: '张检测师',
passRate: '待确定',
remark: '海上风电场,需要特殊设备'
}
])
const pagination = reactive({
current: 1,
pageSize: 10,
total: 3,
showTotal: true,
showPageSize: true
})
//
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
'pending': 'orange',
'in_progress': 'blue',
'completed': 'green',
'cancelled': 'red'
}
return colorMap[status] || 'gray'
}
//
const getStatusText = (status: string) => {
const textMap: Record<string, string> = {
'pending': '待检测',
'in_progress': '检测中',
'completed': '已完成',
'cancelled': '已取消'
}
return textMap[status] || status
}
//
const getResultColor = (result: string) => {
const colorMap: Record<string, string> = {
'合格': 'green',
'不合格': 'red',
'待整改': 'orange',
'检测中': 'blue',
'待检测': 'gray'
}
return colorMap[result] || 'gray'
}
//
const search = async () => {
loading.value = true
setTimeout(() => {
loading.value = false
}, 1000)
}
const reset = () => {
Object.assign(searchForm, {
projectName: '',
client: '',
status: '',
detectionDate: '',
page: 1,
size: 10
})
pagination.current = 1
search()
}
//
const onPageChange = (page: number) => {
searchForm.page = page
pagination.current = page
search()
}
const onPageSizeChange = (size: number) => {
searchForm.size = size
searchForm.page = 1
pagination.pageSize = size
pagination.current = 1
search()
}
//
const openAddModal = () => {
Message.info('新建检测任务功能开发中...')
}
const exportReport = () => {
Message.info('导出检测报告功能开发中...')
}
const viewDetail = (record: any) => {
Message.info(`查看检测详情: ${record.projectName}`)
}
const viewReport = (record: any) => {
Message.info(`查看检测报告: ${record.projectName}`)
}
const editRecord = (record: any) => {
Message.info(`编辑检测任务: ${record.projectName}`)
}
onMounted(() => {
search()
})
</script>

View File

@ -0,0 +1,439 @@
<template>
<GiPageLayout>
<GiTable
row-key="id"
title="设备管理"
:data="dataList"
:columns="tableColumns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1600 }"
:pagination="pagination"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
@refresh="search"
>
<template #top>
<GiForm
v-model="searchForm"
search
:columns="queryFormColumns"
size="medium"
@search="search"
@reset="reset"
/>
</template>
<template #toolbar-left>
<a-space>
<a-button type="primary" @click="openAddModal">
<template #icon><icon-plus /></template>
<template #default>新增设备</template>
</a-button>
<a-button @click="batchImport">
<template #icon><icon-import /></template>
<template #default>批量导入</template>
</a-button>
<a-button @click="exportData">
<template #icon><icon-export /></template>
<template #default>导出数据</template>
</a-button>
</a-space>
</template>
<!-- 设备类型 -->
<template #deviceType="{ record }">
<a-tag :color="getDeviceTypeColor(record.deviceType)">
{{ record.deviceType }}
</a-tag>
</template>
<!-- 设备状态 -->
<template #status="{ record }">
<a-space>
<div
:class="getStatusDotClass(record.status)"
class="status-dot"
></div>
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</a-space>
</template>
<!-- 健康状况 -->
<template #healthStatus="{ record }">
<a-tag :color="getHealthColor(record.healthStatus)">
{{ record.healthStatus }}
</a-tag>
</template>
<!-- 负载率 -->
<template #loadRate="{ record }">
<a-progress
:percent="record.loadRate"
:color="getLoadColor(record.loadRate)"
size="small"
/>
</template>
<!-- 位置信息 -->
<template #location="{ record }">
<span>{{ record.location.building }} - {{ record.location.room }}</span>
</template>
<!-- 操作列 -->
<template #action="{ record }">
<a-space>
<a-link @click="viewDetail(record)">详情</a-link>
<a-link @click="editRecord(record)">编辑</a-link>
<a-link @click="remoteControl(record)">远程控制</a-link>
<a-link @click="viewMonitoring(record)">监控</a-link>
<a-dropdown>
<a-link>更多 <icon-down /></a-link>
<template #content>
<a-doption @click="deviceMaintenance(record)">设备维护</a-doption>
<a-doption @click="powerControl(record)">电源控制</a-doption>
<a-doption @click="deleteRecord(record)">删除设备</a-doption>
</template>
</a-dropdown>
</a-space>
</template>
</GiTable>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
//
const searchForm = reactive({
deviceName: '',
deviceType: '',
status: '',
location: '',
page: 1,
size: 10
})
//
const queryFormColumns = [
{
field: 'deviceName',
label: '设备名称',
type: 'input' as const,
props: {
placeholder: '请输入设备名称'
}
},
{
field: 'deviceType',
label: '设备类型',
type: 'select' as const,
props: {
placeholder: '请选择设备类型',
options: [
{ label: '服务器', value: '服务器' },
{ label: '交换机', value: '交换机' },
{ label: '路由器', value: '路由器' },
{ label: '存储设备', value: '存储设备' },
{ label: '防火墙', value: '防火墙' },
{ label: '打印机', value: '打印机' }
]
}
},
{
field: 'status',
label: '设备状态',
type: 'select' as const,
props: {
placeholder: '请选择设备状态',
options: [
{ label: '在线', value: 'online' },
{ label: '离线', value: 'offline' },
{ label: '维护中', value: 'maintenance' },
{ label: '故障', value: 'error' }
]
}
},
{
field: 'location',
label: '位置',
type: 'input' as const,
props: {
placeholder: '请输入位置信息'
}
}
]
//
const tableColumns: TableColumnData[] = [
{ title: '设备编号', dataIndex: 'deviceCode', width: 120 },
{ title: '设备名称', dataIndex: 'deviceName', width: 150 },
{ title: '设备类型', dataIndex: 'deviceType', slotName: 'deviceType', width: 120 },
{ title: 'IP地址', dataIndex: 'ipAddress', width: 130 },
{ title: 'MAC地址', dataIndex: 'macAddress', width: 150 },
{ title: '设备状态', dataIndex: 'status', slotName: 'status', width: 130 },
{ title: '健康状况', dataIndex: 'healthStatus', slotName: 'healthStatus', width: 100 },
{ title: '负载率', dataIndex: 'loadRate', slotName: 'loadRate', width: 120 },
{ title: '位置', dataIndex: 'location', slotName: 'location', width: 150 },
{ title: '负责人', dataIndex: 'manager', width: 100 },
{ title: '最后维护', dataIndex: 'lastMaintenance', width: 120 },
{ title: '采购日期', dataIndex: 'purchaseDate', width: 120 },
{ title: '保修期至', dataIndex: 'warrantyUntil', width: 120 },
{ title: '备注', dataIndex: 'remark', width: 200, ellipsis: true, tooltip: true },
{ title: '操作', slotName: 'action', width: 200, fixed: 'right' }
]
//
const loading = ref(false)
const dataList = ref([
{
id: 1,
deviceCode: 'SRV-001',
deviceName: '主服务器-01',
deviceType: '服务器',
ipAddress: '192.168.1.10',
macAddress: '00:1B:21:3C:4D:5E',
status: 'online',
healthStatus: '良好',
loadRate: 65,
location: { building: 'A栋', room: '机房101' },
manager: '张工程师',
lastMaintenance: '2024-03-01',
purchaseDate: '2023-01-15',
warrantyUntil: '2026-01-15',
remark: '核心业务服务器24小时运行'
},
{
id: 2,
deviceCode: 'SW-001',
deviceName: '核心交换机',
deviceType: '交换机',
ipAddress: '192.168.1.1',
macAddress: '00:1B:21:3C:4D:5F',
status: 'online',
healthStatus: '良好',
loadRate: 45,
location: { building: 'A栋', room: '机房101' },
manager: '李工程师',
lastMaintenance: '2024-02-15',
purchaseDate: '2023-03-20',
warrantyUntil: '2026-03-20',
remark: '网络核心设备'
},
{
id: 3,
deviceCode: 'FW-001',
deviceName: '主防火墙',
deviceType: '防火墙',
ipAddress: '192.168.1.254',
macAddress: '00:1B:21:3C:4D:60',
status: 'maintenance',
healthStatus: '注意',
loadRate: 80,
location: { building: 'A栋', room: '机房101' },
manager: '王工程师',
lastMaintenance: '2024-03-10',
purchaseDate: '2023-05-10',
warrantyUntil: '2026-05-10',
remark: '安全防护设备,需要定期更新规则'
},
{
id: 4,
deviceCode: 'STO-001',
deviceName: '存储阵列-01',
deviceType: '存储设备',
ipAddress: '192.168.1.20',
macAddress: '00:1B:21:3C:4D:61',
status: 'offline',
healthStatus: '故障',
loadRate: 0,
location: { building: 'A栋', room: '机房102' },
manager: '赵工程师',
lastMaintenance: '2024-01-20',
purchaseDate: '2023-08-01',
warrantyUntil: '2026-08-01',
remark: '主存储设备,目前离线维修中'
}
])
const pagination = reactive({
current: 1,
pageSize: 10,
total: 4,
showTotal: true,
showPageSize: true
})
//
const getDeviceTypeColor = (type: string) => {
const colorMap: Record<string, string> = {
'服务器': 'blue',
'交换机': 'green',
'路由器': 'cyan',
'存储设备': 'purple',
'防火墙': 'red',
'打印机': 'orange'
}
return colorMap[type] || 'gray'
}
//
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
'online': 'green',
'offline': 'red',
'maintenance': 'orange',
'error': 'red'
}
return colorMap[status] || 'gray'
}
//
const getStatusText = (status: string) => {
const textMap: Record<string, string> = {
'online': '在线',
'offline': '离线',
'maintenance': '维护中',
'error': '故障'
}
return textMap[status] || status
}
//
const getStatusDotClass = (status: string) => {
const classMap: Record<string, string> = {
'online': 'online-dot',
'offline': 'offline-dot',
'maintenance': 'maintenance-dot',
'error': 'error-dot'
}
return classMap[status] || 'offline-dot'
}
//
const getHealthColor = (health: string) => {
const colorMap: Record<string, string> = {
'良好': 'green',
'注意': 'orange',
'故障': 'red',
'未知': 'gray'
}
return colorMap[health] || 'gray'
}
//
const getLoadColor = (load: number) => {
if (load >= 80) return '#ff4d4f'
if (load >= 60) return '#faad14'
return '#52c41a'
}
//
const search = async () => {
loading.value = true
setTimeout(() => {
loading.value = false
}, 1000)
}
const reset = () => {
Object.assign(searchForm, {
deviceName: '',
deviceType: '',
status: '',
location: '',
page: 1,
size: 10
})
pagination.current = 1
search()
}
//
const onPageChange = (page: number) => {
searchForm.page = page
pagination.current = page
search()
}
const onPageSizeChange = (size: number) => {
searchForm.size = size
searchForm.page = 1
pagination.pageSize = size
pagination.current = 1
search()
}
//
const openAddModal = () => {
Message.info('新增设备功能开发中...')
}
const batchImport = () => {
Message.info('批量导入功能开发中...')
}
const exportData = () => {
Message.info('导出数据功能开发中...')
}
const viewDetail = (record: any) => {
Message.info(`查看设备详情: ${record.deviceName}`)
}
const editRecord = (record: any) => {
Message.info(`编辑设备: ${record.deviceName}`)
}
const remoteControl = (record: any) => {
Message.info(`远程控制: ${record.deviceName}`)
}
const viewMonitoring = (record: any) => {
Message.info(`设备监控: ${record.deviceName}`)
}
const deviceMaintenance = (record: any) => {
Message.info(`设备维护: ${record.deviceName}`)
}
const powerControl = (record: any) => {
Message.info(`电源控制: ${record.deviceName}`)
}
const deleteRecord = (record: any) => {
Message.info(`删除设备: ${record.deviceName}`)
}
onMounted(() => {
search()
})
</script>
<style scoped>
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
}
.online-dot {
background-color: #52c41a;
}
.offline-dot {
background-color: #d9d9d9;
}
.maintenance-dot {
background-color: #faad14;
}
.error-dot {
background-color: #ff4d4f;
}
</style>

View File

@ -0,0 +1,407 @@
<template>
<GiPageLayout>
<GiTable
row-key="id"
title="软件管理"
:data="dataList"
:columns="tableColumns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1500 }"
:pagination="pagination"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
@refresh="search"
>
<template #top>
<GiForm
v-model="searchForm"
search
:columns="queryFormColumns"
size="medium"
@search="search"
@reset="reset"
/>
</template>
<template #toolbar-left>
<a-space>
<a-button type="primary" @click="openAddModal">
<template #icon><icon-plus /></template>
<template #default>新增软件</template>
</a-button>
<a-button @click="softwareDiscovery">
<template #icon><icon-search /></template>
<template #default>软件发现</template>
</a-button>
<a-button @click="licenseManagement">
<template #icon><icon-file /></template>
<template #default>许可证管理</template>
</a-button>
</a-space>
</template>
<!-- 软件类型 -->
<template #softwareType="{ record }">
<a-tag :color="getSoftwareTypeColor(record.softwareType)">
{{ record.softwareType }}
</a-tag>
</template>
<!-- 软件状态 -->
<template #status="{ record }">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<!-- 许可证状态 -->
<template #licenseStatus="{ record }">
<a-tag :color="getLicenseStatusColor(record.licenseStatus)">
{{ record.licenseStatus }}
</a-tag>
</template>
<!-- 安装数量 -->
<template #installCount="{ record }">
<a-space>
<span>{{ record.installedCount }}/{{ record.totalLicenses }}</span>
<a-progress
:percent="Math.round((record.installedCount / record.totalLicenses) * 100)"
size="small"
:color="record.installedCount > record.totalLicenses ? '#ff4d4f' : '#52c41a'"
/>
</a-space>
</template>
<!-- 操作列 -->
<template #action="{ record }">
<a-space>
<a-link @click="viewDetail(record)">详情</a-link>
<a-link @click="editRecord(record)">编辑</a-link>
<a-link @click="installSoftware(record)">安装</a-link>
<a-dropdown>
<a-link>更多 <icon-down /></a-link>
<template #content>
<a-doption @click="updateSoftware(record)">更新</a-doption>
<a-doption @click="uninstallSoftware(record)">卸载</a-doption>
<a-doption @click="manageLicense(record)">许可证管理</a-doption>
<a-doption @click="viewUsage(record)">使用统计</a-doption>
<a-doption @click="deleteRecord(record)">删除软件</a-doption>
</template>
</a-dropdown>
</a-space>
</template>
</GiTable>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
//
const searchForm = reactive({
softwareName: '',
softwareType: '',
status: '',
vendor: '',
page: 1,
size: 10
})
//
const queryFormColumns = [
{
field: 'softwareName',
label: '软件名称',
type: 'input' as const,
props: {
placeholder: '请输入软件名称'
}
},
{
field: 'softwareType',
label: '软件类型',
type: 'select' as const,
props: {
placeholder: '请选择软件类型',
options: [
{ label: '操作系统', value: '操作系统' },
{ label: '办公软件', value: '办公软件' },
{ label: '开发工具', value: '开发工具' },
{ label: '数据库', value: '数据库' },
{ label: '安全软件', value: '安全软件' },
{ label: '设计软件', value: '设计软件' }
]
}
},
{
field: 'status',
label: '软件状态',
type: 'select' as const,
props: {
placeholder: '请选择软件状态',
options: [
{ label: '已安装', value: 'installed' },
{ label: '待安装', value: 'pending' },
{ label: '已过期', value: 'expired' },
{ label: '待更新', value: 'need_update' }
]
}
},
{
field: 'vendor',
label: '厂商',
type: 'input' as const,
props: {
placeholder: '请输入厂商名称'
}
}
]
//
const tableColumns: TableColumnData[] = [
{ title: '软件名称', dataIndex: 'softwareName', width: 200 },
{ title: '软件类型', dataIndex: 'softwareType', slotName: 'softwareType', width: 120 },
{ title: '版本号', dataIndex: 'version', width: 120 },
{ title: '厂商', dataIndex: 'vendor', width: 150 },
{ title: '状态', dataIndex: 'status', slotName: 'status', width: 100 },
{ title: '许可证状态', dataIndex: 'licenseStatus', slotName: 'licenseStatus', width: 120 },
{ title: '安装数量', dataIndex: 'installCount', slotName: 'installCount', width: 150 },
{ title: '购买日期', dataIndex: 'purchaseDate', width: 120 },
{ title: '到期日期', dataIndex: 'expiryDate', width: 120 },
{ title: '年费用', dataIndex: 'annualCost', width: 100 },
{ title: '负责人', dataIndex: 'manager', width: 100 },
{ title: '备注', dataIndex: 'remark', width: 200, ellipsis: true, tooltip: true },
{ title: '操作', slotName: 'action', width: 180, fixed: 'right' }
]
//
const loading = ref(false)
const dataList = ref([
{
id: 1,
softwareName: 'Microsoft Office 365',
softwareType: '办公软件',
version: '2024.1',
vendor: 'Microsoft',
status: 'installed',
licenseStatus: '有效',
installedCount: 125,
totalLicenses: 150,
purchaseDate: '2023-01-01',
expiryDate: '2024-12-31',
annualCost: 98000,
manager: '张管理员',
remark: '企业版订阅,包含全功能套件'
},
{
id: 2,
softwareName: 'Windows Server 2022',
softwareType: '操作系统',
version: '2022',
vendor: 'Microsoft',
status: 'installed',
licenseStatus: '有效',
installedCount: 8,
totalLicenses: 10,
purchaseDate: '2023-03-15',
expiryDate: '2026-03-15',
annualCost: 45000,
manager: '李管理员',
remark: '服务器操作系统标准版'
},
{
id: 3,
softwareName: 'Adobe Creative Suite',
softwareType: '设计软件',
version: '2024',
vendor: 'Adobe',
status: 'installed',
licenseStatus: '即将过期',
installedCount: 15,
totalLicenses: 20,
purchaseDate: '2023-06-01',
expiryDate: '2024-05-31',
annualCost: 156000,
manager: '王管理员',
remark: '创意设计套件包含PS、AI等软件'
},
{
id: 4,
softwareName: 'Oracle Database 19c',
softwareType: '数据库',
version: '19.3.0',
vendor: 'Oracle',
status: 'installed',
licenseStatus: '有效',
installedCount: 2,
totalLicenses: 4,
purchaseDate: '2023-02-01',
expiryDate: '2025-02-01',
annualCost: 280000,
manager: '赵管理员',
remark: '企业级数据库,支持高并发'
},
{
id: 5,
softwareName: 'Kaspersky Endpoint Security',
softwareType: '安全软件',
version: '12.3',
vendor: 'Kaspersky',
status: 'need_update',
licenseStatus: '有效',
installedCount: 200,
totalLicenses: 200,
purchaseDate: '2023-09-01',
expiryDate: '2024-09-01',
annualCost: 32000,
manager: '钱管理员',
remark: '终端安全防护软件,需要更新版本'
}
])
const pagination = reactive({
current: 1,
pageSize: 10,
total: 5,
showTotal: true,
showPageSize: true
})
//
const getSoftwareTypeColor = (type: string) => {
const colorMap: Record<string, string> = {
'操作系统': 'blue',
'办公软件': 'green',
'开发工具': 'purple',
'数据库': 'orange',
'安全软件': 'red',
'设计软件': 'cyan'
}
return colorMap[type] || 'gray'
}
//
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
'installed': 'green',
'pending': 'gray',
'expired': 'red',
'need_update': 'orange'
}
return colorMap[status] || 'gray'
}
//
const getStatusText = (status: string) => {
const textMap: Record<string, string> = {
'installed': '已安装',
'pending': '待安装',
'expired': '已过期',
'need_update': '待更新'
}
return textMap[status] || status
}
//
const getLicenseStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
'有效': 'green',
'即将过期': 'orange',
'已过期': 'red',
'待激活': 'gray'
}
return colorMap[status] || 'gray'
}
//
const search = async () => {
loading.value = true
setTimeout(() => {
loading.value = false
}, 1000)
}
const reset = () => {
Object.assign(searchForm, {
softwareName: '',
softwareType: '',
status: '',
vendor: '',
page: 1,
size: 10
})
pagination.current = 1
search()
}
//
const onPageChange = (page: number) => {
searchForm.page = page
pagination.current = page
search()
}
const onPageSizeChange = (size: number) => {
searchForm.size = size
searchForm.page = 1
pagination.pageSize = size
pagination.current = 1
search()
}
//
const openAddModal = () => {
Message.info('新增软件功能开发中...')
}
const softwareDiscovery = () => {
Message.info('软件发现功能开发中...')
}
const licenseManagement = () => {
Message.info('许可证管理功能开发中...')
}
const viewDetail = (record: any) => {
Message.info(`查看软件详情: ${record.softwareName}`)
}
const editRecord = (record: any) => {
Message.info(`编辑软件: ${record.softwareName}`)
}
const installSoftware = (record: any) => {
Message.info(`安装软件: ${record.softwareName}`)
}
const updateSoftware = (record: any) => {
Message.info(`更新软件: ${record.softwareName}`)
}
const uninstallSoftware = (record: any) => {
Message.info(`卸载软件: ${record.softwareName}`)
}
const manageLicense = (record: any) => {
Message.info(`许可证管理: ${record.softwareName}`)
}
const viewUsage = (record: any) => {
Message.info(`使用统计: ${record.softwareName}`)
}
const deleteRecord = (record: any) => {
Message.info(`删除软件: ${record.softwareName}`)
}
onMounted(() => {
search()
})
</script>
<style scoped>
/* 这里可以添加自定义样式 */
</style>

View File

@ -0,0 +1,486 @@
<template>
<GiPageLayout>
<div class="system-backup-container">
<!-- 备份概览 -->
<a-card title="备份概览" class="mb-6">
<a-row :gutter="16">
<a-col :span="6">
<a-statistic title="总备份数量" :value="backupStats.totalBackups" />
</a-col>
<a-col :span="6">
<a-statistic title="成功备份" :value="backupStats.successBackups" />
</a-col>
<a-col :span="6">
<a-statistic title="失败备份" :value="backupStats.failedBackups" />
</a-col>
<a-col :span="6">
<a-statistic title="备份总大小" :value="backupStats.totalSize" suffix="GB" />
</a-col>
</a-row>
</a-card>
<!-- 备份任务管理 -->
<a-card title="备份任务管理" class="mb-6">
<template #extra>
<a-space>
<a-button type="primary" @click="createBackupTask">
<template #icon><icon-plus /></template>
创建备份任务
</a-button>
<a-button @click="scheduleBackup">
<template #icon><icon-schedule /></template>
定时备份设置
</a-button>
</a-space>
</template>
<GiTable
row-key="id"
:data="backupTasks"
:columns="taskColumns"
:pagination="taskPagination"
:loading="tasksLoading"
>
<!-- 备份类型 -->
<template #backupType="{ record }">
<a-tag :color="getBackupTypeColor(record.backupType)">
{{ record.backupType }}
</a-tag>
</template>
<!-- 任务状态 -->
<template #status="{ record }">
<a-space>
<div
:class="getStatusDotClass(record.status)"
class="status-dot"
></div>
<a-tag :color="getTaskStatusColor(record.status)">
{{ getTaskStatusText(record.status) }}
</a-tag>
</a-space>
</template>
<!-- 进度条 -->
<template #progress="{ record }">
<a-progress
v-if="record.status === 'running'"
:percent="record.progress"
size="small"
/>
<span v-else>-</span>
</template>
<!-- 备份大小 -->
<template #backupSize="{ record }">
<span v-if="record.backupSize">{{ record.backupSize }} GB</span>
<span v-else>-</span>
</template>
<!-- 操作 -->
<template #taskAction="{ record }">
<a-space>
<a-link @click="viewTaskDetail(record)">详情</a-link>
<a-link
@click="startBackup(record)"
v-if="record.status === 'stopped'"
>
开始
</a-link>
<a-link
@click="stopBackup(record)"
v-if="record.status === 'running'"
>
停止
</a-link>
<a-link @click="editTask(record)">编辑</a-link>
<a-link @click="deleteTask(record)">删除</a-link>
</a-space>
</template>
</GiTable>
</a-card>
<!-- 备份记录 -->
<a-card title="备份记录">
<template #extra>
<a-space>
<a-button @click="cleanupBackups">
<template #icon><icon-delete /></template>
清理过期备份
</a-button>
<a-button @click="restoreFromBackup">
<template #icon><icon-redo /></template>
恢复数据
</a-button>
</a-space>
</template>
<GiTable
row-key="id"
:data="backupRecords"
:columns="recordColumns"
:pagination="recordPagination"
:loading="recordsLoading"
>
<!-- 备份类型 -->
<template #recordBackupType="{ record }">
<a-tag :color="getBackupTypeColor(record.backupType)">
{{ record.backupType }}
</a-tag>
</template>
<!-- 备份状态 -->
<template #recordStatus="{ record }">
<a-tag :color="getRecordStatusColor(record.status)">
{{ getRecordStatusText(record.status) }}
</a-tag>
</template>
<!-- 文件大小 -->
<template #fileSize="{ record }">
<span class="font-medium">{{ record.fileSize }}</span>
</template>
<!-- 耗时 -->
<template #duration="{ record }">
<span>{{ record.duration }}</span>
</template>
<!-- 操作 -->
<template #recordAction="{ record }">
<a-space>
<a-link @click="downloadBackup(record)">下载</a-link>
<a-link @click="restoreBackup(record)">恢复</a-link>
<a-link @click="verifyBackup(record)">验证</a-link>
<a-link @click="deleteBackup(record)">删除</a-link>
</a-space>
</template>
</GiTable>
</a-card>
</div>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
//
const backupStats = reactive({
totalBackups: 156,
successBackups: 148,
failedBackups: 8,
totalSize: 2456.8
})
//
const backupTasks = ref([
{
id: 1,
taskName: '系统全量备份',
backupType: '全量备份',
targetPath: '/backup/system/full',
schedule: '每天 02:00',
status: 'running',
progress: 65,
backupSize: 125.6,
lastRun: '2024-03-15 02:00:00',
nextRun: '2024-03-16 02:00:00',
operator: '系统自动'
},
{
id: 2,
taskName: '数据库增量备份',
backupType: '增量备份',
targetPath: '/backup/database/incremental',
schedule: '每小时执行',
status: 'stopped',
progress: 0,
backupSize: null,
lastRun: '2024-03-15 01:00:00',
nextRun: '2024-03-15 16:00:00',
operator: '数据库管理员'
},
{
id: 3,
taskName: '用户文件备份',
backupType: '差异备份',
targetPath: '/backup/files/diff',
schedule: '每周日 03:30',
status: 'completed',
progress: 100,
backupSize: 89.2,
lastRun: '2024-03-10 03:30:00',
nextRun: '2024-03-17 03:30:00',
operator: '文件管理员'
}
])
//
const backupRecords = ref([
{
id: 1,
backupName: '系统全量备份_20240315',
backupType: '全量备份',
backupTime: '2024-03-15 02:00:00',
completedTime: '2024-03-15 04:30:00',
duration: '2小时30分钟',
fileSize: '125.6 GB',
filePath: '/backup/system/full/backup_20240315.tar.gz',
status: 'success',
operator: '系统自动'
},
{
id: 2,
backupName: '数据库增量备份_20240315',
backupType: '增量备份',
backupTime: '2024-03-15 01:00:00',
completedTime: '2024-03-15 01:15:00',
duration: '15分钟',
fileSize: '2.3 GB',
filePath: '/backup/database/incremental/inc_20240315.sql',
status: 'success',
operator: '数据库管理员'
},
{
id: 3,
backupName: '配置文件备份_20240314',
backupType: '配置备份',
backupTime: '2024-03-14 23:30:00',
completedTime: '2024-03-14 23:35:00',
duration: '5分钟',
fileSize: '156 MB',
filePath: '/backup/config/config_20240314.zip',
status: 'success',
operator: '系统管理员'
},
{
id: 4,
backupName: '应用数据备份_20240314',
backupType: '应用备份',
backupTime: '2024-03-14 22:00:00',
completedTime: '',
duration: '',
fileSize: '',
filePath: '',
status: 'failed',
operator: '应用管理员'
}
])
//
const taskColumns: TableColumnData[] = [
{ title: '任务名称', dataIndex: 'taskName', width: 200 },
{ title: '备份类型', dataIndex: 'backupType', slotName: 'backupType', width: 120 },
{ title: '目标路径', dataIndex: 'targetPath', width: 200 },
{ title: '执行计划', dataIndex: 'schedule', width: 120 },
{ title: '状态', dataIndex: 'status', slotName: 'status', width: 120 },
{ title: '进度', dataIndex: 'progress', slotName: 'progress', width: 120 },
{ title: '备份大小', dataIndex: 'backupSize', slotName: 'backupSize', width: 100 },
{ title: '上次执行', dataIndex: 'lastRun', width: 160 },
{ title: '下次执行', dataIndex: 'nextRun', width: 160 },
{ title: '操作人员', dataIndex: 'operator', width: 120 },
{ title: '操作', slotName: 'taskAction', width: 200 }
]
//
const recordColumns: TableColumnData[] = [
{ title: '备份名称', dataIndex: 'backupName', width: 250 },
{ title: '备份类型', dataIndex: 'backupType', slotName: 'recordBackupType', width: 120 },
{ title: '开始时间', dataIndex: 'backupTime', width: 160 },
{ title: '完成时间', dataIndex: 'completedTime', width: 160 },
{ title: '耗时', dataIndex: 'duration', slotName: 'duration', width: 120 },
{ title: '文件大小', dataIndex: 'fileSize', slotName: 'fileSize', width: 100 },
{ title: '状态', dataIndex: 'status', slotName: 'recordStatus', width: 100 },
{ title: '操作人员', dataIndex: 'operator', width: 120 },
{ title: '操作', slotName: 'recordAction', width: 200 }
]
//
const taskPagination = reactive({
current: 1,
pageSize: 10,
total: 3,
showTotal: true
})
const recordPagination = reactive({
current: 1,
pageSize: 10,
total: 4,
showTotal: true
})
const tasksLoading = ref(false)
const recordsLoading = ref(false)
//
const getBackupTypeColor = (type: string) => {
const colorMap: Record<string, string> = {
'全量备份': 'blue',
'增量备份': 'green',
'差异备份': 'orange',
'配置备份': 'purple',
'应用备份': 'cyan'
}
return colorMap[type] || 'gray'
}
//
const getTaskStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
'running': 'blue',
'stopped': 'gray',
'completed': 'green',
'failed': 'red'
}
return colorMap[status] || 'gray'
}
//
const getTaskStatusText = (status: string) => {
const textMap: Record<string, string> = {
'running': '运行中',
'stopped': '已停止',
'completed': '已完成',
'failed': '失败'
}
return textMap[status] || status
}
//
const getRecordStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
'success': 'green',
'failed': 'red',
'partial': 'orange'
}
return colorMap[status] || 'gray'
}
//
const getRecordStatusText = (status: string) => {
const textMap: Record<string, string> = {
'success': '成功',
'failed': '失败',
'partial': '部分成功'
}
return textMap[status] || status
}
//
const getStatusDotClass = (status: string) => {
const classMap: Record<string, string> = {
'running': 'running-dot',
'stopped': 'stopped-dot',
'completed': 'completed-dot',
'failed': 'failed-dot'
}
return classMap[status] || 'stopped-dot'
}
//
const createBackupTask = () => {
Message.info('创建备份任务功能开发中...')
}
const scheduleBackup = () => {
Message.info('定时备份设置功能开发中...')
}
const viewTaskDetail = (task: any) => {
Message.info(`查看任务详情: ${task.taskName}`)
}
const startBackup = (task: any) => {
Message.info(`开始备份: ${task.taskName}`)
}
const stopBackup = (task: any) => {
Message.info(`停止备份: ${task.taskName}`)
}
const editTask = (task: any) => {
Message.info(`编辑任务: ${task.taskName}`)
}
const deleteTask = (task: any) => {
Message.info(`删除任务: ${task.taskName}`)
}
const cleanupBackups = () => {
Message.info('清理过期备份功能开发中...')
}
const restoreFromBackup = () => {
Message.info('恢复数据功能开发中...')
}
const downloadBackup = (record: any) => {
Message.info(`下载备份: ${record.backupName}`)
}
const restoreBackup = (record: any) => {
Message.info(`恢复备份: ${record.backupName}`)
}
const verifyBackup = (record: any) => {
Message.info(`验证备份: ${record.backupName}`)
}
const deleteBackup = (record: any) => {
Message.info(`删除备份: ${record.backupName}`)
}
</script>
<style scoped>
.system-backup-container {
padding: 16px;
}
.mb-6 {
margin-bottom: 24px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
}
.running-dot {
background-color: #1890ff;
animation: pulse 1.5s infinite;
}
.stopped-dot {
background-color: #d9d9d9;
}
.completed-dot {
background-color: #52c41a;
}
.failed-dot {
background-color: #ff4d4f;
}
@keyframes pulse {
0% {
transform: scale(0.95);
box-shadow: 0 0 0 0 rgba(24, 144, 255, 0.7);
}
70% {
transform: scale(1);
box-shadow: 0 0 0 10px rgba(24, 144, 255, 0);
}
100% {
transform: scale(0.95);
box-shadow: 0 0 0 0 rgba(24, 144, 255, 0);
}
}
</style>

View File

@ -12,10 +12,19 @@
>
<a-form ref="formRef" :model="form" :rules="formRules" auto-label-width :layout="width >= 700 ? 'horizontal' : 'vertical'">
<a-form-item label="菜单类型" field="type">
<a-radio-group v-model="form.type" type="button" :disabled="isUpdate" @change="onChangeType">
<a-radio :value="1">目录</a-radio>
<a-radio :value="2">菜单</a-radio>
<a-radio :value="3">按钮</a-radio>
<a-radio-group
v-model="form.type"
type="button"
:disabled="isUpdate || loadingMenuType"
@change="onChangeType"
>
<a-radio
v-for="option in menuTypeOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="上级菜单" field="parentId">
@ -27,6 +36,8 @@
:data="(menuSelectTree as any)"
:fallback-option="false"
:filter-tree-node="filterOptions"
:loading="loadingMenuTree"
:disabled="loadingMenuTree"
/>
</a-form-item>
<a-row>
@ -144,6 +155,7 @@ import { type ColProps, type FormInstance, Message, type TreeNodeData } from '@a
import { useWindowSize } from '@vueuse/core'
import { mapTree } from 'xe-utils'
import { type MenuResp, getMenuDetail, updateMenuNew, addMenu, type MenuAddReq, type MenuUpdateReq, getMenuTree } from '@/apis/system/menu'
import { listMenuType } from '@/apis/common/common'
import { useResetReactive } from '@/hooks'
import { filterTree, transformPathToName } from '@/utils'
import { useComponentPaths } from '@/hooks/modules/useComponentPaths'
@ -165,8 +177,12 @@ const formRef = ref<FormInstance>()
const menuTreeData = ref<MenuResp[]>([])
const loadingMenuTree = ref(false)
//
const menuTypeOptions = ref<Array<{ label: string; value: string | number }>>([])
const loadingMenuType = ref(false)
const [form, resetForm] = useResetReactive({
type: 1,
type: 1, // 使1=, 2=, 3=
sort: 999,
isExternal: false,
isCache: false,
@ -227,23 +243,76 @@ const loadMenuTree = async () => {
try {
loadingMenuTree.value = true
const { data } = await getMenuTree()
menuTreeData.value = data
menuTreeData.value = data || []
} catch (error) {
console.error('加载菜单树失败:', error)
Message.error('加载菜单树失败,请稍后重试')
menuTreeData.value = []
} finally {
loadingMenuTree.value = false
}
}
// - 使
//
const menuTypeMap: Record<string, number> = {
'catalog': 1,
'route': 2, // API 'route' 'menu'
'button': 3
}
//
const loadMenuTypeOptions = async () => {
try {
loadingMenuType.value = true
const { data } = await listMenuType()
// API[{catalog: ""}, {route: ""}, {button: ""}]
menuTypeOptions.value = data.map(item => {
//
const key = Object.keys(item)[0]
const label = item[key]
return {
label: label,
value: menuTypeMap[key] || 1
}
})
} catch (error) {
console.error('加载菜单类型失败:', error)
// API使
menuTypeOptions.value = [
{ label: '目录', value: 1 },
{ label: '菜单', value: 2 },
{ label: '按钮', value: 3 }
]
} finally {
loadingMenuType.value = false
}
}
// -
const menuSelectTree = computed(() => {
const menus = JSON.parse(JSON.stringify(menuTreeData.value)) as MenuResp[]
const data = filterTree(menus, (i) => [1, 2].includes(i.type))
return mapTree(data, (i) => ({
key: i.id,
title: (i as any).menuName || i.title, //
const menus = JSON.parse(JSON.stringify(menuTreeData.value)) as any[]
//
const data = filterTree(menus, (i) => {
const menuType = i.menuType || i.type
// APIAPI
return ['catalog', 'route', 'menu', '1', '2', 1, 2].includes(menuType)
})
const treeData = mapTree(data, (i) => ({
key: i.menuId || i.id, // APImenuId
title: i.menuName || i.title, // APImenuName
children: i.children,
}))
// ""
const result = [
{
key: '0',
title: '无上级菜单',
children: treeData,
}
]
return result
})
//
@ -254,6 +323,16 @@ const filterOptions = (searchKey: string, nodeData: TreeNodeData) => {
return false
}
// API
const convertTypeToApiFormat = (type: number): string => {
const reverseMap: Record<number, string> = {
1: 'catalog',
2: 'route', // API使 'route' 'menu'
3: 'button'
}
return reverseMap[type] || String(type)
}
//
const save = async () => {
try {
@ -263,13 +342,13 @@ const save = async () => {
//
const updateMenuData: MenuUpdateReq = {
menuName: form.title,
menuType: String(form.type),
menuType: convertTypeToApiFormat(form.type),
orderNum: form.sort,
parentId: form.parentId || '0',
perms: form.permission || '',
terminalType: '', //
url: form.path || '',
visible: form.isHidden ? '1' : '0'
visible: form.isHidden ? '0' : '1' // '0''1'
}
await updateMenuNew(dataId.value, updateMenuData)
@ -278,13 +357,13 @@ const save = async () => {
//
const newMenuData: MenuAddReq = {
menuName: form.title,
menuType: String(form.type),
menuType: convertTypeToApiFormat(form.type),
orderNum: form.sort,
parentId: form.parentId || '0',
perms: form.permission || '',
terminalType: '', //
url: form.path || '',
visible: form.isHidden ? '1' : '0'
visible: form.isHidden ? '0' : '1' // '0''1'
}
// 使
@ -303,8 +382,8 @@ const onAdd = async (id?: string) => {
reset()
form.parentId = id || ''
dataId.value = ''
//
await loadMenuTree()
//
await Promise.all([loadMenuTree(), loadMenuTypeOptions()])
visible.value = true
}
@ -312,21 +391,52 @@ const onAdd = async (id?: string) => {
const onUpdate = async (id: string) => {
reset()
dataId.value = id
//
await loadMenuTree()
//
await Promise.all([loadMenuTree(), loadMenuTypeOptions()])
try {
// 使
const { data } = await getMenuDetail(id)
//
form.title = data.menuName
form.type = Number(data.menuType)
form.sort = data.orderNum
form.parentId = data.parentId
form.permission = data.perms
form.path = data.url
form.isHidden = data.visible === '1'
//
form.title = data.menuName || ''
//
form.type = menuTypeMap[data.menuType] || Number(data.menuType) || 1
//
form.sort = data.orderNum || 999
form.parentId = data.parentId || '0' // parentId
form.permission = data.perms || ''
form.path = data.url || ''
// - API'0''1'
form.isHidden = String(data.visible) === '0'
// 使any
const anyData = data as any
// API
if (anyData.name) form.name = anyData.name
if (anyData.component) form.component = anyData.component
if (anyData.redirect) form.redirect = anyData.redirect
if (anyData.icon) form.icon = anyData.icon
//
if (anyData.isExternal !== undefined) {
form.isExternal = String(anyData.isExternal) === '1' || anyData.isExternal === true
}
if (anyData.isCache !== undefined) {
form.isCache = String(anyData.isCache) === '1' || anyData.isCache === true
}
if (anyData.status !== undefined) {
form.status = String(anyData.status) === '1' || anyData.status === true ? 1 : 2
}
console.log('菜单详情数据回显完成:', {
原始数据: data,
表单数据: form
})
} catch (error) {
console.error('获取菜单详情失败:', error)
Message.error('获取菜单详情失败')

View File

@ -44,17 +44,19 @@
</template>
</a-button>
</template>
<template #title="{ record }">
<GiSvgIcon :name="record.icon" :size="15" />
<span style="margin-left: 5px; vertical-align: middle">{{ record.title }}</span>
<template #menuName="{ record }">
<GiSvgIcon :name="(record as any).icon" :size="15" />
<span style="margin-left: 5px; vertical-align: middle">{{ (record as any).menuName || (record as any).title }}</span>
</template>
<template #type="{ record }">
<a-tag v-if="record.type === 1" color="arcoblue">目录</a-tag>
<a-tag v-if="record.type === 2" color="green">菜单</a-tag>
<a-tag v-if="record.type === 3">按钮</a-tag>
<template #menuType="{ record }">
<a-tag :color="getMenuTypeColor((record as any).menuType || (record as any).type)" size="small">
{{ getMenuTypeLabel((record as any).menuType || (record as any).type) }}
</a-tag>
</template>
<template #status="{ record }">
<GiCellStatus :status="record.status" />
<template #visible="{ record }">
<a-tag :color="getMenuVisibleColor((record as any).visible)" size="small">
{{ getMenuVisibleLabel((record as any).visible) }}
</a-tag>
</template>
<template #isExternal="{ record }">
<a-tag v-if="record.isExternal" color="arcoblue" size="small"></a-tag>
@ -74,9 +76,9 @@
<a-link v-permission="['system:menu:delete']" status="danger" title="删除" @click="onDelete(record)">删除</a-link>
<a-link
v-permission="['system:menu:create']"
:disabled="![1, 2].includes(record.type)"
:title="![1, 2].includes(record.type) ? '不可添加下级菜单' : '新增'"
@click="onAdd(record.id)"
:disabled="!canAddSubMenu((record as any).menuType || (record as any).type)"
:title="!canAddSubMenu((record as any).menuType || (record as any).type) ? '不可添加下级菜单' : '新增'"
@click="onAdd((record as any).menuId || (record as any).id)"
>
新增
</a-link>
@ -111,6 +113,63 @@ const {
const dataList = computed(() => tableData.value)
//
const getMenuTypeLabel = (type: any): string => {
// APIAPI
const typeMap: Record<string | number, string> = {
'catalog': '目录',
'route': '菜单',
'button': '按钮',
1: '目录',
2: '菜单',
3: '按钮'
}
return typeMap[type] || String(type)
}
//
const getMenuTypeColor = (type: any): string => {
// APIAPI
const colorMap: Record<string | number, string> = {
'catalog': 'arcoblue',
'route': 'green',
'button': 'orange',
1: 'arcoblue',
2: 'green',
3: 'orange'
}
return colorMap[type] || 'gray'
}
//
const canAddSubMenu = (type: any): boolean => {
return ['catalog', 'route', 1, 2].includes(type)
}
//
const getMenuVisibleLabel = (visible: any): string => {
// APIvisible '0''1'
if (visible === '0' || visible === 0 || visible === false) {
return '隐藏'
}
if (visible === '1' || visible === 1 || visible === true) {
return '显示'
}
return String(visible) || '未知'
}
//
const getMenuVisibleColor = (visible: any): string => {
// 绿
if (visible === '0' || visible === 0 || visible === false) {
return 'red' //
}
if (visible === '1' || visible === 1 || visible === true) {
return 'green' // 绿
}
return 'gray'
}
const columns: TableInstance['columns'] = [
{ title: '菜单标题', dataIndex: 'menuName', slotName: 'menuName', width: 170, fixed: !isMobile() ? 'left' : undefined },
{ title: '类型', dataIndex: 'menuType', slotName: 'menuType', align: 'center' },
@ -140,9 +199,12 @@ const reset = () => {
}
//
const onDelete = (record: MenuResp) => {
return handleDelete(() => deleteMenu(record.menuId), {
content: `是否确定菜单「${record.title}」?`,
const onDelete = (record: any) => {
// API
const menuId = record.menuId || record.id
const menuTitle = record.menuName || record.title
return handleDelete(() => deleteMenu(menuId), {
content: `是否确定删除菜单「${menuTitle}」?`,
showModal: true,
})
}
@ -176,8 +238,10 @@ const onAdd = (parentId?: string) => {
}
//
const onUpdate = (record: MenuResp) => {
MenuAddModalRef.value?.onUpdate(record.menuId)
const onUpdate = (record: any) => {
// API
const menuId = record.menuId || record.id
MenuAddModalRef.value?.onUpdate(menuId)
}
</script>

View File

@ -191,7 +191,7 @@ const onClose = () => {
//
const open = () => {
fetchUnreadNotices()
// fetchUnreadNotices()
}
defineExpose({

View File

@ -61,13 +61,13 @@ const tabItems = computed(() => [
])
const getMessageData = async () => {
const { data } = await getUnreadMessageCount()
unreadMessageCount.value = data.total
// const { data } = await getUnreadMessageCount()
// unreadMessageCount.value = data.total
}
const getNoticeData = async () => {
const { data } = await getUnreadNoticeCount()
unreadNoticeCount.value = data.total
// const { data } = await getUnreadNoticeCount()
// unreadNoticeCount.value = data.total
}
onMounted(() => {