<template>
  <!-- 问诊详情：见 doc/app/在线问诊/在线问诊接口说明.md -->
  <view :key="layoutKey" :class="pageRootClass" class="cd-page">
    <component
      :is="feedScrollerTag"
      ref="feedScroll"
      id="cd-feed"
      class="cd-feed"
      :class="{ 'cd-feed--h5': useNativeFeedScroll }"
      scroll-y
      enable-back-to-top
      :scroll-top="feedScrollTop"
      :scroll-into-view="scrollAnchor"
      scroll-into-view-alignment="end"
      scroll-with-animation
      :upper-threshold="80"
      @scrolltoupper="onScrollToUpper"
      @scroll="onFeedScroll"
    >
      <view
        id="cd-feed-inner"
        class="cd-feed__inner"
        :class="{ 'cd-feed__inner--has-preview': !!pendingMedia }"
      >
        <view v-if="loadingOlder" class="feed-hint">
          <text class="text-body feed-hint__txt">{{ $t('consultDetailPage.loadingMore') }}</text>
        </view>
        <view v-if="noMoreOlder && messages.length" class="feed-hint">
          <text class="text-body feed-hint__txt">{{ $t('consultDetailPage.noMoreHistory') }}</text>
        </view>
        <view v-if="messagesLoading && !messages.length" class="feed-hint">
          <text class="text-body feed-hint__txt">{{ $t('consultDetailPage.loading') }}</text>
        </view>

        <block v-if="messageGroups.length">
          <block v-for="(group, gi) in messageGroups" :key="'g-' + gi">
            <view class="chat-date">
              <text class="text-body chat-date__txt">{{ group.dateLabel }}</text>
            </view>
            <view
              v-for="m in group.items"
              :key="m.id"
              :id="'cd-msg-' + m.id"
              class="cd-bubble-row"
              :class="isUserMessage(m) ? 'cd-bubble-row--user' : 'cd-bubble-row--bot'"
            >
              <view v-if="!isUserMessage(m)" class="cd-avatar cd-avatar--vet">
                <up-icon name="account-fill" color="#2563eb" :size="18" />
              </view>
              <view
                class="cd-bubble"
                :class="[
                  isUserMessage(m) ? 'cd-bubble--user' : 'cd-bubble--bot',
                  m.msgType === 2 ? 'cd-bubble--image' : ''
                ]"
              >
                <text v-if="!isUserMessage(m)" class="text-body cd-bubble__who">{{ peerName }}</text>
                <text v-else class="text-body cd-bubble__who">{{ $t('consultDetailPage.userBubble') }}</text>

                <text v-if="m.msgType === 1" class="text-body cd-bubble__txt">{{ m.content }}</text>
                <image
                  v-else-if="m.msgType === 2"
                  class="cd-bubble__img"
                  :src="mediaUrl(m.content)"
                  mode="widthFix"
                  @load="onBubbleMediaLoad"
                  @click="previewImage(m.content)"
                />
                <video
                  v-else-if="m.msgType === 3"
                  class="cd-bubble__video"
                  :src="mediaUrl(m.content)"
                  controls
                  object-fit="contain"
                />
                <view
                  v-else-if="m.msgType === 4"
                  class="voice-msg"
                  :class="{ 'voice-msg--on': playingVoiceId === m.id }"
                  role="button"
                  @click="toggleVoice(m)"
                >
                  <text class="text-body voice-msg__dur">{{ formatVoiceDuration(m.mediaDuration) }}</text>
                  <text class="voice-msg__label">{{ $t('consultDetailPage.msgVoice') }}</text>
                </view>
                <text v-else class="text-body cd-bubble__txt">{{ m.content }}</text>

                <text class="text-body cd-bubble__time">{{ formatMsgTime(m.sendTime) }}</text>
              </view>
              <view v-if="isUserMessage(m)" class="cd-avatar cd-avatar--user">
                <up-icon name="account-fill" color="#16a34a" :size="18" />
              </view>
            </view>
          </block>
        </block>

        <view v-else-if="sessionReady && !messagesLoading" class="feed-empty">
          <text class="text-body feed-empty__txt">{{ $t('consultDetailPage.emptyChat') }}</text>
        </view>

        <view id="cd-msg-tail" class="cd-feed__tail" />
      </view>
    </component>

    <view v-if="sessionReady" class="cd-composer">
      <view v-if="pendingMedia" class="cd-preview">
        <image
          v-if="pendingMedia.kind === 'image'"
          class="cd-preview__img"
          :src="pendingMedia.path"
          mode="widthFix"
        />
        <view v-else class="cd-preview__video-wrap">
          <image
            v-if="pendingMedia.thumb"
            class="cd-preview__img"
            :src="pendingMedia.thumb"
            mode="aspectFill"
          />
          <view v-else class="cd-preview__video-ph">
            <up-icon name="play-right" color="#ffffff" :size="28" />
          </view>
          <text class="cd-preview__tag text-body">{{ $t('consultDetailPage.videoTag') }}</text>
        </view>
        <view class="cd-preview__mask" role="button" @click="clearPendingMedia">
          <up-icon name="close" color="#ffffff" :size="14" />
        </view>
      </view>

      <view class="cd-composer-row">
        <!-- <view
          class="cd-mic"
          :class="{ 'cd-mic--rec': isRecording }"
          role="button"
          @touchstart.stop.prevent="onMicTouchStart"
          @touchend.stop.prevent="onMicTouchEnd"
          @touchcancel.stop.prevent="onMicTouchEnd"
        >
          <up-icon name="mic" :color="isRecording ? '#EF4444' : '#22C55E'" :size="26" />
        </view> -->
        <view class="cd-input-shell">
          <textarea
            v-model="draft"
            class="cd-textarea"
            :placeholder="$t('consultDetailPage.inputPlaceholder')"
            placeholder-class="cd-textarea-ph"
            maxlength="2000"
            :disabled="sending"
            :cursor-spacing="16"
            :show-confirm-bar="false"
            :fixed="true"
            auto-height
          />
        </view>
        <view class="cd-icon-btn" role="button" @click="onAttachTap">
          <up-icon name="photo" color="#22C55E" :size="26" />
        </view>
        <up-button
          type="primary"
          size="small"
          :text="sending ? $t('consultDetailPage.uploading') : $t('consultDetailPage.send')"
          :disabled="sendDisabled"
          :custom-style="sendBtnStyle"
          @click="send"
        />
      </view>
    </view>
  </view>
