Bläddra i källkod

feat: 座位表的渲染和相关交互

YuJing 1 månad sedan
förälder
incheckning
a3cbe20c37

+ 7 - 0
features/feature/src/main/ets/model/Index.ets

@@ -0,0 +1,7 @@
+// 撤销数据
+@ObservedV2
+export class undoData {
+  student?: string[]
+  seatingPlan?: Array<string[]>
+  columnMode?: number
+}

+ 148 - 24
features/feature/src/main/ets/pages/SeatingPlanPage.ets

@@ -1,51 +1,175 @@
 import { SeatingPlanPageViewModel } from '../viewModel/SeatingPlanPageViewModel';
 import { window } from '@kit.ArkUI';
+import { unifiedDataChannel, uniformTypeDescriptor } from '@kit.ArkData';
+import { BasicType } from 'basic';
 
 // 座位表页面
 @ComponentV2
 struct SeatingPlanPage {
   @Local vm: SeatingPlanPageViewModel = new SeatingPlanPageViewModel(this.getUIContext());
 
-  aboutToAppear() {
-
-  }
-
-  aboutToDisappear(): void {
-    this.vm._onBackPressed()
-  }
-
   build() {
     NavDestination() {
-      Column() {
+      Row({space: 13}){
+        // 学生列表
         Column(){
           List({space: 4}){
-            ForEach(this.vm.dataSource, (item: number, index) => {
+            ForEach(this.vm.dataSource, (item: string, index) => {
               ListItem(){
-                Text('四个字的人')
-                  .width('100%')
-                  .fontSize(14)
-                  .borderRadius(15)
-                  .border({width: 1, color: item == 1 ? '#7186F9' : '#FB9BC1'})
-                  .padding({top: 7, bottom: 7})
+                Row(){
+                  Text(item)
+                    .fontSize(16)
+                    .fontWeight(500)
+                    .fontColor('#333333')
+                }
+                .width(77)
+                .height(30)
+                .borderRadius(10)
+                .backgroundColor(Color.White)
+                .alignItems(VerticalAlign.Center)
+                .justifyContent(FlexAlign.Center)
+                .border({ width: 1, color: index%2 == 0 ? '#7186F9' : '#FB9BC1'})
+                .padding({top: 7, bottom: 7})
+                .draggable(true)
+                .onDragStart((event) => {
+                  let data: unifiedDataChannel.PlainText = new unifiedDataChannel.PlainText();
+                  data.abstract = item;
+                  this.vm._addUndoData()
+                  this.vm.dataSource.splice(index, 1);
+                  (event as DragEvent).setData(new unifiedDataChannel.UnifiedData(data));
+                })
+                .onDragEnd((event) => {
+                  this.vm._onDragEnd(event)
+                })
               }
             })
           }
-          .width('100%')
-          .height('100%')
+          .width("100%")
+          .height("100%")
           .scrollBar(BarState.Off)
         }
         .width(95)
+        .height("100%")
+        .borderRadius(15)
+        .backgroundColor('#E1E5FF')
+        .justifyContent(FlexAlign.Start)
+        .alignItems(HorizontalAlign.Center)
+        .border({width:1, color: '#5668FC'})
+        .padding({ top: 6, left:8, right: 8, bottom: 6 })
+
+        // 操作按钮 和 座位表
+        Column({space: 11}){
+          // 功能控制按钮
+          Row(){
+            ForEach(this.vm.controlBtn, (item: BasicType, index) => {
+              this.controlButton(item.text!, item.click!, item.message ? true : false)
+            })
+          }
+          .height(30)
+          .width("100%")
+          .justifyContent(FlexAlign.SpaceBetween)
+
+          // 座位表
+          Column(){
+            Scroll(){
+              List({space: 24}){
+                ListItem(){
+                  Row(){
+                    Text('讲台')
+                      .fontSize(16)
+                      .fontWeight(500)
+                      .borderRadius(10)
+                      .backgroundColor(Color.White)
+                      .padding({left: 120, top: 8, right: 120, bottom: 8})
+                  }
+                  .padding({left: 200, right: 200})
+                  .justifyContent(FlexAlign.Center)
+                  .alignItems(VerticalAlign.Center)
+                }
+
+                ForEach(this.vm.seatingPlan, (seating: string[], x: number) => {
+                  ListItem(){
+                    Row(){
+                      ForEach(seating, (item: string, y: number) => {
+                        Row(){
+                          Text(item)
+                        }
+                        .width(70)
+                        .height(40)
+                        .borderRadius(10)
+                        .backgroundColor('#E7EAFF')
+                        .alignItems(VerticalAlign.Center)
+                        .justifyContent(FlexAlign.Center)
+                        .margin({right: this.vm.columnMode == 1 ? 14 : ((y+1)%2 == 1 ? 7 : 30)})
+                        .allowDrop([uniformTypeDescriptor.UniformDataType.PLAIN_TEXT])
+                        .onClick(() => { this.vm._onStudentClick(seating, x, y) })
+                        .onDrop((dragEvent?: DragEvent) => {
+                          this.vm.getDataFromUdmf((dragEvent as DragEvent), (event: DragEvent) => {
+                            let records: Array<unifiedDataChannel.UnifiedRecord> = event.getData().getRecords();
+                            let plainText: unifiedDataChannel.PlainText = records[0] as unifiedDataChannel.PlainText;
+
+                            this.vm._onDrag(seating, x, y, plainText.abstract ?? '')
+
+                            event.setResult(DragResult.DRAG_SUCCESSFUL);
+                          })
+                        })
+                      })
+                    }
+                  }
+                })
+              }
+            }
+            .height("100%")
+            .align(Alignment.TopStart)
+            .scrollable(ScrollDirection.Horizontal)
+          }
+          .width('100%')
+          .layoutWeight(1)
+          .alignItems(HorizontalAlign.Center)
+          .justifyContent(FlexAlign.Center)
+        }
+        .layoutWeight(1)
         .height('100%')
-        .padding({top: 6, bottom: 6, left: 8, right: 8})
+        .alignItems(HorizontalAlign.Start)
+        .justifyContent(FlexAlign.Start)
       }
-      .width('100%')
-      .height('100%')
-      .justifyContent(FlexAlign.Center)
-      .alignItems(HorizontalAlign.Start)
+      .width("100%")
+      .height("100%")
+      .padding({ left: 15, top: 17, right: 15, bottom: 17 })
     }
     .hideTitleBar(true)
+    .backgroundColor('#F5F6FF')
+    .padding({ top: this.vm.safeTop})
     .onBackPressed(() => { return this.vm._onBackPressed() })
-    .padding({ left: this.vm.safeTop })
+  }
+
+  // 控制按钮
+  @Builder controlButton(text: string, onClick: () => void, isDouble: boolean = false){
+    if(!isDouble) {
+      Text(text)
+        .fontSize(16)
+        .borderRadius(10)
+        .fontColor(Color.White)
+        .backgroundColor('#7186F9')
+        .padding({left: 10, top: 6, right: 10, bottom: 6})
+        .onClick(onClick)
+    } else {
+      Row(){
+        Text('单人')
+          .fontSize(16)
+          .borderRadius({bottomLeft: 10, topLeft: 10})
+          .fontColor(Color.White)
+          .backgroundColor(this.vm.columnMode == 1 ? '#E7EAFF' : '#7186F9')
+          .padding({left: 10, top: 6, right: 10, bottom: 6})
+
+        Text('双人')
+          .fontSize(16)
+          .borderRadius({topRight: 10, bottomRight: 10})
+          .fontColor(Color.White)
+          .backgroundColor(this.vm.columnMode == 2 ? '#E7EAFF' : '#7186F9')
+          .padding({left: 10, top: 6, right: 10, bottom: 6})
+      }.onClick(onClick)
+    }
   }
 }
 

+ 276 - 8
features/feature/src/main/ets/viewModel/SeatingPlanPageViewModel.ets

@@ -1,15 +1,77 @@
-import { YTAvoid, yTRouter } from "basic"
+import { BasicType, IBestToast, YTAvoid, yTRouter } from "basic"
 import { window } from "@kit.ArkUI"
+import { unifiedDataChannel } from "@kit.ArkData"
+import { BusinessError } from "@kit.BasicServicesKit"
+import { undoData } from "../model/Index"
 
 @ObservedV2
 export class SeatingPlanPageViewModel{
   @Trace safeTop: number = 0
   // 学生列表
-  @Trace dataSource: number[] = [0, 1, 1, 0, 0, 1, 0, 1, 0, 0]
-  // 座位表
-  @Trace seatingPlan: string[][] = []
+  @Trace dataSource: string[] = ['张三', '李四', '王五', '赵六', '孙七', '周八', '吴九', '小十', '张三', '李四', '王五', '赵六', '孙七', '周八', '吴九', '小十']
+  // 座位表 - 二维数组
+  @Trace seatingPlan: Array<string[]> = new Array(4).fill(new Array(8).fill(''))
+  // 当前显示的模式: 1 - 单列  2 - 双列
+  @Trace columnMode: number = 1
 
+  // 是否保存了 - 上次的操作是否为保存
+  isSave: boolean = false
+  // 撤销数据
+  undoDataList: Array<undoData> = []
+  // 上下文对象
   conText: UIContext
+  // 控制按钮
+  controlBtn: Array<BasicType> = [
+    {
+      text: '添加行',
+      click: () => {
+        this._onAddRow()
+      }
+    },
+    {
+      text: '添加列',
+      click: () => {
+        this._onAddColumn()
+      }
+    },
+    {
+      text: '删除行',
+      click: () => {
+        this._onDeleteRow()
+      }
+    },
+    {
+      text: '删除列',
+      click: () => {
+        this._onDeleteColumn()
+      }
+    },
+    {
+      text: '单、双列切换',
+      message: 'hh',
+      click: () => {
+        this._onChangeColumn()
+      }
+    },
+    {
+      text: '撤销操作',
+      click: () => {
+        this._onUndo()
+      }
+    },
+    {
+      text: '保存',
+      click: () => {
+        this._onSave()
+      }
+    },
+    {
+      text: '返回',
+      click: () => {
+        this._onBackPressed()
+      }
+    }
+  ]
 
   constructor(conText: UIContext) {
     this.safeTop = AppStorage.get(YTAvoid.SAFE_TOP_KEY) as number
@@ -21,18 +83,224 @@ export class SeatingPlanPageViewModel{
       })
   }
 
-  
+  /**
+   * 点击学生
+   * @param seating 当前维度的学生列表
+   * @param x
+   * @param y
+   */
+  _onStudentClick(seating: string[], x : number, y : number){
+    if(!seating[y]) return
+
+    let arr = [...seating]
+    let i = arr[y]
+    arr[y] = ''
+
+    this._addUndoData()
+    this.dataSource.push(i)
+    this.seatingPlan.splice(x, 1, arr)
+  }
 
   /**
-   * 重写的返回逻辑
-   * @returns
+   * 接收方 - 接收到拖拽的学生
+   * @param seat 当前维度的学生列表
+   * @param x
+   * @param y
+   * @param value 接收到的学生
    */
-  _onBackPressed(){
+  _onDrag(seat: string[], x : number, y : number, value: string){
+    let arr = [...seat]
+
+    // 原位置上有学生
+    if(arr[y]) {
+      animateToImmediately({
+        duration: 300
+      } ,() => {
+        this.dataSource.push(arr[y])
+      })
+    }
+
+    arr[y] = value
+    this.seatingPlan[x] = arr
+  }
+
+  /**
+   * 数据源 - 拖拽事件结束
+   * @param event 拖拽事件传递的事件参数
+   */
+  _onDragEnd(event: DragEvent){
+    // onDragEnd里取到的result值在接收方onDrop设置
+    if (event.getResult() === DragResult.DRAG_SUCCESSFUL) {
+
+    } else if (event.getResult() === DragResult.DRAG_FAILED) {
+      // 拖拽结束 - 没有在接收方接收到数据, 撤销本次操作
+      this._onUndo()
+    }
+  }
+
+  /**
+   * 添加行
+   */
+  _onAddRow() {
+    let x = this.seatingPlan.length
+    let y = this.seatingPlan[0].length
+
+    this._addUndoData()
+    this.seatingPlan.push(new Array(y).fill(''))
+  }
+
+  /**
+   * 添加列
+   */
+  _onAddColumn() {
+    let x = this.seatingPlan.length
+    let y = this.seatingPlan[0].length
+    this._addUndoData()
+    for (let index = 0; index < this.seatingPlan.length; index++) {
+      let arr = [...this.seatingPlan[index]]
+      arr.push('')
+      if(this.columnMode == 2) arr.push('')
+      this.seatingPlan[index] = arr
+    }
+  }
+
+  /**
+   * 删除行
+   */
+  _onDeleteRow() {
+    let x = this.seatingPlan.length
+    let y = this.seatingPlan[0].length
+
+    if(x == 1){
+      IBestToast.show('行数不足1行')
+      return
+    }
+    this._addUndoData()
+    this.seatingPlan.splice(x-1, 1)
+  }
+
+  /**
+   * 删除列
+   */
+  _onDeleteColumn() {
+    let x = this.seatingPlan.length
+    let y = this.seatingPlan[0].length
+
+    if(y == 1) {
+      IBestToast.show('列数不足1列')
+      return
+    }
+    this._addUndoData()
+    for (let index = 0; index < x; index++) {
+      let arr = [...this.seatingPlan[index]]
+      arr.splice(y-1, 1)
+      this.seatingPlan[index] = arr
+    }
+  }
+
+  /**
+   *  单、双列切换
+   */
+  _onChangeColumn() {
+    // todo 在切换的时候判断一下列数是否可以足够显示 双列, 如果不行则在后方补齐
+    let x = this.seatingPlan[0].length
+    if(x%2 == 1 && this.columnMode == 1) this._onAddColumn()
+    this._addUndoData()
+    this.columnMode = this.columnMode == 1 ? 2 : 1
+  }
+
+  /**
+   *  撤销操作
+   */
+  _onUndo() {
+    if(this.undoDataList.length == 0) return
+
+    const undoDate: undoData = this.undoDataList.pop()!
+    this.seatingPlan = [...undoDate.seatingPlan!]
+    this.dataSource = [...undoDate.student!]
+    this.columnMode = undoDate.columnMode!
+  }
+
+  /**
+   * 保存
+   */
+  _onSave() {
+    if(this.isSave) return
+    this.isSave = true
+  }
+
+  /**
+   * 添加 《撤销》 数据 - 仅保存 10 次操作
+   */
+  _addUndoData() {
+    this.isSave = false
+    const undoData: undoData = {
+      seatingPlan: [...this.seatingPlan],
+      columnMode: this.columnMode,
+      student: [...this.dataSource],
+    }
+    this.undoDataList.push(undoData)
+    if(this.undoDataList.length == 10) {
+      this.undoDataList.splice(0, 1)
+    }
+  }
+
+  /**
+   * 回正屏幕并返回
+   */
+  _onBack(){
     window.getLastWindow(this.conText.getHostContext())
       .then(res => {
         res.setPreferredOrientation(window.Orientation.UNSPECIFIED)
         yTRouter.pop('')
       })
+  }
+
+  /**
+   * 重写的返回逻辑
+   * @returns
+   */
+  _onBackPressed(){
+    if(!this.isSave) {
+      yTRouter.router2DoubleConfirmDiaLog({
+        text: '编辑内容未保存,确定离开吗?',
+        color: '#7186F9'
+      }, (res) => {
+        if(res && res.result == 'true') {
+          this._onBack()
+        }
+      })
+    } else {
+      this._onBack()
+    }
     return true;
   }
+
+  /******** 拖拽事件用的方法 ***********/
+  getDataFromUdmf(event: DragEvent, callback: (data: DragEvent) => void) {
+    if (this.getDataFromUdmfRetry(event, callback)) {
+      return;
+    }
+    setTimeout(() => {
+      this.getDataFromUdmfRetry(event, callback);
+    }, 1500);
+  }
+
+  getDataFromUdmfRetry(event: DragEvent, callback: (data: DragEvent) => void) {
+    try {
+      let data: UnifiedData = event.getData();
+      if (!data) {
+        return false;
+      }
+      let records: Array<unifiedDataChannel.UnifiedRecord> = data.getRecords();
+      if (!records || records.length <= 0) {
+        return false;
+      }
+      callback(event);
+      return true;
+    } catch (e) {
+      console.error("getData failed, code = " + (e as BusinessError).code + ", message = " + (e as BusinessError).message);
+      return false;
+    }
+  }
 }

+ 1 - 1
products/entry/src/main/ets/pages/Index.ets

@@ -54,7 +54,7 @@ struct Index {
   build() {
     Navigation(yTRouter) {
       Column() {
-        Tabs({ controller: this.tabsController }) {
+        Tabs({ controller: this.tabsController, index: 1 }) {
           ForEach(this.contentList, (_: BasicType<undefined>, index) => {
             TabContent() {
               if (index == 0) {