RecordButton.tsx 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. import React from 'react';
  2. import { Button, Space } from 'antd';
  3. import { SendOutlined, CloseOutlined } from '@ant-design/icons';
  4. import type { ClientState } from '@/types';
  5. import { px2vw } from '@/utils/viewport';
  6. import { getOssUrl } from '@/utils/oss';
  7. import { wrapTrackedClick } from '@/utils/tracking';
  8. const progressRing = getOssUrl('progress-ring.svg');
  9. const micIcon = getOssUrl('mic-icon.svg');
  10. interface RecordButtonProps {
  11. state: ClientState;
  12. onStartRecord: () => void;
  13. onSend: () => void;
  14. onCancel: () => void;
  15. /** 埋点用页面 ID,默认 chatbot;教程页传 tutorial_1 ~ tutorial_10 */
  16. pageId?: string;
  17. }
  18. /**
  19. * 录音控制按钮组件 - 根据Figma设计
  20. * 麦克风按钮:96x96,带114x114渐变进度环
  21. */
  22. export const RecordButton: React.FC<RecordButtonProps> = ({
  23. state,
  24. onStartRecord,
  25. onSend,
  26. onCancel,
  27. pageId = 'chatbot',
  28. }) => {
  29. // 空闲或错误状态:仅显示麦克风按钮(隐藏 Tap to chat 提示)
  30. if (state === 'idle' || state === 'error') {
  31. return (
  32. <div className="flex flex-col items-center justify-center relative">
  33. {/* 麦克风按钮 - 响应式尺寸 */}
  34. <div className="relative flex items-center justify-center">
  35. {/* 进度环 (114x114) - 响应式 */}
  36. <div className="absolute flex items-center justify-center"
  37. style={{
  38. width: px2vw(114),
  39. height: px2vw(114),
  40. maxWidth: '32vw',
  41. maxHeight: '32vw',
  42. }}>
  43. <img
  44. src={progressRing}
  45. alt="Progress Ring"
  46. className="w-full h-full object-contain"
  47. />
  48. </div>
  49. {/* 麦克风按钮背景 (96x96) - 响应式 */}
  50. <button
  51. onClick={wrapTrackedClick(pageId, 'mic', onStartRecord)}
  52. className="relative rounded-full flex items-center justify-center cursor-pointer transition-all duration-300 hover:scale-105 active:scale-95 focus:outline-none touch-manipulation"
  53. style={{
  54. width: px2vw(96),
  55. height: px2vw(96),
  56. minWidth: px2vw(44),
  57. minHeight: px2vw(44),
  58. maxWidth: '28vw',
  59. maxHeight: '28vw',
  60. background: 'rgba(255, 255, 255, 0.1)',
  61. backdropFilter: 'blur(15px)',
  62. WebkitBackdropFilter: 'blur(15px)',
  63. border: '1px solid transparent',
  64. backgroundImage: `conic-gradient(from 102deg at 53% 39%, rgba(64, 64, 64, 0.5) 3%, rgba(64, 64, 64, 0.35) 9%, rgba(255, 255, 255, 0.5) 17%, rgba(255, 255, 255, 0.5) 30%, rgba(64, 64, 64, 0.35) 52%, rgba(249, 249, 249, 1) 58%, rgba(255, 255, 255, 0.5) 80%, rgba(249, 249, 249, 0.5) 91%)`,
  65. boxShadow: '-1px 3px 6px 0px rgba(0, 0, 0, 0.08), -3px 11px 11px 0px rgba(0, 0, 0, 0.07), -7px 24px 15px 0px rgba(0, 0, 0, 0.04), -13px 43px 18px 0px rgba(0, 0, 0, 0.01), -20px 68px 20px 0px rgba(0, 0, 0, 0)',
  66. }}
  67. >
  68. {/* 麦克风图标 - 响应式尺寸 */}
  69. <div className="flex items-center justify-center"
  70. style={{
  71. width: px2vw(32),
  72. height: px2vw(32),
  73. maxWidth: '10vw',
  74. maxHeight: '10vw',
  75. }}>
  76. <img
  77. src={micIcon}
  78. alt="Microphone"
  79. className="w-full h-full object-contain"
  80. />
  81. </div>
  82. </button>
  83. </div>
  84. </div>
  85. );
  86. }
  87. // 录音状态:显示取消/发送按钮
  88. if (state === 'recording') {
  89. return (
  90. <div className="flex flex-col items-center justify-center">
  91. {/* 录音状态提示 - 响应式尺寸 */}
  92. <div className="flex items-center justify-center"
  93. style={{
  94. marginBottom: px2vw(20),
  95. gap: px2vw(8),
  96. }}>
  97. <span className="inline-block rounded-full bg-red-500 animate-pulse"
  98. style={{
  99. width: px2vw(8),
  100. height: px2vw(8),
  101. }} />
  102. <span className="text-white"
  103. style={{
  104. fontSize: px2vw(12),
  105. }}>
  106. 录音中...
  107. </span>
  108. </div>
  109. {/* 按钮组 - 响应式尺寸 */}
  110. <Space size="large" className="flex items-center justify-center flex-wrap">
  111. <Button
  112. size="large"
  113. icon={<CloseOutlined />}
  114. onClick={wrapTrackedClick(pageId, 'stop', onCancel)}
  115. className="bg-white/20 border-white/30 text-white hover:bg-white/30 touch-manipulation"
  116. style={{
  117. width: px2vw(90),
  118. height: px2vw(48),
  119. minWidth: px2vw(44),
  120. minHeight: px2vw(44),
  121. fontSize: px2vw(12),
  122. }}
  123. >
  124. 取消
  125. </Button>
  126. <Button
  127. type="primary"
  128. size="large"
  129. icon={<SendOutlined />}
  130. onClick={wrapTrackedClick(pageId, 'send', onSend)}
  131. className="touch-manipulation"
  132. style={{
  133. width: px2vw(90),
  134. height: px2vw(48),
  135. minWidth: px2vw(44),
  136. minHeight: px2vw(44),
  137. fontSize: px2vw(12),
  138. }}
  139. >
  140. 发送
  141. </Button>
  142. </Space>
  143. </div>
  144. );
  145. }
  146. // 发送状态:显示加载中
  147. if (state === 'sending') {
  148. return (
  149. <div className="flex flex-col items-center justify-center">
  150. <Button
  151. type="primary"
  152. loading
  153. size="large"
  154. className="touch-manipulation"
  155. style={{
  156. width: px2vw(120),
  157. height: px2vw(50),
  158. minWidth: '44px',
  159. minHeight: '44px',
  160. fontSize: px2vw(14),
  161. }}
  162. >
  163. 处理中...
  164. </Button>
  165. </div>
  166. );
  167. }
  168. // 播放状态:显示禁用按钮
  169. if (state === 'playing') {
  170. return (
  171. <div className="flex flex-col items-center justify-center">
  172. <Button
  173. type="primary"
  174. size="large"
  175. disabled
  176. className="touch-manipulation"
  177. style={{
  178. width: px2vw(120),
  179. height: px2vw(50),
  180. minWidth: '44px',
  181. minHeight: '44px',
  182. fontSize: px2vw(14),
  183. }}
  184. >
  185. 播放中...
  186. </Button>
  187. </div>
  188. );
  189. }
  190. return null;
  191. };