Parcourir la source

Merge branch 'master' of https://e.coding.net/jiaolongcloud/link/things-ui

lvkun996 il y a 2 ans
Parent
commit
dea716d563

+ 1 - 1
src/api/iot/device.ts

@@ -275,7 +275,7 @@ export const getDeviceTopology = (modelId: string) => {
 
 /** 设备上下线 */
 export const getDeviceSession = (params: {deviceId: string, start: number, end: number}) => {
-  return request<IOT.API.DEVICE.Session[]>({
+  return request<IOT.API.DEVICE.Session>({
     url: '/deviceAnalysis/session',
     method: 'GET',
     params

+ 5 - 5
src/controller/common/common.ts

@@ -2,11 +2,11 @@ import { delFile, getTransport } from '@/api/common'
 import { TransportEnum } from '@/enum/common'
 export class CommonController {
   static dataTypeByKeyMap = new Map([
-    ['LONG', 'longValue'],
-    ['STRING', 'stringValue'],
-    ['JSON', 'jsonValue'],
-    ['DOUBLE', 'doubleValue'],
-    ['BOOLEAN', 'booleanValue']
+    ['LONG', 'value'],
+    ['STRING', 'value'],
+    ['JSON', 'value'],
+    ['DOUBLE', 'value'],
+    ['BOOLEAN', 'value']
   ])
 
   static async getTransport () {

+ 11 - 6
src/controller/iot/device.ts

@@ -7,6 +7,7 @@ import {
 import { DeviceMsgEnum, OtaStatusEnum } from '@/enum/common'
 import { message } from 'ant-design-vue'
 import dayjs from 'dayjs'
+import { number } from 'echarts'
 
 export class DeviceContriller {
   static deviceStatus = [
@@ -250,12 +251,16 @@ export class DeviceContriller {
     const { data, sum } = await getDeviceSession(params)
 
     return {
-      data: data.map(item => {
-        return {
-          ...item,
-          createAt: dayjs(item.createAt).format('YYYY/MM/DD')
-        }
-      }),
+      data: {
+        sessionEntities: data.sessionEntities.map(item => {
+          return {
+            ...item,
+            createAt: dayjs(item.createAt).format('YYYY/MM/DD')
+          }
+        }),
+        offlineNum: data.offlineNum,
+        onlineNum: data.onlineNum
+      },
       sum
     }
   }

+ 7 - 7
src/pages/Iot/dataServer/history.vue

@@ -3,8 +3,8 @@
   <a-row>
     <a-col>
       <a-space>
-        <span>产品: </span>
-        <a-select allowClear v-model:value="queryParams.modelId" style="width: 170px;" >
+
+        <a-select allowClear v-model:value="queryParams.modelId" placeholder="请选择产品" style="width: 170px;" >
           <a-select-option
             v-for="item in state.modelList"
             :key="item.id"
@@ -13,8 +13,8 @@
             {{item.modelLabel}}
           </a-select-option>
         </a-select>
-        <span>设备: </span>
-        <a-select allowClear v-model:value="queryParams.deviceId" style="width: 170px;" >
+
+        <a-select allowClear v-model:value="queryParams.deviceId" placeholder="请选择设备" style="width: 170px;" >
           <a-select-option
             v-for="item in state.deviceList"
             :key="item.id"
@@ -23,7 +23,7 @@
             {{item.deviceLabel}}
           </a-select-option>
         </a-select>
-        <span>触发时间: </span>
+
         <a-range-picker v-model:value="time" :format="['DD/MM/YYYY', 'DD/MM/YYYY']"  />
         <a-button type="primary"  @click="getHistoryPage">搜索</a-button>
       </a-space>
@@ -93,8 +93,8 @@ const queryParams = reactive({
   page: 1,
   pageSize: 10,
   total: 0,
-  deviceId: '',
-  modelId: '',
+  deviceId: null,
+  modelId: null,
   attributeKey: '',
   startTime: '',
   endTime: ''

+ 19 - 20
src/pages/Iot/devOps/msgTracking.vue

@@ -11,21 +11,20 @@
   <a-card style="margin-top: 20px" >
     <a-row  justify="space-between" v-if="state.dataSource.length === 0" >
       <a-col>
-        <a-form layout="inline" >
-          <a-form-item label="业务类型" >
-            <a-select allowClear v-model:value="queryParamsState.eventType" style="width: 170px;"  >
-            <a-select-option
-              v-for="item in eventTypeList"
-              :key="item.key"
-              :value="item.key"
-              placeholder="请选择业务类型"
-            >
-              {{item.name}}
-            </a-select-option>
-          </a-select>
-          </a-form-item>
-          <a-form-item label="消息状态" >
-            <a-select allowClear v-model:value="queryParamsState.success" style="width: 170px;" >
+        <a-row :gutter="[8, 8]" >
+          <a-col>
+            <a-select allowClear  placeholder="请选择业务类型" v-model:value="queryParamsState.eventType" style="width: 170px;"  >
+              <a-select-option
+                v-for="item in eventTypeList"
+                :key="item.key"
+                :value="item.key"
+              >
+                {{item.name}}
+              </a-select-option>
+            </a-select>
+          </a-col>
+          <a-col>
+            <a-select allowClear placeholder="请选择消息状态" v-model:value="queryParamsState.success" style="width: 170px;" >
               <a-select-option
                 v-for="item in successList"
                 :key="item.key"
@@ -34,11 +33,11 @@
                 {{item.name}}
               </a-select-option>
             </a-select>
-          </a-form-item>
-          <a-form-item label="时间区域" >
+          </a-col>
+          <a-col>
             <a-range-picker v-model:value="time" :format="['DD/MM/YYYY', 'DD/MM/YYYY']"  />
-          </a-form-item>
-        </a-form>
+          </a-col>
+        </a-row>
 
     </a-col>
     <a-col>
@@ -139,7 +138,7 @@ const queryParamsState = reactive({
   deviceId: deviceId,
   page: 1,
   pageSize: 10,
-  eventType: '',
+  eventType: null,
   success: false,
   total: 0,
   startTime: '',

+ 91 - 77
src/pages/Iot/device/analysis.vue

@@ -20,7 +20,7 @@
               <a-select
                 v-model:value="state.searchDeviceLabel"
                 show-search
-                placeholder="输入设备名"
+                placeholder="输入设备名检索设备"
                 :default-active-first-option="false"
                 style="width: 170px"
                 :show-arrow="false"
@@ -28,13 +28,14 @@
                 :not-found-content="null"
                 :options="state.dataSource"
                 @search="getDeviceLabel"
+                @change="selectDevice"
               >
                 <template v-if="state.fetching" #notFoundContent>
                   <a-spin size="small" />
                 </template>
               </a-select>
             </a-spin>
-            <a-range-picker  @change="changeRangePicker" format="YYYY/MM/DD"  />
+            <a-range-picker  v-model:value="times" @change="changeRangePicker"  />
           </a-space>
         </a-col>
         <a-col :span="24">
@@ -102,16 +103,20 @@ const state = reactive<{
   [key: string]: any
 }>({
   deviceId: '',
-  searchDeviceLabel: '',
+  searchDeviceLabel: null,
   start: 0,
   end: '',
   dataSource: [],
   attrSource: [],
   loading: false,
   analysisType: '',
-  spinning: false
+  spinning: false,
+  onlineNum: 0,
+  offlineNum: 0
 })
 
+const times = ref([dayjs(), dayjs()])
+
 state.deviceId = route.query.id as string
 
 const onTabChange = (key: 'session' | 'attr') => activeTabKey.value = key
@@ -121,75 +126,81 @@ watch(
   () => activeTabKey.value === 'session' ? getDeviceSession() : getDeviceAttr()
 )
 
-// 获取当前时间到半小时之前
-// const get
-
-// watch(
-//   () => state.attrSource,
-//   () => {
-//     nextTick(() => {
-//       Object.keys(state.attrSource).forEach(key => {
-//         const chartDom = document.getElementById('device-attr-' + key)
-
-//         const myChart = echarts.init(chartDom!)
-//         const attrItem = state.attrSource[key]
-
-//         attrEchartsJson.xAxis.data = (attrItem.map(item => dayjs(item.ts).format('HH:MM:ss')) || []) as never[]
-
-//         attrEchartsJson.series[0].data = attrItem.map(item => item.longValue)
-
-//         attrEchartsJson.title.text = key
-
-//         myChart.setOption(attrEchartsJson)
-//       })
-//       state.loading = false
-//     })
-//   }
-// )
-
-// watch(
-//   () => state.dataSource,
-//   () => {
-//     if (activeTabKey.value === 'session') {
-//       const chartDom = document.getElementById('device-session')
-//       const chartDomScatter = document.getElementById('device-session-scatter')
-//       const myChart = echarts.init(chartDom!)
-//       const chartDomScatterChart = echarts.init(chartDomScatter!)
-//       sessionEchartsJson.xAxis.data = state.dataSource.map(item => item.createAt) as never[]
-//       sessionEchartsJson.series[0].data = state.dataSource.map(item => item.sessionType === 'CONNECT' ? '上线' : '下线')
-
-//       const connectCount = state.dataSource.filter(item => item.sessionType === 'CONNECT').length
-//       const disconnectCount = state.dataSource.length - connectCount
-
-//       barOption.series[0].data = [
-//         {
-//           value: calculateAverage(connectCount),
-//           groupId: 'male'
-//         },
-//         {
-//           value: calculateAverage(disconnectCount),
-//           groupId: 'female'
-//         }
-//       ]
+// 获取设备属性分析数据
+watch(
+  () => state.attrSource,
+  () => {
+    nextTick(() => {
+      Object.keys(state.attrSource).forEach(key => {
+        const chartDom = document.getElementById('device-attr-' + key)
 
-//       scatterOption.series[0].data = state.dataSource.filter(item => item.sessionType === 'CONNECT').map(item => [0, connectCount])
+        const myChart = echarts.init(chartDom!)
+        const attrItem = state.attrSource[key]
 
-//       scatterOption.series[1].data = state.dataSource.filter(item => item.sessionType !== 'CONNECT').map(item => [1, disconnectCount])
+        attrEchartsJson.xAxis.data = (attrItem.map(item => dayjs(item.ts).format('HH:MM:ss')) || []) as never[]
 
-//       let currentOption = scatterOption
+        attrEchartsJson.series[0].data = attrItem.map(item => item.longValue)
 
-//       setInterval(function () {
-//         currentOption = currentOption === scatterOption ? barOption : scatterOption
-//         chartDomScatterChart.setOption(currentOption, true)
-//       }, 2000)
+        attrEchartsJson.title.text = key
 
-//       chartDomScatterChart.setOption(currentOption)
-//       myChart.setOption(sessionEchartsJson)
+        myChart.setOption(attrEchartsJson)
+      })
+      state.loading = false
+    })
+  }
+)
 
-//       state.loading = false
-//     }
-//   }
-// )
+// 获取上下线分析图表数据
+// state.dataSource
+watch(
+  () => state.deviceId,
+  () => {
+    setTimeout(() => {
+      if (activeTabKey.value === 'session') {
+        const chartDom = document.getElementById('device-session')
+        const chartDomScatter = document.getElementById('device-session-scatter')
+        console.log('chartDom:', chartDom)
+
+        const myChart = echarts.init(chartDom!)
+        const chartDomScatterChart = echarts.init(chartDomScatter!)
+        sessionEchartsJson.xAxis.data = state.dataSource.map(item => item.createAt) as never[]
+        sessionEchartsJson.series[0].data = state.dataSource.map(item => item.sessionType === 'CONNECT' ? '上线' : '下线')
+
+        const connectCount = state.dataSource.filter(item => item.sessionType === 'CONNECT').length
+        const disconnectCount = state.dataSource.length - connectCount
+
+        barOption.series[0].data = [
+          {
+            value: calculateAverage(connectCount),
+            groupId: 'male'
+          },
+          {
+            value: calculateAverage(disconnectCount),
+            groupId: 'female'
+          }
+        ]
+
+        sessionEchartsJson.legend.data = [`上线数量 ${state.onlineNum}`, `下线数量 ${state.offlineNum}`]
+
+        scatterOption.series[0].data = state.dataSource.filter(item => item.sessionType === 'CONNECT').map(item => [0, connectCount])
+
+        scatterOption.series[1].data = state.dataSource.filter(item => item.sessionType !== 'CONNECT').map(item => [1, disconnectCount])
+
+        let currentOption = scatterOption
+
+        setInterval(function () {
+          currentOption = currentOption === scatterOption ? barOption : scatterOption
+          chartDomScatterChart.setOption(currentOption, true)
+        }, 2000)
+
+        chartDomScatterChart.setOption(currentOption)
+        myChart.setOption(sessionEchartsJson)
+
+        state.loading = false
+      }
+    }, 3000)
+  }
+)
 
 watch(
   () => state.deviceId,
@@ -197,21 +208,21 @@ watch(
 )
 
 const changeRangePicker = (time) => {
-  const [startTime, endTime] = time
-
-  state.start = new Date(startTime).getTime()
-  state.end = new Date(endTime).getTime()
-
   activeTabKey.value === 'session' ? getDeviceSession() : getDeviceAttr()
 }
 
+const selectDevice = async (value: records) => {
+  const device = await DeviceContriller.byId(value)
+  state.deviceId = device.id
+  console.log('device:', state.deviceId)
+}
+
 const getDeviceLabel = async (val: string) => {
-  console.log('val:', val)
   if (!val) return
   state.spinning = true
   const { data } = await DeviceContriller.labelsByLabel(val)
   state.spinning = false
-  state.dataSource = data.map(item => ({ value: item, label: item }))
+  state.dataSource = data.map(item => ({ value: item.id, label: item.deviceLabel }))
   console.log(data)
 }
 
@@ -219,13 +230,16 @@ const getDeviceSession = async () => {
   if (!state.deviceId) return
   state.loading = true
   const { data } = await DeviceContriller.getSession({ deviceId: state.deviceId, start: state.start, end: state.end })
-  state.dataSource = data
+  state.dataSource = data.sessionEntities
+  state.offlineNum = data.offlineNum
+  state.onlineNum = data.onlineNum
 }
 
 const getDeviceAttr = async () => {
   if (!state.deviceId) return
   state.loading = true
-  const { data } = await DeviceContriller.getAttr({ deviceId: state.deviceId, start: state.start, end: state.end })
+
+  const { data } = await DeviceContriller.getAttr({ deviceId: state.deviceId, start: 1, end: state.end })
   state.attrSource = data
 }
 
@@ -237,7 +251,7 @@ const getDeviceList = async () => {
 }
 
 onMounted(() => {
-  getDeviceList()
+  // getDeviceList()
   if (state.deviceId) {
     getDeviceSession()
   }

+ 1 - 1
src/pages/Iot/device/components/overview.vue

@@ -145,7 +145,7 @@ const ok = async () => {
 
 const getLiveData = async () => {
   const { data } = await DeviceContriller.getLiveData(deviceId)
-  state.liveDataSource = [{ key: 'participant', label: '耐心', dataUnit: '%', ts: 19283831323218 }]
+  state.liveDataSource = data
 }
 
 const { start } = useScheduler(getLiveData, 1000)

+ 3 - 2
src/pages/Iot/device/index.vue

@@ -22,7 +22,7 @@
     <a-row justify="space-between" >
       <a-col :span="20"  >
         <a-form layout="inline" >
-          <a-form-item label="设备状态"  >
+          <a-form-item  >
             <a-select  allowClear style="width: 176px;"  v-model:value="searchState.deviceStatus" placeholder="请输入设备状态">
               <a-select-option
                 v-for="item in DeviceContriller.deviceStatus"
@@ -234,7 +234,7 @@ const searchState = reactive({
   page: 1,
   pageSize: 10,
   total: 0,
-  deviceStatus: '',
+  deviceStatus: null,
   modelId: modelId,
   searchKey: 'deviceId',
   searchValue: '',
@@ -338,4 +338,5 @@ onMounted(() => {
   font-size: 18px;
   color: @sublabel-color;
 }
+
 </style>

+ 2 - 2
src/pages/Iot/device/json/echartsJson.ts

@@ -6,7 +6,7 @@ export const sessionEchartsJson = {
     trigger: 'axis'
   },
   legend: {
-    data: []
+    data: ['上线数量', '下线数量']
   },
   grid: {
     left: '3%',
@@ -30,7 +30,7 @@ export const sessionEchartsJson = {
   },
   series: [
     {
-      name: '上线',
+      name: 'offlineNum',
       type: 'line',
       stack: 'Total',
       data: ['上线', '下线', '下线', '上线']

+ 4 - 4
src/pages/Iot/ota/index.vue

@@ -4,13 +4,13 @@
     <a-col :span="16" >
       <a-row :gutter="[8, 8]" >
         <a-col>
-          包名:<a-input allowClear style="width: 170px;" v-model:value="queryState.label" ></a-input>
+          <a-input allowClear placeholder="请输入包名" style="width: 170px;" v-model:value="queryState.label" ></a-input>
         </a-col>
         <a-col>
-          版本:<a-input allowClear style="width: 170px;" v-model:value="queryState.version" ></a-input>
+          <a-input   placeholder="请输入版本" allowClear style="width: 170px;" v-model:value="queryState.version" ></a-input>
         </a-col>
         <a-col>
-          包类型:<a-select style="width: 170px;" allowClear v-model:value="queryState.pkgType" >
+          <a-select style="width: 170px;" allowClear v-model:value="queryState.pkgType" placeholder="请选择包类型" >
           <a-select-option
               v-for="item in OtaController.pkgTypeList"
               :key="item.value"
@@ -135,7 +135,7 @@ const queryState = reactive({
   total: 0,
   label: '',
   version: '',
-  pkgType: ''
+  pkgType: null
 })
 
 const state = reactive<{

+ 14 - 26
src/pages/Iot/rule/forwardRule.vue

@@ -4,23 +4,17 @@
     :list="state.forwardCount"
   />
 <a-card style="margin-top: 20px;" >
-  <a-row justify="space-between" >
+  <a-row justify="space-between"   >
     <a-col :span="18" >
-    <a-form style="width: 100%;"  :label-col="{span: 6}" :wrapper-col="{span: 16}" >
-      <a-row style="width: 100%" :gutter="[8, 8]" >
-        <a-col :xs="20" :md="12" :xl="7">
-          <a-form-item label="规则ID" >
-            <a-input allowClear v-model:value="queryParams.ruleId" placeholder="请输入规则id"></a-input>
-          </a-form-item>
+      <a-row style="width: 100%"  :gutter="[8,8]" >
+        <a-col :xs="20" :md="12" :xl="5">
+          <a-input  allowClear v-model:value="queryParams.ruleId" placeholder="请输入规则id"></a-input>
         </a-col>
-        <a-col :xs="20" :md="12" :xl="7">
-          <a-form-item label="规则名称" >
-              <a-input allowClear v-model:value="queryParams.ruleLabel" placeholder="请输入规则名称" ></a-input>
-            </a-form-item>
+        <a-col :xs="20" :md="12" :xl="5">
+          <a-input allowClear v-model:value="queryParams.ruleLabel" placeholder="请输入规则名称" ></a-input>
         </a-col>
-        <a-col :xs="20" :md="12" :xl="7" >
-          <a-form-item label="数据来源" >
-              <a-select allowClear v-model:value="queryParams.subjectResource"    placeholder="选择数据来源">
+        <a-col :xs="20" :md="12" :xl="5" >
+              <a-select allowClear style="width: 100% !important;" v-model:value="queryParams.subjectResource"    placeholder="选择数据来源">
                 <a-select-option
                   v-for="item in subjectResourceList"
                   :key="item.key"
@@ -29,11 +23,9 @@
                 {{item.name}}
                 </a-select-option>
               </a-select>
-            </a-form-item>
         </a-col>
-        <a-col :xs="20" :md="12" :xl="7" >
-          <a-form-item label="触发事件" >
-              <a-select allowClear v-model:value="queryParams.subjectEvent"  placeholder="选择触发事件">
+        <a-col :xs="20" :md="12" :xl="5" >
+              <a-select allowClear style="width: 100% !important;" v-model:value="queryParams.subjectEvent"  placeholder="选择触发事件">
                 <a-select-option
                   v-for="item in Array.from(RuleController.SubjectEventMap, ([key, value]) => ({ ...value, value: value.key }))"
                   :key="item.key"
@@ -42,11 +34,9 @@
                 {{item.name}}
                 </a-select-option>
               </a-select>
-            </a-form-item>
         </a-col>
-        <a-col :xs="20" :md="12" :xl="7" >
-          <a-form-item label="状态" >
-              <a-select allowClear v-model:value="queryParams.status" >
+        <a-col :xs="20" :md="12" :xl="4" >
+              <a-select allowClear style="width: 100% !important;" v-model:value="queryParams.status" >
                 <a-select-option
                   v-for="item in statusList"
                   :key="item.key"
@@ -55,10 +45,8 @@
                 {{item.name}}
                 </a-select-option>
               </a-select>
-            </a-form-item>
         </a-col>
       </a-row>
-    </a-form>
     </a-col>
     <a-col>
       <a-space>
@@ -588,8 +576,8 @@ let queryParams = reactive({
   ruleId: '',
   ruleLabel: '',
   status: '',
-  subjectEvent: '',
-  subjectResource: ''
+  subjectEvent: null,
+  subjectResource: null
 })
 
 const state = reactive({

+ 11 - 11
src/pages/Iot/sys/notice.vue

@@ -2,15 +2,14 @@
   <a-card>
     <a-row>
       <a-col span="12">
-        <a-form layout="inline" >
-          <a-form-item label="通知名称" >
-            <a-input allowClear v-model:value="queryParamsState.label" placeholder="请输入通知名称"/>
-          </a-form-item>
-          <a-form-item  label='通知数据来源' >
+        <a-row :gutter="[8, 8]" >
+          <a-col> <a-input allowClear v-model:value="queryParamsState.label" placeholder="请输入通知名称"/>  </a-col>
+          <a-col>
             <a-select
-            allowClear
+              allowClear
               style="width: 170px"
               v-model:value="queryParamsState.source"
+              placeholder="请选择数据来源"
             >
               <a-select-option
                 v-for="item in sourceList"
@@ -20,11 +19,12 @@
                 {{item.label}}
               </a-select-option>
             </a-select>
-          </a-form-item >
-          <a-form-item>
+          </a-col>
+          <a-col>
             <a-button type="primary" @click="getNoticePage">搜索</a-button>
-          </a-form-item>
-      </a-form>
+          </a-col>
+        </a-row>
+
       </a-col>
       <a-col span="12" >
         <a-row justify="end" >
@@ -211,7 +211,7 @@ const queryParamsState = reactive({
   page: 1,
   pageSize: 10,
   label: '',
-  source: ''
+  source: null
 })
 
 const state = reactive({

+ 10 - 10
src/pages/Iot/task/manage.vue

@@ -7,15 +7,14 @@
 <a-card style="margin-top: 20px;" >
   <a-row justify="space-between" >
     <a-col span="12" >
-      <a-form layout="inline" >
-          <a-form-item label="任务名称" >
-            <a-input allowClear v-model:value="queryParamsState.taskLabel" placeholder="请输入任务名称"/>
-          </a-form-item>
-          <a-form-item  label='任务状态' >
+        <a-row :gutter="[8, 8]" >
+          <a-col><a-input allowClear placeholder="请输入任务名称" v-model:value="queryParamsState.taskLabel"/></a-col>
+          <a-col>
             <a-select
               allowClear
               style="width: 170px"
               v-model:value="queryParamsState.status"
+              placeholder="请选择任务状态"
             >
               <a-select-option
                 v-for="item in statusList"
@@ -25,11 +24,12 @@
                 {{item.label}}
               </a-select-option>
             </a-select>
-          </a-form-item >
-          <a-form-item>
+          </a-col>
+          <a-col>
             <a-button type="primary" @click="getTaskPage">搜索</a-button>
-          </a-form-item>
-      </a-form>
+          </a-col>
+        </a-row>
+
     </a-col>
     <a-col span="12" >
       <a-row justify="end" >
@@ -253,7 +253,7 @@ const queryParamsState = reactive({
   pageSize: 10,
   total: 0,
   taskLabel: '',
-  status: ''
+  status: null
 })
 
 const modalTitle = computed(() => state.opraState === 'add' ? '新增任务' : '编辑任务')

+ 10 - 6
src/type/iot.d.ts

@@ -226,12 +226,16 @@ declare namespace IOT {
 
       interface Session {
         'id': string,
-        'createAt': 1683171293012,
-        'updateAt': string,
-        'deviceId': string,
-        'sessionType': 'CONNECT' | 'DISCONNECT',
-        'connectTime': string,
-        'activityTime': string
+        offlineNum: number
+        onlineNum: number
+        sessionEntities: {
+          'createAt': 1683171293012,
+          'updateAt': string,
+          'deviceId': string,
+          'sessionType': 'CONNECT' | 'DISCONNECT',
+          'connectTime': string,
+          'activityTime': string
+        }[]
       }
 
       interface Attr {