ai-chat-bot-interface 1.5.8 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ai-chat-bot-interface",
3
3
  "private": false,
4
- "version": "1.5.8",
4
+ "version": "1.6.0",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "description": "A AI chat bot interface. (private)",
@@ -19,6 +19,7 @@
19
19
  "dependencies": {
20
20
  "markdown-it": "^14.1.0",
21
21
  "v-viewer": "^3.0.21",
22
+ "vant": "^4.9.21",
22
23
  "vue": "^3.5.13"
23
24
  },
24
25
  "devDependencies": {
package/src/App.vue CHANGED
@@ -5,8 +5,8 @@ import ChatUi from './ChatUi.vue';
5
5
  <template>
6
6
  <div style="width: 100vw; height: 100vh">
7
7
  <chat-ui
8
- bot-id="7485656400967516197"
9
- token="pat_gYSCaegGxJ7qZXl4fBkCc4KYhV1t7YPQ5PVwggWdZnFmSKtgSHs8SoebV4epdQvj"
8
+ bot-id="7572460298389962778"
9
+ token="sat_cSmj7ZyuePbkEpOKR7F259YuIfp2fjX9rJ3Om2O0gB9JzrGjAk8pSL269rbu8kzr"
10
10
  uid="262598"
11
11
  :def-msg="{ placeholder: '发消息...' }"
12
12
  :show-header="true"
package/src/ChatUi.vue CHANGED
@@ -4,11 +4,11 @@
4
4
  <div v-if="showHeader" class="cui_header">
5
5
  <div class="title" @click.stop="queryHistoryList">
6
6
  <div class="back" @click.stop="handleBack">
7
- <back-icon/>
7
+ <back-icon />
8
8
  </div>
9
9
  <div class="name_box">
10
10
  <div class="name">{{ name }}</div>
11
- <div v-if="nameSub" class="name_sub">{{ nameSub}}</div>
11
+ <div v-if="nameSub" class="name_sub">{{ nameSub }}</div>
12
12
  </div>
13
13
  </div>
14
14
  <div class="btn_group">
@@ -18,26 +18,26 @@
18
18
  </button>
19
19
  <template v-show="false">
20
20
  <button class="btn">
21
- <clear-icon/>
21
+ <clear-icon />
22
22
  </button>
23
23
  <button class="btn">
24
- <close-icon/>
24
+ <close-icon />
25
25
  </button>
26
26
  </template>
27
27
  </div>
28
28
  </div>
29
29
  <div class="cui_content">
30
30
  <div
31
- v-if="botInfo && botInfo.onboarding_info"
32
- style="text-align: left; margin-top: 50px"
31
+ v-if="botInfo && botInfo.onboarding_info"
32
+ style="text-align: left; margin-top: 50px"
33
33
  >
34
34
  <div style="text-align: center">
35
35
  <img
36
- :src="botInfo.icon_url"
37
- alt="icon"
38
- width="64"
39
- height="64"
40
- style="border-radius: 15px"
36
+ :src="botInfo.icon_url"
37
+ alt="icon"
38
+ width="64"
39
+ height="64"
40
+ style="border-radius: 15px"
41
41
  />
42
42
  <p class="board_name">{{ botInfo.name }}</p>
43
43
  </div>
@@ -45,11 +45,11 @@
45
45
  <div class="board_desc">{{ botInfo.onboarding_info.prologue }}</div>
46
46
  <div class="flexcss">
47
47
  <span
48
- v-for="(item, idx) in botInfo.onboarding_info.suggested_questions"
49
- :key="idx"
50
- class="board_sug"
51
- @click.stop="chatConv([{ content: item, text: item }])"
52
- >{{ item }}</span
48
+ v-for="(item, idx) in botInfo.onboarding_info.suggested_questions"
49
+ :key="idx"
50
+ class="board_sug"
51
+ @click.stop="chatConv([{ content: item, text: item }])"
52
+ >{{ item }}</span
53
53
  >
54
54
  </div>
55
55
  </div>
@@ -57,7 +57,7 @@
57
57
  <div v-if="conv.role === 'assistant'" class="replay role_sys">
58
58
  <div class="replay_content">
59
59
  <div class="name">
60
- <img class="avatar" :src="logo" alt="avatar"/>
60
+ <img class="avatar" :src="logo" alt="avatar" />
61
61
  {{ name }}
62
62
  <!--<span class="time">12:30</span>-->
63
63
  </div>
@@ -77,8 +77,14 @@
77
77
  </p>
78
78
  </template>
79
79
  <template v-if="conv.content">
80
- <assistant-replay v-if="contentTye === 'text'" :content="conv.content" />
81
- <markdown-viewer v-else-if="contentTye ==='markdown'" :content="conv.content" />
80
+ <assistant-replay
81
+ v-if="contentTye === 'text'"
82
+ :content="conv.content"
83
+ />
84
+ <markdown-viewer
85
+ v-else-if="contentTye === 'markdown'"
86
+ :content="conv.content"
87
+ />
82
88
  <template v-if="!isAnswering">
83
89
  <div v-if="conv.extra.length">
84
90
  <template v-for="(comp, idx) in conv.extra" :key="idx">
@@ -90,7 +96,9 @@
90
96
  {{ comp.planParse }}
91
97
  </p>-->
92
98
  <dishes-list
93
- v-if="finalCardList.dishes && comp.showType === 'card'"
99
+ v-if="
100
+ finalCardList.dishes && comp.showType === 'card'
101
+ "
94
102
  :sku-list="comp.skuList"
95
103
  :is-mini="!showHeader"
96
104
  :def-msg="finalDefMsg"
@@ -104,21 +112,34 @@
104
112
  @select="handleCardTap({ type: 'match' }, comp)"
105
113
  />
106
114
  <store-list
107
- v-if="finalCardList.store && comp.showType === 'store'"
115
+ v-if="
116
+ finalCardList.store && comp.showType === 'store'
117
+ "
108
118
  :list="comp.storeList"
109
119
  @select="handleStoreSel"
110
120
  />
121
+ <personal-form
122
+ v-if="
123
+ finalCardList.personalForm &&
124
+ comp.showType === 'personalForm'
125
+ "
126
+ @submit="chatConv"
127
+ />
111
128
  </template>
112
129
  </div>
113
130
  <div v-else-if="handleTextNeedBtn(conv.content)">
114
131
  <div
115
132
  class="cui_btn cui_btn_2"
116
133
  @click.stop="handleCardTap({ type: 'match' }, {})"
117
- >{{finalDefMsg.useSolution}}</div>
134
+ >
135
+ {{ finalDefMsg.useSolution }}
136
+ </div>
118
137
  </div>
119
138
  </template>
120
139
  </template>
121
- <loading-icon2 v-if="isAnswering"/>
140
+ <loading-icon2
141
+ v-if="isAnswering && index === historyList.length - 1"
142
+ />
122
143
  </div>
123
144
  </div>
124
145
  </div>
@@ -127,18 +148,18 @@
127
148
  <div class="name">
128
149
  User_{{ uid }}
129
150
  <!--<span class="time">12:30</span>-->
130
- <img class="avatar" :src="avatar" alt="avatar"/>
151
+ <img class="avatar" :src="avatar" alt="avatar" />
131
152
  </div>
132
153
  <div class="box">
133
154
  <p class="text" v-html="conv.content" />
134
155
  <div v-if="conv.extra.length">
135
- <imge-list :list="conv.extra"/>
156
+ <imge-list :list="conv.extra" />
136
157
  </div>
137
158
  </div>
138
159
  </div>
139
160
  </div>
140
161
  </template>
141
- <div ref="endTarget" style="height: 100px"/>
162
+ <div ref="endTarget" style="height: 100px" />
142
163
  </div>
143
164
  <operate-module
144
165
  v-model="inputText"
@@ -156,12 +177,12 @@
156
177
  </template>
157
178
 
158
179
  <script setup>
159
- import {computed, nextTick, onMounted, ref} from 'vue';
180
+ import { computed, nextTick, onMounted, ref } from 'vue';
160
181
  import ClearIcon from './components/icons/ClearIcon.vue';
161
182
  import CloseIcon from './components/icons/CloseIcon.vue';
162
183
  import NewSessionIcon from './components/icons/NewSessionIcon.vue';
163
184
  import SendIcon from './components/icons/SendIcon.vue';
164
- import {get, post} from './utils/request';
185
+ import { get, post } from './utils/request';
165
186
  import DishesCard from './components/DishesCard.vue';
166
187
  import DishesList from './components/DishesList.vue';
167
188
  import PlanCard from './components/PlanCard.vue';
@@ -175,8 +196,8 @@ import ThinkingIcon from './components/icons/ThinkingIcon.vue';
175
196
  import OkIcon from './components/icons/OkIcon.vue';
176
197
  import AssistantReplay from './components/assistantReplay/assistantReplay.vue';
177
198
  import StoreList from './components/StoreList/StoreList.vue';
178
- import MarkdownViewer from "./components/MarkdownPlan/MarkdownViewer.vue";
179
-
199
+ import MarkdownViewer from './components/MarkdownPlan/MarkdownViewer.vue';
200
+ import PersonalForm from '@/components/personalForm/personalForm.vue';
180
201
 
181
202
  const chatOptions = computed(() => {
182
203
  return {
@@ -190,6 +211,7 @@ const conversationId = ref('');
190
211
  const isAnswering = ref(false);
191
212
  const isReq = ref('00');
192
213
  const historyList = ref([]);
214
+ const isFirst = ref(false);
193
215
 
194
216
  const msgObj = {
195
217
  placeholder: '發消息⋯',
@@ -205,13 +227,14 @@ const msgObj = {
205
227
  thinking: '思考中...',
206
228
  thinkCompleted: '思考完成',
207
229
  useSolution: '用該方案配餐',
208
- aiTips: '【内容由AI生成,仅供参考】'
230
+ aiTips: '【内容由AI生成,仅供参考】',
209
231
  };
210
232
  const defCardList = {
211
233
  store: true,
212
234
  dishes: true,
213
235
  plan: true,
214
- }
236
+ personalForm: false,
237
+ };
215
238
 
216
239
  const props = defineProps({
217
240
  logo: {
@@ -254,9 +277,9 @@ const props = defineProps({
254
277
  tagList: {
255
278
  type: Array,
256
279
  default: () => [
257
- {name: '人工客服', value: 'kefu', type: 'chat', msg: '人工客服'},
258
- {name: '查看菜单', value: 'menu', type: 'call', msg: '查看菜单'},
259
- {name: '体检报告', value: 'exam', type: 'upload', msg: '体检报告'},
280
+ { name: '人工客服', value: 'kefu', type: 'chat', msg: '人工客服' },
281
+ { name: '查看菜单', value: 'menu', type: 'call', msg: '查看菜单' },
282
+ { name: '体检报告', value: 'exam', type: 'upload', msg: '体检报告' },
260
283
  ],
261
284
  },
262
285
  defMsg: {
@@ -265,7 +288,7 @@ const props = defineProps({
265
288
  },
266
289
  contentTye: {
267
290
  type: String,
268
- default: 'text'
291
+ default: 'text',
269
292
  },
270
293
  cardList: {
271
294
  type: Object,
@@ -273,12 +296,12 @@ const props = defineProps({
273
296
  store: true,
274
297
  dishes: true,
275
298
  plan: true,
276
- })
299
+ }),
277
300
  },
278
301
  storage: {
279
302
  type: String,
280
- default: 'sessionStorage'
281
- }
303
+ default: 'sessionStorage',
304
+ },
282
305
  });
283
306
 
284
307
  const Emits = defineEmits(['call']);
@@ -292,9 +315,9 @@ const finalDefMsg = computed(() => {
292
315
  const finalCardList = computed(() => {
293
316
  return {
294
317
  ...defCardList,
295
- ...props.cardList
296
- }
297
- })
318
+ ...props.cardList,
319
+ };
320
+ });
298
321
 
299
322
  const endTarget = ref(null);
300
323
  const inputText = ref('');
@@ -303,22 +326,22 @@ const botInfo = ref({});
303
326
  const storage = {
304
327
  setItem: (key, value) => {
305
328
  props.storage === 'sessionStorage'
306
- ? sessionStorage.setItem(key, value)
307
- : localStorage.setItem(key, value);
329
+ ? sessionStorage.setItem(key, value)
330
+ : localStorage.setItem(key, value);
308
331
  },
309
332
  getItem: (key) => {
310
333
  return props.storage === 'sessionStorage'
311
- ? sessionStorage.getItem(key)
312
- : localStorage.getItem(key);
313
- }
314
- }
315
-
334
+ ? sessionStorage.getItem(key)
335
+ : localStorage.getItem(key);
336
+ },
337
+ };
316
338
 
317
339
  onMounted(async () => {
318
340
  if (storage.getItem('conversationId')) {
319
341
  conversationId.value = storage.getItem('conversationId');
320
342
  await queryBotInfo();
321
343
  await queryHistoryList();
344
+ isFirst.value = false;
322
345
  } else {
323
346
  await createConv();
324
347
  }
@@ -326,10 +349,11 @@ onMounted(async () => {
326
349
  });
327
350
 
328
351
  const createConv = async () => {
352
+ isFirst.value = true;
329
353
  const res = await post(
330
- 'https://api.coze.cn/v1/conversation/create',
331
- {bot_id: props.botId, connector_id: '999'},
332
- {...chatOptions.value},
354
+ 'https://api.coze.cn/v1/conversation/create',
355
+ { bot_id: props.botId, connector_id: '999' },
356
+ { ...chatOptions.value },
333
357
  );
334
358
  console.log(res);
335
359
  if (res.code === 0 && res.data) {
@@ -405,10 +429,10 @@ const chatConv = async (data) => {
405
429
  // botId = '7474884145253023795';
406
430
  additional_messages.push({
407
431
  content: JSON.stringify(
408
- item.content.map((con) => ({
409
- type: con.type,
410
- file_id: con.file_id,
411
- })),
432
+ item.content.map((con) => ({
433
+ type: con.type,
434
+ file_id: con.file_id,
435
+ })),
412
436
  ),
413
437
  content_type: 'object_string',
414
438
  role: 'user',
@@ -434,21 +458,21 @@ const chatConv = async (data) => {
434
458
  }
435
459
  });
436
460
  historyList.value.push(uObj);
437
- isReq.value = '02'
461
+ isReq.value = '02';
438
462
  const res = await fetch(
439
- `https://api.coze.cn/v3/chat?conversation_id=${conversationId.value}`,
440
- {
441
- method: 'POST',
442
- headers: {
443
- ...chatOptions.value.headers,
444
- // 'Content-Type': 'text/event-stream',
445
- },
446
- body: JSON.stringify({
447
- bot_id: botId,
448
- user_id: props.uid,
449
- stream: true,
450
- connector_id: '999',
451
- additional_messages /*[
463
+ `https://api.coze.cn/v3/chat?conversation_id=${conversationId.value}`,
464
+ {
465
+ method: 'POST',
466
+ headers: {
467
+ ...chatOptions.value.headers,
468
+ // 'Content-Type': 'text/event-stream',
469
+ },
470
+ body: JSON.stringify({
471
+ bot_id: botId,
472
+ user_id: props.uid,
473
+ stream: true,
474
+ connector_id: '999',
475
+ additional_messages /*[
452
476
  {
453
477
  content: '[{"type":"image","file_id":"7475569020654436390"}]',
454
478
  content_type: 'object_string',
@@ -460,14 +484,14 @@ const chatConv = async (data) => {
460
484
  content: isInCode ? data.code : inText, // '配餐1600kcal,身高173,体重60kg,生成一天的套餐',
461
485
  },
462
486
  ]*/,
463
- custom_variables: {
464
- uid: props.uid,
465
- },
466
- }),
467
- credentials: 'same-origin', // 默认同源策略
468
- mode: 'cors', // 默认跨域模式
469
- cache: 'default', // 默认缓存策略
470
- },
487
+ custom_variables: {
488
+ uid: props.uid,
489
+ },
490
+ }),
491
+ credentials: 'same-origin', // 默认同源策略
492
+ mode: 'cors', // 默认跨域模式
493
+ cache: 'default', // 默认缓存策略
494
+ },
471
495
  );
472
496
  isReq.value = '03';
473
497
  historyList.value.push({
@@ -485,20 +509,30 @@ const chatConv = async (data) => {
485
509
  const reader = res.body.getReader();
486
510
  const decoder = new TextDecoder('utf-8');
487
511
  let buffer = '';
512
+
513
+ const handlePersonalForm = () => {
514
+ console.log('======= End ======', historyList.value);
515
+ if (isFirst.value) {
516
+ historyList.value[idx].extra.push({ showType: 'personalForm' });
517
+ isFirst.value = false;
518
+ }
519
+ };
520
+
488
521
  // 逐块读取数据
489
522
  while (true) {
490
- const {done, value} = await reader.read();
523
+ const { done, value } = await reader.read();
491
524
  if (done) {
492
525
  console.log('Stream has ended.');
493
526
  isAnswering.value = false;
494
527
  isReq.value = '00';
495
528
  historyList.value[idx].status = 'ended';
529
+ handlePersonalForm();
496
530
  scrollToEnd();
497
531
  break;
498
532
  }
499
533
 
500
534
  // 解码数据块
501
- const chunk = decoder.decode(value, {stream: true});
535
+ const chunk = decoder.decode(value, { stream: true });
502
536
  buffer += chunk;
503
537
 
504
538
  // 处理数据块
@@ -536,20 +570,20 @@ const chatConv = async (data) => {
536
570
  ) {
537
571
  historyList.value[idx].status = 'answering';
538
572
  historyList.value[idx].content = handleText(
539
- historyList.value[idx].content + strObj.content,
573
+ historyList.value[idx].content + strObj.content,
540
574
  );
541
575
  scrollToEnd();
542
576
  } else if (
543
- strObj.hasOwnProperty('type') &&
544
- strObj.type === 'tool_response'
577
+ strObj.hasOwnProperty('type') &&
578
+ strObj.type === 'tool_response'
545
579
  ) {
546
580
  const extraObj = JSON.parse(strObj.content);
547
581
  if (extraObj.hasOwnProperty('showType')) {
548
- historyList.value[idx].extra.push({...extraObj});
582
+ historyList.value[idx].extra.push({ ...extraObj });
549
583
  } else if (extraObj.hasOwnProperty('response_for_model')) {
550
584
  const modelObj = JSON.parse(extraObj.response_for_model);
551
585
  if (modelObj.hasOwnProperty('showType')) {
552
- historyList.value[idx].extra.push({...modelObj});
586
+ historyList.value[idx].extra.push({ ...modelObj });
553
587
  }
554
588
  }
555
589
  scrollToEnd();
@@ -562,9 +596,9 @@ const chatConv = async (data) => {
562
596
 
563
597
  const queryHistoryList = async () => {
564
598
  const res = await post(
565
- `https://api.coze.cn/v1/conversation/message/list?conversation_id=${conversationId.value}`,
566
- {order: 'asc'},
567
- {...chatOptions.value},
599
+ `https://api.coze.cn/v1/conversation/message/list?conversation_id=${conversationId.value}`,
600
+ { order: 'asc' },
601
+ { ...chatOptions.value },
568
602
  );
569
603
  if (res.code === 0 && res.data && res.data.length) {
570
604
  historyList.value = [];
@@ -593,7 +627,7 @@ const queryHistoryList = async () => {
593
627
  conversation_id: row.conversation_id,
594
628
  bot_id: row.bot_id,
595
629
  role: row.role,
596
- extra: [{...cardObj}],
630
+ extra: [{ ...cardObj }],
597
631
  });
598
632
  } else if (cardObj.hasOwnProperty('response_for_model')) {
599
633
  const modelObj = JSON.parse(cardObj.response_for_model);
@@ -603,7 +637,7 @@ const queryHistoryList = async () => {
603
637
  conversation_id: row.conversation_id,
604
638
  bot_id: row.bot_id,
605
639
  role: row.role,
606
- extra: [{...modelObj}],
640
+ extra: [{ ...modelObj }],
607
641
  });
608
642
  }
609
643
  }
@@ -611,8 +645,8 @@ const queryHistoryList = async () => {
611
645
  console.log('== 解析错误 sys==');
612
646
  }
613
647
  } else if (
614
- row.content_type === 'object_string' &&
615
- row.role === 'user'
648
+ row.content_type === 'object_string' &&
649
+ row.role === 'user'
616
650
  ) {
617
651
  try {
618
652
  const strObj = JSON.parse(row.content);
@@ -622,7 +656,7 @@ const queryHistoryList = async () => {
622
656
  bot_id: row.bot_id,
623
657
  role: row.role,
624
658
  meta_data: row.meta_data,
625
- extra: strObj.map((item) => ({...item, showType: item.type})),
659
+ extra: strObj.map((item) => ({ ...item, showType: item.type })),
626
660
  });
627
661
  } catch (e) {
628
662
  console.log('== 解析错误 user==');
@@ -632,15 +666,15 @@ const queryHistoryList = async () => {
632
666
  });
633
667
  cardList.forEach((card) => {
634
668
  const idx =
635
- card.role === 'user'
636
- ? historyList.value.findIndex(
637
- (c) =>
638
- c.meta_data.chat_group === card.meta_data.chat_group &&
639
- c.role === card.role,
640
- )
641
- : historyList.value.findIndex(
642
- (c) => c.chat_id === card.chat_id && c.role === card.role,
643
- );
669
+ card.role === 'user'
670
+ ? historyList.value.findIndex(
671
+ (c) =>
672
+ c.meta_data.chat_group === card.meta_data.chat_group &&
673
+ c.role === card.role,
674
+ )
675
+ : historyList.value.findIndex(
676
+ (c) => c.chat_id === card.chat_id && c.role === card.role,
677
+ );
644
678
  if (idx > -1) {
645
679
  historyList.value[idx].extra = [...card.extra];
646
680
  }
@@ -652,12 +686,12 @@ const queryHistoryList = async () => {
652
686
 
653
687
  const queryBotInfo = async () => {
654
688
  const res = await get(
655
- `https://api.coze.cn/v1/bot/get_online_info?bot_id=${props.botId}`,
656
- {...chatOptions.value},
689
+ `https://api.coze.cn/v1/bot/get_online_info?bot_id=${props.botId}`,
690
+ { ...chatOptions.value },
657
691
  );
658
692
  console.log(res);
659
693
  if (res.code === 0 && res.data) {
660
- botInfo.value = {...res.data};
694
+ botInfo.value = { ...res.data };
661
695
  }
662
696
  };
663
697
 
@@ -672,7 +706,7 @@ const handleText = (str) => {
672
706
  // );
673
707
  };
674
708
  const handleBack = () => {
675
- Emits('call', {type: 'back'});
709
+ Emits('call', { type: 'back' });
676
710
  };
677
711
  const handleStoreSel = (info) => {
678
712
  chatConv([{ content: info.content, text: info.text }]);
@@ -681,18 +715,18 @@ const handleTagSel = (info) => {
681
715
  console.log(info);
682
716
  switch (info.type) {
683
717
  case 'chat':
684
- chatConv([{content: info.msg, text: info.msg}]);
718
+ chatConv([{ content: info.msg, text: info.msg }]);
685
719
  break;
686
720
  case 'call':
687
- Emits('call', {...info});
721
+ Emits('call', { ...info });
688
722
  break;
689
723
  }
690
724
  };
691
725
 
692
- const handleCardTap = ({type}, info) => {
726
+ const handleCardTap = ({ type }, info) => {
693
727
  switch (type) {
694
728
  case 'change':
695
- chatConv([{content: '換一套菜品', text: '換一套菜品'}]);
729
+ chatConv([{ content: '換一套菜品', text: '換一套菜品' }]);
696
730
  break;
697
731
  case 'match':
698
732
  chatConv([
@@ -703,7 +737,7 @@ const handleCardTap = ({type}, info) => {
703
737
  ]);
704
738
  break;
705
739
  default:
706
- Emits('call', {type, info});
740
+ Emits('call', { type, info });
707
741
  }
708
742
  };
709
743
  const handleCall = (data) => {
@@ -721,8 +755,8 @@ const handleCall = (data) => {
721
755
  const handlePlanParse = (list) => {
722
756
  const pIdx = list.findIndex((p) => p.showType === 'plan');
723
757
  return pIdx > -1
724
- ? {show: true, text: list[pIdx].planParse}
725
- : {show: false};
758
+ ? { show: true, text: list[pIdx].planParse }
759
+ : { show: false };
726
760
  };
727
761
 
728
762
  const scrollToEnd = () => {
@@ -731,8 +765,6 @@ const scrollToEnd = () => {
731
765
  });
732
766
  };
733
767
 
734
-
735
-
736
768
  const handleTextNeedBtn = (str) => {
737
769
  const regExp = /#全日總熱量:(\d*?)kcal/g;
738
770
  return regExp.test(str);
@@ -8,7 +8,7 @@
8
8
  </div>
9
9
  </div>
10
10
  <div v-if="cate.list && cate.list.length" style="padding: 15px 0">
11
- <p class="title">{{ cate.categoryType }} {{ cate.totalKcal }}kcal</p>
11
+ <p class="title">{{ cate.categoryType }} {{ categoryEnergy(cate) }}kcal</p>
12
12
  <div>
13
13
  <template v-for="dList in cate.list" :key="dList.id">
14
14
  <dishes-card
@@ -62,6 +62,15 @@ const props = defineProps({
62
62
  }
63
63
  });
64
64
  const Emits = defineEmits(['select']);
65
+ const categoryEnergy = (cate) => {
66
+ let energy = 0;
67
+ (cate?.list || []).forEach(list => {
68
+ (list?.getCategoryList ||[]).forEach(item => {
69
+ energy += item.energy || 0;
70
+ })
71
+ })
72
+ return energy
73
+ }
65
74
  const showDate = (idx) => {
66
75
  return (
67
76
  props.skuList
@@ -0,0 +1,26 @@
1
+ <template>
2
+ <svg
3
+ width="1em"
4
+ height="1em"
5
+ viewBox="0 0 44 44"
6
+ version="1.1"
7
+ xmlns="http://www.w3.org/2000/svg"
8
+ xmlns:xlink="http://www.w3.org/1999/xlink"
9
+ >
10
+ <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
11
+ <g transform="translate(44, 0) rotate(90)">
12
+ <polyline
13
+ stroke="currentColor"
14
+ stroke-width="4"
15
+ stroke-linecap="round"
16
+ stroke-linejoin="round"
17
+ points="16 10 28 22 16 34"
18
+ />
19
+ </g>
20
+ </g>
21
+ </svg>
22
+ </template>
23
+
24
+ <script setup lang="ts"></script>
25
+
26
+ <style scoped></style>
@@ -0,0 +1,19 @@
1
+ <template>
2
+ <svg
3
+ width="1em"
4
+ height="1em"
5
+ viewBox="0 0 25 24"
6
+ fill="none"
7
+ xmlns="http://www.w3.org/2000/svg"
8
+ >
9
+ <path
10
+ d="M10.3193 16.5L14.8193 12L10.3193 7.5"
11
+ stroke="currentColor"
12
+ stroke-linecap="round"
13
+ />
14
+ </svg>
15
+ </template>
16
+
17
+ <script setup></script>
18
+
19
+ <style scoped></style>
@@ -0,0 +1,508 @@
1
+ <script setup>
2
+ import { computed, onMounted, ref } from 'vue';
3
+ import ArrowRight from '@/components/icons/ArrowRight.vue';
4
+ import Popup from '@/components/popup/popup.vue';
5
+ import { showToast } from 'vant';
6
+
7
+ const showPopup = ref(false);
8
+
9
+ const dialogInfo = ref({
10
+ key: 'sex',
11
+ sex: '',
12
+ age: '',
13
+ height: '',
14
+ weight: '',
15
+ sport: '',
16
+ taste: [],
17
+ taboo: [],
18
+ title: '标题',
19
+ unit: '',
20
+ dialogHeight: '50vh',
21
+ });
22
+
23
+ const tasteList = [
24
+ '不吃辣',
25
+ '不吃葱',
26
+ '不吃姜',
27
+ '不吃蒜',
28
+ '不吃糖',
29
+ '不吃鱼',
30
+ '不吃虾',
31
+ '不吃牛肉',
32
+ '不吃鸡肉',
33
+ '不吃猪肉',
34
+ ];
35
+ const tabooList = ['高GI', '高饱和脂肪酸', '高嘌呤', '高盐', '高胆固醇'];
36
+
37
+ const pfForm = ref([
38
+ { key: 'sex', label: '性别', value: '', placeholder: '请输选择' },
39
+ {
40
+ key: 'age',
41
+ label: '年龄',
42
+ value: '',
43
+ placeholder: '请输入年龄',
44
+ unit: '岁',
45
+ },
46
+ {
47
+ key: 'height',
48
+ label: '身高',
49
+ value: '',
50
+ placeholder: '请输入身高',
51
+ unit: 'cm',
52
+ },
53
+ {
54
+ key: 'weight',
55
+ label: '体重',
56
+ value: '',
57
+ placeholder: '请输入体重',
58
+ unit: 'kg',
59
+ },
60
+ { key: 'sport', label: '日常运动水平', value: '', placeholder: '请输入' },
61
+ {
62
+ key: 'taste',
63
+ label: '口味偏好及饮食禁忌',
64
+ value: '',
65
+ taste: [],
66
+ taboo: [],
67
+ placeholder: '请输入',
68
+ },
69
+ ]);
70
+
71
+ const Emits = defineEmits(['submit']);
72
+
73
+ onMounted(() => {
74
+ const form = localStorage.getItem('personalInfo');
75
+ if (form) {
76
+ pfForm.value = JSON.parse(form);
77
+ }
78
+ });
79
+
80
+ const isValid = computed(() => {
81
+ return pfForm.value
82
+ .filter((item) => item.key !== 'taste')
83
+ .every((item) => item.value !== '');
84
+ });
85
+
86
+ const handleSelect = (item) => {
87
+ dialogInfo.value.key = item.key;
88
+
89
+ switch (item.key) {
90
+ case 'sex':
91
+ dialogInfo.value.title = '选择性别';
92
+ dialogInfo.value.dialogHeight = '300px';
93
+ dialogInfo.value.sex = item.value;
94
+ break;
95
+ case 'age':
96
+ dialogInfo.value.title = '设置年龄';
97
+ dialogInfo.value.dialogHeight = '240px';
98
+ dialogInfo.value.unit = item.unit;
99
+ dialogInfo.value.age = item.value;
100
+ break;
101
+ case 'height':
102
+ dialogInfo.value.title = '设置身高';
103
+ dialogInfo.value.dialogHeight = '240px';
104
+ dialogInfo.value.unit = item.unit;
105
+ dialogInfo.value.height = item.value;
106
+ break;
107
+ case 'weight':
108
+ dialogInfo.value.title = '设置体重';
109
+ dialogInfo.value.dialogHeight = '240px';
110
+ dialogInfo.value.unit = item.unit;
111
+ dialogInfo.value.weight = item.value;
112
+ break;
113
+ case 'sport':
114
+ dialogInfo.value.title = '日常运动水平';
115
+ dialogInfo.value.dialogHeight = '360px';
116
+ dialogInfo.value.unit = '';
117
+ dialogInfo.value.sport = item.value;
118
+ break;
119
+ case 'taste':
120
+ dialogInfo.value.title = '口味偏好&饮食禁忌';
121
+ dialogInfo.value.dialogHeight = '500px';
122
+ dialogInfo.value.unit = '';
123
+ dialogInfo.value.taste = [...item.taste];
124
+ dialogInfo.value.taboo = [...item.taboo];
125
+ break;
126
+ }
127
+ console.log(dialogInfo.value, item);
128
+ showPopup.value = true;
129
+ };
130
+
131
+ const handleConfirm = () => {
132
+ switch (dialogInfo.value.key) {
133
+ case 'sex':
134
+ if (!dialogInfo.value.sex) {
135
+ showToast('请选择性别');
136
+ return;
137
+ }
138
+ pfForm.value.find((item) => item.key === 'sex').value =
139
+ dialogInfo.value.sex;
140
+ break;
141
+ case 'age':
142
+ if (!dialogInfo.value.age) {
143
+ showToast('请输入年龄');
144
+ return;
145
+ }
146
+ pfForm.value.find((item) => item.key === 'age').value =
147
+ dialogInfo.value.age;
148
+ break;
149
+ case 'height':
150
+ if (!dialogInfo.value.height) {
151
+ showToast('请输入身高');
152
+ return;
153
+ }
154
+ pfForm.value.find((item) => item.key === 'height').value =
155
+ dialogInfo.value.height;
156
+ break;
157
+ case 'weight':
158
+ if (!dialogInfo.value.weight) {
159
+ showToast('请输入体重');
160
+ return;
161
+ }
162
+ pfForm.value.find((item) => item.key === 'weight').value =
163
+ dialogInfo.value.weight;
164
+ break;
165
+ case 'sport':
166
+ if (!dialogInfo.value.sport) {
167
+ showToast('请选择日常运动水平');
168
+ return;
169
+ }
170
+ pfForm.value.find((item) => item.key === 'sport').value =
171
+ dialogInfo.value.sport;
172
+ break;
173
+ case 'taste':
174
+ pfForm.value.find((item) => item.key === 'taste').taste =
175
+ dialogInfo.value.taste;
176
+ pfForm.value.find((item) => item.key === 'taste').taboo =
177
+ dialogInfo.value.taboo;
178
+ pfForm.value.find((item) => item.key === 'taste').value = [
179
+ ...dialogInfo.value.taste,
180
+ ...dialogInfo.value.taboo,
181
+ ].join('、');
182
+ break;
183
+ }
184
+
185
+ showPopup.value = false;
186
+ };
187
+
188
+ const handleSubmit = () => {
189
+ if (!isValid.value) {
190
+ showToast('请填写完整信息');
191
+ return;
192
+ }
193
+ localStorage.setItem('personalInfo', JSON.stringify(pfForm.value));
194
+ const msg = `
195
+ 性别:${pfForm.value.find((item) => item.key === 'sex').value};
196
+ 年龄:${pfForm.value.find((item) => item.key === 'age').value}${
197
+ pfForm.value.find((item) => item.key === 'age').unit || ''
198
+ };
199
+ 身高:${pfForm.value.find((item) => item.key === 'height').value}${
200
+ pfForm.value.find((item) => item.key === 'height').unit || ''
201
+ };
202
+ 体重:${pfForm.value.find((item) => item.key === 'weight').value}${
203
+ pfForm.value.find((item) => item.key === 'weight').unit || ''
204
+ };
205
+ 日常运动水平:${
206
+ pfForm.value.find((item) => item.key === 'sport').value
207
+ };
208
+ 口味偏好及饮食禁忌:${
209
+ pfForm.value.find((item) => item.key === 'taste').value
210
+ ? pfForm.value.find((item) => item.key === 'taste').value
211
+ : '没有饮食禁忌'
212
+ };
213
+ `;
214
+ console.log(msg);
215
+ Emits('submit', [{ content: msg, text: msg }]);
216
+ };
217
+
218
+ const selectTag = (item, type) => {
219
+ if (dialogInfo.value[type].includes(item)) {
220
+ dialogInfo.value[type] = dialogInfo.value[type].filter((i) => i !== item);
221
+ } else {
222
+ dialogInfo.value[type].push(item);
223
+ }
224
+ };
225
+ </script>
226
+
227
+ <template>
228
+ <div class="pf_wrap">
229
+ <div class="pf_form">
230
+ <div v-for="item in pfForm" :key="item.key" class="pf_row">
231
+ <div class="title">{{ item.label }}</div>
232
+ <div class="content" @click="handleSelect(item)">
233
+ <span
234
+ class="text_row"
235
+ :style="{ color: item.value ? '#000' : '#75759d' }"
236
+ >{{
237
+ item.value ? `${item.value} ${item.unit || ''}` : item.placeholder
238
+ }}</span
239
+ >
240
+ <arrow-right class="arrow" />
241
+ </div>
242
+ </div>
243
+ </div>
244
+ <div
245
+ class="pf_btn"
246
+ :class="{ disabled: !isValid }"
247
+ @click.stop="handleSubmit"
248
+ >
249
+ 确定
250
+ </div>
251
+ </div>
252
+
253
+ <popup
254
+ v-model:show="showPopup"
255
+ :title="dialogInfo.title"
256
+ :height="dialogInfo.dialogHeight"
257
+ @confirm="handleConfirm"
258
+ >
259
+ <template #default>
260
+ <div v-if="dialogInfo.key === 'sex'" class="sex_wrap">
261
+ <div
262
+ :class="{ sex: true, selected: dialogInfo.sex === '男' }"
263
+ @click="dialogInfo.sex = '男'"
264
+ >
265
+
266
+ </div>
267
+ <div
268
+ :class="{ sex: true, selected: dialogInfo.sex === '女' }"
269
+ @click="dialogInfo.sex = '女'"
270
+ >
271
+
272
+ </div>
273
+ </div>
274
+ <div
275
+ v-if="['age', 'height', 'weight'].includes(dialogInfo.key)"
276
+ class="input_wrap"
277
+ >
278
+ <input
279
+ v-model="dialogInfo[dialogInfo.key]"
280
+ class="input"
281
+ type="text"
282
+ placeholder="请输入"
283
+ />
284
+ <div class="unit">{{ dialogInfo.unit }}</div>
285
+ </div>
286
+ <div v-if="dialogInfo.key === 'sport'" class="sport_wrap">
287
+ <div
288
+ :class="{ row: true, selected: dialogInfo.sport === '久坐' }"
289
+ @click.stop="dialogInfo.sport = '久坐'"
290
+ >
291
+ <div class="name">久坐</div>
292
+ </div>
293
+ <div
294
+ :class="{ row: true, selected: dialogInfo.sport === '轻体力' }"
295
+ @click.stop="dialogInfo.sport = '轻体力'"
296
+ >
297
+ <div class="name">轻体力</div>
298
+ </div>
299
+ <div
300
+ :class="{ row: true, selected: dialogInfo.sport === '中体力' }"
301
+ @click.stop="dialogInfo.sport = '中体力'"
302
+ >
303
+ <div class="name">中体力</div>
304
+ </div>
305
+ </div>
306
+ <div v-if="dialogInfo.key === 'taste'" class="taste_wrap">
307
+ <div class="title">口味偏好</div>
308
+ <div class="box">
309
+ <div
310
+ :class="{ tag: true, selected: dialogInfo.taste.includes(item) }"
311
+ v-for="item in tasteList"
312
+ :key="item"
313
+ @click.stop="selectTag(item, 'taste')"
314
+ >
315
+ {{ item }}
316
+ </div>
317
+ </div>
318
+ <div class="line"></div>
319
+ <div class="title">饮食禁忌</div>
320
+ <div class="box">
321
+ <div
322
+ :class="{ tag: true, selected: dialogInfo.taboo.includes(item) }"
323
+ v-for="item in tabooList"
324
+ :key="item"
325
+ @click.stop="selectTag(item, 'taboo')"
326
+ >
327
+ {{ item }}
328
+ </div>
329
+ </div>
330
+ </div>
331
+ </template>
332
+ </popup>
333
+ </template>
334
+
335
+ <style scoped lang="less">
336
+ .pf {
337
+ &_wrap {
338
+ width: 100%;
339
+ }
340
+
341
+ &_form {
342
+ padding: 0 15px;
343
+ background: #fff;
344
+ border-radius: 8px;
345
+ }
346
+
347
+ &_row {
348
+ display: flex;
349
+ flex-direction: row;
350
+ align-items: center;
351
+ justify-content: space-between;
352
+ border-bottom: 1px solid #dadada;
353
+ height: 54px;
354
+ line-height: 54px;
355
+
356
+ &:last-child {
357
+ border-bottom: none;
358
+ }
359
+
360
+ .title {
361
+ font-size: 14px;
362
+ font-weight: 600;
363
+ }
364
+
365
+ .content {
366
+ display: flex;
367
+ flex-direction: row;
368
+ align-items: center;
369
+ justify-content: flex-end;
370
+ }
371
+
372
+ .arrow {
373
+ color: #000;
374
+ font-size: 22px;
375
+ }
376
+ .text_row {
377
+ display: inline-block;
378
+ font-size: 14px;
379
+ text-align: right;
380
+ width: 160px;
381
+ text-overflow: ellipsis;
382
+ white-space: nowrap;
383
+ overflow: hidden;
384
+ }
385
+ .input {
386
+ font-size: 14px;
387
+ text-align: right;
388
+ outline: none;
389
+ border: none;
390
+ }
391
+ }
392
+
393
+ &_btn {
394
+ cursor: pointer;
395
+ height: 40px;
396
+ line-height: 40px;
397
+ border-radius: 20px;
398
+ text-align: center;
399
+ font-size: 14px;
400
+ font-weight: 600;
401
+ color: #000;
402
+ background: #00dc4e;
403
+ margin-top: 15px;
404
+
405
+ &.disabled {
406
+ color: #fff;
407
+ background: #d9d9d9;
408
+ }
409
+ }
410
+ }
411
+ .sex {
412
+ &_wrap {
413
+ padding: 50px 0;
414
+ .sex {
415
+ transition: 100ms ease-in-out;
416
+ font-size: 15px;
417
+ font-weight: 400;
418
+ text-align: center;
419
+ height: 58px;
420
+ line-height: 58px;
421
+ border-top: 1px solid #fff;
422
+ border-bottom: 1px solid #fff;
423
+ &.selected {
424
+ font-size: 20px;
425
+ font-weight: 600;
426
+ color: #039938;
427
+ border-color: #d9d9d9;
428
+ }
429
+ }
430
+ }
431
+ }
432
+ .input {
433
+ &_wrap {
434
+ text-align: center;
435
+ padding: 20px 0;
436
+ .input {
437
+ height: 50px;
438
+ line-height: 50px;
439
+ font-size: 36px;
440
+ text-align: center;
441
+ width: 200px;
442
+ outline: none;
443
+ border: none;
444
+ }
445
+ .unit {
446
+ font-size: 15px;
447
+ font-weight: 400;
448
+ line-height: 22px;
449
+ }
450
+ }
451
+ }
452
+ .sport {
453
+ &_wrap {
454
+ padding: 20px 0;
455
+
456
+ .row {
457
+ transition: 100ms ease-in-out;
458
+ font-size: 15px;
459
+ font-weight: 400;
460
+ text-align: center;
461
+ height: 58px;
462
+ line-height: 58px;
463
+ border-top: 1px solid #fff;
464
+ border-bottom: 1px solid #fff;
465
+
466
+ &.selected {
467
+ font-size: 20px;
468
+ font-weight: 600;
469
+ color: #039938;
470
+ border-color: #d9d9d9;
471
+ }
472
+ }
473
+ }
474
+ }
475
+ .taste {
476
+ &_wrap {
477
+ padding: 20px;
478
+ .title {
479
+ font-size: 15px;
480
+ font-weight: 600;
481
+ line-height: 32px;
482
+ margin-bottom: 16px;
483
+ }
484
+ .box {
485
+ display: inline-flex;
486
+ flex-wrap: wrap;
487
+ gap: 10px;
488
+ }
489
+ .line {
490
+ border-top: 1px solid #e9e9e9;
491
+ margin: 25px 0;
492
+ }
493
+ .tag {
494
+ height: 32px;
495
+ line-height: 32px;
496
+ padding: 0 15px;
497
+ background-color: #f3f4f5;
498
+ border-radius: 16px;
499
+ font-size: 14px;
500
+ text-align: center;
501
+ &.selected {
502
+ color: #fff;
503
+ background-color: #000;
504
+ }
505
+ }
506
+ }
507
+ }
508
+ </style>
@@ -0,0 +1,178 @@
1
+ <script setup>
2
+ import { computed, ref, watch } from 'vue';
3
+ import ArrowDown from '@/components/icons/ArrowDown.vue';
4
+
5
+ const props = defineProps({
6
+ show: {
7
+ type: Boolean,
8
+ default: false,
9
+ },
10
+ height: {
11
+ type: String,
12
+ default: '50vh',
13
+ },
14
+ title: {
15
+ type: String,
16
+ default: '',
17
+ },
18
+ });
19
+ const emit = defineEmits(['update:show', 'confirm', 'close']);
20
+
21
+ const visibleEle = ref(false);
22
+ const bottomPosi = ref(`-${props.height}`);
23
+ const animationInfo = ref({
24
+ delay: 300,
25
+ bottom: `-${props.height}`,
26
+ opacity: 0,
27
+ });
28
+ const handleAnimation = (type) => {
29
+ if (type) {
30
+ animationInfo.value.bottom = '0';
31
+ animationInfo.value.opacity = 0.5;
32
+ } else {
33
+ animationInfo.value.bottom = `-${props.height}`;
34
+ animationInfo.value.opacity = 0;
35
+ }
36
+ };
37
+ const handleConfirm = () => {
38
+ emit('confirm', {});
39
+ };
40
+
41
+ watch(
42
+ () => props.show,
43
+ (newVal, oldValue) => {
44
+ if (newVal) {
45
+ bottomPosi.value = `-${props.height}`;
46
+ visibleEle.value = true;
47
+ setTimeout(() => {
48
+ handleAnimation(true);
49
+ }, 0);
50
+ } else {
51
+ handleAnimation(false);
52
+ setTimeout(() => {
53
+ visibleEle.value = false;
54
+ }, 300);
55
+ }
56
+ },
57
+ );
58
+ const handleClose = () => {
59
+ emit('update:show', false);
60
+ handleAnimation(false);
61
+ setTimeout(() => {
62
+ visibleEle.value = false;
63
+ }, 300);
64
+ };
65
+ </script>
66
+
67
+ <template>
68
+ <div
69
+ v-if="visibleEle"
70
+ class="pup_mask"
71
+ :style="{
72
+ backgroundColor: `rgba(0, 0, 0, ${animationInfo.opacity})`,
73
+ }"
74
+ @click.stop="handleClose"
75
+ >
76
+ <div
77
+ class="pup_wrap"
78
+ :style="{ height: height, bottom: animationInfo.bottom }"
79
+ @click.stop="() => {}"
80
+ >
81
+ <div class="pup_header">
82
+ <div class="left">
83
+ <div class="icon_down" @click.stop="handleClose">
84
+ <arrow-down />
85
+ </div>
86
+ </div>
87
+ <div class="title">{{ title }}</div>
88
+ <div class="right">
89
+ <div class="btn" @click="handleConfirm">确定</div>
90
+ </div>
91
+ </div>
92
+ <div class="pup_content" :style="{ height: `calc(height - 56px)` }">
93
+ <slot />
94
+ </div>
95
+ </div>
96
+ </div>
97
+ </template>
98
+
99
+ <style scoped lang="less">
100
+ @delay: 200ms;
101
+ @headerHeight: 56px;
102
+ .pup {
103
+ &_mask {
104
+ transition: background-color @delay ease-in-out;
105
+ width: 100vw;
106
+ height: 100vh;
107
+ position: fixed;
108
+ top: 0;
109
+ left: 0;
110
+ z-index: 899;
111
+ }
112
+ &_wrap {
113
+ transition: bottom @delay ease-in-out;
114
+ width: 100%;
115
+ height: 50vh;
116
+ background: #fff;
117
+ border-radius: 16px 16px 0 0;
118
+ overflow: hidden;
119
+ position: absolute;
120
+ bottom: 0;
121
+ left: 0;
122
+ z-index: 900;
123
+ }
124
+ &_header {
125
+ display: flex;
126
+ flex-direction: row;
127
+ align-items: center;
128
+ justify-content: space-between;
129
+ height: @headerHeight;
130
+ line-height: @headerHeight;
131
+ background-color: #fff;
132
+ position: relative;
133
+
134
+ .icon_down {
135
+ padding: 0 16px;
136
+ font-size: 24px;
137
+ }
138
+ .title {
139
+ width: 100%;
140
+ text-align: center;
141
+ font-size: 16px;
142
+ font-weight: 600;
143
+ }
144
+ .left {
145
+ height: @headerHeight;
146
+ display: flex;
147
+ flex-direction: row;
148
+ align-items: center;
149
+ justify-content: flex-start;
150
+ position: absolute;
151
+ left: 0;
152
+ top: 0;
153
+ z-index: 5;
154
+ }
155
+ .right {
156
+ height: @headerHeight;
157
+ display: flex;
158
+ flex-direction: row;
159
+ align-items: center;
160
+ justify-content: flex-end;
161
+ position: absolute;
162
+ right: 0;
163
+ top: 0;
164
+ z-index: 5;
165
+ }
166
+ .btn {
167
+ cursor: pointer;
168
+ padding: 0 16px;
169
+ height: 32px;
170
+ line-height: 32px;
171
+ margin-right: 16px;
172
+ text-align: center;
173
+ border-radius: 16px;
174
+ background-color: #00dc4e;
175
+ }
176
+ }
177
+ }
178
+ </style>
package/src/main.js CHANGED
@@ -2,6 +2,7 @@ import { createApp } from 'vue';
2
2
  import './style.css';
3
3
  import App from './App.vue';
4
4
  import VueViewer from 'v-viewer';
5
+ import 'vant/lib/index.css';
5
6
 
6
7
  createApp(App)
7
8
  .use(VueViewer, {