Blame view

lib/pages/practice/bloc/topic_picture_bloc.dart 10.7 KB
45c862fd   吴启风   feat:闯关交互优化-答错晃动、...
1
2
  import 'dart:async';
  
2eb67dd4   liangchengyou   feat:调整代码
3
  import 'package:audioplayers/audioplayers.dart';
688b7b5c   liangchengyou   feat:更新文件结构
4
  import 'package:flutter/cupertino.dart';
2eb67dd4   liangchengyou   feat:调整代码
5
6
  import 'package:flutter/foundation.dart';
  import 'package:flutter/services.dart';
688b7b5c   liangchengyou   feat:更新文件结构
7
  import 'package:flutter_bloc/flutter_bloc.dart';
2eb67dd4   liangchengyou   feat:调整代码
8
  import 'package:flutter_easyloading/flutter_easyloading.dart';
354ac7e6   吴启风   feat:隐藏视频跟读入口、底部草...
9
  import 'package:permission_handler/permission_handler.dart';
aeafd474   吴启风   feat:选择题作答后播放音效&答...
10
  import 'package:wow_english/common/extension/string_extension.dart';
2eb67dd4   liangchengyou   feat:调整代码
11
12
13
  import 'package:wow_english/common/request/dao/listen_dao.dart';
  import 'package:wow_english/common/request/exception.dart';
  import 'package:wow_english/models/course_process_entity.dart';
22f36232   吴启风   feat:过渡页-练习环节
14
15
16
  import 'package:wow_english/pages/section/subsection/base_section/bloc.dart';
  import 'package:wow_english/pages/section/subsection/base_section/event.dart';
  import 'package:wow_english/pages/section/subsection/base_section/state.dart';
c623c7b2   吴启风   feat:语音跟读作答结果动效&语音
17
  import 'package:wow_english/utils/audio_player_util.dart';
3c1d5c64   liangchengyou   feat:练习功能完成
18
  import 'package:wow_english/utils/toast_util.dart';
7652f701   liangchengyou   feat:课程购买UI逻辑
19
  
e811f164   吴启风   feat:权限申请页面增加隐私合规...
20
  import '../../../common/permission/permissionRequester.dart';
c623c7b2   吴启风   feat:语音跟读作答结果动效&语音
21
  import '../../../common/utils/click_with_music_controller.dart';
642081ad   吴启风   feat:lottie动画加载优化...
22
  import '../../../common/utils/show_star_reward_dialog.dart';
819ae43b   吴启风   feat:体验优化-练习题目取消阻...
23
  import '../../../models/voice_result_type.dart';
22f36232   吴启风   feat:过渡页-练习环节
24
  import '../../../route/route.dart';
354ac7e6   吴启风   feat:隐藏视频跟读入口、底部草...
25
  
7652f701   liangchengyou   feat:课程购买UI逻辑
26
  part 'topic_picture_event.dart';
1e22b7d1   吴启风   feat:儿歌/视频环节接口请求时机优化
27
  
688b7b5c   liangchengyou   feat:更新文件结构
28
  part 'topic_picture_state.dart';
7652f701   liangchengyou   feat:课程购买UI逻辑
29
  
3c1d5c64   liangchengyou   feat:练习功能完成
30
31
32
  enum VoicePlayState {
    ///未知
    unKnow,
1f5969b8   biao   修复 练习和绘本播放音频问题
33
  
3c1d5c64   liangchengyou   feat:练习功能完成
34
35
    ///播放中
    playing,
1f5969b8   biao   修复 练习和绘本播放音频问题
36
  
3c1d5c64   liangchengyou   feat:练习功能完成
37
38
    ///播放完成
    completed,
1f5969b8   biao   修复 练习和绘本播放音频问题
39
  
3c1d5c64   liangchengyou   feat:练习功能完成
40
41
42
43
    ///播放终止
    stop
  }
  