</template>

<script>
import UButton from 'uview-plus/components/u-button/u-button.vue'
import UIcon from 'uview-plus/components/u-icon/u-icon.vue'
import tabPage from '@/mixins/tabPage'
import { ensureApiToken } from '@/utils/apiAuth'
import { uploadFile } from '@/utils/upload'
import {
  MSG_TYPE_TEXT,
  MSG_TYPE_IMAGE,
  MSG_TYPE_VIDEO,
  MSG_TYPE_VOICE,
  MESSAGE_PAGE_SIZE,
  isUserMessage,
  mediaUrl,
  normalizeMessage,
  formatMsgTime,
  formatVoiceDuration,
  buildMessageGroups
} from '@/utils/aiConsult'
import { MEDIA_RULES, extOf } from '@/utils/aiLlmChat'
import {
  openOnlineConsultSession,
  listOnlineConsultMessages,
  sendOnlineConsultMessage
} from '@/api/onlineConsult'

const POLL_INTERVAL_MS = 5000

export default {
  components: {
    'up-button': UButton,
    'up-icon': UIcon
  },
  mixins: [tabPage],
  data() {
    return {
      navTitleKey: 'consultDetailPage.navTitle',
      sessionId: '',
      sessionReady: false,
      vetResourceId: '',
      vetN: 1,
      vetName: '',
      vetSub: '',
      vetIntro: '',
      avatarText: '',
      draft: '',
      messages: [],
      pendingMedia: null,
      messagesLoading: false,
      loadingOlder: false,
      noMoreOlder: false,
      sending: false,
      scrollAnchor: '',
      feedScrollTop: 0,
      scrollTopNonce: 0,
      scrollBottomTimer: null,
      pollTimer: null,
      useNativeFeedScroll: false,
      playingVoiceId: null,
      innerAudio: null,
      isRecording: false,
      micLongPressTimer: null,
      recordStartAt: 0,
      recorder: null,
      sendBtnStyle:
        'max-width:64rpx;min-height:64rpx;height:auto;padding:12rpx 20rpx;display:flex;align-items:center;justify-content:center;border-radius:12rpx;'
    }
  },
  computed: {
    feedScrollerTag() {
      return this.useNativeFeedScroll ? 'view' : 'scroll-view'
    },
    peerName() {
      if (this.vetName) {
        return this.vetName
      }
      return this.$t('consultDetailPage.vetNameTpl', { n: this.vetN })
    },
    messageGroups() {
      return buildMessageGroups(this.messages, (k, p) => this.$t(k, p))
    },
    sendDisabled() {
      const t = (this.draft || '').trim()
      return this.sending || !this.sessionId || (!t && !this.pendingMedia)
    }
  },
  onLoad(query) {
    // #ifdef H5
    this.useNativeFeedScroll = true
    // #endif
    if (!ensureApiToken()) return
    const q = query || {}
    this.sessionId = this.decodeQuery(q, 'sessionId')
    this.vetResourceId = this.decodeQuery(q, 'vetResourceId') || this.decodeQuery(q, 'id')
    const n = parseInt(q.n, 10)
    this.vetN = Number.isFinite(n) && n > 0 ? n : 1
    this.vetName = this.decodeQuery(q, 'name')
    this.vetSub = this.decodeQuery(q, 'sub')
    this.vetIntro = this.decodeQuery(q, 'intro')
    this.avatarText = this.decodeQuery(q, 'avatar')

    if (this.sessionId) {
      this.sessionReady = true
      this.loadMessages(false)
      this.startPoll()
      return
    }
    if (this.vetResourceId) {
      this.openSession()
      return
    }
    uni.showToast({ title: this.$t('consultDetailPage.openSessionFail'), icon: 'none' })
    setTimeout(() => uni.navigateBack(), 1500)
  },
  onShow() {
    const p = uni.setNavigationBarTitle({
      title: this.peerName || this.$t(this.navTitleKey)
    })
    if (p && typeof p.catch === 'function') {
      p.catch(() => {})
    }
    if (this.sessionReady) {
      this.pollNewMessages()
      this.startPoll()
      if (this.messages.length && !this.messagesLoading && !this.loadingOlder) {
        this.scheduleScrollToBottom()
      }
    }
  },
  onHide() {
    this.stopPoll()
  },
  mounted() {
    // #ifndef H5
    this.initRecorderManager()
    // #endif
  },
  onUnload() {
    this.stopPoll()
    if (this.scrollBottomTimer) {
      clearTimeout(this.scrollBottomTimer)
      this.scrollBottomTimer = null
    }
    if (this.micLongPressTimer) {
      clearTimeout(this.micLongPressTimer)
      this.micLongPressTimer = null
    }
    if (this.isRecording && this.recorder) {
      try {
        this.recorder.stop()
      } catch (e) {
        /* noop */
      }
    }
    this.stopVoice()
  },
  methods: {
    isUserMessage,
    mediaUrl,
    formatMsgTime,
    formatVoiceDuration,
    decodeQuery(q, key) {
      const raw = q && q[key]
      if (raw == null || raw === '') {
        return ''
      }
      try {
        return decodeURIComponent(String(raw))
      } catch (e) {
        return String(raw)
      }
    },
    initRecorderManager() {
      // #ifdef H5
      return
      // #endif
      if (typeof uni.getRecorderManager !== 'function') {
        return
      }
      const rm = uni.getRecorderManager()
      if (!rm || typeof rm.onStop !== 'function') {
        return
      }
      rm.onStop((res) => this.onRecordStop(res))
      if (typeof rm.onError === 'function') {
        rm.onError(() => {
          this.isRecording = false
          uni.showToast({ title: this.$t('consultDetailPage.recordFail'), icon: 'none' })
        })
      }
      this.recorder = rm
    },
    openSession() {
      uni.showLoading({ title: this.$t('consultDetailPage.loading'), mask: true })
      const vetId = Number(this.vetResourceId) || this.vetResourceId
      openOnlineConsultSession({ vetResourceId: vetId })
        .then((res) => {
          const session = res.data || {}
          const sid = session.id != null ? String(session.id) : ''
          if (!sid) {
            uni.showToast({ title: this.$t('consultDetailPage.openSessionFail'), icon: 'none' })
            setTimeout(() => uni.navigateBack(), 1500)
            return
          }
          this.sessionId = sid
          if (session.receiverName && !this.vetName) {
            this.vetName = session.receiverName
          }
          this.sessionReady = true
          this.loadMessages(false)
          this.startPoll()
        })
        .catch(() => {
          uni.showToast({ title: this.$t('consultDetailPage.openSessionFail'), icon: 'none' })
          setTimeout(() => uni.navigateBack(), 1500)
        })
        .finally(() => {
          uni.hideLoading()
        })
    },
    loadMessages(older) {
      if (!this.sessionId) return
      const beforeId = older && this.messages.length ? this.messages[0].id : undefined
      if (older) {
        this.loadingOlder = true
      } else {
        this.messagesLoading = true
      }
      listOnlineConsultMessages(this.sessionId, {
        beforeId,
        pageSize: MESSAGE_PAGE_SIZE
      })
        .then((res) => {
          const batch = (res.data || []).map((m) => normalizeMessage(m))
          if (older) {
            if (!batch.length) {
              this.noMoreOlder = true
            } else {
              const feedEl = this.useNativeFeedScroll ? this.getFeedScrollEl() : null
              const prevScrollHeight = feedEl ? feedEl.scrollHeight : 0
              const prevScrollTop = feedEl ? feedEl.scrollTop : 0
              this.messages = batch.concat(this.messages)
              if (batch.length < MESSAGE_PAGE_SIZE) {
                this.noMoreOlder = true
              }
              if (feedEl && prevScrollHeight > 0) {
                this.$nextTick(() => {
                  feedEl.scrollTop = feedEl.scrollHeight - prevScrollHeight + prevScrollTop
                })
              }
            }
          } else {
            this.messages = batch
            this.noMoreOlder = batch.length > 0 && batch.length < MESSAGE_PAGE_SIZE
            this.scheduleScrollToBottom()
          }
        })
        .finally(() => {
          this.messagesLoading = false
          this.loadingOlder = false
          if (!older) {
            this.scheduleScrollToBottom()
          }
        })
    },
    pollNewMessages() {
      if (!this.sessionId || this.messagesLoading || this.loadingOlder || this.sending) return
      listOnlineConsultMessages(this.sessionId, { pageSize: MESSAGE_PAGE_SIZE })
        .then((res) => {
          const batch = (res.data || []).map((m) => normalizeMessage(m))
          if (!batch.length) return
          const lastId = this.messages.length ? this.messages[this.messages.length - 1].id : 0
          const newer = batch.filter((m) => m.id && m.id > lastId)
          if (!newer.length) return
          this.messages = this.messages.concat(newer)
          this.scheduleScrollToBottom(false)
        })
        .catch(() => {
          /* silent poll */
        })
    },
    startPoll() {
      this.stopPoll()
      this.pollTimer = setInterval(() => this.pollNewMessages(), POLL_INTERVAL_MS)
    },
    stopPoll() {
      if (this.pollTimer) {
        clearInterval(this.pollTimer)
        this.pollTimer = null
      }
    },
    onScrollToUpper() {
      if (this.loadingOlder || this.noMoreOlder || !this.messages.length) return
      this.loadMessages(true)
    },
    onFeedScroll(e) {
      if (!this.useNativeFeedScroll) return
      const el = (e && e.target) || this.getFeedScrollEl()
      if (!el || el.scrollTop > 40) return
      this.onScrollToUpper()
    },
    getFeedScrollEl() {
      const ref = this.$refs.feedScroll
      if (!ref) return null
      if (ref.$el) return ref.$el
      return ref
    },
    scrollToBottomNative() {
      const el = this.getFeedScrollEl()
      if (!el) {
        return false
      }
      const tail = el.querySelector('#cd-msg-tail')
      if (tail && typeof tail.scrollIntoView === 'function') {
        tail.scrollIntoView({ block: 'end', inline: 'nearest', behavior: 'auto' })
      }
      if (typeof el.scrollHeight === 'number') {
        const top = Math.max(0, el.scrollHeight - el.clientHeight)
        el.scrollTop = top
      }
      return true
    },
    onBubbleMediaLoad() {
      this.scheduleScrollToBottom(false)
    },
    scheduleScrollToBottom(immediate = true) {
      if (this.scrollBottomTimer) {
        clearTimeout(this.scrollBottomTimer)
        this.scrollBottomTimer = null
      }
      const run = () => this.scrollToBottom()
      if (immediate) {
        run()
      }
      ;[80, 200, 450, 800, 1200].forEach((ms) => {
        setTimeout(run, ms)
      })
    },
    scrollByAnchor() {
      const last = this.messages[this.messages.length - 1]
      const anchor = last && last.id != null ? 'cd-msg-' + last.id : 'cd-msg-tail'
      this.scrollAnchor = ''
      this.$nextTick(() => {
        this.scrollAnchor = anchor
      })
    },
    scrollToBottom() {
      this.$nextTick(() => {
        if (this.useNativeFeedScroll) {
          const run = () => this.scrollToBottomNative()
          run()
          if (typeof requestAnimationFrame === 'function') {
            requestAnimationFrame(run)
          }
          return
        }
        uni.createSelectorQuery()
          .in(this)
          .select('#cd-feed-inner')
          .boundingClientRect()
          .select('#cd-feed')
          .boundingClientRect()
          .exec((res) => {
            const inner = res && res[0]
            const viewport = res && res[1]
            if (!inner || !viewport || inner.height <= 0 || viewport.height <= 0) {
              this.scrollByAnchor()
              return
            }
            const maxTop = Math.max(0, Math.ceil(inner.height - viewport.height))
            if (maxTop <= 0) {
              this.feedScrollTop = 0
              this.scrollByAnchor()
              return
            }
            const nextTop = maxTop + this.scrollTopNonce
            this.scrollTopNonce = this.scrollTopNonce ? 0 : 1
            if (this.feedScrollTop === nextTop) {
              this.feedScrollTop = 0
              this.$nextTick(() => {
                this.feedScrollTop = nextTop
              })
            } else {
              this.feedScrollTop = nextTop
            }
            setTimeout(() => this.scrollByAnchor(), 50)
          })
      })
    },
    clearPendingMedia() {
      this.pendingMedia = null
    },
    onAttachTap() {
      uni.showActionSheet({
        itemList: [this.$t('consultDetailPage.pickImage'), this.$t('consultDetailPage.pickVideo')],
        success: (res) => {
          if (res.tapIndex === 0) this.chooseImage()
          else if (res.tapIndex === 1) this.chooseVideo()
        }
      })
    },
    validateMediaFile(filePath, fileName, kind) {
      const rule = MEDIA_RULES[kind]
      const ext = extOf(fileName || filePath)
      if (!rule.exts.includes(ext)) {
        uni.showToast({ title: this.$t('aiOnlineConsult.' + rule.errFmt), icon: 'none' })
        return false
      }
      if ((fileName || filePath).includes(',')) {
        uni.showToast({ title: this.$t('aiOnlineConsult.errComma'), icon: 'none' })
        return false
      }
      return true
    },
    checkFileSize(filePath, kind) {
      const rule = MEDIA_RULES[kind]
      return new Promise((resolve) => {
        uni.getFileInfo({
          filePath,
          success: (res) => {
            if (res.size / 1024 / 1024 >= rule.maxMb) {
              uni.showToast({ title: this.$t('aiOnlineConsult.' + rule.errMb), icon: 'none' })
              resolve(false)
              return
            }
            resolve(true)
          },
          fail: () => resolve(true)
        })
      })
    },
    chooseImage() {
      uni.chooseImage({
        count: 1,
        sizeType: ['compressed'],
        sourceType: ['album', 'camera'],
        success: async (res) => {
          const path = res.tempFilePaths && res.tempFilePaths[0]
          if (!path) return
          const name = (res.tempFiles && res.tempFiles[0] && res.tempFiles[0].name) || path
          if (!this.validateMediaFile(path, name, 'image')) return
          if (!(await this.checkFileSize(path, 'image'))) return
          this.pendingMedia = { kind: 'image', path, msgType: MSG_TYPE_IMAGE }
        },
        fail: () => {
          uni.showToast({ title: this.$t('consultDetailPage.imagePickFail'), icon: 'none' })
        }
      })
    },
    chooseVideo() {
      uni.chooseVideo({
        sourceType: ['album', 'camera'],
        maxDuration: 120,
        compressed: true,
        success: async (res) => {
          const path = res.tempFilePath || ''
          if (!path) return
          if (!this.validateMediaFile(path, path, 'video')) return
          if (!(await this.checkFileSize(path, 'video'))) return
          this.pendingMedia = {
            kind: 'video',
            path,
            thumb: res.thumbTempFilePath || '',
            msgType: MSG_TYPE_VIDEO,
            mediaDuration: res.duration ? Math.round(res.duration) : undefined
          }
        },
        fail: () => {
          uni.showToast({ title: this.$t('consultDetailPage.videoPickFail'), icon: 'none' })
        }
      })
    },
    onMicTouchStart() {
      if (this.micLongPressTimer) {
        clearTimeout(this.micLongPressTimer)
        this.micLongPressTimer = null
      }
      this.micLongPressTimer = setTimeout(() => {
        this.micLongPressTimer = null
        this.startRecord()
      }, 480)
    },
    onMicTouchEnd() {
      if (this.micLongPressTimer) {
        clearTimeout(this.micLongPressTimer)
        this.micLongPressTimer = null
        return
      }
      if (this.isRecording) {
        this.stopRecord()
      }
    },
    startRecord() {
      if (!this.recorder) {
        uni.showToast({ title: this.$t('consultDetailPage.recordUnsupported'), icon: 'none' })
        return
      }
      this.isRecording = true
      this.recordStartAt = Date.now()
      try {
        this.recorder.start({
          duration: 60000,
          sampleRate: 16000,
          numberOfChannels: 1,
          encodeBitRate: 96000,
          format: 'mp3'
        })
      } catch (e) {
        this.isRecording = false
        uni.showToast({ title: this.$t('consultDetailPage.recordFail'), icon: 'none' })
      }
    },
    stopRecord() {
      if (!this.recorder || !this.isRecording) return
      try {
        this.recorder.stop()
      } catch (e) {
        this.isRecording = false
      }
    },
    async onRecordStop(res) {
      this.isRecording = false
      const ms = Date.now() - (this.recordStartAt || 0)
      if (ms < 500 || !res || !res.tempFilePath) return
      const duration = Math.max(1, Math.round(ms / 1000))
      await this.sendVoiceImmediately(res.tempFilePath, duration)
    },
    async sendVoiceImmediately(filePath, mediaDuration) {
      if (!this.sessionId || this.sending) return
      uni.showLoading({ title: this.$t('consultDetailPage.uploading'), mask: true })
      try {
        const url = await uploadFile(filePath)
        await this.sendMessage({
          msgType: MSG_TYPE_VOICE,
          content: url,
          mediaDuration
        })
      } catch (e) {
        uni.showToast({ title: this.$t('consultDetailPage.sendFail'), icon: 'none' })
      } finally {
        uni.hideLoading()
      }
    },
    async sendMessage(body) {
      const res = await sendOnlineConsultMessage(this.sessionId, body)
      const msg = normalizeMessage(res.data || {})
      if (msg.id) {
        const exists = this.messages.some((m) => m.id === msg.id)
        if (!exists) {
          this.messages.push(msg)
        }
      }
      this.scheduleScrollToBottom()
    },
    async send() {
      if (this.sendDisabled) return
      const text = (this.draft || '').trim()
      const pending = this.pendingMedia
      this.sending = true
      try {
        if (text) {
          await this.sendMessage({ msgType: MSG_TYPE_TEXT, content: text })
          this.draft = ''
        }
        if (pending) {
          uni.showLoading({ title: this.$t('consultDetailPage.uploading'), mask: true })
          const url = await uploadFile(pending.path)
          await this.sendMessage({
            msgType: pending.msgType,
            content: url,
            mediaDuration: pending.mediaDuration
          })
          this.pendingMedia = null
        }
      } catch (e) {
        uni.showToast({ title: this.$t('consultDetailPage.sendFail'), icon: 'none' })
      } finally {
        this.sending = false
        uni.hideLoading()
        this.scheduleScrollToBottom()
      }
    },
    previewImage(path) {
      const url = mediaUrl(path)
      if (url) {
        uni.previewImage({ urls: [url] })
      }
    },
    stopVoice() {
      if (this.innerAudio) {
        this.innerAudio.stop()
        this.innerAudio.destroy()
        this.innerAudio = null
      }
      this.playingVoiceId = null
    },
    toggleVoice(m) {
      if (!m || !m.content) return
      if (this.playingVoiceId === m.id) {
        this.stopVoice()
        return
      }
      this.stopVoice()
      const audio = uni.createInnerAudioContext()
      audio.src = mediaUrl(m.content)
      audio.onEnded(() => this.stopVoice())
      audio.onError(() => {
        uni.showToast({ title: this.$t('consultDetailPage.voicePlayFail'), icon: 'none' })
        this.stopVoice()
      })
      this.innerAudio = audio
      this.playingVoiceId = m.id
      audio.play()
    }
  }
}
</script>

