<template>
  <div class="articles-page">
    <div class="page-header">
      <div>
        <h2 class="page-title">📝 文章管理</h2>
        <p class="page-desc">管理平台文章内容，支持分类、封面、推荐与状态流转</p>
      </div>
      <a-space>
        <a-button @click="navigateTo('/admin/article-categories')">
          分类管理
        </a-button>
        <a-button type="primary" @click="handleCreate">
          <template #icon><PlusOutlined /></template>
          新增文章
        </a-button>
        <a-button @click="loadArticles" :loading="loading">
          <template #icon><ReloadOutlined /></template>
          刷新
        </a-button>
      </a-space>
    </div>

    <a-row :gutter="[16, 16]" class="mb-6">
      <a-col :xs="12" :md="6" v-for="stat in statCards" :key="stat.key">
        <div
          class="stat-card"
          :class="[
            stat.color,
            {
              active:
                (filterStatus === undefined && stat.key === -1) ||
                filterStatus === stat.key,
            },
          ]"
          @click="handleStatFilter(stat.key)"
        >
          <div class="stat-icon">{{ stat.icon }}</div>
          <div class="stat-info">
            <div class="stat-value">{{ stat.value }}</div>
            <div class="stat-label">{{ stat.label }}</div>
          </div>
        </div>
      </a-col>
    </a-row>

    <div class="panel">
      <div class="panel-header">
        <span class="panel-title">📋 文章列表</span>
        <a-space wrap>
          <a-select v-model:value="filterStatus" style="width: 120px" @change="handleSearch">
            <a-select-option :value="undefined">全部状态</a-select-option>
            <a-select-option :value="0">已发布</a-select-option>
            <a-select-option :value="1">待审核</a-select-option>
            <a-select-option :value="2">已驳回</a-select-option>
            <a-select-option :value="3">违规</a-select-option>
          </a-select>
          <a-select
            v-model:value="filterCategoryId"
            allow-clear
            placeholder="全部分类"
            style="width: 180px"
            @change="handleSearch"
          >
            <a-select-option
              v-for="item in categoryOptions"
              :key="item.categoryId"
              :value="item.categoryId"
            >
              {{ item.title }}
            </a-select-option>
          </a-select>
          <a-input-search
            v-model:value="searchKeyword"
            placeholder="搜索标题 / 摘要 / 作者"
            style="width: 240px"
            @search="handleSearch"
          />
        </a-space>
      </div>

      <a-table
        :columns="columns"
        :data-source="pagedArticles"
        :loading="loading"
        :pagination="tablePagination"
        row-key="articleId"
        @change="handleTableChange"
        size="middle"
      >
        <template #bodyCell="{ column, record }">
          <template v-if="column.key === 'info'">
            <div class="article-info-cell">
              <img v-if="record.image" :src="record.image" class="article-thumb" />
              <div v-else class="article-thumb-empty">📄</div>
              <div class="article-info-text">
                <div class="article-title">{{ record.title }}</div>
                <div class="article-meta">
                  <span v-if="record.author">✍️ {{ record.author }}</span>
                  <span v-if="resolveCategoryName(record)" class="meta-item">📁 {{ resolveCategoryName(record) }}</span>
                </div>
                <div class="article-overview">{{ record.overview || '暂无摘要' }}</div>
              </div>
            </div>
          </template>

          <template v-if="column.key === 'status'">
            <a-tag :color="statusColor(record.status)">{{ statusText(record.status) }}</a-tag>
          </template>

          <template v-if="column.key === 'metrics'">
            <div class="metrics-cell">
              <div>👁 {{ record.actualViews || 0 }} 次阅读</div>
              <div>❤️ {{ record.likes || 0 }} 点赞</div>
            </div>
          </template>

          <template v-if="column.key === 'recommend'">
            <a-switch
              :checked="!!record.recommend"
              size="small"
              @change="(val: boolean) => handleToggleRecommend(record, val)"
            />
          </template>

          <template v-if="column.key === 'createTime'">
            <span class="text-sm text-gray">{{ record.createTime?.substring(0, 10) || '-' }}</span>
          </template>

          <template v-if="column.key === 'action'">
            <a-space>
              <a-button type="link" size="small" @click="handleView(record)">预览</a-button>
              <a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button>
              <a-popconfirm title="确认删除此文章？" @confirm="handleDelete(record)">
                <a-button type="link" size="small" danger>删除</a-button>
              </a-popconfirm>
            </a-space>
          </template>
        </template>
      </a-table>
    </div>

    <a-modal
      v-model:open="showFormModal"
      :title="editingArticle?.articleId ? '编辑文章' : '新增文章'"
      width="760px"
      :confirm-loading="saving"
      @ok="handleSave"
      @cancel="showFormModal = false"
    >
      <a-form :model="formData" layout="vertical">
        <a-form-item label="文章标题" required>
          <a-input
            v-model:value="formData.title"
            placeholder="请输入文章标题"
            :maxlength="200"
            show-count
          />
        </a-form-item>

        <a-row :gutter="16">
          <a-col :span="12">
            <a-form-item label="作者">
              <a-input v-model:value="formData.author" placeholder="文章作者" />
            </a-form-item>
          </a-col>
          <a-col :span="12">
            <a-form-item label="来源">
              <a-input v-model:value="formData.source" placeholder="文章来源" />
            </a-form-item>
          </a-col>
        </a-row>

        <a-row :gutter="16">
          <a-col :span="12">
            <a-form-item label="文章分类">
              <a-select
                v-model:value="formData.categoryId"
                allow-clear
                show-search
                option-filter-prop="label"
                placeholder="请选择文章分类"
                :options="categorySelectOptions"
              />
            </a-form-item>
          </a-col>
          <a-col :span="12">
            <a-form-item label="状态">
              <a-select v-model:value="formData.status">
                <a-select-option :value="0">已发布</a-select-option>
                <a-select-option :value="1">待审核</a-select-option>
                <a-select-option :value="2">已驳回</a-select-option>
                <a-select-option :value="3">违规</a-select-option>
              </a-select>
            </a-form-item>
          </a-col>
        </a-row>

        <a-form-item label="封面图">
          <div class="cover-upload-wrap">
            <div v-if="formData.image" class="cover-preview-card">
              <img :src="formData.image" class="cover-preview-image" />
              <div class="cover-preview-actions">
                <a-button size="small" @click="handlePreviewImage(formData.image)">预览</a-button>
                <a-button size="small" danger @click="handleRemoveCover">移除</a-button>
              </div>
            </div>
            <a-upload
              accept="image/*"
              :show-upload-list="false"
              :before-upload="beforeImageUpload"
              :custom-request="handleCoverUpload"
            >
              <a-button :loading="imageUploading">上传封面</a-button>
            </a-upload>
            <div class="field-hint">支持 jpg/png/webp，建议横版封面，单张不超过 5MB</div>
          </div>
        </a-form-item>

        <a-form-item label="文章摘要">
          <a-textarea
            v-model:value="formData.overview"
            :rows="3"
            placeholder="文章简短描述"
            :maxlength="500"
            show-count
          />
        </a-form-item>

        <a-form-item label="文章内容" required>
          <div class="content-editor-wrap">
            <div class="editor-tabs">
              <a-radio-group v-model:value="editorMode" button-style="solid" size="small">
                <a-radio-button value="edit">编辑</a-radio-button>
                <a-radio-button value="preview">预览</a-radio-button>
              </a-radio-group>
            </div>
            <div v-show="editorMode === 'edit'">
              <MarkdownEditor
                v-model="formData.content"
                placeholder="请输入 Markdown 内容，支持 # 标题、**加粗**、*斜体*、[链接](url)、![图片](url)、代码块等语法"
                :show-preview="false"
                min-height="320px"
              />
            </div>
            <div v-show="editorMode === 'preview'" class="preview-only-mode">
              <MarkdownRenderer v-if="formData.content" :content="formData.content" />
              <div v-else class="empty-preview">暂无内容</div>
            </div>
          </div>
        </a-form-item>

        <a-form-item label="内容格式">
          <a-tag :color="isMarkdown ? 'blue' : 'default'">
            {{ isMarkdown ? 'Markdown' : '纯文本/HTML' }}
          </a-tag>
          <span class="format-hint">
            当前编辑器支持 Markdown 语法编写
          </span>
        </a-form-item>

        <a-form-item label="是否推荐">
          <a-switch v-model:checked="formRecommend" />
          <span class="switch-tip">推荐文章将优先出现在列表与前台推荐位</span>
        </a-form-item>
      </a-form>
    </a-modal>

    <a-modal
      v-model:open="showPreviewModal"
      :title="previewData?.title || '文章预览'"
      width="760px"
      :footer="null"
    >
      <template v-if="previewData">
        <div class="preview-meta">
          <a-tag :color="statusColor(previewData.status)">{{ statusText(previewData.status) }}</a-tag>
          <a-tag v-if="previewData.recommend" color="gold">推荐</a-tag>
          <a-tag v-if="previewData.categoryName" color="blue">{{ previewData.categoryName }}</a-tag>
          <span class="preview-meta-text">{{ previewData.createTime?.substring(0, 16) || '-' }}</span>
        </div>
        <div v-if="previewData.image" class="preview-cover-wrap">
          <img :src="previewData.image" class="preview-cover" />
        </div>
        <div class="preview-summary" v-if="previewData.overview">{{ previewData.overview }}</div>
        <a-divider />
        <div class="preview-content">
          <MarkdownRenderer v-if="isPreviewMarkdown" :content="previewData.content" />
          <div v-else v-html="previewData.content || previewData.overview || '暂无内容'"></div>
        </div>
      </template>
    </a-modal>

    <a-modal v-model:open="showImagePreview" title="封面预览" :footer="null" width="640px">
      <img v-if="previewImageUrl" :src="previewImageUrl" class="image-preview-modal" />
    </a-modal>
  </div>
