resolve: 解决合并冲突

This commit is contained in:
wuzhen committed 2025-07-10 18:02:35 +08:00
commit 80dface225
97 files changed
+4789 -1686

No files matched your search

+23 -68
View File
@@ -1,84 +1,39 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { HallReservationVO, HallReservationForm, HallReservationQuery } from '@/api/hq/hallReservation/types';
import { HallReservationVO, HallReservationQuery } from '../hallReservation/types';
import { HallReservationAuditForm, TimeCheckParams, TimeCheckResult } from './types';
import { TableDataInfo, R } from '@/api/types';
/**
* 查询报告厅预约列表
* @param query
* @returns {*}
*/
export const listHallReservation = (query?: HallReservationQuery): AxiosPromise<HallReservationVO[]> => {
return request({
// 查询报告厅预约列表
export function listHallReservation(query: HallReservationQuery) {
return request<TableDataInfo<HallReservationVO>>({
url: '/hq/hallReservation/list',
method: 'get',
params: query
});
};
}
/**
* 查询报告厅预约详细
* @param id
*/
export const getHallReservation = (id: string | number): AxiosPromise<HallReservationVO> => {
return request({
// 获取报告厅预约详细信息
export function getHallReservation(id: string | number) {
return request<R<HallReservationVO>>({
url: '/hq/hallReservation/' + id,
method: 'get'
});
};
/**
* 新增报告厅预约
* @param data
*/
export const addHallReservation = (data: HallReservationForm) => {
return request({
url: '/hq/hallReservation',
method: 'post',
data: data
});
};
/**
* 修改报告厅预约
* @param data
*/
export const updateHallReservation = (data: HallReservationForm) => {
return request({
url: '/hq/hallReservation',
method: 'put',
data: data
});
};
/**
* 删除报告厅预约
* @param id
*/
export const delHallReservation = (id: string | number | Array<string | number>) => {
return request({
url: '/hq/hallReservation/' + id,
method: 'delete'
});
};
/**
* 审核报告厅预约(批准/拒绝)
* @param data
*/
export interface HallReservationAuditForm {
/** 预约记录ID列表 */
ids: Array<string | number>;
/** 审核状态:1-批准,2-拒绝 */
status: string;
/** 审核备注 */
auditRemark?: string;
}
export const auditHallReservation = (data: HallReservationAuditForm) => {
return request({
// 审核报告厅预约
export function auditHallReservation(data: HallReservationAuditForm) {
return request<R<any>>({
url: '/hq/hallReservation/audit',
method: 'put',
data: data
});
};
}
// 检查报告厅预约时间是否存在冲突
export function checkTimeConflict(data: TimeCheckParams) {
return request<R<TimeCheckResult>>({
url: '/hq/hallReservation/checkTimeConflict',
method: 'post',
data: data
});
}
+31 -192
View File
@@ -1,200 +1,39 @@
export interface HallReservationVO {
/**
* 主键ID
*/
id: string | number;
/**
* 报告厅ID
*/
hallId: string | number;
/**
* 报告厅名称
*/
hallName: string;
/**
* 开始时间
*/
startTime: string;
/**
* 结束时间
*/
endTime: string;
/**
* 预约用途
*/
reservationPurpose: string;
/**
* 申请人id
*/
applicantId: string | number;
/**
* 申请人姓名
*/
applicantName: string;
/**
* 申请人电话
*/
applicantPhone: string;
/**
* 预约状态
*/
status: string;
/**
* 审核人id
*/
auditBy: number;
/**
* 审核人
*/
auditName: string;
/**
* 审核时间
*/
auditTime: string;
/**
* 审核备注
*/
auditRemark: string;
import { HallReservationVO } from '../hallReservation/types';
/**
* 报告厅预约审核表单
*/
export interface HallReservationAuditForm {
/** 预约ID列表 */
ids: Array<string | number>;
/** 审核状态 */
status: string;
/** 审核备注 */
auditRemark?: string;
}
export interface HallReservationForm extends BaseEntity {
/**
* 主键ID
*/
id?: string | number;
/**
* 报告厅ID
*/
hallId?: string | number;
/**
* 报告厅名称
*/
hallName?: string;
/**
* 开始时间
*/
startTime?: string;
/**
* 结束时间
*/
endTime?: string;
/**
* 预约用途
*/
reservationPurpose?: string;
/**
* 申请人id
*/
applicantId?: string | number;
/**
* 申请人姓名
*/
applicantName?: string;
/**
* 申请人电话
*/
applicantPhone?: string;
/**
* 预约状态
*/
status?: string;
/**
* 审核人id
*/
auditBy?: number;
/**
* 审核人
*/
auditName?: string;
/**
* 审核时间
*/
auditTime?: string;
/**
* 审核备注
*/
auditRemark?: string;
/**
* 时间冲突检查参数
*/
export interface TimeCheckParams {
/** 报告厅ID */
hallId: string | number;
/** 预约开始时间 */
startTime: string;
/** 预约结束时间 */
endTime: string;
/** 排除的预约ID */
excludeId?: string | number;
}
export interface HallReservationQuery extends PageQuery {
/**
* 报告厅ID
*/
hallId?: string | number;
/**
* 报告厅名称
*/
hallName?: string;
/**
* 开始时间
*/
startTime?: string;
/**
* 结束时间
*/
endTime?: string;
/**
* 开始时间查询参数(字符串格式)
*/
startTimeStr?: string;
/**
* 结束时间查询参数(字符串格式)
*/
endTimeStr?: string;
/**
* 申请人ID
*/
applicantId?: string | number;
/**
* 申请人姓名
*/
applicantName?: string;
/**
* 预约状态
*/
status?: string;
/**
* 日期范围参数
*/
params?: any;
/**
* 时间冲突检查结果
*/
export interface TimeCheckResult {
/** 是否存在冲突 */
hasConflict: boolean;
/** 冲突的预约列表 */
conflictList: HallReservationVO[];
}
+1 -1
View File
@@ -1,6 +1,6 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { RepairCategoryVO, RepairCategoryForm, RepairCategoryQuery } from '@/api/hq/hq/pm/types';
import { RepairCategoryVO, RepairCategoryForm, RepairCategoryQuery } from '@/api/hq/pm/types';
/**
* 查询维修类型列表
+15 -1
View File
@@ -209,6 +209,19 @@ export const deptTreeSelect = (): AxiosPromise<DeptTreeVO[]> => {
});
};
/** 获取具有指定权限的用户列表 */
export function listUsersByPermission(permission: string, tenantId?: string): AxiosPromise<UserVO[]> {
return request({
url: '/system/user/list-by-permission',
method: 'get',
params: {
permission,
tenantId
}
});
}
export default {
listUser,
getUser,
@@ -225,5 +238,6 @@ export default {
getAuthRole,
updateAuthRole,
deptTreeSelect,
listUserByDeptId
listUserByDeptId,
listUsersByPermission
};
+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1752052172719" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8134" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M756.01248 336.814072l-20.336164 0 0 510.696308-67.101257 0L668.575058 336.814072 556.73383 336.814072l0 510.696308-67.106374 0L489.627457 336.814072 377.791345 336.814072l0 510.696308-67.102281 0L310.689065 336.814072l-42.702568 0c-50.532907 0-91.50711-0.727571-91.50711 0l0 533.587668c0 49.123814 40.974203 88.944751 91.50711 88.944751l488.019843 0c50.544163 0 91.50097-39.820937 91.50097-88.944751L847.50731 336.814072C847.512427 333.065701 806.562782 336.814072 756.01248 336.814072L756.01248 336.814072zM891.467521 176.488597 690.943508 176.488597l0-54.57292c0-31.630395-56.529481-57.263192-89.479941-57.263192L422.525176 64.652486c-32.933063 0-89.468684 25.632797-89.468684 57.263192l0 54.57292L132.566248 176.488597c-25.160029 0-45.548382 20.033265-45.548382 44.740994 0 24.695448 20.382213 44.727691 45.548382 44.727691L891.467521 265.957281c25.137517 0 45.514613-20.032242 45.514613-44.727691C936.975994 196.521862 916.593781 176.488597 891.467521 176.488597L891.467521 176.488597zM601.468684 176.488597 422.530292 176.488597l0-22.367427 178.938392 0L601.468684 176.488597 601.468684 176.488597zM601.468684 176.488597" fill="#272636" p-id="8135"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1752052134276" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5447" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M220.094304 322.742967l247.380709-82.457849c33.513211-11.171411 70.457561-2.450813 95.438462 22.529064l198.273366 198.270297c24.979878 24.977831 33.704569 61.924228 22.534181 95.439485l-82.463989 247.376616a93.251661 93.251661 0 0 1-64.445649 60.616444L63.720126 1017.340337c-24.88164 6.634081-50.436615-8.158804-57.070696-33.040445a46.613551 46.613551 0 0 1 0-24.028205L159.471719 387.186569a93.250638 93.250638 0 0 1 60.622585-64.443602m125.403877 420.158452a91.065883 91.065883 0 0 1 70.947724-26.441156c1.682312 0.12075 3.266387 0.181125 4.754271 0.181125 62.869761 0 113.84054-50.966687 113.84054-113.842587 0-62.87283-50.97078-113.841564-113.84054-113.841564-62.87283 0-113.84361 50.968733-113.84361 113.841564 0 1.490954 0.061398 3.072982 0.182148 4.752224a91.080209 91.080209 0 0 1-26.437063 70.94977L156.663773 802.936627c-17.781956 17.781956-17.781956 46.617644 0 64.398577 17.781956 17.784003 46.615597 17.784003 64.401647 0l124.432761-124.433785zM579.671104 80.436823c100.483351-100.484375 263.404629-100.484375 363.89105 0 100.484375 100.485398 100.484375 263.404629 0 363.890027-28.784523 28.784523-75.449239 28.784523-104.229668 0L579.671104 184.666491c-28.783499-28.781453-28.783499-75.448215 0-104.229668" fill="#040000" p-id="5448"></path></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1752052118757" class="icon" viewBox="0 0 1829 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4426" xmlns:xlink="http://www.w3.org/1999/xlink" width="357.2265625" height="200"><path d="M914.297945 0C314.456393 0 0 441.928431 0 512.019026c0 70.052544 314.456393 511.980974 914.297945 511.980974 599.8035 0 914.297945-441.928431 914.297945-511.980974C1828.633942 441.928431 1514.101446 0 914.297945 0zM914.297945 905.888596c-224.46494 0-406.388466-176.329977-406.388466-393.86957 0-217.539593 181.923526-393.86957 406.388466-393.86957 224.426889 0 406.388466 176.291925 406.388466 393.86957C1320.686411 729.558619 1138.762885 905.888596 914.297945 905.888596zM914.297945 512.019026c-37.252276-40.867155 60.653859-196.953811 0-196.953811-112.213444 0-203.194233 88.20304-203.194233 196.953811s90.980789 196.915759 203.194233 196.915759c112.175393 0 203.194233-88.164988 203.194233-196.915759C1117.492178 462.057597 945.918621 546.759912 914.297945 512.019026z" fill="#090909" p-id="4427"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -1686,3 +1686,8 @@ body {
outline: none !important;
}
}
// 隐藏图片上传列表的删除按钮
.el-upload-list--picture-card .el-upload-list__item .el-icon--close {
display: none !important;
}
+7 -6
View File
@@ -441,12 +441,13 @@ img {
}
// 禁用状态
.disabled,
[disabled] {
opacity: 0.6;
cursor: not-allowed;
pointer-events: none;
}
// 禁用状态取消,让分页组件可以点击跳转
// .disabled,
// [disabled] {
// opacity: 0.6;
// cursor: not-allowed;
// pointer-events: none;
// }
// 加载状态
.loading {
+2 -1
View File
@@ -4,7 +4,8 @@
:root {
// Theme color
// --art-primary: 50, 50, 222;
--art-primary: 93, 135, 255;
// --art-primary: 93, 135, 255;
--art-primary: 0, 58, 255;
--art-secondary: 73, 190, 255;
--art-error: 250, 137, 107;
--art-info: 0, 75, 178;
@@ -9,6 +9,7 @@
:to="{ path: tag.path ? tag.path : '', query: tag.query }"
class="tags-view-item"
:style="activeStyle(tag)"
@click="handleTagClick(tag, $event)"
@click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
@contextmenu.prevent="openMenu(tag, $event)"
>
@@ -232,6 +233,27 @@ const handleScroll = () => {
closeMenu();
};
// 点击标签时,如果标签已激活,则执行刷新操作
const handleTagClick = (tag: RouteLocationNormalized, e: MouseEvent) => {
// 阻止 router-link 默认行为,统一由脚本控制导航和刷新
e.preventDefault();
if (isActive(tag)) {
// 当前已激活,直接刷新
refreshSelectedTag(tag);
} else {
// 尚未激活,先导航到对应路由,导航成功后再刷新
router
.push({ path: tag.path || '', query: tag.query })
.then(() => {
refreshSelectedTag(tag);
})
.catch(() => {
/* 忽略重复导航错误 */
});
}
};
onMounted(() => {
initTags();
addTags();
+558 -58
View File
@@ -3,31 +3,65 @@
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div ref="searchRef" v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="100px">
<el-form-item label="报告厅ID" prop="hallId">
<el-input v-model="queryParams.hallId" placeholder="请输入报告厅ID" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="报告厅名称" prop="hallName">
<el-input v-model="queryParams.hallName" placeholder="请输入报告厅名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="预约开始时间" prop="startTime">
<el-date-picker clearable v-model="queryParams.startTime" type="date" placeholder="请选择开始时间" value-format="YYYY-MM-DD" />
</el-form-item>
<el-form-item label="预约结束时间" prop="endTime">
<el-date-picker clearable v-model="queryParams.endTime" type="date" placeholder="请选择结束时间" value-format="YYYY-MM-DD" />
</el-form-item>
<el-form-item label="申请人姓名" prop="applicantName">
<el-input v-model="queryParams.applicantName" placeholder="请输入申请人姓名" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="预约状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择预约状态" clearable>
<el-option v-for="dict in hall_reserv_status" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<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-form ref="queryFormRef" :model="queryParams" label-width="100px">
<!-- 第一行:4个检索元素 -->
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="报告厅名称" prop="hallName">
<el-input v-model="queryParams.hallName" placeholder="请输入报告厅名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="预约开始时间" prop="startTime">
<el-date-picker
clearable
v-model="queryParams.startTime"
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="endTime">
<el-date-picker
clearable
v-model="queryParams.endTime"
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="applicantName">
<el-input v-model="queryParams.applicantName" placeholder="请输入申请人姓名" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
</el-row>
<!-- 第二行:剩余检索元素 -->
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="预约状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择预约状态" clearable style="width: 100%">
<el-option v-for="dict in hall_reserv_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>
@@ -53,7 +87,14 @@
</el-row>
</template>
<el-table v-loading="loading" border :data="hallReservationList" @selection-change="handleSelectionChange" style="margin-bottom: 0">
<el-table
v-loading="loading"
border
:data="hallReservationList"
@selection-change="handleSelectionChange"
@row-dblclick="handleView"
style="margin-bottom: 0"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="报告厅ID" align="center" prop="hallId" />
<el-table-column label="报告厅名称" align="center" prop="hallName" />
@@ -80,13 +121,9 @@
:hide-after="100"
effect="dark"
>
<span class="purpose-text">
{{
scope.row.reservationPurpose && scope.row.reservationPurpose.length > 100
? scope.row.reservationPurpose.substring(0, 100) + '...'
: scope.row.reservationPurpose
}}
</span>
<div class="purpose-text line-clamp-3">
{{ scope.row.reservationPurpose }}
</div>
</el-tooltip>
</template>
</el-table-column>
@@ -112,17 +149,13 @@
:hide-after="100"
effect="dark"
>
<span class="audit-remark-text">
{{
scope.row.auditRemark && scope.row.auditRemark.length > 100
? scope.row.auditRemark.substring(0, 100) + '...'
: scope.row.auditRemark
}}
</span>
<div class="audit-remark-text line-clamp-3">
{{ scope.row.auditRemark }}
</div>
</el-tooltip>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="220">
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="220" fixed="right">
<template #default="scope">
<!-- 查看按钮,所有记录都可以查看 -->
<el-tooltip content="查看" placement="top">
@@ -180,9 +213,9 @@
<span class="form-text">{{ viewForm.hallName }}</span>
</el-form-item>
<!-- 报告厅图片展示区域 -->
<el-form-item label="报告厅图片" v-if="viewHallImages.length > 0">
<el-form-item label="报告厅图片">
<div class="view-hall-images">
<div class="image-gallery">
<div class="image-gallery" v-if="viewHallImages.length > 0">
<div v-for="(image, index) in viewHallImages" :key="index" class="image-item" @click="previewImage(image, index)">
<img :src="image" class="gallery-image" @error="handleImageError" />
<div class="image-overlay">
@@ -190,6 +223,10 @@
</div>
</div>
</div>
<div v-else class="no-images-placeholder">
<el-icon><Picture /></el-icon>
<span>暂无图片</span>
</div>
</div>
</el-form-item>
<el-form-item label="预约开始时间">
@@ -265,8 +302,7 @@
<div class="reservation-details">
<div class="detail-row">
<span class="detail-label">申请人:</span>
<span class="detail-value">{{ record.applicantName }}</span>
<span class="detail-phone">({{ record.applicantPhone || '手机号暂无' }})</span>
<span class="detail-value"> {{ record.applicantName }}({{ record.applicantPhone || '手机号暂无' }}) </span>
</div>
<div class="detail-row">
<span class="detail-label">预约时间:</span>
@@ -278,6 +314,46 @@
<span class="detail-label">预约用途:</span>
<span class="detail-value purpose-content">{{ record.reservationPurpose }}</span>
</div>
<div class="detail-row">
<span class="detail-label">审核建议:</span>
<span class="detail-value audit-suggestion-content" :class="getConflictStatusClass(record)">
<template v-if="getConflictCheckStatus(record).isLoading">
<el-icon class="rotate-loading"><Loading /></el-icon> 正在检查时间冲突...
</template>
<template v-else-if="getConflictCheckStatus(record).hasError">
<el-icon class="text-warning"><Warning /></el-icon> 冲突检查失败,请手动核实
</template>
<template v-else-if="hasTimeConflict(record)">
<el-icon class="text-danger"><Warning /></el-icon> 发现时间冲突,建议拒绝
<div class="conflict-details" v-if="getConflictReservations(record).length > 0">
<small>冲突详情:</small>
<ul class="conflict-list">
<li v-for="conflict in getConflictReservations(record)" :key="conflict.id">
<el-tag type="warning" size="small" v-if="conflict.internal">本次批次</el-tag>
<el-tag type="info" size="small" v-else>已审批</el-tag>
{{ conflict.applicantName }} ({{ parseTime(conflict.startTime, '{h}:{i}') }}-{{
parseTime(conflict.endTime, '{h}:{i}')
}})
</li>
</ul>
</div>
</template>
<template v-else-if="getConflictCheckStatus(record).isChecked">
<el-icon class="text-success"><Check /></el-icon> 无时间冲突,可以批准
</template>
<template v-else>
<el-icon class="text-info"><InfoFilled /></el-icon> 未检查时间冲突
<el-button
type="text"
size="small"
@click="manualCheckConflict(record)"
:loading="getConflictCheckStatus(record).isLoading"
>
立即检查
</el-button>
</template>
</span>
</div>
</div>
</div>
</div>
@@ -294,6 +370,22 @@
{{ auditForm.status === '1' ? '批准' : '拒绝' }}
</el-tag>
</div>
<!-- 批准时显示冲突检查统计 -->
<div v-if="auditForm.status === '1'" class="conflict-summary">
<span class="audit-label">冲突检查统计:</span>
<div class="summary-stats">
<el-tag type="info" size="small"> 总计:{{ selectedRecords.length }} 个预约 </el-tag>
<el-tag type="success" size="small"> 无冲突:{{ getNoConflictCount() }} 个 </el-tag>
<el-tag type="danger" size="small" v-if="getConflictCount() > 0"> 有冲突:{{ getConflictCount() }} 个 </el-tag>
<el-tag type="warning" size="small" v-if="getUncheckCount() > 0"> 未检查:{{ getUncheckCount() }} 个 </el-tag>
</div>
<div v-if="getConflictCount() > 0" class="conflict-warning-text">
<el-icon><Warning /></el-icon>
建议先处理时间冲突的预约,或选择拒绝有冲突的申请
</div>
</div>
<div class="audit-remark-section">
<span class="audit-label">审核备注:</span>
<el-input
@@ -320,7 +412,8 @@
</template>
<script setup name="HallAudit" lang="ts">
import { listHallReservation, getHallReservation, auditHallReservation, HallReservationAuditForm } from '@/api/hq/hallAudit';
import { listHallReservation, getHallReservation, auditHallReservation, HallReservationAuditForm, checkTimeConflict } from '@/api/hq/hallAudit';
import dayjs from 'dayjs';
import { listHallInfo } from '@/api/hq/hallInfo';
import { HallReservationVO, HallReservationQuery, HallReservationForm } from '@/api/hq/hallReservation/types';
import { HallInfoVO } from '@/api/hq/hallInfo/types';
@@ -385,6 +478,10 @@ const auditForm = ref({
const selectedRecords = ref<HallReservationVO[]>([]);
const auditLoading = ref(false);
// 审核建议相关状态
const conflictCheckResults = reactive(new Map<string | number, any>());
const conflictCheckLoading = reactive(new Set<string | number>());
const queryParams = ref<HallReservationQuery>({
pageNum: 1,
pageSize: 10,
@@ -430,7 +527,7 @@ const getHallList = async () => {
const getHallImages = (imageUrl: string): string[] => {
if (!imageUrl || imageUrl.trim() === '') return [];
// 支持多种分隔符:逗号、分号、空格
const separators = /[,;|\s]+/;
const separators = /[,;|\\s]+/;
const images = imageUrl
.split(separators)
.map((url) => url.trim())
@@ -680,13 +777,22 @@ const handleReject = async (row?: HallReservationVO) => {
};
/** 打开审核对话框 */
const openAuditDialog = (status: string, ids: Array<string | number>, records: HallReservationVO[], title: string) => {
const openAuditDialog = async (status: string, ids: Array<string | number>, records: HallReservationVO[], title: string) => {
auditForm.value.status = status;
auditForm.value.auditRemark = '';
auditForm.value.ids = ids;
selectedRecords.value = records;
auditDialog.title = title;
auditDialog.visible = true;
// 在打开审核对话框时统一检查时间冲突(无论批准/拒绝)
// 清除选中记录旧的缓存(尤其是 internal 冲突信息)
records.forEach((rec) => conflictCheckResults.delete(rec.id));
if (records.length > 0) {
console.log('审核对话框打开,开始检查时间冲突...');
await batchCheckTimeConflict(records);
}
};
/** 确认审核操作 */
@@ -753,6 +859,242 @@ const handleExport = () => {
);
};
/** 检查单个记录的时间冲突 */
const checkRecordTimeConflict = async (record: HallReservationVO) => {
const recordId = record.id;
// 如果已经在检查中,跳过
if (conflictCheckLoading.has(recordId)) {
return;
}
// 如果已有缓存结果,直接返回
if (conflictCheckResults.has(recordId)) {
return conflictCheckResults.get(recordId);
}
try {
conflictCheckLoading.add(recordId);
const res = await checkTimeConflict({
hallId: record.hallId,
startTime: dayjs(record.startTime).format('YYYY-MM-DD HH:mm:ss'),
endTime: dayjs(record.endTime).format('YYYY-MM-DD HH:mm:ss'),
excludeId: record.id
});
const result = {
hasConflict: res.data.hasConflict,
conflictList: (res.data.conflictList || []).map((item: any) => ({ ...item, internal: false })),
conflictMessage: res.data.conflictMessage || '',
checkTime: new Date().getTime()
};
// 缓存结果
conflictCheckResults.set(recordId, result);
return result;
} catch (error) {
console.error('检查时间冲突失败:', error);
const errorResult = {
hasConflict: false,
conflictList: [],
conflictMessage: '冲突检查失败',
checkTime: new Date().getTime(),
error: true
};
conflictCheckResults.set(recordId, errorResult);
return errorResult;
} finally {
conflictCheckLoading.delete(recordId);
}
};
/** 批量检查选中记录的时间冲突 */
const batchCheckTimeConflict = async (records: HallReservationVO[]) => {
console.log('开始批量检查时间冲突,记录数:', records.length);
const promises = records.map((record) => checkRecordTimeConflict(record));
await Promise.all(promises);
// 前端额外校验勾选记录之间的时间交叉冲突(同一报告厅)
addInternalConflictInfo(records);
console.log('批量时间冲突检查完成');
};
/** 判断两个时间段是否重叠 */
const isTimeOverlap = (startA: any, endA: any, startB: any, endB: any) => {
const sA = dayjs(startA);
const eA = dayjs(endA);
const sB = dayjs(startB);
const eB = dayjs(endB);
return sA.isBefore(eB) && sB.isBefore(eA); // 开始 < 另一结束 且 另一开始 < 结束
};
/** 在选中记录之间标记内部冲突 */
const addInternalConflictInfo = (records: HallReservationVO[]) => {
// 若不足两条记录无需内部冲突检测
if (records.length < 2) return;
const hallGroups: Record<string, HallReservationVO[]> = {};
records.forEach((rec) => {
const key = `${rec.hallId || ''}`;
hallGroups[key] = hallGroups[key] || [];
hallGroups[key].push(rec);
});
Object.values(hallGroups).forEach((list) => {
// 对同一报告厅的申请做两两比较
for (let i = 0; i < list.length; i++) {
for (let j = i + 1; j < list.length; j++) {
const r1 = list[i];
const r2 = list[j];
if (isTimeOverlap(r1.startTime, r1.endTime, r2.startTime, r2.endTime)) {
// r1 冲突信息
const res1 = conflictCheckResults.get(r1.id) || {
hasConflict: false,
conflictList: [],
conflictMessage: '',
checkTime: new Date().getTime()
};
res1.hasConflict = true;
res1.conflictList.push({ ...r2, internal: true });
conflictCheckResults.set(r1.id, res1);
// r2 冲突信息
const res2 = conflictCheckResults.get(r2.id) || {
hasConflict: false,
conflictList: [],
conflictMessage: '',
checkTime: new Date().getTime()
};
res2.hasConflict = true;
res2.conflictList.push({ ...r1, internal: true });
conflictCheckResults.set(r2.id, res2);
}
}
}
});
};
/** 获取记录的冲突检查结果 */
const getConflictResult = (record: HallReservationVO) => {
return (
conflictCheckResults.get(record.id) || {
hasConflict: false,
conflictList: [],
conflictMessage: '未检查',
checkTime: 0
}
);
};
/** 检查是否存在时间冲突(同步方法) */
const hasTimeConflict = (record: HallReservationVO): boolean => {
const result = getConflictResult(record);
return result.hasConflict;
};
/** 获取时间冲突的预约列表(同步方法) */
const getConflictReservations = (record: HallReservationVO) => {
const result = getConflictResult(record);
return result.conflictList || [];
};
/** 获取冲突检查状态 */
const getConflictCheckStatus = (record: HallReservationVO) => {
const isLoading = conflictCheckLoading.has(record.id);
const result = getConflictResult(record);
return {
isLoading,
isChecked: result.checkTime > 0,
hasError: result.error || false,
result
};
};
/** 获取冲突状态对应的CSS类 */
const getConflictStatusClass = (record: HallReservationVO) => {
const status = getConflictCheckStatus(record);
if (status.isLoading) {
return 'checking-conflict';
}
if (status.hasError) {
return 'check-error';
}
if (status.isChecked) {
return hasTimeConflict(record) ? 'has-conflict' : 'no-conflict';
}
return 'not-checked';
};
/** 获取审核建议文本 */
const getAuditSuggestionText = (record: HallReservationVO) => {
const status = getConflictCheckStatus(record);
if (status.isLoading) {
return '正在检查时间冲突...';
}
if (status.hasError) {
return '冲突检查失败,请手动核实';
}
if (status.isChecked) {
return hasTimeConflict(record) ? '发现时间冲突,建议拒绝' : '无时间冲突,可以批准';
}
return '未检查时间冲突';
};
/** 手动触发时间冲突检查 */
const manualCheckConflict = async (record: HallReservationVO) => {
// 清除缓存,强制重新检查
conflictCheckResults.delete(record.id);
await checkRecordTimeConflict(record);
};
/** 获取无冲突的预约数量 */
const getNoConflictCount = () => {
return selectedRecords.value.filter((record) => {
const status = getConflictCheckStatus(record);
return status.isChecked && !hasTimeConflict(record);
}).length;
};
/** 获取有冲突的预约数量 */
const getConflictCount = () => {
return selectedRecords.value.filter((record) => {
const status = getConflictCheckStatus(record);
return status.isChecked && hasTimeConflict(record);
}).length;
};
/** 获取未检查的预约数量 */
const getUncheckCount = () => {
return selectedRecords.value.filter((record) => {
const status = getConflictCheckStatus(record);
return !status.isChecked && !status.isLoading;
}).length;
};
/** 获取预约状态文本 */
const getStatusText = (status: string) => {
const statusMap: { [key: string]: string } = {
'0': '审核中',
'1': '已批准',
'2': '已拒绝'
};
return statusMap[status] || status;
};
/** 页面加载完成后的初始化 */
onMounted(() => {
getList();
getHallList();
@@ -855,22 +1197,31 @@ onMounted(() => {
/* 表格中预约用途文本样式 */
.purpose-text {
display: inline-block;
max-width: 100%;
display: -webkit-box;
word-break: break-all;
word-wrap: break-word;
line-height: 1.4;
cursor: pointer;
padding: 0 4px;
}
/* 表格中审核备注文本样式 */
.audit-remark-text {
display: inline-block;
max-width: 100%;
display: -webkit-box;
word-break: break-all;
word-wrap: break-word;
line-height: 1.4;
cursor: pointer;
padding: 0 4px;
}
/* 三行省略样式 */
.line-clamp-3 {
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
max-height: 4.2em;
}
/* 查看详情表单样式 */
@@ -920,11 +1271,6 @@ onMounted(() => {
width: 100%;
}
/* 选中报告厅的图片展示区域 */
.selected-hall-images {
width: 100%;
}
.image-gallery {
display: flex;
flex-wrap: wrap;
@@ -977,6 +1323,30 @@ onMounted(() => {
font-size: 20px;
}
/* 暂无图片占位样式 */
.no-images-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
max-width: 450px;
height: 120px;
background-color: #f5f7fa;
border: 2px dashed #dcdfe6;
border-radius: 8px;
color: #909399;
}
.no-images-placeholder .el-icon {
font-size: 32px;
margin-bottom: 8px;
}
.no-images-placeholder span {
font-size: 14px;
}
/* 审核对话框样式 */
.audit-form .form-text {
color: #303133;
@@ -1026,7 +1396,7 @@ onMounted(() => {
.hall-id {
font-size: 14px;
color: #909399;
background: #f0f2f5;
/* background: #f0f2f5; */
padding: 4px 8px;
border-radius: 4px;
}
@@ -1161,6 +1531,136 @@ onMounted(() => {
font-size: 14px;
resize: none !important;
}
/* 审核建议样式 - 增强版 */
.audit-suggestion-content {
line-height: 1.6;
display: inline-flex;
align-items: center;
gap: 4px;
flex-wrap: wrap;
}
.audit-suggestion-content .el-icon {
margin-right: 4px;
}
/* 冲突详情在新行展示 */
.audit-suggestion-content .conflict-details {
flex-basis: 100%;
}
/* 不同状态的颜色 */
.audit-suggestion-content.checking-conflict {
color: #409eff; /* 检查中 - 蓝色 */
}
.audit-suggestion-content.check-error {
color: #e6a23c; /* 检查错误 - 橙色 */
}
.audit-suggestion-content.has-conflict {
color: #f56c6c; /* 有冲突 - 红色 */
}
.audit-suggestion-content.no-conflict {
color: #67c23a; /* 无冲突 - 绿色 */
}
.audit-suggestion-content.not-checked {
color: #909399; /* 未检查 - 灰色 */
}
/* 冲突详情样式 */
.conflict-details {
margin-top: 8px;
padding: 8px;
background-color: #fef2f2;
border-left: 3px solid #f56c6c;
border-radius: 4px;
font-size: 12px;
}
.conflict-list {
margin: 4px 0 0 0;
padding-left: 16px;
list-style-type: disc;
}
.conflict-list li {
margin: 2px 0;
color: #666;
}
/* 文本颜色工具类 */
.text-success {
color: #67c23a;
}
.text-warning {
color: #e6a23c;
}
.text-danger {
color: #f56c6c;
}
.text-info {
color: #409eff;
}
/* 加载动画 */
.rotate-loading {
animation: rotate 2s linear infinite;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* 冲突检查统计样式 */
.conflict-summary {
margin: 0px 0;
padding: 0px;
/* background-color: #f9fafe; */
border-radius: 6px;
/* border: 1px solid #e4e7ed; */
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap; /* 超出时自动换行 */
}
.summary-stats {
display: inline-flex; /* 与标签同行 */
gap: 8px;
margin-top: 0; /* 移除顶部间距,保持同一行 */
flex-wrap: wrap; /* 达到最大宽度时自身也可换行 */
}
.conflict-warning-text {
margin-top: 8px;
color: #e6a23c;
font-size: 14px;
display: flex;
align-items: center;
gap: 4px;
}
/* 搜索按钮容器样式 */
.search-btn-container {
display: flex;
justify-content: flex-end;
margin-top: 10px;
.el-form-item {
margin-bottom: 0 !important;
}
}
</style>
<style>
+99 -20
View File
@@ -3,19 +3,30 @@
<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" :inline="true" label-width="100px">
<el-form-item label="报告厅名称" prop="hallName">
<el-input v-model="queryParams.hallName" placeholder="请输入报告厅名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="报告厅状态" prop="hallStatus">
<el-select v-model="queryParams.hallStatus" placeholder="请选择报告厅状态" clearable>
<el-option v-for="dict in hall_status" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<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-form ref="queryFormRef" :model="queryParams" label-width="100px">
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="报告厅名称" prop="hallName">
<el-input v-model="queryParams.hallName" placeholder="请输入报告厅名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="报告厅状态" prop="hallStatus">
<el-select v-model="queryParams.hallStatus" placeholder="请选择报告厅状态" clearable style="width: 100%">
<el-option v-for="dict in hall_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>
@@ -40,11 +51,11 @@
</el-row>
</template>
<el-table v-loading="loading" border :data="hallInfoList" @selection-change="handleSelectionChange" style="margin-bottom: 20px">
<el-table v-loading="loading" border :data="hallInfoList" @selection-change="handleSelectionChange" @row-dblclick="handleView">
<el-table-column type="selection" width="55" align="center" />
<!-- <el-table-column label="主键ID" align="center" prop="id" v-if="true" /> -->
<el-table-column label="报告厅名称" align="center" prop="hallName" />
<el-table-column label="报告厅图片" align="center" prop="hallImage" width="100">
<el-table-column label="报告厅图片" align="center" prop="hallImage" width="200">
<template #default="scope">
<div v-if="scope.row.hallImage" class="table-image">
<image-preview :src="scope.row.hallImage" :width="50" :height="50" />
@@ -54,21 +65,25 @@
</div>
</template>
</el-table-column>
<el-table-column label="报告厅状态" align="center" prop="hallStatus">
<el-table-column label="报告厅状态" align="center" prop="hallStatus" width="120">
<template #default="scope">
<dict-tag :options="hall_status" :value="scope.row.hallStatus" />
</template>
</el-table-column>
<el-table-column label="报告厅描述" align="center" prop="hallDescription" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="160">
<el-table-column label="报告厅描述" align="center" prop="hallDescription">
<template #default="scope">
<div class="description-cell">{{ scope.row.hallDescription }}</div>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="240">
<template #default="scope">
<el-tooltip content="查看" placement="top">
<el-button
link
type="info"
type="primary"
icon="View"
size="default"
style="font-size: 16px; margin-right: 8px"
style="font-size: 16px; margin-right: 8px; color: blue"
@click="handleView(scope.row)"
v-hasPermi="['hq:hallInfo:query']"
></el-button>
@@ -477,4 +492,68 @@ onMounted(() => {
background-color: #fff;
margin-top: 0px !important;
}
.el-popper.description-tooltip-popper {
width: 300px !important;
max-width: 300px !important;
}
.el-popper.description-tooltip-popper .el-popper__content {
width: 300px !important;
max-width: 300px !important;
}
.el-popper.description-tooltip-popper .el-tooltip__content {
width: 100% !important;
max-height: 200px !important;
overflow-y: auto !important;
white-space: pre-wrap !important;
word-break: break-all !important;
text-align: left !important;
line-height: 1.5 !important;
padding: 8px 12px !important;
/* 自定义滚动条样式 */
&::-webkit-scrollbar {
width: 6px;
height: 6px;
}
&::-webkit-scrollbar-thumb {
border-radius: 3px;
background-color: rgba(144, 147, 153, 0.3);
&:hover {
background-color: rgba(144, 147, 153, 0.5);
}
}
&::-webkit-scrollbar-track {
border-radius: 3px;
background-color: transparent;
}
}
.description-cell {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.5;
max-height: 4.5em;
text-align: left;
padding: 0 10px;
}
/* 搜索按钮容器样式 */
.search-btn-container {
display: flex;
justify-content: flex-end;
margin-top: 10px;
.el-form-item {
margin-bottom: 0 !important;
}
}
</style>
+263 -62
View File
@@ -3,28 +3,54 @@
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div ref="searchRef" v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="100px">
<el-form-item label="报告厅ID" prop="hallId">
<el-input v-model="queryParams.hallId" placeholder="请输入报告厅ID" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="报告厅名称" prop="hallName">
<el-input v-model="queryParams.hallName" placeholder="请输入报告厅名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="预约开始时间" prop="startTime">
<el-date-picker clearable v-model="queryParams.startTime" type="date" placeholder="请选择开始时间" value-format="YYYY-MM-DD" />
</el-form-item>
<el-form-item label="预约结束时间" prop="endTime">
<el-date-picker clearable v-model="queryParams.endTime" type="date" placeholder="请选择结束时间" value-format="YYYY-MM-DD" />
</el-form-item>
<el-form-item label="预约状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择预约状态" clearable>
<el-option v-for="dict in hall_reserv_status" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<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-form ref="queryFormRef" :model="queryParams" label-width="100px">
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="报告厅名称" prop="hallName">
<el-input v-model="queryParams.hallName" placeholder="请输入报告厅名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="预约开始时间" prop="startTime">
<el-date-picker
clearable
v-model="queryParams.startTime"
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="endTime">
<el-date-picker
clearable
v-model="queryParams.endTime"
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="status">
<el-select v-model="queryParams.status" placeholder="请选择预约状态" clearable style="width: 100%">
<el-option v-for="dict in hall_reserv_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>
@@ -37,23 +63,26 @@
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['hq:hallReservation:add']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['hq:hallReservation:edit']"
>修改</el-button
>
<el-button icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['hq:hallReservation:edit']">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['hq:hallReservation:remove']"
>删除</el-button
>
<el-button icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['hq:hallReservation:remove']">删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button icon="Download" @click="handleExport" v-hasPermi="['hq:hallReservation:export']">导出</el-button>
<el-button icon="Download" @click="handleExport" v-hasPermi="['hq:hallReservation:export']">导出</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" border :data="hallReservationList" @selection-change="handleSelectionChange" style="margin-bottom: 0">
<el-table
v-loading="loading"
border
:data="hallReservationList"
@selection-change="handleSelectionChange"
style="margin-bottom: 0"
@row-dblclick="handleView"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="报告厅ID" align="center" prop="hallId" />
<el-table-column label="报告厅名称" align="center" prop="hallName" />
@@ -78,13 +107,9 @@
:hide-after="100"
effect="dark"
>
<span class="purpose-text">
{{
scope.row.reservationPurpose && scope.row.reservationPurpose.length > 100
? scope.row.reservationPurpose.substring(0, 100) + '...'
: scope.row.reservationPurpose
}}
</span>
<div class="purpose-text">
{{ scope.row.reservationPurpose }}
</div>
</el-tooltip>
</template>
</el-table-column>
@@ -110,17 +135,13 @@
:hide-after="100"
effect="dark"
>
<span class="audit-remark-text">
{{
scope.row.auditRemark && scope.row.auditRemark.length > 100
? scope.row.auditRemark.substring(0, 100) + '...'
: scope.row.auditRemark
}}
</span>
<div class="audit-remark-text">
{{ scope.row.auditRemark }}
</div>
</el-tooltip>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="220">
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="220" fixed="right">
<template #default="scope">
<!-- 查看按钮,所有记录都可以查看 -->
<el-tooltip content="查看" placement="top">
@@ -186,7 +207,7 @@
<span class="hall-id">(ID: {{ hall.id }})</span>
</div>
<div class="hall-description" v-if="hall.hallDescription">
{{ hall.hallDescription }}
{{ displayHallDescription(hall.hallDescription) }}
</div>
<div class="hall-images">
<template v-if="getHallImages(hall.hallImage || '').length > 0">
@@ -206,9 +227,9 @@
</el-select>
</el-form-item>
<!-- 报告厅图片展示区域 -->
<el-form-item label="报告厅图片" v-if="selectedHallImages.length > 0">
<el-form-item label="报告厅图片">
<div class="selected-hall-images">
<div class="image-gallery">
<div class="image-gallery" v-if="selectedHallImages.length > 0">
<div v-for="(image, index) in selectedHallImages" :key="index" class="image-item" @click="previewImage(image, index)">
<img :src="image" class="gallery-image" @error="handleImageError" />
<div class="image-overlay">
@@ -216,6 +237,10 @@
</div>
</div>
</div>
<div v-else class="no-images-placeholder">
<el-icon><Picture /></el-icon>
<span>暂无图片</span>
</div>
</div>
</el-form-item>
<el-form-item label="预约开始时间" prop="startTime">
@@ -226,6 +251,7 @@
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="请选择预约开始时间"
style="width: 100%"
@change="handleStartTimeChange"
>
</el-date-picker>
</el-form-item>
@@ -269,9 +295,9 @@
<span class="form-text">{{ viewForm.hallName }}</span>
</el-form-item>
<!-- 报告厅图片展示区域 -->
<el-form-item label="报告厅图片" v-if="viewHallImages.length > 0">
<el-form-item label="报告厅图片">
<div class="view-hall-images">
<div class="image-gallery">
<div class="image-gallery" v-if="viewHallImages.length > 0">
<div v-for="(image, index) in viewHallImages" :key="index" class="image-item" @click="previewImage(image, index)">
<img :src="image" class="gallery-image" @error="handleImageError" />
<div class="image-overlay">
@@ -279,6 +305,10 @@
</div>
</div>
</div>
<div v-else class="no-images-placeholder">
<el-icon><Picture /></el-icon>
<span>暂无图片</span>
</div>
</div>
</el-form-item>
<el-form-item label="预约开始时间">
@@ -301,6 +331,19 @@
<el-form-item label="预约状态">
<dict-tag :options="hall_reserv_status" :value="viewForm.status" />
</el-form-item>
<!-- 当状态为审核中时显示可审核人员列表 -->
<el-form-item label="可审核人员" v-if="viewForm.status === '0' && auditorList.length > 0">
<div class="auditor-list">
<div v-for="auditor in auditorList" :key="auditor.userId" class="auditor-item">
<el-avatar :size="32" :src="auditor.avatar">{{ auditor.nickName?.substring(0, 1) }}</el-avatar>
<div class="auditor-info">
<div class="auditor-name">{{ auditor.nickName || auditor.userName }}</div>
<div class="auditor-dept">{{ auditor.dept?.deptName }}</div>
</div>
</div>
<div v-if="auditorList.length === 0" class="no-auditor">暂无可审核人员</div>
</div>
</el-form-item>
<el-form-item label="审核人" v-if="viewForm.auditName">
<span class="form-text">{{ viewForm.auditName }}</span>
</el-form-item>
@@ -325,7 +368,7 @@
<script setup name="HallReservation" lang="ts">
import { listHallReservation, getHallReservation, delHallReservation, addHallReservation, updateHallReservation } from '@/api/hq/hallReservation';
import { listHallInfo } from '@/api/hq/hallInfo';
import { getUserProfile } from '@/api/system/user';
import { getUserProfile, listUsersByPermission } from '@/api/system/user';
import { HallReservationVO, HallReservationQuery, HallReservationForm } from '@/api/hq/hallReservation/types';
import { HallInfoVO } from '@/api/hq/hallInfo/types';
import { useUserStore } from '@/store/modules/user';
@@ -375,6 +418,44 @@ const viewForm = ref<HallReservationForm>({
auditRemark: undefined
});
// 添加审核人员列表
const auditorList = ref<any[]>([]);
/** 获取具有审核权限的人员列表 */
const getAuditorList = async () => {
try {
const res = await listUsersByPermission('hq:hallAudit:list');
auditorList.value = res.data || [];
console.log('审核人员列表:', auditorList.value);
} catch (error) {
console.error('获取审核人员列表失败:', error);
auditorList.value = [];
}
};
/** 验证结束时间 */
const validateEndTime = (rule: any, value: any, callback: any) => {
if (value && form.value.startTime) {
const startTime = new Date(form.value.startTime).getTime();
const endTime = new Date(value).getTime();
if (endTime <= startTime) {
callback(new Error('结束时间必须晚于开始时间'));
} else {
callback();
}
} else {
callback();
}
};
/** 开始时间变化时触发结束时间验证 */
const handleStartTimeChange = () => {
// 如果结束时间已经填写,则触发验证
if (data.form.endTime) {
hallReservationFormRef.value?.validateField('endTime');
}
};
const initFormData: HallReservationForm = {
id: undefined,
hallId: undefined,
@@ -390,6 +471,7 @@ const initFormData: HallReservationForm = {
auditTime: undefined,
auditRemark: undefined
};
const data = reactive<PageData<HallReservationForm, HallReservationQuery>>({
form: { ...initFormData },
queryParams: {
@@ -409,7 +491,25 @@ const data = reactive<PageData<HallReservationForm, HallReservationQuery>>({
rules: {
hallName: [{ required: true, message: '报告厅名称不能为空', trigger: 'change' }],
startTime: [{ required: true, message: '预约开始时间不能为空', trigger: 'blur' }],
endTime: [{ required: true, message: '预约结束时间不能为空', trigger: 'blur' }],
endTime: [
{ required: true, message: '预约结束时间不能为空', trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
if (value && data.form.startTime) {
const startTime = new Date(data.form.startTime).getTime();
const endTime = new Date(value).getTime();
if (endTime <= startTime) {
callback(new Error('结束时间必须晚于开始时间'));
} else {
callback();
}
} else {
callback();
}
},
trigger: 'blur'
}
],
reservationPurpose: [{ required: true, message: '预约用途不能为空', trigger: 'blur' }]
}
});
@@ -635,6 +735,11 @@ const handleView = async (row: HallReservationVO) => {
const res = await getHallReservation(row.id);
Object.assign(viewForm.value, res.data);
// 如果状态是审核中,获取审核人员列表
if (viewForm.value.status === '0') {
await getAuditorList();
}
// 查找对应的报告厅并显示图片
if (viewForm.value.hallName) {
const selectedHall = hallList.value.find((hall) => hall.hallName === viewForm.value.hallName);
@@ -747,6 +852,13 @@ const handleExport = () => {
);
};
// 在 <script setup> 部分适当位置添加工具函数
/** 将文本截断到指定长度(默认20个字符),超出部分以省略号结尾 */
const displayHallDescription = (desc: string, maxLen = 20) => {
if (!desc) return '';
return desc.length > maxLen ? desc.slice(0, maxLen) + '...' : desc;
};
onMounted(() => {
getList();
getHallList();
@@ -808,19 +920,19 @@ onMounted(() => {
font-size: 12px;
color: #606266;
margin-bottom: 4px;
line-height: 1.3;
max-height: 36px;
max-width: 240px; /* 新增:限制最大宽度,防止下拉框过宽 */
overflow: hidden;
white-space: nowrap; /* 单行显示 */
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
display: block;
}
/* 下拉框图片容器:每行最多 6 张图片 */
.hall-images {
display: flex;
display: grid;
grid-template-columns: repeat(6, 40px); /* 6 列,每列宽 40px */
gap: 4px;
flex-wrap: wrap;
}
.hall-image {
@@ -849,22 +961,28 @@ onMounted(() => {
/* 表格中预约用途文本样式 */
.purpose-text {
display: inline-block;
max-width: 100%;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
word-break: break-all;
word-wrap: break-word;
line-height: 1.4;
cursor: pointer;
max-height: 4.2em;
}
/* 表格中审核备注文本样式 */
.audit-remark-text {
display: inline-block;
max-width: 100%;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
word-break: break-all;
word-wrap: break-word;
line-height: 1.4;
cursor: pointer;
max-height: 4.2em;
}
/* 查看详情表单样式 */
@@ -923,6 +1041,7 @@ onMounted(() => {
display: flex;
flex-wrap: wrap;
gap: 12px;
max-width: 450px; /* 与预约用途保持一致 */
}
.image-item {
@@ -970,6 +1089,88 @@ onMounted(() => {
color: white;
font-size: 20px;
}
/* 暂无图片占位样式 */
.no-images-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
max-width: 450px; /* 与预约用途保持一致 */
height: 120px;
background-color: #f5f7fa;
border: 2px dashed #dcdfe6;
border-radius: 8px;
color: #909399;
}
.no-images-placeholder .el-icon {
font-size: 32px;
margin-bottom: 8px;
}
.no-images-placeholder span {
font-size: 14px;
}
/* 审核人员列表样式 */
.auditor-list {
display: flex;
flex-wrap: wrap;
gap: 16px;
padding: 8px 0;
}
.auditor-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background-color: #f5f7fa;
border-radius: 8px;
transition: all 0.3s ease;
}
.auditor-item:hover {
background-color: #ecf5ff;
transform: translateY(-2px);
}
.auditor-info {
display: flex;
flex-direction: column;
}
.auditor-name {
font-size: 14px;
color: #303133;
font-weight: 500;
}
.auditor-dept {
font-size: 12px;
color: #909399;
}
.no-auditor {
color: #909399;
font-size: 14px;
font-style: italic;
}
/* 页面容器样式 */
/* 搜索按钮容器样式 */
.search-btn-container {
display: flex;
justify-content: flex-end;
margin-top: 10px;
.el-form-item {
margin-bottom: 0 !important;
}
}
</style>
<style>
+97 -55
View File
@@ -4,27 +4,44 @@
<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" :inline="true" label-width="100px">
<el-form-item label="类型名称" prop="categoryName">
<el-input v-model="queryParams.categoryName" placeholder="请输入类型名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="类型编码" prop="categoryCode">
<el-input v-model="queryParams.categoryCode" placeholder="请输入类型编码" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="技能等级" prop="skillLevel">
<el-select v-model="queryParams.skillLevel" placeholder="请选择技能等级" clearable>
<el-option v-for="dict in pm_skill_level" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
<el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<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-form ref="queryFormRef" :model="queryParams" label-width="100px">
<!-- 第一行:4个检索元素 -->
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="类型名称" prop="categoryName">
<el-input v-model="queryParams.categoryName" placeholder="请输入类型名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="类型编码" prop="categoryCode">
<el-input v-model="queryParams.categoryCode" placeholder="请输入类型编码" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="技能等级" prop="skillLevel">
<el-select v-model="queryParams.skillLevel" placeholder="请选择技能等级" clearable style="width: 100%">
<el-option v-for="dict in pm_skill_level" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable style="width: 100%">
<el-option v-for="dict in sys_normal_disable" :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>
@@ -53,15 +70,17 @@
:default-expand-all="isExpandAll"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
border
header-align="center"
@row-click="handleRowClick"
style="width: 100%"
class="tree-table"
>
<el-table-column prop="categoryName" label="类型名称" min-width="260" show-overflow-tooltip>
<el-table-column align="left" header-align="center" prop="categoryName" label="类型名称" min-width="260" show-overflow-tooltip>
<template #default="scope">
<span class="category-name-cell">
<el-icon v-if="scope.row.iconUrl" class="category-icon" :style="{ color: scope.row.colorCode }">
<!-- <el-icon v-if="scope.row.iconUrl" class="category-icon" :style="{ color: scope.row.colorCode }">
<component :is="getCategoryIcon(scope.row.categoryCode)" />
</el-icon>
</el-icon> -->
<span class="category-name">{{ scope.row.categoryName }}</span>
<el-tag v-if="scope.row.childrenCount > 0" size="small" type="info" class="children-count-tag">
{{ scope.row.childrenCount }}个子类
@@ -69,8 +88,8 @@
</span>
</template>
</el-table-column>
<el-table-column prop="categoryCode" label="类型编码" width="150" show-overflow-tooltip />
<el-table-column prop="categoryDescription" label="类型描述" min-width="200" show-overflow-tooltip />
<el-table-column prop="categoryCode" label="类型编码" width="150" align="left" header-align="center" show-overflow-tooltip />
<el-table-column prop="categoryDescription" label="类型描述" min-width="200" align="left" header-align="center" show-overflow-tooltip />
<el-table-column prop="skillLevel" label="技能等级" width="100" align="center">
<template #default="scope">
<el-tag :type="getSkillLevelTagType(scope.row.skillLevel)" size="small">
@@ -78,7 +97,7 @@
</el-tag>
</template>
</el-table-column>
<el-table-column prop="estimatedDuration" label="预估时长" width="120" align="center">
<!-- <el-table-column prop="estimatedDuration" label="预估时长" width="120" align="center">
<template #default="scope">
<span v-if="scope.row.estimatedDuration">{{ formatDuration(scope.row.estimatedDuration) }}</span>
<span v-else>-</span>
@@ -90,7 +109,7 @@
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="usageCount" label="使用次数" width="100" align="center" />
<el-table-column prop="usageCount" label="使用次数" width="100" align="center" /> -->
<el-table-column prop="sortOrder" label="排序" width="80" align="center" />
<el-table-column prop="status" label="状态" width="80" align="center">
<template #default="scope">
@@ -170,7 +189,7 @@
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<!-- <el-row :gutter="20">
<el-col :span="12">
<el-form-item label="预估时长" prop="estimatedDuration">
<el-input-number v-model="form.estimatedDuration" :min="0" :max="9999" placeholder="分钟" style="width: 100%" />
@@ -181,13 +200,13 @@
<el-input-number v-model="form.standardCost" :min="0" :precision="2" placeholder="元" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
</el-row> -->
<el-row :gutter="20">
<el-col :span="12">
<!-- <el-col :span="12">
<el-form-item label="分类颜色" prop="colorCode">
<el-color-picker v-model="form.colorCode" :predefine="predefineColors" :clearable="false" style="width: 100%" />
</el-form-item>
</el-col>
</el-col> -->
<el-col :span="12">
<el-form-item label="排序顺序" prop="sortOrder">
<el-input-number v-model="form.sortOrder" :min="0" placeholder="数字越小越靠前" style="width: 100%" />
@@ -231,9 +250,9 @@
<el-descriptions :column="2" border>
<el-descriptions-item label="类型名称">
<div class="category-name-view">
<el-icon v-if="viewData.iconUrl" class="category-icon" :style="{ color: viewData.colorCode }">
<!-- <el-icon v-if="viewData.iconUrl" class="category-icon" :style="{ color: viewData.colorCode }">
<component :is="getCategoryIcon(viewData.categoryCode)" />
</el-icon>
</el-icon> -->
<span class="category-name">{{ viewData.categoryName }}</span>
</div>
</el-descriptions-item>
@@ -244,7 +263,7 @@
{{ getSkillLevelText(viewData.skillLevel) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="预估时长">
<!-- <el-descriptions-item label="预估时长">
{{ viewData.estimatedDuration ? formatDuration(viewData.estimatedDuration) : '-' }}
</el-descriptions-item>
<el-descriptions-item label="标准成本">
@@ -253,7 +272,7 @@
<el-descriptions-item label="使用次数">{{ viewData.usageCount }}</el-descriptions-item>
<el-descriptions-item label="最后使用时间">
{{ viewData.lastUsedTime ? proxy.parseTime(viewData.lastUsedTime, '{y}-{m}-{d} {h}:{i}') : '-' }}
</el-descriptions-item>
</el-descriptions-item> -->
<el-descriptions-item label="排序顺序">{{ viewData.sortOrder }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="viewData.status === '1' ? 'success' : 'danger'" size="small">
@@ -286,6 +305,8 @@ import {
updateRepairCategory,
getRepairCategoryTree
} from '@/api/hq/pm/repairCategory';
import { ref, reactive, getCurrentInstance, nextTick, onMounted, type ComponentInternalInstance } from 'vue';
import type { FormInstance as ElFormInstance, FormRules as ElFormRules } from 'element-plus';
import { RepairCategoryVO, RepairCategoryForm, RepairCategoryQuery } from '@/api/hq/pm/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -296,6 +317,8 @@ const loading = ref(true);
const showSearch = ref(true);
const isExpandAll = ref(false);
const refreshTable = ref(true);
// 标记首次加载,用于控制默认展示主分类树
const initialLoad = ref(true);
// 字典数据
const pm_skill_level = ref([
@@ -363,24 +386,20 @@ const buttonLoading = ref(false);
const getList = async () => {
loading.value = true;
try {
// 根据是否有搜索条件决定使用哪个接口
let res;
const hasSearchConditions =
queryParams.value.categoryName || queryParams.value.categoryCode || queryParams.value.skillLevel || queryParams.value.status;
if (hasSearchConditions) {
// 有搜索条件时使用列表接口,然后手动构建树形结构
res = await listRepairCategory(queryParams.value);
if (res.code === 200 && res.rows) {
categoryList.value = buildTreeFromList(res.rows);
// 首次加载时使用树形接口获取所有分类
if (initialLoad.value) {
const res = await getRepairCategoryTree();
if (res.code === 200 && res.data) {
categoryList.value = res.data;
} else {
categoryList.value = [];
}
initialLoad.value = false; // 首次加载完成后,后续操作都使用列表接口
} else {
// 无搜索条件时直接使用树形接口
res = await getRepairCategoryTree();
if (res.code === 200 && res.data) {
categoryList.value = res.data;
// 后续操作使用列表接口,并根据搜索条件过滤
const res = await listRepairCategory(queryParams.value);
if (res.code === 200 && res.rows) {
categoryList.value = buildTreeFromList(res.rows);
} else {
categoryList.value = [];
}
@@ -468,6 +487,10 @@ const handleUpdate = async (row: RepairCategoryVO) => {
getCategoryTree();
const res = await getRepairCategory(row.id);
Object.assign(form.value, res.data);
// 若后端未返回技能等级,默认设置为中级
if (!form.value.skillLevel) {
form.value.skillLevel = '2';
}
dialog.visible = true;
dialog.title = '修改维修类型';
};
@@ -479,6 +502,11 @@ const handleView = async (row: RepairCategoryVO) => {
viewDialog.visible = true;
};
/** 行点击预览 */
const handleRowClick = (row: RepairCategoryVO) => {
handleView(row);
};
/** 提交按钮 */
const submitForm = () => {
categoryFormRef.value?.validate(async (valid: boolean) => {
@@ -515,13 +543,17 @@ const handleDelete = async (row: RepairCategoryVO) => {
/** 状态修改 */
const handleStatusChange = async (row: RepairCategoryVO) => {
const originalStatus = row.status;
const text = row.status === '1' ? '启用' : '停用';
try {
await proxy?.$modal.confirm('确认要' + text + '"' + row.categoryName + '"类型吗?');
await updateRepairCategory({ id: row.id, status: row.status });
await proxy?.$modal.confirm(`确认要${text}"${row.categoryName}"类型吗?`);
// 移除前端辅助字段,只保留后端需要的核心字段
const { children, ...updateData } = row as any;
await updateRepairCategory(updateData);
proxy?.$modal.msgSuccess(text + '成功');
} catch {
row.status = row.status === '1' ? '0' : '1';
} catch (e) {
// 如果请求失败或被取消,则还原原状态
row.status = originalStatus;
}
};
@@ -543,7 +575,7 @@ const reset = () => {
categoryDescription: '',
parentId: 0, // 使用数字类型,确保与树形选择器数据类型一致
requiredSkills: '',
skillLevel: '',
skillLevel: '2', // 默认中级
estimatedDuration: undefined,
standardCost: undefined,
iconUrl: '',
@@ -840,4 +872,14 @@ onMounted(() => {
:deep(.el-color-picker__color) {
border: 0px !important;
}
/* 搜索按钮容器样式 */
.search-btn-container {
display: flex;
justify-content: flex-end;
margin-top: 10px;
.el-form-item {
margin-bottom: 0 !important;
}
}
</style>