Browse Source

民意首页处理

main
wang 1 month ago
parent
commit
1ba30c640a
  1. 40
      smart-flow-web/src/router/flow/flow.js
  2. 322
      smart-flow-web/src/views/jwpy/mydc/my-survey/dashboard.vue
  3. 48
      smart-flow-web/src/views/jwpy/mydc/my-survey/index.vue

40
smart-flow-web/src/router/flow/flow.js

@ -15,35 +15,53 @@ export const flowRouters = [
] ]
}, },
{ {
path: '/mydc', path: '/jwpy',
component: SmartLayout, component: SmartLayout,
children: [ children: [
{ {
path: 'questionnaire', path: 'home/dashboard',
component: () => import('/@/views/jwpy/mydc/my-survey/dashboard.vue'),
name: 'SurveyDashboard',
meta: { title: '首页' }
},
{
path: 'flow/task-pool',
component: () => import('/@/views/jwpy/jwpy/flow/task-pool.vue'),
name: 'SurveyTaskPool',
meta: { title: '任务大厅' }
},
{
path: 'home/todo',
component: () => import('/@/views/jwpy/mydc/my-survey/index.vue'),
name: 'SurveyTodo',
meta: { title: '待呼任务' }
},
{
path: 'home/draft',
component: () => import('/@/views/jwpy/mydc/my-survey/index.vue'),
name: 'SurveyDraft',
meta: { title: '暂存任务' }
},
{
path: 'mydc/questionnaire',
component: () => import('/@/views/jwpy/mydc/questionnaire/index.vue'), component: () => import('/@/views/jwpy/mydc/questionnaire/index.vue'),
name: 'QuestionnaireList', name: 'QuestionnaireList',
meta: { title: '问卷设计与管理' } meta: { title: '问卷设计与管理' }
}, },
{ {
path: 'questionnaire-designer', path: 'mydc/questionnaire-designer',
component: () => import('/@/views/jwpy/mydc/questionnaire/designer.vue'), component: () => import('/@/views/jwpy/mydc/questionnaire/designer.vue'),
name: 'QuestionnaireDesigner', name: 'QuestionnaireDesigner',
meta: { title: '问卷设计器' } meta: { title: '问卷设计器' }
}, },
{ {
path: 'my-survey', path: 'mydc/sample',
component: () => import('/@/views/jwpy/mydc/my-survey/index.vue'),
name: 'MySurvey',
meta: { title: '我的调查任务' }
},
{
path: 'sample',
component: () => import('/@/views/jwpy/mydc/sample/index.vue'), component: () => import('/@/views/jwpy/mydc/sample/index.vue'),
name: 'SurveySample', name: 'SurveySample',
meta: { title: '调查样本库管理' } meta: { title: '调查样本库管理' }
}, },
{ {
path: 'web-opinion', path: 'mydc/web-opinion',
component: () => import('/@/views/jwpy/mydc/web-opinion/index.vue'), component: () => import('/@/views/jwpy/mydc/web-opinion/index.vue'),
name: 'WebOpinion', name: 'WebOpinion',
meta: { title: '网络民意监测' } meta: { title: '网络民意监测' }

322
smart-flow-web/src/views/jwpy/mydc/my-survey/dashboard.vue

@ -0,0 +1,322 @@
<template>
<div class="dashboard-container">
<!-- 顶部数据看板卡片 -->
<a-row :gutter="16" style="margin-bottom: 20px">
<a-col :span="6" v-for="item in stats" :key="item.title">
<a-card :bordered="false" class="premium-stat-card">
<div class="stat-content">
<div class="icon-wrap" :style="{ background: item.bgColor, color: item.color }">
<component :is="item.icon" />
</div>
<div class="stat-text">
<div class="stat-label">{{ item.title }}</div>
<div class="stat-value">
{{ item.value }}<span class="stat-unit">{{ item.unit }}</span>
</div>
</div>
</div>
<div class="stat-footer">
<span class="trend-label">较昨日</span>
<span :class="['trend-value', item.trendType]">
{{ item.trend }}
</span>
</div>
</a-card>
</a-col>
</a-row>
<!-- 中间图表 -->
<a-row :gutter="16" style="margin-bottom: 20px">
<a-col :span="14">
<a-card title="7天呼叫接通与回访趋势" :bordered="false" class="chart-card">
<div id="trend-chart" class="chart-dom"></div>
</a-card>
</a-col>
<a-col :span="10">
<a-card title="民意测评样本来源占比" :bordered="false" class="chart-card">
<div id="source-chart" class="chart-dom"></div>
</a-card>
</a-col>
</a-row>
<!-- 下方看板信息预警词云与最新满意反馈 -->
<a-row :gutter="16">
<a-col :span="12">
<a-card title="敏感诉求热词预警" :bordered="false" class="info-card">
<div class="word-cloud-container">
<span v-for="word in wordCloud" :key="word.name"
:style="{ fontSize: word.size + 'px', color: word.color }"
class="word-item">
{{ word.name }}
</span>
</div>
</a-card>
</a-col>
<a-col :span="12">
<a-card title="最新不满意回访警示" :bordered="false" class="info-card">
<a-list item-layout="horizontal" :data-source="recentComplaints">
<template #renderItem="{ item }">
<a-list-item>
<a-list-item-meta :description="`联系电话:${item.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')} | 来源: ${item.source}`">
<template #title>
<div class="complaint-title-row">
<span class="complaint-desc">{{ item.reason }}</span>
<a-tag color="error">待处理</a-tag>
</div>
</template>
<template #avatar>
<a-avatar style="background-color: #f5222d"></a-avatar>
</template>
</a-list-item-meta>
</a-list-item>
</template>
</a-list>
</a-card>
</a-col>
</a-row>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import {
PhoneOutlined,
CheckCircleOutlined,
MehOutlined,
SmileOutlined
} from '@ant-design/icons-vue';
import * as echarts from 'echarts';
const stats = ref([
{ title: '累计拨打任务', value: '1,429', unit: '次', trend: '+12.5%', trendType: 'up', icon: PhoneOutlined, color: '#1890ff', bgColor: 'rgba(24, 144, 255, 0.1)' },
{ title: '成功联络样本', value: '982', unit: '个', trend: '+8.3%', trendType: 'up', icon: CheckCircleOutlined, color: '#52c41a', bgColor: 'rgba(82, 196, 26, 0.1)' },
{ title: '回访成功率', value: '68.7', unit: '%', trend: '-1.2%', trendType: 'down', icon: MehOutlined, color: '#fa8c16', bgColor: 'rgba(250, 140, 22, 0.1)' },
{ title: '群众满意率', value: '94.2', unit: '%', trend: '+0.5%', trendType: 'up', icon: SmileOutlined, color: '#722ed1', bgColor: 'rgba(114, 46, 209, 0.1)' }
]);
const wordCloud = ref([
{ name: '出警慢', size: 28, color: '#f5222d' },
{ name: '态度冷淡', size: 24, color: '#fa541c' },
{ name: '排队拥挤', size: 22, color: '#fa8c16' },
{ name: '电话难打', size: 20, color: '#fadb14' },
{ name: '流程繁琐', size: 18, color: '#13c2c2' },
{ name: '未收到通知', size: 16, color: '#52c41a' },
{ name: '系统卡顿', size: 14, color: '#2f54eb' },
{ name: '答复敷衍', size: 13, color: '#722ed1' }
]);
const recentComplaints = ref([
{ phone: '13899990001', source: '110接处警', reason: '报警后20分钟才到现场,且民警现场言语生硬。' },
{ phone: '13988880002', source: '户政窗口', reason: '因系统故障导致排队2小时没办成,告知不清晰。' },
{ phone: '13566660003', source: '案件办理', reason: '被盗案件立案后一直没有收到进展通知,多次回拨未果。' }
]);
let trendChart = null;
let sourceChart = null;
const initCharts = () => {
//
const trendDom = document.getElementById('trend-chart');
if (trendDom) {
trendChart = echarts.init(trendDom);
trendChart.setOption({
tooltip: { trigger: 'axis' },
legend: { data: ['外呼任务数', '成功回访数'] },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: {
type: 'category',
boundaryGap: false,
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
},
yAxis: { type: 'value' },
series: [
{
name: '外呼任务数',
type: 'line',
smooth: true,
data: [120, 132, 101, 134, 90, 230, 210],
itemStyle: { color: '#1890ff' }
},
{
name: '成功回访数',
type: 'line',
smooth: true,
data: [80, 95, 78, 98, 65, 170, 150],
itemStyle: { color: '#52c41a' }
}
]
});
}
//
const sourceDom = document.getElementById('source-chart');
if (sourceDom) {
sourceChart = echarts.init(sourceDom);
sourceChart.setOption({
tooltip: { trigger: 'item' },
legend: { bottom: '0', left: 'center' },
series: [
{
name: '样本来源',
type: 'pie',
radius: ['45%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 8,
borderColor: '#fff',
borderWidth: 2
},
label: { show: false },
data: [
{ value: 450, name: '110接处警', itemStyle: { color: '#f5222d' } },
{ value: 280, name: '户政窗口', itemStyle: { color: '#1890ff' } },
{ value: 150, name: '出入境', itemStyle: { color: '#722ed1' } },
{ value: 120, name: '车驾管', itemStyle: { color: '#52c41a' } },
{ value: 90, name: '案件办理', itemStyle: { color: '#fa8c16' } }
]
}
]
});
}
};
const handleResize = () => {
trendChart && trendChart.resize();
sourceChart && sourceChart.resize();
};
onMounted(() => {
initCharts();
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
});
</script>
<style scoped>
.dashboard-container {
padding: 1px 0;
}
.premium-stat-card {
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.03);
transition: transform 0.3s ease, box-shadow 0.3s ease;
overflow: hidden;
border: 1px solid #f0f0f0;
}
.premium-stat-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.08);
}
.stat-content {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.icon-wrap {
width: 46px;
height: 46px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
margin-right: 16px;
}
.stat-text {
display: flex;
flex-direction: column;
}
.stat-label {
font-size: 13px;
color: #8c8c8c;
margin-bottom: 2px;
}
.stat-value {
font-size: 24px;
font-weight: 700;
color: #262626;
line-height: 1;
}
.stat-unit {
font-size: 12px;
color: #8c8c8c;
font-weight: normal;
margin-left: 2px;
}
.stat-footer {
font-size: 12px;
color: #bfbfbf;
display: flex;
align-items: center;
gap: 6px;
border-top: 1px dashed #f0f0f0;
padding-top: 10px;
}
.trend-value.up {
color: #52c41a;
font-weight: 600;
}
.trend-value.down {
color: #ff4d4f;
font-weight: 600;
}
.chart-card, .info-card {
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.03);
border: 1px solid #f0f0f0;
}
.chart-dom {
width: 100%;
height: 280px;
}
.word-cloud-container {
height: 240px;
display: flex;
flex-wrap: wrap;
align-content: center;
justify-content: center;
gap: 16px;
padding: 10px;
background: #fafafa;
border-radius: 8px;
}
.word-item {
font-weight: 600;
cursor: pointer;
transition: transform 0.2s ease;
}
.word-item:hover {
transform: scale(1.15);
}
.complaint-title-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.complaint-desc {
font-size: 14px;
font-weight: 600;
color: #262626;
}
</style>

48
smart-flow-web/src/views/jwpy/mydc/my-survey/index.vue

@ -7,7 +7,7 @@
<template> <template>
<div class="page-container"> <div class="page-container">
<!-- 今日任务概览卡片 --> <!-- 今日任务概览卡片 -->
<a-row :gutter="16" style="margin-bottom: 16px"> <a-row :gutter="16" style="margin-bottom: 16px" v-if="isTodoView">
<a-col :span="6"> <a-col :span="6">
<a-card :bordered="false" class="status-card-simple"> <a-card :bordered="false" class="status-card-simple">
<div class="card-label">今日待回访样本</div> <div class="card-label">今日待回访样本</div>
@ -168,12 +168,16 @@
</template> </template>
<script setup> <script setup>
import { ref, reactive, onMounted } from 'vue'; import { ref, reactive, onMounted, watch } from 'vue';
import { useRoute } from 'vue-router';
import { message, Modal } from 'ant-design-vue'; import { message, Modal } from 'ant-design-vue';
import { PhoneOutlined, FormOutlined } from '@ant-design/icons-vue'; import { PhoneOutlined, FormOutlined } from '@ant-design/icons-vue';
import DictLabel from '/@/components/support/dict-label/index.vue'; import DictLabel from '/@/components/support/dict-label/index.vue';
import { DICT_CODE_ENUM } from '/@/constants/support/dict-const'; import { DICT_CODE_ENUM } from '/@/constants/support/dict-const';
const route = useRoute();
const isTodoView = ref(true);
const queryForm = reactive({ const queryForm = reactive({
name: '', name: '',
status: undefined status: undefined
@ -210,21 +214,45 @@ const mockTasks = [
]; ];
const loadTasks = () => { const loadTasks = () => {
const path = route.path;
isTodoView.value = !path.endsWith('/draft');
const localTasks = localStorage.getItem('local_survey_tasks'); const localTasks = localStorage.getItem('local_survey_tasks');
let list = [];
if (!localTasks) { if (!localTasks) {
localStorage.setItem('local_survey_tasks', JSON.stringify(mockTasks)); localStorage.setItem('local_survey_tasks', JSON.stringify(mockTasks));
tableData.value = mockTasks; list = mockTasks;
} else { } else {
const list = JSON.parse(localTasks); list = JSON.parse(localTasks);
tableData.value = list.filter(item => {
let matched = true;
if (queryForm.name && !item.name.includes(queryForm.name)) matched = false;
if (queryForm.status && item.status.toString() !== queryForm.status) matched = false;
return matched;
});
} }
tableData.value = list.filter(item => {
let matched = true;
if (queryForm.name && !item.name.includes(queryForm.name)) matched = false;
if (queryForm.status !== undefined && queryForm.status !== '') {
if (item.status.toString() !== queryForm.status) matched = false;
} else {
//
if (isTodoView.value) {
// (0) / (2)
if (item.status !== 0 && item.status !== 2) matched = false;
} else {
// / (2) / 访 (3)
if (item.status !== 2 && item.status !== 3) matched = false;
}
}
return matched;
});
}; };
watch(
() => route.path,
() => {
loadTasks();
}
);
const getBizTypeColor = (bizType) => { const getBizTypeColor = (bizType) => {
const map = { '110': 'red', 'hz': 'blue', 'crj': 'purple', 'cjg': 'green', 'ajbl': 'orange' }; const map = { '110': 'red', 'hz': 'blue', 'crj': 'purple', 'cjg': 'green', 'ajbl': 'orange' };
return map[bizType] || 'cyan'; return map[bizType] || 'cyan';

Loading…
Cancel
Save