</template>

<script setup lang="ts">
import { PlusOutlined, ReloadOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import {
  listAppArticle as listCmsArticle,
  addAppArticle as addCmsArticle,
  updateAppArticle as updateCmsArticle,
  removeAppArticle as removeCmsArticle,
} from '@/api/app/article'
import { listAppArticleCategory as listCmsArticleCategory } from '@/api/app/articleCategory'
import { uploadFile } from '@/api/system/file'
import type { AppArticle as CmsArticle } from '@/api/app/article/model'
import type { AppArticleCategory as CmsArticleCategory } from '@/api/app/articleCategory/model'
import MarkdownEditor from '@/components/admin/MarkdownEditor.vue'
import MarkdownRenderer from '@/components/admin/MarkdownRenderer.vue'

definePageMeta({ layout: 'admin' })
useHead({ title: '文章管理 - 平台管理' })

type UploadRequestOption = {
  file?: File
  onSuccess?: (body: unknown, file: File) => void
  onError?: (err: unknown) => void
}

const ANNOUNCE_MODEL = 'announcement'

const loading = ref(false)
const imageUploading = ref(false)
const allArticles = ref<CmsArticle[]>([])
const categoryOptions = ref<CmsArticleCategory[]>([])
const filterStatus = ref<number | undefined>(undefined)
const filterCategoryId = ref<number | undefined>(undefined)
const searchKeyword = ref('')

const pagination = reactive({
  current: 1,
  pageSize: 20,
  showSizeChanger: true,
  showQuickJumper: true,
})

const statCards = reactive([
  { key: 0, icon: '✅', label: '已发布', value: 0, color: 'green' },
  { key: 1, icon: '⏳', label: '待审核', value: 0, color: 'orange' },
  { key: 2, icon: '❌', label: '已驳回', value: 0, color: 'red' },
  { key: -1, icon: '📝', label: '全部文章', value: 0, color: 'blue' },
])

const columns = [
  { title: '文章信息', key: 'info', width: 360 },
  { title: '状态', key: 'status', width: 110 },
  { title: '数据', key: 'metrics', width: 120 },
  { title: '推荐', key: 'recommend', width: 80 },
  { title: '创建时间', key: 'createTime', width: 120 },
  { title: '操作', key: 'action', width: 170 },
]

const showFormModal = ref(false)
const saving = ref(false)
const editingArticle = ref<CmsArticle | null>(null)
const formData = reactive<CmsArticle>({
  title: '',
  author: '',
  source: '',
  overview: '',
  content: '',
  status: 0,
  categoryId: undefined,
  image: '',
})
const formRecommend = ref(false)
const editorMode = ref<'edit' | 'preview'>('edit')

const isMarkdown = computed(() => {
  return formData.content && (
    /[#*`_\[\]()!>-]/.test(formData.content) ||
    /^(#{1,6}\s|[-*]\s|\d+\.\s|>)/m.test(formData.content)
  )
})

const isPreviewMarkdown = computed(() => {
  if (!previewData.value?.content) return false
  return (
    /[#*`_\[\]()!>-]/.test(previewData.value.content) ||
    /^(#{1,6}\s|[-*]\s|\d+\.\s|>)/m.test(previewData.value.content)
  )
})

const showPreviewModal = ref(false)
const previewData = ref<CmsArticle | null>(null)
const showImagePreview = ref(false)
const previewImageUrl = ref('')

const categorySelectOptions = computed(() =>
  categoryOptions.value.map(item => ({
    value: item.categoryId,
    label: item.title || `分类 ${item.categoryId}`,
  }))
)

const standardArticles = computed(() => allArticles.value.filter(isStandardArticle))

const filteredArticles = computed(() => {
  const keyword = searchKeyword.value.trim().toLowerCase()

  return [...standardArticles.value]
    .filter(item => filterStatus.value === undefined || item.status === filterStatus.value)
    .filter(item => filterCategoryId.value === undefined || item.categoryId === filterCategoryId.value)
    .filter(item => {
      if (!keyword) return true
      return [item.title, item.overview, item.author, item.source]
        .some(value => String(value || '').toLowerCase().includes(keyword))
    })
    .sort((a, b) => {
      const timeA = a.createTime || ''
      const timeB = b.createTime || ''
      if (timeA && timeB && timeA !== timeB) {
        return timeB.localeCompare(timeA)
      }
      return (b.articleId || 0) - (a.articleId || 0)
    })
})

const pagedArticles = computed(() => {
  const start = (pagination.current - 1) * pagination.pageSize
  return filteredArticles.value.slice(start, start + pagination.pageSize)
})

const tablePagination = computed(() => ({
  current: pagination.current,
  pageSize: pagination.pageSize,
  total: filteredArticles.value.length,
  showSizeChanger: pagination.showSizeChanger,
  showQuickJumper: pagination.showQuickJumper,
}))

async function loadCategories(silent = false) {
  try {
    const list = await listCmsArticleCategory({ status: 0 })
    categoryOptions.value = (list || [])
      .filter(item => item.categoryId)
      .sort((a, b) => (a.sortNumber || 0) - (b.sortNumber || 0))
  } catch (e: any) {
    if (!silent) {
      message.error(e?.message || '加载文章分类失败')
    }
  }
}

async function loadArticles() {
  loading.value = true
  try {
    const list = await listCmsArticle()
    allArticles.value = list || []
    ensurePaginationInRange()
    updateStats()
  } catch (e: any) {
    message.error(e?.message || '加载文章列表失败')
  } finally {
    loading.value = false
  }
}

function updateStats() {
  const list = standardArticles.value
  statCards[0].value = list.filter(item => item.status === 0).length
  statCards[1].value = list.filter(item => item.status === 1).length
  statCards[2].value = list.filter(item => item.status === 2).length
  statCards[3].value = list.length
}

function ensurePaginationInRange() {
  const total = filteredArticles.value.length
  const maxPage = Math.max(1, Math.ceil(total / pagination.pageSize))
  if (pagination.current > maxPage) {
    pagination.current = maxPage
  }
}

function isStandardArticle(item: CmsArticle) {
  return (item.model || '').trim() !== ANNOUNCE_MODEL
}

function handleStatFilter(key: number) {
  filterStatus.value = key === -1 ? undefined : key
  pagination.current = 1
}

function handleSearch() {
  pagination.current = 1
}

function handleTableChange(pag: { current: number; pageSize: number }) {
  pagination.current = pag.current
  pagination.pageSize = pag.pageSize
  ensurePaginationInRange()
}

function resetForm() {
  Object.assign(formData, {
    articleId: undefined,
    title: '',
    author: '',
    source: '',
    overview: '',
    content: '',
    status: 0,
    categoryId: undefined,
    image: '',
  })
  formRecommend.value = false
}

function handleCreate() {
  editingArticle.value = null
  resetForm()
  showFormModal.value = true
}

function handleEdit(record: CmsArticle) {
  editingArticle.value = record
  Object.assign(formData, {
    articleId: record.articleId,
    title: record.title || '',
    author: record.author || '',
    source: record.source || '',
    overview: record.overview || '',
    content: record.content || '',
    status: record.status ?? 0,
    categoryId: record.categoryId,
    image: record.image || '',
  })
  formRecommend.value = !!record.recommend
  showFormModal.value = true
}

function handleView(record: CmsArticle) {
  previewData.value = {
    ...record,
    categoryName: resolveCategoryName(record),
  }
  showPreviewModal.value = true
}

async function handleSave() {
  if (!formData.title?.trim()) {
    message.warning('请输入文章标题')
    return
  }
  if (!formData.content?.trim()) {
    message.warning('请输入文章内容')
    return
  }

  saving.value = true
  try {
    const data: CmsArticle = {
      ...formData,
      model: undefined,
      categoryName: resolveCategoryNameById(formData.categoryId),
      recommend: formRecommend.value ? 1 : 0,
    }
    if (editingArticle.value?.articleId) {
      await updateCmsArticle(data)
      message.success('文章已更新')
    } else {
      await addCmsArticle(data)
      message.success('文章已创建')
    }
    showFormModal.value = false
    await loadArticles()
  } catch (e: any) {
    message.error(e?.message || '保存失败')
  } finally {
    saving.value = false
  }
}

async function handleDelete(record: CmsArticle) {
  try {
    await removeCmsArticle(record.articleId)
    message.success('文章已删除')
    await loadArticles()
  } catch (e: any) {
    message.error(e?.message || '删除失败')
  }
}

async function handleToggleRecommend(record: CmsArticle, val: boolean) {
  try {
    await updateCmsArticle({ articleId: record.articleId, recommend: val ? 1 : 0 })
    message.success(val ? '已加入推荐' : '已取消推荐')
    await loadArticles()
  } catch (e: any) {
    message.error(e?.message || '操作失败')
  }
}

function beforeImageUpload(file: File) {
  if (!file.type.startsWith('image/')) {
    message.error('只能上传图片文件')
    return false
  }
  if (file.size > 5 * 1024 * 1024) {
    message.error('图片大小不能超过 5MB')
    return false
  }
  return true
}

async function handleCoverUpload(option: UploadRequestOption) {
  const rawFile = option.file
  if (!rawFile) return

  imageUploading.value = true
  try {
    const record = await uploadFile(rawFile)
    const url = (record?.url || record?.downloadUrl || '').trim()
    if (!url) throw new Error('上传成功但未返回图片地址')
    formData.image = url
    option.onSuccess?.(record, rawFile)
    message.success('封面上传成功')
  } catch (e) {
    option.onError?.(e)
    message.error(e instanceof Error ? e.message : '封面上传失败')
  } finally {
    imageUploading.value = false
  }
}

function handleRemoveCover() {
  formData.image = ''
}

function handlePreviewImage(url?: string) {
  if (!url) return
  previewImageUrl.value = url
  showImagePreview.value = true
}

function resolveCategoryName(record: CmsArticle) {
  return record.categoryName || resolveCategoryNameById(record.categoryId)
}

function resolveCategoryNameById(categoryId?: number) {
  if (!categoryId) return ''
  return categoryOptions.value.find(item => item.categoryId === categoryId)?.title || ''
}

function statusText(status?: number) {
  const map: Record<number, string> = {
    0: '已发布',
    1: '待审核',
    2: '已驳回',
    3: '违规',
  }
  return map[status ?? -1] || '-'
}

function statusColor(status?: number) {
  const map: Record<number, string> = {
    0: 'success',
    1: 'orange',
    2: 'error',
    3: 'volcano',
  }
  return map[status ?? -1] || 'default'
}

watch([filteredArticles, () => pagination.pageSize], () => {
  ensurePaginationInRange()
})

onMounted(async () => {
  await loadCategories(true)
  await loadArticles()
})
</script>

<style scoped>
.articles-page { min-height: 100%; }

.page-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 20px;
}

.page-title { font-size: 18px; font-weight: 700; color: #1f2937; margin: 0; }
.page-desc { font-size: 13px; color: #9ca3af; margin: 2px 0 0; }

.stat-card {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 16px;
  border-radius: 12px;
  border: 2px solid transparent;
  cursor: pointer;
  transition: all 0.2s;
}

.stat-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); }
.stat-card.active { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12); }
.stat-card.blue { background: #eff6ff; border-color: #dbeafe; }
.stat-card.green { background: #f0fdf4; border-color: #bbf7d0; }
.stat-card.orange { background: #fff7ed; border-color: #fed7aa; }
.stat-card.red { background: #fff1f2; border-color: #fecdd3; }
.stat-card.active.blue { border-color: #3b82f6; }
.stat-card.active.green { border-color: #22c55e; }
.stat-card.active.orange { border-color: #f97316; }
.stat-card.active.red { border-color: #ef4444; }
.stat-icon { font-size: 28px; flex-shrink: 0; }
.stat-value { font-size: 22px; font-weight: 700; color: rgba(0, 0, 0, 0.85); line-height: 1.2; }
.stat-label { font-size: 12px; color: rgba(0, 0, 0, 0.45); margin-top: 2px; }

.panel { background: #fff; border: 1px solid #f0f0f0; border-radius: 12px; overflow: hidden; }
.panel-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 18px;
  border-bottom: 1px solid #f5f5f5;
  flex-wrap: wrap;
  gap: 10px;
}
.panel-title { font-size: 14px; font-weight: 600; color: rgba(0, 0, 0, 0.85); }

.article-info-cell { display: flex; align-items: flex-start; gap: 12px; }
.article-thumb {
  width: 72px;
  height: 48px;
  border-radius: 8px;
  object-fit: cover;
  flex-shrink: 0;
  border: 1px solid #f0f0f0;
}
.article-thumb-empty {
  width: 72px;
  height: 48px;
  border-radius: 8px;
  background: #f3f4f6;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 20px;
  flex-shrink: 0;
}
.article-info-text { flex: 1; min-width: 0; }
.article-title {
  font-size: 14px;
  font-weight: 500;
  color: rgba(0, 0, 0, 0.85);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.article-meta {
  font-size: 12px;
  color: rgba(0, 0, 0, 0.45);
  margin-top: 4px;
}
.meta-item { margin-left: 8px; }
.article-overview {
  margin-top: 4px;
  font-size: 12px;
  color: rgba(0, 0, 0, 0.45);
  line-height: 1.5;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.metrics-cell { font-size: 12px; color: rgba(0, 0, 0, 0.45); line-height: 1.7; }

.cover-upload-wrap { display: flex; flex-direction: column; gap: 10px; }
.cover-preview-card {
  width: 220px;
  padding: 8px;
  border: 1px dashed #d9d9d9;
  border-radius: 10px;
  background: #fafafa;
}
.cover-preview-image {
  width: 100%;
  height: 124px;
  object-fit: cover;
  border-radius: 8px;
  display: block;
}
.cover-preview-actions { display: flex; gap: 8px; margin-top: 8px; }
.field-hint { font-size: 12px; color: rgba(0, 0, 0, 0.45); }
.switch-tip { margin-left: 8px; font-size: 12px; color: rgba(0, 0, 0, 0.45); }

.content-editor-wrap {
  border: 1px solid #d9d9d9;
  border-radius: 8px;
  overflow: hidden;
}

.editor-tabs {
  padding: 8px 12px;
  background: #fafafa;
  border-bottom: 1px solid #f0f0f0;
}

.preview-only-mode {
  padding: 16px;
  min-height: 320px;
  background: #fff;
}

.empty-preview {
  color: rgba(0, 0, 0, 0.25);
  font-style: italic;
  text-align: center;
  padding: 60px 0;
}

.format-hint {
  margin-left: 8px;
  font-size: 12px;
  color: rgba(0, 0, 0, 0.45);
}

.preview-meta { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.preview-meta-text { font-size: 12px; color: rgba(0, 0, 0, 0.45); }
.preview-cover-wrap { margin: 16px 0 12px; }
.preview-cover {
  width: 100%;
  max-height: 320px;
  object-fit: cover;
  border-radius: 12px;
  border: 1px solid #f0f0f0;
}
.preview-summary {
  margin-top: 12px;
  padding: 12px 14px;
  background: #fafafa;
  border-radius: 10px;
  color: rgba(0, 0, 0, 0.65);
  line-height: 1.7;
}
.preview-content {
  font-size: 15px;
  line-height: 1.8;
  color: rgba(0, 0, 0, 0.85);
}
.image-preview-modal {
  width: 100%;
  max-height: 70vh;
  object-fit: contain;
  border-radius: 8px;
}

.text-sm { font-size: 12px; }
.text-gray { color: rgba(0, 0, 0, 0.45); }
.mb-6 { margin-bottom: 24px; }
</style>