1f5969b8   biao   修复 练习和绘本播放音频问题
44
45
  class TopicPictureBloc
      extends BaseSectionBloc<TopicPictureEvent, TopicPictureState> {
624214d0   liangchengyou   feat:看题选字/选图UI和部分逻辑
46
47
    final PageController pageController;
  
608c05b4   liangchengyou   feat:兑换课程
48
    final String courseLessonId;
624214d0   liangchengyou   feat:看题选字/选图UI和部分逻辑
49
50
51
  
    int _currentPage = 0;
  
819ae43b   吴启风   feat:体验优化-练习题目取消阻...
52
    int _optionSelectItem = -1;
2eb67dd4   liangchengyou   feat:调整代码
53
54
55
  
    CourseProcessEntity? _entity;
  
1e22b7d1   吴启风   feat:儿歌/视频环节接口请求时机优化
56
57
    CourseProcessEntity? get entity => _entity;
  
2eb67dd4   liangchengyou   feat:调整代码
58
    ///正在评测
d4d91cb0   吴启风   feat:lottie动效组件封装...
59
    bool _isRecording = false;
2eb67dd4   liangchengyou   feat:调整代码
60
  
3c1d5c64   liangchengyou   feat:练习功能完成
61
62
63
    ///正在播放音频
    VoicePlayState _voicePlayState = VoicePlayState.unKnow;
  
624214d0   liangchengyou   feat:看题选字/选图UI和部分逻辑
64
65
    int get currentPage => _currentPage + 1;
  
819ae43b   吴启风   feat:体验优化-练习题目取消阻...
66
67
    /// 选择题选中项
    int get optionSelectItem => _optionSelectItem;
624214d0   liangchengyou   feat:看题选字/选图UI和部分逻辑
68
  
d4d91cb0   吴启风   feat:lottie动效组件封装...
69
    bool get isRecording => _isRecording;
2eb67dd4   liangchengyou   feat:调整代码
70
  
1f5969b8   biao   修复 练习和绘本播放音频问题
71
    VoicePlayState get voicePlayState => _voicePlayState;
3c1d5c64   liangchengyou   feat:练习功能完成
72
  
e3c2820c   liangchengyou   feat:先声SDK逻辑调整
73
    late MethodChannel methodChannel;
2eb67dd4   liangchengyou   feat:调整代码
74
  
e3c2820c   liangchengyou   feat:先声SDK逻辑调整
75
    late AudioPlayer audioPlayer;
2eb67dd4   liangchengyou   feat:调整代码
76
  
354ac7e6   吴启风   feat:隐藏视频跟读入口、底部草...
77
78
    final BuildContext context;
  
07b173c9   吴启风   feat:练习页上方进度信息改成进...
79
80
81
82
    final Color? moduleColor;
  
    TopicPictureBloc(
        this.context, this.pageController, this.courseLessonId, this.moduleColor)
1f5969b8   biao   修复 练习和绘本播放音频问题
83
        : super(TopicPictureInitial()) {
624214d0   liangchengyou   feat:看题选字/选图UI和部分逻辑
84
      on<CurrentPageIndexChangeEvent>(_pageControllerChange);
3c1d5c64   liangchengyou   feat:练习功能完成
85
86
87
      on<VoicePlayStateChangeEvent>(_voicePlayStateChange);
      on<XSVoiceResultEvent>(_voiceXsResult);
      on<XSVoiceInitEvent>(_initVoiceSdk);
624214d0   liangchengyou   feat:看题选字/选图UI和部分逻辑
88
      on<SelectItemEvent>(_selectItemLoad);
e3c2820c   liangchengyou   feat:先声SDK逻辑调整
89
      on<RequestDataEvent>(_requestData);
ae77d87f   吴启风   feat:fix语音题无法手动停止...
90
      on<XSVoiceStartEvent>(_voiceXsStart);
3c1d5c64   liangchengyou   feat:练习功能完成
91
      on<XSVoiceStopEvent>(_voiceXsStop);
aeafd474   吴启风   feat:选择题作答后播放音效&答...
92
      on<VoicePlayEvent>(_questionVoicePlay);
3c1d5c64   liangchengyou   feat:练习功能完成
93
      on<InitBlocEvent>((event, emit) {
e3c2820c   liangchengyou   feat:先声SDK逻辑调整
94
95
        //音频播放器
        audioPlayer = AudioPlayer();
3c1d5c64   liangchengyou   feat:练习功能完成
96
        audioPlayer.onPlayerStateChanged.listen((event) async {
819ae43b   吴启风   feat:体验优化-练习题目取消阻...
97
98
99
100
101
102
103
104
105
          debugPrint('播放状态变化 _voicePlayState=$_voicePlayState event=$event');
          if (event == PlayerState.completed) {
            debugPrint('播放完成');
            _voicePlayState = VoicePlayState.completed;
          }
          if (event == PlayerState.stopped) {
            debugPrint('播放结束');
            _voicePlayState = VoicePlayState.stop;
          }
e3c2820c   liangchengyou   feat:先声SDK逻辑调整
106
  
819ae43b   吴启风   feat:体验优化-练习题目取消阻...
107
108
109
          if (event == PlayerState.playing) {
            debugPrint('正在播放中');
            _voicePlayState = VoicePlayState.playing;
3c1d5c64   liangchengyou   feat:练习功能完成
110
          }
819ae43b   吴启风   feat:体验优化-练习题目取消阻...
111
112
113
114
          if (isClosed) {
            return;
          }
          add(VoicePlayStateChangeEvent());
e3c2820c   liangchengyou   feat:先声SDK逻辑调整
115
116
        });
  
1f5969b8   biao   修复 练习和绘本播放音频问题
117
118
        methodChannel =
            const MethodChannel('wow_english/sing_sound_method_channel');
e3c2820c   liangchengyou   feat:先声SDK逻辑调整
119
        methodChannel.setMethodCallHandler((call) async {
1f5969b8   biao   修复 练习和绘本播放音频问题
120
121
122
123
          if (call.method == 'voiceResult') {
            //评测结果
            await audioPlayer.setAudioContext(AudioContext());
            await audioPlayer.setBalance(0.0);
e3c2820c   liangchengyou   feat:先声SDK逻辑调整
124
            add(XSVoiceResultEvent(call.arguments));
b90a1518   liangchengyou   feat:练习接口逻辑完成
125
126
127
            return;
          }
  
1f5969b8   biao   修复 练习和绘本播放音频问题
128
129
          if (call.method == 'voiceStart') {
            //评测开始
b90a1518   liangchengyou   feat:练习接口逻辑完成
130
131
132
133
134
135
            if (kDebugMode) {
              print('评测开始');
            }
            return;
          }
  
1f5969b8   biao   修复 练习和绘本播放音频问题
136
137
138
139
          if (call.method == 'voiceEnd') {
            await audioPlayer.setAudioContext(AudioContext());
            await audioPlayer.setBalance(0.0);
            //评测结束
b90a1518   liangchengyou   feat:练习接口逻辑完成
140
141
142
143
144
145
            if (kDebugMode) {
              print('评测结束');
            }
            return;
          }
  
1f5969b8   biao   修复 练习和绘本播放音频问题
146
147
148
149
150
          if (call.method == 'voiceFail') {
            //评测失败
            await audioPlayer.setAudioContext(AudioContext());
            await audioPlayer.setBalance(0.0);
  
b90a1518   liangchengyou   feat:练习接口逻辑完成
151
152
            EasyLoading.showToast('评测失败');
            return;
e3c2820c   liangchengyou   feat:先声SDK逻辑调整
153
154
          }
        });
2eb67dd4   liangchengyou   feat:调整代码
155
      });
624214d0   liangchengyou   feat:看题选字/选图UI和部分逻辑
156
157
158
    }
  
    @override
1f5969b8   biao   修复 练习和绘本播放音频问题
159
    Future<void> close() {
624214d0   liangchengyou   feat:看题选字/选图UI和部分逻辑
160
      pageController.dispose();
3c1d5c64   liangchengyou   feat:练习功能完成
161
      audioPlayer.release();
2eb67dd4   liangchengyou   feat:调整代码
162
      audioPlayer.dispose();
6b0947ca   吴启风   feat:绘本增加initVoic...
163
      _voiceXsCancel();
624214d0   liangchengyou   feat:看题选字/选图UI和部分逻辑
164
165
166
      return super.close();
    }
  
e3c2820c   liangchengyou   feat:先声SDK逻辑调整
167
    ///请求数据
1f5969b8   biao   修复 练习和绘本播放音频问题
168
169
    void _requestData(
        RequestDataEvent event, Emitter<TopicPictureState> emitter) async {
2eb67dd4   liangchengyou   feat:调整代码
170
      try {
45c862fd   吴启风   feat:闯关交互优化-答错晃动、...
171
172
        _entity = await ListenDao.process(courseLessonId);
        emitter(RequestDataState());
2eb67dd4   liangchengyou   feat:调整代码
173
174
      } catch (e) {
        if (e is ApiException) {
1f5969b8   biao   修复 练习和绘本播放音频问题
175
          showToast(e.message ?? '请求失败,请检查网络连接');
2eb67dd4   liangchengyou   feat:调整代码
176
177
178
179
        }
      }
    }
  
e3c2820c   liangchengyou   feat:先声SDK逻辑调整
180
    ///页面切换
1f5969b8   biao   修复 练习和绘本播放音频问题
181
182
    void _pageControllerChange(CurrentPageIndexChangeEvent event,
        Emitter<TopicPictureState> emitter) async {
819ae43b   吴启风   feat:体验优化-练习题目取消阻...
183
      await pageResetIfNeed();
aeafd474   吴启风   feat:选择题作答后播放音效&答...
184
185
186
      debugPrint('翻页 $_currentPage->${event.pageIndex}');
      if (_currentPage == _entity?.topics?.length) {
        return;
3c1d5c64   liangchengyou   feat:练习功能完成
187
      }
aeafd474   吴启风   feat:选择题作答后播放音效&答...
188
      _currentPage = event.pageIndex;
2eb67dd4   liangchengyou   feat:调整代码
189
      final topics = _entity?.topics?[_currentPage];
3c1d5c64   liangchengyou   feat:练习功能完成
190
      if (topics?.type != 3 && topics?.type != 4) {
2eb67dd4   liangchengyou   feat:调整代码
191
        if (topics?.audioUrl != null) {
1f5969b8   biao   修复 练习和绘本播放音频问题
192
          final urlStr = topics?.audioUrl ?? '';
2eb67dd4   liangchengyou   feat:调整代码
193
          if (urlStr.isNotEmpty) {
3c1d5c64   liangchengyou   feat:练习功能完成
194
195
            debugPrint(urlStr);
            await audioPlayer.play(UrlSource(urlStr));
2eb67dd4   liangchengyou   feat:调整代码
196
197
          }
        }
2eb67dd4   liangchengyou   feat:调整代码
198
      }
624214d0   liangchengyou   feat:看题选字/选图UI和部分逻辑
199
200
201
      emitter(CurrentPageIndexState());
    }
  
e3c2820c   liangchengyou   feat:先声SDK逻辑调整
202
    ///选择
1f5969b8   biao   修复 练习和绘本播放音频问题
203
204
    void _selectItemLoad(
        SelectItemEvent event, Emitter<TopicPictureState> emitter) async {
819ae43b   吴启风   feat:体验优化-练习题目取消阻...
205
206
207
208
209
      _optionSelectItem = event.selectIndex;
      emitter(SelectItemChangeState());
      if (checkAnswerRight(_optionSelectItem) == true) {
        /// 如果选择题答(选)对后题目没播完,则暂停播放题目。答错的话继续播放体验也不错
        await closePlayerResource();
642081ad   吴启风   feat:lottie动画加载优化...
210
        showStarRewardDialog(context);
819ae43b   吴启风   feat:体验优化-练习题目取消阻...
211
        await _playResultSound(true);
45c862fd   吴启风   feat:闯关交互优化-答错晃动、...
212
      } else {
819ae43b   吴启风   feat:体验优化-练习题目取消阻...
213
        await _playResultSound(false);
3c1d5c64   liangchengyou   feat:练习功能完成
214
      }
7652f701   liangchengyou   feat:课程购买UI逻辑
215
    }
2eb67dd4   liangchengyou   feat:调整代码
216
  
45c862fd   吴启风   feat:闯关交互优化-答错晃动、...
217
218
219
    ///为空则数据异常,用于是否晃动时需要
    bool? checkAnswerRight(int selectIndex) {
      CourseProcessTopics? topics = _entity?.topics?[_currentPage];
c623c7b2   吴启风   feat:语音跟读作答结果动效&语音
220
221
222
223
      if (topics == null ||
          topics.topicAnswerList == null ||
          selectIndex < 0 ||
          selectIndex >= topics.topicAnswerList!.length) {
45c862fd   吴启风   feat:闯关交互优化-答错晃动、...
224
225
226
227
228
229
230
        return null;
      }
      CourseProcessTopicsTopicAnswerList? answerList =
          topics.topicAnswerList?[selectIndex];
      return answerList?.correct != 0;
    }
  
3c1d5c64   liangchengyou   feat:练习功能完成
231
    ///初始化SDK
1f5969b8   biao   修复 练习和绘本播放音频问题
232
233
234
    _initVoiceSdk(
        XSVoiceInitEvent event, Emitter<TopicPictureState> emitter) async {
      methodChannel.invokeMethod('initVoiceSdk', event.data);
3c1d5c64   liangchengyou   feat:练习功能完成
235
236
    }
  
e3c2820c   liangchengyou   feat:先声SDK逻辑调整
237
    ///先声测试
1f5969b8   biao   修复 练习和绘本播放音频问题
238
239
    void _voiceXsStart(
        XSVoiceStartEvent event, Emitter<TopicPictureState> emitter) async {
53e9e6db   吴启风   feat:绘本语音评测逻辑
240
      await audioPlayer.stop();
354ac7e6   吴启风   feat:隐藏视频跟读入口、底部草...
241
      // 调用封装好的权限检查和请求方法
1e22b7d1   吴启风   feat:儿歌/视频环节接口请求时机优化
242
243
      bool result = await requestPermission(
          context, Permission.microphone, "录音", "用于开启录音,识别您的开口作答并给出反馈");
354ac7e6   吴启风   feat:隐藏视频跟读入口、底部草...
244
      if (result) {
1f5969b8   biao   修复 练习和绘本播放音频问题
245
246
247
248
249
        methodChannel.invokeMethod('startVoice', {
          'word': event.testWord,
          'type': event.type,
          'userId': event.userId.toString()
        });
d4d91cb0   吴启风   feat:lottie动效组件封装...
250
        _isRecording = true;
354ac7e6   吴启风   feat:隐藏视频跟读入口、底部草...
251
252
        emitter(XSVoiceTestState());
      }
e3c2820c   liangchengyou   feat:先声SDK逻辑调整
253
254
    }
  
3c1d5c64   liangchengyou   feat:练习功能完成
255
    ///终止评测
819ae43b   吴启风   feat:体验优化-练习题目取消阻...
256
    Future<void> _voiceXsStop(
1f5969b8   biao   修复 练习和绘本播放音频问题
257
        XSVoiceStopEvent event, Emitter<TopicPictureState> emitter) async {
3c1d5c64   liangchengyou   feat:练习功能完成
258
259
260
      methodChannel.invokeMethod('stopVoice');
    }
  
6b0947ca   吴启风   feat:绘本增加initVoic...
261
    ///取消评测(用于处理退出页面后录音未停止等异常情况的保护操作)
819ae43b   吴启风   feat:体验优化-练习题目取消阻...
262
263
264
265
    Future<void> _voiceXsCancel({bool force = false}) async {
      if (_isRecording || force) {
        methodChannel.invokeMethod('cancelVoice');
      }
6b0947ca   吴启风   feat:绘本增加initVoic...
266
267
    }
  
3c1d5c64   liangchengyou   feat:练习功能完成
268
    ///先声评测结果
1f5969b8   biao   修复 练习和绘本播放音频问题
269
270
    void _voiceXsResult(
        XSVoiceResultEvent event, Emitter<TopicPictureState> emitter) async {
819ae43b   吴启风   feat:体验优化-练习题目取消阻...
271
272
      _isRecording = false;
      emitter(XSVoiceTestState());
e3c2820c   liangchengyou   feat:先声SDK逻辑调整
273
      final Map args = event.message as Map;
cb38bc90   liangchengyou   feat:视频跟读逻辑处理
274
275
      final result = args['result'] as Map;
      final overall = result['overall'].toString();
c623c7b2   吴启风   feat:语音跟读作答结果动效&语音
276
      int score = int.parse(overall);
819ae43b   吴启风   feat:体验优化-练习题目取消阻...
277
278
279
      final voiceResult = VoiceResultType.fromScore(score);
      if (voiceResult.lottieFilePath != null) {
        showCheerRewardDialog(context, lottieFile: voiceResult.lottieFilePath!);
642081ad   吴启风   feat:lottie动画加载优化...
280
      }
819ae43b   吴启风   feat:体验优化-练习题目取消阻...
281
282
283
284
285
286
      await ClickWithMusicController.instance.playMusicAndPerformAction(
          context,
          voiceResult.audioType,
          () {
                if (isLastPage()) {showStepPage();};
              });
642081ad   吴启风   feat:lottie动画加载优化...
287
288
    }
  
aeafd474   吴启风   feat:选择题作答后播放音效&答...
289
    // 暂时没用上
1f5969b8   biao   修复 练习和绘本播放音频问题
290
291
    void _voicePlayStateChange(VoicePlayStateChangeEvent event,
        Emitter<TopicPictureState> emitter) async {
3c1d5c64   liangchengyou   feat:练习功能完成
292
293
294
      emitter(VoicePlayStateChange());
    }
  
aeafd474   吴启风   feat:选择题作答后播放音效&答...
295
    // 题目音频播放
1f5969b8   biao   修复 练习和绘本播放音频问题
296
297
    void _questionVoicePlay(
        VoicePlayEvent event, Emitter<TopicPictureState> emitter) async {
819ae43b   吴启风   feat:体验优化-练习题目取消阻...
298
      await pageResetIfNeed();
3c1d5c64   liangchengyou   feat:练习功能完成
299
      final topics = _entity?.topics?[_currentPage];
1f5969b8   biao   修复 练习和绘本播放音频问题
300
301
302
      final urlStr = topics?.audioUrl ?? '';
      await audioPlayer.play(UrlSource(urlStr),
          balance: 0.0, ctx: AudioContext());
3c1d5c64   liangchengyou   feat:练习功能完成
303
    }
aeafd474   吴启风   feat:选择题作答后播放音效&答...
304
  
819ae43b   吴启风   feat:体验优化-练习题目取消阻...
305
306
307
308
309
310
311
312
313
314
    /// 重置状态,音频播放、录音以及一些变量等。用于翻页,打断等场景
    Future<void> pageResetIfNeed() async {
      _optionSelectItem = -1;
      _isRecording = false;
      _voicePlayState = VoicePlayState.stop;
  
      await closePlayerResource();
      await _voiceXsCancel();
    }
  
aeafd474   吴启风   feat:选择题作答后播放音效&答...
315
    Future<void> closePlayerResource() async {
819ae43b   吴启风   feat:体验优化-练习题目取消阻...
316
      if (voicePlayState == VoicePlayState.playing) {
aeafd474   吴启风   feat:选择题作答后播放音效&答...
317
318
        await audioPlayer.stop();
      }
819ae43b   吴启风   feat:体验优化-练习题目取消阻...
319
      await ClickWithMusicController.instance.reset();
aeafd474   吴启风   feat:选择题作答后播放音效&答...
320
321
    }
  
22f36232   吴启风   feat:过渡页-练习环节
322
    ///播放选择结果音效
819ae43b   吴启风   feat:体验优化-练习题目取消阻...
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
    Future<void> _playResultSound(bool isCorrect) async {
      await ClickWithMusicController.instance.playMusicAndPerformAction(context,
          isCorrect ? AudioPlayerUtilType.right : AudioPlayerUtilType.wrong, () {
        if (isCorrect) {
          if (isLastPage()) {
            showStepPage();
          } else {
            // 答对后且播放完自动翻页
            pageController.nextPage(
              duration: const Duration(milliseconds: 250),
              curve: Curves.ease,
            );
          }
        }
      });
aeafd474   吴启风   feat:选择题作答后播放音效&答...
338
    }
22f36232   吴启风   feat:过渡页-练习环节
339
340
341
342
343
344
345
346
347
348
  
    ///是否是最后一页
    bool isLastPage() {
      return currentPage == _entity?.topics?.length;
    }
  
    ///展示过渡页
    void showStepPage() {
      ///如果最后一页是语音问答题,评测完后自动翻页
      sectionComplete(() {
1f5969b8   biao   修复 练习和绘本播放音频问题
349
350
351
352
353
354
        popPage(data: {
          'currentStep': currentPage,
          'courseLessonId': courseLessonId,
          'isCompleted': true,
          'nextSection': true
        });
22f36232   吴启风   feat:过渡页-练习环节
355
      }, againSectionTap: () {
22f36232   吴启风   feat:过渡页-练习环节
356
357
358
        pageController.jumpToPage(0);
      });
    }
7652f701   liangchengyou   feat:课程购买UI逻辑
359
  }