Procházet zdrojové kódy

add 客户管理页面

haveyou před 1 rokem
rodič
revize
78294df942

+ 3 - 2
src/components/CommonPage/CommonTable.vue

@@ -1,5 +1,5 @@
 <template>
-  <ele-card :body-style="{ paddingTop: '8px' }" flex-table>
+  <ele-card :body-style="{ paddingTop: '8px',...bodyStyle }" flex-table>
     <!-- 表格 -->
     <ele-pro-table
       ref="tableRef"
@@ -41,7 +41,8 @@
     },
     pageUrl: { type: String, default: '/system/post/list' },
     exportUrl: { type: String, default: '/system/post/export' },
-    columns: { type: Array, default: () => [] }
+    columns: { type: Array, default: () => [] },
+    bodyStyle: { type: Object, default: () => ({}) }
   });
   let { proxy } = getCurrentInstance();
 

+ 82 - 0
src/components/DateSearch/index.vue

@@ -0,0 +1,82 @@
+<template>
+  <div class="flex items-center">
+    <div class="quick-select">
+      <el-check-tag
+        class="ml-3"
+        :checked="activeValue == item.value"
+        @change="onChange(item)"
+        v-for="item in checkList"
+        >{{ item.label }}</el-check-tag
+      >
+    </div>
+
+    <div class="ml-3">
+      <el-date-picker
+        :style="{ width: dateType == 'daterange' ? '260px' : '360px' }"
+        style="max-width: 360px; margin-right: 16px"
+        v-model="dataRange"
+        :type="dateType"
+        range-separator="-"
+        start-placeholder="开始时间"
+        end-placeholder="结束时间"
+        @change="handleDateChange"
+      />
+      <el-button style="width: 80px" type="primary" @click="search"
+        >查询</el-button
+      >
+      <el-button style="width: 80px" type="info" @click="reset">重置</el-button>
+    </div>
+  </div>
+</template>
+
+<script setup>
+  const props = defineProps({
+    startKey: { type: String, default: 'startTime' },
+    endKey: { type: String, default: 'endTime' },
+    formatterKey: { type: String, default: 'YYYY-MM-DD' },
+    dateType: { type: String, default: 'daterange' }
+  });
+
+  import { dayjs } from 'element-plus';
+
+  const activeValue = ref(0);
+  let checkList = reactive([
+    { label: '7日', value: 1, number: 7 },
+    { label: '15日', value: 2, number: 15 },
+    { label: '30日', value: 3, number: 30 }
+  ]);
+
+  const emit = defineEmits(['search']);
+  function onChange(item) {
+    activeValue.value = item.value;
+    dataRange.value = getLastDays(item.number);
+  }
+
+  function getLastDays(number = 7) {
+    const endDate = dayjs();
+    const startDate = endDate.subtract(number - 1, 'day');
+    return [
+      startDate.format(props.formatterKey),
+      endDate.format(props.formatterKey)
+    ];
+  }
+
+  const dataRange = ref([]);
+
+  function search() {
+    let date = {};
+    date[props.startKey] = dataRange.value[0] || '';
+    date[props.endKey] = dataRange.value[1] || '';
+    emit('search', date);
+  }
+
+  function reset() {
+    dataRange.value = [];
+    activeValue.value = 0;
+    emit('search', {});
+  }
+
+  function handleDateChange(value) {
+    activeValue.value = 0;
+  }
+</script>

+ 52 - 0
src/views/customer/blackMobile/components/page-edit.vue

