BasicInfoSender.vue 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. <script setup lang="ts">
  2. import { h } from 'vue';
  3. import { NumberOutlined } from '@ant-design/icons-vue';
  4. import { Button, Flex } from 'ant-design-vue';
  5. import { Sender } from 'ant-design-x-vue';
  6. import { useMessagesContext } from '@/modules/chat/composables';
  7. import { useGuideStore } from '@/stores';
  8. import type { GuideUser } from '@/stores/guide.store.ts';
  9. import type { MessageRendererEmits, MessageRendererProps } from '@/modules/chat/renderer/index.ts';
  10. import type { BasicInfoPickerKey, BasicInfoPickerModel } from '@/modules/chat/types';
  11. import type { BasicInfoPickerValue } from '@/modules/chat/config';
  12. import { basicInfoPickerGroup } from '@/modules/chat/config';
  13. const STRING_SEPARATOR = ', ';
  14. defineOptions({ inheritAttrs: false });
  15. interface Props extends MessageRendererProps {
  16. group?: Array<BasicInfoPickerKey | BasicInfoPickerValue>;
  17. }
  18. type Emits = MessageRendererEmits;
  19. const { group = [], complete } = defineProps<Props>();
  20. const emits = defineEmits<Emits>();
  21. const open = defineModel('open', { default: false });
  22. const senderInstance = useTemplateRef<AntXSenderInstance>('sender-ref');
  23. const pickers = shallowRef<BasicInfoPickerValue[]>([]);
  24. const pickerIndex = ref(0);
  25. const picker = computed(() => pickers.value.at(pickerIndex.value));
  26. watchEffect(() => {
  27. for (let item of group) {
  28. if (typeof item === 'string') item = Object.assign({ key: item }, basicInfoPickerGroup[item]);
  29. if (item) pickers.value.push(item);
  30. }
  31. triggerRef(pickers);
  32. pickerIndex.value = 0;
  33. });
  34. const pending = ref(false);
  35. watchEffect(() => {
  36. pending.value = !complete;
  37. if (complete) {
  38. onTrigger(true);
  39. nextTick(() => senderInstance.value?.focus({ cursor: 'end' }));
  40. }
  41. });
  42. const guide = useGuideStore();
  43. const { append } = useMessagesContext();
  44. const { loading, model } = toRefs(reactive({ loading: false, model: {} as BasicInfoPickerModel }));
  45. function onTrigger(key?: BasicInfoPickerKey | boolean) {
  46. if (key == null) open.value = !open.value;
  47. else if (typeof key === 'boolean') open.value = key;
  48. else {
  49. open.value = true;
  50. pickerIndex.value = pickers.value.findIndex((picker) => picker.key === key);
  51. }
  52. }
  53. const getDisplayValue = (values: string[] | string | undefined, columns: VantPickerColumn): string => {
  54. if (!Array.isArray(values)) values = values ? values.split(STRING_SEPARATOR) : [];
  55. return values
  56. .map((value, index) => {
  57. const column = Array.isArray(columns[index]) ? columns[index] : columns;
  58. return column.find((col) => col.value === value)?.text;
  59. })
  60. .join(STRING_SEPARATOR);
  61. };
  62. const displayValue = ref();
  63. const pickerInstance = useTemplateRef<VantPickerInstance>('picker-ref');
  64. const pickerProps = computed(() => {
  65. const { key, title, confirmButtonText = '确定', columns = [], defaultValue, clickOnConfirm = false, onConfirm } = picker.value ?? {};
  66. const value = model.value[key!] ?? defaultValue;
  67. displayValue.value = getDisplayValue(value, columns);
  68. return {
  69. title: typeof title === 'function' ? title(model.value) : title,
  70. columns,
  71. confirmButtonText,
  72. showToolbar: false,
  73. loading: pending.value,
  74. modelValue: value ? value.split(STRING_SEPARATOR) : [],
  75. 'onUpdate:modelValue'(value) {
  76. displayValue.value = getDisplayValue(value, columns);
  77. },
  78. onClickOption(event) {
  79. if (clickOnConfirm) setTimeout(() => unref(pickerProps).onConfirm(event), 0);
  80. },
  81. async onConfirm(event) {
  82. model.value[key!] = event.selectedValues.join(STRING_SEPARATOR);
  83. if (await onConfirm?.(model)) {
  84. open.value = false;
  85. senderProps.value.onSubmit();
  86. } else {
  87. const index = pickerIndex.value + 1;
  88. if (index === pickers.value.length) {
  89. open.value = false;
  90. senderProps.value.onSubmit();
  91. } else pickerIndex.value = index;
  92. }
  93. },
  94. } satisfies VantPickProps;
  95. });
  96. const senderProps = computed(() => {
  97. return {
  98. readOnly: true,
  99. value: open.value ? displayValue.value : ' ',
  100. loading: loading.value,
  101. disabled: pending.value,
  102. onSubmit() {
  103. if (open.value) return pickerInstance.value?.confirm();
  104. const key = pickers.value.find((picker) => model.value[picker.key] == null)?.key;
  105. if (key) onTrigger(key);
  106. else {
  107. pickerIndex.value = pickers.value.length - 1;
  108. open.value = false;
  109. onSubmit(pickers.value.map((picker) => getDisplayValue(model.value[picker.key], picker.columns!)).join(STRING_SEPARATOR));
  110. }
  111. },
  112. } satisfies AntXSenderProps;
  113. });
  114. const senderHeaderProps = computed(() => {
  115. const { title, confirmButtonText } = pickerProps.value;
  116. return {
  117. open: open.value,
  118. title: title
  119. ? h(Flex, { justify: 'space-between' }, () => [
  120. h('span', null, title),
  121. !!confirmButtonText &&
  122. h(
  123. Button,
  124. {
  125. type: 'text',
  126. size: 'small',
  127. style: { color: '#1677ff' },
  128. onClick() {
  129. pickerInstance.value?.confirm();
  130. },
  131. },
  132. () => confirmButtonText,
  133. ),
  134. ])
  135. : void 0,
  136. };
  137. });
  138. const prefixProps = computed(() => {
  139. return {
  140. type: 'text',
  141. icon: h(NumberOutlined),
  142. disabled: pending.value,
  143. } satisfies AntButtonProps;
  144. });
  145. async function onSubmit(content: string | VNode) {
  146. guide.updateUser(<GuideUser>model.value);
  147. append({ role: 'user', content });
  148. emits('next');
  149. }
  150. </script>
  151. <template>
  152. <Sender ref="sender-ref" v-bind="senderProps">
  153. <template #header>
  154. <Sender.Header v-bind="senderHeaderProps">
  155. <van-picker ref="picker-ref" v-bind="pickerProps" :key="picker?.key" />
  156. </Sender.Header>
  157. </template>
  158. <template #prefix>
  159. <a-flex>
  160. <template v-for="(picker, i) in pickers" :key="picker.key">
  161. <a-button v-if="i <= pickerIndex" :class="{ active: open && pickerIndex === i }" v-bind="prefixProps" @click="onTrigger(picker.key)">
  162. {{ open && i === pickerIndex ? '' : getDisplayValue(model[picker.key], picker.columns!) }}
  163. </a-button>
  164. </template>
  165. </a-flex>
  166. </template>
  167. </Sender>
  168. </template>
  169. <style scoped lang="scss">
  170. :deep(.ant-sender-prefix) {
  171. .ant-btn-text {
  172. &.active {
  173. color: #1677ff;
  174. }
  175. }
  176. }
  177. :deep(.ant-sender-header-content) {
  178. --van-picker-background: transparent;
  179. --van-picker-mask-color: transparent;
  180. --van-picker-group-background: transparent;
  181. --van-picker-loading-mask-color: transparent;
  182. .van-picker-column__item {
  183. &--selected {
  184. color: #1677ff;
  185. }
  186. }
  187. }
  188. </style>