浏览代码

feat: 带看数据统计

tangning 2 年之前
父节点
当前提交
39ac8fcc93

+ 1 - 1
.env.development

@@ -6,7 +6,7 @@ VITE_PUBLIC_PATH = ./
 
 # Cross-domain proxy, you can configure multiple
 # Please note that no line breaks
-VITE_PROXY = [["/qjkankan","https://test.4dkankan.com/qjkankan"],["/upload","https://v4-uat.4dkankan.com/service/manage/common/upload/files"],["/service","https://v4-uat.4dkankan.com/service"]]
+VITE_PROXY = [["/qjkankan","https://test.4dkankan.com/qjkankan"],["/takelook","https://v4-test.4dkankan.com/takelook"],["/upload","https://v4-uat.4dkankan.com/service/manage/common/upload/files"],["/service","https://v4-uat.4dkankan.com/service"]]
 # VITE_PROXY=[["/api","https://vvbin.cn/test"]]
 
 # Delete console

+ 36 - 1
src/api/statistics/index.ts

@@ -2,6 +2,7 @@ import { defHttp } from '/@/utils/http/axios';
 import { PageParams, ListGetResultModel, DelParams, roleParams, companyExcelParams } from './model';
 import { Result, FileStream } from '/#/axios';
 let qjurl = window.location.host == 'v4-uat.4dkankan.com'?'https://test.4dkankan.com':''