<style lang="scss" scoped>
@import '@/styles/morandi.scss';
@import '@/styles/tab-page.scss';

.cd-page {
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 100vh;
  height: 100%;
  box-sizing: border-box;
  background: $morandi-bg-page;
}

.cd-feed {
  flex: 1;
  min-height: 0;
  height: 0;
  min-width: 0;
  box-sizing: border-box;
  background: $morandi-feed;
}

.cd-feed--h5 {
  overflow-x: hidden;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}

.cd-feed__inner {
  display: flex;
  flex-direction: column;
  gap: 20rpx;
  min-width: 0;
  padding: 16rpx 16rpx 24rpx;
  padding-bottom: calc(220rpx + env(safe-area-inset-bottom));
  box-sizing: border-box;
}

.cd-feed__inner--has-preview {
  padding-bottom: calc(340rpx + env(safe-area-inset-bottom));
}

.cd-feed__tail {
  height: 1px;
  width: 100%;
}

.feed-hint,
.feed-empty {
  padding: 16rpx 0;
  display: flex;
  justify-content: center;
}

.feed-hint__txt,
.feed-empty__txt {
  font-size: 24rpx;
  color: $morandi-text-muted;
}

.chat-date {
  display: flex;
  justify-content: center;
  padding: 8rpx 0;
}

