From 787ccc59ba89bacc075562a161ecf02bc76ebadc Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期五, 24 七月 2026 17:15:53 +0800
Subject: [PATCH] 银川 1.采购申请添加入库状态查询条件

---
 src/views/im/home/components/user/recommend-card-dialog.vue |  322 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 322 insertions(+), 0 deletions(-)

diff --git a/src/views/im/home/components/user/recommend-card-dialog.vue b/src/views/im/home/components/user/recommend-card-dialog.vue
new file mode 100644
index 0000000..8c74cb1
--- /dev/null
+++ b/src/views/im/home/components/user/recommend-card-dialog.vue
@@ -0,0 +1,322 @@
+<script lang="ts" setup>
+import type { Conversation, FriendLite } from '../../types'
+
+import { computed, ref } from 'vue'
+
+import { IconifyIcon as Icon } from '#/packages/icons/src'
+
+import { Button, Input, message, Modal } from 'ant-design-vue'
+
+import { createGroup } from '#/api/im/group'
+import { CardBubble } from '#/views/im/home/components/card'
+
+import { ImContentType, ImConversationType, isGroupConversation } from '../../../utils/constants'
+import { getConversationKey } from '../../../utils/conversation'
+import { buildDefaultGroupName } from '../../../utils/group'
+import { type CardTarget, serializeMessage } from '../../../utils/message'
+import { getGroupDisplayName, isGroupQuit } from '../../../utils/user'
+import { useMessageSender } from '../../composables/useMessageSender'
+import { FacePicker } from '../../pages/conversation/components/input'
+import { useConversationStore } from '../../store/conversationStore'
+import { useFriendStore } from '../../store/friendStore'
+import { useGroupStore } from '../../store/groupStore'
+import { ConversationPickerPanel } from '../picker'
+import { FriendPickerPanel } from '../picker'
+
+defineOptions({ name: 'ImRecommendCardDialog' })
+
+const conversationStore = useConversationStore()
+const friendStore = useFriendStore()
+const groupStore = useGroupStore()
+const { sendRaw, send } = useMessageSender()
+
+const visible = ref(false)
+const target = ref<CardTarget | null>(null)
+const view = ref<'contact' | 'conversation'>('conversation') // 褰撳墠瑙嗗浘锛氶粯璁や細璇濋�夋嫨锛屻�屽垱寤鸿亰澶┿�嶅叆鍙e垏鍒板ソ鍙嬮�夋嫨
+const selectedKeys = ref<string[]>([])
+const selectedFriendIds = ref<number[]>([])
+const leaveMessage = ref('')
+const sending = ref(false)
+const emojiVisible = ref(false) // 琛ㄦ儏闈㈡澘鏄鹃殣锛氬彸渚� smile icon 鍒囨崲
+
+defineExpose({
+  /** 鎵撳紑鎺ㄨ崘寮圭獥锛歳eset 鈫� 鐏屽弬 鈫� visible=true */
+  open(opts: { target: CardTarget }) {
+    target.value = opts.target
+    view.value = 'conversation'
+    selectedKeys.value = []
+    selectedFriendIds.value = []
+    leaveMessage.value = ''
+    emojiVisible.value = false
+    sending.value = false
+    visible.value = true
+  }
+})
+
+/** 寮圭獥鏍囬锛氫細璇濊鍥炬寜 target 绫诲瀷鍒嗘枃妗堬紱濂藉弸瑙嗗浘鍥哄畾涓恒�岄�夋嫨濂藉弸銆� */
+const headerTitle = computed(() => {
+  if (view.value === 'contact') {
+    return '閫夋嫨濂藉弸'
+  }
+  return isGroupConversation(target.value?.targetType) ? '鎶婅繖涓兢鎺ㄨ崘缁欐湅鍙�' : '鎶婁粬鎺ㄨ崘缁欐湅鍙�'
+})
+
+/** 鍊欓�変細璇濓細浠� store 鎷挎帓搴忓悗鐨勫垪琛紙hide 鐢� Panel 鎺� hideKeys 杩囨护锛夛紱鍘嗗彶閫�缇ょ兢涓嶅彲琚帹鑽愰�変腑锛堥�変簡鍚庣涔熶細鎷掞級 */
+const candidateConversations = computed<Conversation[]>(() =>
+  conversationStore.getSortedConversationList.filter(
+    (conversation) =>
+      !(
+        conversation.type === ImConversationType.GROUP &&
+        isGroupQuit(groupStore.getGroup(conversation.targetId))
+      )
+  )
+)
+
+/** 闅愯棌 key锛氫笉鑳芥妸鍚嶇墖鎺ㄥ洖鍚嶇墖鏈韩鐨勪細璇濓紙鐢ㄦ埛鍚嶇墖閬垮厤鑷帹銆佺兢鍚嶇墖閬垮厤鎺ㄥ洖璇ョ兢锛� */
+const hideKeys = computed<string[]>(() => {
+  const t = target.value
+  if (!t) {
+    return []
+  }
+  return [getConversationKey({ type: t.targetType, targetId: t.targetId })]
+})
+
+/** 濂藉弸瑙嗗浘鍊欓�夊垪琛細鐩存帴澶嶇敤 friendStore Lite 瑙嗗浘 */
+const friends = computed<FriendLite[]>(() => friendStore.getActiveFriendLiteList)
+
+/** 鎶婇�変腑鐨� emoji 鎷煎埌鐣欒█鏈熬锛汧acePicker 鑷韩璐熻矗鍏抽棴闈㈡澘 */
+function handleEmojiSelect(emoji: string) {
+  leaveMessage.value = `${leaveMessage.value}${emoji}`
+}
+
+/** 鍒囧埌濂藉弸瑙嗗浘锛氭竻鎺変箣鍓嶅湪浼氳瘽瑙嗗浘杈撳叆鐨勭暀瑷�锛岄伩鍏嶅湪涓嶅彲瑙佽緭鍏ユ閲屾妸鐣欒█闈欓粯鍙戝埌鏂扮兢 */
+function handleSwitchToContact() {
+  view.value = 'contact'
+  leaveMessage.value = ''
+  emojiVisible.value = false
+}
+
+/**
+ * 纭鍙戦�侊紙浼氳瘽瑙嗗浘锛夛細姣忎釜閫変腑浼氳瘽鍏堝彂 CARD锛孋ARD 鎴愬姛鍚庢墠鍙戠暀瑷�锛堜繚璇併�屽厛鐪嬪埌鍚嶇墖銆嶇殑椤哄簭鎰忓浘锛�
+ *
+ * 鏂囨鑱氬悎锛氬叏閮ㄦ垚鍔熴�屽凡杞彂銆嶃�佸叏閮ㄥけ璐ャ�岃浆鍙戝け璐ワ細A銆丅銆嶃�侀儴鍒嗗け璐ャ�屽凡杞彂锛屼絾 X銆乊 澶辫触銆嶏紱
+ * 澶辫触鐨勬秷鎭互 FAILED 鐘舵�佺暀鍦ㄥ搴斾細璇濇皵娉¢噷锛屽彲鍙抽敭閲嶈瘯
+ */
+async function handleSend() {
+  const card = target.value
+  if (!card?.targetId || selectedKeys.value.length === 0) {
+    return
+  }
+  const byKey = new Map(candidateConversations.value.map((c) => [getConversationKey(c), c]))
+  const targets = selectedKeys.value
+    .map((key) => byKey.get(key))
+    .filter((c): c is Conversation => c != null)
+  if (targets.length === 0) {
+    return
+  }
+  const cardContent = serializeMessage({ ...card })
+  const leaveText = leaveMessage.value.trim()
+  sending.value = true
+  try {
+    const tasks = targets.map(async (conversation) => {
+      const cardOk = await sendRaw(ImContentType.CARD, cardContent, { conversation })
+      if (!cardOk) {
+        return { conversation, ok: false }
+      }
+      const ok = leaveText ? await send(leaveText, { conversation }) : true
+      return { conversation, ok }
+    })
+    const results = await Promise.all(tasks)
+    const failedNames = results.filter((r) => !r.ok).map((r) => r.conversation.name || '鏈懡鍚嶄細璇�')
+    // 鎶婂懡涓殑鐩爣鎺ㄥ埌鏈�杩戣浆鍙戝垪琛紙閮ㄥ垎澶辫触涔熸帹锛氱敤鎴风殑"鎰忓浘"宸茶〃杈撅級
+    conversationStore.pushRecentForwardConversationKeyList(targets.map((c) => getConversationKey(c)))
+    if (failedNames.length === 0) {
+      message.success('宸茶浆鍙�')
+    } else if (failedNames.length === targets.length) {
+      message.error(`杞彂澶辫触锛�${failedNames.join('銆�')}`)
+    } else {
+      message.warning(`宸茶浆鍙戯紝浣� ${failedNames.join('銆�')} 澶辫触`)
+    }
+    visible.value = false
+  } finally {
+    sending.value = false
+  }
+}
+
+/**
+ * 濂藉弸瑙嗗浘鍙戦�侊細鍏堝缓缇わ紙鍚屾椂閭�璇锋墍閫夊ソ鍙嬶級鈫� 缁欐柊缇ゅ彂鍚嶇墖 鈫� 鍙戠暀瑷� 鈫� 鍏冲脊绐�
+ *
+ * 璺熶細璇濊鍥剧殑宸埆锛氬厛瑕� createGroup 鎷垮埌 groupId锛屼箣鍚庢瀯閫犱竴涓� GROUP 绫诲瀷鐨� conversation 瀵硅薄缁� sendRaw 鐢�
+ * 锛坰endRaw 鍐呴儴浼氳嚜鍔� insertMessage 鎶婃柊缇ょ櫥璁拌繘 store锛屾渶杩戣浆鍙戝垪琛ㄤ篃鑳芥甯告帹锛�
+ */
+async function handleCreateGroupAndSend() {
+  const card = target.value
+  if (!card?.targetId || selectedFriendIds.value.length === 0) {
+    return
+  }
+  const byId = new Map(friends.value.map((f) => [f.id, f]))
+  const members = selectedFriendIds.value
+    .map((id) => byId.get(id))
+    .filter((f): f is FriendLite => f != null)
+  if (members.length === 0) {
+    return
+  }
+  sending.value = true
+  try {
+    const memberUserIds = members.map((m) => m.id)
+    const name = buildDefaultGroupName(members)
+    const group = await createGroup({ name, memberUserIds, joinApproval: false })
+    if (!group?.id) {
+      throw new Error('鍒涘缓缇ゅけ璐ワ細鏈繑鍥炵兢缂栧彿')
+    }
+    // upsert 杩� groupStore锛岀渷涓�娆� fetchGroupList
+    groupStore.upsertGroup({
+      id: group.id,
+      name: group.name,
+      avatar: group.avatar,
+      notice: group.notice,
+      ownerUserId: group.ownerUserId
+    })
+    // 缁欐柊缇ゆ瀯閫犱竴涓复鏃� conversation 瀵硅薄缁� sendRaw 鐢紱sendRaw 鍐呴儴浼氳嚜鍔� insertMessage 鐧昏
+    const newConversation: Conversation = {
+      type: ImConversationType.GROUP,
+      targetId: group.id,
+      name: getGroupDisplayName(group) || name,
+      avatar: group.avatar || '',
+      unreadCount: 0,
+      lastContent: '',
+      lastSendTime: 0
+    }
+    const cardOk = await sendRaw(ImContentType.CARD, serializeMessage({ ...card }), {
+      conversation: newConversation
+    })
+    if (!cardOk) {
+      message.warning('缇ゅ凡鍒涘缓锛屼絾鍚嶇墖鍙戦�佸け璐ワ紝璇风◢鍚庡湪缇ら噷閲嶈瘯')
+      visible.value = false
+      return
+    }
+    const leaveText = leaveMessage.value.trim()
+    if (leaveText) {
+      await send(leaveText, { conversation: newConversation })
+    }
+    conversationStore.pushRecentForwardConversationKeyList([getConversationKey(newConversation)])
+    message.success('宸插垱寤虹兢鑱婂苟鍙戦��')
+    visible.value = false
+  } finally {
+    sending.value = false
+  }
+}
+</script>
+
+<template>
+  <!--
+    鎶婂悕鐗囨帹鑽愮粰鏈嬪弸锛堢敤鎴� / 缇ら�氱敤锛�
+    - dialog 澹崇敱鏈粍浠舵寔鏈夛紱閫夋嫨 UI 濮旀墭 ConversationPickerPanel / FriendPickerPanel
+    - view='conversation'锛氶�夊凡鏈変細璇濆彂鍚嶇墖锛堥粯璁よ鍥撅級
+    - view='contact'锛氫粠銆屽垱寤鸿亰澶┿�嶅叆鍙h繘鍏ワ紝閫夊ソ鍙嬪缓缇ゅ啀鍙戝悕鐗囷紝涓氬姟澹冲眰鍒囪鍥撅紝涓や釜 Panel 浜掍笉鐭ラ亾瀵规柟
+    - 閫� 1 璧般�屽彂閫併�嶃�佸涓蛋銆屽垎鍒彂閫�(n)銆嶆枃妗堬紝涓庡井淇′竴鑷�
+    - 澶辫触鐨勬秷鎭互 FAILED 鐘舵�佺暀鍦ㄥ搴斾細璇濇皵娉¢噷锛屼緵鍙抽敭閲嶈瘯
+    - 瀵瑰鎺ュ彛锛歳ef + open({ target })锛屼笉鍐嶈蛋 v-model
+  -->
+  <Modal
+    v-model:open="visible"
+    width="720px"
+    :mask-closable="false"
+    :footer="view === 'conversation' ? null : undefined"
+    class="im-picker-dialog im-recommend-dialog"
+  >
+    <template #header>
+      <div class="flex gap-2 items-center">
+        <Icon
+          v-if="view === 'contact'"
+          icon="ant-design:arrow-left-outlined"
+          :size="16"
+          class="cursor-pointer text-[var(--ant-color-text-secondary)] transition-colors duration-150 hover:text-[var(--ant-color-primary)]"
+          @click="view = 'conversation'"
+        />
+        <span class="text-base text-[var(--ant-color-text)]">
+          {{ headerTitle }}
+        </span>
+      </div>
+    </template>
+
+    <div class="h-[480px]">
+      <!-- 浼氳瘽瑙嗗浘锛氶�夊凡鏈変細璇濆彂閫� -->
+      <ConversationPickerPanel
+        v-if="view === 'conversation'"
+        v-model:selected-keys="selectedKeys"
+        :conversations="candidateConversations"
+        :recent-forward-conversation-keys="conversationStore.recentForwardConversationKeys"
+        :hide-keys="hideKeys"
+        :show-create-chat="true"
+        @create-chat="handleSwitchToContact"
+        @remove-recent="conversationStore.removeRecentForwardConversationKey"
+      >
+        <template #footer>
+          <div class="flex flex-col gap-3 px-4 py-3">
+            <!-- 鍚嶇墖棰勮鍗� -->
+            <CardBubble v-if="target" :card="target" />
+
+            <!-- 鐣欒█锛堝崟琛岋級锛氬彸渚ц〃鎯呮寜閽Е鍙� FacePicker锛涢�変腑 emoji 鎷煎埌鏈熬 -->
+            <div class="relative">
+              <Input v-model:value="leaveMessage" :maxlength="100" placeholder="缁欐湅鍙嬬暀瑷�">
+                <template #suffix>
+                  <Icon
+                    icon="ant-design:smile-outlined"
+                    :size="18"
+                    class="cursor-pointer text-[var(--ant-color-text-secondary)] hover:text-[var(--ant-color-primary)]"
+                    @click.stop="emojiVisible = !emojiVisible"
+                  />
+                </template>
+              </Input>
+              <FacePicker
+                v-model:visible="emojiVisible"
+                mode="emoji"
+                class="bottom-full right-0 mb-2"
+                @select-emoji="handleEmojiSelect"
+              />
+            </div>
+
+            <!-- 鎿嶄綔鎸夐挳锛氶�� 0/1 鏄剧ず銆屽彂閫併�嶃�佸涓樉绀恒�屽垎鍒彂閫�(n)銆� -->
+            <div class="flex gap-2 justify-end">
+              <Button @click="visible = false">鍙栨秷</Button>
+              <Button
+                type="primary"
+                :loading="sending"
+                :disabled="selectedKeys.length === 0"
+                @click="handleSend"
+              >
+                {{ selectedKeys.length > 1 ? `鍒嗗埆鍙戦�侊紙${selectedKeys.length}锛塦 : '鍙戦��' }}
+              </Button>
+            </div>
+          </div>
+        </template>
+      </ConversationPickerPanel>
+
+      <!-- 濂藉弸瑙嗗浘锛氶�夊ソ鍙嬪缓缇ゅ悗鍙戦�� -->
+      <FriendPickerPanel v-else v-model:selected-ids="selectedFriendIds" :friends="friends" />
+    </div>
+
+    <!-- 濂藉弸瑙嗗浘鐨� dialog footer锛氬缓缇ゅ苟鍙戦�� -->
+    <template v-if="view === 'contact'" #footer>
+      <Button @click="visible = false">鍙栨秷</Button>
+      <Button
+        type="primary"
+        :loading="sending"
+        :disabled="selectedFriendIds.length === 0"
+        @click="handleCreateGroupAndSend"
+      >
+        鍒涘缓缇よ亰骞跺彂閫�
+      </Button>
+    </template>
+  </Modal>
+</template>
+
+<style scoped lang="scss">
+@use '../picker/picker-dialog' as picker;
+
+/* :deep 绌块�� el-dialog 鍐呴儴绫伙紱澶嶇敤 picker 鍏叡 mixin */
+.im-picker-dialog {
+  @include picker.styles;
+}
+</style>

--
Gitblit v1.9.3