+let dkurl = window.location.host == 'v4-uat.4dkankan.com'?'https://v4-test.4dkankan.com':''
 enum Api {
   buryPointList = '/service/manage/buryPoint/list',
   buryPointAdd = '/service/manage/buryPoint/add',
@@ -20,6 +21,9 @@ enum Api {
   workTrend = '/qjkankan/api/age/report/workTrend',
   qjuserTrend = '/qjkankan/api/age/report/userTrend',
   volumeTrend = '/qjkankan/api/age/report/volumeTrend',
+  roomData = '/takelook/roomData',
+  takeLookTop5 = '/takelook/takeLookTop5',
+  roomVisitData = '/takelook/roomVisitData',
 }
 
 /**
@@ -125,7 +129,38 @@ export const buryPointList = (params: PageParams) =>
       ignoreCancelToken: true,
     },
   });
-  
+  export const roomVisitData = (params) =>
+  defHttp.post<Result>({
+    url: dkurl + Api.roomVisitData,
+    data: params,
+    headers: {
+      // @ts-ignore
+      withToken: false,
+      ignoreCancelToken: true,
+    },
+  });
+  export const takeLookTop5 = (params) =>
+  defHttp.get<Result>({
+    url: dkurl + Api.takeLookTop5,
+    params,
+    data: params,
+    headers: {
+      // @ts-ignore
+      withToken: false,
+      ignoreCancelToken: true
+    },
+  });
+  export const roomData = (params) =>
+  defHttp.get<Result>({
+    url: dkurl + Api.roomData,
+    params,
+    data: params,
+    headers: {
+      // @ts-ignore
+      ignoreCancelToken: true,
+      withToken: false,
+    },
+  });
   export const workType = (params) =>
   defHttp.get<Result>({
     url: qjurl + Api.workType,

+ 1 - 1
src/utils/http/axios/index.ts

@@ -136,7 +136,7 @@ const transform: AxiosTransform = {
   requestInterceptors: (config, options) => {
     // 请求之前处理config
     const token = getToken();
-    if (token && (config as Recordable)?.requestOptions?.withToken !== false) {
+    if (token && (config as Recordable)?.requestOptions?.withToken !== false && (config as Recordable)?.headers?.withToken !== false) {
       // jwt token
       (config as Recordable).headers.Authorization = options.authenticationScheme
         ? `${options.authenticationScheme} ${token}`

+ 293 - 0
src/views/product/updataTips/AddModal.vue

@@ -0,0 +1,293 @@
+<template>
+  <BasicModal
+    v-bind="$attrs"
+    @register="register"
+    title="新增"
+    :confirmLoading="loading"
+    @visible-change="handleVisibleChange"
+    @cancel="resetFields"
+    :width="550"
+    @ok="handleSubmit"
+  >
+    <div class="pt-2px pr-3px" style="width:600px">
+      <BasicForm @register="registerForm" :model="model" >
+        <template #text="{ model, field }">
+          {{ model[field]  }}
+        </template>
+      </BasicForm>
+    </div>
+  </BasicModal>
+</template>
+<script lang="ts">
+  import { defineComponent, ref, nextTick, onMounted, reactive } from 'vue';
+  import { BasicModal, useModalInner } from '/@/components/Modal';
+  import { BasicForm, FormSchema, useForm } from '/@/components/Form/index';
+  import { useMessage } from '/@/hooks/web/useMessage';
+  import { uploadApi, SpaceSdkUpload } from '/@/api/product/index';
+  import { useI18n } from '/@/hooks/web/useI18n';
+  import { isEmojiCharacter } from '/@/utils';
+  const { t } = useI18n();
+  export default defineComponent({
+    components: { BasicModal, BasicForm },
+    props: {
+      userData: { type: Object },
+    },
+    emits: ['update', 'register'],
+    setup(props, { emit }) {
+      const modelRef = ref({});
+      const loading = ref(false)
+      const fileFlow = reactive({
+        file:null
+      })
+      const { createMessage } = useMessage();
+      const schemas: FormSchema[] = [
+        {
+          field: 'type',
+          component: 'Input',
+          label: t('routes.product.types'),
+          colProps: {
+            span: 20,
+          },
+        },{
+          field: 'ctivated',
+          label: '提示时段',
+          component: 'RangePicker',
+          componentProps: {
+            format: 'YYYY-MM-DD',
+            valueFormat:'YYYY-MM-DD',
+            width:100,
+          },
+          colProps: {
+            xl: 6,
+            xxl: 6,
+          },
+        },
+        {
+          field: 'version',
+          component: 'Input',
+          label: '中文标题',
+          required: true,
+          colProps: {
+            span: 20,
+          },
+          rules: [
+            {
+              required: true,
+              // @ts-ignore
+              validator: async (rule, value) => {
+                if (!value?.trim()) {
+                  return Promise.reject(t('common.inputText')+'中文标题');
+                }
+                if(/.*[\u4e00-\u9fa5]+.*$/.test(value)){
+                  /* eslint-disable-next-line */
+                  return Promise.reject('不支持中文字符');
+                }
+                if(isEmojiCharacter(value)){
+                  /* eslint-disable-next-line */
+                  return Promise.reject('不支持emoji表情');
+                }
+                return Promise.resolve();
+              },
+              trigger: 'change',
+            },
+          ],
+          componentProps: {
+            maxLength: 15,
+          },
+        },
+        {
+          field: 'versions',
+          component: 'Input',
+          label: '英文标题',
+          required: true,
+          colProps: {
+            span: 20,
+          },
+          rules: [
+            {
+              required: true,
+              // @ts-ignore
+              validator: async (rule, value) => {
+                if (!value?.trim()) {
+                  return Promise.reject(t('common.inputText')+'英文标题');
+                }
+                if(/.*[\u4e00-\u9fa5]+.*$/.test(value)){
+                  /* eslint-disable-next-line */
+                  return Promise.reject('不支持中文字符');
+                }
+                if(isEmojiCharacter(value)){
+                  /* eslint-disable-next-line */
+                  return Promise.reject('不支持emoji表情');
+                }
+                return Promise.resolve();
+              },
+              trigger: 'change',
+            },
+          ],
+          componentProps: {
+            maxLength: 15,
+          },
+        },
+        {
+          field: 'imprintCh',
+          component: 'InputTextArea',
+          // required: true,
+          label: t('routes.product.description_zh'),
+          componentProps: {
+            rows:4,
+          },
+          colProps: {
+            span: 20,
+          },
+        },
+        {
+          field: 'imprintEn',
+          component: 'InputTextArea',
+          // required: true,
+          label: t('routes.product.description_en'),
+          componentProps: {
+            rows:4,
+          },
+          colProps: {
+            span: 20,
+          },
+        },
+        {
+          field: 'file',
+          component: 'Upload',
+          label: '图片',
+          required: true,
+          rules: [{ required: true, message: t('common.uploadMessge') }],
+          // helpMessage: t('routes.corporation.uploadHelp'),
+          itemProps: {
+            validateTrigger: 'onBlur',
+          },
+          componentProps: {
+            api: uploadApi,
+            maxNumber: 1,
+            maxSize: 1000,
+            fileFlow:true,
+            accept: ['zip','rar','png'],
+            afterFetch: function (data) {
+              console.log('uploadApi',data)
+              // Reflect.set(data, 'url', data.file);
+              fileFlow.file = data.file
+              return data;
+            },
+          },
+
+          colProps: {
+            span: 20,
+          },
+        },
+        {
+          field: 'isTop',
+          component: 'Input',
+          label: '详情链接',
+          componentProps: {
+            maxLength: 15,
+          },
+          colProps: {
+            span: 20,
+          },
+        },
+      ];
+      const [registerForm, { validate, resetFields, setFieldsValue }] = useForm({
+        labelWidth: 120,
+        schemas,
+        showActionButtonGroup: false,
+        actionColOptions: {
+          span: 24,
+        },
+      });
+      onMounted(() => {});
+      let addListFunc = () => {};
+      const [register, { closeModal }] = useModalInner((data) => {
+        data && onDataReceive(data);
+      });
+      function renderOwnTypeLabel(type: number): string {
+        switch (type) {
+          case 2:
+            return t('routes.product.sdkType.2');
+          case 1:
+            return t('routes.product.sdkType.1');
+          case 3:
+            return t('routes.product.type.3');
+          default:
+            return '';
+        }
+      }
+      function rendercameraTypeLabel(cameraType: number): string {
+        switch (cameraType) {
+          case 4:
+            return t('routes.devices.cameraName.4');
+          case 1:
+            return t('routes.devices.cameraName.1');
+          case 9:
+            return t('routes.devices.cameraName.9');
+          case 10:
+            return t('routes.devices.cameraName.10');
+          case 6:
+            return t('routes.devices.cameraName.6');
+          default:
+            return '';
+        }
+      }
+      function onDataReceive(data) {
+        modelRef.value = data
+        resetFields();
+        setFieldsValue({
+          type:renderOwnTypeLabel(Number(data)),
+        });
+      }
+      const handleSubmit = async () => {
+        loading.value = true
+        try {
+          const params = await validate();
+          const apiData = {
+            data:{
+            ...params as any,
+            platformType:modelRef.value,
+            file:fileFlow.file,
+            isTop:params.isTop && params.isTop[0] || 0
+            }
+          }
+          console.log('res', apiData);
+          const res = await SpaceSdkUpload(apiData);
+          console.log('res', res);
+          closeModal();
+          resetFields();
+          createMessage.success(t('common.optSuccess'));
+          emit('update');
+          loading.value = false
+        } catch (error) {
+          loading.value = false
+          console.log('not passing', error);
+        }
+      };
+      function handleVisibleChange(v) {
+        v && props.userData && nextTick(() => onDataReceive(props.userData));
+      }
+      return {
+        register,
+        rendercameraTypeLabel,
+        renderOwnTypeLabel,
+        schemas,
+        registerForm,
+        model: modelRef,
+        fileFlow,
+        handleVisibleChange,
+        handleSubmit,
+        loading,
+        addListFunc,
+        resetFields,
+        t,
+      };
+    },
+  });
+</script>
+<style lang="less">
+.ant-calendar-picker-input.ant-input{
+  width: 300px;
+}
+</style>

