935 lines
23 KiB
Plaintext
935 lines
23 KiB
Plaintext
<!-- 多语言AI资讯系统主页 - 严格遵循UTS Android开发规范 -->
|
||
<template>
|
||
<scroll-view direction="vertical" class="info-home" :enable-back-to-top="true">
|
||
<view class="header">
|
||
<view class="header-content">
|
||
<view class="header-actions">
|
||
<view class="action-btn" @click="showLanguageSelector">
|
||
<text class="action-text">{{ currentLanguageName }}{{$t('mt.category.politics')}}</text>
|
||
</view>
|
||
<view class="action-btn" @click="navigateToTopics">
|
||
<text class="action-icon">📑</text>
|
||
</view>
|
||
<view class="action-btn" @click="navigateToSearch">
|
||
<text class="action-icon">🔍</text>
|
||
</view>
|
||
<view class="action-btn" @click="navigateToChat">
|
||
<text class="action-icon">💬</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 分类标签栏 -->
|
||
<view class="category-section">
|
||
<scroll-view direction="horizontal" class="category-scroll" >
|
||
<view style="white-space: nowrap;flex-direction: row;">
|
||
<view
|
||
v-for="(category, index) in categoriesList"
|
||
:key="category.id"
|
||
class="category-tab"
|
||
:class="{ 'is-active': selectedCategoryId === category.id, 'is-last': index === categoriesList.length - 1 }"
|
||
@click="selectCategory(category)"
|
||
style="display: inline-block; margin-right: 12px;">
|
||
<text class="category-text" :class="{ 'is-active': selectedCategoryId === category.id }">{{ getCategoryName(category) }}</text>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
|
||
<!-- 精选内容区域 -->
|
||
<view class="featured-section" v-if="featuredContentsList.length > 0">
|
||
<view class="section-header">
|
||
<text class="section-title">{{ $t('mt.section.featured') }}</text>
|
||
</view>
|
||
<scroll-view direction="horizontal" class="featured-scroll" :scroll-x="true">
|
||
<view class="featured-cards">
|
||
<view
|
||
v-for="(content, index) in featuredContentsList"
|
||
:key="content.id"
|
||
class="featured-card"
|
||
:style="{ width: cardWidth + 'px' }"
|
||
@click="navigateToDetail(content)">
|
||
<view class="card-header">
|
||
<text class="card-title">{{ content.title }}</text>
|
||
<view class="quality-badge" :style="{ backgroundColor: getQualityScoreColorLocal(content.quality_score) }">
|
||
<text class="quality-text">{{ getQualityScoreTextLocal(content.quality_score) }}</text>
|
||
</view>
|
||
</view>
|
||
<text class="card-summary">{{ content.summary }}</text>
|
||
<view class="card-footer">
|
||
<text class="card-author">{{ content.author }}</text>
|
||
<text class="card-time">{{ formatRelativeTimeLocal(content.published_at) }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
|
||
<!-- 内容列表区域 -->
|
||
<view class="content-section">
|
||
<view class="section-header">
|
||
<text class="section-title">{{ $t('mt.section.latest') }}</text>
|
||
<view class="section-actions">
|
||
<view class="sort-btn" @click="showSortOptions">
|
||
<text class="sort-text">{{ sortOptionText }}</text>
|
||
</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="content-list" v-if="contentsList.length > 0">
|
||
<view
|
||
v-for="(content, index) in contentsList"
|
||
:key="content.id"
|
||
class="content-item"
|
||
@click="navigateToDetail(content)">
|
||
<view class="content-header">
|
||
<text class="content-title">{{ content.title }}</text>
|
||
<view class="content-meta">
|
||
<!-- <text class="content-category">{{ getCategoryDisplayNameByIdLocal(content.category_id) }}</text> -->
|
||
<text class="content-time">{{ formatRelativeTimeLocal(content.published_at) }}</text>
|
||
</view>
|
||
</view>
|
||
<text class="content-summary">{{ content.summary }}</text>
|
||
<view class="content-footer">
|
||
<view class="content-stats">
|
||
<text class="stat-item">👁 {{ content.view_count }}</text>
|
||
<text class="stat-item">👍 {{ content.like_count }}</text>
|
||
<text class="stat-item">📤 {{ content.share_count }}</text>
|
||
</view>
|
||
<view class="quality-badge" :style="{ backgroundColor: getQualityScoreColorLocal(content.quality_score) }">
|
||
<text class="quality-text">{{ getQualityScoreTextLocal(content.quality_score) }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 空状态 -->
|
||
<view class="empty-section" v-if="contentsList.length === 0 && !pageState.loading && pageState.error === null">
|
||
<text class="empty-text">{{ $t('mt.empty.content') }}</text>
|
||
<view class="refresh-btn" @click="refreshData">
|
||
<text class="refresh-text">{{ $t('mt.action.refresh') }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 加载更多 -->
|
||
<view class="load-more-section" v-if="contentsList.length > 0 && hasMore">
|
||
<view class="load-more-btn" @click="loadMore" v-if="!loadingMore">
|
||
<text class="load-more-text">{{ $t('mt.button.loadMore') }}</text>
|
||
</view>
|
||
<view class="loading-more" v-if="loadingMore">
|
||
<text class="loading-more-text">{{ $t('mt.loadingMore') }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 语言选择弹窗 -->
|
||
<view class="modal-overlay" v-if="showLanguageModal" @click="hideLanguageSelector">
|
||
<view class="language-modal" @click.stop="">
|
||
<view class="modal-header">
|
||
<text class="modal-title">{{ $t('mt.modal.selectLanguage') }}</text>
|
||
<view class="modal-close" @click="hideLanguageSelector">
|
||
<text class="close-text">×</text>
|
||
</view>
|
||
</view>
|
||
<view class="language-list">
|
||
<view
|
||
v-for="(language, index) in languagesList"
|
||
:key="language.id"
|
||
class="language-item"
|
||
:class="{ active: currentLanguageCode === language.code }"
|
||
@click="selectLanguage(language)">
|
||
<text class="language-name">{{ getLanguageDisplayNameLocal(language.code) }}</text>
|
||
<text class="language-native">{{ language.native_name }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 排序选择弹窗 -->
|
||
<view class="modal-overlay" v-if="showSortModal" @click="hideSortOptions">
|
||
<view class="sort-modal">
|
||
<view class="modal-header">
|
||
<text class="modal-title">{{ $t('mt.modal.sort') }}</text>
|
||
<view class="modal-close" @click="hideSortOptions">
|
||
<text class="close-text">×</text>
|
||
</view>
|
||
</view>
|
||
<view class="sort-list">
|
||
<view
|
||
v-for="(option, index) in sortOptionsList"
|
||
:key="option.value"
|
||
class="sort-item"
|
||
:class="{ active: currentSortOption === option.value }"
|
||
@click="selectSortOption(option)">
|
||
<text class="sort-name">{{ option.text }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import supa from '@/components/supadb/aksupainstance.uts'
|
||
import {OrderOptions} from '@/components/supadb/aksupa.uts'
|
||
|
||
import {
|
||
OptionItem,
|
||
InfoContent,
|
||
LanguageData,
|
||
Language,
|
||
PageState,
|
||
ResponsiveState,
|
||
|
||
LANGUAGE_OPTIONS,
|
||
SORT_OPTIONS,
|
||
formatRelativeTimeKey,
|
||
getQualityScoreColor,
|
||
getQualityScoreText,
|
||
CategoryData,
|
||
CategoryTranslation
|
||
} from './types.uts'
|
||
|
||
import { tt } from '@/utils/i18nfun.uts'
|
||
|
||
|
||
|
||
// 页面状态 - 严格使用简单类型避免复杂嵌套
|
||
const pageState = ref<PageState>({
|
||
loading: false,
|
||
error: null,
|
||
currentPage: 1,
|
||
pageSize: 20,
|
||
total: 0
|
||
})
|
||
|
||
// 响应式状态
|
||
const responsiveState = ref<ResponsiveState>({
|
||
isLargeScreen: false,
|
||
isSmallScreen: true,
|
||
screenWidth: 375,
|
||
cardColumns: 1
|
||
})
|
||
|
||
// UI状态变量 - 与template交互使用1维变量
|
||
const showLanguageModal = ref<boolean>(false)
|
||
const showSortModal = ref<boolean>(false)
|
||
const loadingMore = ref<boolean>(false)
|
||
const hasMore = ref<boolean>(true)
|
||
|
||
// 当前选择状态 - 使用简单字符串类型
|
||
const selectedCategoryId = ref<string>('all')
|
||
const currentLanguageCode = ref<string>('zh-CN')
|
||
const currentLanguageName = ref<string>('简体中文')
|
||
const currentSortOption = ref<string>('published_at_desc')
|
||
const sortOptionText = ref<string>('最新发布')
|
||
|
||
// 数据列表 - 直接使用强类型 InfoContent 数组
|
||
const contentsList = ref<Array<InfoContent>>([])
|
||
const featuredContentsList = ref<Array<InfoContent>>([])
|
||
const categoriesList = ref<CategoryData[]>([])
|
||
const languagesList = ref<Array<LanguageData>>([])
|
||
|
||
// 选项列表
|
||
const sortOptionsList = ref<Array<OptionItem>>(SORT_OPTIONS)
|
||
|
||
// 计算属性
|
||
const cardWidth = computed((): number => {
|
||
return responsiveState.value.isLargeScreen ? 300 : 280
|
||
})
|
||
|
||
const contentFilter = computed((): string => {
|
||
let filter = "status=eq.published"
|
||
if (selectedCategoryId.value !== '') {
|
||
filter += `&category_id=eq.${selectedCategoryId.value}`
|
||
}
|
||
// 根据排序选项构建order参数
|
||
const sortParts = currentSortOption.value.split('_')
|
||
const column = sortParts.slice(0, -1).join('_')
|
||
const direction = sortParts[sortParts.length - 1] === 'desc' ? 'desc' : 'asc'
|
||
filter += `&order=${column}.${direction}`
|
||
return filter
|
||
})
|
||
|
||
// Supabase组件引用 - 移除,使用直接调用方式
|
||
// const contentsRef = ref<SupadbComponentPublicInstance | null>(null)
|
||
|
||
|
||
// 响应式处理
|
||
const handleResize = () => {
|
||
const systemInfo = uni.getSystemInfoSync()
|
||
const screenWidth = systemInfo.screenWidth
|
||
responsiveState.value.screenWidth = screenWidth
|
||
responsiveState.value.isLargeScreen = screenWidth >= 768
|
||
responsiveState.value.isSmallScreen = screenWidth < 768
|
||
responsiveState.value.cardColumns = screenWidth >= 768 ? 2 : 1
|
||
}
|
||
|
||
|
||
// 错误处理
|
||
const handleError = (error: any) => {
|
||
pageState.value.loading = false
|
||
pageState.value.error = '加载失败,请稍后重试'
|
||
console.error('Contents loading error:', error)
|
||
}
|
||
|
||
// 处理内容数据
|
||
const handleContentsData = (data: Array<InfoContent>) => {
|
||
pageState.value.loading = false
|
||
pageState.value.error = null
|
||
|
||
if (pageState.value.currentPage === 1) {
|
||
contentsList.value = []
|
||
}
|
||
|
||
// 直接赋值强类型数组
|
||
contentsList.value = contentsList.value.concat(data)
|
||
|
||
// 检查是否还有更多数据
|
||
hasMore.value = data.length === pageState.value.pageSize
|
||
}
|
||
|
||
// 本地工具函数 - 只保留必要的辅助函数,内容相关 getter 可全部移除,模板直接用 content.xxx
|
||
// 保留分类/语言/格式化等工具
|
||
const formatRelativeTimeLocal = (dateString: string): string => {
|
||
const key = formatRelativeTimeKey(dateString)
|
||
const now = new Date()
|
||
const date = new Date(dateString)
|
||
const diff = now.getTime() - date.getTime()
|
||
const seconds = Math.floor(diff / 1000)
|
||
const minutes = Math.floor(seconds / 60)
|
||
const hours = Math.floor(minutes / 60)
|
||
const days = Math.floor(hours / 24)
|
||
if (key === 'mt.time.daysAgo') return tt(key, { days })
|
||
if (key === 'mt.time.hoursAgo') return tt(key, { hours })
|
||
if (key === 'mt.time.minutesAgo') return tt(key, { minutes })
|
||
return tt(key)
|
||
}
|
||
|
||
const getLanguageDisplayNameLocal = (languageCode: string): string => {
|
||
return tt(languageCode)
|
||
}
|
||
const getQualityScoreTextLocal = (score: number): string => {
|
||
// console.log(score)
|
||
// return score.toString()
|
||
return getQualityScoreText(score*100)
|
||
}
|
||
const getQualityScoreColorLocal = (score: number): string => {
|
||
return getQualityScoreColor(score*100)
|
||
}
|
||
|
||
|
||
// 语言选择
|
||
const showLanguageSelector = () => {
|
||
showLanguageModal.value = true
|
||
}
|
||
|
||
const hideLanguageSelector = () => {
|
||
showLanguageModal.value = false
|
||
}
|
||
|
||
const selectLanguage = (language: LanguageData) => {
|
||
currentLanguageCode.value = language.code
|
||
currentLanguageName.value = getLanguageDisplayNameLocal(language.code)
|
||
hideLanguageSelector()
|
||
}
|
||
|
||
// 排序选择
|
||
const showSortOptions = () => {
|
||
showSortModal.value = true
|
||
}
|
||
|
||
const hideSortOptions = () => {
|
||
showSortModal.value = false
|
||
}
|
||
|
||
|
||
// 排序选项类型定义,保证类型安全
|
||
|
||
// 排序选项转换,保证 v-for 传递类型安全
|
||
// const sortOptionsListTyped: Array<OptionItem> = SORT_OPTIONS.map(opt => ({
|
||
// value: opt.value,
|
||
// text: opt.text
|
||
// }))
|
||
|
||
// 加载内容数据 - 使用 executeAs 替代 supadb 组件
|
||
const loadContents = async () => {
|
||
if (supa === null) return
|
||
pageState.value.loading = true
|
||
pageState.value.error = null
|
||
try {
|
||
let query = supa.from('ak_contents')
|
||
.select('*,ak_contents_category_id_fkey(name_key)', {})
|
||
.order('published_at', { ascending: false } as OrderOptions)
|
||
// 应用分类筛选
|
||
if (selectedCategoryId.value !== 'all') {
|
||
query = query.eq('category_id', selectedCategoryId.value)
|
||
}
|
||
// 应用分页
|
||
const start = (pageState.value.currentPage - 1) * pageState.value.pageSize
|
||
const end = start + pageState.value.pageSize - 1
|
||
query = query.range(start, end)
|
||
const result = await query.executeAs<InfoContent>()
|
||
if (result.data !== null && Array.isArray(result.data)) {
|
||
handleContentsData(result.data as Array<InfoContent>)
|
||
} else {
|
||
throw new Error('Failed to load contents')
|
||
}
|
||
} catch (e) {
|
||
handleError(e)
|
||
}
|
||
}
|
||
const selectSortOption = async (option: OptionItem) => {
|
||
currentSortOption.value = option.value
|
||
sortOptionText.value = option.text
|
||
hideSortOptions()
|
||
pageState.value.currentPage = 1
|
||
await loadContents()
|
||
}
|
||
// 分类选择
|
||
const selectCategory = async (category: CategoryData) => {
|
||
selectedCategoryId.value = category.id
|
||
pageState.value.currentPage = 1
|
||
await loadContents()
|
||
}
|
||
|
||
const getCategoryName = (category: CategoryData):string => {
|
||
const translations = category.translations;
|
||
const name = translations?.[0]?.name??'--';
|
||
return name;
|
||
}
|
||
|
||
// 加载精选内容 - 使用 executeAs 替代模拟数据
|
||
const loadFeaturedContents = async () => {
|
||
if (supa === null) return
|
||
try {
|
||
const result = await supa.from('ak_contents')
|
||
.select('*', {})
|
||
.eq('is_featured', true)
|
||
.order('published_at', { ascending: false })
|
||
.limit(5)
|
||
.executeAs<Array<InfoContent>>()
|
||
if (result.data !== null && Array.isArray(result.data)) {
|
||
featuredContentsList.value = result.data as Array<InfoContent>
|
||
}
|
||
} catch (e) {
|
||
console.error('Featured contents loading error:', e)
|
||
featuredContentsList.value = []
|
||
}
|
||
}
|
||
|
||
// 加载更多
|
||
const loadMore = async () => {
|
||
if (loadingMore.value || !hasMore.value) return
|
||
loadingMore.value = true
|
||
pageState.value.currentPage += 1
|
||
setTimeout(() => {
|
||
loadContents().then(() => {
|
||
loadingMore.value = false
|
||
})
|
||
}, 500)
|
||
}
|
||
|
||
// 刷新数据
|
||
const refreshData = async () => {
|
||
pageState.value.currentPage = 1
|
||
await loadContents()
|
||
}
|
||
|
||
// 重试加载
|
||
const retryLoad = async () => {
|
||
pageState.value.error = null
|
||
await loadContents()
|
||
}
|
||
|
||
// 导航函数
|
||
const navigateToDetail = (content: InfoContent) => {
|
||
const contentId = content.id
|
||
uni.navigateTo({
|
||
url: `/pages/info/detail?id=${contentId}`
|
||
})
|
||
}
|
||
|
||
const navigateToSearch = () => {
|
||
uni.navigateTo({
|
||
url: '/pages/info/search'
|
||
})
|
||
}
|
||
|
||
const navigateToChat = () => {
|
||
uni.navigateTo({
|
||
url: '/pages/info/chat'
|
||
})
|
||
}
|
||
|
||
const navigateToTopics = () => {
|
||
uni.navigateTo({
|
||
url: '/pages/info/topics'
|
||
})
|
||
}
|
||
|
||
// 从数据库动态加载分类
|
||
const loadCategories = async () => {
|
||
if (supa === null) return
|
||
try {
|
||
const lang = currentLanguageCode.value
|
||
console.log(lang)
|
||
const result = await supa
|
||
.from('ak_content_categories')
|
||
.select('*,translations:ak_content_category_translations(name)', {})
|
||
.eq('translations.language_code', lang)
|
||
.order('sort_order', { ascending: true })
|
||
.executeAs<CategoryData>()
|
||
console.log(result)
|
||
if (result.data !== null && Array.isArray(result.data)) {
|
||
categoriesList.value = result.data as CategoryData[]
|
||
} else {
|
||
categoriesList.value = []
|
||
}
|
||
} catch (e) {
|
||
console.error('加载分类失败:', e)
|
||
categoriesList.value = []
|
||
}
|
||
}
|
||
|
||
// 初始化数据
|
||
const initializeData = async () => {
|
||
// 动态加载分类列表
|
||
await loadCategories()
|
||
|
||
// 初始化语言列表(强类型 LanguageData)
|
||
languagesList.value = []
|
||
for (let i: Int = 0; i < LANGUAGE_OPTIONS.length; i++) {
|
||
const language: LanguageData = LANGUAGE_OPTIONS[i]
|
||
languagesList.value.push(language)
|
||
}
|
||
|
||
// await loadContents()
|
||
// await loadFeaturedContents()
|
||
}
|
||
|
||
// 生命周期
|
||
onMounted(() => {
|
||
initializeData()
|
||
handleResize()
|
||
})
|
||
</script>
|
||
|
||
<style>
|
||
/* uts-android 兼容性重构:
|
||
1. 拆分所有嵌套选择器为扁平 class(如 .category-tab.is-active、.category-text.is-active)。
|
||
2. 所有“最后一个”元素的 margin-right/margin-bottom 用 .is-last 控制,移除伪类。
|
||
3. 移除所有 gap、flex-wrap、嵌套选择器、伪类等不兼容写法。
|
||
4. 在注释中补充重构说明,便于后续维护。
|
||
*/
|
||
.info-home {
|
||
flex: 1;
|
||
background-color: #f8fafc;
|
||
}
|
||
.header {
|
||
background-color: #ffffff;
|
||
border-bottom-width: 1px;
|
||
border-bottom-color: #e2e8f0;
|
||
padding-top: 10px;
|
||
padding-bottom: 10px;
|
||
}
|
||
.header-content {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding-left: 16px;
|
||
padding-right: 16px;
|
||
}
|
||
.title {
|
||
font-size: 20px;
|
||
font-weight: bold;
|
||
color: #1f2937;
|
||
}
|
||
.header-actions {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
.action-btn {
|
||
padding: 8px 12px;
|
||
margin-left: 8px;
|
||
background-color: #f1f5f9;
|
||
border-radius: 20px;
|
||
}
|
||
.action-text {
|
||
font-size: 14px;
|
||
color: #475569;
|
||
}
|
||
.action-icon {
|
||
font-size: 16px;
|
||
}
|
||
.category-section {
|
||
background-color: #ffffff;
|
||
padding-top: 12px;
|
||
padding-bottom: 12px;
|
||
border-bottom-width: 1px;
|
||
border-bottom-color: #e2e8f0;
|
||
}
|
||
/* 横向滚动兼容 UTS Android */
|
||
.category-scroll {
|
||
height: 40px;
|
||
display: flex;
|
||
flex-direction: row;
|
||
padding-left: 16px;
|
||
padding-right: 16px;
|
||
min-width: max-content;
|
||
}
|
||
.category-tab {
|
||
flex: 0 0 auto;
|
||
padding: 8px 16px;
|
||
margin-right: 12px;
|
||
background-color: #f8fafc;
|
||
border-radius: 20px;
|
||
border-width: 1px;
|
||
border-color: #e2e8f0;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.category-tab.is-active {
|
||
background-color: #3b82f6;
|
||
border-color: #3b82f6;
|
||
}
|
||
.category-tab.is-last {
|
||
margin-right: 0;
|
||
}
|
||
.category-text {
|
||
font-size: 14px;
|
||
color: #64748b;
|
||
white-space: nowrap;
|
||
}
|
||
.category-text.is-active {
|
||
color: #ffffff;
|
||
}
|
||
.featured-section {
|
||
background-color: #ffffff;
|
||
margin-top: 8px;
|
||
padding: 16px 0;
|
||
}
|
||
.section-header {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding-left: 16px;
|
||
padding-right: 16px;
|
||
margin-bottom: 12px;
|
||
}
|
||
.section-title {
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
color: #1f2937;
|
||
}
|
||
.section-actions {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
.sort-btn {
|
||
padding: 6px 12px;
|
||
background-color: #f1f5f9;
|
||
border-radius: 16px;
|
||
}
|
||
.sort-text {
|
||
font-size: 12px;
|
||
color: #475569;
|
||
}
|
||
.featured-scroll {
|
||
height: 180px;
|
||
}
|
||
.featured-cards {
|
||
display: flex;
|
||
flex-direction: row;
|
||
padding-left: 16px;
|
||
padding-right: 16px;
|
||
}
|
||
.featured-card {
|
||
background-color: #f8fafc;
|
||
border-radius: 12px;
|
||
padding: 16px;
|
||
margin-right: 12px;
|
||
border-width: 1px;
|
||
border-color: #e2e8f0;
|
||
}
|
||
.featured-card.is-last {
|
||
margin-right: 0;
|
||
}
|
||
.card-header {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
margin-bottom: 8px;
|
||
}
|
||
.card-title {
|
||
flex: 1;
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
color: #1f2937;
|
||
line-height: 22px;
|
||
}
|
||
.quality-badge {
|
||
background-color: #10b981;
|
||
border-radius: 12px;
|
||
padding: 4px 8px;
|
||
margin-left: 8px;
|
||
}
|
||
.quality-text {
|
||
font-size: 12px;
|
||
color: #ffffff;
|
||
}
|
||
.card-summary {
|
||
font-size: 14px;
|
||
color: #64748b;
|
||
line-height: 20px;
|
||
margin-bottom: 12px;
|
||
}
|
||
.card-footer {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
.card-author {
|
||
font-size: 12px;
|
||
color: #94a3b8;
|
||
}
|
||
.card-time {
|
||
font-size: 12px;
|
||
color: #94a3b8;
|
||
}
|
||
.content-section {
|
||
background-color: #ffffff;
|
||
margin-top: 8px;
|
||
padding: 16px 0;
|
||
}
|
||
.loading-section {
|
||
padding: 40px 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.loading-text {
|
||
font-size: 14px;
|
||
color: #94a3b8;
|
||
}
|
||
.error-section {
|
||
padding: 40px 16px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.error-text {
|
||
font-size: 14px;
|
||
color: #ef4444;
|
||
margin-bottom: 16px;
|
||
text-align: center;
|
||
}
|
||
.retry-btn {
|
||
background-color: #3b82f6;
|
||
border-radius: 20px;
|
||
padding: 8px 24px;
|
||
}
|
||
.retry-text {
|
||
font-size: 14px;
|
||
color: #ffffff;
|
||
}
|
||
.content-list {
|
||
padding-left: 16px;
|
||
padding-right: 16px;
|
||
}
|
||
.content-item {
|
||
background-color: #f8fafc;
|
||
border-radius: 12px;
|
||
padding: 16px;
|
||
margin-bottom: 12px;
|
||
border-width: 1px;
|
||
border-color: #e2e8f0;
|
||
}
|
||
.content-header {
|
||
margin-bottom: 8px;
|
||
}
|
||
.content-title {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
color: #1f2937;
|
||
line-height: 22px;
|
||
margin-bottom: 4px;
|
||
}
|
||
.content-meta {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
.content-category {
|
||
font-size: 12px;
|
||
color: #3b82f6;
|
||
background-color: #eff6ff;
|
||
padding: 2px 8px;
|
||
border-radius: 10px;
|
||
margin-right: 8px;
|
||
}
|
||
.content-time {
|
||
font-size: 12px;
|
||
color: #94a3b8;
|
||
}
|
||
.content-summary {
|
||
font-size: 14px;
|
||
color: #64748b;
|
||
line-height: 20px;
|
||
margin-bottom: 12px;
|
||
}
|
||
.content-footer {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
.content-stats {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
.stat-item {
|
||
font-size: 12px;
|
||
color: #94a3b8;
|
||
margin-right: 16px;
|
||
}
|
||
.stat-item.is-last {
|
||
margin-right: 0;
|
||
}
|
||
.empty-section {
|
||
padding: 60px 16px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.empty-text {
|
||
font-size: 16px;
|
||
color: #94a3b8;
|
||
margin-bottom: 16px;
|
||
}
|
||
.refresh-btn {
|
||
background-color: #3b82f6;
|
||
border-radius: 20px;
|
||
padding: 10px 24px;
|
||
}
|
||
.refresh-text {
|
||
font-size: 14px;
|
||
color: #ffffff;
|
||
}
|
||
.load-more-section {
|
||
padding: 20px 16px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.load-more-btn {
|
||
background-color: #f1f5f9;
|
||
border-radius: 20px;
|
||
padding: 10px 24px;
|
||
border-width: 1px;
|
||
border-color: #e2e8f0;
|
||
}
|
||
.load-more-text {
|
||
font-size: 14px;
|
||
color: #475569;
|
||
}
|
||
.loading-more {
|
||
padding: 10px 24px;
|
||
}
|
||
.loading-more-text {
|
||
font-size: 14px;
|
||
color: #94a3b8;
|
||
}
|
||
.modal-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
}
|
||
.language-modal, .sort-modal {
|
||
background-color: #ffffff;
|
||
border-radius: 16px;
|
||
margin: 20px;
|
||
max-height: 500px;
|
||
min-width: 280px;
|
||
}
|
||
.modal-header {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 16px 20px;
|
||
border-bottom-width: 1px;
|
||
border-bottom-color: #e2e8f0;
|
||
}
|
||
.modal-title {
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
color: #1f2937;
|
||
}
|
||
.modal-close {
|
||
width: 32px;
|
||
height: 32px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 16px;
|
||
background-color: #f1f5f9;
|
||
}
|
||
.close-text {
|
||
font-size: 20px;
|
||
color: #64748b;
|
||
}
|
||
.language-list, .sort-list {
|
||
max-height: 400px;
|
||
}
|
||
.language-item, .sort-item {
|
||
padding: 16px 20px;
|
||
border-bottom-width: 1px;
|
||
border-bottom-color: #f1f5f9;
|
||
}
|
||
.language-item.is-active, .sort-item.is-active {
|
||
background-color: #eff6ff;
|
||
}
|
||
.language-name {
|
||
font-size: 16px;
|
||
color: #1f2937;
|
||
margin-bottom: 4px;
|
||
}
|
||
.language-name.is-active {
|
||
color: #3b82f6;
|
||
}
|
||
.language-native {
|
||
font-size: 14px;
|
||
color: #64748b;
|
||
}
|
||
</style>
|