1106 lines
25 KiB
Plaintext
1106 lines
25 KiB
Plaintext
<!-- 内容详情页面 - UTSJSONObject 优化版本 -->
|
|
<template>
|
|
<scroll-view direction="vertical" class="content-detail" :scroll-y="true" :enable-back-to-top="true">
|
|
<!-- 导航栏 -->
|
|
<view class="detail-header">
|
|
<view class="header-content">
|
|
<view class="back-btn" @click="goBack">
|
|
<text class="back-icon">←</text>
|
|
</view>
|
|
<text class="header-title">{{ $t('mt.title.news') }}</text>
|
|
<view class="header-actions">
|
|
<view class="action-btn" @click="showLanguageSwitcher">
|
|
<text class="action-icon">🌐</text>
|
|
</view>
|
|
<view class="action-btn" @click="shareContent">
|
|
<text class="action-icon">📤</text>
|
|
</view>
|
|
<view class="action-btn" @click="toggleBookmark">
|
|
<text class="action-icon">{{ isBookmarked ? '★' : '☆' }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 加载状态 -->
|
|
<view class="loading-section" v-if="pageState.loading">
|
|
<text class="loading-text">{{ $t('mt.status.loading') }}</text>
|
|
</view>
|
|
|
|
<!-- 错误状态 -->
|
|
<view class="error-section" v-if="pageState.error !== null">
|
|
<text class="error-text">{{ pageState.error }}</text>
|
|
<view class="retry-btn" @click="retryLoad">
|
|
<text class="retry-text">{{ $t('mt.action.retry') }}</text>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 内容详情 -->
|
|
<view class="detail-content" v-if="contentData !== null && !pageState.loading">
|
|
<!-- 内容头部 -->
|
|
<view class="content-header">
|
|
<text class="content-title">{{ contentData?.title }}</text>
|
|
<view class="content-meta">
|
|
<view class="meta-row">
|
|
<text class="meta-label">{{ $t('mt.detail.author') }}</text>
|
|
<text class="meta-value">{{ contentData?.author }}</text>
|
|
</view>
|
|
<view class="meta-row">
|
|
<text class="meta-label">{{ $t('mt.detail.publishedAt') }}</text>
|
|
<text class="meta-value">{{ contentData?.published_at }}</text>
|
|
</view>
|
|
<view class="meta-row">
|
|
<text class="meta-label">{{ $t('mt.detail.originalLanguage') }}</text>
|
|
<text class="meta-value">{{ contentData?.original_language }}</text>
|
|
</view>
|
|
<view class="meta-row">
|
|
<text class="meta-label">{{ $t('mt.detail.qualityScore') }}</text>
|
|
<view class="quality-score">
|
|
<view class="quality-bar"
|
|
:style="{ width: `${(contentData?.quality_score ?? 0) * 100}%`, backgroundColor: getQualityScoreColor(contentData?.quality_score ?? 0) }">
|
|
</view>
|
|
<text
|
|
class="quality-text">{{ getQualityScoreText(contentData?.quality_score ?? 0) }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 内容摘要 -->
|
|
<view class="content-summary" v-if="contentData?.summary !== ''">
|
|
<text class="summary-title">{{ $t('mt.detail.summary') }}</text>
|
|
<text class="summary-text">{{ contentData?.summary }}</text>
|
|
</view>
|
|
|
|
<!-- 语言切换 -->
|
|
<view class="language-switcher" v-if="availableTranslations.length > 0">
|
|
<text class="switcher-title">{{ $t('mt.detail.selectLanguage') }}</text>
|
|
<scroll-view direction="horizontal" class="language-scroll" :scroll-x="true">
|
|
<view class="language-tabs">
|
|
<view class="language-tab" :class="{ active: currentLanguage === 'original' }"
|
|
@click="switchToOriginal">
|
|
<text class="language-text">{{ $t('mt.detail.originalText') }}</text>
|
|
</view>
|
|
<view v-for="translation in availableTranslations" :key="translation.id" class="language-tab"
|
|
:class="{ active: currentLanguage === translation.id }"
|
|
@click="switchToTranslation(translation)">
|
|
<text class="language-text">{{ translation.language_id }}</text>
|
|
<view class="translation-quality" v-if="translation.human_verified">
|
|
<text class="quality-mark">✓</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</scroll-view>
|
|
</view>
|
|
|
|
<!-- 内容正文 -->
|
|
<view class="content-body">
|
|
<text class="body-text">{{ currentContentText }}</text>
|
|
</view>
|
|
|
|
<!-- 内容标签 -->
|
|
<view class="content-tags" v-if="contentTags.length > 0">
|
|
<text class="tags-title">{{ $t('mt.detail.tags') }}</text>
|
|
<view class="tags-list">
|
|
<view v-for="tag in contentTags" :key="tag" class="tag-item" @click="searchByTag(tag)">
|
|
<text class="tag-text"># {{ tag }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 统计信息 -->
|
|
<view class="content-stats">
|
|
<view class="stats-row">
|
|
<view class="stat-item" @click="likeContent">
|
|
<text class="stat-icon">👍</text>
|
|
<text class="stat-text">{{ contentData?.like_count }}</text>
|
|
</view>
|
|
<view class="stat-item" @click="shareContent">
|
|
<text class="stat-icon">📤</text>
|
|
<text class="stat-text">{{ contentData?.share_count }}</text>
|
|
</view>
|
|
<view class="stat-item">
|
|
<text class="stat-icon">👁</text>
|
|
<text class="stat-text">{{ contentData?.view_count }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 来源信息 -->
|
|
<view class="content-source" v-if="contentData?.source_url !== ''">
|
|
<text class="source-title">{{ $t('mt.detail.source') }}</text>
|
|
<view class="source-link" @click="openSource">
|
|
<text class="link-text">{{ contentData?.source_url }}</text>
|
|
<text class="link-icon">🔗</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 相关推荐 -->
|
|
<view class="related-section" v-if="relatedContentsList.length > 0">
|
|
<view class="section-header">
|
|
<text class="section-title">{{ $t('mt.detail.related') }}</text>
|
|
</view>
|
|
<view class="related-list">
|
|
<view v-for="content in relatedContentsList" :key="content.id" class="related-item"
|
|
@click="navigateToContent(content)">
|
|
<view class="related-content">
|
|
<text class="related-title">{{ content.title }}</text>
|
|
<text class="related-summary">{{ content.summary }}</text>
|
|
<view class="related-meta">
|
|
<text class="related-time">{{ formatRelativeTimeKey(content.published_at) }}</text>
|
|
<view class="related-quality"
|
|
:style="{ backgroundColor: getQualityScoreColor(content.quality_score) }">
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 评论区域 -->
|
|
<view class="comments-section" v-if="contentData !== null">
|
|
<Comments :targetType="'content'" :targetId="contentData?.id ?? ''" />
|
|
</view>
|
|
</scroll-view>
|
|
|
|
<!-- AI助手按钮 -->
|
|
<view class="ai-assistant-btn" @click="openAIChat">
|
|
<text class="ai-icon">🤖</text>
|
|
<text class="ai-text">{{ $t('mt.title.chat') }}</text>
|
|
</view>
|
|
|
|
<!-- 语言切换弹窗 -->
|
|
<view class="language-modal" v-if="showLanguageModal" @click="hideLanguageSwitcher">
|
|
<view class="language-modal-content" @click.stop>
|
|
<view class="modal-header">
|
|
<text class="modal-title">选择语言 / Select Language</text>
|
|
<view class="modal-close" @click="hideLanguageSwitcher">
|
|
<text class="close-icon">✕</text>
|
|
</view>
|
|
</view>
|
|
<view class="language-options">
|
|
<view class="language-option"
|
|
:class="{ active: currentAppLanguage === 'zh-CN' }"
|
|
@click="switchAppLanguage('zh-CN')">
|
|
<text class="language-name">简体中文</text>
|
|
<text class="language-code">zh-CN</text>
|
|
<text v-if="currentAppLanguage === 'zh-CN'" class="selected-mark">✓</text>
|
|
</view>
|
|
<view class="language-option"
|
|
:class="{ active: currentAppLanguage === 'en' }"
|
|
@click="switchAppLanguage('en')">
|
|
<text class="language-name">English</text>
|
|
<text class="language-code">en</text>
|
|
<text v-if="currentAppLanguage === 'en'" class="selected-mark">✓</text>
|
|
</view>
|
|
<view class="language-option"
|
|
:class="{ active: currentAppLanguage === 'zh-TW' }"
|
|
@click="switchAppLanguage('zh-TW')">
|
|
<text class="language-name">繁體中文</text>
|
|
<text class="language-code">zh-TW</text>
|
|
<text v-if="currentAppLanguage === 'zh-TW'" class="selected-mark">✓</text>
|
|
</view>
|
|
<view class="language-option"
|
|
:class="{ active: currentAppLanguage === 'ja' }"
|
|
@click="switchAppLanguage('ja')">
|
|
<text class="language-name">日本語</text>
|
|
<text class="language-code">ja</text>
|
|
<text v-if="currentAppLanguage === 'ja'" class="selected-mark">✓</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script setup lang="uts">
|
|
import supa from '@/components/supadb/aksupainstance.uts'
|
|
import Comments from './comments.uvue'
|
|
import type {
|
|
InfoContent,
|
|
TranslationData,
|
|
PageState
|
|
} from './types.uts'
|
|
import {
|
|
getQualityScoreText,
|
|
getQualityScoreColor,
|
|
formatRelativeTimeKey,
|
|
} from './types.uts'
|
|
import { state as userState, getCurrentUser, setUserProfile, setIsLoggedIn } from '@/utils/store.uts'
|
|
import { setClipboardData } from '@/uni_modules/lime-clipboard'
|
|
import { tt } from '@/utils/i18nfun.uts'
|
|
import i18n from '@/i18n/index.uts' // 保留用于语言切换
|
|
|
|
// 页面参数
|
|
const contentId = ref<string>('')
|
|
|
|
// 页面状态
|
|
const pageState = ref<PageState>({
|
|
loading: true,
|
|
error: null,
|
|
currentPage: 1,
|
|
pageSize: 20,
|
|
total: 0
|
|
})
|
|
|
|
// 数据状态
|
|
const contentData = ref<InfoContent | null>(null)
|
|
const availableTranslations = ref<Array<TranslationData>>([])
|
|
const relatedContentsList = ref<Array<InfoContent>>([])
|
|
|
|
// 交互状态
|
|
const currentLanguage = ref<string>('original')
|
|
const isBookmarked = ref<boolean>(false)
|
|
// 是否已点赞,类型安全,后端接口应返回 is_liked 字段
|
|
const hasLiked = ref<boolean>(false)
|
|
// 语言切换相关状态
|
|
const showLanguageModal = ref<boolean>(false)
|
|
const currentAppLanguage = ref<string>('zh-CN') // 当前应用语言
|
|
// 用户信息 - 统一用全局 userState
|
|
const userProfile = computed(() : void => {
|
|
userState.userProfile
|
|
})
|
|
const isLoggedIn = computed(() => userState.isLoggedIn)
|
|
|
|
|
|
// 计算属性
|
|
const currentContentText = computed(() : string => {
|
|
const data = contentData.value
|
|
if (data == null) return ''
|
|
if (currentLanguage.value === 'original') {
|
|
return data.content
|
|
} else {
|
|
const translation = availableTranslations.value.find(t => t.id === currentLanguage.value)
|
|
if (translation != null) {
|
|
return translation.content
|
|
}
|
|
}
|
|
return data.content
|
|
})
|
|
|
|
const contentTags = computed(() : Array<string> => {
|
|
const data = contentData.value
|
|
if (data == null) return []
|
|
return data.tags ?? []
|
|
})
|
|
|
|
// 监听语言变化,保留 i18n 实例用于语言切换
|
|
const currentI18nLocale = computed(() => i18n.global.locale.value)
|
|
|
|
// 监听全局语言变化,同步本地状态
|
|
watch(currentI18nLocale, (newLocale) => {
|
|
if (newLocale !== currentAppLanguage.value) {
|
|
currentAppLanguage.value = newLocale
|
|
}
|
|
})
|
|
|
|
|
|
const loadTranslations = async () => {
|
|
if (contentId.value === '' || supa === null) return
|
|
try {
|
|
const result = await supa.from('ak_content_translations')
|
|
.select('*', {})
|
|
.eq('content_id', contentId.value)
|
|
.executeAs<TranslationData>()
|
|
if (result.data !== null && Array.isArray(result.data)) {
|
|
availableTranslations.value = result.data as Array<TranslationData>
|
|
}
|
|
} catch (e) {
|
|
console.error('Translations loading error:', e)
|
|
availableTranslations.value = []
|
|
}
|
|
}
|
|
|
|
const loadRelatedContents = async () => {
|
|
if (contentId.value === '' || supa === null) return
|
|
try {
|
|
// 获取相关内容(同分类的其他内容)
|
|
const result = await supa.from('ak_contents')
|
|
.select('*', {})
|
|
.neq('id', contentId.value)
|
|
.limit(5)
|
|
.order('published_at', { ascending: false })
|
|
.executeAs<InfoContent>()
|
|
if (result.data !== null && Array.isArray(result.data)) {
|
|
relatedContentsList.value = result.data as Array<InfoContent>
|
|
}
|
|
} catch (e) {
|
|
console.error('Related contents loading error:', e)
|
|
relatedContentsList.value = []
|
|
}
|
|
}
|
|
|
|
const recordViewBehavior = () => {
|
|
// 记录用户查看行为
|
|
console.log('Recording view behavior for content:', contentId.value)
|
|
// 这里可以调用API记录用户行为
|
|
}
|
|
|
|
// 交互函数
|
|
const switchToOriginal = () => {
|
|
currentLanguage.value = 'original'
|
|
}
|
|
|
|
const switchToTranslation = (translation : TranslationData) => {
|
|
currentLanguage.value = translation.id
|
|
}
|
|
|
|
const toggleBookmark = () => {
|
|
if (!isLoggedIn.value) {
|
|
uni.showToast({
|
|
title: '请先登录后再收藏',
|
|
icon: 'none'
|
|
})
|
|
// 可引导跳转登录页
|
|
return
|
|
}
|
|
isBookmarked.value = !isBookmarked.value
|
|
const action = isBookmarked.value ? '收藏' : '取消收藏'
|
|
uni.showToast({
|
|
title: `${action}成功`,
|
|
icon: 'success'
|
|
})
|
|
}
|
|
|
|
const likeContent = () => {
|
|
if (!isLoggedIn.value) {
|
|
uni.showToast({
|
|
title: '请先登录后再点赞',
|
|
icon: 'none'
|
|
})
|
|
// 可引导跳转登录页
|
|
return
|
|
}
|
|
if (hasLiked.value) {
|
|
uni.showToast({
|
|
title: '您已经点过赞了',
|
|
icon: 'none'
|
|
})
|
|
return
|
|
}
|
|
hasLiked.value = true
|
|
// 更新点赞数
|
|
const data = contentData.value
|
|
if (data != null) {
|
|
const currentLikes = data.like_count
|
|
// 直接赋值,避免 set 方法和类型推断问题
|
|
data.like_count = currentLikes + 1
|
|
}
|
|
uni.showToast({
|
|
title: '点赞成功',
|
|
icon: 'success'
|
|
})
|
|
}
|
|
|
|
const shareContent = () => {
|
|
const data = contentData.value
|
|
if (data == null) return
|
|
const title = data.title
|
|
const summary = data.summary
|
|
const url = `https://example.com/info/detail?id=${contentId.value}`
|
|
|
|
uni.showActionSheet({
|
|
itemList: ['复制链接', '分享到微信', '分享到微博'],
|
|
success: (res) => {
|
|
switch (res.tapIndex) {
|
|
case 0:
|
|
// 复制链接,使用 lime-clipboard
|
|
setClipboardData({
|
|
data: `${title}\n${summary}\n${url}`,
|
|
success: (result) => {
|
|
uni.showToast({
|
|
title: '链接已复制',
|
|
icon: 'success'
|
|
})
|
|
},
|
|
fail: (err) => {
|
|
uni.showToast({
|
|
title: '复制失败',
|
|
icon: 'none'
|
|
})
|
|
}
|
|
})
|
|
break
|
|
case 1:
|
|
// 分享到微信
|
|
uni.showToast({
|
|
title: '请使用系统分享功能',
|
|
icon: 'none'
|
|
})
|
|
break
|
|
case 2:
|
|
// 分享到微博
|
|
uni.showToast({
|
|
title: '请使用系统分享功能',
|
|
icon: 'none'
|
|
})
|
|
break
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
const searchByTag = (tag : string) => {
|
|
try {
|
|
uni.navigateTo({
|
|
url: `/pages/info/search?keyword=${encodeURIComponent(tag)}`
|
|
})
|
|
} catch (error) {
|
|
console.error('导航异常:', error)
|
|
}
|
|
}
|
|
|
|
const openSource = () => {
|
|
const data = contentData.value
|
|
if (data == null) return
|
|
const sourceUrl = data.source_url
|
|
if (sourceUrl === '') return
|
|
uni.showModal({
|
|
title: '打开原文链接',
|
|
content: '是否在浏览器中打开原文链接?',
|
|
success: (res) => {
|
|
if (res.confirm) {
|
|
// 在这里可以调用系统浏览器打开链接
|
|
console.log('Opening source URL:', sourceUrl)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
const openAIChat = () => {
|
|
const data = contentData.value
|
|
if (data == null) return
|
|
const title = data.title
|
|
try {
|
|
uni.navigateTo({
|
|
url: `/pages/info/chat?context=${encodeURIComponent(title)}`
|
|
})
|
|
} catch (error) {
|
|
console.error('导航异常:', error)
|
|
}
|
|
}
|
|
|
|
const navigateToContent = (content : InfoContent) => {
|
|
const relatedContentId = content.id
|
|
try {
|
|
uni.redirectTo({
|
|
url: `/pages/info/detail?id=${relatedContentId}`
|
|
})
|
|
} catch (error) {
|
|
console.error('导航异常:', error)
|
|
}
|
|
}
|
|
|
|
const goBack = () => {
|
|
try {
|
|
uni.navigateBack({
|
|
delta: 1
|
|
})
|
|
} catch (error) {
|
|
console.error('返回异常:', error)
|
|
}
|
|
}
|
|
|
|
// 语言切换相关函数
|
|
const showLanguageSwitcher = () => {
|
|
showLanguageModal.value = true
|
|
}
|
|
|
|
const hideLanguageSwitcher = () => {
|
|
showLanguageModal.value = false
|
|
}
|
|
|
|
const switchAppLanguage = (languageCode: string) => {
|
|
currentAppLanguage.value = languageCode
|
|
// 使用 i18n 全局语言切换
|
|
i18n.global.locale.value = languageCode
|
|
hideLanguageSwitcher()
|
|
|
|
// 显示切换成功提示(使用新语言显示)
|
|
const successMessages = {
|
|
'zh-CN': '已切换到简体中文',
|
|
'en': 'Switched to English',
|
|
'zh-TW': '已切換到繁體中文',
|
|
'ja': '日本語に切り替えました'
|
|
}
|
|
const message = successMessages[languageCode] || `Language switched to ${languageCode}`
|
|
uni.showToast({
|
|
title: message,
|
|
icon: 'success',
|
|
duration: 2000
|
|
})
|
|
}
|
|
// 数据加载函数 - 使用 executeAs 替代模拟数据
|
|
const loadContentData = async () => {
|
|
if (contentId.value === '' || supa === null) return
|
|
|
|
pageState.value.loading = true
|
|
pageState.value.error = null
|
|
|
|
try {
|
|
const result = await supa.from('ak_contents')
|
|
.select('*', {})
|
|
.eq('id', contentId.value)
|
|
.single()
|
|
.executeAs<InfoContent>()
|
|
|
|
if (result.data !== null && Array.isArray(result.data)) {
|
|
const realcontent = result.data as InfoContent[]
|
|
contentData.value = realcontent[0]
|
|
pageState.value.loading = false
|
|
// 记录访问行为
|
|
recordViewBehavior()
|
|
// 加载翻译和相关内容
|
|
await loadTranslations()
|
|
await loadRelatedContents()
|
|
} else {
|
|
throw new Error('Content not found')
|
|
}
|
|
} catch (e) {
|
|
pageState.value.loading = false
|
|
pageState.value.error = '内容加载失败,请稍后重试'
|
|
console.error('Content loading error:', e)
|
|
}
|
|
}
|
|
const retryLoad = () => {
|
|
loadContentData()
|
|
}
|
|
|
|
|
|
onLoad((options) => {
|
|
// 初始化当前语言,从 i18n 获取
|
|
currentAppLanguage.value = i18n.global.locale.value || 'zh-CN'
|
|
|
|
if (options["id"] !== null) {
|
|
contentId.value = options.getString("id") ?? ''
|
|
loadContentData()
|
|
}
|
|
if (userState.isLoggedIn) {
|
|
getCurrentUser()
|
|
}
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
// 清理工作
|
|
})
|
|
</script>
|
|
|
|
<style>
|
|
.content-detail {
|
|
flex: 1;
|
|
background-color: #f5f5f5;
|
|
}
|
|
|
|
.detail-header {
|
|
background-color: #ffffff;
|
|
border-bottom: 1px solid #e5e5e5;
|
|
}
|
|
|
|
.header-content {
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 12px 16px;
|
|
}
|
|
|
|
.back-btn {
|
|
width: 40px;
|
|
height: 40px;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
border-radius: 20px;
|
|
background-color: #f3f4f6;
|
|
}
|
|
|
|
.back-icon {
|
|
font-size: 18px;
|
|
color: #374151;
|
|
}
|
|
|
|
.header-title {
|
|
font-size: 18px;
|
|
font-weight: bold;
|
|
color: #1f2937;
|
|
}
|
|
|
|
.header-actions {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
}
|
|
|
|
.action-btn {
|
|
margin-left: 8px;
|
|
width: 40px;
|
|
height: 40px;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
border-radius: 20px;
|
|
background-color: #f3f4f6;
|
|
}
|
|
|
|
.action-icon {
|
|
font-size: 16px;
|
|
color: #374151;
|
|
}
|
|
|
|
.loading-section,
|
|
.error-section {
|
|
padding: 60px 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.loading-text,
|
|
.error-text {
|
|
font-size: 16px;
|
|
color: #6b7280;
|
|
}
|
|
|
|
.retry-btn {
|
|
margin-top: 16px;
|
|
padding: 8px 16px;
|
|
background-color: #3b82f6;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.retry-text {
|
|
font-size: 14px;
|
|
color: #ffffff;
|
|
}
|
|
|
|
.detail-content {
|
|
background-color: #ffffff;
|
|
margin: 12px;
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
}
|
|
|
|
.content-header {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.content-title {
|
|
font-size: 24px;
|
|
font-weight: bold;
|
|
color: #1f2937;
|
|
line-height: 32px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.content-meta {
|
|
border-top: 1px solid #e5e5e5;
|
|
padding-top: 16px;
|
|
}
|
|
|
|
.meta-row {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.meta-label {
|
|
font-size: 14px;
|
|
color: #6b7280;
|
|
width: 80px;
|
|
}
|
|
|
|
.meta-value {
|
|
font-size: 14px;
|
|
color: #374151;
|
|
flex: 1;
|
|
}
|
|
|
|
.quality-score {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
flex: 1;
|
|
}
|
|
|
|
.quality-bar {
|
|
flex: 1;
|
|
height: 6px;
|
|
background-color: #e5e5e5;
|
|
border-radius: 3px;
|
|
margin-right: 8px;
|
|
}
|
|
|
|
.quality-fill {
|
|
height: 100%;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.quality-text {
|
|
font-size: 12px;
|
|
color: #6b7280;
|
|
}
|
|
|
|
.content-summary {
|
|
margin-bottom: 20px;
|
|
padding: 16px;
|
|
background-color: #f9fafb;
|
|
border-radius: 8px;
|
|
border-left: 4px solid #3b82f6;
|
|
}
|
|
|
|
.summary-title {
|
|
font-size: 16px;
|
|
font-weight: bold;
|
|
color: #1f2937;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.summary-text {
|
|
font-size: 14px;
|
|
color: #6b7280;
|
|
line-height: 22px;
|
|
}
|
|
|
|
.language-switcher {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.switcher-title {
|
|
font-size: 16px;
|
|
font-weight: bold;
|
|
color: #1f2937;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.language-scroll {
|
|
height: 50px;
|
|
}
|
|
|
|
.language-tabs {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
}
|
|
|
|
.language-tab {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
margin-right: 12px;
|
|
padding: 8px 16px;
|
|
border-radius: 20px;
|
|
background-color: #f3f4f6;
|
|
}
|
|
|
|
.language-tab.is-active {
|
|
background-color: #3b82f6;
|
|
}
|
|
|
|
.language-text {
|
|
font-size: 14px;
|
|
color: #374151;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.language-tab.is-active .language-text {
|
|
color: #ffffff;
|
|
}
|
|
|
|
.translation-quality {
|
|
margin-left: 4px;
|
|
width: 16px;
|
|
height: 16px;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
border-radius: 8px;
|
|
background-color: #10b981;
|
|
}
|
|
|
|
.quality-mark {
|
|
font-size: 10px;
|
|
color: #ffffff;
|
|
}
|
|
|
|
.content-body {
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.body-text {
|
|
font-size: 16px;
|
|
color: #374151;
|
|
line-height: 28px;
|
|
}
|
|
|
|
.content-tags {
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.tags-title {
|
|
font-size: 16px;
|
|
font-weight: bold;
|
|
color: #1f2937;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.tags-list {
|
|
display: flex;
|
|
flex-direction: row;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.tag-item {
|
|
margin-right: 8px;
|
|
margin-bottom: 8px;
|
|
padding: 6px 12px;
|
|
background-color: #eff6ff;
|
|
border-radius: 16px;
|
|
}
|
|
|
|
.tag-text {
|
|
font-size: 14px;
|
|
color: #3b82f6;
|
|
}
|
|
|
|
.content-stats {
|
|
margin-bottom: 24px;
|
|
padding: 16px;
|
|
background-color: #f9fafb;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.stats-row {
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: space-around;
|
|
align-items: center;
|
|
}
|
|
|
|
.stat-item {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
padding: 8px 16px;
|
|
}
|
|
|
|
.stat-icon {
|
|
font-size: 20px;
|
|
margin-right: 8px;
|
|
}
|
|
|
|
.stat-text {
|
|
font-size: 16px;
|
|
color: #374151;
|
|
}
|
|
|
|
.content-source {
|
|
padding: 16px;
|
|
background-color: #f9fafb;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.source-title {
|
|
font-size: 14px;
|
|
color: #6b7280;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.source-link {
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.link-text {
|
|
font-size: 14px;
|
|
color: #3b82f6;
|
|
flex: 1;
|
|
}
|
|
|
|
.link-icon {
|
|
font-size: 16px;
|
|
color: #3b82f6;
|
|
}
|
|
|
|
.related-section {
|
|
background-color: #ffffff;
|
|
margin: 12px;
|
|
border-radius: 12px;
|
|
}
|
|
|
|
.section-header {
|
|
padding: 16px;
|
|
border-bottom: 1px solid #e5e5e5;
|
|
}
|
|
|
|
.section-title {
|
|
font-size: 18px;
|
|
font-weight: bold;
|
|
color: #1f2937;
|
|
}
|
|
|
|
.related-list {
|
|
padding: 0 16px;
|
|
}
|
|
|
|
.related-item {
|
|
padding: 16px 0;
|
|
border-bottom: 1px solid #f3f4f6;
|
|
}
|
|
|
|
.related-content {
|
|
flex: 1;
|
|
}
|
|
|
|
.related-title {
|
|
font-size: 16px;
|
|
font-weight: bold;
|
|
color: #1f2937;
|
|
line-height: 22px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.related-summary {
|
|
font-size: 14px;
|
|
color: #6b7280;
|
|
line-height: 20px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.related-meta {
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.related-time {
|
|
font-size: 12px;
|
|
color: #9ca3af;
|
|
}
|
|
|
|
.related-quality {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.comments-section {
|
|
background-color: #ffffff;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.ai-assistant-btn {
|
|
position: fixed;
|
|
bottom: 20px;
|
|
right: 20px;
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
padding: 12px 16px;
|
|
background-color: #3b82f6;
|
|
border-radius: 24px;
|
|
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
|
|
}
|
|
|
|
.ai-icon {
|
|
font-size: 20px;
|
|
margin-right: 8px;
|
|
}
|
|
|
|
.ai-text {
|
|
font-size: 14px;
|
|
color: #ffffff;
|
|
}
|
|
|
|
/* 语言切换弹窗样式 */
|
|
.language-modal {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
z-index: 9999;
|
|
}
|
|
|
|
.language-modal-content {
|
|
background-color: #ffffff;
|
|
border-radius: 12px;
|
|
width: 280px;
|
|
max-width: 90%;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.modal-header {
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 16px 20px;
|
|
border-bottom: 1px solid #e5e5e5;
|
|
}
|
|
|
|
.modal-title {
|
|
font-size: 18px;
|
|
font-weight: bold;
|
|
color: #1f2937;
|
|
}
|
|
|
|
.modal-close {
|
|
width: 32px;
|
|
height: 32px;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
border-radius: 16px;
|
|
background-color: #f3f4f6;
|
|
}
|
|
|
|
.close-icon {
|
|
font-size: 14px;
|
|
color: #6b7280;
|
|
}
|
|
|
|
.language-options {
|
|
padding: 8px 0;
|
|
}
|
|
|
|
.language-option {
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 12px 20px;
|
|
transition: background-color 0.2s;
|
|
}
|
|
|
|
.language-option:hover {
|
|
background-color: #f9fafb;
|
|
}
|
|
|
|
.language-option.active {
|
|
background-color: #eff6ff;
|
|
}
|
|
|
|
.language-name {
|
|
font-size: 16px;
|
|
color: #1f2937;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.language-code {
|
|
font-size: 14px;
|
|
color: #6b7280;
|
|
}
|
|
|
|
.selected-mark {
|
|
font-size: 16px;
|
|
color: #3b82f6;
|
|
font-weight: bold;
|
|
}
|
|
</style> |