+ 254 - 0
src/views/product/updataTips/EditModal.vue

@@ -0,0 +1,254 @@
+<template>
+  <BasicModal
+    v-bind="$attrs"
+    @register="register"
+    :confirmLoading="loading"
+    title="编辑版本"
+    @visible-change="handleVisibleChange"
+    @cancel="resetFields"
+    @ok="handleSubmit"
+  >
+    <div class="pt-2px pr-3px">
+      <BasicForm @register="registerForm" :model="model" >
+        <template #text="{ model, field }">
+          {{ model[field]  }}
+        </template>
+        <template #Checkbox="{ model, field }">
+          <div>
+            <Checkbox v-model:checked="model[field]">(勾选此项,将在四维看看官网置顶)</Checkbox>
+          </div>
+        </template>
+      </BasicForm>
+    </div>
+  </BasicModal>
+</template>
+<script lang="ts">
+  import { defineComponent, ref, nextTick, onMounted, reactive } from 'vue';
+  import { BasicModal, useModalInner } from '/@/components/Modal';
+  import { BasicForm, FormSchema, useForm } from '/@/components/Form/index';
+  import { Checkbox  } from 'ant-design-vue';
+  import { useMessage } from '/@/hooks/web/useMessage';
+  import { useI18n } from '/@/hooks/web/useI18n';
+  import { uploadApi, SpaceSdkUpdate } from '/@/api/product/index';
+  import { isEmojiCharacter } from '/@/utils';
+
+  const { t } = useI18n();
+  export default defineComponent({
+    components: { BasicModal, BasicForm, Checkbox },
+    props: {
+      userData: { type: Object },
+    },
+    emits: ['update', 'register'],
+    setup(props, { emit }) {
+      const modelRef = ref({});
+      const loading = ref(false)
+      const fileFlow = reactive({
+        file:null
+      })
+      const { createMessage } = useMessage();
+      const schemas: FormSchema[] = [
+        {
+          field: 'fileUrl',
+          component: 'Input',
+          label: t('routes.product.types'),
+          show: false,
+          colProps: {
+            span: 24,
+          },
+        },{
+          field: 'id',
+          component: 'Input',
+          label: t('routes.product.types'),
+          show: false,
+          colProps: {
+            span: 24,
+          },
+        },{
+          field: 'platformType',
+          component: 'Input',
+          label: t('routes.product.types'),
+          show: false,
+          colProps: {
+            span: 24,
+          },
+        },{
+          field: 'type',
+          component: 'Input',
+          label: t('routes.product.types'),
+          slot: 'text',
+          colProps: {
+            span: 24,
+          },
+        },
+        {
+          field: 'version',
+          component: 'Input',
+          label: t('routes.product.version'),
+          required: true,
+          colProps: {
+            span: 24,
+          },
+          rules: [
+            {
+              required: true,
+              // @ts-ignore
+              validator: async (rule, value) => {
+                if (!value?.trim()) {
+                  return Promise.reject(t('common.inputText')+t('routes.product.version'));
+                }
+                if(/.*[\u4e00-\u9fa5]+.*$/.test(value)){
+                  /* eslint-disable-next-line */
+                  return Promise.reject('不支持中文字符');
+                }
+                if(isEmojiCharacter(value)){
+                  /* eslint-disable-next-line */
+                  return Promise.reject('不支持emoji表情');
+                }
+                return Promise.resolve();
+              },
+              trigger: 'change',
+            },
+          ],
+          componentProps: {
+            disabled:true,
+            maxLength: 15,
+            onChange: (data) => {
+              console.log('data', data);
+            },
+          },
+        },
+        {
+          field: 'imprintCh',
+          component: 'InputTextArea',
+          // required: true,
+          label: t('routes.product.description_zh'),
+          componentProps: {
+            rows:4,
+          },
+          colProps: {
+            span: 24,
+          },
+        },
+        {
+          field: 'imprintEn',
+          component: 'InputTextArea',
+          // required: true,
+          label: t('routes.product.description_en'),
+          componentProps: {
+            rows:4,
+          },
+          colProps: {
+            span: 24,
+          },
+        },
+        {
+          field: 'isTop',
+          component: 'CheckboxGroup',
+          label: '官网置顶',
+          slot:'Checkbox',
+          componentProps: {
+            options: [
+              { label: '(勾选此项,将在四维看看官网置顶)', value: 1 },
+            ],
+          },
+          colProps: {
+            span: 24,
+          },
+        },
+      ];
+      const [registerForm, { validate, resetFields, setFieldsValue }] = useForm({
+        labelWidth: 120,
+        schemas,
+        showActionButtonGroup: false,
+        actionColOptions: {
+          span: 24,
+        },
+      });
+      onMounted(() => {});
+      let addListFunc = () => {};
+      const [register, { closeModal }] = useModalInner((data) => {
+        console.log('useModalInner',data)
+        data && onDataReceive(data);
+      });
+      function renderOwnTypeLabel(type: number): string {
+        switch (type) {
+          case 2:
+            return t('routes.product.sdkType.2');
+          case 1:
+            return t('routes.product.sdkType.1');
+          case 3:
+            return t('routes.product.type.3');
+          default:
+            return '';
+        }
+      }
+      function rendercameraTypeLabel(cameraType: number): string {
+        switch (cameraType) {
+          case 4:
+            return t('routes.devices.cameraName.4');
+          case 1:
+            return t('routes.devices.cameraName.1');
+          case 9:
+            return t('routes.devices.cameraName.9');
+          case 10:
+            return t('routes.devices.cameraName.10');
+          case 6:
+            return t('routes.devices.cameraName.6');
+          default:
+            return '';
+        }
+      }
+      function onDataReceive(data) {
+        modelRef.value = data
+        let setData = {
+          ...data,
+          type:renderOwnTypeLabel(Number(data.type)),
+        }
+        console.log('onDataReceive',data,setData)
+        resetFields();
+        setFieldsValue(setData);
+      }
+      const handleSubmit = async () => {
+        loading.value = true
+        try {
+          const params = await validate();
+          console.log('modelRef',params)
+          const apiData = {
+            // ...modelRef.value,
+            ...params as any,
+            isTop:params.isTop?1:0
+          }
+          console.log('res', apiData);
+          const res = await SpaceSdkUpdate(apiData);
+          console.log('res', res);
+          closeModal();
+          resetFields();
+          createMessage.success(t('common.optSuccess'));
+          emit('update');
+          loading.value = false
+        } catch (error) {
+          loading.value = false
+          console.log('not passing', error);
+        }
+      };
+      function handleVisibleChange(v) {
+        v && props.userData && nextTick(() => onDataReceive(props.userData));
+      }
+      return {
+        register,
+        rendercameraTypeLabel,
+        renderOwnTypeLabel,
+        schemas,
+        registerForm,
+        model: modelRef,
+        fileFlow,
+        handleVisibleChange,
+        handleSubmit,
+        addListFunc,
+        resetFields,
+        loading,
+        t,
+      };
+    },
+  });
+</script>

