SeatLayout.vue 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. <template>
  2. <!--
  3. 座位布局组件
  4. 功能:管理学生座位的显示和交互
  5. 支持两种模式:
  6. 1. 全览模式:缩放显示所有座位,支持双击进入点名模式
  7. 2. 点名模式:正常尺寸显示,支持滚动和学生状态切换
  8. -->
  9. <view class="seat-layout-container">
  10. <!-- 全览模式 (overview 和 overview-simple) -->
  11. <view
  12. v-if="mode !== 'attendance'"
  13. class="seat-layout overview-mode"
  14. :style="overviewContainerStyle"
  15. >
  16. <view class="overview-content" :style="layoutStyle" @touchstart="handleTouchStart" @touchend="handleTouchEnd">
  17. <!-- 学生卡片网格 -->
  18. <view class="seat-grid" :style="gridStyle">
  19. <StudentCard
  20. v-for="student in students"
  21. :key="student.id"
  22. :student="student"
  23. :attendance-mode="false"
  24. :style="getCardPosition(student)"
  25. @statusChange="handleStatusChange"
  26. @click="handleCardClick"
  27. />
  28. </view>
  29. </view>
  30. </view>
  31. <!-- 点名模式 -->
  32. <scroll-view
  33. v-else
  34. ref="scrollViewRef"
  35. class="seat-layout attendance-mode"
  36. scroll-x="true"
  37. scroll-y="true"
  38. enable-flex="true"
  39. :scroll-top="scrollTop"
  40. :scroll-left="scrollLeft"
  41. @touchstart="handleTouchStart"
  42. @touchend="handleTouchEnd"
  43. >
  44. <!-- 学生卡片网格容器 -->
  45. <view class="scroll-content" :style="scrollContentStyle">
  46. <view class="seat-grid" :style="gridStyle">
  47. <StudentCard
  48. v-for="student in students"
  49. :key="student.id"
  50. :student="student"
  51. :attendance-mode="true"
  52. :style="getCardPosition(student)"
  53. @statusChange="handleStatusChange"
  54. @click="handleCardClick"
  55. />
  56. </view>
  57. </view>
  58. </scroll-view>
  59. </view>
  60. </template>
  61. <script setup>
  62. /**
  63. * 座位布局组件
  64. *
  65. * 主要功能:
  66. * 1. 学生座位的网格布局显示
  67. * 2. 全览模式和点名模式的切换
  68. * 3. 座位布局的缩放和滚动控制
  69. * 4. 双击事件处理和模式切换
  70. * 5. 滚动位置重置和页面滚动控制
  71. */
  72. import { ref, computed, watch, defineProps, defineEmits } from 'vue'
  73. import StudentCard from './StudentCard.vue'
  74. const props = defineProps({
  75. students: {
  76. type: Array,
  77. default: () => []
  78. },
  79. layout: {
  80. type: Object,
  81. default: () => ({ rows: 20, cols: 10 })
  82. },
  83. mode: {
  84. type: String,
  85. default: 'overview' // 'overview' | 'attendance'
  86. }
  87. })
  88. const emit = defineEmits(['modeChange', 'statusChange', 'studentClick'])
  89. // 触摸相关状态
  90. const lastTap = ref(0)
  91. const touchStartTime = ref(0)
  92. const touchStartPos = ref({ x: 0, y: 0 })
  93. // 滚动控制
  94. const scrollTop = ref(0)
  95. const scrollLeft = ref(0)
  96. const scrollViewRef = ref(null)
  97. // 卡片尺寸常量
  98. const CARD_WIDTH = 200 // rpx
  99. const CARD_HEIGHT = 280 // rpx - 根据新的卡片布局调整(20+224+20+文字+20+10)
  100. const CARD_GAP = 12 // rpx - 适中的间距,避免太挤或太松
  101. // 计算布局样式
  102. const layoutStyle = computed(() => {
  103. const totalWidthRpx = props.layout.cols * (CARD_WIDTH + CARD_GAP) - CARD_GAP // rpx单位
  104. const totalHeightRpx = props.layout.rows * (CARD_HEIGHT + CARD_GAP) - CARD_GAP // rpx单位
  105. console.log('SeatLayout 计算样式:', {
  106. mode: props.mode,
  107. students: props.students.length,
  108. layout: props.layout,
  109. totalWidthRpx,
  110. totalHeightRpx
  111. })
  112. if (props.mode === 'overview' || props.mode === 'overview-simple') {
  113. // 全览模式:缩放到屏幕宽度的90%,留边距
  114. const systemInfo = uni.getSystemInfoSync()
  115. const screenWidthPx = systemInfo.windowWidth // px单位
  116. const screenWidthRpx = screenWidthPx * 2 // 转换为rpx (1px = 2rpx)
  117. const targetWidthRpx = screenWidthRpx * 0.9 // 使用90%宽度,留10%边距
  118. const scale = targetWidthRpx / totalWidthRpx
  119. return {
  120. transform: `scale(${scale})`,
  121. transformOrigin: 'top left',
  122. width: `${totalWidthRpx}rpx`,
  123. height: `${totalHeightRpx}rpx`
  124. }
  125. } else {
  126. // 点名模式:正常尺寸,可滚动
  127. return {
  128. transform: 'scale(1)',
  129. transformOrigin: 'top left',
  130. width: `${totalWidthRpx}rpx`,
  131. height: `${totalHeightRpx}rpx`
  132. }
  133. }
  134. })
  135. // 网格样式
  136. const gridStyle = computed(() => ({
  137. display: 'grid',
  138. gridTemplateColumns: `repeat(${props.layout.cols}, ${CARD_WIDTH}rpx)`,
  139. gridTemplateRows: `repeat(${props.layout.rows}, auto)`, // 改为auto,让行高自适应卡片内容
  140. gap: `${CARD_GAP}rpx`,
  141. padding: `${CARD_GAP}rpx`
  142. }))
  143. // 全览模式容器样式
  144. const overviewContainerStyle = computed(() => ({
  145. width: '100%',
  146. minHeight: '600rpx', // 最小高度,允许内容撑开
  147. overflow: 'visible', // 允许内容显示
  148. position: 'relative'
  149. }))
  150. // 滚动内容样式
  151. const scrollContentStyle = computed(() => {
  152. const totalWidthRpx = props.layout.cols * (CARD_WIDTH + CARD_GAP) + CARD_GAP
  153. // 高度改为自适应,不再固定计算
  154. return {
  155. width: `${totalWidthRpx}rpx`,
  156. minWidth: '100%',
  157. minHeight: '100%'
  158. // 移除固定高度,让内容自适应
  159. }
  160. })
  161. // 获取卡片位置
  162. const getCardPosition = (student) => {
  163. return {
  164. gridColumn: student.col,
  165. gridRow: student.row
  166. }
  167. }
  168. // 处理触摸开始
  169. const handleTouchStart = (e) => {
  170. touchStartTime.value = Date.now()
  171. touchStartPos.value = {
  172. x: e.touches[0].clientX,
  173. y: e.touches[0].clientY
  174. }
  175. }
  176. // 处理触摸结束
  177. const handleTouchEnd = () => {
  178. const touchEndTime = Date.now()
  179. const touchDuration = touchEndTime - touchStartTime.value
  180. // 检测双击
  181. if (touchDuration < 300) { // 快速点击
  182. const now = Date.now()
  183. if (now - lastTap.value < 300) {
  184. // 双击检测成功
  185. handleDoubleClick()
  186. }
  187. lastTap.value = now
  188. }
  189. }
  190. // 处理双击事件
  191. const handleDoubleClick = () => {
  192. console.log('双击事件,当前模式:', props.mode)
  193. if (props.mode === 'attendance') {
  194. // 从点名模式退出
  195. const newMode = 'overview'
  196. console.log('从点名模式退出到:', newMode)
  197. emit('modeChange', newMode)
  198. } else {
  199. // 从全览模式(overview 或 overview-simple)进入点名模式
  200. const newMode = 'attendance'
  201. console.log('进入点名模式:', newMode)
  202. emit('modeChange', newMode)
  203. }
  204. }
  205. // 处理学生状态变化
  206. const handleStatusChange = (statusData) => {
  207. emit('statusChange', statusData)
  208. }
  209. // 处理卡片点击
  210. const handleCardClick = (student) => {
  211. emit('studentClick', student)
  212. }
  213. // 监听模式变化,重置滚动位置
  214. watch(() => props.mode, (newMode, oldMode) => {
  215. console.log('模式变化:', oldMode, '->', newMode)
  216. if (oldMode === 'attendance' && (newMode === 'overview' || newMode === 'overview-simple')) {
  217. console.log('从点名模式退出,重置页面滚动位置')
  218. // 使用 uni.pageScrollTo 强制滚动到页面顶部
  219. uni.pageScrollTo({
  220. scrollTop: 0,
  221. duration: 300,
  222. success: () => {
  223. console.log('页面滚动重置成功')
  224. },
  225. fail: (err) => {
  226. console.log('页面滚动重置失败:', err)
  227. }
  228. })
  229. // 同时重置 scroll-view 的滚动位置
  230. scrollTop.value = 0
  231. scrollLeft.value = 0
  232. console.log('滚动位置重置完成')
  233. }
  234. })
  235. </script>
  236. <style lang="scss" scoped>
  237. .seat-layout-container {
  238. width: 100%;
  239. min-height: 100%;
  240. position: relative;
  241. overflow: visible;
  242. .seat-layout {
  243. transition: transform 0.3s ease;
  244. &.overview-mode {
  245. // 全览模式样式
  246. pointer-events: auto;
  247. .overview-content {
  248. width: 100%;
  249. height: 100%;
  250. }
  251. .seat-grid {
  252. pointer-events: none; // 禁用卡片交互
  253. }
  254. }
  255. &.attendance-mode {
  256. // 点名模式样式
  257. width: 100%;
  258. height: 100%;
  259. .scroll-content {
  260. display: flex;
  261. flex-direction: column;
  262. }
  263. .seat-grid {
  264. pointer-events: auto; // 启用卡片交互
  265. flex-shrink: 0;
  266. }
  267. }
  268. }
  269. .seat-grid {
  270. min-height: 100%;
  271. }
  272. .mode-tip {
  273. position: absolute;
  274. bottom: 40rpx;
  275. left: 50%;
  276. transform: translateX(-50%);
  277. background-color: rgba(0, 0, 0, 0.7);
  278. color: white;
  279. padding: 20rpx 40rpx;
  280. border-radius: 40rpx;
  281. font-size: 28rpx;
  282. z-index: 100;
  283. }
  284. }
  285. </style>