浏览代码

Merge branch 'feature/1.2.0' into dev

gemercheung 1 年之前
父节点
当前提交
f66313ca35

+ 5 - 2
package.json

@@ -23,6 +23,8 @@
   "dependencies": {
     "@ant-design/icons-vue": "^6.1.0",
     "@types/node": "^18.7.18",
+    "@wangeditor/editor": "^5.1.23",
+    "@wangeditor/editor-for-vue": "^5.1.12",
     "ant-design-vue": "3.3.0-beta.3",
     "axios": "^0.27.2",
     "dayjs": "^1.11.6",
@@ -38,7 +40,7 @@
     "vue-i18n": "^9.2.2",
     "vue-router": "4",
     "vue3-autocounter": "^1.0.6",
-    "vue3-otp-input": "^0.3.8"
+    "vue3-otp-input": "0.4.2"
   },
   "devDependencies": {
     "@types/lodash-es": "^4.17.6",
@@ -46,6 +48,7 @@
     "@typescript-eslint/eslint-plugin": "^5.36.2",
     "@typescript-eslint/parser": "^5.36.2",
     "@vitejs/plugin-vue": "^3.1.0",
+    "@vue-macros/volar": "^0.12.2",
     "chalk": "^5.0.1",
     "eslint": "^8.23.1",
     "eslint-config-prettier": "^8.5.0",
@@ -58,7 +61,7 @@
     "typescript": "^4.6.4",
     "unplugin-auto-import": "^0.11.2",
     "unplugin-vue-components": "^0.22.7",
-    "unplugin-vue-define-options": "^0.11.2",
+    "unplugin-vue-define-options": "^1.3.11",
     "vite": "^3.1.0",
     "vite-plugin-chunk-split": "^0.4.7",
     "vue-eslint-parser": "^9.1.0",

文件差异内容过多而无法显示
+ 1317 - 513
pnpm-lock.yaml


+ 2 - 0
src/api/constant.ts

@@ -19,4 +19,6 @@ export const DEL_ROOM = '/takelook/roomDelete'
 export const ROOM_ACTION_CHECKER = '/takelook/checkCanDel'
 export const GET_ROOM_LEADER_MINI_CODE = '/takelook/roomGetHostCode'
 export const GET_ROOM_MINI_CODE = '/takelook/roomGetShareCode'
+
+export const BASE_UPLOAD = '/takelook/upload/file'
 // /takelook/roomGetHostCode

+ 23 - 7
src/api/room.ts

@@ -11,6 +11,7 @@ import axios from './instance'
 import type { PageResult, Scene } from './'
 import type { SScene } from './scene'
 import type { Dayjs } from 'dayjs'
+import { head } from 'lodash-es'
 
 export type SRoom = {
   roomId: number
@@ -22,13 +23,18 @@ export type SRoom = {
   roomCoverUrl: string
   roomViewCount: number
   createTime: string
-  useTimeList?: Dayjs[],
+  useTimeList?: [Dayjs, Dayjs],
   visitPassword?: string,
   maxMan?: number,
   isRoamMode?: boolean,
   freeRoamLock?: number
   takeLookLock?: number
   userObjList?: AuthUser[]
+  location?: string
+  image?: string[]
+  head?: string
+  gps?: string
+  video?: string[]
 }
 
 export interface RoomScene {
@@ -39,7 +45,7 @@ export interface RoomScene {
 
 export type AuthUser = {
   userName: string
-  useTimeList?: Dayjs[]
+  useTimeList?: [Dayjs, Dayjs]
 }
 export interface Room {
   id: number
@@ -51,13 +57,17 @@ export interface Room {
   leaderName: string
   roomStatus?: number
   hostStatus?: number
-  useTimeList?: Dayjs[],
+  useTimeList?: [Dayjs, Dayjs],
   visitPassword?: string,
   maxMan?: number,
   isRoamMode?: boolean,
   freeRoamLock?: number
   takeLookLock?: number
-  userObjList?: AuthUser[]
+  userObjList?: AuthUser[],
+  scenesAlbum?: string[]
+  scenesVideo?: string
+  gps?: string
+  head?: string
 }
 
 export type Rooms = Room[]
@@ -73,9 +83,12 @@ const serverToLocal = (sroom: SRoom): Room => ({
   useTimeList: sroom.useTimeList,
   maxMan: sroom.maxMan,
   roomStatus: sroom.roomStatus,
-  hostStatus:sroom.hostStatus,
+  hostStatus: sroom.hostStatus,
   visitPassword: sroom.visitPassword,
-  userObjList: sroom.userObjList
+  userObjList: sroom.userObjList,
+  scenesAlbum: sroom.image,
+  // scenesAvatar: sroom.head,
+  scenesVideo: sroom.video?.length ? sroom.video[0] : '',
 })
 const localToServer = (room: Room): SRoom => ({
   roomId: room.id,
@@ -92,7 +105,10 @@ const localToServer = (room: Room): SRoom => ({
   visitPassword: room.visitPassword,
   takeLookLock: room.takeLookLock || 0,
   freeRoamLock: room.freeRoamLock || 0,
-  userObjList: room.userObjList
+  userObjList: room.userObjList,
+  image: room.scenesAlbum,
+  head: room.head,
+  video: [room.scenesVideo || ''],
 })
 
 export const fetchRomms = async (status: number = 1): Promise<Rooms> => {

+ 3 - 0
src/components.d.ts

@@ -12,10 +12,12 @@ declare module '@vue/runtime-core' {
     ACard: typeof import('ant-design-vue/es')['Card']
     ACol: typeof import('ant-design-vue/es')['Col']
     AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider']
+    ADrawer: typeof import('ant-design-vue/es')['Drawer']
     ADropdown: typeof import('ant-design-vue/es')['Dropdown']
     AEmpty: typeof import('ant-design-vue/es')['Empty']
     AForm: typeof import('ant-design-vue/es')['Form']
     AFormItem: typeof import('ant-design-vue/es')['FormItem']
+    AImage: typeof import('ant-design-vue/es')['Image']
     AInput: typeof import('ant-design-vue/es')['Input']
     AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
     ALayout: typeof import('ant-design-vue/es')['Layout']
@@ -36,6 +38,7 @@ declare module '@vue/runtime-core' {
     ATabPane: typeof import('ant-design-vue/es')['TabPane']
     ATabs: typeof import('ant-design-vue/es')['Tabs']
     ATextarea: typeof import('ant-design-vue/es')['Textarea']
+    AUpload: typeof import('ant-design-vue/es')['Upload']
     BarChartOutlined: typeof import('@ant-design/icons-vue')['BarChartOutlined']
     CloseOutlined: typeof import('@ant-design/icons-vue')['CloseOutlined']
     DataList: typeof import('./components/data-list/index.vue')['default']

+ 2 - 2
src/helper/index.ts

@@ -4,7 +4,7 @@ import { genMount } from '@/shared'
 const { app } = userApp()
 export const mount = genMount(app)
 
-export const renderCompoent = <P>(comp: ComponentConstructor<P>, props: P) => {
+export const renderComponent = <P>(comp: ComponentConstructor<P>, props: P) => {
   const element = document.createElement('div')
   const { destroy } = mount(comp, { props, element })
   return () => {
@@ -26,5 +26,5 @@ export const renderModal = <P>(
     }
   } as P
 
-  const destroy = renderCompoent(comp, props)
+  const destroy = renderComponent(comp, props)
 }

+ 46 - 0
src/hook/useScript.ts

@@ -0,0 +1,46 @@
+import { onMounted, onUnmounted, ref } from 'vue';
+
+interface ScriptOptions {
+  src: string;
+}
+
+export function useScript(opts: ScriptOptions) {
+  const isLoading = ref(false);
+  const error = ref(false);
+  const success = ref(false);
+  let script: HTMLScriptElement;
+
+  const promise = new Promise((resolve, reject) => {
+    onMounted(() => {
+      script = document.createElement('script');
+      script.type = 'text/javascript';
+      script.onload = function () {
+        isLoading.value = false;
+        success.value = true;
+        error.value = false;
+        resolve('');
+      };
+
+      script.onerror = function (err) {
+        isLoading.value = false;
+        success.value = false;
+        error.value = true;
+        reject(err);
+      };
+
+      script.src = opts.src;
+      document.head.appendChild(script);
+    });
+  });
+
+  onUnmounted(() => {
+    script && script.remove();
+  });
+
+  return {
+    isLoading,
+    error,
+    success,
+    toPromise: () => promise,
+  };
+}

+ 1 - 0
src/locales/lang/en/room.ts

@@ -58,5 +58,6 @@ export default {
   usingTime: "Time of Use",
   usingTime1: "Duratation",
   usingTime2: "Create",
+  deletedRoom: 'Deleted Studios',
 
 }

+ 3 - 3
src/locales/lang/zh/room.ts

@@ -26,7 +26,7 @@ export default {
     addLeastScene: '至少添加一个场景',
     advanceConfig: '高级配置'
   },
-  roomOnfired: '房间直播中, 请先关闭直播再修改 !',
+  roomOnfired: '房间直播中, 请先关闭直播再删除 !',
   copyLink: '复制链接',
   linkCopySuccess: '链接复制成功!',
   deletedScenes: '删除场景',
@@ -56,6 +56,6 @@ export default {
   usingTime: "使用时间",
   usingTime1: "时间段",
   usingTime2: "创建时间",
-  nodata: "您还没有房间,请先创建房间~"
-  
+  nodata: "您还没有房间,请先创建房间~",
+  deletedRoom: '删除房间',
 }

+ 4 - 2
src/store/modules/room.ts

@@ -15,13 +15,15 @@ import {
 } from '@/api'
 
 import { Room as SRoom, Scenes } from '@/api'
+import { Dayjs } from 'dayjs'
 
 export type { RoomScene } from '@/api'
 export type Rooms = Room[]
 export type Room = SRoom & {
   miniCode?: string
   leaderMiniCode?: string
-  scenes: Scenes
+  scenes: Scenes,
+  location?: string
 }
 
 export interface ShareLinkType {
@@ -47,7 +49,7 @@ export const createRoom = (room: Partial<Room>): Room => {
     desc: '',
     cover: '',
     scenes: [],
-    useTimeList: [],
+    useTimeList: ['' as unknown as Dayjs, '' as unknown as Dayjs],
     maxMan: 10,
     ...room,
 

+ 223 - 0
src/views/room/edit-room/album-list.vue

@@ -0,0 +1,223 @@
+<template>
+  <a-list :grid="{ gutter: 20, column: 3 }" :data-source="current">
+    <template
+      #renderItem="{
+        item,
+        index
+      }: {
+        item: string | typeof addMarked,
+        index: number
+      }"
+    >
+      <a-list-item class="scene-item">
+        <div v-if="item === addMarked" class="add-album">
+          <a-upload
+            v-model:file-list="albumFile"
+            name="file"
+            accept=".png,.jpg,.jpeg"
+            :show-upload-list="false"
+            :action="baseURL + '/takelook/upload/file'"
+            :multiple="true"
+            :before-upload="handleBeforeUpload"
+            @change="handleABlumChange"
+          >
+            <div class="add-item-icon">
+              <a-button shape="circle" class="button" type="primary">
+                <plus-outlined class="add-room-icon" />
+              </a-button>
+            </div>
+          </a-upload>
+        </div>
+
+        <div v-else class="scene-sign">
+          <a-image v-if="item" :src="item" :height="118" />
+          <span class="delete-scene" @click="deleteAblum(index)">
+            <close-outlined class="delete-scene-icon" />
+          </span>
+        </div>
+      </a-list-item>
+    </template>
+  </a-list>
+</template>
+
+<script lang="ts" setup>
+import { computed, ref, watch, unref } from 'vue'
+import { Modal, message } from 'ant-design-vue'
+import type { UploadChangeParam } from 'ant-design-vue'
+import { baseURL } from '@/env'
+import { useI18n } from '@/hook/useI18n'
+import { watchEffect } from 'vue'
+
+export interface FileItem {
+  uid: string
+  response:
+    | {
+        ok?: number
+        data: string
+      }
+    | undefined
+  name?: string
+  status?: string
+  url?: string
+  type?: string
+  size?: number
+  originFileObj?: any
+}
+
+const { t } = useI18n()
+
+defineOptions({
+  name: 'RoomSceneList'
+})
+const props = defineProps<{ data: string[] }>()
+
+const addMarked = Symbol('add-album')
+const albumFile = ref<any[]>([])
+const albumFileExist = ref<any[]>([])
+
+const current = computed(() => [addMarked, ...albumFileExist.value])
+
+const deleteAblum = (index: number) => {
+  console.log('index', index)
+  if (index - 1 > -1) {
+    albumFileExist.value.splice(index - 1, 1)
+  }
+  const syncData = albumFileExist.value.length ? albumFileExist.value : []
+  emit('syncList', syncData)
+}
+const emit = defineEmits(['syncList'])
+
+watchEffect(() => {
+  if (props.data?.length) {
+    albumFileExist.value = props.data
+  }
+})
+
+const handleABlumChange = (info: UploadChangeParam) => {
+  if (info.file.status === 'done') {
+    const { code, data } = info.file.response
+    if (code === 0) {
+      albumFileExist.value.push(data)
+    }
+
+    const syncData = albumFileExist.value.length ? albumFileExist.value : []
+    emit('syncList', syncData)
+  } else if (info.file.status === 'error') {
+    message.error(`${info.file.name} file upload failed.`)
+  }
+}
+
+const handleBeforeUpload = (file: any) => {
+  const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png'
+  if (!isJpgOrPng) {
+    message.error('上传类型有误!')
+  }
+  const isLt5M = file.size / 1024 / 1024 < 5
+  if (!isLt5M) {
+    message.error('上传超出大小!')
+  }
+  return isJpgOrPng && isLt5M
+}
+</script>
+
+<style lang="scss" scoped>
+.add-album {
+  border: 1px solid #ebedf0;
+  border-radius: 4px;
+  height: 100%;
+  width: 100%;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+
+  .button {
+    width: 40px;
+    height: 40px;
+    line-height: 34px;
+    background: linear-gradient(144deg, #00aefb 0%, #0076f6 100%);
+  }
+
+  .add-room-icon {
+    font-size: 18px;
+  }
+}
+
+.scene-sign {
+  height: 100%;
+  border-radius: 4px;
+  overflow: hidden;
+  position: relative;
+  img {
+    width: 100%;
+    height: 100%;
+    display: block;
+    object-fit: cover;
+  }
+  .title {
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    height: 24px;
+    background: rgba(0, 0, 0, 0.5);
+    padding: 5px;
+    font-size: 12px;
+    color: #fff;
+    margin: 0;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+
+  .delete-scene {
+    z-index: 2;
+    position: absolute;
+    right: 0;
+    top: 0;
+    width: 52px;
+    height: 52px;
+    background-color: rgba(0, 0, 0, 0.5);
+    color: #fa5555;
+    font-size: 14px;
+    border-radius: 50%;
+    display: flex;
+    align-items: flex-end;
+    transform: translate(100%, -100%);
+    transition: all 0.3s ease;
+    opacity: 0;
+    cursor: pointer;
+
+    .delete-scene-icon {
+      padding: 10px;
+    }
+  }
+
+  &:hover .delete-scene {
+    transform: translate(50%, -50%);
+    opacity: 1;
+  }
+  .status-cover {
+    z-index: 1;
+    position: absolute;
+    left: 0;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    background-color: rgba(0, 0, 0, 0.5);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    p {
+      color: #fff;
+    }
+  }
+}
+</style>
+
+<style>
+.scene-item {
+  height: 120px;
+}
+</style>

+ 168 - 0
src/views/room/edit-room/avatar.vue

@@ -0,0 +1,168 @@
+<template>
+  <a-upload
+    v-model:file-list="avatarFile"
+    name="file"
+    accept=".png,.jpg,.jpeg"
+    :show-upload-list="false"
+    :action="baseURL + '/takelook/upload/file'"
+    :max-count="1"
+    class="uploader"
+    :headers="{
+      authorization: 'authorization-text'
+    }"
+    :disabled="avatarFile.length > 0"
+    @change="handleAvatarChange"
+  >
+    <div
+      class="add-item-icon scene-sign"
+      v-if="avatarFile.length > 0 && avatarFile[0].response"
+    >
+      <a-image class="avatar" :src="avatarFile[0].response.data" alt="avatar" />
+      <span class="delete-scene" @click="deleteAvatar(avatarFile[0])">
+        <close-outlined class="delete-scene-icon" />
+      </span>
+    </div>
+    <div class="add-item-icon" v-else>
+      <a-button shape="circle" class="button" type="primary">
+        <plus-outlined class="add-room-icon" />
+      </a-button>
+    </div>
+  </a-upload>
+</template>
+<script lang="ts" setup>
+import { ref } from 'vue'
+import { message, type UploadChangeParam } from 'ant-design-vue'
+import type { FileItem } from './album-list.vue'
+import { baseURL } from '@/env'
+import { watchEffect } from 'vue'
+const emit = defineEmits(['sync'])
+const avatarFile = ref<any[]>([])
+const props = defineProps<{ value: string | undefined }>()
+
+watchEffect(() => {
+  if (props.value?.length) {
+    const tempData = {} as FileItem
+    tempData.uid = `data-0`
+    tempData.response = {
+      data: props.value,
+      ok: 0
+    }
+    if (!avatarFile.value?.length) {
+      console.log('mapper', tempData)
+      avatarFile.value = [tempData]
+    }
+  } else {
+    avatarFile.value = []
+  }
+})
+
+const handleAvatarChange = (info: UploadChangeParam) => {
+  if (info.file.status === 'done') {
+    const { code, data } = info.file.response
+    if (code === 0) {
+      emit('sync', data || '')
+    }
+  } else if (info.file.status === 'error') {
+    message.error(`${info.file.name} file upload failed.`)
+  }
+}
+const deleteAvatar = (item: any) => {
+  const index = avatarFile.value.findIndex(i => i.uid === item.uid)
+  if (index > -1) {
+    avatarFile.value.splice(index, 1)
+  }
+  emit('sync', '')
+}
+</script>
+
+<style lang="scss" scoped>
+.add-item-icon {
+  width: 120px;
+  height: 120px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  border: 1px solid #ebedf0;
+  overflow: hidden;
+  .avatar {
+    max-width: 100%;
+    // border-radius: 50%;
+  }
+  .button {
+    background: linear-gradient(144deg, #00aefb 0%, #0076f6 100%);
+    width: 40px;
+    height: 40px;
+  }
+}
+.scene-sign {
+  border-radius: 4px;
+  overflow: hidden;
+  position: relative;
+  z-index: 10;
+  img {
+    width: 100%;
+    height: 100%;
+    display: block;
+    object-fit: cover;
+  }
+  .title {
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    height: 24px;
+    background: rgba(0, 0, 0, 0.5);
+    padding: 5px;
+    font-size: 12px;
+    color: #fff;
+    margin: 0;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+
+  .delete-scene {
+    z-index: 2;
+    position: absolute;
+    right: 0;
+    top: 0;
+    width: 52px;
+    height: 52px;
+    background-color: rgba(0, 0, 0, 0.5);
+    color: #fa5555;
+    font-size: 14px;
+    border-radius: 50%;
+    display: flex;
+    align-items: flex-end;
+    transform: translate(100%, -100%);
+    transition: all 0.3s ease;
+    opacity: 0;
+    cursor: pointer;
+
+    .delete-scene-icon {
+      padding: 10px;
+    }
+  }
+
+  &:hover .delete-scene {
+    transform: translate(50%, -50%);
+    opacity: 1;
+  }
+  .status-cover {
+    z-index: 1;
+    position: absolute;
+    left: 0;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    background-color: rgba(0, 0, 0, 0.5);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    p {
+      color: #fff;
+    }
+  }
+}
+</style>

+ 386 - 48
src/views/room/edit-room/index.vue

@@ -1,7 +1,8 @@
 <template>
   <a-config-provider :locale="getAntdLocale">
-    <a-modal
+    <a-drawer
       :visible="visible"
+      placement="right"
       :title="
         isRoomEnd
           ? t('room.endRoomTitle')
@@ -9,27 +10,30 @@
           ? t('room.createRoom')
           : t('room.editRoom')
       "
-      :after-close="onCancel"
-      width="912px"
-      :style="{
-        top: '10px',
-        minWidth: '912px'
-      }"
-      @cancel="visible = false"
+      @close="onCancel"
+      width="476px"
     >
       <template #footer>
-        <a-button class="action-bottom" size="middle" @click="visible = false">
-          {{ t('base.cancel') }}
-        </a-button>
-        <a-button
-          v-if="!isRoomEnd"
-          class="action-bottom"
-          type="primary"
-          size="middle"
-          @click="saveRoom"
-        >
-          {{ t('base.save') }}
-        </a-button>
+        <div class="footer">
+          <a-button
+            class="action-bottom"
+            size="middle"
+            @click="visible = false"
+          >
+            {{ t('base.cancel') }}
+          </a-button>
+          <a-button
+            v-if="!isRoomEnd"
+            class="action-bottom"
+            type="primary"
+            size="middle"
+            :disabled="isLive"
+            @click="saveRoom"
+          >
+            {{ t('base.save') }}
+          </a-button>
+        </div>
+
         <!-- <a-button
         v-if="room"
         class="action-bottom"
@@ -70,7 +74,38 @@
               :disabled="isRoomEnd"
             />
           </a-form-item>
+
+          <!-- <a-form-item label="位置" name="map">
+            <a-input
+              v-model:value.trim="current.location"
+              placeholder=""
+              :maxlength="50"
+              show-count
+              id="location"
+              :disabled="isRoomEnd"
+            />
+            <div style="margin: 10px 0"></div>
+            <div ref="mapRef" style="width: 100%; height: 194px"></div>
+          </a-form-item> -->
+
           <a-form-item :label="t('room.form.desc')" name="desc">
+            <!-- <QuillEditor style="{minHeight: '400px';}" theme="snow" v-model:content="current.desc"  /> -->
+
+            <!-- <div style="border: 1px solid #ccc">
+              <Editor
+                style="height: 150px; overflow-y: hidden"
+                v-model="current.desc"
+                :defaultConfig="{
+                  maxLength: 200,
+                  placeholder: t('room.form.descplaceHolder')
+                }"
+                mode="simple"
+                @onCreated="handleEditorCreated"
+                @onMaxLength="handleEditorOnMaxLength"
+                @onBlur="handleEditorOnBlur"
+                @customPaste="handleEditorCustomPaste"
+              />
+            </div> -->
             <a-textarea
               v-model:value="current.desc"
               :placeholder="t('room.form.descplaceHolder')"
@@ -80,6 +115,7 @@
             />
           </a-form-item>
           <!-- <h4>{{ t('room.form.host') }}</h4> -->
+
           <a-form-item
             :label="t('room.form.nickname')"
             name="leaderName"
@@ -96,7 +132,24 @@
               :disabled="isRoomEnd"
             />
           </a-form-item>
-          <!-- <h4>{{ t('room.form.selectScene') }}</h4> -->
+
+          <a-form-item label="头像" name="head">
+            <EditAvatar
+              v-model:value.trim="current.head"
+              @sync="handleAvatarSync"
+            />
+          </a-form-item>
+          <!-- 相册 -->
+          <!-- <a-form-item label="相册" name="title">
+            <EditAlbum
+              :data="current.scenesAlbum || []"
+              @sync-list="handleAlbumSync"
+            />
+          </a-form-item>
+          <a-form-item label="视频" name="title">
+            <EditVideo :data="current.scenesVideo" @sync="handleVideoSync" />
+          </a-form-item> -->
+
           <a-form-item
             :label="t('room.form.addScene')"
             class="select-scene"
@@ -104,6 +157,7 @@
             style="margin-bottom: 2px"
           >
           </a-form-item>
+
           <EditScenes
             :class="{ disabled: isRoomEnd }"
             :scenes="current.scenes"
@@ -120,12 +174,11 @@
           >
             <a-range-picker
               :show-time="{ format: 'HH:mm' }"
-              format="YYYY-MM-DD HH:mm:ss"
-              style="width: 80%"
+              format="YYYY-MM-DD HH:mm"
+              style="width: 85%"
               :disabled="isRoomEnd"
               :disabledDate="disabledDate"
               :disabledTime="disabledTime"
-              @calendarChange="onRangeChange"
               v-model:value="current.useTimeList"
             />
           </a-form-item>
@@ -136,17 +189,19 @@
             style="margin-bottom: 2px"
           >
             <v-otp-input
+              ref="otpInput"
               v-model:value="current.visitPassword"
               class="otp-container"
               input-classes="otp-input"
               input-type="number"
               separator="-"
               :num-inputs="4"
-              :should-auto-focus="false"
+              :should-auto-focus="true"
+              :should-focus-order="true"
               :class="{ disabled: isRoomEnd }"
-              :is-input-num="true"
               :conditionalClass="['one', 'two', 'three', 'four']"
               :placeholder="['-', '-', '-', '-']"
+              @on-change="handlePsOnChange"
             />
           </a-form-item>
           <a-form-item name="scenes" style="margin-bottom: 2px">
@@ -214,7 +269,7 @@
           </a-form-item>
         </a-form>
       </div>
-    </a-modal>
+    </a-drawer>
 
     <a-modal
       v-model:visible="authvisible"
@@ -262,7 +317,8 @@ import {
   reactive,
   computed,
   unref,
-  createVNode
+  createVNode,
+  shallowRef
 } from 'vue'
 
 import { createRoom, useRoomStore } from '@/store/modules/room'
@@ -270,6 +326,9 @@ import { props } from './props'
 import { message, Modal } from 'ant-design-vue'
 import { mainURL } from '@/env'
 import EditScenes from './scene-list.vue'
+import EditAlbum from './album-list.vue'
+import EditAvatar from './avatar.vue'
+import EditVideo from './video.vue'
 import unScenePng from '@/assets/images/un-scene.png'
 import VOtpInput from 'vue3-otp-input'
 import type { Scene } from '@/store/modules/scene'
@@ -278,9 +337,31 @@ import { useI18n } from '@/hook/useI18n'
 import { useLocale } from '@/locales/useLocale'
 import dayjs, { Dayjs } from 'dayjs'
 import duration from 'dayjs/plugin/duration'
-import { addAuthUser } from '@/api'
+import utc from 'dayjs/plugin/utc'
+import localizedFormat from 'dayjs/plugin/localizedFormat'
+import customParseFormat from 'dayjs/plugin/customParseFormat'
+import timezone from 'dayjs/plugin/timezone'
+import { addAuthUser, checkRoomEditOrDel } from '@/api'
 import { ExclamationCircleOutlined } from '@ant-design/icons-vue'
+import { useScript } from '@/hook/useScript'
+import type { UploadChangeParam } from 'ant-design-vue'
+import {
+  watchEffect,
+  getCurrentInstance,
+  onMounted,
+  onBeforeUnmount
+} from 'vue'
+import { nextTick } from 'vue'
+
+// import '@wangeditor/editor/dist/css/style.css' // 引入 css
+// import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
+
 dayjs.extend(duration)
+dayjs.extend(utc)
+dayjs.extend(localizedFormat)
+dayjs.extend(customParseFormat)
+dayjs.extend(timezone)
+
 // const titleValidator = ref({
 //   validator: (_, value) =>
 //     !value.includes(' ')
@@ -290,45 +371,81 @@ dayjs.extend(duration)
 
 interface AuthUserFormState {
   userName: string
-  useTimeList?: Dayjs[]
+  useTimeList?: [Dayjs, Dayjs]
+  // useTimeList?: [string, string]
 }
 
 export default defineComponent({
   name: 'EditRoom',
-  components: { EditScenes, VOtpInput },
+  components: {
+    EditScenes,
+    VOtpInput,
+    EditAlbum,
+    EditAvatar,
+    EditVideo
+    // Editor,
+    // Toolbar
+  },
   props,
   setup(props) {
     const visible = ref(true)
     const authvisible = ref(false)
-
+    const mapRef = ref<HTMLDivElement | null>(null)
     const authformState: UnwrapRef<AuthUserFormState> = reactive({
       userName: ''
-      // useTimeList: []
     })
     const { getAntdLocale } = useLocale()
     const roomStore = useRoomStore()
     const isRoomEnd = computed(() => props.room?.roomStatus === 2)
     const { getLocale } = useLocale()
     const { t } = useI18n()
+    const {
+      proxy: { $forceUpdate }
+    }: any = getCurrentInstance()
+    const avatarFile = ref<any[]>([])
+    const isLive = ref(false)
     const returnLocale = computed(() => {
       if (unref(getLocale).includes('zh')) {
         return 'zh'
       }
       return unref(getLocale)
     })
+    const editorRef = shallowRef()
 
     const otpInput = ref('')
     const formRef = ref<FormInstance>()
-    const current = reactive(createRoom(props.room || {}))
-    if (current.useTimeList?.length) {
-      current.useTimeList = current.useTimeList?.map(i => dayjs(i))
-    }
+    const current = reactive({
+      ...createRoom(props.room || {})
+    })
+
+    watchEffect(async () => {
+      if(typeof current.head === 'undefined'){
+        current.head = ''
+      }
+      if (current.useTimeList?.length) {
+        // debugger
+        current.useTimeList = current.useTimeList
+          .filter(i => i)
+          .map(i => dayjs(i)) as unknown as [Dayjs, Dayjs]
+      }
+      if (props.room?.id) {
+        const res = await checkRoomEditOrDel(props.room)
+        if (props.room.hostStatus === 1 || !res) {
+          console.error('当前场景已在直播中或不能删除。。。。。')
+          isLive.value = true
+        }
+      }
+    })
+
     if (!current.visitPassword) {
       current.visitPassword = ''
     }
     if (!current.userObjList) {
       current.userObjList = []
     }
+    if (!current.userObjList) {
+      current.userObjList = []
+    }
 
     const deleteScene = (scene: Scene) => {
       const index = current.scenes.indexOf(scene)
@@ -361,6 +478,7 @@ export default defineComponent({
         current.visitPassword.length > 0 &&
         current.visitPassword.length < 4
       ) {
+        // otpInput.value?.clearInput();
         return message.error(t('room.passwordError'))
       } else {
         current.takeLookLock = 1
@@ -374,13 +492,14 @@ export default defineComponent({
       props.onSave && props.onSave(current)
       visible.value = false
     }
+
     // const startSync = () => {
     //   window.open(roomStore.getShareUrl({
 
     //   }))
     // }
 
-    const handleNickRegex = async (_: any, value: string) => {
+    const handleNickRegex = async (_: any, value: string): Promise<void> => {
       console.log('value', value)
       const regex = new RegExp(
         '^([\u4E00-\uFA29]|[\uE7C7-\uE7F3]|[a-zA-Z0-9_]){1,15}$'
@@ -388,7 +507,7 @@ export default defineComponent({
       if (value?.length && !regex.test(value)) {
         return Promise.reject(t('room.nickNameRegrexError'))
       }
-      return Promise.resolve('')
+      return Promise.resolve()
     }
 
     const handleAuthConfirm = async () => {
@@ -437,7 +556,7 @@ export default defineComponent({
       }
     }
 
-    const onRangeChange = (range: Dayjs[]) => {
+    const onRangeChange = (range: [Dayjs, Dayjs]): void => {
       const select = range.map((i, index) => {
         if (i) {
           const diff = i.diff(dayjs(), 'minutes')
@@ -451,8 +570,202 @@ export default defineComponent({
       })
       console.log('onRangeChange', event)
     }
+    const onCancel = () => {
+      visible.value = false
+    }
+    ;(window as any)._AMapSecurityConfig = {
+      securityJsCode: 'cbac8ddaf1e7346784b24f10cae245a3'
+    }
+    const A_MAP_URL =
+      'https://webapi.amap.com/maps?v=2.0&key=5a2d384532ae531bf99bd8487c4f03d2'
+    const UI = 'https://webapi.amap.com/ui/1.0/main.js?v=1.0.11'
+    // const { toPromise: toUIPromise } = useScript({ src: UI })
+    const { toPromise } = useScript({ src: A_MAP_URL })
+
+    const initMap = async () => {
+      await toPromise()
+      // await toUIPromise();
+
+      console.log('mapRef', unref(mapRef))
+      const AMap = (window as any).AMap
+      // console.log('AMap', AMap)
+      // ** 有数据时默认位置
+      // const center = [113.549574, 22.215372]
+
+      // var icon = new AMap.Icon({
+      //   size: new AMap.Size(40, 50), // 图标尺寸
+      //   image: '//webapi.amap.com/theme/v1.3/images/newpc/way_btn2.png', // Icon的图像
+      //   imageOffset: new AMap.Pixel(0, -60), // 图像相对展示区域的偏移量,适于雪碧图等
+      //   imageSize: new AMap.Size(40, 50) // 根据所设置的大小拉伸或压缩图片
+      // })
+      var marker = new AMap.Marker({
+        position: new AMap.LngLat(113.549574, 22.215372),
+        title: current.location,
+        zoom: 13
+      })
+
+      const map = new AMap.Map(unref(mapRef), {
+        zoom: 15,
+        resizeEnable: true
+        // center: center
+        // resizeEnable: true
+        // viewMode: '3D',
+        // resizeEnable: true
+        // floorControl: true,
+        // showIndoorMap: true
+      })
+      map.add(marker)
+
+      AMap.plugin(
+        [
+          'AMap.Scale',
+          'AMap.Geocoder',
+          'AMap.Toolbar',
+          'AMap.AutoComplete',
+          'AMap.PlaceSearch',
+          'AMap.Geolocation'
+        ],
+        function () {
+          const autoOptions = {
+            // city: '珠海',
+            input: 'location'
+          }
+          const autocomplete = new AMap.Autocomplete(autoOptions)
+          const placeSearch = new AMap.PlaceSearch({
+            // city: '珠海', // 默认城市,一定要有,不然没有放大效果
+            map: map // 地图,选中会有放大功能,绑定上面创建的地图即可
+          })
+          AMap.Event.addListener(autocomplete, 'select', function (e: any) {
+            placeSearch.search(e.poi.name)
+            // console.log('location', e.poi.location) // 获取选中的的地址的经纬度
+            console.log('select', e.poi) // 获取选中的的地址的经纬度
+            if (e.poi.name) {
+              current.location = e.poi.name
+            }
+          })
+          AMap.Event.addListener(autocomplete, 'click', function (e: any) {
+            console.log('click', e)
+          })
+          // var geolocation = new AMap.Geolocation({
+          //   // 是否使用高精度定位,默认:true
+          //   enableHighAccuracy: true,
+          //   // 设置定位超时时间,默认:无穷大
+          //   timeout: 10000,
+          //   // 定位按钮的停靠位置的偏移量,默认:Pixel(10, 20)
+          //   buttonOffset: new AMap.Pixel(10, 20),
+          //   //  定位成功后调整地图视野范围使定位位置及精度范围视野内可见,默认:false
+          //   zoomToAccuracy: true,
+          //   //  定位按钮的排放位置,  RB表示右下
+          //   buttonPosition: 'RB'
+          // })
+
+          // geolocation.getCurrentPosition()
+          // AMap.Event.addListener(geolocation, 'complete', onComplete)
+          // AMap.Event.addListener(geolocation, 'error', onError)
+
+          // function onComplete(data: any) {
+          //   console.log('onComplete-data', data)
+          // }
+
+          function onError(data: any) {
+            console.log('onError-data', data)
+            // 定位出错
+          }
+        }
+      )
+    }
+
+    onMounted(() => {
+      // initMap()
+    })
+    onBeforeUnmount(() => {
+      const editor = editorRef.value
+      if (editor == null) return
+      editor.destroy()
+    })
+    const handleAvatarChange = (info: UploadChangeParam) => {
+      // if (info.file.status !== 'uploading') {
+      //   console.log(info.file, info.fileList)
+      // }
+      if (info.file.status === 'done') {
+        console.log('info.file', info.file)
+        if (info.file.response) {
+          const { code, data } = info.file.response
+          if (code === 0) {
+            current.head = data
+          }
+        }
+        // message.success(`${info.file.name} file uploaded successfully`)
+      } else if (info.file.status === 'error') {
+        message.error(`${info.file.name} file upload failed.`)
+      }
+    }
+
+    const handleAlbumSync = (list: any[]) => {
+      current.scenesAlbum = list
+      // console.log('handleAlbumSync', list)
+    }
+    const handleVideoSync = (link: any) => {
+      // debugger
+      current.scenesVideo = link
+    }
+    const handleAvatarSync = (avatarURL: string) => {
+      if (unref(formRef)) {
+        current.head = avatarURL || ''
+        console.log('avatarURL-更新后', unref(formRef)?.getFieldsValue())
+      }
+    }
+    const handleEditorCreated = (editor: any) => {
+      editorRef.value = editor // 记录 editor 实例,重要!
+      // const toolbar = DomEditor.getToolbar(editor)
+      // const curToolbarConfig = toolbar.getConfig()
+      // console.log(curToolbarConfig.toolbarKeys) // 当前菜单排序和分组
+    }
+    const handleEditorOnMaxLength = (editor: any) => {
+      const text = editor.getText()
+      if (text.length > 200) {
+        const html = text
+          .substring(0, 200)
+          .split(/\n/)
+          .map((line: string) => `<p>${line}</p>`)
+          .join('\n')
+        current.desc = html
+      }
+    }
+    const handleEditorOnBlur = (editor: any) => {
+      const text = editor.getText()
+
+      if (text.length > 200) {
+        debugger
+        //   const html = text
+        //     .substring(0, 200)
+        //     .split(/\n/)
+        //     .map((line: string) => `<p>${line}</p>`)
+        //     .join('\n')
+        //   current.desc = html
+      }
+    }
+    const handleEditorCustomPaste = (editor: any) => {
+      // debugger
+      // const text = editor.getText()
+      // if (text.length > 200) {
+      //   const html = text
+      //     .substring(0, 200)
+      //     .split(/\n/)
+      //     .map((line: string) => `<p>${line}</p>`)
+      //     .join('\n')
+      //   current.desc = html
+      // }
+    }
+    const handlePsOnChange = (value: string) => {
+      console.log('OTP changed: ', value)
+    }
 
     return {
+      isLive,
+      handleEditorOnMaxLength,
+      handleEditorCustomPaste,
+      handleEditorOnBlur,
       handleNickRegex,
       returnLocale,
       t,
@@ -477,9 +790,17 @@ export default defineComponent({
       createVNode,
       Modal,
       ExclamationCircleOutlined,
-      onRangeChange
-      // authTargetUser,
-      // authTargetUserTime
+      onRangeChange,
+      onCancel,
+      mapRef,
+      avatarFile,
+      handleAvatarChange,
+      handleAlbumSync,
+      handleAvatarSync,
+      handleVideoSync,
+      editorRef,
+      handleEditorCreated,
+      handlePsOnChange
     }
   }
 })
@@ -488,12 +809,16 @@ export default defineComponent({
 <style lang="scss" scoped>
 .edit-room-layout {
   display: flex;
-  max-height: 700px;
-  overflow: hidden;
+  // max-height: 700px;
+  min-height: 100%;
+  width: 100%;
+  // overflow: hidden;
 
   .info {
-    max-height: 700px;
+    // max-height: 700px;
+    width: 100%;
     overflow-y: scroll;
+    min-height: 100%;
     overflow-x: hidden;
   }
 }
@@ -506,6 +831,7 @@ export default defineComponent({
   background: #f7f8fa;
   display: flex;
   align-items: center;
+  display: none;
 
   iframe {
     width: 320px;
@@ -549,11 +875,23 @@ export default defineComponent({
     opacity: 0.5;
   }
 }
+.footer {
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-end;
+  align-items: center;
+  > button {
+    &:first-child {
+      margin-right: 10px;
+    }
+  }
+}
 </style>
 
 <style lang="scss">
 .edit-room-layout {
-  min-width: 864px;
+  // min-width: 864px;
+
   .ant-form-item {
     margin-bottom: 16px;
   }
@@ -579,7 +917,7 @@ export default defineComponent({
       position: absolute;
       bottom: 4px;
       right: 8px;
-      margin: 0;
+      margin: 0 !important;
     }
   }
 

+ 7 - 3
src/views/room/edit-room/scene-list.vue

@@ -1,5 +1,5 @@
 <template>
-  <a-list :grid="{ gutter: 20, column: 4 }" :data-source="current">
+  <a-list :grid="{ gutter: 20, column: 3 }" :data-source="current">
     <template #renderItem="{ item }: { item: Scene | typeof addMarked }">
       <a-list-item class="scene-item">
         <div v-if="item === addMarked" class="add-scene" @click="selectScenes">
@@ -15,7 +15,10 @@
           </span>
           <p class="title">{{ item.title }}</p>
 
-          <div v-if="(Number(item.status) !== SceneStatus.SUCCESS)" class="status-cover">
+          <div
+            v-if="Number(item.status) !== SceneStatus.SUCCESS"
+            class="status-cover"
+          >
             <p>{{ sceneStatusDesc(item.status) }}</p>
           </div>
         </div>
@@ -41,7 +44,7 @@ import { useI18n } from '@/hook/useI18n'
 
 const { t } = useI18n()
 
-defineOptions<{ name: 'RoomSceneList' }>()
+defineOptions({ name: 'RoomSceneList' })
 const props = defineProps<{ scenes: Scene[] }>()
 const emit = defineEmits<{
   (e: 'delete', scene: Scene): void
@@ -102,6 +105,7 @@ const selectScenes = () => {
   .button {
     width: 40px;
     height: 40px;
+    line-height: 34px;
     background: linear-gradient(144deg, #00aefb 0%, #0076f6 100%);
   }
 

+ 201 - 0
src/views/room/edit-room/video.vue

@@ -0,0 +1,201 @@
+<template>
+  <a-upload
+    v-model:file-list="videoFile"
+    name="file"
+    :show-upload-list="false"
+    accept=".mp4"
+    :action="baseURL + '/takelook/upload/file'"
+    :max-count="1"
+    class="uploader"
+    :headers="{
+      authorization: 'authorization-text'
+    }"
+    :disabled="videoFile.length > 0"
+    @change="handleAvatarChange"
+    @preview="handleVideoPreview"
+  >
+    <div
+      class="add-item-icon scene-sign"
+      v-if="videoFile.length > 0 && videoFile[0].response"
+      @click.stop="handleVideoPreview"
+    >
+      <video class="avatar" :src="videoFile[0].response.data" alt="video" />
+      <span class="delete-scene" @click.stop="deleteAvatar(videoFile[0])">
+        <close-outlined class="delete-scene-icon" />
+      </span>
+    </div>
+    <div class="add-item-icon" v-else>
+      <a-button shape="circle" class="button" type="primary">
+        <plus-outlined class="add-room-icon" />
+      </a-button>
+    </div>
+    <a-modal
+      :visible="showVideoPreview"
+      :footer="null"
+      width="800px"
+      @cancel="showVideoPreview = false"
+    >
+      <video controls id="previewVideo"></video>
+    </a-modal>
+  </a-upload>
+</template>
+<script lang="ts" setup>
+import { ref } from 'vue'
+import { message, type UploadChangeParam } from 'ant-design-vue'
+import type { FileItem } from './album-list.vue'
+import { baseURL } from '@/env'
+import { watchEffect } from 'vue'
+import { nextTick } from 'vue'
+const emit = defineEmits(['sync'])
+const videoFile = ref<any[]>([])
+const props = defineProps<{ data: string | undefined }>()
+const showVideoPreview = ref(false)
+const previewSrc = ref('')
+
+watchEffect(() => {
+  if (props.data?.length) {
+    const tempData = {} as FileItem
+    tempData.uid = `data-0`
+    tempData.response = {
+      data: props.data,
+      ok: 0
+    }
+    if (!videoFile.value?.length) {
+      console.log('mapper', tempData)
+      videoFile.value = [tempData]
+    }
+  } else {
+    videoFile.value = []
+  }
+})
+
+const handleAvatarChange = (info: UploadChangeParam) => {
+  if (info.file.status === 'done') {
+    const { code, data } = info.file.response
+    if (code === 0) {
+      console.log('syncVideo', data)
+      previewSrc.value = data
+      emit('sync', data || '')
+    }
+  } else if (info.file.status === 'error') {
+    message.error(`${info.file.name} file upload failed.`)
+  }
+}
+const deleteAvatar = (item: any) => {
+  const index = videoFile.value.findIndex(i => i.uid === item.uid)
+  if (index > -1) {
+    videoFile.value.splice(index, 1)
+  }
+  emit('sync', '')
+}
+const handleVideoPreview = () => {
+  showVideoPreview.value = true
+  nextTick(() => {
+    const video = document.getElementById('previewVideo')
+    if (video) {
+      // debugger
+      const src = previewSrc.value || videoFile.value[0].response.data
+      console.log('src', src)
+      ;(video as any).src = src
+    }
+  })
+}
+</script>
+
+<style lang="scss" scoped>
+.add-item-icon {
+  width: 120px;
+  height: 120px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  border: 1px solid #ebedf0;
+  overflow: hidden;
+  .avatar {
+    max-width: 100%;
+    // border-radius: 50%;
+  }
+  .button {
+    background: linear-gradient(144deg, #00aefb 0%, #0076f6 100%);
+    width: 40px;
+    height: 40px;
+  }
+}
+.scene-sign {
+  border-radius: 4px;
+  overflow: hidden;
+  position: relative;
+  z-index: 10;
+  img {
+    width: 100%;
+    height: 100%;
+    display: block;
+    object-fit: cover;
+  }
+  .title {
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    height: 24px;
+    background: rgba(0, 0, 0, 0.5);
+    padding: 5px;
+    font-size: 12px;
+    color: #fff;
+    margin: 0;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+
+  .delete-scene {
+    z-index: 2;
+    position: absolute;
+    right: 0;
+    top: 0;
+    width: 52px;
+    height: 52px;
+    background-color: rgba(0, 0, 0, 0.5);
+    color: #fa5555;
+    font-size: 14px;
+    border-radius: 50%;
+    display: flex;
+    align-items: flex-end;
+    transform: translate(100%, -100%);
+    transition: all 0.3s ease;
+    opacity: 0;
+    cursor: pointer;
+
+    .delete-scene-icon {
+      padding: 10px;
+    }
+  }
+
+  &:hover .delete-scene {
+    transform: translate(50%, -50%);
+    opacity: 1;
+  }
+  .status-cover {
+    z-index: 1;
+    position: absolute;
+    left: 0;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    background-color: rgba(0, 0, 0, 0.5);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    p {
+      color: #fff;
+    }
+  }
+}
+</style>
+<style lang="scss">
+#previewVideo {
+  width: 100%;
+  min-height: 500px;
+}
+</style>

+ 7 - 9
src/views/room/list.vue

@@ -165,7 +165,7 @@ const deleteRoom = async (room: Room) => {
 
   Modal.confirm({
     content: t('room.deletedScenesWaring'),
-    title: t('room.deletedScenes'),
+    title: t('room.deletedRoom'),
     width: '400px',
     okText: t('base.delete'),
     icon: null,
@@ -249,16 +249,11 @@ const webSyncRoom = async (room: Room) => {
 
 const editRoom = async (room?: Room) => {
   if (room) {
-    const res = await checkRoomEditOrDel(room)
-    if (!res) {
-      message.error(t('room.roomOnfired'))
-      return
-    }
-    const scenes = await roomStore.setRoomScenes(room)
-    // if (scenes.hostStatus  === 1) {
+    // if (!res) {
     //   message.error(t('room.roomOnfired'))
     //   return
     // }
+    const scenes = await roomStore.setRoomScenes(room)
     room = Object.assign({}, room, scenes)
   }
   // debugger
@@ -266,7 +261,10 @@ const editRoom = async (room?: Room) => {
     room,
     async onSave(actionRoom) {
       if (room) {
-        await roomStore.update(actionRoom)
+        const res = await checkRoomEditOrDel(room)
+        if (res) {
+          await roomStore.update(actionRoom)
+        }
       } else {
         await roomStore.insert(actionRoom)
       }

+ 3 - 1
src/views/room/modal/mini-sync.vue

@@ -17,7 +17,9 @@
 <script lang="ts" setup>
 import { ref } from 'vue'
 
-defineOptions<{ name: 'MiniSync' }>()
+defineOptions({
+  name: 'MiniSync'
+})
 defineProps<{
   title: string
   miniCode: string

+ 10 - 4
src/views/scene/list.vue

@@ -55,10 +55,13 @@ import { useSceneStore } from '@/store/modules/scene'
 import DataList from '@/components/data-list/index.vue'
 import type { Scene } from '@/store/modules/scene'
 import { useI18n } from '@/hook/useI18n'
+import { TableColumnsType } from 'ant-design-vue'
 
 const { t } = useI18n()
 
-defineOptions<{ name: 'SceneList' }>()
+defineOptions({
+  name: 'SceneList'
+})
 
 type SceneKey = Scene['num']
 
@@ -78,16 +81,19 @@ const filterScenes = computed(() =>
 
 sceneStore.fetch()
 
-const sceneColumns = [
+const sceneColumns: TableColumnsType = [
   {
     title: t('room.form.scene'),
     dataIndex: 'cover',
-    key: 'cover'
+    key: 'cover',
+    width: 100
   },
   {
     title: t('room.form.name'),
     dataIndex: 'title',
-    key: 'title'
+    key: 'title',
+    ellipsis: true,
+    width: 300
   },
   {
     title: t('room.form.time'),

+ 11 - 12
src/views/statistic/tab/tab1.vue

@@ -83,7 +83,7 @@
             </li>
           </ul>
           <div class="topic-list-empty" v-else>
-             <a-empty :image="simpleImage" />
+            <a-empty :image="simpleImage" />
           </div>
         </a-card>
       </a-col>
@@ -97,7 +97,7 @@
             </li>
           </ul>
           <div class="topic-list-empty" v-else>
-             <a-empty :image="simpleImage" />
+            <a-empty :image="simpleImage" />
           </div>
         </a-card>
       </a-col>
@@ -141,7 +141,7 @@
           <!-- userVisitList?.length -->
           <div id="chart-2" class="chart" v-if="userVisitList?.length"></div>
           <div class="chart-empty" v-else>
-             <a-empty :image="simpleImage" />
+            <a-empty :image="simpleImage" />
           </div>
         </a-card>
       </a-col>
@@ -151,10 +151,9 @@
         <a-card title="">
           <div id="chart-3" class="chart" v-if="userMsgManList?.length"></div>
           <div class="chart-empty" v-else>
-             <a-empty :image="simpleImage" />
+            <a-empty :image="simpleImage" />
           </div>
         </a-card>
-        
       </a-col>
     </a-row>
   </div>
@@ -175,13 +174,13 @@ import Vue3Autocounter from 'vue3-autocounter'
 import * as echarts from 'echarts'
 import type { ECharts } from 'echarts'
 import { useStatisticStore } from '@/store/modules/statistic'
-import { Empty } from 'ant-design-vue';
+import { Empty } from 'ant-design-vue'
 const statisticStore = useStatisticStore()
 const { t } = useI18n()
 const takeLookList = computed(() => statisticStore.top5.takeLookList)
 const danmakuList = computed(() => statisticStore.top5.danmakuList)
 const heroStatus = computed(() => statisticStore.roomData)
-const simpleImage  = Empty.PRESENTED_IMAGE_SIMPLE;
+const simpleImage = Empty.PRESENTED_IMAGE_SIMPLE
 let onlineTimeChart: ECharts
 let roomUseVisitChart: ECharts
 let roomMsgChart: ECharts
@@ -192,11 +191,11 @@ const props = defineProps<{
 
 interface FormState {
   roomTitle: string
-  userTime: string[]
+  userTime: [string, string]
 }
 const formState: UnwrapRef<FormState> = reactive({
   roomTitle: '',
-  userTime: []
+  userTime: ['', '']
 })
 
 const onelineTimeCount = computed(() => statisticStore.onlineTimeCount)
@@ -486,7 +485,7 @@ watch(
       initTab()
     } else {
       formState.roomTitle = ''
-      formState.userTime = []
+      formState.userTime = ['', '']
     }
   },
   {
@@ -515,7 +514,7 @@ const handleResize = () => {
 const handleFinish = async () => {
   await statisticStore.fetchRoomVisitChart({
     timeList: formState.userTime
-      ? formState.userTime.map(item => item.toString())
+      ? formState.userTime.filter(i => i).map(item => item.toString())
       : [],
     roomTitle: formState.roomTitle?.length ? formState.roomTitle : ''
   })
@@ -609,7 +608,7 @@ const handleFinish = async () => {
     }
   }
 }
-.chart-empty{
+.chart-empty {
   min-height: 500px;
   display: flex;
   justify-content: center;

+ 22 - 16
src/views/statistic/tab/tab2.vue

@@ -73,7 +73,7 @@
 
 <script lang="ts" setup>
 import { computed, onMounted, UnwrapRef, watch, reactive, unref } from 'vue'
-import { TableColumnProps } from 'ant-design-vue'
+import { TableColumnProps, TablePaginationConfig } from 'ant-design-vue'
 import { useStatisticStore } from '@/store/modules/statistic'
 import { useI18n } from '@/hook/useI18n'
 
@@ -84,11 +84,11 @@ const props = defineProps<{
 
 const statisticStore = useStatisticStore()
 // const total = computed(() => statisticStore.allRoom.total)
-const pagination = computed(() => {
+const pagination = computed<TablePaginationConfig>(() => {
   return {
     current: statisticStore.allRoom.pageNum,
     total: statisticStore.allRoom.total,
-    pageSize: statisticStore.allRoom.pageSize, //每页中显示10条数据
+    pageSize: statisticStore.allRoom.pageSize,
     showSizeChanger: true,
     onChange: (current: number, page: any) => {
       console.log('page', current, page)
@@ -96,19 +96,19 @@ const pagination = computed(() => {
       pagination.value.pageSize = page
       fetchList()
     },
-    pageSizeOptions: ['10', '20', '50', '100'], //每页中显示的数据
+    pageSizeOptions: ['10', '20', '50', '100'],
     showTotal: (total: string) => t('statistic.pageCount').replace('%N%', total) //分页中显示总的数据
-  }
+  } as unknown as TablePaginationConfig
 })
 
 const roomList = computed(() => statisticStore.allRoom.list)
 interface FormState {
   roomTitle: string
-  userTime: string[]
+  userTime: [string, string]
 }
 const formState: UnwrapRef<FormState> = reactive({
   roomTitle: '',
-  userTime: []
+  userTime: ['', '']
 })
 
 const columns: TableColumnProps[] = [
@@ -118,7 +118,8 @@ const columns: TableColumnProps[] = [
   // },
   {
     title: t('room.roomTitle'),
-    dataIndex: 'roomTitle'
+    dataIndex: 'roomTitle',
+    width: 130
   },
   {
     title: t('statistic.relatedScenes'),
@@ -126,23 +127,28 @@ const columns: TableColumnProps[] = [
   },
   {
     title: t('statistic.duration'),
-    dataIndex: 'lookTime'
+    dataIndex: 'lookTime',
+    width: 100
   },
   {
     title: t('statistic.createTime'),
-    dataIndex: 'createTime'
+    dataIndex: 'createTime',
+    width: 130
   },
   {
     title: t('statistic.status'),
-    dataIndex: 'roomStatus'
+    dataIndex: 'roomStatus',
+    width: 80
   },
   {
     title: t('statistic.watch'),
-    dataIndex: 'lookManCount'
+    dataIndex: 'lookManCount',
+    width: 70
   },
   {
     title: t('statistic.share'),
-    dataIndex: 'shareCount'
+    dataIndex: 'shareCount',
+    width: 70
   }
 ]
 
@@ -151,7 +157,7 @@ const fetchList = () => {
     pageNum: pagination.value.current,
     pageSize: pagination.value.pageSize,
     timeList: formState.userTime
-      ? formState.userTime.map(item => item.toString())
+      ? formState.userTime.filter(i => i).map(item => item.toString())
       : [],
     roomTitle: formState.roomTitle?.length ? formState.roomTitle : ''
   })
@@ -161,7 +167,7 @@ const exportList = () => {
     pageNum: pagination.value.current,
     pageSize: pagination.value.pageSize,
     timeList: formState.userTime
-      ? formState.userTime.map(item => item.toString())
+      ? formState.userTime.filter(i => i).map(item => item.toString())
       : [],
     roomTitle: formState.roomTitle?.length ? formState.roomTitle : ''
   })
@@ -176,7 +182,7 @@ watch(
       fetchList()
     } else {
       formState.roomTitle = ''
-      formState.userTime = []
+      formState.userTime = ['', '']
     }
   },
   {

+ 8 - 8
src/views/statistic/tab/tab3.vue

@@ -80,7 +80,7 @@
 
 <script lang="ts" setup>
 import { computed, onMounted, UnwrapRef, watch, unref, reactive } from 'vue'
-import { TableColumnProps } from 'ant-design-vue'
+import { TableColumnProps, TablePaginationConfig } from 'ant-design-vue'
 import { useStatisticStore } from '@/store/modules/statistic'
 import dayjs from 'dayjs'
 import { useI18n } from '@/hook/useI18n'
@@ -98,11 +98,11 @@ console.log('msgList', msgList)
 
 interface FormState {
   roomTitle: string
-  userTime: string[]
+  userTime: [string, string]
 }
 const formState: UnwrapRef<FormState> = reactive({
   roomTitle: '',
-  userTime: []
+  userTime: ['', '']
 })
 const columns: TableColumnProps[] = [
   {
@@ -146,7 +146,7 @@ const columns: TableColumnProps[] = [
   }
 ]
 
-const pagination = computed(() => {
+const pagination = computed<TablePaginationConfig>(() => {
   return {
     current: statisticStore.roomMsg.pageNum,
     total: statisticStore.roomMsg.total,
@@ -160,7 +160,7 @@ const pagination = computed(() => {
     },
     pageSizeOptions: ['10', '20', '50', '100'], //每页中显示的数据
     showTotal: (total: string) => t('statistic.pageCount').replace('%N%', total) //分页中显示总的数据
-  }
+  } as unknown as TablePaginationConfig
 })
 
 const fetchList = () => {
@@ -172,7 +172,7 @@ const fetchList = () => {
       // startTime: formState.userTime?.length ? formState.userTime[0] : '',
       // endTime: formState.userTime?.length ? formState.userTime[1] : '',
       timeList: formState.userTime
-        ? formState.userTime.map(item => item.toString())
+        ? formState.userTime.filter(i => i).map(item => item.toString())
         : [],
       roomTitle: formState.roomTitle?.length ? formState.roomTitle : ''
     })
@@ -188,7 +188,7 @@ const exportList = () => {
       // startTime: formState.userTime?.length ? formState.userTime[0] : '',
       // endTime: formState.userTime?.length ? formState.userTime[1] : '',
       timeList: formState.userTime
-        ? formState.userTime.map(item => item.toString())
+        ? formState.userTime.filter(i => i).map(item => item.toString())
         : [],
       roomTitle: formState.roomTitle?.length ? formState.roomTitle : ''
     })
@@ -204,7 +204,7 @@ watch(
       fetchList()
     } else {
       formState.roomTitle = ''
-      formState.userTime = []
+      formState.userTime = ['', '']
     }
   },
   {

+ 8 - 1
tsconfig.json

@@ -32,5 +32,12 @@
     "src/**/*.vue",
     "types/**/*.ts"
   ],
-  "references": [{ "path": "./tsconfig.node.json" }]
+  "references": [{ "path": "./tsconfig.node.json" }],
+  "vueCompilerOptions": {
+    "target": 3, // or 2.7 for Vue 2
+    "plugins": [
+      "@vue-macros/volar/define-options"
+      // ...more feature
+    ]
+  }
 }

文件差异内容过多而无法显示
+ 3039 - 0
yarn.lock