+ 297 - 0
src/views/product/updataTips/index.vue

@@ -0,0 +1,297 @@
+<template>
+  <PageWrapper contentBackground>
+    <template #footer>
+      <a-tabs v-model:activeKey="searchInfo.platformType" @change="tabChange">
+        <a-tab-pane key="1" tab="四维看看" />
+        <a-tab-pane key="2" tab="四维深时" />
+        <a-tab-pane key="3" tab="四维全景" />
+        <a-tab-pane key="4" tab="四维带看" />
+      </a-tabs>
+    </template>
+    <div class="desc-wrap-BasicTable">
+      <BasicTable @register="registerTimeTable">
+        <template #toolbar>
+          <a-button
+            type="primary"
+            v-if="getTypeCheckPerm('sdk-add')"
+            @click="
+              () => {
+                openAddModal(true, searchInfo.platformType);
+              }
+            "
+            >新增</a-button
+          >
+        </template>
+        <template #action="{ record }">
+          <TableAction
+            :actions="[
+              {
+                label: '编辑',
+                //icon: 'ep:edit',
+                ifShow: getTypeCheckPerm('sdk-update'),
+                onClick: handleEdit.bind(null, record),
+              },
+              {
+                label: '删除',
+                ifShow: getTypeCheckPerm('sdk-delete') && record.status != 1,
+                //icon: 'ic:outline-delete-outline',
+                onClick: handleDelete.bind(null, record),
+              },
+            ]"
+          />
+        </template>
+      </BasicTable>
+    </div>
+    <AddModal @update="reload" @register="registerAddModal" />
+    <EditModal @register="registerEditModal" @update="reload" />
+  </PageWrapper>
+</template>
+<script lang="ts">
+import { defineComponent, reactive, h } from 'vue';
+import { Time } from '/@/components/Time';
+import { BasicTable, useTable, FormProps, TableAction, BasicColumn } from '/@/components/Table';
+import { PageWrapper } from '/@/components/Page';
+import { Divider, Card, Empty, Descriptions, Steps, Tabs,Switch } from 'ant-design-vue';
+import { SpaceSdkList, SpaceSdkDelete, SpaceSdkOnline } from '/@/api/product';
+import { SpaceSdkTop } from '/@/api/product/index';
+import { useModal } from '/@/components/Modal';
+import { useI18n } from '/@/hooks/web/useI18n';
+import AddModal from './AddModal.vue';
+import EditModal from './EditModal.vue';
+import { useMessage } from '/@/hooks/web/useMessage';
+import { usePermissionStore } from '/@/store/modules/permission';
+export default defineComponent({
+  components: {
+    BasicTable,
+    AddModal,
+    EditModal,
+    TableAction,
+    PageWrapper,
+    [Divider.name]: Divider,
+    [Card.name]: Card,
+    Empty,
+    [Descriptions.name]: Descriptions,
+    [Descriptions.Item.name]: Descriptions.Item,
+    [Steps.name]: Steps,
+    [Steps.Step.name]: Steps.Step,
+    [Tabs.name]: Tabs,
+    [Tabs.TabPane.name]: Tabs.TabPane,
+  },
+  setup() {
+    const { t } = useI18n();
+    const permissionStore = usePermissionStore();
+    const { getCheckPerm } = permissionStore;
+    const searchInfo = reactive<Recordable>({
+      platformType: '1',
+    });
+    const [registerAddModal, { openModal: openAddModal }] = useModal();
+    const [registerEditModal, { openModal: openEditModal }] = useModal();
+    const { createMessage, createConfirm } = useMessage();
+    const searchForm: Partial<FormProps> = {
+      labelWidth: 100,
+      schemas: [
+        {
+          field: 'version',
+          label: '版本号',
+          component: 'Input',
+          componentProps: {
+            maxLength: 100,
+          },
+          colProps: {
+            xl: 6,
+            xxl: 6,
+          },
+        },
+      ],
+    };
+    const sdkTableSchema: BasicColumn[] = [
+      {
+        title: '版本号',
+        width: 80,
+        dataIndex: 'version',
+      },
+      {
+        title: '版本更新说明',
+        width: 240,
+        dataIndex: 'imprintCh',
+      },
+      {
+        title: '首次发布时间',
+        width: 120,
+        dataIndex: 'publishTime',
+        customRender: ({ record }) => {
+          return (
+            record.publishTime &&
+            h(Time, {
+              value: record.publishTime,
+              mode: 'datetime',
+            })
+          );
+        },
+      },{
+        title: '创建人',
+        width: 120,
+        dataIndex: 'createName',
+      },{
+        title: '创建时间',
+        width: 120,
+        dataIndex: 'createTime',
+        customRender: ({ record }) => {
+          return (
+            record.createTime &&
+            h(Time, {
+              value: record.createTime,
+              mode: 'datetime',
+            })
+          );
+        },
+      },{
+        title: '置顶',
+        dataIndex: 'isTop',
+        ifShow: getCheckPerm('sdk-top'),
+        width: 80,
+        customRender: ({ record }) => {
+          if (!Reflect.has(record, 'isTop')) {
+            record.pendingStatus = false;
+          }
+          return h(Switch, {
+            checked: record.isTop == 1 ? true : false,
+            checkedChildren: '是',
+            unCheckedChildren: '否',
+            disabled:record.status != 1,
+            loading: false,
+            onChange: async (checked: boolean) => {
+              record.pendingStatus = true;
+              const newStatus = checked?1:0;
+              await SpaceSdkTop({...record,isTop:newStatus});
+              Reflect.set(record, 'isTop', checked);
+              createMessage.success(t('common.optSuccess'));
+              reload()
+            },
+          });
+        },
+      },{
+        title: '发布状态',
+        dataIndex: 'status',
+        width: 80,
+        customRender: ({ record }) => {
+          return renderType(record.status)
+        },
+      },
+    ];
+    const [registerTimeTable, { reload }] = useTable({
+      api: SpaceSdkList,
+      title: '',
+      columns: sdkTableSchema,
+      useSearchForm: true,
+      formConfig: searchForm,
+      showTableSetting: true,
+      showIndexColumn:false,
+      rowKey: 'id',
+      fetchSetting: {
+        pageField: 'pageNum',
+        sizeField: 'pageSize',
+        listField: 'list',
+        totalField: 'total',
+      },
+      searchInfo: searchInfo,
+      actionColumn: {
+        width: 200,
+        title: '操作',
+        dataIndex: 'action',
+        slots: { customRender: 'action' },
+      },
+      canResize: true,
+    });
+    function tabChange(val: string) {
+      console.log('tabChange', val);
+      reload();
+    }
+    function renderType(type: number): string {
+      switch (type) {
+        case 0:
+          return t(`routes.product.statusType.0`);
+        case 1:
+          return t(`routes.product.statusType.1`);
+        default:
+          return t(`routes.product.statusType.0`);
+      }
+    }
+    async function handleDelete(record: Recordable) {
+      createConfirm({
+        iconType: 'warning',
+        title: () => h('span', t('sys.app.logoutTip')),
+        content: () => h('span', '确定要删除吗?'),
+        onOk: async () => {
+          await SpaceSdkDelete({ id: record.id });
+          createMessage.success(t('common.optSuccess'));
+          reload();
+        },
+      });
+    }
+    function handleOpen(record: Recordable) {
+      console.log('点击了启用', record);
+    }
+    async function handlePublish(record: Recordable) {
+      createConfirm({
+        iconType: 'warning',
+        title: () => h('span', t('sys.app.logoutTip')),
+        content: () => h('span', '是否确定发布该文件?发布后将在官网展示。'),
+        onOk: async () => {
+          await SpaceSdkOnline({ id: record.id, status:1 });//状态 0 - 未发布 1 -发布  2-下架
+          createMessage.success(t('common.optSuccess'));
+          reload();
+        },
+      });
+    }
+    async function handleOff(record: Recordable) {
+      createConfirm({
+        iconType: 'warning',
+        title: () => h('span', t('sys.app.logoutTip')),
+        content: () => h('span', '是否确定下架?下架后官网不再展示该SDK。'),
+        onOk: async () => {
+          await SpaceSdkOnline({ id: record.id, status:2 });//状态 0 - 未发布 1 -发布  2-下架
+          createMessage.success(t('common.optSuccess'));
+          reload();
+        },
+      });
+    }
+    function handleEdit(record: Recordable) {
+      console.log('record', record);
+      openEditModal(true, {
+        ...record,
+        type:searchInfo.platformType
+      });
+    }
+    
+    function getTypeCheckPerm(val){
+        let myType = searchInfo.platformType
+        return getCheckPerm(val) || getCheckPerm(`${val}-${myType}`)
+      }
+    return {
+      registerTimeTable,
+      handleDelete,
+      handleOpen,
+      handlePublish,
+      tabChange,
+      handleOff,
+      reload,
+      registerAddModal,
+      registerEditModal,
+      openAddModal,
+      handleEdit,
+      getTypeCheckPerm,
+      t,
+      searchInfo,
+    };
+  },
+});
+</script>
+<style lang="less" scoped>
+.desc-wrap-BasicTable {
+  background-color: #f0f2f5;
+  .vben-basic-table-form-container {
+    padding: 0;
+  }
+}
+</style>