@@ -0,0 +1,52 @@
+<!-- 搜索表单 -->
+<template>
+  <simple-form-modal
+    title="手机号黑名单"
+    :items="formItems"
+    ref="editRef"
+    :baseUrl="baseUrl"
+    @success="(data) => emit('success', data)"
+  ></simple-form-modal>
+</template>
+
+<script setup>
+  import { reactive, ref, defineEmits, getCurrentInstance } from 'vue';
+  import { useFormData } from '@/utils/use-form-data';
+  import SimpleFormModal from '@/components/CommonPage/SimpleFormModal.vue';
+  const { proxy } = getCurrentInstance();
+
+  const emit = defineEmits(['success']);
+
+  const formItems = computed(() => {
+    return [
+      {
+        type: 'input',
+        label: '手机号',
+        prop: 'mobile',
+        required: true,
+        rules: [{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号' }]
+      },
+      {
+        type: 'textarea',
+        label: '拉黑原因',
+        prop: 'reason',
+        required: true
+      }
+    ];
+  });
+  //默认值
+  const baseUrl = reactive({
+    add: '/baseinfo/schoolInfo/save',
+    update: '/baseinfo/schoolInfo/edit'
+  });
+  const formData = ref({ schoolTag: '1' });
+
+  const editRef = ref(null);
+
+  function handleOpen(data = {}) {
+    formData.value = Object.assign(formData.value, data || {});
+    editRef.value?.handleOpen(formData.value);
+  }
+
+  defineExpose({ handleOpen });
+</script>

+ 114 - 0
src/views/customer/blackMobile/index.vue

@@ -0,0 +1,114 @@
+<template>
+  <ele-page flex-table>
+    <common-table
+      ref="pageRef"
+      :pageConfig="pageConfig"
+      :columns="columns"
+      :tools="false"
+    >
+      <template #toolbar>
+        <div class="flex justify-between items-center">
+          <el-button
+            type="primary"
+            plain
+            v-permission="'customer:blackMobile:add'"
+            @click="handleUpdate()"
+          >
+            新增
+          </el-button>
+
+          <div class="flex items-center">
+            <el-input v-model="queryParams.mobile" class="mr-6" placeholder="请输入手机号"></el-input>
+            <el-button
+              style="width: 80px"
+              type="primary"
+              @click="reload(queryParams)"
+              >查询</el-button
+            >
+            <el-button
+              style="width: 80px"
+              type="info"
+              @click="queryParams.mobile = ''"
+              >重置</el-button
+            >
+          </div>
+        </div>
+      </template>
+
+      <template #action="{ row }">
+        <div>
+          <el-button
+            type="primary"
+            link
+            v-permission="'customer:blackMobile:detail'"
+            @click="handleUpdate(row)"
+          >
+            [编辑]
+          </el-button>
+          <el-button
+            type="danger"
+            link
+            v-permission="'customer:blackMobile:delete'"
+            @click="handleDelete(row)"
+          >
+            [删除]
+          </el-button>
+        </div>
+      </template>
+    </common-table>
+    <page-edit ref="pageEditRef" @success="reload()" />
+  </ele-page>
+</template>
+
+<script setup>
+  import { ref, reactive } from 'vue';
+  import CommonTable from '@/components/CommonPage/CommonTable.vue';
+  import pageEdit from './components/page-edit.vue';
+
+  defineOptions({ name: 'blackMobileList' });
+
+  const queryParams = reactive({ mobile: '' });
+  /** 表格列配置 */
+  const columns = ref([
+    { label: '拉黑号码', prop: 'phoneNum', align: 'center' },
+    { label: '拉黑时间', prop: 'createTime', align: 'center', width: 200 },
+    { label: '操作员', prop: 'contactsName', align: 'center' },
+    { label: '拉黑原因', prop: 'addressDetail', align: 'center', minWidth: 160 },
+    {
+      columnKey: 'action',
+      label: '操作',
+      width: 220,
+      align: 'center',
+      slot: 'action'
+    }
+  ]);
+
+  /** 页面组件实例 */
+  const pageRef = ref(null);
+
+  const pageConfig = reactive({
+    pageUrl: '/baseinfo/godown/pagelist',
+    exportUrl: '/baseinfo/godown/export',
+    fileName: '黑名单手机号',
+    cacheKey: 'blackMobileTable'
+  });
+
+  //刷新表格
+  function reload(where) {
+    pageRef.value?.reload(where);
+  }
+
+  //删除
+  function handleDelete(row) {
+    pageRef.value?.messageBoxConfirm({
+      message: '确认删除?',
+      fetch: () => request.post('/baseinfo/godown/delete', { id: row.id })
+    });
+  }
+
+  //编辑
+  const pageEditRef = ref(null);
+  function handleUpdate(row) {
+    pageEditRef.value?.handleOpen(row);
+  }
+</script>

+ 70 - 0
src/views/customer/blacklist/components/page-edit.vue

@@ -0,0 +1,70 @@
+<!-- 搜索表单 -->
+<template>
+  <simple-form-modal
+    :title="title"
+    :items="formItems"
+    ref="editRef"
+    :baseUrl="baseUrl"
+    label-width="120px"
+    @success="(data) => emit('success', data)"
+  ></simple-form-modal>
+</template>
+
+<script setup>
+  import { reactive, ref, defineEmits, getCurrentInstance } from 'vue';
+  import { useFormData } from '@/utils/use-form-data';
+  import SimpleFormModal from '@/components/CommonPage/SimpleFormModal.vue';
+  const { proxy } = getCurrentInstance();
+
+  const emit = defineEmits(['success']);
+
+  const formItems = computed(() => {
+    return [
+      {
+        type: 'input',
+        label: '用户名',
+        prop: 'username',
+        required: true
+      },
+      {
+        type: 'input',
+        label: '联系方式',
+        prop: 'mobile',
+        required: true,
+        rules: [{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号' }]
+      },
+      {
+        type: 'dictSelect',
+        label: '加入黑名单原因',
+        prop: 'reason',
+        required: true,
+        props: {
+          code: 'school_tag'
+        }
+      },
+      {
+        type: 'textarea',
+        label: '备注',
+        prop: 'reason',
+        required: true
+      }
+    ];
+  });
+  //默认值
+  const baseUrl = reactive({
+    add: '/baseinfo/schoolInfo/save',
+    update: '/baseinfo/schoolInfo/edit'
+  });
+  const formData = ref({ schoolTag: '1' });
+
+  const editRef = ref(null);
+
+  const title = ref('添加黑名单');
+  function handleOpen(data = {}) {
+    title.value = data.id ? '编辑黑名单' : '添加黑名单';
+    formData.value = Object.assign(formData.value, data || {});
+    editRef.value?.handleOpen(formData.value);
+  }
+
+  defineExpose({ handleOpen });
+</script>

+ 39 - 0
src/views/customer/blacklist/components/page-search.vue

@@ -0,0 +1,39 @@
+<!-- 搜索表单 -->
+<template>
+  <ele-card :body-style="{ paddingBottom: '8px' }">
+    <ProSearch
+      :items="formItems"
+      ref="searchRef"
+      @search="search"
+      :initKeys="initKeys"
+    ></ProSearch>
+  </ele-card>
+</template>
+
+<script setup>
+  import { reactive, ref, defineEmits } from 'vue';
+  import ProSearch from '@/components/CommonPage/ProSearch2.vue';
+
+  const emit = defineEmits(['search']);
+
+  const formItems = computed(() => {
+    return [
+      { type: 'input', label: '客户昵称/uid', prop: 'name' },
+      { type: 'input', label: '联系方式', prop: 'phone' },
+    ];
+  });
+
+  const initKeys = reactive({
+    name: '',
+    nickName: '',
+    openid: '',
+    phone: '',
+    status: ''
+  });
+
+  const searchRef = ref(null);
+  /** 搜索 */
+  const search = (data) => {
+    emit('search', { ...data });
+  };
+</script>

+ 221 - 0
src/views/customer/blacklist/index.vue

@@ -0,0 +1,221 @@
+<template>
+  <ele-page flex-table>
+    <page-search @search="reload"></page-search>
+
+    <common-table ref="pageRef" :pageConfig="pageConfig" :columns="columns">
+      <template #toolbar>
+        <el-button
+          type="primary"
+          v-permission="'customer:blacklist:add'"
+          @click="handleUpdate()"
+          :icon="PlusOutlined"
+        >
+          添加黑名单
+        </el-button>
+        <el-button
+          type="success"
+          plain
+          v-permission="'customer:blacklist:export'"
+          @click="handleExportExcel"
+          :icon="DownloadOutlined"
+        >
+          导出EXCEL
+        </el-button>
+      </template>
+
+      <template #picture="{ row }">
+        <div class="flex flex-col">
+          <el-avatar
+            src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"
+          />
+          <el-text style="text-align: center">{{ row.schoolName }}</el-text>
+        </div>
+      </template>
+
+      <template #schoolTag="{ row }">
+        <dict-data code="school_tag" type="tag" :model-value="row.schoolTag" />
+      </template>
+
+      <template #action="{ row }">
+        <div>
+          <el-button
+            type="success"
+            link
+            v-permission="'customer:blacklist:detail'"
+            @click="handleDetail(row)"
+          >
+            [用户详情]
+          </el-button>
+          <el-button
+            type="warning"
+            link
+            v-permission="'customer:blacklist:userTag'"
+            @click="handleUpdate(row)"
+          >
+            [用户标签]
+          </el-button>
+          <el-button
+            type="warning"
+            link
+            v-permission="'customer:blacklist:accountDetail'"
+            @click="handleUpdate(row)"
+          >
+            [账户明细]
+          </el-button>
+          <el-button
+            type="success"
+            link
+            v-permission="'customer:blacklist:history'"
+            @click="handleBlacklistHistory(row)"
+          >
+            [拉黑历史]
+          </el-button>
+          <el-button
+            type="danger"
+            link
+            v-permission="'customer:blacklist:remove'"
+            @click="handleAddBlacklist(row)"
+          >
+            [移出黑名单]
+          </el-button>
+        </div>
+      </template>
+    </common-table>
+
+    <page-edit ref="editRef" @success="reload()"></page-edit>
+    <blacklist-history ref="historyRef"></blacklist-history>
+    <customer-detail ref="detailRef"></customer-detail>
+  </ele-page>
+</template>
+
+<script setup>
+  import { ref, reactive } from 'vue';
+  import { ElMessageBox } from 'element-plus/es';
+  import { EleMessage } from 'ele-admin-plus/es';
+  import { DownloadOutlined, PlusOutlined } from '@/components/icons';
+  import CommonTable from '@/components/CommonPage/CommonTable.vue';
+  import pageSearch from './components/page-search.vue';
+  import pageEdit from './components/page-edit.vue'
+  import blacklistHistory from '@/views/customer/list/components/blacklist-history.vue';
+  import customerDetail from '@/views/customer/list/components/customer-detail.vue';
+  import { useDictData } from '@/utils/use-dict-data';
+  import request from '@/utils/request';
+
+  defineOptions({ name: 'customerList' });
+  const [schoolLevelDicts, schoolTagDicts] = useDictData([
+    'school_level',
+    'school_tag'
+  ]);
+
+  /** 表格列配置 */
+  const columns = ref([
+    {
+      type: 'selection',
+      columnKey: 'selection',
+      width: 50,
+      align: 'center',
+      fixed: 'left'
+    },
+    {
+      label: '用户昵称',
+      prop: 'schoolName',
+      align: 'center',
+      minWidth: 100,
+      slot: 'picture'
+    },
+    { label: '用户名', prop: 'provinceName', align: 'center' },
+    { label: '用户OpenId', prop: 'cityName', align: 'center' },
+    { label: '手机号', prop: 'departmentName', align: 'center' },
+    {
+      label: '用户类型',
+      prop: 'schoolLevel',
+      align: 'center',
+      formatter: (row) =>
+        schoolLevelDicts.value.find((d) => d.dictValue == row.schoolLevel)
+          ?.dictLabel
+    },
+    { label: '用户标签', prop: 'departmentName', align: 'center' },
+    {
+      label: '状态',
+      prop: 'schoolTag',
+      align: 'center',
+      slot: 'schoolTag',
+      formatter: (row) =>
+        schoolTagDicts.value.find((d) => d.dictValue == row.schoolTag)
+          ?.dictLabel
+    },
+    {
+      label: '创建时间',
+      prop: 'createTime',
+      align: 'center',
+      width: 160,
+      formatter: (row) => {
+        return row.createTime ? new Date(row.createTime).toLocaleString() : '';
+      }
+    },
+    {
+      columnKey: 'action',
+      label: '操作',
+      width: 200,
+      align: 'center',
+      slot: 'action'
+    }
+  ]);
+
+  /** 页面组件实例 */
+  const pageRef = ref(null);
+
+  const pageConfig = reactive({
+    pageUrl: '/baseinfo/schoolInfo/list',
+    exportUrl: '/baseinfo/schoolInfo/export',
+    fileName: '客户列表',
+    cacheKey: 'customerListTable'
+  });
+
+  //刷新表格
+  function reload(where) {
+    pageRef.value?.reload(where);
+  }
+
+  //批量删除
+  function handleBatchDelete(row) {
+    let selections = row ? [row] : pageRef.value?.getSelections();
+    let ids = selections.map((item) => item.id).join(',');
+    let url = `/baseinfo/schoolInfo/removeById/${ids}`;
+    pageRef.value?.operatBatch({
+      title: '确认删除?',
+      method: 'post',
+      url,
+      row
+    });
+  }
+  //导出excel
+  function handleExportExcel() {
+    pageRef.value?.exportData('高校列表');
+  }
+
+  //移出黑名单
+  function handleAddBlacklist(row) {
+    pageRef.value?.messageBoxConfirm({
+      message: '确认移出黑名单?',
+      fetch: () => request.post('/baseinfo/schoolInfo/edit', data)
+    });
+  }
+
+  //编辑页面
+  const editRef = ref(null);
+  function handleUpdate(row) {
+    editRef.value?.handleOpen(row);
+  }
+  //详情页面
+  const detailRef = ref(null);
+  function handleDetail(row) {
+    detailRef.value?.handleOpen(row);
+  }
+
+  //拉黑历史
+  const historyRef = ref(null);
+  function handleBlacklistHistory(row) {
+    historyRef.value?.handleOpen(row);
+  }
+</script>

+ 64 - 0
src/views/customer/list/components/blacklist-history.vue

@@ -0,0 +1,64 @@
+<!-- 编辑弹窗 -->
+<template>
+  <ele-modal form :width="800" v-model="visible" title="拉黑历史">
+    <SimpleTable
+      ref="tableRef"
+      style="width: 100%"
+      :columns="columns"
+      :fetchPage="fetchPage"
+      :formaterData="formaterData"
+    >
+    </SimpleTable>
+
+    <template #footer>
+      <el-button @click="handleCancel">关闭</el-button>
+    </template>
+  </ele-modal>
+</template>
+
+<script setup>
+  import { ref, reactive, nextTick } from 'vue';
+  import { Flag, ChatDotSquare } from '@element-plus/icons-vue';
+  import SimpleTable from '@/components/CommonPage/SimpleTable.vue';
+  import request from '@/utils/request';
+
+  /** 弹窗是否打开 */
+  const visible = defineModel({ type: Boolean });
+
+  /** 关闭弹窗 */
+  const handleCancel = () => {
+    visible.value = false;
+  };
+
+  /** 弹窗打开事件 */
+  let bookId = ref();
+  const tableRef = ref();
+  const handleOpen = (row) => {
+    visible.value = true;
+    bookId.value = row.id;
+    nextTick(() => {
+      tableRef.value?.reload();
+    });
+  };
+
+  function formaterData(data) {
+    return { rows: data.data };
+  }
+
+  function fetchPage(params) {
+    return request.get('/book/bookInfo/getChangeLog/' + bookId.value, {
+      params
+    });
+  }
+
+  // 表格数据
+  const columns = reactive([
+    { label: '拉黑时间', prop: 'createTime', width: 180 },
+    { label: '拉黑原因', prop: 'changeAttribute' },
+    { label: '操作员', prop: 'createName', width: 100 }
+  ]);
+
+  defineExpose({
+    handleOpen
+  });
+</script>

+ 72 - 0
src/views/customer/list/components/customer-detail.vue

@@ -0,0 +1,72 @@
+<template>
+  <ele-drawer
+    v-model="visible"
+    title="客户详情"
+    :size="1180"
+    style="max-width: 100%"
+    :body-style="{
+      height: '100%',
+      display: 'flex',
+      'flex-direction': 'column'
+    }"
+  >
+    <div class="flex items-center">
+      <el-avatar
+        :size="50"
+        src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"
+      />
+      <el-text style="margin-left: 10px">微信用户</el-text>
+    </div>
+
+    <div class="flex items-center mb-5 mt-5 ml-3">
+      <el-statistic :value="562">
+        <template #title>
+          <el-text>账户余额</el-text>
+        </template>
+      </el-statistic>
+      <el-statistic :value="562" style="margin-left: 80px">
+        <template #title>
+          <el-text>总订单数</el-text>
+        </template>
+      </el-statistic>
+    </div>
+
+    <el-tabs v-model="activeName" class="demo-tabs flex-1">
+      <el-tab-pane label="用户基础信息" name="baseinfo">
+        <base-info />
+      </el-tab-pane>
+      <el-tab-pane label="余额变动" name="accountChange">
+        <account-change />
+      </el-tab-pane>
+      <el-tab-pane label="扫描记录" name="scanLog">
+        <scan-log />
+      </el-tab-pane>
+      <el-tab-pane label="推荐信息" name="recommend">
+        <recommend-info />
+      </el-tab-pane>
+      <el-tab-pane label="意见反馈" name="fallback">
+        <fallback-info />
+      </el-tab-pane>
+    </el-tabs>
+  </ele-drawer>
+</template>
+
+<script setup>
+  import baseInfo from '@/views/customer/list/components/detail/base-info.vue';
+  import accountChange from '@/views/customer/list/components/detail/account-change.vue';
+  import scanLog from '@/views/customer/list/components/detail/scan-log.vue';
+  import recommendInfo from '@/views/customer/list/components/detail/recommend-info.vue';
+  import fallbackInfo from '@/views/customer/list/components/detail/fallback-info.vue';
+  const visible = ref(false);
+
+  function handleOpen(row) {
+    visible.value = true;
+    nextTick(() => {});
+  }
+
+  const activeName = ref('baseinfo');
+
+  defineExpose({
+    handleOpen
+  });
+</script>

+ 39 - 0
src/views/customer/list/components/detail/account-change.vue

@@ -0,0 +1,39 @@
+<template>
+  <common-table
+    ref="pageRef"
+    :pageConfig="pageConfig"
+    :columns="columns"
+    :tools="false"
+    :bodyStyle="{ padding: 0 }"
+  >
+  </common-table>
+</template>
+
+<script setup>
+  import { ref, reactive } from 'vue';
+  import CommonTable from '@/components/CommonPage/CommonTable.vue';
+
+  defineOptions({ name: 'accountChangeLog' });
+
+  /** 表格列配置 */
+  const columns = ref([
+    { label: '变动记录', prop: 'cover', align: 'center' },
+    { label: '余额变化', prop: 'baseInfo', align: 'center' },
+    { label: '变化后余额', prop: 'money', align: 'center' },
+    { label: '发生时间', prop: 'createTime', align: 'center' }
+  ]);
+
+  /** 页面组件实例 */
+  const pageRef = ref(null);
+
+  const pageConfig = reactive({
+    pageUrl: '/book/bookInfo/list',
+    fileName: '余额变动',
+    cacheKey: 'accountChangeLogTable'
+  });
+
+  //刷新表格
+  function reload(where) {
+    pageRef.value?.reload(where);
+  }
+</script>

+ 23 - 0
src/views/customer/list/components/detail/base-info.vue

@@ -0,0 +1,23 @@
+<template>
+  <div class="base-info">
+    <div class="common-title">基础信息</div>
+    <el-row :gutter="12" class="p-4 leading-8">
+      <el-col :span="8">用户名:048a4d98sad8a7d9</el-col>
+      <el-col :span="8">性别:男</el-col>
+      <el-col :span="8">真实姓名:***</el-col>
+      <el-col :span="8">联系方式:19299299222</el-col>
+      <el-col :span="16">地址信息:河南鹤壁浚县*********</el-col>
+    </el-row>
+    <div class="common-title">概况信息</div>
+    <el-row :gutter="12" class="p-4 leading-8">
+      <el-col :span="24">用户标签:多次购买</el-col>
+      <el-col :span="12">注册时间:2024-06-26 21:55:03</el-col>
+      <el-col :span="12">最近登录时间:2024-06-26 21:55:03</el-col>
+    </el-row>
+
+    <div class="common-title">备注信息</div>
+    <el-row :gutter="12" class="p-4 leading-8">
+      <el-col :span="24">备注:无</el-col>
+    </el-row>
+  </div>
+</template>

+ 80 - 0
src/views/customer/list/components/detail/fallback-info.vue

@@ -0,0 +1,80 @@
+<template>
+  <common-table
+    ref="pageRef"
+    :pageConfig="pageConfig"
+    :columns="columns"
+    :tools="false"
+    :body-style="{ padding: 0 }"
+  >
+    <template #action="{ row }">
+      <div>
+        <el-button type="primary" link @click="handleUpdate(row, 'detail')">
+          查看详情
+        </el-button>
+        <el-button type="primary" link @click="handleUpdate(row)">
+          去处理
+        </el-button>
+      </div>
+    </template>
+  </common-table>
+  <deal-fallback ref="dealFallbackRef" @done="reload()" />
+</template>
+
+<script setup>
+  import { ref, reactive } from 'vue';
+  import CommonTable from '@/components/CommonPage/CommonTable.vue';
+  import dealFallback from '@/views/optimization/fallback/components/deal-fallback.vue';
+  import { useDictData } from '@/utils/use-dict-data';
+
+  defineOptions({ name: 'fallbackInfoList' });
+  const [useStatusDicts] = useDictData(['sys_normal_disable']);
+
+  /** 表格列配置 */
+  const columns = ref([
+    {
+      label: '内容',
+      prop: 'paymentCode',
+      align: 'center',
+      minWidth: 200
+    },
+    { label: '分类', prop: 'type', align: 'center' },
+
+    {
+      label: '状态',
+      prop: 'useStatus',
+      align: 'center',
+      formatter: (row) =>
+        useStatusDicts.value.find((d) => d.dictValue == row.useStatus)
+          ?.dictLabel
+    },
+    { label: '时间', prop: 'createTime', align: 'center', width: 180 },
+    {
+      columnKey: 'action',
+      label: '操作',
+      width: 160,
+      align: 'center',
+      slot: 'action'
+    }
+  ]);
+
+  /** 页面组件实例 */
+  const pageRef = ref(null);
+
+  const pageConfig = reactive({
+    pageUrl: '/baseinfo/godown/pagelist',
+    exportUrl: '/baseinfo/godown/export',
+    fileName: '意见反馈',
+    cacheKey: 'fallbackInfoTable'
+  });
+
+  //刷新表格
+  function reload(where) {
+    pageRef.value?.reload(where);
+  }
+
+  //去处理
+  const dealFallbackRef = ref(null);
+  function handleUpdate(row, type) {
+    dealFallbackRef.value?.handleOpen(row, type);
+  }
+</script>

+ 39 - 0
src/views/customer/list/components/detail/recommend-info.vue

@@ -0,0 +1,39 @@
+<template>
+  <common-table
+    ref="pageRef"
+    :pageConfig="pageConfig"
+    :columns="columns"
+    :tools="false"
+    :bodyStyle="{ padding: 0 }"
+  >
+  </common-table>
+</template>
+
+<script setup>
+  import { ref, reactive } from 'vue';
+  import CommonTable from '@/components/CommonPage/CommonTable.vue';
+
+  defineOptions({ name: 'recommendInfo' });
+
+  /** 表格列配置 */
+  const columns = ref([
+    { label: 'UID', prop: 'cover', align: 'center' },
+    { label: '昵称', prop: 'baseInfo', align: 'center' },
+    { label: '手机号', prop: 'money', align: 'center' },
+    { label: '邀请时间', prop: 'createTime', align: 'center' }
+  ]);
+
+  /** 页面组件实例 */
+  const pageRef = ref(null);
+
+  const pageConfig = reactive({
+    pageUrl: '/book/bookInfo/list',
+    fileName: '推荐信息',
+    cacheKey: 'recommendInfo'
+  });
+
+  //刷新表格
+  function reload(where) {
+    pageRef.value?.reload(where);
+  }
+</script>

+ 39 - 0
src/views/customer/list/components/detail/scan-log.vue

@@ -0,0 +1,39 @@
+<template>
+  <common-table
+    ref="pageRef"
+    :pageConfig="pageConfig"
+    :columns="columns"
+    :tools="false"
+    :bodyStyle="{ padding: 0 }"
+  >
+  </common-table>
+</template>
+
+<script setup>
+  import { ref, reactive } from 'vue';
+  import CommonTable from '@/components/CommonPage/CommonTable.vue';
+
+  defineOptions({ name: 'accountChangeLog' });
+
+  /** 表格列配置 */
+  const columns = ref([
+    { label: '序号', type: 'index', align: 'center', width: 60 },
+    { label: 'ISBN', prop: 'baseInfo', align: 'center' },
+    { label: '商品名称', prop: 'money', align: 'center' },
+    { label: '扫描时间', prop: 'createTime', align: 'center' }
+  ]);
+
+  /** 页面组件实例 */
+  const pageRef = ref(null);
+
+  const pageConfig = reactive({
+    pageUrl: '/book/bookInfo/list',
+    fileName: '余额变动',
+    cacheKey: 'accountChangeLogTable'
+  });
+
+  //刷新表格
+  function reload(where) {
+    pageRef.value?.reload(where);
+  }
+</script>

+ 49 - 0
src/views/customer/list/components/page-search.vue

@@ -0,0 +1,49 @@
+<!-- 搜索表单 -->
+<template>
+  <ele-card :body-style="{ paddingBottom: '8px' }">
+    <ProSearch
+      :items="formItems"
+      ref="searchRef"
+      @search="search"
+      :initKeys="initKeys"
+    ></ProSearch>
+  </ele-card>
+</template>
+
+<script setup>
+  import { reactive, ref, defineEmits } from 'vue';
+  import ProSearch from '@/components/CommonPage/ProSearch2.vue';
+
+  const emit = defineEmits(['search']);
+
+  const formItems = computed(() => {
+    return [
+      { type: 'input', label: '用户名', prop: 'name' },
+      { type: 'input', label: '用户OpenId', prop: 'openid' },
+      { type: 'input', label: '用户昵称', prop: 'nickName' },
+      { type: 'input', label: '手机号', prop: 'phone' },
+      {
+        type: 'dictSelect',
+        label: '状态',
+        prop: 'schoolLevel',
+        props: {
+          code: 'school_level'
+        }
+      }
+    ];
+  });
+
+  const initKeys = reactive({
+    name: '',
+    nickName: '',
+    openid: '',
+    phone: '',
+    status: ''
+  });
+
+  const searchRef = ref(null);
+  /** 搜索 */
+  const search = (data) => {
+    emit('search', { ...data });
+  };
+</script>

+ 211 - 0
src/views/customer/list/index.vue

@@ -0,0 +1,211 @@
+<template>
+  <ele-page flex-table>
+    <page-search @search="reload"></page-search>
+
+    <common-table ref="pageRef" :pageConfig="pageConfig" :columns="columns">
+      <template #toolbar>
+        <el-button
+          type="success"
+          plain
+          v-permission="'customer:list:export'"
+          @click="handleExportExcel"
+          :icon="DownloadOutlined"
+        >
+          导出EXCEL
+        </el-button>
+      </template>
+
+      <template #picture="{ row }">
+        <div class="flex flex-col">
+          <el-avatar
+            src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"
+          />
+          <el-text style="text-align: center">{{ row.schoolName }}</el-text>
+        </div>
+      </template>
+
+      <template #schoolTag="{ row }">
+        <dict-data code="school_tag" type="tag" :model-value="row.schoolTag" />
+      </template>
+
+      <template #action="{ row }">
+        <div>
+          <el-button
+            type="success"
+            link
+            v-permission="'customer:list:detail'"
+            @click="handleDetail(row)"
+          >
+            [用户详情]
+          </el-button>
+          <el-button
+            type="warning"
+            link
+            v-permission="'customer:list:userTag'"
+            @click="handleUpdate(row)"
+          >
+            [用户标签]
+          </el-button>
+          <el-button
+            type="warning"
+            link
+            v-permission="'customer:list:accountDetail'"
+            @click="handleUpdate(row)"
+          >
+            [账户明细]
+          </el-button>
+          <el-button
+            type="success"
+            link
+            v-permission="'customer:list:blacklistHistory'"
+            @click="handleBlacklistHistory(row)"
+          >
+            [拉黑历史]
+          </el-button>
+          <el-button
+            type="danger"
+            link
+            v-permission="'customer:list:blacklist'"
+            @click="handleAddBlacklist(row)"
+          >
+            [加入黑名单]
+          </el-button>
+        </div>
+      </template>
+    </common-table>
+
+    <blacklist-history ref="historyRef"></blacklist-history>
+    <customer-detail ref="detailRef"></customer-detail>
+  </ele-page>
+</template>
+
+<script setup>
+  import { ref, reactive } from 'vue';
+  import { ElMessageBox } from 'element-plus/es';
+  import { EleMessage } from 'ele-admin-plus/es';
+  import { DownloadOutlined } from '@/components/icons';
+  import CommonTable from '@/components/CommonPage/CommonTable.vue';
+  import pageSearch from './components/page-search.vue';
+  import blacklistHistory from '@/views/customer/list/components/blacklist-history.vue';
+  import customerDetail from '@/views/customer/list/components/customer-detail.vue';
+  import { useDictData } from '@/utils/use-dict-data';
+  import request from '@/utils/request';
+
+  defineOptions({ name: 'customerList' });
+  const [schoolLevelDicts, schoolTagDicts] = useDictData([
+    'school_level',
+    'school_tag'
+  ]);
+
+  /** 表格列配置 */
+  const columns = ref([
+    {
+      type: 'selection',
+      columnKey: 'selection',
+      width: 50,
+      align: 'center',
+      fixed: 'left'
+    },
+    {
+      label: '用户昵称',
+      prop: 'schoolName',
+      align: 'center',
+      minWidth: 100,
+      slot: 'picture'
+    },
+    { label: '用户名', prop: 'provinceName', align: 'center' },
+    { label: '用户OpenId', prop: 'cityName', align: 'center' },
+    { label: '手机号', prop: 'departmentName', align: 'center' },
+    {
+      label: '用户类型',
+      prop: 'schoolLevel',
+      align: 'center',
+      formatter: (row) =>
+        schoolLevelDicts.value.find((d) => d.dictValue == row.schoolLevel)
+          ?.dictLabel
+    },
+    { label: '用户标签', prop: 'departmentName', align: 'center' },
+    {
+      label: '状态',
+      prop: 'schoolTag',
+      align: 'center',
+      slot: 'schoolTag',
+      formatter: (row) =>
+        schoolTagDicts.value.find((d) => d.dictValue == row.schoolTag)
+          ?.dictLabel
+    },
+    {
+      label: '创建时间',
+      prop: 'createTime',
+      align: 'center',
+      width: 160,
+      formatter: (row) => {
+        return row.createTime ? new Date(row.createTime).toLocaleString() : '';
+      }
+    },
+    {
+      columnKey: 'action',
+      label: '操作',
+      width: 200,
+      align: 'center',
+      slot: 'action'
+    }
+  ]);
+
+  /** 页面组件实例 */
+  const pageRef = ref(null);
+
+  const pageConfig = reactive({
+    pageUrl: '/baseinfo/schoolInfo/list',
+    exportUrl: '/baseinfo/schoolInfo/export',
+    fileName: '客户列表',
+    cacheKey: 'customerListTable'
+  });
+
+  //刷新表格
+  function reload(where) {
+    pageRef.value?.reload(where);
+  }
+
+  //批量删除
+  function handleBatchDelete(row) {
+    let selections = row ? [row] : pageRef.value?.getSelections();
+    let ids = selections.map((item) => item.id).join(',');
+    let url = `/baseinfo/schoolInfo/removeById/${ids}`;
+    pageRef.value?.operatBatch({
+      title: '确认删除?',
+      method: 'post',
+      url,
+      row
+    });
+  }
+  //导出excel
+  function handleExportExcel() {
+    pageRef.value?.exportData('高校列表');
+  }
+
+  //加入黑名单
+  function handleAddBlacklist(row) {
+    pageRef.value?.messageBoxConfirm({
+      message: '确认加入黑名单?',
+      fetch: () => request.post('/baseinfo/schoolInfo/edit', data)
+    });
+  }
+
+  //编辑页面
+  const editRef = ref(null);
+  function handleUpdate(row) {
+    editRef.value?.handleOpen(row);
+  }
+  //详情页面
+  const detailRef = ref(null);
+  function handleDetail(row) {
+    detailRef.value?.handleOpen(row);
+  }
+
+  //拉黑历史
+  const historyRef = ref(null);
+  function handleBlacklistHistory(row) {
+    historyRef.value?.handleOpen(row);
+  }
+</script>

+ 76 - 0
src/views/customer/stat/components/user-active-compare.vue

@@ -0,0 +1,76 @@
+<template>
+  <div class="flex flex-col">
+    <el-row :gutter="16" class="mt-4">
+      <el-col :span="8">
+        <div class="statistic-card">
+          <el-statistic title="今日" :value="98500" suffix="次"> </el-statistic>
+          <div class="statistic-footer">
+            <span>对比昨日</span>
+            <el-text type="success">
+              24%<el-icon><Top /></el-icon>
+            </el-text>
+          </div>
+        </div>
+      </el-col>
+      <el-col :span="8">
+        <el-statistic :value="693700" title="合计" suffix="次"> </el-statistic>
+      </el-col>
+      <el-col :span="8">
+        <el-statistic :value="690" title="均值" suffix="次"> </el-statistic>
+      </el-col>
+    </el-row>
+    <v-chart ref="saleChartRef" style="height: 280px" :option="options" />
+  </div>
+</template>
+<script setup>
+  import VChart from 'vue-echarts';
+  import { use } from 'echarts/core';
+  import { CanvasRenderer } from 'echarts/renderers';
+  import { LineChart } from 'echarts/charts';
+  import {
+    GridComponent,
+    TooltipComponent,
+    LegendComponent,
+    ToolboxComponent
+  } from 'echarts/components';
+
+  // 按需加载echarts
+  use([
+    CanvasRenderer,
+    LineChart,
+    GridComponent,
+    TooltipComponent,
+    LegendComponent
+  ]);
+
+  const options = reactive({
+    tooltip: {
+      trigger: 'axis'
+    },
+    legend: {
+      data: ['活跃用户数']
+    },
+    grid: {
+      left: '3%',
+      right: '4%',
+      bottom: '3%',
+      containLabel: true
+    },
+    xAxis: {
+      type: 'category',
+      boundaryGap: false,
+      data: ['1月', '2月', '3月', '4月', '5月']
+    },
+    yAxis: {
+      type: 'value'
+    },
+    series: [
+      {
+        name: '活跃用户数',
+        type: 'line',
+        data: [120, 132, 234, 134, 90],
+        smooth: true
+      }
+    ]
+  });
+</script>

+ 77 - 0
src/views/customer/stat/components/user-add-compare.vue

@@ -0,0 +1,77 @@
+<template>
+  <div class="flex flex-col">
+    <el-row :gutter="16" class="mt-4">
+      <el-col :span="8">
+        <div class="statistic-card">
+          <el-statistic title="今日" :value="98500" suffix="人"> </el-statistic>
+          <div class="statistic-footer">
+            <span>对比昨日</span>
+            <el-text type="success">
+              24%<el-icon><Top /></el-icon>
+            </el-text>
+          </div>
+        </div>
+      </el-col>
+      <el-col :span="8">
+        <el-statistic :value="693700" title="昨日" suffix="人"> </el-statistic>
+      </el-col>
+      <el-col :span="8">
+        <el-statistic :value="690" title="均值" suffix="次"> </el-statistic>
+      </el-col>
+    </el-row>
+    <v-chart ref="saleChartRef" style="height: 280px" :option="options" />
+  </div>
+</template>
+<script setup>
+  import VChart from 'vue-echarts';
+  import { use } from 'echarts/core';
+  import { CanvasRenderer } from 'echarts/renderers';
+  import { LineChart } from 'echarts/charts';
+  import {
+    GridComponent,
+    TooltipComponent,
+    LegendComponent,
+    ToolboxComponent
+  } from 'echarts/components';
+  import { Top, Bottom } from '@element-plus/icons-vue';
+
+  // 按需加载echarts
+  use([
+    CanvasRenderer,
+    LineChart,
+    GridComponent,
+    TooltipComponent,
+    LegendComponent
+  ]);
+
+  const options = reactive({
+    tooltip: {
+      trigger: 'axis'
+    },
+    legend: {
+      data: ['新增用户数']
+    },
+    grid: {
+      left: '3%',
+      right: '4%',
+      bottom: '3%',
+      containLabel: true
+    },
+    xAxis: {
+      type: 'category',
+      boundaryGap: false,
+      data: ['1月', '2月', '3月', '4月', '5月']
+    },
+    yAxis: {
+      type: 'value'
+    },
+    series: [
+      {
+        name: '新增用户数',
+        type: 'line',
+        data: [120, 132, 234, 134, 90],
+        smooth: true
+      }
+    ]
+  });
+</script>

+ 61 - 0
src/views/customer/stat/components/user-data-line.vue

@@ -0,0 +1,61 @@
+<template>
+  <v-chart ref="saleChartRef" style="height: 350px" :option="options" />
+</template>
+<script setup>
+  import VChart from 'vue-echarts';
+  import { use } from 'echarts/core';
+  import { CanvasRenderer } from 'echarts/renderers';
+  import { LineChart } from 'echarts/charts';
+  import {
+    GridComponent,
+    TooltipComponent,
+    LegendComponent,
+    ToolboxComponent
+  } from 'echarts/components';
+
+  // 按需加载echarts
+  use([
+    CanvasRenderer,
+    LineChart,
+    GridComponent,
+    TooltipComponent,
+    LegendComponent
+  ]);
+
+  const options = reactive({
+    tooltip: {
+      trigger: 'axis'
+    },
+    legend: {
+      data: ['新增用户', '活跃用户'],
+    },
+    grid: {
+      left: '3%',
+      right: '4%',
+      bottom: '3%',
+      containLabel: true
+    },
+    xAxis: {
+      type: 'category',
+      boundaryGap: false,
+      data: ['1月', '2月', '3月', '4月', '5月', '6月', '7日']
+    },
+    yAxis: {
+      type: 'value'
+    },
+    series: [
+      {
+        name: '新增用户',
+        type: 'line',
+        data: [120, 132, 234, 134, 90, 230, 210],
+        smooth: true
+      },
+      {
+        name: '活跃用户',
+        type: 'line',
+        data: [220, 182, 101, 134, 90, 120, 310],
+        smooth: true
+      }
+    ]
+  });
+</script>

+ 148 - 0
src/views/customer/stat/index.vue

@@ -0,0 +1,148 @@
+<template>
+  <ele-page flex-table>
+    <ele-card flex-table>
+      <div class="flex justify-between items-start mb-10">
+        <div class="flex items-start justify-around" style="width: 600px">
+          <el-statistic
+            title="今日新增"
+            :value="562"
+            value-style="font-size:24px"
+          >
+            <template #suffix>
+              <el-icon style="vertical-align: -0.125em" color="#52c41a">
+                <Top />
+              </el-icon>
+              <el-text type="success" size="small">15%</el-text>
+            </template>
+          </el-statistic>
+          <el-statistic
+            title="昨日新增"
+            :value="562"
+            value-style="font-size:24px"
+          >
+            <template #suffix>
+              <el-icon style="vertical-align: -0.125em" color="#ff4d4f">
+                <Bottom />
+              </el-icon>
+              <el-text type="danger" size="small">25%</el-text>
+            </template>
+          </el-statistic>
+          <el-statistic
+            title="7日新增"
+            :value="562"
+            value-style="font-size:24px"
+          >
+            <template #suffix>
+              <el-icon style="vertical-align: -0.125em" color="#ff4d4f">
+                <Bottom />
+              </el-icon>
+              <el-text type="danger" size="small">25%</el-text>
+            </template>
+          </el-statistic>
+          <el-statistic
+            title="30日新增"
+            :value="562"
+            value-style="font-size:24px"
+          >
+            <template #suffix>
+              <el-icon style="vertical-align: -0.125em" color="#ff4d4f">
+                <Bottom />
+              </el-icon>
+              <el-text type="danger" size="small">25%</el-text>
+            </template>
+          </el-statistic>
+        </div>
+        <date-search></date-search>
+      </div>
+
+      <div class="common-card mb-10">
+        <div class="common-title">用户数据</div>
+        <user-data-line></user-data-line>
+      </div>
+      <div class="common-card flex gap-6">
+        <div class="flex-1 gray-card">
+          <div class="common-title">今日对比昨日| 分时新增新用户数量</div>
+          <user-add-compare></user-add-compare>
+        </div>
+        <div class="flex-1 gray-card">
+          <div class="common-title">今日对比昨日| 分时活跃新用户数量</div>
+          <user-active-compare></user-active-compare>
+        </div>
+      </div>
+    </ele-card>
+  </ele-page>
+</template>
+
+<script setup>
+  import { ref, reactive } from 'vue';
+  import { Bottom, Top } from '@element-plus/icons-vue';
+  import dateSearch from '@/components/DateSearch/index.vue';
+  import pageSearch from '@/views/finance/withdrawal/components/page-search.vue';
+  import { useDictData } from '@/utils/use-dict-data';
+  import userDataLine from '@/views/customer/stat/components/user-data-line.vue';
+  import userActiveCompare from '@/views/customer/stat/components/user-active-compare.vue';
+  import userAddCompare from '@/views/customer/stat/components/user-add-compare.vue';
+
+  defineOptions({ name: 'withdrawal' });
+  const [useStatusDicts] = useDictData(['use_status']);
+
+  const useStatus = ref('1');
+  function handleStatusChange(value) {
+    pageRef.value.reload({ useStatus: value });
+  }
+
+  /** 表格列配置 */
+  const columns = ref([
+    { label: '交易时间', prop: 'createTime', align: 'center', width: 180 },
+    { label: '用户UID', prop: 'uid', align: 'center', minWidth: 140 },
+    {
+      label: '支付单号/流水号',
+      prop: 'paymentCode',
+      align: 'center',
+      minWidth: 160
+    },
+    { label: '对方账户', prop: 'addressDetail', align: 'center' },
+    { label: '结算金额', prop: 'money', align: 'center' },
+    {
+      label: '交易状态',
+      prop: 'useStatus',
+      align: 'center',
+      formatter: (row) =>
+        useStatusDicts.value.find((d) => d.dictValue == row.useStatus)
+          ?.dictLabel
+    },
+    {
+      label: '交易类型',
+      prop: 'paymentType',
+      align: 'center',
+      formatter: (row) =>
+        useStatusDicts.value.find((d) => d.dictValue == row.useStatus)
+          ?.dictLabel
+    },
+    { label: '订单编号', prop: 'code', align: 'center' }
+  ]);
+
+  /** 页面组件实例 */
+  const pageRef = ref(null);
+
+  const pageConfig = reactive({
+    pageUrl: '/baseinfo/godown/pagelist',
+    exportUrl: '/baseinfo/godown/export',
+    fileName: '佣金记录',
+    cacheKey: 'commissionTable'
+  });
+
+  //刷新表格
+  function reload(where) {
+    pageRef.value?.reload(where);
+  }
+</script>
+
+<style lang="scss">
+  .gray-card {
+    border-radius: 10px;
+    padding: 20px;
+    box-sizing: border-box;
+    background: #f9f8f7;
+  }
+</style>

+ 1 - 1
src/views/optimization/complain/index.vue

@@ -27,7 +27,7 @@
           <el-button
             type="primary"
             link
-            v-permission="'optimization:list:detail'"
+            v-permission="'optimization:complain:detail'"
             @click="handleDetail(row)"
           >
             详情

+ 3 - 3
src/views/optimization/service/index.vue

@@ -23,7 +23,7 @@
           <el-button
             type="primary"
             link
-            v-permission="'optimization:list:detail'"
+            v-permission="'optimization:service:detail'"
             @click="handleUpdate(row)"
           >
             [编辑]
@@ -31,7 +31,7 @@
           <el-button
             :type="row.useStatus == 1 ? 'warning' : 'primary'"
             link
-            v-permission="'optimization:list:changeStatus'"
+            v-permission="'optimization:service:changeStatus'"
             @click="handleChangeStatus(row)"
           >
             {{ row.useStatus == 1 ? '[停用]' : '[启用]' }}
@@ -39,7 +39,7 @@
           <el-button
             type="danger"
             link
-            v-permission="'optimization:list:delete'"
+            v-permission="'optimization:service:delete'"
             @click="handleDelete(row)"
           >
             [删除]