소스 검색

销售端新增sop任务处理详情+统计看板

cgp 1 개월 전
부모
커밋
2f64316489
4개의 변경된 파일607개의 추가작업 그리고 0개의 파일을 삭제
  1. 61 0
      src/api/qw/companyUserTask.js
  2. 17 0
      src/api/qw/sopTaskStats.js
  3. 339 0
      src/views/qw/companyUserTask/index.vue
  4. 190 0
      src/views/qw/companyUserTaskStats/index.vue

+ 61 - 0
src/api/qw/companyUserTask.js

@@ -0,0 +1,61 @@
+import request from '@/utils/request'
+
+// 查询销售处理sop任务列表
+export function listCompanyUserTask(query) {
+  return request({
+    url: '/qw/companyUserTask/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询销售处理sop任务详细
+export function getCompanyUserTask(id) {
+  return request({
+    url: '/qw/companyUserTask/' + id,
+    method: 'get'
+  })
+}
+
+// 查询解密后的用户电话
+export function getUserPhone(id) {
+  return request({
+    url: '/qw/companyUserTask/getUserPhone/' + id,
+    method: 'get'
+  })
+}
+
+// 新增销售处理sop任务
+export function addCompanyUserTask(data) {
+  return request({
+    url: '/qw/companyUserTask',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改销售处理sop任务
+export function updateCompanyUserTask(data) {
+  return request({
+    url: '/qw/companyUserTask',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除销售处理sop任务
+export function delCompanyUserTask(id) {
+  return request({
+    url: '/qw/companyUserTask/' + id,
+    method: 'delete'
+  })
+}
+
+// 导出销售处理sop任务
+export function exportCompanyUserTask(query) {
+  return request({
+    url: '/qw/companyUserTask/export',
+    method: 'get',
+    params: query
+  })
+}

+ 17 - 0
src/api/qw/sopTaskStats.js

@@ -0,0 +1,17 @@
+import request from '@/utils/request'
+
+//获取指定时间段的统计数据
+export function getTaskStatistics()
+{
+  return request({
+    url: '/qw/companyUserTaskStats/stats',
+    method: 'get'
+  })
+}
+
+export function getTaskTrendLast7Days() {
+  return request({
+    url: '/qw/companyUserTaskStats/trend/last7days',
+    method: 'get'
+  })
+}

+ 339 - 0
src/views/qw/companyUserTask/index.vue

@@ -0,0 +1,339 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="医生姓名" prop="doctorName">
+        <el-input
+          v-model="queryParams.doctorName"
+          placeholder="请输入医生姓名"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="客户姓名" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入客户姓名"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="处理状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="请选择处理状态" clearable size="small">
+          <el-option
+            v-for="dict in statusOptions"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="dict.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <div class="tab-status-filter mb10">
+      <el-radio-group v-model="queryParams.status" @change="handleStatusChange" size="small">
+        <el-radio-button label="">全部</el-radio-button>
+        <el-radio-button label="0">待处理</el-radio-button>
+        <el-radio-button label="1">已处理</el-radio-button>
+      </el-radio-group>
+    </div>
+    <el-table border v-loading="loading" :data="companyUserTaskList" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="55" align="center"/>
+      <el-table-column label="医生姓名" align="center" prop="doctorName"/>
+      <el-table-column label="客户姓名" align="center" prop="name"/>
+      <el-table-column label="客户头像" align="center" prop="avatar" width="100px">
+        <template slot-scope="scope">
+          <el-popover
+            placement="right"
+            title=""
+            trigger="hover">
+            <img slot="reference" :src="scope.row.avatar" width="60px">
+            <img :src="scope.row.avatar" style="max-width: 200px;">
+          </el-popover>
+        </template>
+      </el-table-column>
+      <el-table-column label="处理状态" align="center" prop="status">
+        <template slot-scope="scope">
+          <dict-tag :options="statusOptions" :value="scope.row.status"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="备注" align="center" prop="remark"/>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-view"
+            @click="handleView(scope.row)"
+          >详情
+          </el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-delete"
+            @click="handleDelete(scope.row)"
+            v-hasPermi="['companyUserTask:companyUserTask:remove']"
+          >删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <!-- 销售处理sop任务详情对话框 -->
+    <el-dialog :title="title" :visible.sync="open" width="600px" append-to-body>
+      <el-form :model="form" label-width="100px" size="small">
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="医生姓名">
+              <span class="detail-value">{{ form.doctorName || '—' }}</span>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="客户姓名">
+              <span class="detail-value">{{ form.name || '—' }}</span>
+            </el-form-item>
+          </el-col>
+
+          <el-col :span="12">
+            <el-form-item label="客户电话">
+              <div style="display: flex; align-items: center; gap: 8px;">
+    <span class="detail-value">
+      {{ decryptedPhone || (form.phone ? '******' : '—') }}
+    </span>
+                <el-button
+                  v-if="form.id"
+                  type="text"
+                  icon="el-icon-search"
+                  size="mini"
+                  @click="fetchDecryptedPhone(form.id)"
+                  :loading="phoneLoading"
+                  title="点击查看真实号码"
+                  style="padding: 0; margin-left: 0;"
+                ></el-button>
+              </div>
+            </el-form-item>
+          </el-col>
+
+          <el-col :span="12">
+            <el-form-item label="处理状态">
+              <dict-tag :options="statusOptions" :value="form.status"/>
+            </el-form-item>
+          </el-col>
+
+          <el-col :span="24">
+            <el-form-item label="备注">
+              <span class="detail-value">{{ form.remark || '—' }}</span>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="cancel">关 闭</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import {
+  listCompanyUserTask,
+  getCompanyUserTask,
+  getUserPhone,
+  delCompanyUserTask,
+  updateCompanyUserTask,
+} from "@/api/qw/companyUserTask";
+
+export default {
+  name: "CompanyUserTask",
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 选中数组
+      ids: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 销售处理sop任务表格数据
+      companyUserTaskList: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 处理状态字典
+      statusOptions: [],
+      // 用于存储解密后的电话
+      decryptedPhone: '',
+      // 电话加载状态
+      phoneLoading: false,
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        externalId: null,
+        doctorId: null,
+        doctorName: null,
+        userId: null,
+        name: null,
+        status: null,
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {},
+
+    };
+  },
+  created() {
+    this.getList();
+    this.getDicts("sop_task_status").then(response => {
+      this.statusOptions = response.data;
+    });
+  },
+  methods: {
+    /** 查询销售处理sop任务列表 */
+    getList() {
+      this.loading = true;
+      listCompanyUserTask(this.queryParams).then(response => {
+        this.companyUserTaskList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        id: null,
+        externalId: null,
+        doctorId: null,
+        userId: null,
+        status: null,
+        remark: null
+      };
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id)
+      this.single = selection.length !== 1
+      this.multiple = !selection.length
+    },
+
+    /** 详情按钮操作 */
+    handleView(row) {
+      const id = row.id || this.ids;
+      getCompanyUserTask(id).then(response => {
+        this.form = response.data || {}; // 安全兜底
+        this.decryptedPhone = '';        // 清空上次解密的电话
+        this.open = true;
+        this.title = "销售处理SOP任务详情";
+      });
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          if (this.form.id != null) {
+            updateCompanyUserTask(this.form).then(response => {
+              this.msgSuccess("修改成功");
+              this.open = false;
+              this.getList();
+            });
+          } else {
+            addCompanyUserTask(this.form).then(response => {
+              this.msgSuccess("新增成功");
+              this.open = false;
+              this.getList();
+            });
+          }
+        }
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const ids = row.id || this.ids;
+      this.$confirm('是否确认删除销售处理sop任务编号为"' + ids + '"的数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function () {
+        return delCompanyUserTask(ids);
+      }).then(() => {
+        this.getList();
+        this.msgSuccess("删除成功");
+      }).catch(() => {
+      });
+    },
+    /** 切换状态Tab */
+    handleStatusChange() {
+      this.queryParams.pageNum = 1; // 切换时重置页码
+      this.getList();
+    },
+    /** 获取并显示解密后的电话 */
+    async fetchDecryptedPhone(id) {
+      if (!id) return;
+      this.phoneLoading = true;
+      try {
+        const response = await getUserPhone(id);
+        this.decryptedPhone = response.userPhone || '—';
+      } catch (error) {
+        this.$message.error('获取电话失败');
+        this.decryptedPhone = '获取失败';
+      } finally {
+        this.phoneLoading = false;
+      }
+    },
+  }
+};
+</script>
+
+<style scoped>
+.mb10 {
+  margin-bottom: 10px;
+}
+
+.tab-status-filter {
+  margin-bottom: 10px;
+}
+
+.detail-value {
+  font-weight: 500;
+  color: #606266;
+}
+</style>

+ 190 - 0
src/views/qw/companyUserTaskStats/index.vue

@@ -0,0 +1,190 @@
+<template>
+  <div class="dashboard-container">
+    <el-card class="box-card" shadow="never">
+      <div slot="header" class="clearfix">
+        <span>sop任务处理统计看板</span>
+      </div>
+
+      <!-- 三个时间维度并列 -->
+      <el-row :gutter="20">
+        <el-col :span="8">
+          <div class="stat-card">
+            <h4>今日任务</h4>
+            <p class="total">总计:<strong>{{ stats.day.processed + stats.day.pending }}</strong></p>
+            <div ref="chartDay" style="width:100%; height:200px;"></div>
+            <div class="stat-numbers">
+              <p>已处理:<span class="success">{{ stats.day.processed }}</span></p>
+              <p>待处理:<span class="warning">{{ stats.day.pending }}</span></p>
+            </div>
+          </div>
+        </el-col>
+
+        <el-col :span="8">
+          <div class="stat-card">
+            <h4>本周任务</h4>
+            <p class="total">总计:<strong>{{ stats.week.processed + stats.week.pending }}</strong></p>
+            <div ref="chartWeek" style="width:100%; height:200px;"></div>
+            <div class="stat-numbers">
+              <p>已处理:<span class="success">{{ stats.week.processed }}</span></p>
+              <p>待处理:<span class="warning">{{ stats.week.pending }}</span></p>
+            </div>
+          </div>
+        </el-col>
+
+        <el-col :span="8">
+          <div class="stat-card">
+            <h4>本月任务</h4>
+            <p class="total">总计:<strong>{{ stats.week.processed + stats.week.pending }}</strong></p>
+            <div ref="chartMonth" style="width:100%; height:200px;"></div>
+            <div class="stat-numbers">
+              <p>已处理:<span class="success">{{ stats.month.processed }}</span></p>
+              <p>待处理:<span class="warning">{{ stats.month.pending }}</span></p>
+            </div>
+          </div>
+        </el-col>
+      </el-row>
+      <!-- 趋势图 -->
+      <el-row style="margin-top: 20px;">
+        <el-col :span="24">
+          <el-card shadow="never">
+            <div ref="chartTrend" style="width:100%; height:300px;"></div>
+          </el-card>
+        </el-col>
+      </el-row>
+    </el-card>
+  </div>
+</template>
+<script>
+
+import * as echarts from 'echarts';
+import {getTaskStatistics,getTaskTrendLast7Days} from "@/api/qw/sopTaskStats";
+
+export default {
+  name: 'TaskStatistics',
+  data() {
+    return {
+      stats: {
+        day: { processed: 0, pending: 0 },
+        week: { processed: 0, pending: 0 },
+        month: { processed: 0, pending: 0 }
+      },
+      //最近7天趋势数据
+      trendData: {
+        dates: [],     // ['12-18', '12-19', ..., '12-24']
+        totals: []     // [25, 30, 28, ...]
+      }
+    };
+  },
+  mounted() {
+    this.fetchStats();
+  },
+  methods: {
+    async fetchStats() {
+      const res = await getTaskStatistics();
+      this.stats = res.data;
+      const trendRes = await getTaskTrendLast7Days();
+      this.trendData = trendRes.data;
+
+      this.$nextTick(() => {
+        this.initCharts();
+        this.initTrendChart(); // 初始化趋势图
+      });
+    },
+
+    initCharts() {
+      this.initChart(this.$refs.chartDay, this.stats.day);
+      this.initChart(this.$refs.chartWeek, this.stats.week);
+      this.initChart(this.$refs.chartMonth, this.stats.month);
+    },
+    initChart(dom, data) {
+      const chart = echarts.init(dom);
+      chart.setOption({
+        animation: false,
+        tooltip: { trigger: 'item' },
+        series: [{
+          type: 'pie',
+          radius: ['40%', '70%'],
+          avoidLabelOverlap: false,
+          label: { show: false },
+          emphasis: {
+            scale: true,        // 选中时放大(正常)
+            scaleSize: 10,      // 放大程度
+            label: {
+              show: true,
+              fontSize: '16',
+              fontWeight: 'bold'
+            }
+          },
+          // 禁用 hover 时的动画效果
+          hoverAnimation: false,
+          data: [
+            { value: data.processed, name: '已处理', itemStyle: { color: '#409EFF' } },
+            { value: data.pending, name: '待处理', itemStyle: { color: '#C0C4CC' } }
+          ]
+        }]
+      });
+    },
+    initTrendChart() {
+      const chart = echarts.init(this.$refs.chartTrend);
+      chart.setOption({
+        tooltip: { trigger: 'axis' },
+        xAxis: {
+          type: 'category',
+          data: this.trendData.dates
+        },
+        yAxis: {
+          type: 'value'
+        },
+        series: [{
+          data: this.trendData.totals,
+          type: 'line',
+          smooth: true,
+          symbol: 'circle',
+          symbolSize: 6,
+          lineStyle: { width: 3, color: '#409EFF' },
+          itemStyle: { color: '#409EFF' },
+          areaStyle: {
+            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+              { offset: 0, color: 'rgba(64, 158, 255, 0.3)' },
+              { offset: 1, color: 'rgba(64, 158, 255, 0.0)' }
+            ])
+          }
+        }]
+      });
+    }
+  },
+  beforeDestroy() {
+    // 销毁图表避免内存泄漏
+    [this.$refs.chartDay, this.$refs.chartWeek, this.$refs.chartMonth, this.$refs.chartTrend].forEach(ref => {
+      if (ref) echarts.getInstanceByDom(ref)?.dispose();
+    });
+  }
+};
+</script>
+
+<style scoped>
+.stat-card {
+  text-align: center;
+}
+.stat-card h4 {
+  margin-bottom: 12px;
+  color: #555;
+}
+.stat-numbers p {
+  margin: 6px 0;
+  font-size: 14px;
+}
+
+.total {
+  margin: 8px 0;
+  font-size: 14px;
+  color: #909399;
+}
+.total strong {
+  color: #303133;
+  font-weight: bold;
+}
+.success { color: #409EFF; font-weight: bold; }
+.warning { color: #C0C4CC; font-weight: bold; }
+</style>
+