+ 1 - 1
src/views/productOperation/livestream.vue

@@ -148,7 +148,7 @@
           },
         },
         {
-          title: '浏览量',
+          title: '观看次数',
           dataIndex: 'roomViewCount',
           sorter: true,
           width: 80,

+ 55 - 0
src/views/statistics/components/shuffle.vue

@@ -0,0 +1,55 @@
+<template>
+  <div class="shuffle">
+    <div class="top">
+      <div class="left">{{nameObj.name}}</div>
+      <div class="right">{{nameObj.value}}</div>
+    </div>
+    <div class="list">
+      <div class="item" v-for="(item, index) in list" :key="index">
+        <div class="left">{{item.dataKey}}</div>
+        <div class="right">{{item.dataCount}}</div>
+      </div>
+    </div>
+  </div>
+</template>
+<script lang="ts" setup>
+  import { ref } from 'vue';
+  const props = defineProps({
+    loading: Boolean,
+    list: Array,
+    nameObj: Object,
+  });
+</script>
+<style lang="less" scoped>
+  .fiexDiv {
+    display: flex;
+    justify-content: space-between;
+  }
+  .shuffle {
+    margin-top: 15px;
+    line-height: 30px;
+    .top {
+      .fiexDiv;
+    }
+    .list {
+
+      .item {
+        .fiexDiv;
+        &:nth-child(1){
+                color: #f14646;
+            }
+        &:nth-child(2){
+                color: plum;
+            }
+        &:nth-child(3){
+                color: #e1a925;
+            }
+      }
+
+    }
+    .right {
+      min-width: 30px;
+      text-align: left;
+    }
+  }
+</style>

+ 125 - 0
src/views/statistics/livestream/VisitAnalysis.vue

@@ -0,0 +1,125 @@
+<template>
+  <div ref="chartRef" :style="{ height, width }"></div>
+</template>
+<script lang="ts">
+  import { basicProps } from './props';
+  // import { dateUtil } from '/@/utils/dateUtil';
+</script>
+<script lang="ts" setup>
+  import { ref, Ref, watch } from 'vue';
+  import { useECharts } from '/@/hooks/web/useECharts';
+
+  const props = defineProps({
+    ...basicProps,
+  });
+  const bulletChatAmountsData = ref<number[]>([]);
+  const userAmountData = ref<number[]>([]);
+  const yixStringData = ref<string[]>([]);
+  const maxSize = ref(0);
+
+  const chartRef = ref<HTMLDivElement | null>(null);
+  const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>);
+
+  function handleSetOptions() {
+    setOptions({
+      tooltip: {
+        trigger: 'axis',
+        axisPointer: {
+          lineStyle: {
+            width: 1,
+            color: '#019680',
+          },
+        },
+      },
+      legend: {
+        orient: 'horizontal',
+        bottom: 0,
+      },
+      xAxis: {
+        type: 'category',
+        data: yixStringData.value,
+        splitLine: {
+          show: true,
+          lineStyle: {
+            width: 1,
+            type: 'solid',
+            color: 'rgba(226,226,226,0.5)',
+          },
+        },
+        axisTick: {
+          show: false,
+        },
+      },
+      yAxis: [
+        {
+          type: 'value',
+          max: maxSize.value,
+          splitNumber: 4,
+          axisTick: {
+            show: false,
+          },
+          splitArea: {
+            show: true,
+            areaStyle: {
+              color: ['rgba(255,255,255,0.2)', 'rgba(226,226,226,0.2)'],
+            },
+          },
+        },
+      ],
+      grid: { left: '2%', right: '2%', top: '2%', bottom: '10%', containLabel: true },
+      series: [
+        {
+          smooth: true,
+          data: userAmountData.value,
+          type: 'line',
+          areaStyle: {},
+          name: '留言总人数',
+          itemStyle: {
+            color: '#5ab1ef',
+          },
+        },
+        {
+          smooth: true,
+          data: bulletChatAmountsData.value,
+          type: 'line',
+          name: '留言总数',
+          areaStyle: {},
+          itemStyle: {
+            color: '#019680',
+          },
+        },
+      ],
+    });
+  }
+
+  watch(
+    () => [props.bulletChatAmounts, props.userAmount],
+    ([data1, data2]) => {
+      bulletChatAmountsData.value = data1.reduce<number[]>(
+        (prev: number[], current) => prev.concat(Number(current.dataCount)),
+        [],
+      );
+
+      yixStringData.value = data1.reduce<string[]>(
+        (prev: string[], current) => prev.concat(current.dataKey),
+        [],
+      );
+      userAmountData.value = data2.reduce<number[]>(
+        (prev: number[], current) => prev.concat(Number(current.dataCount)),
+        [],
+      );
+      console.log(yixStringData.value,data1, data2)
+      console.log('viewStatics-data', bulletChatAmountsData.value);
+      const maxNumber = Math.max(...bulletChatAmountsData.value.concat(userAmountData.value));
+      const pow = Math.pow(10, maxNumber.toString().length - 1);
+      maxSize.value = maxNumber > 10 ? Math.floor(maxNumber / 10) * 10 + pow * 2 : 10;
+      console.log('maxSize', maxSize.value);
+      handleSetOptions();
+      // viewStaticsData.value = data;
+    },
+    {
+      immediate: true,
+      deep: true,
+    },
+  );
+</script>

