0841b1a103
- 前端:Vue3 + TS + Element Plus,24 个页面路由(课件/组题/教案/动画/思维导图/作文批改/命题/课堂/资源/社区等) - 后端:FastAPI + SQLAlchemy + SQLite,17 个路由模块,AI 服务层含降级模板 - AI:glm-5.x 推理模型已禁用思维链,确保输出真实内容 - 修复:ai_service 两处请求体注入 enable_thinking/thinking disabled - 测试账号:13900999999 / test1234
494 lines
21 KiB
Vue
494 lines
21 KiB
Vue
<template>
|
||
<div class="page-container">
|
||
<div class="page-header">
|
||
<div>
|
||
<h2>模板社区</h2>
|
||
<p>交流互动课件、教学动画和课堂工具模板,沉淀可改编的教学案例。</p>
|
||
</div>
|
||
<button class="create-btn" @click="openCreateDialog">
|
||
<el-icon><EditPen /></el-icon> 发布模板
|
||
</button>
|
||
</div>
|
||
|
||
<div class="scope-tabs">
|
||
<button
|
||
v-for="scope in scopeOptions"
|
||
:key="scope.value"
|
||
type="button"
|
||
:class="{ active: activeScope === scope.value }"
|
||
@click="setScope(scope.value)"
|
||
>
|
||
{{ scope.label }}
|
||
</button>
|
||
</div>
|
||
|
||
<div class="community-tabs">
|
||
<button
|
||
v-for="tag in ['全部', ...hotTags]"
|
||
:key="tag"
|
||
type="button"
|
||
:class="{ active: activeTag === tag }"
|
||
@click="activeTag = tag"
|
||
>
|
||
{{ tag }}
|
||
</button>
|
||
</div>
|
||
|
||
<div class="community-layout">
|
||
<div class="posts-section">
|
||
<div v-loading="loading" class="post-list">
|
||
<div v-for="post in filteredPosts" :key="post.id" class="post-card" @click="openPost(post)">
|
||
<div class="post-cover" :class="postCover(post)">
|
||
<span>{{ postTypeLabel(post.post_type) }}</span>
|
||
</div>
|
||
<div class="post-body">
|
||
<div class="post-type-badge">
|
||
<el-tag :type="post.post_type === 'share' ? 'success' : post.post_type === 'help' ? 'warning' : undefined" size="small" effect="plain">
|
||
{{ postTypeLabel(post.post_type) }}
|
||
</el-tag>
|
||
</div>
|
||
<h3>{{ post.title }}</h3>
|
||
<div class="post-meta">
|
||
<span>{{ post.author?.name || '社区教师' }}</span>
|
||
<span v-if="primaryAttachment(post)">{{ resourceTypeLabel(primaryAttachment(post).resource_type) }}</span>
|
||
<span v-if="post.is_favorited">已收藏</span>
|
||
</div>
|
||
<p class="post-excerpt">{{ postExcerpt(post) }}</p>
|
||
<div class="post-tags">
|
||
<el-tag v-for="tag in post.tags" :key="tag" size="small" effect="plain" style="margin-right: 4px">{{ tag }}</el-tag>
|
||
</div>
|
||
<div class="post-stats">
|
||
<span><el-icon><View /></el-icon> {{ post.views }}</span>
|
||
<span><el-icon><Star /></el-icon> {{ post.likes }}</span>
|
||
<span><el-icon><ChatDotRound /></el-icon> {{ post.comments_count }}</span>
|
||
</div>
|
||
<div class="post-actions">
|
||
<button type="button" @click.stop="openPost(post)">查看详情</button>
|
||
<button type="button" class="primary" @click.stop="useTemplate(post)">使用模板</button>
|
||
<template v-if="isOwner(post)">
|
||
<button type="button" @click.stop="openEditDialog(post)">
|
||
<el-icon><Edit /></el-icon> 编辑
|
||
</button>
|
||
<el-popconfirm title="确定删除这个社区模板?" @confirm="handleDeletePost(post)">
|
||
<template #reference>
|
||
<button type="button" class="danger" @click.stop>
|
||
<el-icon><Delete /></el-icon> 删除
|
||
</button>
|
||
</template>
|
||
</el-popconfirm>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-if="!loading && filteredPosts.length === 0" class="empty-state">
|
||
<el-icon :size="48" color="#CBD5E1"><ChatDotRound /></el-icon>
|
||
<p>{{ emptyText }}</p>
|
||
<button type="button" @click="openCreateDialog">发布第一个模板</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sidebar-section">
|
||
<div class="hot-tags-card">
|
||
<h4>热门标签</h4>
|
||
<div class="hot-tags">
|
||
<span v-for="tag in hotTags" :key="tag" class="hot-tag" @click="activeTag = tag">{{ tag }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="hot-tags-card">
|
||
<h4>推荐模板</h4>
|
||
<div v-if="rankedPosts.length" class="template-rank">
|
||
<button v-for="(item, index) in rankedPosts" :key="item.id" type="button" @click="openPost(item)">
|
||
<span :class="rankTheme(index)">{{ index + 1 }}</span>
|
||
<div>
|
||
<strong>{{ item.title }}</strong>
|
||
<small>{{ primaryTag(item) }} · {{ formatCount(item.views) }}浏览</small>
|
||
</div>
|
||
</button>
|
||
</div>
|
||
<div v-else class="rank-empty">暂无推荐模板</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<el-dialog v-model="showCreate" :title="editingPost ? '编辑模板' : '发布模板'" width="600px" :close-on-click-modal="false">
|
||
<el-form :model="postForm" label-position="top">
|
||
<el-form-item label="标题">
|
||
<el-input v-model="postForm.title" placeholder="模板标题" />
|
||
</el-form-item>
|
||
<el-form-item label="类型">
|
||
<div class="type-select">
|
||
<label v-for="t in postTypes" :key="t.value" class="type-option" :class="{ active: postForm.post_type === t.value }" @click="postForm.post_type = t.value">
|
||
{{ t.label }}
|
||
</label>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="内容">
|
||
<el-input v-model="postForm.content" type="textarea" :rows="8" placeholder="描述模板适用的学科、年级、教学场景和使用方法..." />
|
||
</el-form-item>
|
||
<el-form-item label="标签">
|
||
<el-select v-model="postForm.tags" multiple filterable allow-create placeholder="添加标签" style="width: 100%">
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="关联资源">
|
||
<el-select v-model="postForm.resource_id" clearable filterable placeholder="选择一个已发布资源作为模板来源" style="width: 100%" @visible-change="loadMyResourcesIfNeeded">
|
||
<el-option
|
||
v-for="item in myResources"
|
||
:key="item.id"
|
||
:label="`${item.title} · ${resourceTypeLabel(item.resource_type)}`"
|
||
:value="item.id"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="showCreate = false">取消</el-button>
|
||
<el-button type="primary" @click="handleSubmitPost">{{ editingPost ? '保存修改' : '发布' }}</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, ref, reactive, onMounted, watch } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { ElMessage } from 'element-plus'
|
||
import { getPosts, getPostRanking, createPost, updatePost, deletePost } from '@/api/community'
|
||
import { getMyResources } from '@/api/resource'
|
||
import { useUserStore } from '@/stores/user'
|
||
|
||
const router = useRouter()
|
||
const route = useRoute()
|
||
const userStore = useUserStore()
|
||
const loading = ref(false)
|
||
const posts = ref<any[]>([])
|
||
const rankedPosts = ref<any[]>([])
|
||
const hotTags = ['互动课件', '教学动画', '课堂游戏', 'AI工具', '公开课', '单元复习', '实验模拟']
|
||
const postTypes = [{ value: 'discussion', label: '案例' }, { value: 'share', label: '模板' }, { value: 'help', label: '需求' }]
|
||
const scopeOptions = [
|
||
{ value: 'all', label: '全部模板' },
|
||
{ value: 'mine', label: '我的发布' },
|
||
{ value: 'favorite', label: '我的收藏' },
|
||
]
|
||
const resourceTypes = [
|
||
{ value: 'courseware', label: '课件' },
|
||
{ value: 'animation', label: '动画' },
|
||
{ value: 'exercise', label: '习题' },
|
||
{ value: 'lesson_plan', label: '教案' },
|
||
{ value: 'exam', label: '试卷' },
|
||
{ value: 'other', label: '资源' },
|
||
]
|
||
const activeTag = ref('全部')
|
||
const activeScope = ref('all')
|
||
const showCreate = ref(false)
|
||
const editingPost = ref<any>(null)
|
||
const myResources = ref<any[]>([])
|
||
const loadingResources = ref(false)
|
||
const postForm = reactive({ title: '', content: '', post_type: 'discussion', tags: [] as string[], resource_id: null as number | null })
|
||
const isLoggedIn = computed(() => !!localStorage.getItem('access_token'))
|
||
const currentUserId = computed(() => userStore.user?.id)
|
||
const emptyText = computed(() => {
|
||
if (activeScope.value === 'mine') return '还没有发布模板'
|
||
if (activeScope.value === 'favorite') return '还没有收藏模板'
|
||
return '暂无模板内容'
|
||
})
|
||
|
||
const filteredPosts = computed(() => {
|
||
if (activeTag.value === '全部') return posts.value
|
||
return posts.value.filter(post => post.tags?.includes(activeTag.value))
|
||
})
|
||
|
||
function postTypeLabel(type: string) {
|
||
return postTypes.find(item => item.value === type)?.label || '讨论'
|
||
}
|
||
|
||
function resourceTypeLabel(type: string) {
|
||
return resourceTypes.find(item => item.value === type)?.label || '资源'
|
||
}
|
||
|
||
function primaryTag(post: any) {
|
||
return post.tags?.[0] || postTypeLabel(post.post_type)
|
||
}
|
||
|
||
function postExcerpt(post: any) {
|
||
const content = String(post.content || '')
|
||
return content.length > 150 ? `${content.slice(0, 150)}...` : content
|
||
}
|
||
|
||
function rankTheme(index: number) {
|
||
return ['rank-cream', 'rank-yellow', 'rank-mint'][index % 3]
|
||
}
|
||
|
||
function formatCount(value: any) {
|
||
const n = Number(value || 0)
|
||
if (n >= 10000) return `${(n / 10000).toFixed(1)}万`
|
||
return String(n)
|
||
}
|
||
|
||
function postCover(post: any) {
|
||
if (post.post_type === 'share') return 'cover-green'
|
||
if (post.post_type === 'help') return 'cover-yellow'
|
||
return 'cover-blue'
|
||
}
|
||
|
||
function targetPathForPost(post: any) {
|
||
const attachment = primaryAttachment(post)
|
||
if (attachment?.content_ref) return pathFromContentRef(attachment.content_ref)
|
||
const tags = post.tags || []
|
||
if (tags.includes('教学动画') || tags.includes('实验模拟')) return '/animation'
|
||
if (tags.includes('课堂游戏')) return '/exercise'
|
||
if (tags.includes('AI工具')) return '/classroom'
|
||
if (tags.includes('单元复习')) return '/lesson-plan'
|
||
return '/courseware/create'
|
||
}
|
||
|
||
function useTemplate(post: any) {
|
||
const attachment = primaryAttachment(post)
|
||
if (attachment?.resource_id) {
|
||
router.push(`/resources/${attachment.resource_id}`)
|
||
return
|
||
}
|
||
const prompt = `参考社区模板《${post.title}》进行原创改编。模板说明:${post.content || ''}`
|
||
localStorage.setItem('quick_create_prompt', prompt)
|
||
router.push(targetPathForPost(post))
|
||
}
|
||
|
||
function primaryAttachment(post: any) {
|
||
return Array.isArray(post.attachments) ? post.attachments[0] : null
|
||
}
|
||
|
||
function pathFromContentRef(ref: string) {
|
||
const [type, id] = String(ref).split(':')
|
||
if (type === 'courseware') return `/courseware/${id}`
|
||
if (type === 'animation') return `/animation?open=${id}`
|
||
if (type === 'exercise') return `/exercise?open=${id}`
|
||
if (type === 'lesson_plan') return `/lesson-plan?open=${id}`
|
||
if (type === 'exam') return `/exam?open=${id}`
|
||
return '/courseware/create'
|
||
}
|
||
|
||
async function loadData() {
|
||
loading.value = true
|
||
try {
|
||
const params = activeScope.value === 'all' ? undefined : { scope: activeScope.value }
|
||
const [res, ranking] = await Promise.all([
|
||
getPosts(params),
|
||
getPostRanking({ limit: 5 }),
|
||
]) as any[]
|
||
posts.value = Array.isArray(res) ? res : []
|
||
rankedPosts.value = Array.isArray(ranking) ? ranking : []
|
||
} catch {
|
||
posts.value = []
|
||
rankedPosts.value = []
|
||
}
|
||
finally { loading.value = false }
|
||
}
|
||
|
||
function syncScopeFromRoute() {
|
||
const scope = route.query.scope
|
||
const nextScope = scopeOptions.some(item => item.value === scope) ? String(scope) : 'all'
|
||
if (nextScope !== 'all' && !ensureLoggedIn(`/community?scope=${nextScope}`)) {
|
||
activeScope.value = 'all'
|
||
return false
|
||
}
|
||
activeScope.value = nextScope
|
||
activeTag.value = '全部'
|
||
return true
|
||
}
|
||
|
||
function resetPostForm() {
|
||
postForm.title = ''
|
||
postForm.content = ''
|
||
postForm.post_type = 'discussion'
|
||
postForm.tags = []
|
||
postForm.resource_id = null
|
||
editingPost.value = null
|
||
}
|
||
|
||
function openCreateDialog() {
|
||
if (!ensureLoggedIn('/community')) return
|
||
resetPostForm()
|
||
showCreate.value = true
|
||
}
|
||
|
||
function openEditDialog(post: any) {
|
||
if (!ensureLoggedIn('/community')) return
|
||
editingPost.value = post
|
||
postForm.title = post.title || ''
|
||
postForm.content = post.content || ''
|
||
postForm.post_type = post.post_type || 'discussion'
|
||
postForm.tags = Array.isArray(post.tags) ? [...post.tags] : []
|
||
postForm.resource_id = primaryAttachment(post)?.resource_id || null
|
||
showCreate.value = true
|
||
}
|
||
|
||
async function handleSubmitPost() {
|
||
if (!ensureLoggedIn('/community')) return
|
||
if (!postForm.title.trim() || !postForm.content.trim()) {
|
||
ElMessage.warning('请填写标题和内容')
|
||
return
|
||
}
|
||
const selectedResource = myResources.value.find(item => item.id === postForm.resource_id)
|
||
const payload = {
|
||
title: postForm.title,
|
||
content: postForm.content,
|
||
post_type: postForm.post_type,
|
||
tags: postForm.tags,
|
||
attachments: selectedResource ? [{
|
||
resource_id: selectedResource.id,
|
||
resource_type: selectedResource.resource_type,
|
||
content_ref: selectedResource.content_ref,
|
||
title: selectedResource.title,
|
||
}] : [],
|
||
}
|
||
if (editingPost.value) {
|
||
await updatePost(editingPost.value.id, payload)
|
||
ElMessage.success('已保存修改')
|
||
} else {
|
||
await createPost(payload)
|
||
ElMessage.success('发布成功')
|
||
}
|
||
showCreate.value = false
|
||
resetPostForm()
|
||
loadData()
|
||
}
|
||
|
||
async function openPost(post: any) {
|
||
router.push(`/community/${post.id}`)
|
||
}
|
||
|
||
async function handleDeletePost(post: any) {
|
||
if (!ensureLoggedIn('/community')) return
|
||
await deletePost(post.id)
|
||
ElMessage.success('已删除模板')
|
||
loadData()
|
||
}
|
||
|
||
async function setScope(scope: string) {
|
||
if (scope !== 'all' && !ensureLoggedIn('/community')) return
|
||
activeScope.value = scope
|
||
activeTag.value = '全部'
|
||
const nextScope = scope === 'all' ? undefined : scope
|
||
if (route.query.scope !== nextScope) {
|
||
await router.replace({ path: '/community', query: scope === 'all' ? {} : { scope } })
|
||
return
|
||
}
|
||
await loadData()
|
||
}
|
||
|
||
function isOwner(post: any) {
|
||
return currentUserId.value && post.user_id === currentUserId.value
|
||
}
|
||
|
||
async function loadMyResourcesIfNeeded(open: boolean) {
|
||
if (!open || myResources.value.length || loadingResources.value) return
|
||
if (!ensureLoggedIn('/community')) return
|
||
loadingResources.value = true
|
||
try {
|
||
const data: any = await getMyResources({ limit: 100 })
|
||
myResources.value = Array.isArray(data?.data) ? data.data : Array.isArray(data) ? data : []
|
||
} finally {
|
||
loadingResources.value = false
|
||
}
|
||
}
|
||
|
||
function ensureLoggedIn(redirect: string) {
|
||
if (localStorage.getItem('access_token')) return true
|
||
ElMessage.warning('请先登录后再参与社区互动')
|
||
router.push({ path: '/login', query: { redirect } })
|
||
return false
|
||
}
|
||
|
||
onMounted(async () => {
|
||
if (localStorage.getItem('access_token') && !userStore.user) {
|
||
await userStore.fetchProfile().catch(() => undefined)
|
||
}
|
||
if (syncScopeFromRoute()) loadData()
|
||
})
|
||
|
||
watch(() => route.query.scope, () => {
|
||
if (syncScopeFromRoute()) loadData()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.page-header p { margin-top: 6px; color: var(--text-muted); font-size: 14px; }
|
||
.create-btn { display: flex; align-items: center; gap: 6px; background: var(--gradient); border: none; color: #fff; padding: 10px 20px; border-radius: var(--radius-sm); font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.3s; }
|
||
.create-btn:hover { opacity: 0.9; }
|
||
|
||
.scope-tabs,
|
||
.community-tabs { display: flex; align-items: center; gap: 10px; overflow-x: auto; scrollbar-width: none; }
|
||
.scope-tabs { margin-bottom: 10px; }
|
||
.community-tabs { margin-bottom: 18px; }
|
||
.scope-tabs::-webkit-scrollbar,
|
||
.community-tabs::-webkit-scrollbar { display: none; }
|
||
.scope-tabs button,
|
||
.community-tabs button { height: 34px; border: 1px solid #dbe8de; border-radius: 17px; background: #fff; color: #51665f; padding: 0 14px; white-space: nowrap; cursor: pointer; }
|
||
.scope-tabs button.active,
|
||
.scope-tabs button:hover,
|
||
.community-tabs button.active,
|
||
.community-tabs button:hover { border-color: #9ed0aa; background: #eaf7e9; color: #00543d; font-weight: 800; }
|
||
|
||
.community-layout { display: grid; grid-template-columns: 1fr 280px; gap: 24px; }
|
||
|
||
.post-list { display: flex; flex-direction: column; gap: 12px; }
|
||
.post-card { display: grid; grid-template-columns: 170px minmax(0, 1fr); gap: 16px; background: #fff; border: 1px solid var(--border-color); border-radius: var(--radius-lg); padding: 12px; cursor: pointer; transition: all 0.3s; }
|
||
.post-card:hover { box-shadow: var(--shadow-md); transform: translateY(-1px); }
|
||
.post-cover { position: relative; min-height: 124px; border-radius: 10px; overflow: hidden; }
|
||
.post-cover span { position: absolute; left: 10px; bottom: 10px; height: 24px; display: inline-flex; align-items: center; padding: 0 9px; border-radius: 12px; background: rgba(255,255,255,.88); color: #00543d; font-size: 12px; font-weight: 900; }
|
||
.cover-green { background: linear-gradient(135deg, #064231, #00844e); }
|
||
.cover-yellow { background: linear-gradient(135deg, #fff1a1, #ffd84d); }
|
||
.cover-blue { background: linear-gradient(135deg, #dbe8ff, #c8ddff); }
|
||
.post-body { min-width: 0; padding: 6px 4px; }
|
||
.post-type-badge { margin-bottom: 8px; }
|
||
.post-body h3 { font-size: 16px; font-weight: 600; color: var(--text-primary); margin-bottom: 8px; }
|
||
.post-meta { display: flex; align-items: center; flex-wrap: wrap; gap: 7px; margin-bottom: 7px; color: #75867f; font-size: 12px; }
|
||
.post-meta span { display: inline-flex; align-items: center; height: 22px; border-radius: 11px; background: #f1f6f1; padding: 0 8px; }
|
||
.post-excerpt { font-size: 14px; color: var(--text-muted); line-height: 1.6; margin-bottom: 12px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||
.post-tags { margin-bottom: 12px; }
|
||
.post-stats { display: flex; gap: 16px; font-size: 13px; color: var(--text-muted); }
|
||
.post-stats span { display: flex; align-items: center; gap: 4px; }
|
||
.post-actions { display: flex; gap: 8px; margin-top: 12px; }
|
||
.post-actions button { height: 32px; border: 1px solid #dbe8de; border-radius: 8px; background: #fff; color: #31584d; padding: 0 12px; font-weight: 800; cursor: pointer; }
|
||
.post-actions button .el-icon { margin-right: 4px; vertical-align: -2px; }
|
||
.post-actions button.primary { border-color: #00543d; background: #00543d; color: #fff; }
|
||
.post-actions button.danger { border-color: #f3c9c9; color: #b42318; }
|
||
.post-actions button:hover { border-color: #9ed0aa; background: #f6fbf6; color: #00543d; }
|
||
.post-actions button.primary:hover { background: #004532; color: #fff; }
|
||
.post-actions button.danger:hover { border-color: #f0a8a8; background: #fff5f5; color: #9f1c14; }
|
||
|
||
.sidebar-section { position: sticky; top: 32px; }
|
||
.hot-tags-card { background: #fff; border: 1px solid var(--border-color); border-radius: var(--radius-lg); padding: 20px; margin-bottom: 14px; }
|
||
.hot-tags-card h4 { font-size: 15px; font-weight: 600; color: var(--text-primary); margin-bottom: 12px; }
|
||
.hot-tags { display: flex; flex-wrap: wrap; gap: 8px; }
|
||
.hot-tag { padding: 4px 12px; background: var(--primary-bg); color: var(--primary); border-radius: 20px; font-size: 13px; cursor: pointer; transition: all 0.2s; }
|
||
.hot-tag:hover { background: var(--primary); color: #fff; }
|
||
.template-rank { display: flex; flex-direction: column; gap: 10px; }
|
||
.template-rank button { display: grid; grid-template-columns: 56px minmax(0, 1fr); gap: 10px; align-items: center; border: none; background: transparent; padding: 0; text-align: left; cursor: pointer; }
|
||
.template-rank button > span { height: 42px; border-radius: 8px; display: inline-flex; align-items: center; justify-content: center; color: #173f35; font-weight: 900; }
|
||
.rank-cream { background: #f7e3a1; }
|
||
.rank-yellow { background: #ffd84d; }
|
||
.rank-mint { background: #dff7e5; }
|
||
.template-rank strong { display: block; color: #173f35; font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.template-rank small { color: #87958f; font-size: 12px; }
|
||
.rank-empty { border: 1px dashed #dbe8de; border-radius: 10px; color: #87958f; font-size: 13px; padding: 14px; text-align: center; }
|
||
|
||
.type-select { display: flex; gap: 8px; }
|
||
.type-option { padding: 8px 20px; border: 1px solid var(--border-color); border-radius: var(--radius-sm); font-size: 14px; color: var(--text-secondary); cursor: pointer; transition: all 0.2s; }
|
||
.type-option.active { background: var(--primary-bg); border-color: var(--primary); color: var(--primary); font-weight: 500; }
|
||
|
||
.empty-state { display: flex; flex-direction: column; align-items: center; gap: 12px; padding: 60px 0; }
|
||
.empty-state p { color: var(--text-muted); font-size: 15px; }
|
||
.empty-state button { border: none; border-radius: 18px; background: #00543d; color: #fff; padding: 8px 18px; font-weight: 800; cursor: pointer; }
|
||
|
||
@media (max-width: 980px) {
|
||
.community-layout { grid-template-columns: 1fr; }
|
||
.sidebar-section { position: static; }
|
||
}
|
||
|
||
@media (max-width: 640px) {
|
||
.post-card { grid-template-columns: 1fr; }
|
||
.post-actions { flex-direction: column; }
|
||
}
|
||
</style>
|