铜陵经侦
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

598 lines
19 KiB

<!--
* 周调度:勾选案件生成标红模板,手填调度内容,推送人员全员确认后锁定可打印
-->
<template>
<div class="weekly-schedule-container">
<a-form class="smart-query-form">
<a-row class="smart-query-form-row" v-privilege="'weekly-schedule:query'">
<a-form-item label="调度编号" class="smart-query-form-item">
<a-input style="width: 180px" v-model:value="queryForm.scheduleNo" placeholder="请输入调度编号" allowClear />
</a-form-item>
<a-form-item label="状态" class="smart-query-form-item">
<a-select style="width: 140px" v-model:value="queryForm.status" placeholder="请选择" allowClear>
<a-select-option v-for="item in statusOptions" :key="item.value" :value="item.value">{{ item.desc }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item class="smart-query-form-item">
<a-button-group>
<a-button type="primary" @click="onSearch" v-privilege="'weekly-schedule:query'">
<template #icon><SearchOutlined /></template>
查询
</a-button>
<a-button @click="resetQuery" v-privilege="'weekly-schedule:query'">
<template #icon><ReloadOutlined /></template>
重置
</a-button>
</a-button-group>
</a-form-item>
</a-row>
</a-form>
<div class="section-card">
<a-row class="smart-table-btn-block">
<div class="smart-table-operate-block">
<a-button type="primary" v-privilege="'weekly-schedule:add'" @click="openCreate">
<template #icon><PlusOutlined /></template>
新增
</a-button>
<a-radio-group v-model:value="listTab" button-style="solid" class="list-tab" @change="onTabChange">
<a-radio-button :value="false">全部</a-radio-button>
<a-radio-button :value="true">待我确认</a-radio-button>
</a-radio-group>
</div>
<div class="smart-table-setting-block">
<TableOperator v-model="columns" :tableId="TABLE_ID_CONST.BUSINESS.CASE.WEEKLY_SCHEDULE" :refresh="queryData" />
</div>
</a-row>
<a-table
size="small"
:dataSource="tableData"
:columns="columns"
rowKey="scheduleId"
:scroll="{ x: 1200 }"
bordered
:pagination="false"
:loading="tableLoading"
>
<template #bodyCell="{ record, column }">
<template v-if="column.dataIndex === 'status'">
<a-tag :color="getWeeklyScheduleStatusColor(record.status)">{{ record.statusName || getWeeklyScheduleStatusName(record.status) }}</a-tag>
</template>
<template v-else-if="column.dataIndex === 'progress'">
{{ record.confirmedCount || 0 }}/{{ record.memberCount || 0 }}
</template>
<template v-else-if="column.dataIndex === 'createTime'">
{{ formatTime(record.createTime) }}
</template>
<template v-else-if="column.dataIndex === 'action'">
<div class="smart-table-operate">
<a-button type="link" size="small" v-privilege="'weekly-schedule:view'" @click="openDetail(record)">查看</a-button>
<a-button type="link" size="small" v-if="record.canPush" v-privilege="'weekly-schedule:push'" @click="handlePush(record)">推送</a-button>
<a-popconfirm title="确认调度内容后不可更改,确定确认?" ok-text="确定" cancel-text="取消" @confirm="handleConfirm(record)">
<a-button type="link" size="small" v-if="record.canConfirm" v-privilege="'weekly-schedule:confirm'">确认</a-button>
</a-popconfirm>
<a-button type="link" size="small" v-if="record.canPrint" v-privilege="'weekly-schedule:print'" @click="handlePrint(record)">打印</a-button>
</div>
</template>
</template>
</a-table>
<div class="smart-query-table-page">
<a-pagination
showSizeChanger
showQuickJumper
show-less-items
:pageSizeOptions="PAGE_SIZE_OPTIONS"
v-model:current="queryForm.pageNum"
v-model:pageSize="queryForm.pageSize"
:total="pagination.total"
@change="onPageChange"
@showSizeChange="onPageChange"
:show-total="(total) => `共${total}条`"
/>
</div>
</div>
<a-modal
v-model:open="createVisible"
title="新增周调度"
:width="1180"
:maskClosable="false"
:confirm-loading="createLoading"
@cancel="closeCreate"
>
<div class="create-layout">
<div class="create-left">
<a-form layout="inline" class="case-search-form">
<a-form-item label="案件名称">
<a-input v-model:value="caseQuery.caseName" placeholder="关键字模糊搜索" allowClear style="width: 160px" />
</a-form-item>
<a-form-item label="案件编号">
<a-input v-model:value="caseQuery.caseNo" placeholder="案件编号" allowClear style="width: 140px" />
</a-form-item>
<a-form-item>
<a-button type="primary" @click="searchCases">搜索</a-button>
</a-form-item>
</a-form>
<a-table
size="small"
:dataSource="caseTableData"
:columns="caseColumns"
rowKey="caseId"
:pagination="false"
:loading="caseLoading"
:row-selection="{ selectedRowKeys: selectedCaseIds, onChange: onSelectCases }"
:scroll="{ y: 280 }"
bordered
/>
<div class="smart-query-table-page">
<a-pagination
size="small"
:pageSize="caseQuery.pageSize"
v-model:current="caseQuery.pageNum"
:total="casePagination.total"
@change="onCasePageChange"
:show-total="(total) => `共${total}条`"
/>
</div>
</div>
<div class="create-right">
<div class="paper-preview">
<WeeklyScheduleMinutes
:form="createForm"
:cases="selectedCases"
:members="selectedMembers"
editable
/>
</div>
<div class="member-block">
<div class="member-header">
<span>参加调度人员</span>
<a-button size="small" @click="openEmployeeSelect">选择人员</a-button>
</div>
<div class="member-tags">
<a-tag v-for="item in selectedMembers" :key="item.employeeId" closable @close="removeMember(item.employeeId)">
{{ item.actualName }}
</a-tag>
<span v-if="!selectedMembers.length" class="empty-hint">请勾选参加调度人员进行推送</span>
</div>
</div>
</div>
</div>
<template #footer>
<a-button @click="closeCreate">取消</a-button>
<a-button @click="submitCreate(false)" :loading="createLoading" v-privilege="'weekly-schedule:add'">保存草稿</a-button>
<a-button type="primary" @click="submitCreate(true)" :loading="createLoading" v-privilege="'weekly-schedule:push'">推送</a-button>
</template>
</a-modal>
<EmployeeTableSelectModal ref="selectEmployeeModal" @selectData="onSelectEmployees" />
<a-drawer v-model:open="detailVisible" title="周调度详情" placement="right" :width="780">
<div class="paper-preview" v-if="currentRecord">
<div class="meta-line">调度编号:{{ currentRecord.scheduleNo }} 状态:{{ currentRecord.statusName }} 创建人:{{ currentRecord.createUserName || '-' }}</div>
<WeeklyScheduleMinutes
:form="currentRecord"
:cases="currentRecord.caseList || []"
:members="currentRecord.memberList || []"
/>
<div class="content-label">确认进度</div>
<a-tag v-for="item in currentRecord.memberList || []" :key="item.employeeId" :color="item.confirmedFlag ? 'success' : 'warning'">
{{ item.employeeName }}{{ item.confirmedFlag ? '(已确认)' : '(待确认)' }}
</a-tag>
<div class="detail-actions">
<a-popconfirm v-if="currentRecord.canConfirm" title="确认调度内容后不可更改,确定确认?" @confirm="handleConfirm(currentRecord)">
<a-button type="primary" v-privilege="'weekly-schedule:confirm'">确认</a-button>
</a-popconfirm>
<a-button v-if="currentRecord.canPush" v-privilege="'weekly-schedule:push'" @click="handlePush(currentRecord)">推送</a-button>
<a-button v-if="currentRecord.canPrint" v-privilege="'weekly-schedule:print'" @click="printWeeklySchedule(currentRecord)">打印</a-button>
</div>
</div>
</a-drawer>
</div>
</template>
<script setup>
import { reactive, ref, onMounted } from 'vue';
import { message } from 'ant-design-vue';
import { SearchOutlined, ReloadOutlined, PlusOutlined } from '@ant-design/icons-vue';
import _ from 'lodash';
import dayjs from 'dayjs';
import TableOperator from '/@/components/support/table-operator/index.vue';
import EmployeeTableSelectModal from '/@/components/system/employee-table-select-modal/index.vue';
import { PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
import { TABLE_ID_CONST } from '/@/constants/support/table-id-const';
import { smartSentry } from '/@/lib/smart-sentry';
import { weeklyScheduleApi } from '/@/api/business/case-management/weekly-schedule-api.js';
import { employeeApi } from '/@/api/system/employee-api';
import {
WEEKLY_SCHEDULE_STATUS_ENUM,
getWeeklyScheduleStatusName,
getWeeklyScheduleStatusColor,
} from '/@/constants/business/case-management/case-supervision-const.js';
import { printWeeklySchedule } from '/@/utils/print-weekly-schedule.js';
import WeeklyScheduleMinutes from './weekly-schedule-minutes.vue';
import { useUserStore } from '/@/store/modules/system/user';
const statusOptions = Object.values(WEEKLY_SCHEDULE_STATUS_ENUM);
const listTab = ref(false);
const columns = ref([
{ title: '调度编号', dataIndex: 'scheduleNo', width: 160 },
{ title: '调度内容', dataIndex: 'scheduleContent', width: 260, ellipsis: true },
{ title: '案件数', dataIndex: 'caseCount', width: 80, align: 'center' },
{ title: '确认进度', dataIndex: 'progress', width: 100, align: 'center' },
{ title: '状态', dataIndex: 'status', width: 100, align: 'center' },
{ title: '创建人', dataIndex: 'createUserName', width: 100 },
{ title: '创建时间', dataIndex: 'createTime', width: 160 },
{ title: '操作', dataIndex: 'action', fixed: 'right', width: 200 },
]);
const queryFormState = {
scheduleNo: '',
status: undefined,
pendingMine: false,
pageNum: 1,
pageSize: 10,
};
const queryForm = reactive(_.cloneDeep(queryFormState));
const tableLoading = ref(false);
const tableData = ref([]);
const pagination = reactive({ total: 0 });
const createVisible = ref(false);
const createLoading = ref(false);
const userStore = useUserStore();
const createForm = reactive({
meetingTime: undefined,
meetingPlace: '',
scheduleUnit: '',
hostName: '',
recorderName: '',
});
const selectedCases = ref([]);
const selectedMembers = ref([]);
const employeeMap = ref([]);
const selectEmployeeModal = ref();
const caseQuery = reactive({ caseNo: '', caseName: '', pageNum: 1, pageSize: 8 });
const caseTableData = ref([]);
const casePagination = reactive({ total: 0 });
const caseLoading = ref(false);
const caseColumns = [
{ title: '案件名称', dataIndex: 'caseName', ellipsis: true },
{ title: '案件编号', dataIndex: 'caseNo', width: 140 },
{ title: '主办民警', dataIndex: 'handlerName', width: 90 },
{ title: '办案单位', dataIndex: 'handleDept', width: 130, ellipsis: true },
];
const selectedCaseIds = ref([]);
const detailVisible = ref(false);
const currentRecord = ref(null);
function resetQuery() {
const pageSize = queryForm.pageSize;
Object.assign(queryForm, _.cloneDeep(queryFormState));
queryForm.pageSize = pageSize;
listTab.value = false;
queryData();
}
function onSearch() {
queryForm.pageNum = 1;
queryData();
}
function onTabChange() {
queryForm.pendingMine = listTab.value;
queryForm.pageNum = 1;
queryData();
}
async function queryData() {
queryForm.pendingMine = listTab.value;
tableLoading.value = true;
try {
const res = await weeklyScheduleApi.query(queryForm);
tableData.value = res.data?.list || [];
pagination.total = res.data?.total || 0;
} catch (e) {
smartSentry.captureError(e);
} finally {
tableLoading.value = false;
}
}
function onPageChange(page, pageSize) {
queryForm.pageNum = page;
queryForm.pageSize = pageSize;
queryData();
}
function emptyCreateForm() {
createForm.meetingTime = undefined;
createForm.meetingPlace = '';
createForm.scheduleUnit = userStore.departmentName || '';
createForm.hostName = '';
createForm.recorderName = '';
}
async function openCreate() {
emptyCreateForm();
selectedCases.value = [];
selectedCaseIds.value = [];
selectedMembers.value = [];
createVisible.value = true;
await loadEmployees();
await searchCases();
}
function closeCreate() {
createVisible.value = false;
}
async function searchCases() {
caseQuery.pageNum = 1;
await loadCasePage();
}
async function loadCasePage() {
caseLoading.value = true;
try {
const res = await weeklyScheduleApi.searchCase(caseQuery);
caseTableData.value = res.data?.list || [];
casePagination.total = res.data?.total || 0;
} catch (e) {
smartSentry.captureError(e);
} finally {
caseLoading.value = false;
}
}
function onCasePageChange(page) {
caseQuery.pageNum = page;
loadCasePage();
}
function onSelectCases(keys) {
const map = new Map(selectedCases.value.map((item) => [item.caseId, item]));
caseTableData.value.forEach((row) => {
const existing = map.get(row.caseId);
map.set(row.caseId, {
...row,
itemContent: existing?.itemContent || '',
});
});
selectedCases.value = keys.map((id) => map.get(id)).filter(Boolean);
selectedCaseIds.value = keys;
}
async function loadEmployees() {
try {
const res = await employeeApi.queryAll();
employeeMap.value = res.data || [];
} catch (e) {
smartSentry.captureError(e);
}
}
function openEmployeeSelect() {
selectEmployeeModal.value?.showModal(selectedMembers.value.map((item) => item.employeeId));
}
function onSelectEmployees(ids) {
const list = selectedMembers.value.slice();
(ids || []).forEach((id) => {
if (list.some((item) => item.employeeId === id)) {
return;
}
const emp = employeeMap.value.find((item) => item.employeeId === id);
list.push({ employeeId: id, actualName: emp?.actualName || String(id) });
});
selectedMembers.value = list;
}
function removeMember(employeeId) {
selectedMembers.value = selectedMembers.value.filter((item) => item.employeeId !== employeeId);
}
async function submitCreate(pushFlag) {
if (!selectedCases.value.length) {
message.warning('请勾选调度案件');
return;
}
const itemContentList = selectedCases.value.map((item) => item.itemContent || '');
if (pushFlag && !itemContentList.some((text) => String(text).trim())) {
message.warning('请输入调度内容');
return;
}
if (pushFlag && !selectedMembers.value.length) {
message.warning('请勾选参加调度人员');
return;
}
createLoading.value = true;
try {
await weeklyScheduleApi.create({
caseIdList: selectedCases.value.map((item) => item.caseId),
itemContentList,
meetingTime: createForm.meetingTime,
meetingPlace: createForm.meetingPlace,
scheduleUnit: createForm.scheduleUnit,
hostName: createForm.hostName,
recorderName: createForm.recorderName,
memberIdList: selectedMembers.value.map((item) => item.employeeId),
pushFlag,
});
message.success(pushFlag ? '已推送,等待参加人员确认' : '草稿已保存');
closeCreate();
queryData();
} catch (e) {
smartSentry.captureError(e);
} finally {
createLoading.value = false;
}
}
async function openDetail(record) {
try {
const res = await weeklyScheduleApi.detail(record.scheduleId);
currentRecord.value = res.data;
detailVisible.value = true;
} catch (e) {
smartSentry.captureError(e);
}
}
async function handlePush(record) {
try {
const res = await weeklyScheduleApi.push(record.scheduleId);
message.success(res.msg || res.data || '已推送');
queryData();
if (detailVisible.value) {
openDetail(record);
}
} catch (e) {
smartSentry.captureError(e);
}
}
async function handleConfirm(record) {
try {
const res = await weeklyScheduleApi.confirm(record.scheduleId);
message.success(res.msg || res.data || '确认成功');
queryData();
if (detailVisible.value) {
openDetail(record);
}
} catch (e) {
smartSentry.captureError(e);
}
}
async function handlePrint(record) {
try {
const res = await weeklyScheduleApi.detail(record.scheduleId);
printWeeklySchedule(res.data);
} catch (e) {
smartSentry.captureError(e);
}
}
function formatTime(value) {
if (!value) {
return '-';
}
return dayjs(value).isValid() ? dayjs(value).format('YYYY-MM-DD HH:mm') : value;
}
onMounted(() => {
queryData();
});
</script>
<style lang="less" scoped>
.weekly-schedule-container {
padding: 16px;
.section-card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
padding: 16px;
margin-bottom: 16px;
}
.list-tab {
margin-left: 12px;
}
:deep(.smart-table-operate) {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
}
.create-layout {
display: flex;
gap: 16px;
min-height: 460px;
}
.create-left,
.create-right {
flex: 1;
min-width: 0;
}
.case-search-form {
margin-bottom: 8px;
}
.paper-preview {
border: 1px solid #e8e8e8;
border-radius: 6px;
padding: 12px;
background: #fafafa;
max-height: 560px;
overflow: auto;
}
.case-block {
border: 1px solid #f0f0f0;
background: #fff;
padding: 8px 12px;
margin-bottom: 8px;
line-height: 1.9;
}
.label {
display: inline-block;
width: 80px;
color: #595959;
}
.red-field {
color: #cf1322;
font-weight: 600;
}
.content-label {
margin: 8px 0 6px;
font-weight: 600;
}
.content-text {
white-space: pre-wrap;
min-height: 48px;
}
.empty-hint {
color: #8c8c8c;
font-size: 13px;
}
.member-block {
margin-top: 12px;
}
.member-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.member-tags {
min-height: 32px;
}
.meta-line {
line-height: 2;
}
.detail-actions {
margin-top: 16px;
display: flex;
gap: 8px;
}
</style>