+ 97 - 0
src/views/statistics/livestream/VisitAnalysisBar.vue

@@ -0,0 +1,97 @@
+<template>
+  <div ref="chartRef" :style="{ height, width }"></div>
+</template>
+<script lang="ts">
+  import { basicProps } from './props';
+  // import { dateUtil } from '/@/utils/dateUtil';
+</script>
+<script lang="ts" setup>
+  import { ref, Ref, watch } from 'vue';
+  // import type { dataItemType } from './props';
+  import { useECharts } from '/@/hooks/web/useECharts';
+  const props = defineProps({
+    ...basicProps,
+  });
+
+  const viewStaticsData = ref<number[]>([]);
+  const shareStaticsData = ref<number[]>([]);
+  const yixStringData = ref<string[]>([]);
+  const maxSize = ref(0);
+  const chartRef = ref<HTMLDivElement | null>(null);
+  const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>);
+
+  function handlesetOptions() {
+    setOptions({
+      tooltip: {
+        trigger: 'axis',
+        axisPointer: {
+          lineStyle: {
+            width: 1,
+            color: '#019680',
+          },
+        },
+      },
+      legend: {
+        orient: 'horizontal',
+        bottom: 0,
+      },
+      grid: { left: '2%', right: '2%', top: '2%', bottom: '10%', containLabel: true },
+      xAxis: {
+        type: 'category',
+        // data: [...new Array(30)].map((_item, index) => `${index + 1}日`),
+        data: yixStringData.value,
+      },
+      yAxis: {
+        type: 'value',
+        max: maxSize.value,
+        splitNumber: 4,
+      },
+      series: [
+        {
+          data: viewStaticsData.value,
+          type: 'bar',
+          itemStyle: { color: '#38a0ff' },
+          barMaxWidth: 80,
+          name: '用户浏览量',
+        },
+        {
+          data: shareStaticsData.value,
+          type: 'bar',
+          itemStyle: { color: '#4cca73' },
+          barMaxWidth: 80,
+          name: '用户分享数',
+        },
+      ],
+    });
+  }
+  // props.viewStatics,
+  watch(
+    () => [props.viewStatics, props.shareStatics],
+    ([data1, data2]) => {
+      console.log('viewStatics-data', data1);
+      viewStaticsData.value = data1.reduce<number[]>(
+        (prev: number[], current) => prev.concat(Number(current.dataCount)),
+        [],
+      );
+
+      yixStringData.value = data1.reduce<string[]>(
+        (prev: string[], current) => prev.concat(current.dataKey),
+        [],
+      );
+      shareStaticsData.value = data2.reduce<number[]>(
+        (prev: number[], current) => prev.concat(Number(current.dataCount)),
+        [],
+      );
+
+      const maxNumber = Math.max(...viewStaticsData.value.concat(shareStaticsData.value));
+      const pow = Math.pow(10, maxNumber.toString().length - 1);
+      maxSize.value = maxNumber > 10 ? Math.floor(maxNumber / 10) * 10 + pow * 2 : 10;
+      console.log('maxSize', maxSize.value);
+      handlesetOptions();
+    },
+    {
+      immediate: true,
+      deep: true,
+    },
+  );
+</script>

