民意中心系统
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.
 
 
 
 

752 lines
19 KiB

<!--
* 警务评议与问题处置 - 任务随机认领大厅
*
* @Author: Antigravity
* @Date: 2026-06-10
-->
<template>
<div class="task-pool-wrapper">
<div class="lobby-grid">
<!-- 左栏控制与领单中心 (稍微变大更显眼) -->
<section class="lobby-left-card">
<header class="lobby-header">
<div class="system-badge">民意感知系统</div>
<h1 class="main-title">任务认领中心</h1>
</header>
<!-- 液晶待领数监控 (稍微放大更清晰) -->
<div class="quantity-monitor-container">
<div class="water-ripple-ring" :class="{ 'is-loading': loading }">
<span class="ring-pulse pulse-1"></span>
<span class="ring-pulse pulse-2"></span>
<div class="inner-display">
<span class="count-label">大池待领工单</span>
<div class="number-glow">
<span v-if="loading" class="loading-dots">...</span>
<span v-else class="count-value">{{ unclaimedCount }}</span>
</div>
<span class="unit">件</span>
</div>
</div>
</div>
<!-- 随机领单操作 (按钮稍微变大,极简质感) -->
<div class="action-panel">
<a-tooltip :title="!hasClaimPrivilege ? '当前角色无认领权限,仅限市级民意岗和市访评员操作' : ''" placement="top">
<div class="btn-container">
<button
class="random-btn"
:class="{ 'is-disabled': !hasClaimPrivilege || unclaimedCount === 0 }"
:disabled="!hasClaimPrivilege || unclaimedCount === 0 || claiming"
@click="handleRandomClaim"
>
<span class="btn-ripple"></span>
<span class="btn-content">
<template v-if="claiming">
<LoadingOutlined class="btn-icon animate-spin" />
正在派单中...
</template>
<template v-else-if="!hasClaimPrivilege">
<LockOutlined class="btn-icon" />
无权认领
</template>
<template v-else-if="unclaimedCount === 0">
大池已领空
</template>
<template v-else>
<ThunderboltOutlined class="btn-icon" />
随机认领工单
</template>
</span>
</button>
</div>
</a-tooltip>
<div class="claim-rules" v-if="hasClaimPrivilege && unclaimedCount > 0">
<InfoCircleOutlined class="rule-icon" />
<span>系统采用乐观锁随机分派,确保派单的公正性与随机性</span>
</div>
</div>
<div class="lobby-footer">
<a-button type="link" @click="fetchLobbyData" class="refresh-link">
<template #icon><ReloadOutlined /></template>
同步最新池状态
</a-button>
</div>
</section>
<!-- 右栏:大厅简报与动态播报 -->
<section class="lobby-right-card">
<!-- 看板 1:我的今日战绩 -->
<div class="brief-card my-stats">
<h3 class="panel-title"><DashboardOutlined class="title-icon" /> 个人今日概览</h3>
<div class="stats-grid">
<div class="stats-item">
<span class="stat-num text-blue">{{ todayClaimCount }}</span>
<span class="stat-label">今日已领 (件)</span>
</div>
<div class="stats-divider"></div>
<div class="stats-item clickable" @click="goToWorkbench">
<span class="stat-num text-purple">{{ workbenchCount }}</span>
<span class="stat-label">待办处理 (件) <RightOutlined class="arrow" /></span>
</div>
</div>
</div>
<!-- 看板 2:大厅实时广播播报 -->
<div class="brief-card real-time-broadcast">
<h3 class="panel-title"><NotificationOutlined class="title-icon" /> 实时认领动态</h3>
<div class="broadcast-list-wrapper">
<div class="broadcast-list">
<div class="broadcast-item" v-for="(item, index) in mockBroadcasts" :key="index">
<span class="b-time">{{ item.time }}</span>
<span class="b-text">
[<strong>{{ item.role }}</strong>] {{ item.name }} 认领了
<a-tag :color="getBizTypeColor(item.bizType)" class="mini-tag">{{ item.bizName }}</a-tag>
<span class="b-slbh">{{ item.slbh.split('-')[2] || item.slbh }}</span>
</span>
</div>
</div>
</div>
</div>
<!-- 看板 3大厅认领规范指引 -->
<div class="brief-card tips-card">
<h3 class="panel-title"><AlertOutlined class="title-icon text-orange" /> 认领工作须知</h3>
<ul class="tips-list">
<li> <strong>时效要求</strong>认领后的工单需在 24 小时内完成首次回访核实</li>
<li>🔒 <strong>数据隔离</strong>系统会自动根据您的属地及警种过滤展示个人名下的待办</li>
</ul>
</div>
</section>
</div>
<!-- 领单成功高级弹窗 -->
<a-modal
v-model:open="successModalVisible"
:footer="null"
:closable="false"
:maskClosable="false"
:width="480"
wrapClassName="success-claim-modal"
>
<div class="success-card">
<div class="success-icon-wrapper">
<CheckCircleFilled class="success-check-icon" />
</div>
<h2 class="success-title">工单认领成功</h2>
<p class="success-msg">已成功将该任务加入您的个人工作台</p>
<!-- 工单简述卡片 -->
<div class="wtcz-brief-card" v-if="claimedWtcz">
<div class="card-item">
<span class="item-label">受理编号</span>
<span class="item-value highlight">{{ claimedWtcz.slbh }}</span>
</div>
<div class="card-item">
<span class="item-label">业务大类</span>
<a-tag color="blue">{{ getBusinessClassificationLabel(claimedWtcz.sjlyNo) }}</a-tag>
</div>
<div class="card-item">
<span class="item-label">涉及单位</span>
<span class="item-value">{{ claimedWtcz.sjdwMc || '暂无涉及单位' }}</span>
</div>
<div class="card-item align-top">
<span class="item-label">诉求内容</span>
<span class="item-value summary">{{ claimedWtcz.sqnr || claimedWtcz.wtms || '无诉求描述' }}</span>
</div>
</div>
<!-- 弹窗操作 -->
<div class="modal-actions">
<a-button type="primary" size="large" block @click="goToWorkbench" class="workbench-action-btn">
立即去工作台处置
</a-button>
<a-button size="large" block @click="successModalVisible = false" class="continue-action-btn">
留在认领大厅
</a-button>
</div>
</div>
</a-modal>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue';
import { useRouter } from 'vue-router';
import { message } from 'ant-design-vue';
import {
ThunderboltOutlined,
ReloadOutlined,
CheckCircleFilled,
InfoCircleOutlined,
DashboardOutlined,
NotificationOutlined,
AlertOutlined,
RightOutlined
} from '@ant-design/icons-vue';
import { wtczApi } from '/@/api/business/jwpy/wtcz-api';
import { useUserStore } from '/@/store/modules/system/user';
const router = useRouter();
const userStore = useUserStore();
// 核心状态变量
const unclaimedCount = ref(0);
const loading = ref(false);
const claiming = ref(false);
const successModalVisible = ref(false);
const claimedWtcz = ref(null);
// 统计数据 (Mock展示与工作台连动)
const todayClaimCount = ref(3);
const workbenchCount = ref(0);
// 判定是否有随机认领权限 (绑定 task:claim)
const hasClaimPrivilege = computed(() => {
return userStore.administratorFlag || userStore.pointsList.includes('task:claim');
});
// 获取大厅待领工单数量及工作台待办数
const fetchLobbyData = async () => {
if (loading.value) return;
loading.value = true;
try {
// 1. 获取大厅待领总数
const res = await wtczApi.getUnclaimedCount();
if (res.data !== undefined) {
unclaimedCount.value = Number(res.data);
}
// 2. 获取当前用户工作台已领待办数
const wbRes = await wtczApi.queryPage({
processorId: userStore.employeeId,
assignStatus: 1,
pageNum: 1,
pageSize: 1
});
if (wbRes.data && wbRes.data.total !== undefined) {
workbenchCount.value = wbRes.data.total;
}
} catch (err) {
message.error('同步大厅数据失败');
} finally {
loading.value = false;
}
};
// 触发随机认领操作
const handleRandomClaim = async () => {
if (claiming.value) return;
claiming.value = true;
try {
const res = await wtczApi.claimRandom();
if (res.success && res.data) {
claimedWtcz.value = res.data;
successModalVisible.value = true;
todayClaimCount.value += 1;
// 成功后重新刷新大厅剩余数量与工作台待办
await fetchLobbyData();
} else {
message.warning(res.msg || '大厅已被领空,或正在高并发操作中,请稍后刷新重试');
}
} catch (err) {
message.error('认领请求失败,请稍后重试');
} finally {
claiming.value = false;
}
};
// 获取业务类型的显示
const getBusinessClassificationLabel = (sjlyNo) => {
const map = {
'110': '110接处警',
'hz': '户政窗口',
'crj': '出入境',
'cjg': '车驾管',
'ajbl': '案件办理'
};
return map[sjlyNo] || sjlyNo || '常规工单';
};
const getBizTypeColor = (bizType) => {
const map = {
'110': 'red',
'hz': 'blue',
'crj': 'purple',
'cjg': 'green',
'ajbl': 'orange'
};
return map[bizType] || 'cyan';
};
// 跳转到工作台
const goToWorkbench = () => {
successModalVisible.value = false;
router.push('/jwpy/flow/my-workbench');
};
// 大厅广播动态数据 (Mock 真实大厅的滚动氛围)
const mockBroadcasts = ref([
{ time: '17:18', role: '市局民意岗', name: '张警官', bizType: '110', bizName: '110接处警', slbh: 'GD-110-2026061001' },
{ time: '17:15', role: '市访评员', name: '李回访', bizType: 'hz', bizName: '户政窗口', slbh: 'GD-hz-2026061034' },
{ time: '17:08', role: '支队承办岗', name: '王所长', bizType: 'crj', bizName: '出入境', slbh: 'GD-crj-2026061019' }
]);
onMounted(() => {
fetchLobbyData();
});
</script>
<style scoped>
/* 容器全屏且高度自适应,锁死 overflow 绝对防滚动条 */
.task-pool-wrapper {
width: 100%;
height: calc(100vh - 130px);
min-height: 540px;
max-height: calc(100vh - 130px);
display: flex;
justify-content: center;
align-items: center;
padding: 16px 40px;
/* 优化极光背景渐变,使四周看起来不单调、饱满优雅 */
background: radial-gradient(circle at 15% 20%, rgba(24, 144, 255, 0.08) 0%, transparent 45%),
radial-gradient(circle at 85% 80%, rgba(114, 46, 209, 0.08) 0%, transparent 45%),
#f0f2f5;
position: relative;
overflow: hidden;
box-sizing: border-box;
}
/* 饱满和谐的网格布局,拓宽最大宽度,解决四周空白问题 */
.lobby-grid {
width: 100%;
max-width: 1260px;
height: 100%;
max-height: 510px;
display: grid;
grid-template-columns: 1.15fr 1.05fr;
gap: 32px;
z-index: 2;
box-sizing: border-box;
}
/* 舒展大气的玻璃卡片 */
.lobby-left-card,
.lobby-right-card {
background: rgba(255, 255, 255, 0.82);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.6);
border-radius: 24px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.02);
padding: 32px 40px;
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
box-sizing: border-box;
}
.lobby-right-card {
padding: 24px 32px;
}
/* 左侧核心控制面板 */
.lobby-left-card {
align-items: center;
}
/* 标题区 */
.lobby-header {
text-align: center;
}
.system-badge {
display: inline-block;
padding: 4px 12px;
background: rgba(24, 144, 255, 0.08);
color: #1890ff;
border-radius: 12px;
font-size: 11px;
font-weight: 600;
margin-bottom: 6px;
}
.main-title {
font-size: 26px;
font-weight: 700;
color: #1f1f1f;
margin: 0;
}
/* 待领数量监控圆环 */
.quantity-monitor-container {
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.water-ripple-ring {
width: 196px;
height: 196px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.9);
border: 4px solid rgba(24, 144, 255, 0.08);
display: flex;
justify-content: center;
align-items: center;
position: relative;
box-shadow: 0 8px 20px rgba(24, 144, 255, 0.04);
}
.ring-pulse {
position: absolute;
top: -4px;
left: -4px;
width: 204px;
height: 204px;
border-radius: 50%;
border: 1px solid rgba(24, 144, 255, 0.2);
animation: pulse-ring 3s cubic-bezier(0.215, 0.610, 0.355, 1) infinite;
pointer-events: none;
}
.pulse-2 {
animation-delay: 1.5s;
}
@keyframes pulse-ring {
0% { transform: scale(0.95); opacity: 1; }
100% { transform: scale(1.12); opacity: 0; }
}
.inner-display {
display: flex;
flex-direction: column;
align-items: center;
z-index: 3;
}
.count-label {
font-size: 11px;
color: #8c8c8c;
margin-bottom: 2px;
}
.number-glow {
height: 64px;
display: flex;
align-items: center;
}
.count-value {
font-size: 58px;
font-weight: 800;
font-family: 'Outfit', sans-serif;
background: linear-gradient(135deg, #1890ff, #722ed1);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
line-height: 1;
}
.loading-dots {
font-size: 26px;
color: #1890ff;
}
.unit {
font-size: 11px;
color: #8c8c8c;
margin-top: 1px;
}
/* 操作面板 */
.action-panel {
width: 100%;
max-width: 320px;
text-align: center;
}
.btn-container {
width: 100%;
margin-bottom: 8px;
}
.random-btn {
width: 100%;
height: 52px;
border: none;
border-radius: 26px;
background: linear-gradient(135deg, #1890ff 0%, #722ed1 100%);
color: #ffffff;
font-size: 16px;
font-weight: 600;
cursor: pointer;
outline: none;
position: relative;
overflow: hidden;
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
box-shadow: 0 6px 16px rgba(114, 46, 209, 0.15);
display: flex;
align-items: center;
justify-content: center;
}
.random-btn:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 8px 20px rgba(114, 46, 209, 0.25);
}
.random-btn:active:not(:disabled) {
transform: translateY(1px);
}
.random-btn.is-disabled {
background: #f5f5f5 !important;
color: #bfbfbf !important;
cursor: not-allowed !important;
box-shadow: none !important;
border: 1px solid #d9d9d9;
}
.btn-icon {
margin-right: 6px;
font-size: 17px;
}
.claim-rules {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
font-size: 11px;
color: #8c8c8c;
}
.rule-icon {
color: #1890ff;
}
.lobby-footer {
width: 100%;
text-align: center;
}
.refresh-link {
color: #8c8c8c;
font-size: 12px;
}
.refresh-link:hover {
color: #1890ff;
}
/* 右侧面板 */
.lobby-right-card {
gap: 12px;
}
.brief-card {
background: rgba(255, 255, 255, 0.8);
border: 1px solid rgba(255, 255, 255, 0.7);
border-radius: 16px;
padding: 14px 18px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.01);
}
.panel-title {
font-size: 13px;
font-weight: 700;
color: #595959;
margin-bottom: 6px;
display: flex;
align-items: center;
gap: 6px;
border-bottom: 1px dashed rgba(0, 0, 0, 0.05);
padding-bottom: 6px;
}
.title-icon {
font-size: 14px;
color: #1890ff;
}
/* 个人今日数据统计 */
.stats-grid {
display: flex;
justify-content: space-around;
align-items: center;
padding: 2px 0;
}
.stats-item {
display: flex;
flex-direction: column;
align-items: center;
}
.stats-item.clickable {
cursor: pointer;
}
.stats-item.clickable:hover .stat-num {
transform: translateY(-1px);
}
.stat-num {
font-size: 24px;
font-weight: 800;
font-family: 'Outfit', sans-serif;
line-height: 1.2;
transition: all 0.3s ease;
}
.text-blue {
color: #1890ff;
}
.text-purple {
color: #722ed1;
}
.stat-label {
font-size: 11px;
color: #8c8c8c;
display: flex;
align-items: center;
gap: 2px;
}
.arrow {
font-size: 8px;
color: #bfbfbf;
}
.stats-divider {
width: 1px;
height: 28px;
background: rgba(0, 0, 0, 0.05);
}
/* 实时动态播报 */
.broadcast-list-wrapper {
max-height: 90px;
overflow-y: auto;
}
.broadcast-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
line-height: 1.4;
padding: 4px 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.01);
}
.broadcast-item:last-child {
border-bottom: none;
}
.b-time {
color: #bfbfbf;
font-family: monospace;
}
.b-text {
color: #595959;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
}
.mini-tag {
font-size: 9px;
padding: 0 4px;
height: 15px;
line-height: 13px;
margin: 0 2px;
}
.b-slbh {
font-family: monospace;
color: #8c8c8c;
}
/* 工作须知 */
.tips-list {
padding-left: 12px;
margin: 0;
display: flex;
flex-direction: column;
gap: 3px;
}
.tips-list li {
font-size: 11px;
color: #595959;
line-height: 1.4;
}
.text-orange {
color: #fa8c16 !important;
}
/* 成功弹窗 */
.success-claim-modal :deep(.ant-modal-content) {
border-radius: 16px;
padding: 24px 20px;
}
.success-card {
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
}
.success-icon-wrapper {
width: 56px;
height: 56px;
background: rgba(82, 196, 26, 0.1);
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 12px;
}
.success-check-icon {
font-size: 36px;
color: #52c41a;
}
.success-title {
font-size: 18px;
font-weight: 700;
color: #262626;
margin-bottom: 4px;
}
.success-msg {
font-size: 12px;
color: #8c8c8c;
margin-bottom: 16px;
}
.wtcz-brief-card {
width: 100%;
background: #f8f9fc;
border: 1px solid #eef1f6;
border-radius: 12px;
padding: 12px;
text-align: left;
margin-bottom: 16px;
display: flex;
flex-direction: column;
gap: 6px;
}
.card-item {
display: flex;
align-items: center;
font-size: 12px;
line-height: 1.4;
}
.card-item.align-top {
align-items: flex-start;
}
.item-label {
color: #8c8c8c;
width: 65px;
flex-shrink: 0;
}
.item-value {
color: #262626;
font-weight: 500;
}
.item-value.highlight {
font-family: monospace;
font-weight: 700;
color: #1890ff;
font-size: 12px;
}
.item-value.summary {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
color: #595959;
}
.modal-actions {
width: 100%;
display: flex;
flex-direction: column;
gap: 10px;
}
.workbench-action-btn,
.continue-action-btn {
border-radius: 12px;
height: 38px;
font-weight: 600;
}
.continue-action-btn {
color: #595959;
border-color: #d9d9d9;
}
</style>