<template>
  <!-- 全局错误兜底:子树渲染/生命周期抛错时显示降级 UI,防止白屏。
       用法:用 <ErrorBoundary> 包裹可能出错的子组件树(slot 透传)。
       根节点固定为单个 <div>(display:contents 布局透明),避免被 <Transition> 包裹时
       渲染非元素根节点(slot 透传)导致过渡动画警告/卡死(App.vue <transition name="page">)。 -->
  <div class="error-boundary-root" :class="{ 'is-error': !!error }">
    <template v-if="error">
      <div class="error-boundary-icon">⚠</div>
      <div class="error-boundary-title">{{ $t('error.title') }}</div>
      <div class="error-boundary-desc">{{ errorMessage }}</div>
      <details class="error-boundary-details">
        <summary>{{ $t('error.stackDetails') }}</summary>
        <pre class="error-boundary-stack">{{ errorStack }}</pre>
      </details>
      <button class="error-boundary-retry" @click="reset">{{ $t('error.retry') }}</button>
    </template>
    <slot v-else />
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onErrorCaptured } from 'vue'

const error = ref<unknown>(null)

const errorMessage = computed(() => {
  if (!error.value) return ''
  if (error.value instanceof Error) return error.value.message
  return String(error.value)
})

const errorStack = computed(() => {
  if (!error.value) return ''
  if (error.value instanceof Error) return error.value.stack || error.value.message
  try {
    return JSON.stringify(error.value, null, 2)
  } catch {
    return String(error.value)
  }
})

onErrorCaptured((err) => {
  error.value = err
  console.error('[ErrorBoundary] 子树渲染错误:', err)
  return false
})

// 重试:重置 errorState,slot 重新挂载子树
function reset() {
  error.value = null
}
</script>

<style scoped>
/* 根节点:正常态 display:contents 布局透明(slot 内容直接参与父级布局,不破坏页面 flex);
   error 态切回 flex 容器承载降级 UI。固定为元素根节点,避免 <Transition> 非元素根警告。 */
.error-boundary-root {
  display: contents;
}
.error-boundary-root.is-error {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 32px 16px;
  color: var(--df-text-secondary, #888);
  text-align: center;
}
.error-boundary-icon {
  font-size: 28px;
  color: var(--df-danger, #e5484d);
  line-height: 1;
}
.error-boundary-title {
  font-size: 14px;
  font-weight: 500;
  color: var(--df-text, #333);
}
.error-boundary-desc {
  font-size: 12px;
  line-height: 1.5;
  max-width: 480px;
  word-break: break-all;
}
.error-boundary-details {
  max-width: 600px;
  width: 100%;
  margin-top: 8px;
  font-size: 11px;
  color: var(--df-text-dim);
}
.error-boundary-details summary {
  cursor: pointer;
  user-select: none;
}
.error-boundary-stack {
  margin-top: 6px;
  padding: 8px;
  background: rgba(0,0,0,0.3);
  border-radius: 4px;
  font-family: var(--df-font-mono, monospace);
  font-size: 10px;
  line-height: 1.4;
  white-space: pre-wrap;
  word-break: break-all;
  max-height: 240px;
  overflow: auto;
}
.error-boundary-retry {
  margin-top: 8px;
  padding: 5px 16px;
  font-size: 12px;
  font-weight: 500;
  font-family: var(--df-font-sans);
  background: var(--df-accent, #7b6ff0);
  color: #fff;
  border: none;
  border-radius: var(--df-radius-sm, 4px);
  cursor: pointer;
  transition: filter 0.15s;
}
.error-boundary-retry:hover {
  filter: brightness(1.1);
}
</style>