+ 197 - 0
src/views/statistics/livestream/index.vue

@@ -0,0 +1,197 @@
+<template>
+  <div class="p-4">
+    <!-- <GrowCard :loading="loading" :list="growCardList" class="enter-y" /> -->
+    <div class="md:flex !my-4 enter-y">
+      <div class="md:w-1/3 w-full !md:mt-0 !mt-4 !md:mr-4 topItem">
+        <div class="deadline">截止至昨日24时</div>
+        <div class="preview">
+          <div class="item">
+            <div class="title">带看房间</div>
+            <div class="value">{{topData.roomCount}}</div>
+          </div>
+          <div class="item">
+            <div class="title">访问总人数</div>
+            <div class="value">{{topData.visitManCount}}</div>
+          </div>
+        </div>
+        <div class="preview">
+          <div class="item">
+            <div class="title">访问总次数</div>
+            <div class="value">{{topData.visitCount}}</div>
+          </div>
+          <div class="item">
+            <div class="title">分享总次数</div>
+            <div class="value">{{topData.shareCount}}</div>
+          </div>
+        </div>
+      </div>
+      <div class="md:w-1/3 w-full !md:mt-0 !mt-4 !md:mr-4 topItem">
+        <div class="deadline">截止至昨日24时</div>
+        <div class="shuffleTitle">观看次数排行(前5)</div>
+        <shuffle :nameObj="{ name: '名称', value: '留言次数' }" :list="topData.takeLookList" />
+      </div>
+      <div class="md:w-1/3 w-full !md:mt-0 !mt-4 !md:mr-4 topItem">
+        <div class="deadline">截止至昨日24时</div>
+        <div class="shuffleTitle">留言数排行(前5)</div>
+        <shuffle :nameObj="{ name: '微信昵称', value: '留言次数' }" :list="topData.danmakuList" />
+      </div>
+    </div>
+    <div class="!my-4 enter-y">
+      <Card title="房间使用情况">
+        <template #extra>
+          <div class="condition">
+            <div class="selct" style="display: inline-block">
+              <!-- <span style="margin-right:15px"></span> -->
+              <Input
+                v-model:value="roomTitle"
+                style="width: 150px; margin-right: 30px"
+                placeholder="请输入房间名称"
+              ></Input>
+            </div>
+            <div class="selct" style="display: inline-block; margin-right: 15px">
+              <RangePicker
+                v-model:value="selectTime"
+                @calendarChange="calendarPriceRangeChange"
+                valueFormat="YYYY-MM-DD"
+                :disabled-date="disabledDate"
+                format="YYYY-MM-DD"
+              />
+            </div>
+            <a-button @click="getData(true)" type="primary">确认</a-button>
+            <a-button @click="export" type="primary">导出</a-button>
+          </div>
+        </template>
+        <div class="echartList">
+          <div class="echars">
+            <VisitAnalysisBar
+              :loading="loading"
+              :viewStatics="viewStaticsData"
+              :shareStatics="shareStaticsData"
+            />
+          </div>
+          <div class="echars">
+            <VisitAnalysis
+              :loading="loading"
+              :bulletChatAmounts="bulletChatAmountsData"
+              :userAmount="userAmountData"
+            />
+          </div>
+        </div>
+      </Card>
+    </div>
+  </div>
+</template>
+<script lang="ts" setup>
+  import { onMounted, reactive, ref } from 'vue';
+  import shuffle from '../components/shuffle.vue';
+  import { Input, DatePicker, Card } from 'ant-design-vue';
+  import VisitAnalysisBar from './VisitAnalysisBar.vue';
+  import VisitAnalysis from './VisitAnalysis.vue';
+  const { RangePicker } = DatePicker;
+  import type { Dayjs } from 'dayjs';
+  import dayjs from 'dayjs';
+  import { roomData, takeLookTop5, roomVisitData } from '/@/api/statistics/index';
+  type RangeValue = [Dayjs, Dayjs];
+  const loading = ref(true);
+  const roomTitle = ref('');
+  const selectPriceDate = ref(dayjs().subtract(6, 'month').format('YYYY-MM-DD'));
+  const selectTime = ref<RangeValue>([
+    dayjs().subtract(1, 'month').format('YYYY-MM-DD'),
+    dayjs().format('YYYY-MM-DD'),
+  ]);
+  const viewStaticsData = ref([]);
+  const shareStaticsData = ref([]);
+  const bulletChatAmountsData = ref([]);
+  const userAmountData = ref([]);
+  const topData = reactive({
+    roomCount: 0,
+    visitManCount: 0,
+    visitCount: 0,
+    shareCount: 0,
+    takeLookList: [],
+    danmakuList: [],
+  });
+  async function getData(type) {
+    try {
+      loading.value = true;
+      if (type) {
+        let { roomCount, shareCount, visitCount, visitManCount } = await roomData({});
+        topData.roomCount = roomCount
+        topData.shareCount = shareCount
+        topData.visitCount = visitCount
+        topData.visitManCount = visitManCount
+        let { takeLookList, danmakuList } = await takeLookTop5({});
+        topData.takeLookList = takeLookList
+        topData.danmakuList = danmakuList
+      }
+      const {userMsgCountList,userMsgManList,userShareList,userVisitList} = await roomVisitData({
+        roomTitle: roomTitle.value,
+        timeList: selectTime.value,
+      });
+      viewStaticsData.value = userMsgManList
+      shareStaticsData.value = userShareList
+      bulletChatAmountsData.value = userMsgManList
+      userAmountData.value = userMsgCountList
+      console.log('visitData', visitData);
+      loading.value = false;
+    } catch (error) {
+      loading.value = false;
+    }
+  }
+
+  const disabledDate = (current: Dayjs) => {
+    if (selectPriceDate.value) {
+      return (
+        current > dayjs(selectPriceDate.value).add(2, 'year') ||
+        current < dayjs(selectPriceDate.value).subtract(2, 'year')
+      );
+    } else {
+      return false;
+    }
+  };
+  function calendarPriceRangeChange(date) {
+    selectPriceDate.value = date[0];
+  }
+  onMounted(() => {
+    getData(true);
+  });
+</script>
+<style lang="less" scoped>
+  .topItem {
+    background: #fff;
+    padding: 30px 20px;
+    width: 100%;
+    .preview {
+      display: flex;
+      width: 100%;
+      align-items: center;
+
+      .item {
+        width: 50%;
+        flex: 1;
+        text-align: center;
+        line-height: 30px;
+        font-size: 24px;
+        .title {
+          font-size: 14px;
+          padding: 20px 0 10px 0;
+        }
+      }
+    }
+    .preview {
+    }
+    .deadline {
+      text-align: right;
+    }
+    .shuffleTitle{
+      font-size:14px;
+    }
+  }
+  .echartList {
+    .echars {
+      height: 280px;
+      width: 100%;
+      margin-bottom: 30px;
+    }
+  }
+</style>

+ 37 - 0
src/views/statistics/livestream/props.ts

@@ -0,0 +1,37 @@
+import { PropType } from 'vue';
+
+export interface BasicProps {
+  width: string;
+  height: string;
+}
+export type dataItemType = {
+  date: string;
+  amount: string | number;
+};
+
+export const basicProps = {
+  width: {
+    type: String as PropType<string>,
+    default: '100%',
+  },
+  height: {
+    type: String as PropType<string>,
+    default: '280px',
+  },
+  viewStatics: {
+    type: Array as PropType<Array<dataItemType>>,
+    default: [],
+  },
+  shareStatics: {
+    type: Array as PropType<Array<dataItemType>>,
+    default: [],
+  },
+  bulletChatAmounts: {
+    type: Array as PropType<Array<dataItemType>>,
+    default: [],
+  },
+  userAmount: {
+    type: Array as PropType<Array<dataItemType>>,
+    default: [],
+  },
+};