feat: 移动端开发规范重大更新

- 🔥 完全重写 .cursor/cursorrules-mobile.md (940行)
  * 基于实际mobile工程Vue 3.4.21+uniapp+uview-plus架构
  * 移除所有JeeSite相关内容,精准匹配实际项目
  * 新增11个完整章节:技术栈、项目结构、页面开发、组件规范等

- 📚 创建完整规范文档 docs/开发文档/移动端开发规范总结.md (800+行)
  * 涵盖从项目架构到部署的全流程指导
  * 提供可直接使用的页面和组件模板
  * 包含现代CSS变量设计系统和用户体验优化

- 🔄 同步更新主配置文件 .cursorrules
  * 更新移动端技术栈描述
  * 调整为Vue 3.4.21+uniapp+uview-plus 3.4.45架构

- 📝 完善变更记录
  * 详细记录规范更新背景和技术价值
  * 为后续维护提供完整历史记录

核心价值:解决规范与实际项目不匹配问题,为Cursor AI提供准确的移动端开发上下文,确保生成代码完全符合实际项目架构。
This commit is contained in:
zlwu committed 2025-07-04 15:51:33 +08:00
1 parent e8ece1b612
commit 28232d5d46
131 files changed
+17170 -5479

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
export * from './types';
export * from './info';
export * from './reservation';
+77
View File
@@ -0,0 +1,77 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { VenueInfoVO, VenueInfoForm, VenueInfoQuery } from './types';
/**
* 查询场地信息列表
*/
export const listVenueInfo = (query?: VenueInfoQuery): AxiosPromise<VenueInfoVO[]> => {
return request({
url: '/venue/info/list',
method: 'get',
params: query
});
};
/**
* 查询场地信息详细
*/
export const getVenueInfo = (id: string | number): AxiosPromise<VenueInfoVO> => {
return request({
url: `/venue/info/${id}`,
method: 'get'
});
};
/**
* 新增场地信息
*/
export const addVenueInfo = (data: VenueInfoForm) => {
return request({
url: '/venue/info',
method: 'post',
data: data
});
};
/**
* 修改场地信息
*/
export const updateVenueInfo = (data: VenueInfoForm) => {
return request({
url: '/venue/info',
method: 'put',
data: data
});
};
/**
* 删除场地信息
*/
export const delVenueInfo = (id: string | number | Array<string | number>) => {
return request({
url: `/venue/info/${id}`,
method: 'delete'
});
};
/**
* 获取可用场地列表
*/
export const getAvailableVenues = (): AxiosPromise<VenueInfoVO[]> => {
return request({
url: '/venue/info/available',
method: 'get'
});
};
/**
* 导出场地信息
*/
export const exportVenueInfo = (query: VenueInfoQuery) => {
return request({
url: '/venue/info/export',
method: 'post',
data: query
});
};
+118
View File
@@ -0,0 +1,118 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import {
VenueReservationVO,
VenueReservationForm,
VenueReservationQuery,
ReservationApproveForm,
ConflictCheckQuery,
ConflictReservationVO
} from './types';
/**
* 查询场地预约列表
*/
export const listVenueReservation = (query?: VenueReservationQuery) => {
return request({
url: '/venue/reservation/list',
method: 'get',
params: query
});
};
/**
* 查询我的预约列表
*/
export const listMyReservations = (query?: VenueReservationQuery) => {
return request({
url: '/venue/reservation/my',
method: 'get',
params: query
});
};
/**
* 查询待审批预约列表
*/
export const listPendingReservations = (query?: VenueReservationQuery) => {
return request({
url: '/venue/reservation/pending',
method: 'get',
params: query
});
};
/**
* 查询场地预约详细
*/
export const getVenueReservation = (id: string | number): AxiosPromise<VenueReservationVO> => {
return request({
url: `/venue/reservation/${id}`,
method: 'get'
});
};
/**
* 新增场地预约
*/
export const addVenueReservation = (data: VenueReservationForm) => {
return request({
url: '/venue/reservation',
method: 'post',
data: data
});
};
/**
* 修改场地预约
*/
export const updateVenueReservation = (data: VenueReservationForm) => {
return request({
url: '/venue/reservation',
method: 'put',
data: data
});
};
/**
* 删除场地预约
*/
export const delVenueReservation = (id: string | number | Array<string | number>) => {
return request({
url: `/venue/reservation/${id}`,
method: 'delete'
});
};
/**
* 检查时间冲突
*/
export const checkTimeConflict = (query: ConflictCheckQuery): AxiosPromise<ConflictReservationVO[]> => {
return request({
url: '/venue/reservation/conflict',
method: 'get',
params: query
});
};
/**
* 审批场地预约
*/
export const approveReservation = (data: ReservationApproveForm) => {
return request({
url: '/venue/reservation/approve',
method: 'post',
data: data
});
};
/**
* 导出场地预约
*/
export const exportVenueReservation = (query: VenueReservationQuery) => {
return request({
url: '/venue/reservation/export',
method: 'post',
data: query
});
};
+209
View File
@@ -0,0 +1,209 @@
/**
* 场地信息查询对象类型
*/
export interface VenueInfoQuery extends PageQuery {
/** 场地名称 */
venueName?: string;
/** 场地编码 */
venueCode?: string;
/** 场地位置 */
venueLocation?: string;
/** 场地类型 */
venueType?: string;
/** 场地状态 */
venueStatus?: string;
/** 管理员姓名 */
venueManagerName?: string;
}
/**
* 场地信息视图对象
*/
export interface VenueInfoVO {
/** 主键ID */
id?: string | number;
/** 场地名称 */
venueName?: string;
/** 场地编码 */
venueCode?: string;
/** 场地位置 */
venueLocation?: string;
/** 场地容量 */
venueCapacity?: number;
/** 场地类型 */
venueType?: string;
/** 场地状态 */
venueStatus?: string;
/** 场地描述 */
venueDescription?: string;
/** 场地图片 */
venueImages?: string;
/** 管理员ID */
venueManagerId?: string | number;
/** 管理员姓名 */
venueManagerName?: string;
/** 创建时间 */
createTime?: string;
/** 更新时间 */
updateTime?: string;
}
/**
* 场地信息表单类型
*/
export interface VenueInfoForm {
/** 主键ID */
id?: string | number;
/** 场地名称 */
venueName?: string;
/** 场地编码 */
venueCode?: string;
/** 场地位置 */
venueLocation?: string;
/** 场地容量 */
venueCapacity?: number;
/** 场地类型 */
venueType?: string;
/** 场地状态 */
venueStatus?: string;
/** 场地描述 */
venueDescription?: string;
/** 场地图片 */
venueImages?: string;
/** 管理员ID */
venueManagerId?: string | number;
/** 管理员姓名 */
venueManagerName?: string;
}
/**
* 场地预约查询对象类型
*/
export interface VenueReservationQuery extends PageQuery {
/** 场地ID */
venueId?: string | number;
/** 场地名称 */
venueName?: string;
/** 申请人姓名 */
applicantName?: string;
/** 预约日期 */
reservationDate?: string;
/** 预约状态 */
reservationStatus?: string;
}
/**
* 场地预约视图对象
*/
export interface VenueReservationVO {
/** 主键ID */
id?: string | number;
/** 场地ID */
venueId?: string | number;
/** 场地名称 */
venueName?: string;
/** 申请人ID */
applicantId?: string | number;
/** 申请人姓名 */
applicantName?: string;
/** 申请人电话 */
applicantPhone?: string;
/** 预约日期 */
reservationDate?: string;
/** 开始时间 */
startTime?: string;
/** 结束时间 */
endTime?: string;
/** 预约目的 */
reservationPurpose?: string;
/** 参与人数 */
participantCount?: number;
/** 特殊要求 */
specialRequirements?: string;
/** 预约状态 */
reservationStatus?: string;
/** 审批备注 */
approveRemark?: string;
/** 审批人ID */
approverId?: string | number;
/** 审批人姓名 */
approverName?: string;
/** 审批时间 */
approveTime?: string;
/** 创建时间 */
createTime?: string;
/** 更新时间 */
updateTime?: string;
}
/**
* 场地预约表单类型
*/
export interface VenueReservationForm {
/** 主键ID */
id?: string | number;
/** 场地ID */
venueId?: string | number;
/** 申请人电话 */
applicantPhone?: string;
/** 预约日期 */
reservationDate?: string;
/** 开始时间 */
startTime?: string;
/** 结束时间 */
endTime?: string;
/** 预约目的 */
reservationPurpose?: string;
/** 参与人数 */
participantCount?: number;
/** 特殊要求 */
specialRequirements?: string;
}
/**
* 预约审批表单类型
*/
export interface ReservationApproveForm {
/** 预约ID */
id?: string | number;
/** 预约状态 */
reservationStatus?: string;
/** 审批备注 */
approveRemark?: string;
}
/**
* 时间冲突检查参数
*/
export interface ConflictCheckQuery {
/** 场地ID */
venueId?: string | number;
/** 预约日期 */
reservationDate?: string;
/** 开始时间 */
startTime?: string;
/** 结束时间 */
endTime?: string;
/** 排除的预约ID */
excludeId?: string | number;
}
/**
* 冲突预约信息
*/
export interface ConflictReservationVO {
/** 预约ID */
id?: string | number;
/** 场地名称 */
venueName?: string;
/** 申请人姓名 */
applicantName?: string;
/** 开始时间 */
startTime?: string;
/** 结束时间 */
endTime?: string;
/** 预约目的 */
reservationPurpose?: string;
/** 预约状态 */
reservationStatus?: string;
}
Whitespace-only changes.
+1 -1
View File
@@ -60,7 +60,7 @@ function addIframe() {
min-height: calc(100vh - 50px);
width: 100%;
position: relative;
overflow: hidden;
overflow: auto;
}
.fixed-header + .app-main {
+421
View File
@@ -0,0 +1,421 @@
<template>
<div class="p-2 page-container">
<!-- 搜索区域 -->
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" label-width="100px">
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="场地名称" prop="venueName">
<el-input v-model="queryParams.venueName" placeholder="请输入场地名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="场地编码" prop="venueCode">
<el-input v-model="queryParams.venueCode" placeholder="请输入场地编码" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="场地位置" prop="venueLocation">
<el-input v-model="queryParams.venueLocation" placeholder="请输入场地位置" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="场地类型" prop="venueType">
<el-select v-model="queryParams.venueType" placeholder="请选择场地类型" clearable style="width: 100%">
<el-option v-for="dict in hq_venue_type" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="场地状态" prop="venueStatus">
<el-select v-model="queryParams.venueStatus" placeholder="请选择场地状态" clearable style="width: 100%">
<el-option label="维修中" value="0" />
<el-option label="可用" value="1" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="管理员" prop="venueManagerName">
<el-input v-model="queryParams.venueManagerName" placeholder="请输入管理员姓名" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
</div>
</transition>
<!-- 表格区域 -->
<el-card shadow="never" class="button-card table-card">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['hq:venue:add']">新增场地</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['hq:venue:edit']">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['hq:venue:remove']"
>删除</el-button
>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['hq:venue:export']">导出</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" :data="venueList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="场地编码" align="center" prop="venueCode" width="120" />
<el-table-column label="场地名称" align="center" prop="venueName" min-width="150" show-overflow-tooltip />
<el-table-column label="场地位置" align="center" prop="venueLocation" min-width="150" show-overflow-tooltip />
<el-table-column label="容量" align="center" prop="venueCapacity" width="80" />
<el-table-column label="场地类型" align="center" prop="venueType" width="120">
<template #default="scope">
<dict-tag :options="hq_venue_type" :value="scope.row.venueType" />
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="venueStatus" width="100">
<template #default="scope">
<el-tag :type="scope.row.venueStatus === '1' ? 'success' : 'danger'">
{{ scope.row.venueStatus === '1' ? '可用' : '维修中' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="管理员" align="center" prop="venueManagerName" width="120" />
<el-table-column label="场地描述" align="center" prop="venueDescription" min-width="200" show-overflow-tooltip />
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}') }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="180" class-name="small-padding fixed-width">
<template #default="scope">
<el-tooltip content="修改" placement="top">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['hq:venue:edit']"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['hq:venue:remove']"></el-button>
</el-tooltip>
<el-tooltip content="查看详情" placement="top">
<el-button link type="info" icon="View" @click="handleDetail(scope.row)"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 添加或修改场地信息对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="800px" append-to-body>
<el-form ref="venueFormRef" :model="form" :rules="rules" label-width="120px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="场地名称" prop="venueName">
<el-input v-model="form.venueName" placeholder="请输入场地名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="场地编码" prop="venueCode">
<el-input v-model="form.venueCode" placeholder="请输入场地编码" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="场地位置" prop="venueLocation">
<el-input v-model="form.venueLocation" placeholder="请输入场地位置" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="场地容量" prop="venueCapacity">
<el-input-number v-model="form.venueCapacity" :min="1" :max="9999" placeholder="请输入场地容量" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="场地类型" prop="venueType">
<el-select v-model="form.venueType" placeholder="请选择场地类型" style="width: 100%">
<el-option v-for="dict in hq_venue_type" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="场地状态" prop="venueStatus">
<el-radio-group v-model="form.venueStatus">
<el-radio value="0">维修中</el-radio>
<el-radio value="1">可用</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="管理员" prop="venueManagerId">
<el-select v-model="form.venueManagerId" placeholder="请选择管理员" clearable style="width: 100%">
<el-option v-for="user in userOptions" :key="user.userId" :label="user.nickName" :value="user.userId" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="场地图片" prop="venueImages">
<image-upload v-model="form.venueImages" :limit="3" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="场地描述" prop="venueDescription">
<el-input v-model="form.venueDescription" type="textarea" :rows="4" placeholder="请输入场地描述" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel">取 消</el-button>
<el-button type="primary" @click="submitForm">确 定</el-button>
</div>
</template>
</el-dialog>
<!-- 场地详情对话框 -->
<el-dialog title="场地详情" v-model="detailDialog.visible" width="800px" append-to-body>
<el-descriptions :column="2" border>
<el-descriptions-item label="场地编码">{{ detailData.venueCode }}</el-descriptions-item>
<el-descriptions-item label="场地名称">{{ detailData.venueName }}</el-descriptions-item>
<el-descriptions-item label="场地位置">{{ detailData.venueLocation }}</el-descriptions-item>
<el-descriptions-item label="场地容量">{{ detailData.venueCapacity }}人</el-descriptions-item>
<el-descriptions-item label="场地类型">
<dict-tag :options="hq_venue_type" :value="detailData.venueType" />
</el-descriptions-item>
<el-descriptions-item label="场地状态">
<el-tag :type="detailData.venueStatus === '1' ? 'success' : 'danger'">
{{ detailData.venueStatus === '1' ? '可用' : '维修中' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="管理员">{{ detailData.venueManagerName }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ parseTime(detailData.createTime) }}</el-descriptions-item>
<el-descriptions-item label="场地描述" :span="2">{{ detailData.venueDescription }}</el-descriptions-item>
<el-descriptions-item label="场地图片" :span="2" v-if="detailData.venueImages">
<image-preview :src="detailData.venueImages" :z-index="3000" />
</el-descriptions-item>
</el-descriptions>
</el-dialog>
</div>
</template>
<script setup name="VenueInfo" lang="ts">
import { listVenueInfo, getVenueInfo, delVenueInfo, addVenueInfo, updateVenueInfo } from '@/api/hq/venue/info';
import { listUser } from '@/api/system/user';
import { VenueInfoVO, VenueInfoQuery, VenueInfoForm } from '@/api/hq/venue/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { hq_venue_type } = toRefs<any>(proxy?.useDict('hq_venue_type'));
const venueList = ref<VenueInfoVO[]>([]);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const userOptions = ref<any[]>([]);
// 查询参数
const queryParams = ref<VenueInfoQuery>({
pageNum: 1,
pageSize: 10,
venueName: undefined,
venueCode: undefined,
venueLocation: undefined,
venueType: undefined,
venueStatus: undefined,
venueManagerName: undefined
});
// 表单参数
const form = ref<VenueInfoForm>({});
const rules = reactive<ElFormRules>({
venueName: [{ required: true, message: '场地名称不能为空', trigger: 'blur' }],
venueCode: [{ required: true, message: '场地编码不能为空', trigger: 'blur' }],
venueLocation: [{ required: true, message: '场地位置不能为空', trigger: 'blur' }],
venueCapacity: [{ required: true, message: '场地容量不能为空', trigger: 'blur' }],
venueType: [{ required: true, message: '场地类型不能为空', trigger: 'change' }],
venueStatus: [{ required: true, message: '场地状态不能为空', trigger: 'change' }],
venueManagerId: [{ required: true, message: '管理员不能为空', trigger: 'change' }]
});
// 对话框
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
// 详情对话框
const detailDialog = reactive({
visible: false
});
const detailData = ref<VenueInfoVO>({});
const queryFormRef = ref<ElFormInstance>();
const venueFormRef = ref<ElFormInstance>();
/** 查询场地信息列表 */
const getList = async () => {
loading.value = true;
const res = await listVenueInfo(queryParams.value);
venueList.value = res.rows;
total.value = res.total;
loading.value = false;
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: VenueInfoVO[]) => {
ids.value = selection.map((item) => item.id);
single.value = selection.length !== 1;
multiple.value = !selection.length;
};
/** 新增按钮操作 */
const handleAdd = () => {
reset();
getUserList();
dialog.visible = true;
dialog.title = '添加场地信息';
};
/** 修改按钮操作 */
const handleUpdate = async (row?: VenueInfoVO) => {
reset();
await getUserList();
const _id = row?.id || ids.value[0];
const res = await getVenueInfo(_id);
Object.assign(form.value, res.data);
dialog.visible = true;
dialog.title = '修改场地信息';
};
/** 详情按钮操作 */
const handleDetail = async (row: VenueInfoVO) => {
const res = await getVenueInfo(row.id!);
detailData.value = res.data;
detailDialog.visible = true;
};
/** 提交按钮 */
const submitForm = () => {
venueFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
if (form.value.id) {
await updateVenueInfo(form.value);
proxy?.$modal.msgSuccess('修改成功');
} else {
await addVenueInfo(form.value);
proxy?.$modal.msgSuccess('新增成功');
}
dialog.visible = false;
await getList();
}
});
};
/** 删除按钮操作 */
const handleDelete = async (row?: VenueInfoVO) => {
const _ids = row?.id || ids.value;
await proxy?.$modal.confirm('是否确认删除场地编号为"' + _ids + '"的数据项?');
await delVenueInfo(_ids);
proxy?.$modal.msgSuccess('删除成功');
await getList();
};
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download(
'venue/info/export',
{
...queryParams.value
},
`venue_info_${new Date().getTime()}.xlsx`
);
};
/** 表单重置 */
const reset = () => {
form.value = {
id: undefined,
venueName: undefined,
venueCode: undefined,
venueLocation: undefined,
venueCapacity: undefined,
venueType: undefined,
venueStatus: '1',
venueDescription: undefined,
venueImages: undefined,
venueManagerId: undefined,
venueManagerName: undefined
};
venueFormRef.value?.resetFields();
};
/** 取消按钮 */
const cancel = () => {
dialog.visible = false;
reset();
};
/** 获取用户列表 */
const getUserList = async () => {
const res = await listUser({ pageNum: 1, pageSize: 1000 });
userOptions.value = res.rows;
};
onMounted(() => {
getList();
});
</script>
<style scoped>
.search-btn-container {
text-align: right;
}
.table-card {
margin-top: 10px;
}
.button-card .el-card__header {
padding: 10px 20px;
}
.button-card .el-card__body {
padding: 0 20px 20px 20px;
}
</style>
@@ -0,0 +1,785 @@
<template>
<div class="p-2 page-container">
<!-- 搜索区域 -->
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" label-width="100px">
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="场地名称" prop="venueName">
<el-input v-model="queryParams.venueName" placeholder="请输入场地名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="申请人" prop="applicantName">
<el-input v-model="queryParams.applicantName" placeholder="请输入申请人姓名" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="预约日期" prop="reservationDate">
<el-date-picker
v-model="queryParams.reservationDate"
type="date"
placeholder="请选择预约日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="预约状态" prop="reservationStatus">
<el-select v-model="queryParams.reservationStatus" placeholder="请选择预约状态" clearable style="width: 100%">
<el-option v-for="dict in hq_reservation_status" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24" class="search-btn-container">
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
</div>
</transition>
<!-- 快捷功能按钮 -->
<el-row :gutter="10" class="mb-[10px]">
<el-col :span="6">
<el-card shadow="hover" class="quick-action-card">
<div class="quick-action" @click="handleAdd">
<el-icon class="quick-icon"><Plus /></el-icon>
<span>新增预约</span>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="quick-action-card">
<div class="quick-action" @click="showMyReservations">
<el-icon class="quick-icon"><User /></el-icon>
<span>我的预约</span>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="quick-action-card">
<div class="quick-action" @click="showPendingReservations">
<el-icon class="quick-icon"><Clock /></el-icon>
<span>待审批</span>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="quick-action-card">
<div class="quick-action" @click="showConflictCheck">
<el-icon class="quick-icon"><Warning /></el-icon>
<span>冲突检查</span>
</div>
</el-card>
</el-col>
</el-row>
<!-- 表格区域 -->
<el-card shadow="never" class="table-card">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['hq:reservation:add']">新增预约</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['hq:reservation:edit']"
>修改</el-button
>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['hq:reservation:remove']"
>删除</el-button
>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['hq:reservation:export']">导出</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" :data="reservationList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="场地名称" align="center" prop="venueName" min-width="150" show-overflow-tooltip />
<el-table-column label="申请人" align="center" prop="applicantName" width="100" />
<el-table-column label="申请人电话" align="center" prop="applicantPhone" width="120" />
<el-table-column label="预约日期" align="center" prop="reservationDate" width="120" />
<el-table-column label="预约时间" align="center" width="200">
<template #default="scope">
<span>{{ formatTime(scope.row.startTime) }} - {{ formatTime(scope.row.endTime) }}</span>
</template>
</el-table-column>
<el-table-column label="预约目的" align="center" prop="reservationPurpose" min-width="150" show-overflow-tooltip />
<el-table-column label="参与人数" align="center" prop="participantCount" width="100" />
<el-table-column label="预约状态" align="center" prop="reservationStatus" width="100">
<template #default="scope">
<dict-tag :options="hq_reservation_status" :value="scope.row.reservationStatus" />
</template>
</el-table-column>
<el-table-column label="审批人" align="center" prop="approverName" width="100" />
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}') }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="220" class-name="small-padding fixed-width">
<template #default="scope">
<el-tooltip content="查看详情" placement="top">
<el-button link type="info" icon="View" @click="handleDetail(scope.row)"></el-button>
</el-tooltip>
<el-tooltip content="修改" placement="top" v-if="canEdit(scope.row)">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['hq:reservation:edit']"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top" v-if="canDelete(scope.row)">
<el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['hq:reservation:remove']"></el-button>
</el-tooltip>
<el-tooltip content="审批" placement="top" v-if="canApprove(scope.row)">
<el-button link type="success" icon="Check" @click="handleApprove(scope.row)" v-hasPermi="['hq:reservation:approve']"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 新增/修改预约对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="800px" append-to-body>
<el-form ref="reservationFormRef" :model="form" :rules="rules" label-width="120px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="选择场地" prop="venueId">
<el-select v-model="form.venueId" placeholder="请选择场地" style="width: 100%" @change="handleVenueChange">
<el-option v-for="venue in availableVenues" :key="venue.id" :label="`${venue.venueName}(${venue.venueLocation})`" :value="venue.id" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="申请人电话" prop="applicantPhone">
<el-input v-model="form.applicantPhone" placeholder="请输入申请人电话" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="预约日期" prop="reservationDate">
<el-date-picker
v-model="form.reservationDate"
type="date"
placeholder="请选择预约日期"
value-format="YYYY-MM-DD"
style="width: 100%"
:disabled-date="disabledDate"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="参与人数" prop="participantCount">
<el-input-number v-model="form.participantCount" :min="1" :max="9999" placeholder="请输入参与人数" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="开始时间" prop="startTime">
<el-time-picker v-model="form.startTime" placeholder="请选择开始时间" format="HH:mm" value-format="HH:mm" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="结束时间" prop="endTime">
<el-time-picker v-model="form.endTime" placeholder="请选择结束时间" format="HH:mm" value-format="HH:mm" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="预约目的" prop="reservationPurpose">
<el-input v-model="form.reservationPurpose" type="textarea" :rows="3" placeholder="请输入预约目的" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="特殊要求">
<el-input v-model="form.specialRequirements" type="textarea" :rows="3" placeholder="请输入特殊要求(如设备需求等)" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel">取 消</el-button>
<el-button type="primary" @click="checkConflictAndSubmit">检查冲突并提交</el-button>
</div>
</template>
</el-dialog>
<!-- 审批对话框 -->
<el-dialog title="预约审批" v-model="approveDialog.visible" width="600px" append-to-body>
<el-form ref="approveFormRef" :model="approveForm" :rules="approveRules" label-width="120px">
<el-row>
<el-col :span="24">
<el-form-item label="预约信息">
<el-descriptions :column="1" border>
<el-descriptions-item label="场地名称">{{ currentReservation.venueName }}</el-descriptions-item>
<el-descriptions-item label="申请人">{{ currentReservation.applicantName }}</el-descriptions-item>
<el-descriptions-item label="预约时间">
{{ currentReservation.reservationDate }} {{ formatTime(currentReservation.startTime) }}-{{ formatTime(currentReservation.endTime) }}
</el-descriptions-item>
<el-descriptions-item label="预约目的">{{ currentReservation.reservationPurpose }}</el-descriptions-item>
</el-descriptions>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="审批结果" prop="reservationStatus">
<el-radio-group v-model="approveForm.reservationStatus">
<el-radio value="1">通过</el-radio>
<el-radio value="2">拒绝</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="审批备注" prop="approveRemark">
<el-input v-model="approveForm.approveRemark" type="textarea" :rows="4" placeholder="请输入审批备注" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="approveDialog.visible = false">取 消</el-button>
<el-button type="primary" @click="submitApprove">确 定</el-button>
</div>
</template>
</el-dialog>
<!-- 详情对话框 -->
<el-dialog title="预约详情" v-model="detailDialog.visible" width="800px" append-to-body>
<el-descriptions :column="2" border>
<el-descriptions-item label="场地名称">{{ detailData.venueName }}</el-descriptions-item>
<el-descriptions-item label="申请人">{{ detailData.applicantName }}</el-descriptions-item>
<el-descriptions-item label="申请人电话">{{ detailData.applicantPhone }}</el-descriptions-item>
<el-descriptions-item label="预约日期">{{ detailData.reservationDate }}</el-descriptions-item>
<el-descriptions-item label="开始时间">{{ formatTime(detailData.startTime) }}</el-descriptions-item>
<el-descriptions-item label="结束时间">{{ formatTime(detailData.endTime) }}</el-descriptions-item>
<el-descriptions-item label="参与人数">{{ detailData.participantCount }}人</el-descriptions-item>
<el-descriptions-item label="预约状态">
<dict-tag :options="hq_reservation_status" :value="detailData.reservationStatus" />
</el-descriptions-item>
<el-descriptions-item label="预约目的" :span="2">{{ detailData.reservationPurpose }}</el-descriptions-item>
<el-descriptions-item label="特殊要求" :span="2">{{ detailData.specialRequirements || '无' }}</el-descriptions-item>
<el-descriptions-item label="审批人" v-if="detailData.approverName">{{ detailData.approverName }}</el-descriptions-item>
<el-descriptions-item label="审批时间" v-if="detailData.approveTime">{{ parseTime(detailData.approveTime) }}</el-descriptions-item>
<el-descriptions-item label="审批备注" :span="2" v-if="detailData.approveRemark">{{ detailData.approveRemark }}</el-descriptions-item>
<el-descriptions-item label="创建时间" :span="2">{{ parseTime(detailData.createTime) }}</el-descriptions-item>
</el-descriptions>
</el-dialog>
<!-- 冲突检查对话框 -->
<el-dialog title="时间冲突检查" v-model="conflictDialog.visible" width="800px" append-to-body>
<el-form ref="conflictFormRef" :model="conflictForm" label-width="120px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="选择场地" prop="venueId">
<el-select v-model="conflictForm.venueId" placeholder="请选择场地" style="width: 100%">
<el-option v-for="venue in availableVenues" :key="venue.id" :label="`${venue.venueName}(${venue.venueLocation})`" :value="venue.id" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="预约日期" prop="reservationDate">
<el-date-picker
v-model="conflictForm.reservationDate"
type="date"
placeholder="请选择预约日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="开始时间" prop="startTime">
<el-time-picker v-model="conflictForm.startTime" placeholder="请选择开始时间" format="HH:mm" value-format="HH:mm" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="结束时间" prop="endTime">
<el-time-picker v-model="conflictForm.endTime" placeholder="请选择结束时间" format="HH:mm" value-format="HH:mm" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-button type="primary" @click="handleConflictCheck">检查冲突</el-button>
</el-col>
</el-row>
</el-form>
<!-- 冲突检查结果 -->
<div v-if="conflictCheckResult.length > 0" class="mt-4">
<el-alert title="发现时间冲突" type="error" :closable="false" show-icon class="mb-4" />
<el-table :data="conflictCheckResult" border>
<el-table-column label="申请人" prop="applicantName" />
<el-table-column label="预约目的" prop="reservationPurpose" />
<el-table-column label="时间段">
<template #default="scope"> {{ formatTime(scope.row.startTime) }} - {{ formatTime(scope.row.endTime) }} </template>
</el-table-column>
<el-table-column label="状态" prop="reservationStatus">
<template #default="scope">
<dict-tag :options="hq_reservation_status" :value="scope.row.reservationStatus" />
</template>
</el-table-column>
</el-table>
</div>
<div v-else-if="conflictChecked" class="mt-4">
<el-alert title="无时间冲突" type="success" :closable="false" show-icon />
</div>
</el-dialog>
</div>
</template>
<script setup name="VenueReservation" lang="ts">
import {
listVenueReservation,
getVenueReservation,
delVenueReservation,
addVenueReservation,
updateVenueReservation,
listMyReservations,
listPendingReservations,
checkTimeConflict,
approveReservation
} from '@/api/hq/venue/reservation';
import { getAvailableVenues } from '@/api/hq/venue/info';
import { VenueReservationVO, VenueReservationQuery, VenueReservationForm, ReservationApproveForm, ConflictCheckQuery } from '@/api/hq/venue/types';
import { checkPermi } from '@/utils/permission';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { hq_reservation_status } = toRefs<any>(proxy?.useDict('hq_reservation_status'));
const reservationList = ref<VenueReservationVO[]>([]);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const availableVenues = ref<any[]>([]);
// 查询参数
const queryParams = ref<VenueReservationQuery>({
pageNum: 1,
pageSize: 10,
venueId: undefined,
venueName: undefined,
applicantName: undefined,
reservationDate: undefined,
reservationStatus: undefined
});
// 表单参数
const form = ref<VenueReservationForm>({});
const rules = reactive<ElFormRules>({
venueId: [{ required: true, message: '请选择场地', trigger: 'change' }],
applicantPhone: [
{ required: true, message: '申请人电话不能为空', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
],
reservationDate: [{ required: true, message: '请选择预约日期', trigger: 'change' }],
startTime: [{ required: true, message: '请选择开始时间', trigger: 'change' }],
endTime: [{ required: true, message: '请选择结束时间', trigger: 'change' }],
reservationPurpose: [{ required: true, message: '预约目的不能为空', trigger: 'blur' }],
participantCount: [{ required: true, message: '参与人数不能为空', trigger: 'blur' }]
});
// 审批表单
const approveForm = ref<ReservationApproveForm>({});
const approveRules = reactive<ElFormRules>({
reservationStatus: [{ required: true, message: '请选择审批结果', trigger: 'change' }],
approveRemark: [{ required: true, message: '审批备注不能为空', trigger: 'blur' }]
});
// 冲突检查表单
const conflictForm = ref<ConflictCheckQuery>({});
const conflictCheckResult = ref<any[]>([]);
const conflictChecked = ref(false);
// 对话框
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
const approveDialog = reactive({
visible: false
});
const detailDialog = reactive({
visible: false
});
const conflictDialog = reactive({
visible: false
});
const detailData = ref<VenueReservationVO>({});
const currentReservation = ref<VenueReservationVO>({});
const queryFormRef = ref<ElFormInstance>();
const reservationFormRef = ref<ElFormInstance>();
const approveFormRef = ref<ElFormInstance>();
const conflictFormRef = ref<ElFormInstance>();
/** 查询预约列表 */
const getList = async () => {
loading.value = true;
const res = await listVenueReservation(queryParams.value);
reservationList.value = res.rows;
total.value = res.total;
loading.value = false;
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: VenueReservationVO[]) => {
ids.value = selection.map((item) => item.id);
single.value = selection.length !== 1;
multiple.value = !selection.length;
};
/** 新增按钮操作 */
const handleAdd = () => {
reset();
getAvailableVenueList();
dialog.visible = true;
dialog.title = '新增预约';
};
/** 修改按钮操作 */
const handleUpdate = async (row?: VenueReservationVO) => {
reset();
await getAvailableVenueList();
const _id = row?.id || ids.value[0];
const res = await getVenueReservation(_id);
const data = res.data;
// 处理时间格式
if (data.startTime) {
const startTime = new Date(data.startTime);
form.value.startTime = `${startTime.getHours().toString().padStart(2, '0')}:${startTime.getMinutes().toString().padStart(2, '0')}`;
}
if (data.endTime) {
const endTime = new Date(data.endTime);
form.value.endTime = `${endTime.getHours().toString().padStart(2, '0')}:${endTime.getMinutes().toString().padStart(2, '0')}`;
}
Object.assign(form.value, {
...data,
startTime: form.value.startTime,
endTime: form.value.endTime
});
dialog.visible = true;
dialog.title = '修改预约';
};
/** 详情按钮操作 */
const handleDetail = async (row: VenueReservationVO) => {
const res = await getVenueReservation(row.id!);
detailData.value = res.data;
detailDialog.visible = true;
};
/** 审批按钮操作 */
const handleApprove = (row: VenueReservationVO) => {
currentReservation.value = row;
approveForm.value = {
id: row.id,
reservationStatus: undefined,
approveRemark: undefined
};
approveDialog.visible = true;
};
/** 检查冲突并提交 */
const checkConflictAndSubmit = async () => {
reservationFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
// 验证时间
if (form.value.startTime && form.value.endTime && form.value.startTime >= form.value.endTime) {
proxy?.$modal.msgError('开始时间必须早于结束时间');
return;
}
// 组装完整的时间字符串
const fullStartTime = `${form.value.reservationDate} ${form.value.startTime}:00`;
const fullEndTime = `${form.value.reservationDate} ${form.value.endTime}:00`;
// 检查冲突
try {
const conflictRes = await checkTimeConflict({
venueId: form.value.venueId,
startTime: fullStartTime,
endTime: fullEndTime,
excludeId: form.value.id
});
if (conflictRes.data && conflictRes.data.length > 0) {
await proxy?.$modal.confirm('检测到时间冲突,是否继续提交?');
}
} catch (error) {
console.error('冲突检查失败:', error);
}
const submitData = {
...form.value,
startTime: fullStartTime,
endTime: fullEndTime
};
if (form.value.id) {
await updateVenueReservation(submitData);
proxy?.$modal.msgSuccess('修改成功');
} else {
await addVenueReservation(submitData);
proxy?.$modal.msgSuccess('新增成功');
}
dialog.visible = false;
await getList();
}
});
};
/** 提交审批 */
const submitApprove = () => {
approveFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
await approveReservation(approveForm.value);
proxy?.$modal.msgSuccess('审批成功');
approveDialog.visible = false;
await getList();
}
});
};
/** 删除按钮操作 */
const handleDelete = async (row?: VenueReservationVO) => {
const _ids = row?.id || ids.value;
await proxy?.$modal.confirm('是否确认删除选中的预约数据?');
await delVenueReservation(_ids);
proxy?.$modal.msgSuccess('删除成功');
await getList();
};
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download(
'venue/reservation/export',
{
...queryParams.value
},
`venue_reservation_${new Date().getTime()}.xlsx`
);
};
/** 显示我的预约 */
const showMyReservations = async () => {
loading.value = true;
const res = await listMyReservations(queryParams.value);
reservationList.value = res.rows;
total.value = res.total;
loading.value = false;
};
/** 显示待审批预约 */
const showPendingReservations = async () => {
loading.value = true;
queryParams.value.reservationStatus = '0';
const res = await listPendingReservations(queryParams.value);
reservationList.value = res.rows;
total.value = res.total;
loading.value = false;
};
/** 显示冲突检查对话框 */
const showConflictCheck = () => {
getAvailableVenueList();
conflictForm.value = {};
conflictCheckResult.value = [];
conflictChecked.value = false;
conflictDialog.visible = true;
};
/** 处理冲突检查 */
const handleConflictCheck = async () => {
if (!conflictForm.value.venueId || !conflictForm.value.reservationDate || !conflictForm.value.startTime || !conflictForm.value.endTime) {
proxy?.$modal.msgError('请填写完整的检查信息');
return;
}
const fullStartTime = `${conflictForm.value.reservationDate} ${conflictForm.value.startTime}:00`;
const fullEndTime = `${conflictForm.value.reservationDate} ${conflictForm.value.endTime}:00`;
try {
const res = await checkTimeConflict({
venueId: conflictForm.value.venueId,
startTime: fullStartTime,
endTime: fullEndTime
});
conflictCheckResult.value = res.data || [];
conflictChecked.value = true;
} catch (error) {
proxy?.$modal.msgError('冲突检查失败');
}
};
/** 表单重置 */
const reset = () => {
form.value = {
id: undefined,
venueId: undefined,
applicantPhone: undefined,
reservationDate: undefined,
startTime: undefined,
endTime: undefined,
reservationPurpose: undefined,
participantCount: undefined,
specialRequirements: undefined
};
reservationFormRef.value?.resetFields();
};
/** 取消按钮 */
const cancel = () => {
dialog.visible = false;
reset();
};
/** 获取可用场地列表 */
const getAvailableVenueList = async () => {
const res = await getAvailableVenues();
availableVenues.value = res.data;
};
/** 场地变更处理 */
const handleVenueChange = () => {
// 场地变更时清空相关信息
};
/** 禁用过去日期 */
const disabledDate = (time: Date) => {
return time.getTime() < Date.now() - 24 * 60 * 60 * 1000;
};
/** 时间格式化 */
const formatTime = (timeStr: string) => {
if (!timeStr) return '';
const time = new Date(timeStr);
return `${time.getHours().toString().padStart(2, '0')}:${time.getMinutes().toString().padStart(2, '0')}`;
};
/** 权限判断 */
const canEdit = (row: VenueReservationVO) => {
return row.reservationStatus === '0' && checkPermi(['hq:reservation:edit']);
};
const canDelete = (row: VenueReservationVO) => {
return row.reservationStatus === '0' && checkPermi(['hq:reservation:remove']);
};
const canApprove = (row: VenueReservationVO) => {
return row.reservationStatus === '0' && checkPermi(['hq:reservation:approve']);
};
onMounted(() => {
getList();
});
</script>
<style scoped>
.search-btn-container {
text-align: right;
}
.table-card {
margin-top: 10px;
}
.quick-action-card {
cursor: pointer;
transition: all 0.3s;
}
.quick-action-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.quick-action {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
text-align: center;
}
.quick-icon {
font-size: 24px;
margin-bottom: 8px;
color: #409eff;
}
.quick-action span {
font-size: 14px;
color: #606266;
font-weight: 500;
}
.quick-action-card:hover .quick-icon {
color: #66b1ff;
}
.quick-action-card:hover span {
color: #409eff;
}
.mt-4 {
margin-top: 16px;
}
.mb-4 {
margin-bottom: 16px;
}
.dialog-footer {
text-align: right;
}
</style>
+548
View File
@@ -0,0 +1,548 @@
<template>
<div class="p-2 page-container">
<!-- 页面标题 -->
<div class="page-header mb-4">
<h2 class="text-xl font-bold text-gray-800">场地使用统计</h2>
<p class="text-gray-600 mt-2">查看场地预约和使用情况的统计数据</p>
</div>
<!-- 统计卡片区域 -->
<el-row :gutter="20" class="mb-6">
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-item">
<div class="stat-icon bg-blue-100">
<el-icon class="text-blue-600" size="24"><Calendar /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ statistics.totalReservations }}</div>
<div class="stat-label">总预约数</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-item">
<div class="stat-icon bg-green-100">
<el-icon class="text-green-600" size="24"><Check /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ statistics.approvedReservations }}</div>
<div class="stat-label">已批准预约</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-item">
<div class="stat-icon bg-yellow-100">
<el-icon class="text-yellow-600" size="24"><Clock /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ statistics.pendingReservations }}</div>
<div class="stat-label">待审批预约</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-item">
<div class="stat-icon bg-red-100">
<el-icon class="text-red-600" size="24"><CloseBold /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ statistics.rejectedReservations }}</div>
<div class="stat-label">已拒绝预约</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 筛选条件 -->
<el-card shadow="hover" class="mb-4">
<el-form :model="queryParams" label-width="80px" inline>
<el-form-item label="统计类型">
<el-select v-model="queryParams.statisticType" placeholder="请选择统计类型" @change="handleStatisticTypeChange">
<el-option label="预约量统计" value="reservation" />
<el-option label="场地使用率" value="usage" />
<el-option label="时间段分析" value="timeSlot" />
</el-select>
</el-form-item>
<el-form-item label="时间范围">
<el-date-picker
v-model="queryParams.dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
@change="handleDateRangeChange"
/>
</el-form-item>
<el-form-item label="场地类型">
<el-select v-model="queryParams.venueType" placeholder="请选择场地类型" clearable>
<el-option v-for="dict in hq_venue_type" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="loadStatisticsData">
<template #icon><Search /></template>
查询
</el-button>
<el-button @click="exportReport">
<template #icon><Download /></template>
导出报表
</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 图表展示区域 -->
<el-row :gutter="20">
<el-col :span="12">
<el-card shadow="hover" class="chart-card">
<template #header>
<div class="card-header">
<span>预约量趋势图</span>
</div>
</template>
<div ref="reservationTrendChart" class="chart-container"></div>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="hover" class="chart-card">
<template #header>
<div class="card-header">
<span>场地类型分布</span>
</div>
</template>
<div ref="venueTypeChart" class="chart-container"></div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" class="mt-4">
<el-col :span="24">
<el-card shadow="hover" class="chart-card">
<template #header>
<div class="card-header">
<span>热门场地排行榜</span>
</div>
</template>
<div ref="popularVenuesChart" class="chart-container"></div>
</el-card>
</el-col>
</el-row>
<!-- 详细数据表格 -->
<el-card shadow="hover" class="mt-4">
<template #header>
<div class="card-header">
<span>详细统计数据</span>
</div>
</template>
<el-table v-loading="loading" :data="detailData" style="width: 100%">
<el-table-column prop="venueName" label="场地名称" width="150" />
<el-table-column prop="venueType" label="场地类型" width="120">
<template #default="scope">
<dict-tag :options="hq_venue_type" :value="scope.row.venueType" />
</template>
</el-table-column>
<el-table-column prop="totalReservations" label="预约总数" width="100" />
<el-table-column prop="approvedCount" label="已批准" width="100" />
<el-table-column prop="pendingCount" label="待审批" width="100" />
<el-table-column prop="rejectedCount" label="已拒绝" width="100" />
<el-table-column prop="usageRate" label="使用率" width="100">
<template #default="scope"> {{ scope.row.usageRate }}% </template>
</el-table-column>
<el-table-column prop="avgDuration" label="平均时长(小时)" />
</el-table>
<pagination
v-show="total > 0"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
:total="total"
@pagination="loadDetailData"
/>
</el-card>
</div>
</template>
<script setup lang="ts" name="VenueStatistics">
import { getCurrentInstance, ref, reactive, onMounted, nextTick, onUnmounted, toRefs } from 'vue';
import { ElMessage } from 'element-plus';
import { Calendar, Check, Clock, CloseBold, Search, Download } from '@element-plus/icons-vue';
import * as echarts from 'echarts';
// 如果有对应的API,可以引入
// import { getVenueStatistics, getDetailStatistics } from '@/api/hq/venue/statistics';
const { proxy } = getCurrentInstance() as any;
const { hq_venue_type } = toRefs<any>(proxy?.useDict('hq_venue_type'));
// 响应式数据
const loading = ref(true);
const total = ref(0);
// 统计概览数据
const statistics = ref({
totalReservations: 156,
approvedReservations: 128,
pendingReservations: 18,
rejectedReservations: 10
});
// 查询参数
const queryParams = ref({
statisticType: 'reservation',
dateRange: [] as string[],
venueType: '',
pageNum: 1,
pageSize: 10
});
// 详细数据
const detailData = ref([
{
venueName: '第一会议室',
venueType: 'meeting_room',
totalReservations: 45,
approvedCount: 38,
pendingCount: 4,
rejectedCount: 3,
usageRate: 85.2,
avgDuration: 2.5
},
{
venueName: '多媒体教室A',
venueType: 'classroom',
totalReservations: 32,
approvedCount: 28,
pendingCount: 2,
rejectedCount: 2,
usageRate: 76.8,
avgDuration: 3.2
},
{
venueName: '体育馆',
venueType: 'sports_field',
totalReservations: 28,
approvedCount: 25,
pendingCount: 2,
rejectedCount: 1,
usageRate: 68.5,
avgDuration: 4.0
}
]);
// 图表引用
const reservationTrendChart = ref();
const venueTypeChart = ref();
const popularVenuesChart = ref();
// 图表实例
let trendChartInstance: echarts.ECharts;
let typeChartInstance: echarts.ECharts;
let popularChartInstance: echarts.ECharts;
// 初始化预约趋势图
const initReservationTrendChart = () => {
const option = {
title: {
text: '预约量趋势',
left: 'center',
textStyle: {
fontSize: 14
}
},
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
data: ['1月', '2月', '3月', '4月', '5月', '6月']
},
yAxis: {
type: 'value'
},
series: [
{
name: '预约量',
type: 'line',
data: [25, 30, 28, 35, 40, 38],
smooth: true,
itemStyle: {
color: '#409EFF'
}
}
]
};
trendChartInstance = echarts.init(reservationTrendChart.value);
trendChartInstance.setOption(option);
};
// 初始化场地类型分布图
const initVenueTypeChart = () => {
const option = {
title: {
text: '场地类型分布',
left: 'center',
textStyle: {
fontSize: 14
}
},
tooltip: {
trigger: 'item'
},
series: [
{
name: '场地类型',
type: 'pie',
radius: '60%',
data: [
{ value: 45, name: '会议室' },
{ value: 32, name: '教室' },
{ value: 28, name: '运动场' },
{ value: 15, name: '礼堂' },
{ value: 8, name: '实验室' }
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
};
typeChartInstance = echarts.init(venueTypeChart.value);
typeChartInstance.setOption(option);
};
// 初始化热门场地排行图
const initPopularVenuesChart = () => {
const option = {
title: {
text: '热门场地排行榜',
left: 'center',
textStyle: {
fontSize: 14
}
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
xAxis: {
type: 'value'
},
yAxis: {
type: 'category',
data: ['第一会议室', '多媒体教室A', '体育馆', '大礼堂', '实验室B']
},
series: [
{
name: '预约次数',
type: 'bar',
data: [45, 32, 28, 22, 18],
itemStyle: {
color: '#67C23A'
}
}
]
};
popularChartInstance = echarts.init(popularVenuesChart.value);
popularChartInstance.setOption(option);
};
// 处理统计类型变化
const handleStatisticTypeChange = () => {
loadStatisticsData();
};
// 处理时间范围变化
const handleDateRangeChange = () => {
loadStatisticsData();
};
// 加载统计数据
const loadStatisticsData = async () => {
try {
loading.value = true;
// 这里应该调用实际的API
// const response = await getVenueStatistics(queryParams.value);
// statistics.value = response.data;
// 更新图表数据
updateCharts();
ElMessage.success('统计数据加载成功');
} catch (error) {
ElMessage.error('加载统计数据失败');
} finally {
loading.value = false;
}
};
// 加载详细数据
const loadDetailData = async () => {
try {
loading.value = true;
// 这里应该调用实际的API
// const response = await getDetailStatistics(queryParams.value);
// detailData.value = response.rows;
// total.value = response.total;
total.value = detailData.value.length;
} catch (error) {
ElMessage.error('加载详细数据失败');
} finally {
loading.value = false;
}
};
// 更新图表
const updateCharts = () => {
// 更新图表数据,这里可以根据实际API返回的数据来更新
// 当前使用模拟数据
};
// 导出报表
const exportReport = () => {
ElMessage.info('报表导出功能开发中...');
};
// 窗口大小变化时重新调整图表
const handleResize = () => {
if (trendChartInstance) trendChartInstance.resize();
if (typeChartInstance) typeChartInstance.resize();
if (popularChartInstance) popularChartInstance.resize();
};
// 生命周期
onMounted(() => {
// 设置默认时间范围为近30天
const end = new Date();
const start = new Date();
start.setDate(start.getDate() - 30);
queryParams.value.dateRange = [start.toISOString().split('T')[0], end.toISOString().split('T')[0]];
nextTick(() => {
initReservationTrendChart();
initVenueTypeChart();
initPopularVenuesChart();
// 监听窗口大小变化
window.addEventListener('resize', handleResize);
});
// 加载数据
loadStatisticsData();
loadDetailData();
});
// 组件卸载时清理事件监听
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
if (trendChartInstance) trendChartInstance.dispose();
if (typeChartInstance) typeChartInstance.dispose();
if (popularChartInstance) popularChartInstance.dispose();
});
</script>
<style scoped>
.page-container {
background-color: #f5f5f5;
min-height: calc(100vh - 120px);
}
.page-header {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.stat-card {
height: 120px;
}
.stat-item {
display: flex;
align-items: center;
height: 80px;
}
.stat-icon {
width: 50px;
height: 50px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16px;
}
.stat-content {
flex: 1;
}
.stat-value {
font-size: 28px;
font-weight: bold;
color: #303133;
line-height: 1;
}
.stat-label {
font-size: 14px;
color: #909399;
margin-top: 4px;
}
.chart-card {
margin-bottom: 20px;
}
.chart-container {
height: 300px;
width: 100%;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
:deep(.el-card__header) {
padding: 16px 20px;
border-bottom: 1px solid #f0f0f0;
}
:deep(.el-card__body) {
padding: 20px;
}
:deep(.el-table) {
font-size: 14px;
}
:deep(.el-table th) {
background-color: #fafafa;
}
</style>