.chat-date__txt {
  font-size: 22rpx;
  color: $morandi-text-muted;
  padding: 4rpx 16rpx;
  border-radius: 8rpx;
  background: rgba(255, 255, 255, 0.6);
}

.cd-bubble-row {
  display: flex;
  align-items: flex-start;
  gap: 12rpx;
  width: 100%;
  min-width: 0;
}

.cd-bubble-row--bot {
  justify-content: flex-start;
}

.cd-bubble-row--user {
  justify-content: flex-end;
}

.cd-avatar {
  flex-shrink: 0;
  width: 56rpx;
  height: 56rpx;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.cd-avatar--vet {
  background: rgba(37, 99, 235, 0.12);
}

.cd-avatar--user {
  background: rgba(34, 197, 94, 0.22);
}

.cd-bubble {
  display: flex;
  flex-direction: column;
  min-width: 0;
  max-width: 72%;
  padding: 16rpx 18rpx;
  border-radius: 12rpx;
  gap: 10rpx;
  box-sizing: border-box;
}

.cd-bubble--bot {
  background: $morandi-bg-card-inner;
  border: 1rpx solid $morandi-border-soft;
  box-shadow: 0 2rpx 8rpx rgba(74, 69, 66, 0.05);
}

.cd-bubble--user {
  background: #8ef09e;
  border: 1rpx solid $morandi-border-soft;
  box-shadow: 0 2rpx 6rpx rgba(74, 69, 66, 0.05);
}

.cd-bubble--image {
  max-width: 85%;
}

.cd-bubble__who {
  font-size: 22rpx;
  font-weight: 600;
  color: $morandi-text-muted;
}

.cd-bubble--user .cd-bubble__who {
  color: #166534;
}

.cd-bubble__time {
  font-size: 20rpx;
  color: $morandi-text-muted;
  align-self: flex-end;
}

.cd-bubble__img {
  display: block;
  width: 200rpx;
  max-width: 100%;
  height: auto;
  border-radius: 8rpx;
  overflow: hidden;
}

.cd-bubble__video {
  width: 100%;
  max-width: 420rpx;
  max-height: 360rpx;
  border-radius: 8rpx;
  background: #000;
}

.cd-bubble__txt {
  font-size: 28rpx;
  line-height: 1.5;
  word-break: break-word;
  overflow-wrap: anywhere;
  color: $morandi-text;
}

.voice-msg {
  display: flex;
  align-items: center;
  gap: 12rpx;
  padding: 8rpx 12rpx;
  border-radius: 8rpx;
  background: rgba(34, 197, 94, 0.12);
}

.voice-msg--on {
  background: rgba(34, 197, 94, 0.22);
}

.voice-msg__dur {
  font-size: 26rpx;
  font-weight: 600;
  color: #166534;
}

.voice-msg__label {
  font-size: 24rpx;
  color: $morandi-text-muted;
}

.cd-composer {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 200;
  display: flex;
  flex-direction: column;
  gap: 12rpx;
  min-width: 0;
  padding: 12rpx 16rpx;
  padding-bottom: calc(12rpx + env(safe-area-inset-bottom));
  background: $morandi-composer;
  border-top: 1rpx solid $morandi-border-soft;
  box-shadow: 0 -8rpx 24rpx rgba(74, 69, 66, 0.06);
}

.cd-preview {
  position: relative;
  align-self: flex-start;
  width: 200rpx;
  border-radius: 12rpx;
  overflow: hidden;
  background: $morandi-bg-muted;
}

.cd-preview__img {
  display: block;
  width: 200rpx;
  max-width: 100%;
  height: auto;
}

.cd-preview__video-wrap {
  position: relative;
  width: 200rpx;
  height: 200rpx;
}

.cd-preview__video-ph {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #374151;
}

.cd-preview__tag {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 4rpx 8rpx;
  font-size: 20rpx;
  color: #ffffff;
  background: rgba(0, 0, 0, 0.45);
  text-align: center;
}

.cd-preview__mask {
  position: absolute;
  right: 0;
  top: 0;
  width: 44rpx;
  height: 44rpx;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.45);
  border-bottom-left-radius: 12rpx;
}

.cd-composer-row {
  display: flex;
  flex-direction: row;
  align-items: flex-end;
  gap: 12rpx;
  min-width: 0;
}

.cd-mic {
  flex-shrink: 0;
  width: 72rpx;
  height: 72rpx;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 12rpx;
  background: $morandi-bg-card-inner;
  border: 1rpx solid $morandi-border-soft;
}

.cd-mic--rec {
  background: #fef2f2;
  border-color: #fecaca;
}

.cd-input-shell {
  flex: 1;
  min-width: 0;
  min-height: 72rpx;
  max-height: 200rpx;
  padding: 10rpx 16rpx;
  box-sizing: border-box;
  background: $morandi-bg-card-inner;
  border-radius: 12rpx;
  border: 1rpx solid $morandi-border-soft;
}

.cd-textarea {
  width: 100%;
  min-height: 48rpx;
  max-height: 180rpx;
  font-size: 28rpx;
  line-height: 1.45;
  color: $morandi-text;
}

.cd-textarea-ph {
  color: $morandi-text-muted;
  font-size: 26rpx;
}

.cd-icon-btn {
  flex-shrink: 0;
  width: 72rpx;
  height: 72rpx;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 12rpx;
  background: $morandi-bg-card-inner;
  border: 1rpx solid $morandi-border-soft;
}
</style>
