路由更新

This commit is contained in:
2025-10-16 18:03:46 +08:00
parent 1199cbc176
commit 0811af6d03
94 changed files with 9511 additions and 667 deletions

View File

@@ -0,0 +1,123 @@
<template>
<div class="ai-config">
<el-form :model="configForm" label-width="150px" class="config-form">
<el-divider content-position="left">模型配置</el-divider>
<el-form-item label="AI模型">
<el-select v-model="configForm.model" placeholder="选择AI模型">
<el-option label="GPT-3.5" value="gpt-3.5" />
<el-option label="GPT-4" value="gpt-4" />
<el-option label="Claude" value="claude" />
</el-select>
</el-form-item>
<el-form-item label="API Key">
<el-input v-model="configForm.apiKey" type="password" show-password />
</el-form-item>
<el-form-item label="API地址">
<el-input v-model="configForm.apiUrl" />
</el-form-item>
<el-divider content-position="left">对话配置</el-divider>
<el-form-item label="温度值">
<el-slider v-model="configForm.temperature" :min="0" :max="2" :step="0.1" show-input />
<span class="help-text">控制回答的随机性值越大回答越随机</span>
</el-form-item>
<el-form-item label="最大token数">
<el-input-number v-model="configForm.maxTokens" :min="100" :max="4000" />
</el-form-item>
<el-form-item label="历史对话轮数">
<el-input-number v-model="configForm.historyTurns" :min="1" :max="20" />
</el-form-item>
<el-divider content-position="left">功能配置</el-divider>
<el-form-item label="启用流式输出">
<el-switch v-model="configForm.enableStreaming" />
</el-form-item>
<el-form-item label="启用文件解读">
<el-switch v-model="configForm.enableFileInterpretation" />
</el-form-item>
<el-form-item label="启用知识库检索">
<el-switch v-model="configForm.enableKnowledgeRetrieval" />
</el-form-item>
<el-form-item label="系统提示词">
<el-input
v-model="configForm.systemPrompt"
type="textarea"
:rows="6"
placeholder="设置AI助手的角色和行为..."
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSave">保存配置</el-button>
<el-button @click="handleTest">测试连接</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ElForm, ElFormItem, ElSelect, ElOption, ElInput, ElSlider, ElInputNumber, ElSwitch, ElButton, ElDivider, ElMessage } from 'element-plus';
const configForm = ref({
model: 'gpt-3.5',
apiKey: '',
apiUrl: '',
temperature: 0.7,
maxTokens: 2000,
historyTurns: 5,
enableStreaming: true,
enableFileInterpretation: true,
enableKnowledgeRetrieval: true,
systemPrompt: ''
});
onMounted(() => {
loadConfig();
});
function loadConfig() {
// TODO: 加载AI配置
}
function handleSave() {
// TODO: 保存配置
ElMessage.success('配置保存成功');
}
function handleTest() {
// TODO: 测试API连接
ElMessage.info('正在测试连接...');
}
function handleReset() {
// TODO: 重置配置
}
</script>
<style lang="scss" scoped>
.ai-config {
padding: 20px;
max-width: 800px;
}
.config-form {
.help-text {
font-size: 12px;
color: #999;
margin-left: 12px;
}
}
</style>

View File

@@ -0,0 +1,37 @@
<template>
<div class="ai-management">
<h1 class="page-title">智能体管理</h1>
<el-tabs v-model="activeTab">
<el-tab-pane label="基础配置" name="config">
<AIConfig />
</el-tab-pane>
<el-tab-pane label="知识库管理" name="knowledge">
<KnowledgeManagement />
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { ElTabs, ElTabPane } from 'element-plus';
import AIConfig from './components/AIConfig.vue';
import KnowledgeManagement from './components/KnowledgeManagement.vue';
const activeTab = ref('config');
</script>
<style lang="scss" scoped>
.ai-management {
padding: 20px;
}
.page-title {
font-size: 28px;
font-weight: 600;
color: #141F38;
margin-bottom: 24px;
}
</style>

View File

@@ -0,0 +1,119 @@
<template>
<div class="knowledge-management">
<div class="action-bar">
<el-button type="primary" @click="showCreateDialog">+ 新增知识</el-button>
<el-button @click="handleImport">批量导入</el-button>
<el-input
v-model="searchKeyword"
placeholder="搜索知识..."
style="width: 300px"
clearable
/>
</div>
<el-table :data="knowledgeList" style="width: 100%">
<el-table-column prop="title" label="标题" min-width="200" />
<el-table-column prop="category" label="分类" width="120" />
<el-table-column prop="tags" label="标签" width="200">
<template #default="{ row }">
<el-tag v-for="tag in row.tags" :key="tag" size="small" style="margin-right: 4px;">
{{ tag }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-switch
v-model="row.status"
@change="toggleStatus(row)"
/>
</template>
</el-table-column>
<el-table-column prop="updateDate" label="更新时间" width="150" />
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="editKnowledge(row)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteKnowledge(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ElButton, ElInput, ElTable, ElTableColumn, ElTag, ElSwitch, ElPagination, ElMessage } from 'element-plus';
const searchKeyword = ref('');
const currentPage = ref(1);
const pageSize = ref(10);
const total = ref(0);
const knowledgeList = ref<any[]>([]);
onMounted(() => {
loadKnowledge();
});
function loadKnowledge() {
// TODO: 加载知识库数据
}
function showCreateDialog() {
// TODO: 显示创建知识对话框
}
function handleImport() {
// TODO: 批量导入
ElMessage.info('批量导入功能开发中');
}
function editKnowledge(row: any) {
// TODO: 编辑知识
}
function deleteKnowledge(row: any) {
// TODO: 删除知识
ElMessage.success('删除成功');
}
function toggleStatus(row: any) {
// TODO: 切换状态
}
function handleSizeChange(val: number) {
pageSize.value = val;
loadKnowledge();
}
function handleCurrentChange(val: number) {
currentPage.value = val;
loadKnowledge();
}
</script>
<style lang="scss" scoped>
.knowledge-management {
padding: 20px;
}
.action-bar {
display: flex;
gap: 16px;
margin-bottom: 20px;
align-items: center;
}
.el-table {
margin-bottom: 20px;
}
</style>

View File

@@ -0,0 +1,82 @@
<template>
<div class="banner-management">
<div class="action-bar">
<el-button type="primary" @click="showCreateDialog">+ 新增Banner</el-button>
</div>
<el-table :data="banners" style="width: 100%">
<el-table-column label="预览" width="200">
<template #default="{ row }">
<img :src="row.image" class="banner-preview" />
</template>
</el-table-column>
<el-table-column prop="title" label="标题" min-width="150" />
<el-table-column prop="linkUrl" label="链接地址" min-width="200" />
<el-table-column prop="sort" label="排序" width="80" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-switch
v-model="row.status"
@change="toggleStatus(row)"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="editBanner(row)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteBanner(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ElButton, ElTable, ElTableColumn, ElSwitch, ElMessage } from 'element-plus';
const banners = ref<any[]>([]);
onMounted(() => {
loadBanners();
});
function loadBanners() {
// TODO: 加载Banner数据
}
function showCreateDialog() {
// TODO: 显示创建Banner对话框
}
function editBanner(row: any) {
// TODO: 编辑Banner
}
function deleteBanner(row: any) {
// TODO: 删除Banner
ElMessage.success('删除成功');
}
function toggleStatus(row: any) {
// TODO: 切换Banner状态
}
</script>
<style lang="scss" scoped>
.banner-management {
padding: 20px;
}
.action-bar {
margin-bottom: 20px;
}
.banner-preview {
width: 100%;
height: 80px;
object-fit: cover;
border-radius: 4px;
}
</style>

View File

@@ -0,0 +1,65 @@
<template>
<div class="column-management">
<div class="action-bar">
<el-button type="primary" @click="showCreateDialog">+ 新增栏目</el-button>
</div>
<el-table :data="columns" style="width: 100%" row-key="id" :tree-props="{children: 'children'}">
<el-table-column prop="name" label="栏目名称" min-width="200" />
<el-table-column prop="code" label="栏目编码" width="150" />
<el-table-column prop="sort" label="排序" width="80" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status ? 'success' : 'info'">
{{ row.status ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="editColumn(row)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteColumn(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ElButton, ElTable, ElTableColumn, ElTag, ElMessage } from 'element-plus';
const columns = ref<any[]>([]);
onMounted(() => {
loadColumns();
});
function loadColumns() {
// TODO: 加载栏目数据
}
function showCreateDialog() {
// TODO: 显示创建栏目对话框
}
function editColumn(row: any) {
// TODO: 编辑栏目
}
function deleteColumn(row: any) {
// TODO: 删除栏目
ElMessage.success('删除成功');
}
</script>
<style lang="scss" scoped>
.column-management {
padding: 20px;
}
.action-bar {
margin-bottom: 20px;
}
</style>

View File

@@ -0,0 +1,41 @@
<template>
<div class="language-management">
<h1 class="page-title">语言管理</h1>
<el-tabs v-model="activeTab">
<el-tab-pane label="Banner管理" name="banner">
<BannerManagement />
</el-tab-pane>
<el-tab-pane label="资源栏目管理" name="column">
<ColumnManagement />
</el-tab-pane>
<el-tab-pane label="标签管理" name="tag">
<TagManagement />
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { ElTabs, ElTabPane } from 'element-plus';
import BannerManagement from './components/BannerManagement.vue';
import ColumnManagement from './components/ColumnManagement.vue';
import TagManagement from './components/TagManagement.vue';
const activeTab = ref('banner');
</script>
<style lang="scss" scoped>
.language-management {
padding: 20px;
}
.page-title {
font-size: 28px;
font-weight: 600;
color: #141F38;
margin-bottom: 24px;
}
</style>

View File

@@ -0,0 +1,81 @@
<template>
<div class="tag-management">
<div class="action-bar">
<el-button type="primary" @click="showCreateDialog">+ 新增标签</el-button>
<el-input
v-model="searchKeyword"
placeholder="搜索标签..."
style="width: 300px"
clearable
/>
</div>
<el-table :data="tags" style="width: 100%">
<el-table-column prop="name" label="标签名称" min-width="150" />
<el-table-column prop="category" label="标签分类" width="120" />
<el-table-column prop="color" label="颜色" width="100">
<template #default="{ row }">
<div class="color-preview" :style="{ background: row.color }"></div>
</template>
</el-table-column>
<el-table-column prop="usageCount" label="使用次数" width="100" />
<el-table-column prop="createDate" label="创建时间" width="150" />
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="editTag(row)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteTag(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ElButton, ElInput, ElTable, ElTableColumn, ElMessage } from 'element-plus';
const searchKeyword = ref('');
const tags = ref<any[]>([]);
onMounted(() => {
loadTags();
});
function loadTags() {
// TODO: 加载标签数据
}
function showCreateDialog() {
// TODO: 显示创建标签对话框
}
function editTag(row: any) {
// TODO: 编辑标签
}
function deleteTag(row: any) {
// TODO: 删除标签
ElMessage.success('删除成功');
}
</script>
<style lang="scss" scoped>
.tag-management {
padding: 20px;
}
.action-bar {
display: flex;
gap: 16px;
margin-bottom: 20px;
align-items: center;
}
.color-preview {
width: 40px;
height: 24px;
border-radius: 4px;
border: 1px solid #e0e0e0;
}
</style>

View File

@@ -0,0 +1,123 @@
<template>
<div class="login-logs">
<div class="filter-bar">
<el-input
v-model="searchKeyword"
placeholder="搜索用户名..."
style="width: 200px"
clearable
/>
<el-select v-model="loginStatus" placeholder="登录状态" style="width: 150px" clearable>
<el-option label="成功" value="success" />
<el-option label="失败" value="failed" />
</el-select>
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleExport">导出</el-button>
<el-button type="danger" @click="handleClear">清空日志</el-button>
</div>
<el-table :data="logs" style="width: 100%">
<el-table-column prop="username" label="用户名" width="120" />
<el-table-column prop="ipAddress" label="IP地址" width="140" />
<el-table-column prop="location" label="登录地点" width="150" />
<el-table-column prop="browser" label="浏览器" width="120" />
<el-table-column prop="os" label="操作系统" width="120" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === 'success' ? 'success' : 'danger'">
{{ row.status === 'success' ? '成功' : '失败' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="message" label="信息" min-width="150" />
<el-table-column prop="loginTime" label="登录时间" width="180" />
</el-table>
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ElInput, ElSelect, ElOption, ElDatePicker, ElButton, ElTable, ElTableColumn, ElTag, ElPagination, ElMessage, ElMessageBox } from 'element-plus';
const searchKeyword = ref('');
const loginStatus = ref('');
const dateRange = ref<[Date, Date] | null>(null);
const currentPage = ref(1);
const pageSize = ref(10);
const total = ref(0);
const logs = ref<any[]>([]);
onMounted(() => {
loadLogs();
});
function loadLogs() {
// TODO: 加载登录日志
}
function handleSearch() {
currentPage.value = 1;
loadLogs();
}
function handleExport() {
// TODO: 导出日志
ElMessage.info('导出功能开发中');
}
async function handleClear() {
try {
await ElMessageBox.confirm('确定要清空所有登录日志吗?此操作不可恢复!', '警告', {
type: 'warning'
});
// TODO: 清空日志
ElMessage.success('日志已清空');
} catch {
// 取消操作
}
}
function handleSizeChange(val: number) {
pageSize.value = val;
loadLogs();
}
function handleCurrentChange(val: number) {
currentPage.value = val;
loadLogs();
}
</script>
<style lang="scss" scoped>
.login-logs {
padding: 20px;
}
.filter-bar {
display: flex;
gap: 16px;
margin-bottom: 20px;
align-items: center;
}
.el-table {
margin-bottom: 20px;
}
</style>

View File

@@ -0,0 +1,147 @@
<template>
<div class="operation-logs">
<div class="filter-bar">
<el-input
v-model="searchKeyword"
placeholder="搜索用户名或操作..."
style="width: 250px"
clearable
/>
<el-select v-model="operationType" placeholder="操作类型" style="width: 150px" clearable>
<el-option label="新增" value="create" />
<el-option label="修改" value="update" />
<el-option label="删除" value="delete" />
<el-option label="查询" value="read" />
</el-select>
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleExport">导出</el-button>
</div>
<el-table :data="logs" style="width: 100%">
<el-table-column prop="username" label="操作人" width="120" />
<el-table-column prop="module" label="操作模块" width="120" />
<el-table-column prop="operation" label="操作类型" width="100">
<template #default="{ row }">
<el-tag :type="getOperationType(row.operation)">
{{ getOperationText(row.operation) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="description" label="操作描述" min-width="200" />
<el-table-column prop="ipAddress" label="IP地址" width="140" />
<el-table-column prop="duration" label="耗时(ms)" width="100" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === 'success' ? 'success' : 'danger'">
{{ row.status === 'success' ? '成功' : '失败' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="operationTime" label="操作时间" width="180" />
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="viewDetail(row)">详情</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ElInput, ElSelect, ElOption, ElDatePicker, ElButton, ElTable, ElTableColumn, ElTag, ElPagination, ElMessage } from 'element-plus';
const searchKeyword = ref('');
const operationType = ref('');
const dateRange = ref<[Date, Date] | null>(null);
const currentPage = ref(1);
const pageSize = ref(10);
const total = ref(0);
const logs = ref<any[]>([]);
onMounted(() => {
loadLogs();
});
function loadLogs() {
// TODO: 加载操作日志
}
function handleSearch() {
currentPage.value = 1;
loadLogs();
}
function handleExport() {
// TODO: 导出日志
ElMessage.info('导出功能开发中');
}
function getOperationType(type: string) {
const typeMap: Record<string, any> = {
'create': 'success',
'update': 'warning',
'delete': 'danger',
'read': 'info'
};
return typeMap[type] || 'info';
}
function getOperationText(type: string) {
const textMap: Record<string, string> = {
'create': '新增',
'update': '修改',
'delete': '删除',
'read': '查询'
};
return textMap[type] || type;
}
function viewDetail(row: any) {
// TODO: 查看操作详情
}
function handleSizeChange(val: number) {
pageSize.value = val;
loadLogs();
}
function handleCurrentChange(val: number) {
currentPage.value = val;
loadLogs();
}
</script>
<style lang="scss" scoped>
.operation-logs {
padding: 20px;
}
.filter-bar {
display: flex;
gap: 16px;
margin-bottom: 20px;
align-items: center;
}
.el-table {
margin-bottom: 20px;
}
</style>

View File

@@ -0,0 +1,190 @@
<template>
<div class="system-config">
<el-form :model="configForm" label-width="150px" class="config-form">
<el-divider content-position="left">基本设置</el-divider>
<el-form-item label="系统名称">
<el-input v-model="configForm.systemName" />
</el-form-item>
<el-form-item label="系统Logo">
<el-upload
class="logo-uploader"
action="#"
:show-file-list="false"
:before-upload="beforeLogoUpload"
>
<img v-if="configForm.logo" :src="configForm.logo" class="logo-preview" />
<el-icon v-else class="logo-uploader-icon">+</el-icon>
</el-upload>
</el-form-item>
<el-form-item label="版权信息">
<el-input v-model="configForm.copyright" />
</el-form-item>
<el-divider content-position="left">安全设置</el-divider>
<el-form-item label="启用验证码">
<el-switch v-model="configForm.enableCaptcha" />
</el-form-item>
<el-form-item label="密码最小长度">
<el-input-number v-model="configForm.minPasswordLength" :min="6" :max="20" />
</el-form-item>
<el-form-item label="会话超时(分钟)">
<el-input-number v-model="configForm.sessionTimeout" :min="5" :max="1440" />
</el-form-item>
<el-form-item label="登录失败锁定">
<el-switch v-model="configForm.enableLoginLock" />
</el-form-item>
<el-form-item label="锁定阈值(次)" v-if="configForm.enableLoginLock">
<el-input-number v-model="configForm.loginLockThreshold" :min="3" :max="10" />
</el-form-item>
<el-divider content-position="left">功能设置</el-divider>
<el-form-item label="启用用户注册">
<el-switch v-model="configForm.enableRegister" />
</el-form-item>
<el-form-item label="启用评论功能">
<el-switch v-model="configForm.enableComment" />
</el-form-item>
<el-form-item label="启用文件上传">
<el-switch v-model="configForm.enableFileUpload" />
</el-form-item>
<el-form-item label="文件上传大小限制(MB)">
<el-input-number v-model="configForm.maxFileSize" :min="1" :max="100" />
</el-form-item>
<el-divider content-position="left">邮件设置</el-divider>
<el-form-item label="启用邮件通知">
<el-switch v-model="configForm.enableEmail" />
</el-form-item>
<el-form-item label="SMTP服务器" v-if="configForm.enableEmail">
<el-input v-model="configForm.smtpHost" />
</el-form-item>
<el-form-item label="SMTP端口" v-if="configForm.enableEmail">
<el-input-number v-model="configForm.smtpPort" :min="1" :max="65535" />
</el-form-item>
<el-form-item label="发件人邮箱" v-if="configForm.enableEmail">
<el-input v-model="configForm.senderEmail" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSave">保存配置</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ElForm, ElFormItem, ElInput, ElInputNumber, ElSwitch, ElButton, ElDivider, ElUpload, ElIcon, ElMessage } from 'element-plus';
const configForm = ref({
systemName: '红色思政学习平台',
logo: '',
copyright: 'Copyright ©红色思政智能体平台',
enableCaptcha: false,
minPasswordLength: 6,
sessionTimeout: 30,
enableLoginLock: true,
loginLockThreshold: 5,
enableRegister: true,
enableComment: true,
enableFileUpload: true,
maxFileSize: 10,
enableEmail: false,
smtpHost: '',
smtpPort: 587,
senderEmail: ''
});
onMounted(() => {
loadConfig();
});
function loadConfig() {
// TODO: 加载系统配置
}
function beforeLogoUpload(file: File) {
const isImage = file.type.startsWith('image/');
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isImage) {
ElMessage.error('只能上传图片文件');
return false;
}
if (!isLt2M) {
ElMessage.error('图片大小不能超过 2MB');
return false;
}
return true;
}
function handleSave() {
// TODO: 保存配置
ElMessage.success('配置保存成功');
}
function handleReset() {
// TODO: 重置配置
loadConfig();
}
</script>
<style lang="scss" scoped>
.system-config {
padding: 20px;
max-width: 800px;
}
.config-form {
padding: 20px 0;
}
.logo-uploader {
:deep(.el-upload) {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: all 0.3s;
&:hover {
border-color: #C62828;
}
}
}
.logo-preview {
width: 178px;
height: 178px;
display: block;
object-fit: contain;
}
.logo-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
text-align: center;
line-height: 178px;
}
</style>

View File

@@ -0,0 +1,41 @@
<template>
<div class="system-logs">
<h1 class="page-title">系统日志</h1>
<el-tabs v-model="activeTab">
<el-tab-pane label="登录日志" name="login">
<LoginLogs />
</el-tab-pane>
<el-tab-pane label="操作日志" name="operation">
<OperationLogs />
</el-tab-pane>
<el-tab-pane label="系统配置" name="config">
<SystemConfig />
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { ElTabs, ElTabPane } from 'element-plus';
import LoginLogs from './components/LoginLogs.vue';
import OperationLogs from './components/OperationLogs.vue';
import SystemConfig from './components/SystemConfig.vue';
const activeTab = ref('login');
</script>
<style lang="scss" scoped>
.system-logs {
padding: 20px;
}
.page-title {
font-size: 28px;
font-weight: 600;
color: #141F38;
margin-bottom: 24px;
}
</style>

View File

@@ -0,0 +1,126 @@
<template>
<div class="article-management">
<div class="action-bar">
<el-button type="primary" @click="showCreateDialog">+ 新增文章</el-button>
<el-button @click="handleDataCollection">数据采集</el-button>
<el-input
v-model="searchKeyword"
placeholder="搜索文章..."
style="width: 300px"
clearable
/>
</div>
<el-table :data="articles" style="width: 100%">
<el-table-column prop="title" label="文章标题" min-width="200" />
<el-table-column prop="category" label="分类" width="120" />
<el-table-column prop="author" label="作者" width="120" />
<el-table-column prop="publishDate" label="发布日期" width="120" />
<el-table-column prop="views" label="阅读量" width="100" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="editArticle(row)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteArticle(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ElButton, ElInput, ElTable, ElTableColumn, ElTag, ElPagination, ElMessage } from 'element-plus';
const searchKeyword = ref('');
const currentPage = ref(1);
const pageSize = ref(10);
const total = ref(0);
const articles = ref<any[]>([]);
onMounted(() => {
loadArticles();
});
function loadArticles() {
// TODO: 加载文章数据
}
function showCreateDialog() {
// TODO: 显示创建文章对话框
}
function handleDataCollection() {
// TODO: 数据采集功能
ElMessage.info('数据采集功能开发中');
}
function editArticle(row: any) {
// TODO: 编辑文章
}
function deleteArticle(row: any) {
// TODO: 删除文章
}
function getStatusType(status: string) {
const typeMap: Record<string, any> = {
'published': 'success',
'draft': 'info',
'pending': 'warning'
};
return typeMap[status] || 'info';
}
function getStatusText(status: string) {
const textMap: Record<string, string> = {
'published': '已发布',
'draft': '草稿',
'pending': '待审核'
};
return textMap[status] || status;
}
function handleSizeChange(val: number) {
pageSize.value = val;
loadArticles();
}
function handleCurrentChange(val: number) {
currentPage.value = val;
loadArticles();
}
</script>
<style lang="scss" scoped>
.article-management {
padding: 20px;
}
.action-bar {
display: flex;
gap: 16px;
margin-bottom: 20px;
align-items: center;
}
.el-table {
margin-bottom: 20px;
}
</style>

View File

@@ -0,0 +1,44 @@
<template>
<div class="data-records">
<el-tabs v-model="activeTab">
<el-tab-pane label="菜单管理" name="menu">
<!-- 菜单管理已在manage/system中实现这里可以引用或重新实现 -->
<div class="redirect-info">
<p>菜单管理功能已在系统管理模块中实现</p>
<el-button type="primary" @click="goToMenuManage">前往菜单管理</el-button>
</div>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { ElTabs, ElTabPane, ElButton } from 'element-plus';
const router = useRouter();
const activeTab = ref('menu');
function goToMenuManage() {
router.push('/admin/system/menu');
}
</script>
<style lang="scss" scoped>
.data-records {
padding: 20px;
}
.redirect-info {
padding: 40px;
text-align: center;
p {
font-size: 16px;
color: #666;
margin-bottom: 20px;
}
}
</style>

View File

@@ -0,0 +1,37 @@
<template>
<div class="resource-management">
<h1 class="page-title">资源管理</h1>
<el-tabs v-model="activeTab" class="resource-tabs">
<el-tab-pane label="文章储备" name="articles">
<ArticleManagement />
</el-tab-pane>
<el-tab-pane label="数据记录" name="data">
<DataRecords />
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { ElTabs, ElTabPane } from 'element-plus';
import ArticleManagement from './components/ArticleManagement.vue';
import DataRecords from './components/DataRecords.vue';
const activeTab = ref('articles');
</script>
<style lang="scss" scoped>
.resource-management {
padding: 20px;
}
.page-title {
font-size: 28px;
font-weight: 600;
color: #141F38;
margin-bottom: 24px;
}
</style>

View File

@@ -0,0 +1,37 @@
<template>
<div class="study-management">
<h1 class="page-title">学习管理</h1>
<el-tabs v-model="activeTab">
<el-tab-pane label="学习任务发布" name="task-publish">
<TaskPublish />
</el-tab-pane>
<el-tab-pane label="学习记录" name="task-records">
<StudyRecords />
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { ElTabs, ElTabPane } from 'element-plus';
import TaskPublish from './components/TaskPublish.vue';
import StudyRecords from './components/StudyRecords.vue';
const activeTab = ref('task-publish');
</script>
<style lang="scss" scoped>
.study-management {
padding: 20px;
}
.page-title {
font-size: 28px;
font-weight: 600;
color: #141F38;
margin-bottom: 24px;
}
</style>

View File

@@ -0,0 +1,149 @@
<template>
<div class="study-records">
<div class="filter-bar">
<el-input
v-model="searchKeyword"
placeholder="搜索用户..."
style="width: 200px"
clearable
/>
<el-select v-model="selectedTask" placeholder="选择任务" style="width: 200px" clearable>
<el-option
v-for="task in tasks"
:key="task.id"
:label="task.name"
:value="task.id"
/>
</el-select>
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleExport">导出</el-button>
</div>
<el-table :data="records" style="width: 100%">
<el-table-column prop="userName" label="用户" width="120" />
<el-table-column prop="taskName" label="任务名称" min-width="180" />
<el-table-column prop="progress" label="完成进度" width="120">
<template #default="{ row }">
<el-progress :percentage="row.progress" />
</template>
</el-table-column>
<el-table-column prop="duration" label="学习时长" width="120" />
<el-table-column prop="startDate" label="开始时间" width="150" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="viewDetail(row)">查看详情</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ElInput, ElSelect, ElOption, ElDatePicker, ElButton, ElTable, ElTableColumn, ElTag, ElProgress, ElPagination, ElMessage } from 'element-plus';
const searchKeyword = ref('');
const selectedTask = ref('');
const dateRange = ref<[Date, Date] | null>(null);
const currentPage = ref(1);
const pageSize = ref(10);
const total = ref(0);
const records = ref<any[]>([]);
const tasks = ref<any[]>([]);
onMounted(() => {
loadTasks();
loadRecords();
});
function loadTasks() {
// TODO: 加载任务列表
}
function loadRecords() {
// TODO: 加载学习记录
}
function handleSearch() {
currentPage.value = 1;
loadRecords();
}
function handleExport() {
// TODO: 导出学习记录
ElMessage.info('导出功能开发中');
}
function getStatusType(status: string) {
const typeMap: Record<string, any> = {
'completed': 'success',
'in-progress': 'warning',
'not-started': 'info'
};
return typeMap[status] || 'info';
}
function getStatusText(status: string) {
const textMap: Record<string, string> = {
'completed': '已完成',
'in-progress': '进行中',
'not-started': '未开始'
};
return textMap[status] || status;
}
function viewDetail(row: any) {
// TODO: 查看学习记录详情
}
function handleSizeChange(val: number) {
pageSize.value = val;
loadRecords();
}
function handleCurrentChange(val: number) {
currentPage.value = val;
loadRecords();
}
</script>
<style lang="scss" scoped>
.study-records {
padding: 20px;
}
.filter-bar {
display: flex;
gap: 16px;
margin-bottom: 20px;
align-items: center;
}
.el-table {
margin-bottom: 20px;
}
</style>

View File

@@ -0,0 +1,135 @@
<template>
<div class="task-publish">
<el-form :model="taskForm" :rules="taskRules" ref="taskFormRef" label-width="120px">
<el-form-item label="任务名称" prop="name">
<el-input v-model="taskForm.name" placeholder="请输入任务名称" />
</el-form-item>
<el-form-item label="任务描述" prop="description">
<el-input
v-model="taskForm.description"
type="textarea"
:rows="4"
placeholder="请输入任务描述"
/>
</el-form-item>
<el-form-item label="任务周期" prop="period">
<el-date-picker
v-model="taskForm.period"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
</el-form-item>
<el-form-item label="关联资源" prop="resources">
<el-button @click="showResourceSelector">选择资源</el-button>
<div class="selected-resources" v-if="taskForm.resources.length">
<el-tag
v-for="resource in taskForm.resources"
:key="resource.id"
closable
@close="removeResource(resource)"
>
{{ resource.name }}
</el-tag>
</div>
</el-form-item>
<el-form-item label="任务接受对象" prop="targets">
<el-radio-group v-model="taskForm.targetType">
<el-radio label="dept">按部门</el-radio>
<el-radio label="role">按权限</el-radio>
<el-radio label="user">选人员</el-radio>
</el-radio-group>
<el-button @click="showTargetSelector" style="margin-left: 16px;">
选择对象
</el-button>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handlePublish">发布任务</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { ElForm, ElFormItem, ElInput, ElDatePicker, ElButton, ElRadioGroup, ElRadio, ElTag, ElMessage, type FormInstance, type FormRules } from 'element-plus';
const taskFormRef = ref<FormInstance>();
const taskForm = ref({
name: '',
description: '',
period: null as [Date, Date] | null,
resources: [] as any[],
targetType: 'dept',
targets: [] as any[]
});
const taskRules: FormRules = {
name: [
{ required: true, message: '请输入任务名称', trigger: 'blur' }
],
description: [
{ required: true, message: '请输入任务描述', trigger: 'blur' }
],
period: [
{ required: true, message: '请选择任务周期', trigger: 'change' }
]
};
function showResourceSelector() {
// TODO: 显示资源选择器
}
function removeResource(resource: any) {
const index = taskForm.value.resources.indexOf(resource);
if (index > -1) {
taskForm.value.resources.splice(index, 1);
}
}
function showTargetSelector() {
// TODO: 显示对象选择器
}
async function handlePublish() {
if (!taskFormRef.value) return;
try {
await taskFormRef.value.validate();
// TODO: 调用发布任务API
ElMessage.success('任务发布成功');
handleReset();
} catch (error) {
console.error('表单验证失败', error);
}
}
function handleReset() {
taskFormRef.value?.resetFields();
taskForm.value.resources = [];
taskForm.value.targets = [];
}
</script>
<style lang="scss" scoped>
.task-publish {
padding: 20px;
max-width: 800px;
}
.selected-resources {
margin-top: 12px;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
</style>

View File

@@ -0,0 +1,304 @@
<template>
<div class="system-overview">
<h1 class="page-title">系统总览</h1>
<!-- 统计卡片 -->
<div class="stats-grid">
<div class="stat-card" v-for="stat in statistics" :key="stat.label">
<div class="stat-icon" :style="{ background: stat.color }">
<i>{{ stat.icon }}</i>
</div>
<div class="stat-content">
<h3>{{ stat.value }}</h3>
<p>{{ stat.label }}</p>
<span class="stat-change" :class="stat.trend">
{{ stat.change }}
</span>
</div>
</div>
</div>
<!-- 图表区域 -->
<el-row :gutter="20" class="charts-row">
<el-col :span="16">
<el-card class="chart-card">
<template #header>
<div class="card-header">
<span>用户活跃度折线图</span>
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
size="small"
/>
</div>
</template>
<div class="chart-container" ref="activityChart"></div>
</el-card>
</el-col>
<el-col :span="8">
<el-card class="chart-card">
<template #header>
<span>资源分类统计(饼图)</span>
</template>
<div class="chart-container" ref="resourcePieChart"></div>
</el-card>
</el-col>
</el-row>
<!-- 今日访问量详情 -->
<el-card class="visit-card">
<template #header>
<span>今日访问量</span>
</template>
<div class="visit-stats">
<div class="visit-item" v-for="item in visitStats" :key="item.label">
<div class="visit-label">{{ item.label }}</div>
<div class="visit-value">{{ item.value }}</div>
</div>
</div>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { ElRow, ElCol, ElCard, ElDatePicker } from 'element-plus';
import * as echarts from 'echarts';
const dateRange = ref<[Date, Date] | null>(null);
const activityChart = ref<HTMLElement | null>(null);
const resourcePieChart = ref<HTMLElement | null>(null);
let activityChartInstance: echarts.ECharts | null = null;
let pieChartInstance: echarts.ECharts | null = null;
const statistics = ref([
{
icon: '👥',
label: '总用户数',
value: '1,234',
change: '+12%',
trend: 'up',
color: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
},
{
icon: '📚',
label: '总资源数',
value: '5,678',
change: '+8%',
trend: 'up',
color: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)'
},
{
icon: '👁',
label: '今日访问量',
value: '892',
change: '+15%',
trend: 'up',
color: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)'
},
{
icon: '✅',
label: '活跃用户',
value: '456',
change: '+5%',
trend: 'up',
color: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)'
}
]);
const visitStats = ref([
{ label: 'UV(独立访客)', value: '892' },
{ label: 'PV(页面浏览量)', value: '3,456' },
{ label: '平均访问时长', value: '5分32秒' },
{ label: '跳出率', value: '35.6%' }
]);
onMounted(() => {
initCharts();
// TODO: 加载实际数据
});
onUnmounted(() => {
if (activityChartInstance) {
activityChartInstance.dispose();
}
if (pieChartInstance) {
pieChartInstance.dispose();
}
});
function initCharts() {
if (activityChart.value) {
activityChartInstance = echarts.init(activityChart.value);
const activityOption = {
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
},
yAxis: {
type: 'value'
},
series: [{
data: [120, 200, 150, 80, 70, 110, 130],
type: 'line',
smooth: true,
areaStyle: {}
}]
};
activityChartInstance.setOption(activityOption);
}
if (resourcePieChart.value) {
pieChartInstance = echarts.init(resourcePieChart.value);
const pieOption = {
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [{
type: 'pie',
radius: '50%',
data: [
{ value: 1048, name: '文章' },
{ value: 735, name: '视频' },
{ value: 580, name: '音频' },
{ value: 484, name: '课程' },
{ value: 300, name: '其他' }
]
}]
};
pieChartInstance.setOption(pieOption);
}
}
</script>
<style lang="scss" scoped>
.system-overview {
padding: 20px;
}
.page-title {
font-size: 28px;
font-weight: 600;
color: #141F38;
margin-bottom: 24px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin-bottom: 20px;
}
.stat-card {
background: white;
padding: 24px;
border-radius: 8px;
display: flex;
gap: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.stat-icon {
width: 64px;
height: 64px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
flex-shrink: 0;
}
.stat-content {
flex: 1;
h3 {
font-size: 28px;
font-weight: 600;
color: #141F38;
margin-bottom: 4px;
}
p {
font-size: 14px;
color: #666;
margin-bottom: 4px;
}
}
.stat-change {
font-size: 13px;
&.up {
color: #4caf50;
}
&.down {
color: #f44336;
}
}
.charts-row {
margin-bottom: 20px;
}
.chart-card {
:deep(.el-card__body) {
padding: 20px;
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.chart-container {
height: 300px;
}
.visit-card {
:deep(.el-card__body) {
padding: 20px;
}
}
.visit-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
}
.visit-item {
text-align: center;
padding: 20px;
background: #f9f9f9;
border-radius: 8px;
}
.visit-label {
font-size: 14px;
color: #666;
margin-bottom: 8px;
}
.visit-value {
font-size: 24px;
font-weight: 600;
color: #C62828;
}
</style>

View File

@@ -0,0 +1,202 @@
<template>
<div class="ai-assistant-page">
<!-- 悬浮球入口可以通过props控制显示/隐藏 -->
<div class="floating-button" @click="togglePanel" v-if="!isPanelVisible">
<img src="@/assets/imgs/ai-icon.svg" alt="AI助手" />
</div>
<!-- AI助手面板 -->
<transition name="slide">
<div class="assistant-panel" v-if="isPanelVisible">
<div class="panel-header">
<h2>AI思政助手</h2>
<div class="header-actions">
<el-button size="small" @click="handleFileUpload">📎 上传文件</el-button>
<el-button size="small" @click="showHistory">📜 历史记录</el-button>
<el-button size="small" @click="togglePanel"></el-button>
</div>
</div>
<div class="panel-tabs">
<div
class="panel-tab"
v-for="tab in tabs"
:key="tab.key"
:class="{ active: activeTab === tab.key }"
@click="activeTab = tab.key"
>
{{ tab.label }}
</div>
</div>
<div class="panel-content">
<component :is="currentComponent" />
</div>
</div>
</transition>
<!-- 历史对话记录弹窗 -->
<el-dialog v-model="historyVisible" title="历史对话记录" width="600px">
<DialogHistory @load-conversation="loadConversation" />
</el-dialog>
<!-- 文件解读与记录弹窗 -->
<el-dialog v-model="fileDialogVisible" title="文件解读与记录" width="800px">
<FileInterpretation />
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { ElButton, ElDialog } from 'element-plus';
import ChatInterface from './components/ChatInterface.vue';
import KnowledgeBase from './components/KnowledgeBase.vue';
import DialogHistory from './components/DialogHistory.vue';
import FileInterpretation from './components/FileInterpretation.vue';
const isPanelVisible = ref(false);
const activeTab = ref('chat');
const historyVisible = ref(false);
const fileDialogVisible = ref(false);
const tabs = [
{ key: 'chat', label: '对话' },
{ key: 'knowledge', label: '知识库' }
];
const componentMap: Record<string, any> = {
'chat': ChatInterface,
'knowledge': KnowledgeBase
};
const currentComponent = computed(() => componentMap[activeTab.value]);
function togglePanel() {
isPanelVisible.value = !isPanelVisible.value;
}
function showHistory() {
historyVisible.value = true;
}
function handleFileUpload() {
fileDialogVisible.value = true;
}
function loadConversation(conversation: any) {
// TODO: 加载历史对话
historyVisible.value = false;
}
</script>
<style lang="scss" scoped>
.ai-assistant-page {
position: relative;
}
.floating-button {
position: fixed;
bottom: 40px;
right: 40px;
width: 64px;
height: 64px;
background: linear-gradient(135deg, #C62828, #E53935);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 4px 12px rgba(198, 40, 40, 0.4);
transition: all 0.3s;
z-index: 1000;
&:hover {
transform: scale(1.1);
box-shadow: 0 6px 16px rgba(198, 40, 40, 0.5);
}
img {
width: 36px;
height: 36px;
}
}
.assistant-panel {
position: fixed;
bottom: 20px;
right: 20px;
width: 450px;
height: 650px;
background: white;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
display: flex;
flex-direction: column;
z-index: 1000;
overflow: hidden;
}
.panel-header {
padding: 20px;
border-bottom: 1px solid #e0e0e0;
display: flex;
justify-content: space-between;
align-items: center;
background: linear-gradient(135deg, #C62828, #E53935);
color: white;
h2 {
font-size: 18px;
font-weight: 600;
}
}
.header-actions {
display: flex;
gap: 8px;
}
.panel-tabs {
display: flex;
background: #f5f5f5;
padding: 0 16px;
}
.panel-tab {
padding: 12px 20px;
cursor: pointer;
font-size: 14px;
color: #666;
position: relative;
transition: all 0.3s;
&:hover {
color: #C62828;
}
&.active {
color: #C62828;
font-weight: 600;
background: white;
border-radius: 8px 8px 0 0;
}
}
.panel-content {
flex: 1;
overflow: hidden;
}
.slide-enter-active,
.slide-leave-active {
transition: all 0.3s ease;
}
.slide-enter-from,
.slide-leave-to {
transform: translateY(100%);
opacity: 0;
}
</style>

View File

@@ -0,0 +1,250 @@
<template>
<div class="chat-interface">
<!-- 消息列表 -->
<div class="messages-container" ref="messagesContainer">
<div
class="message"
v-for="message in messages"
:key="message.id"
:class="message.role"
>
<div class="message-avatar">
<img :src="getAvatar(message.role)" :alt="message.role" />
</div>
<div class="message-content">
<div class="message-text" v-html="message.content"></div>
<div class="message-time">{{ message.timestamp }}</div>
</div>
</div>
<!-- 加载中动画 -->
<div class="message assistant" v-if="isLoading">
<div class="message-avatar">
<img src="@/assets/imgs/ai-avatar.svg" alt="AI" />
</div>
<div class="message-content">
<div class="typing-indicator">
<span></span>
<span></span>
<span></span>
</div>
</div>
</div>
</div>
<!-- 输入框 -->
<div class="input-container">
<el-input
v-model="inputMessage"
type="textarea"
:rows="3"
placeholder="输入您的问题..."
@keydown.enter.prevent="handleSend"
:disabled="isLoading"
/>
<el-button
type="primary"
@click="handleSend"
:loading="isLoading"
:disabled="!inputMessage.trim()"
>
发送
</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, nextTick, onMounted } from 'vue';
import { ElInput, ElButton } from 'element-plus';
const messagesContainer = ref<HTMLElement | null>(null);
const inputMessage = ref('');
const isLoading = ref(false);
const messages = ref<any[]>([
{
id: 1,
role: 'assistant',
content: '您好我是AI思政助手请问有什么可以帮助您的吗',
timestamp: new Date().toLocaleTimeString()
}
]);
onMounted(() => {
// TODO: 加载历史消息
});
async function handleSend() {
if (!inputMessage.value.trim() || isLoading.value) return;
const userMessage = {
id: Date.now(),
role: 'user',
content: inputMessage.value,
timestamp: new Date().toLocaleTimeString()
};
messages.value.push(userMessage);
const question = inputMessage.value;
inputMessage.value = '';
await nextTick();
scrollToBottom();
// 模拟AI回复
isLoading.value = true;
// TODO: 调用AI API
setTimeout(() => {
const aiMessage = {
id: Date.now(),
role: 'assistant',
content: `关于"${question}",我来为您解答...`,
timestamp: new Date().toLocaleTimeString()
};
messages.value.push(aiMessage);
isLoading.value = false;
nextTick(() => {
scrollToBottom();
});
}, 1500);
}
function getAvatar(role: string) {
return role === 'user'
? '@/assets/imgs/user-avatar.svg'
: '@/assets/imgs/ai-avatar.svg';
}
function scrollToBottom() {
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
}
}
</script>
<style lang="scss" scoped>
.chat-interface {
height: 100%;
display: flex;
flex-direction: column;
}
.messages-container {
flex: 1;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
gap: 16px;
}
.message {
display: flex;
gap: 12px;
&.user {
flex-direction: row-reverse;
.message-content {
align-items: flex-end;
}
.message-text {
background: #C62828;
color: white;
}
}
&.assistant {
.message-text {
background: #f5f5f5;
color: #333;
}
}
}
.message-avatar {
width: 36px;
height: 36px;
flex-shrink: 0;
border-radius: 50%;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.message-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.message-text {
padding: 12px 16px;
border-radius: 12px;
font-size: 14px;
line-height: 1.6;
max-width: 80%;
word-wrap: break-word;
}
.message-time {
font-size: 12px;
color: #999;
padding: 0 4px;
}
.typing-indicator {
display: flex;
gap: 4px;
padding: 12px 16px;
background: #f5f5f5;
border-radius: 12px;
span {
width: 8px;
height: 8px;
background: #999;
border-radius: 50%;
animation: typing 1.4s infinite;
&:nth-child(2) {
animation-delay: 0.2s;
}
&:nth-child(3) {
animation-delay: 0.4s;
}
}
}
@keyframes typing {
0%, 60%, 100% {
transform: translateY(0);
}
30% {
transform: translateY(-10px);
}
}
.input-container {
padding: 16px;
border-top: 1px solid #e0e0e0;
display: flex;
gap: 12px;
:deep(.el-textarea) {
flex: 1;
}
}
</style>

View File

@@ -0,0 +1,110 @@
<template>
<div class="dialog-history">
<div class="history-list">
<div
class="history-item"
v-for="conversation in conversations"
:key="conversation.id"
@click="$emit('load-conversation', conversation)"
>
<div class="item-header">
<h4>{{ conversation.title }}</h4>
<span class="item-date">{{ conversation.date }}</span>
</div>
<p class="item-preview">{{ conversation.preview }}</p>
<div class="item-footer">
<span class="item-count">{{ conversation.messageCount }} 条消息</span>
<el-button size="small" type="danger" @click.stop="deleteConversation(conversation)">
删除
</el-button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ElButton, ElMessage } from 'element-plus';
const conversations = ref<any[]>([]);
defineEmits(['load-conversation']);
onMounted(() => {
// TODO: 加载历史对话列表
});
function deleteConversation(conversation: any) {
// TODO: 删除对话
ElMessage.success('已删除对话');
}
</script>
<style lang="scss" scoped>
.dialog-history {
max-height: 500px;
overflow-y: auto;
}
.history-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.history-item {
padding: 16px;
border: 1px solid #e0e0e0;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
&:hover {
border-color: #C62828;
background: #fff5f5;
}
}
.item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
h4 {
font-size: 16px;
font-weight: 600;
color: #141F38;
}
}
.item-date {
font-size: 12px;
color: #999;
}
.item-preview {
font-size: 14px;
color: #666;
margin-bottom: 12px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.item-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 12px;
border-top: 1px solid #f0f0f0;
}
.item-count {
font-size: 13px;
color: #999;
}
</style>

View File

@@ -0,0 +1,205 @@
<template>
<div class="file-interpretation">
<el-tabs v-model="activeTab">
<el-tab-pane label="文件上传" name="upload">
<div class="upload-section">
<el-upload
drag
action="#"
:before-upload="beforeUpload"
:on-success="handleUploadSuccess"
multiple
>
<div class="upload-icon">📁</div>
<div class="upload-text">
<p>点击或拖拽文件到此处上传</p>
<p class="upload-hint">支持 PDFWordTXT 格式</p>
</div>
</el-upload>
<!-- 已上传文件列表 -->
<div class="uploaded-files" v-if="uploadedFiles.length">
<h4>已上传文件</h4>
<div class="file-list">
<div class="file-item" v-for="file in uploadedFiles" :key="file.id">
<div class="file-icon">📄</div>
<div class="file-info">
<h5>{{ file.name }}</h5>
<p>{{ file.size }} · {{ file.uploadDate }}</p>
</div>
<el-button size="small" @click="analyzeFile(file)">解读</el-button>
</div>
</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="历史文件" name="history">
<div class="history-files">
<div class="file-item" v-for="file in historyFiles" :key="file.id">
<div class="file-icon">📄</div>
<div class="file-info">
<h5>{{ file.name }}</h5>
<p>上传时间{{ file.uploadDate }}</p>
<p class="file-summary">{{ file.summary }}</p>
</div>
<div class="file-actions">
<el-button size="small" @click="viewAnalysis(file)">查看解读</el-button>
<el-button size="small" type="danger" @click="deleteFile(file)">删除</el-button>
</div>
</div>
</div>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ElTabs, ElTabPane, ElUpload, ElButton, ElMessage } from 'element-plus';
const activeTab = ref('upload');
const uploadedFiles = ref<any[]>([]);
const historyFiles = ref<any[]>([]);
onMounted(() => {
// TODO: 加载历史文件
});
function beforeUpload(file: File) {
const allowedTypes = ['application/pdf', 'application/msword', 'text/plain',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'];
if (!allowedTypes.includes(file.type)) {
ElMessage.error('只支持 PDF、Word、TXT 格式');
return false;
}
if (file.size > 10 * 1024 * 1024) {
ElMessage.error('文件大小不能超过 10MB');
return false;
}
return true;
}
function handleUploadSuccess(response: any, file: any) {
uploadedFiles.value.push({
id: Date.now(),
name: file.name,
size: formatFileSize(file.size),
uploadDate: new Date().toLocaleString()
});
ElMessage.success('文件上传成功');
}
function formatFileSize(bytes: number): string {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB';
return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
}
function analyzeFile(file: any) {
// TODO: 调用文件解读API
ElMessage.info('正在解读文件...');
}
function viewAnalysis(file: any) {
// TODO: 查看文件解读结果
}
function deleteFile(file: any) {
// TODO: 删除文件
ElMessage.success('文件已删除');
}
</script>
<style lang="scss" scoped>
.file-interpretation {
min-height: 400px;
}
.upload-section {
padding: 20px;
}
.upload-icon {
font-size: 64px;
margin-bottom: 16px;
}
.upload-text {
p {
margin: 8px 0;
font-size: 14px;
color: #666;
}
}
.upload-hint {
font-size: 12px;
color: #999;
}
.uploaded-files {
margin-top: 32px;
h4 {
font-size: 16px;
font-weight: 600;
color: #141F38;
margin-bottom: 16px;
}
}
.file-list,
.history-files {
display: flex;
flex-direction: column;
gap: 12px;
}
.file-item {
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
background: #f9f9f9;
border-radius: 8px;
}
.file-icon {
font-size: 32px;
flex-shrink: 0;
}
.file-info {
flex: 1;
h5 {
font-size: 15px;
font-weight: 600;
color: #141F38;
margin-bottom: 4px;
}
p {
font-size: 13px;
color: #666;
margin: 2px 0;
}
}
.file-summary {
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid #e0e0e0;
}
.file-actions {
display: flex;
gap: 8px;
flex-shrink: 0;
}
</style>

View File

@@ -0,0 +1,147 @@
<template>
<div class="knowledge-base">
<div class="knowledge-header">
<el-input
v-model="searchKeyword"
placeholder="搜索知识库..."
prefix-icon="Search"
clearable
/>
</div>
<div class="knowledge-list">
<div
class="knowledge-item"
v-for="item in filteredKnowledge"
:key="item.id"
@click="viewKnowledge(item)"
>
<div class="item-icon">{{ item.icon }}</div>
<div class="item-info">
<h4>{{ item.title }}</h4>
<p>{{ item.description }}</p>
<div class="item-meta">
<span class="item-category">{{ item.category }}</span>
<span class="item-date">{{ item.updateDate }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { ElInput } from 'element-plus';
const searchKeyword = ref('');
const knowledgeList = ref<any[]>([]);
const filteredKnowledge = computed(() => {
if (!searchKeyword.value) return knowledgeList.value;
return knowledgeList.value.filter(item =>
item.title.includes(searchKeyword.value) ||
item.description.includes(searchKeyword.value)
);
});
onMounted(() => {
// TODO: 加载知识库数据
knowledgeList.value = [
{
id: 1,
icon: '📚',
title: '党的二十大精神',
description: '深入学习党的二十大精神要点',
category: '党史学习',
updateDate: '2024-01-15'
},
{
id: 2,
icon: '🎯',
title: '社会主义核心价值观',
description: '践行社会主义核心价值观',
category: '理论学习',
updateDate: '2024-01-10'
}
];
});
function viewKnowledge(item: any) {
// TODO: 查看知识详情
}
</script>
<style lang="scss" scoped>
.knowledge-base {
height: 100%;
display: flex;
flex-direction: column;
}
.knowledge-header {
padding: 16px;
border-bottom: 1px solid #e0e0e0;
}
.knowledge-list {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.knowledge-item {
display: flex;
gap: 12px;
padding: 16px;
background: #f9f9f9;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
&:hover {
background: #ffe6e6;
transform: translateX(4px);
}
}
.item-icon {
font-size: 32px;
flex-shrink: 0;
}
.item-info {
flex: 1;
h4 {
font-size: 15px;
font-weight: 600;
color: #141F38;
margin-bottom: 6px;
}
p {
font-size: 13px;
color: #666;
margin-bottom: 8px;
}
}
.item-meta {
display: flex;
gap: 12px;
font-size: 12px;
}
.item-category {
color: #C62828;
}
.item-date {
color: #999;
}
</style>

View File

@@ -0,0 +1,39 @@
<template>
<div class="home-page">
<!-- 个人学习数据概览 -->
<LearningDataOverview />
<!-- 书报馆件 -->
<BookHallSection />
<!-- TOP音乐推荐 -->
<TopMusicRecommend />
<!-- 新闻概览 -->
<NewsOverview />
<!-- 导航栏 -->
<NavigationBar />
<!-- 破破栏索 -->
<SearchIndex />
</div>
</template>
<script setup lang="ts">
import LearningDataOverview from './components/LearningDataOverview.vue';
import BookHallSection from './components/BookHallSection.vue';
import TopMusicRecommend from './components/TopMusicRecommend.vue';
import NewsOverview from './components/NewsOverview.vue';
import NavigationBar from './components/NavigationBar.vue';
import SearchIndex from './components/SearchIndex.vue';
</script>
<style lang="scss" scoped>
.home-page {
width: 100%;
min-height: 100vh;
background: #f5f5f5;
}
</style>

View File

@@ -0,0 +1,88 @@
<template>
<div class="book-hall-section">
<h2 class="section-title">书报馆件</h2>
<div class="book-list">
<div class="book-item" v-for="book in books" :key="book.id">
<div class="book-cover">
<img :src="book.cover" :alt="book.title" />
</div>
<div class="book-info">
<h3>{{ book.title }}</h3>
<p>{{ book.author }}</p>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
const books = ref<any[]>([]);
onMounted(() => {
// TODO: 加载书籍数据
});
</script>
<style lang="scss" scoped>
.book-hall-section {
padding: 40px;
background: white;
border-radius: 8px;
margin: 20px;
}
.section-title {
font-size: 24px;
font-weight: 600;
color: #141F38;
margin-bottom: 24px;
}
.book-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 20px;
}
.book-item {
cursor: pointer;
transition: transform 0.3s;
&:hover {
transform: translateY(-5px);
}
}
.book-cover {
width: 100%;
height: 280px;
background: #f5f5f5;
border-radius: 8px;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.book-info {
margin-top: 12px;
h3 {
font-size: 16px;
font-weight: 500;
color: #141F38;
margin-bottom: 4px;
}
p {
font-size: 14px;
color: #666;
}
}
</style>

View File

@@ -0,0 +1,77 @@
<template>
<div class="learning-data-overview">
<h2 class="section-title">个人学习数据概览</h2>
<div class="data-cards">
<div class="data-card" v-for="item in dataItems" :key="item.label">
<div class="card-icon">{{ item.icon }}</div>
<div class="card-content">
<div class="card-value">{{ item.value }}</div>
<div class="card-label">{{ item.label }}</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
const dataItems = ref([
{ icon: '📚', label: '学习时长', value: '0小时' },
{ icon: '✅', label: '完成任务', value: '0个' },
{ icon: '⭐', label: '获得成就', value: '0个' },
{ icon: '📖', label: '阅读文章', value: '0篇' }
]);
onMounted(() => {
// TODO: 加载用户学习数据
});
</script>
<style lang="scss" scoped>
.learning-data-overview {
padding: 40px;
background: white;
border-radius: 8px;
margin: 20px;
}
.section-title {
font-size: 24px;
font-weight: 600;
color: #141F38;
margin-bottom: 24px;
}
.data-cards {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
}
.data-card {
display: flex;
align-items: center;
padding: 20px;
background: #f5f5f5;
border-radius: 8px;
gap: 16px;
}
.card-icon {
font-size: 32px;
}
.card-value {
font-size: 28px;
font-weight: 600;
color: #C62828;
}
.card-label {
font-size: 14px;
color: #666;
margin-top: 4px;
}
</style>

View File

@@ -0,0 +1,69 @@
<template>
<div class="navigation-bar">
<div class="nav-item" v-for="item in navItems" :key="item.id" @click="navigate(item)">
<div class="nav-icon">{{ item.icon }}</div>
<div class="nav-label">{{ item.label }}</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const navItems = ref([
{ id: 1, icon: '📚', label: '资源中心', path: '/resource-center' },
{ id: 2, icon: '📝', label: '学习计划', path: '/study-plan' },
{ id: 3, icon: '👤', label: '个人中心', path: '/user-center' },
{ id: 4, icon: '🤖', label: 'AI助手', path: '/ai-assistant' }
]);
function navigate(item: any) {
router.push(item.path);
}
</script>
<style lang="scss" scoped>
.navigation-bar {
display: flex;
justify-content: center;
gap: 40px;
padding: 40px;
background: white;
border-radius: 8px;
margin: 20px;
}
.nav-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
cursor: pointer;
transition: transform 0.3s;
&:hover {
transform: translateY(-5px);
}
}
.nav-icon {
width: 64px;
height: 64px;
background: linear-gradient(135deg, #C62828, #E53935);
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
}
.nav-label {
font-size: 16px;
font-weight: 500;
color: #141F38;
}
</style>

View File

@@ -0,0 +1,115 @@
<template>
<div class="news-overview">
<h2 class="section-title">新闻概览</h2>
<div class="news-list">
<div class="news-item" v-for="news in newsList" :key="news.id" @click="goToDetail(news)">
<div class="news-image">
<img :src="news.image" :alt="news.title" />
</div>
<div class="news-content">
<h3>{{ news.title }}</h3>
<p class="news-summary">{{ news.summary }}</p>
<div class="news-meta">
<span class="news-date">{{ news.publishDate }}</span>
<span class="news-views">{{ news.views }} 阅读</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const newsList = ref<any[]>([]);
onMounted(() => {
// TODO: 加载新闻数据
});
function goToDetail(news: any) {
router.push(`/news/${news.id}`);
}
</script>
<style lang="scss" scoped>
.news-overview {
padding: 40px;
background: white;
border-radius: 8px;
margin: 20px;
}
.section-title {
font-size: 24px;
font-weight: 600;
color: #141F38;
margin-bottom: 24px;
}
.news-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.news-item {
display: flex;
gap: 16px;
padding: 16px;
border-radius: 8px;
cursor: pointer;
transition: background 0.3s;
&:hover {
background: #f5f5f5;
}
}
.news-image {
width: 200px;
height: 140px;
flex-shrink: 0;
background: #f5f5f5;
border-radius: 8px;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.news-content {
flex: 1;
display: flex;
flex-direction: column;
h3 {
font-size: 18px;
font-weight: 600;
color: #141F38;
margin-bottom: 8px;
}
}
.news-summary {
font-size: 14px;
color: #666;
line-height: 1.6;
margin-bottom: auto;
}
.news-meta {
display: flex;
gap: 16px;
font-size: 12px;
color: #999;
margin-top: 12px;
}
</style>

View File

@@ -0,0 +1,69 @@
<template>
<div class="search-index">
<h2 class="section-title">搜索索引</h2>
<div class="search-tags">
<span
class="tag"
v-for="tag in popularTags"
:key="tag"
@click="handleTagClick(tag)"
>
{{ tag }}
</span>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const popularTags = ref([
'红色思政', '党史学习', '新时代精神', '爱国主义',
'社会主义核心价值观', '中国梦', '改革开放', '脱贫攻坚'
]);
function handleTagClick(tag: string) {
router.push(`/search?keyword=${encodeURIComponent(tag)}`);
}
</script>
<style lang="scss" scoped>
.search-index {
padding: 40px;
background: white;
border-radius: 8px;
margin: 20px;
}
.section-title {
font-size: 24px;
font-weight: 600;
color: #141F38;
margin-bottom: 24px;
}
.search-tags {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.tag {
padding: 8px 16px;
background: #f5f5f5;
border-radius: 20px;
font-size: 14px;
color: #666;
cursor: pointer;
transition: all 0.3s;
&:hover {
background: #C62828;
color: white;
}
}
</style>

View File

@@ -0,0 +1,106 @@
<template>
<div class="top-music-recommend">
<h2 class="section-title">TOP音乐推荐</h2>
<div class="music-list">
<div class="music-item" v-for="music in musicList" :key="music.id">
<div class="music-cover">
<img :src="music.cover" :alt="music.title" />
<div class="play-btn"></div>
</div>
<div class="music-info">
<h3>{{ music.title }}</h3>
<p>{{ music.artist }}</p>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
const musicList = ref<any[]>([]);
onMounted(() => {
// TODO: 加载音乐推荐数据
});
</script>
<style lang="scss" scoped>
.top-music-recommend {
padding: 40px;
background: white;
border-radius: 8px;
margin: 20px;
}
.section-title {
font-size: 24px;
font-weight: 600;
color: #141F38;
margin-bottom: 24px;
}
.music-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 20px;
}
.music-item {
cursor: pointer;
}
.music-cover {
position: relative;
width: 100%;
aspect-ratio: 1;
background: #f5f5f5;
border-radius: 8px;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
.play-btn {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 48px;
height: 48px;
background: rgba(198, 40, 40, 0.9);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
opacity: 0;
transition: opacity 0.3s;
}
&:hover .play-btn {
opacity: 1;
}
}
.music-info {
margin-top: 12px;
h3 {
font-size: 16px;
font-weight: 500;
color: #141F38;
margin-bottom: 4px;
}
p {
font-size: 14px;
color: #666;
}
}
</style>

View File

@@ -1,85 +1,94 @@
<template>
<div class="login-container">
<div class="login-box">
<div class="login-header">
<div class="logo">
<img src="@/assets/logo.png" alt="Logo" />
<!-- 左侧励志区域 -->
<div class="login-left">
<div class="left-content">
<div class="quote-text">
<span class="quote-mark"></span>
<div class="quote-content">
<p>不负时代韶华</p>
<p>争做时代新人</p>
</div>
</div>
<h1 class="title">校园新闻管理系统</h1>
<p class="subtitle">登录您的账户</p>
</div>
<el-form
ref="loginFormRef"
:model="loginForm"
:rules="loginRules"
class="login-form"
size="large"
@submit.prevent="handleLogin"
>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
placeholder="请输入用户名"
prefix-icon="User"
clearable
/>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
placeholder="请输入密码"
prefix-icon="Lock"
show-password
clearable
/>
</el-form-item>
<!-- <el-form-item prop="captcha" v-if="showCaptcha">
<div class="captcha-input">
<el-input
v-model="loginForm.captcha"
placeholder="请输入验证码"
prefix-icon="PictureRounded"
clearable
/>
<div class="captcha-image" @click="refreshCaptcha">
<img :src="captchaImage" alt="验证码" v-if="captchaImage" />
<div class="captcha-loading" v-else>加载中...</div>
</div>
<!-- 右侧登录表单区域 -->
<div class="login-right">
<div class="login-form-container">
<!-- Logo和标题区域 -->
<div class="login-header">
<div class="logo-section">
<div class="logo">
<img src="@/assets/imgs/logo-icon.svg" alt="Logo" />
</div>
<h1 class="platform-title">红色思政学习平台</h1>
</div>
</el-form-item> -->
<h2 class="login-title">账号登陆</h2>
</div>
<el-form-item>
<div class="login-options">
<el-checkbox v-model="loginForm.rememberMe">
记住我
</el-checkbox>
<el-link type="primary" @click="goToForgotPassword">
忘记密码
</el-link>
</div>
</el-form-item>
<!-- 登录表单 -->
<el-form
ref="loginFormRef"
:model="loginForm"
:rules="loginRules"
class="login-form"
@submit.prevent="handleLogin"
>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
placeholder="请输入学号"
class="form-input"
/>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
placeholder="请输入密码"
show-password
class="form-input"
/>
</el-form-item>
<el-form-item>
<div class="login-options">
<div class="remember-me">
<el-checkbox v-model="loginForm.rememberMe">
自动登录
</el-checkbox>
</div>
<el-link type="primary" @click="goToForgotPassword" class="forgot-password">
忘记密码
</el-link>
</div>
</el-form-item>
<el-form-item>
<el-button
type="primary"
:loading="loginLoading"
@click="handleLogin"
class="login-button"
>
登录
</el-button>
<p class="agreement-text">
登录即为同意<span class="agreement-link" style="color: red">红色思政智能体平台</span>
</p>
</el-form-item>
</el-form>
<el-form-item>
<el-button
type="primary"
size="large"
:loading="loginLoading"
@click="handleLogin"
class="login-button"
>
登录
</el-button>
</el-form-item>
</el-form>
</div>
<!-- 底部信息 -->
<div class="login-footer">
<p>
没有账
<el-link type="primary" @click="goToRegister">立即注册</el-link>
<p class="register-link">
没有账<span class="register-link-text" @click="goToRegister">现在就注册</span>
</p>
<p class="copyright">
Copyright ©红色思政智能体平台
</p>
</div>
</div>
@@ -163,9 +172,11 @@ const handleLogin = async () => {
const refreshCaptcha = async () => {
try {
const captchaData = await authApi.getCaptcha();
captchaImage.value = captchaData.captchaImage;
loginForm.captchaId = captchaData.captchaId;
const result = await authApi.getCaptcha();
if (result.data) {
captchaImage.value = result.data.captchaImage;
loginForm.captchaId = result.data.captchaId;
}
} catch (error) {
console.error('获取验证码失败:', error);
ElMessage.error('获取验证码失败');
@@ -189,75 +200,146 @@ onMounted(() => {
<style lang="scss" scoped>
.login-container {
min-height: 100vh;
display: flex;
align-items: center;
width: 100%;
height: 80%;
max-width: 1142px;
margin: auto auto;
box-shadow: 0px 4px 30px 0px rgba(176, 196, 225, 0.25);
border-radius: 30px;
overflow: hidden;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
align-items: center;
}
.login-box {
width: 100%;
max-width: 400px;
background: white;
border-radius: 12px;
padding: 40px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
.login-left {
height: 100%;
flex: 1;
display: flex;
/* min-height: 671px; */
padding: 113px 120px;
border-radius: 30px 0 0 30px;
overflow: hidden;
background: url(/schoolNewsWeb/src/assets/imgs/login-bg.png);
background-size: cover;
background-position: center;
background-repeat: no-repeat;
.left-content {
width: 100%;
max-width: 350px;
}
.quote-text {
color: #FFF2D3;
.quote-mark {
font-family: 'Arial Black', sans-serif;
font-weight: 900;
font-size: 71.096px;
line-height: 0.74;
display: block;
margin-left: 1.48px;
}
.quote-content {
margin-top: 46.66px;
p {
font-family: 'Taipei Sans TC Beta', 'PingFang SC', sans-serif;
font-weight: 700;
font-size: 50px;
line-height: 1.42;
margin: 0;
}
}
}
}
.login-right {
flex: 1;
background: #FFFFFF;
display: flex;
flex-direction: column;
align-items: center;
height: 100%;
border-radius: 0 30px 30px 0;
padding: 40px 0;
}
.login-form-container {
width: 287px;
padding: 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.login-header {
text-align: center;
margin-bottom: 32px;
margin-bottom: 24px;
.logo img {
width: 64px;
height: 64px;
.logo-section {
display: flex;
align-items: center;
gap: 11px;
margin-bottom: 16px;
.logo {
width: 36px;
height: 36px;
background: #C62828;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
padding: 3px;
img {
width: 30px;
height: 30px;
}
}
.platform-title {
font-family: 'Taipei Sans TC Beta', sans-serif;
font-weight: 700;
font-size: 26px;
line-height: 1.31;
color: #141F38;
margin: 0;
}
}
.title {
font-size: 24px;
.login-title {
font-family: 'PingFang SC', sans-serif;
font-weight: 600;
color: #333;
margin: 0 0 8px 0;
}
.subtitle {
color: #666;
font-size: 14px;
font-size: 16px;
line-height: 1.5;
color: #141F38;
margin: 0;
}
}
.login-form {
.captcha-input {
display: flex;
gap: 12px;
.form-input {
width: 100%;
height: 48px;
background: #F2F3F5;
border: none;
border-radius: 12px;
font-size: 14px;
color: rgba(0, 0, 0, 0.3);
.el-input {
flex: 1;
&::placeholder {
color: rgba(0, 0, 0, 0.3);
}
.captcha-image {
width: 100px;
height: 40px;
border: 1px solid #dcdfe6;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
img {
max-width: 100%;
max-height: 100%;
}
.captcha-loading {
font-size: 12px;
color: #999;
}
&:focus {
outline: none;
background: #FFFFFF;
border: 1px solid #C62828;
}
}
@@ -266,21 +348,130 @@ onMounted(() => {
justify-content: space-between;
align-items: center;
width: 100%;
margin-bottom: 16px;
.remember-me {
display: flex;
align-items: center;
gap: 4px;
.el-checkbox {
font-size: 12px;
color: rgba(0, 0, 0, 0.3);
.el-checkbox__label {
font-size: 12px;
line-height: 1.67;
}
}
}
.forgot-password {
font-size: 12px;
color: rgba(0, 0, 0, 0.3);
text-decoration: none;
&:hover {
color: #C62828;
}
}
}
.login-button {
width: 100%;
height: 46px;
background: #C62828;
border: none;
border-radius: 12px;
color: #FFFFFF;
font-family: 'PingFang SC', sans-serif;
font-weight: 500;
font-size: 16px;
line-height: 1.4;
margin-bottom: 8px;
&:hover {
background: #B71C1C;
}
}
.agreement-text {
font-family: 'PingFang SC', sans-serif;
font-weight: 400;
font-size: 10px;
line-height: 1.8;
color: rgba(0, 0, 0, 0.3);
text-align: center;
margin: 0;
}
}
.login-footer {
text-align: center;
margin-top: 24px;
width: 100%;
p {
color: #666;
font-size: 14px;
margin: 0;
.register-link {
font-family: 'PingFang SC', sans-serif;
font-weight: 600;
font-size: 12px;
line-height: 1.83;
color: #141F38;
text-align: center;
margin: 0 0 16px 0;
.register-link-text {
cursor: pointer;
color: #ff0000;
&:hover {
color: #C62828;
}
}
}
.copyright {
font-family: 'PingFang SC', sans-serif;
font-size: 12px;
line-height: 2;
color: #D9D9D9;
text-align: center;
padding-bottom: 0;
}
}
// 响应式设计
@media (max-width: 768px) {
.login-container {
flex-direction: column;
border-radius: 0;
min-height: 100vh;
}
.login-left {
min-height: 300px;
padding: 40px;
.left-content {
max-width: 100%;
}
.quote-text {
.quote-mark {
font-size: 50px;
}
.quote-content p {
font-size: 36px;
}
}
}
.login-right {
min-height: auto;
padding: 20px;
}
.login-form-container {
width: 100%;
max-width: 400px;
}
}
</style>

View File

@@ -0,0 +1,160 @@
<template>
<div class="account-settings">
<div class="settings-section">
<h3>修改密码</h3>
<el-form :model="passwordForm" :rules="passwordRules" ref="passwordFormRef" label-width="120px">
<el-form-item label="当前密码" prop="oldPassword">
<el-input v-model="passwordForm.oldPassword" type="password" show-password />
</el-form-item>
<el-form-item label="新密码" prop="newPassword">
<el-input v-model="passwordForm.newPassword" type="password" show-password />
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input v-model="passwordForm.confirmPassword" type="password" show-password />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleChangePassword">修改密码</el-button>
</el-form-item>
</el-form>
</div>
<div class="settings-section">
<h3>账号安全</h3>
<div class="security-items">
<div class="security-item">
<div class="item-info">
<i class="icon">📱</i>
<div>
<h4>手机绑定</h4>
<p>已绑定手机138****8888</p>
</div>
</div>
<el-button size="small">修改</el-button>
</div>
<div class="security-item">
<div class="item-info">
<i class="icon"></i>
<div>
<h4>邮箱绑定</h4>
<p>已绑定邮箱user@example.com</p>
</div>
</div>
<el-button size="small">修改</el-button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { ElForm, ElFormItem, ElInput, ElButton, ElMessage, type FormInstance, type FormRules } from 'element-plus';
const passwordFormRef = ref<FormInstance>();
const passwordForm = ref({
oldPassword: '',
newPassword: '',
confirmPassword: ''
});
const passwordRules: FormRules = {
oldPassword: [
{ required: true, message: '请输入当前密码', trigger: 'blur' }
],
newPassword: [
{ required: true, message: '请输入新密码', trigger: 'blur' },
{ min: 6, message: '密码至少6个字符', trigger: 'blur' }
],
confirmPassword: [
{ required: true, message: '请确认新密码', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (value !== passwordForm.value.newPassword) {
callback(new Error('两次输入的密码不一致'));
} else {
callback();
}
},
trigger: 'blur'
}
]
};
async function handleChangePassword() {
if (!passwordFormRef.value) return;
try {
await passwordFormRef.value.validate();
// TODO: 调用修改密码API
ElMessage.success('密码修改成功');
passwordFormRef.value.resetFields();
} catch (error) {
console.error('表单验证失败', error);
}
}
</script>
<style lang="scss" scoped>
.account-settings {
max-width: 600px;
}
.settings-section {
padding: 24px 0;
border-bottom: 1px solid #e0e0e0;
&:last-child {
border-bottom: none;
}
h3 {
font-size: 20px;
font-weight: 600;
color: #141F38;
margin-bottom: 24px;
}
}
.security-items {
display: flex;
flex-direction: column;
gap: 16px;
}
.security-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
background: #f9f9f9;
border-radius: 8px;
}
.item-info {
display: flex;
align-items: center;
gap: 16px;
.icon {
font-size: 32px;
}
h4 {
font-size: 16px;
font-weight: 600;
color: #141F38;
margin-bottom: 4px;
}
p {
font-size: 14px;
color: #666;
}
}
</style>

View File

@@ -0,0 +1,112 @@
<template>
<div class="personal-info">
<el-form :model="userForm" label-width="120px" class="info-form">
<el-form-item label="头像">
<div class="avatar-upload">
<img :src="userForm.avatar" alt="头像" class="avatar-preview" />
<el-button size="small" @click="handleAvatarUpload">更换头像</el-button>
</div>
</el-form-item>
<el-form-item label="用户名">
<el-input v-model="userForm.username" disabled />
</el-form-item>
<el-form-item label="姓名">
<el-input v-model="userForm.realName" />
</el-form-item>
<el-form-item label="性别">
<el-radio-group v-model="userForm.gender">
<el-radio :label="1"></el-radio>
<el-radio :label="2"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="userForm.phone" />
</el-form-item>
<el-form-item label="邮箱">
<el-input v-model="userForm.email" />
</el-form-item>
<el-form-item label="部门">
<el-input v-model="userForm.deptName" disabled />
</el-form-item>
<el-form-item label="个人简介">
<el-input
v-model="userForm.bio"
type="textarea"
:rows="4"
placeholder="介绍一下自己吧..."
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSave">保存</el-button>
<el-button @click="handleCancel">取消</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ElForm, ElFormItem, ElInput, ElButton, ElRadio, ElRadioGroup, ElMessage } from 'element-plus';
const userForm = ref({
avatar: '',
username: '',
realName: '',
gender: 1,
phone: '',
email: '',
deptName: '',
bio: ''
});
onMounted(() => {
// TODO: 加载用户信息
});
function handleAvatarUpload() {
// TODO: 上传头像
ElMessage.info('上传头像功能开发中');
}
function handleSave() {
// TODO: 保存用户信息
ElMessage.success('保存成功');
}
function handleCancel() {
// TODO: 重置表单
}
</script>
<style lang="scss" scoped>
.personal-info {
max-width: 600px;
}
.info-form {
padding: 20px 0;
}
.avatar-upload {
display: flex;
align-items: center;
gap: 20px;
}
.avatar-preview {
width: 80px;
height: 80px;
border-radius: 50%;
object-fit: cover;
border: 2px solid #e0e0e0;
}
</style>

View File

@@ -0,0 +1,55 @@
<template>
<div class="profile-page">
<div class="profile-container">
<h1 class="page-title">我导中心</h1>
<el-tabs v-model="activeTab" class="profile-tabs">
<el-tab-pane label="个人信息" name="info">
<PersonalInfo />
</el-tab-pane>
<el-tab-pane label="账号设置" name="settings">
<AccountSettings />
</el-tab-pane>
</el-tabs>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { ElTabs, ElTabPane } from 'element-plus';
import PersonalInfo from './PersonalInfoView.vue';
import AccountSettings from './AccountSettingsView.vue';
const activeTab = ref('info');
</script>
<style lang="scss" scoped>
.profile-page {
min-height: 100vh;
background: #f5f5f5;
padding: 20px;
}
.profile-container {
max-width: 1200px;
margin: 0 auto;
background: white;
padding: 40px;
border-radius: 8px;
}
.page-title {
font-size: 32px;
font-weight: 600;
color: #141F38;
margin-bottom: 32px;
}
.profile-tabs {
:deep(.el-tabs__nav-wrap) {
margin-bottom: 24px;
}
}
</style>

View File

@@ -0,0 +1,167 @@
<template>
<div class="party-history-learning">
<h2 class="page-title">党史学习</h2>
<!-- 分类标签 -->
<div class="category-tabs">
<div
class="category-tab"
v-for="category in categories"
:key="category.id"
:class="{ active: activeCategory === category.id }"
@click="activeCategory = category.id"
>
{{ category.name }}
</div>
</div>
<!-- 文章列表 -->
<div class="article-list">
<div class="article-item" v-for="article in articles" :key="article.id" @click="viewArticle(article)">
<div class="article-image">
<img :src="article.image" :alt="article.title" />
</div>
<div class="article-content">
<h3>{{ article.title }}</h3>
<p class="article-summary">{{ article.summary }}</p>
<div class="article-meta">
<span class="author">{{ article.author }}</span>
<span class="date">{{ article.publishDate }}</span>
<span class="views">{{ article.views }} 阅读</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const activeCategory = ref(1);
const articles = ref<any[]>([]);
const categories = [
{ id: 1, name: '新民主主义革命时期' },
{ id: 2, name: '社会主义革命和建设时期' },
{ id: 3, name: '改革开放和社会主义现代化建设新时期' },
{ id: 4, name: '新时代中国特色社会主义' }
];
onMounted(() => {
// TODO: 加载党史学习文章
});
function viewArticle(article: any) {
router.push(`/resource/article/${article.id}`);
}
</script>
<style lang="scss" scoped>
.party-history-learning {
background: white;
padding: 40px;
border-radius: 8px;
}
.page-title {
font-size: 28px;
font-weight: 600;
color: #141F38;
margin-bottom: 32px;
}
.category-tabs {
display: flex;
gap: 12px;
margin-bottom: 32px;
flex-wrap: wrap;
}
.category-tab {
padding: 8px 20px;
background: #f5f5f5;
border-radius: 20px;
font-size: 14px;
color: #666;
cursor: pointer;
transition: all 0.3s;
&:hover {
background: #ffe6e6;
color: #C62828;
}
&.active {
background: #C62828;
color: white;
}
}
.article-list {
display: flex;
flex-direction: column;
gap: 20px;
}
.article-item {
display: flex;
gap: 20px;
padding: 20px;
border-radius: 8px;
cursor: pointer;
transition: background 0.3s;
border: 1px solid #e0e0e0;
&:hover {
background: #f9f9f9;
}
}
.article-image {
width: 240px;
height: 160px;
flex-shrink: 0;
background: #f5f5f5;
border-radius: 8px;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.article-content {
flex: 1;
display: flex;
flex-direction: column;
h3 {
font-size: 20px;
font-weight: 600;
color: #141F38;
margin-bottom: 12px;
}
}
.article-summary {
font-size: 14px;
color: #666;
line-height: 1.6;
margin-bottom: auto;
}
.article-meta {
display: flex;
gap: 20px;
font-size: 13px;
color: #999;
margin-top: 16px;
}
</style>

View File

@@ -0,0 +1,101 @@
<template>
<div class="policy-interpretation">
<h2 class="page-title">政策解读</h2>
<div class="interpretation-grid">
<div class="interpretation-item" v-for="item in interpretations" :key="item.id" @click="viewInterpretation(item)">
<div class="item-badge">{{ item.category }}</div>
<h3>{{ item.title }}</h3>
<p class="item-summary">{{ item.summary }}</p>
<div class="item-footer">
<span class="item-date">{{ item.publishDate }}</span>
<span class="item-views">{{ item.views }} 阅读</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const interpretations = ref<any[]>([]);
onMounted(() => {
// TODO: 加载政策解读数据
});
function viewInterpretation(item: any) {
router.push(`/resource/interpretation/${item.id}`);
}
</script>
<style lang="scss" scoped>
.policy-interpretation {
background: white;
padding: 40px;
border-radius: 8px;
}
.page-title {
font-size: 28px;
font-weight: 600;
color: #141F38;
margin-bottom: 32px;
}
.interpretation-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 24px;
}
.interpretation-item {
padding: 24px;
border: 1px solid #e0e0e0;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
&:hover {
border-color: #C62828;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
}
.item-badge {
display: inline-block;
padding: 4px 12px;
background: #ffe6e6;
color: #C62828;
border-radius: 4px;
font-size: 12px;
margin-bottom: 12px;
}
h3 {
font-size: 18px;
font-weight: 600;
color: #141F38;
margin-bottom: 12px;
line-height: 1.4;
}
.item-summary {
font-size: 14px;
color: #666;
line-height: 1.6;
margin-bottom: 16px;
}
.item-footer {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #999;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
}
</style>

View File

@@ -0,0 +1,119 @@
<template>
<div class="red-classic">
<h2 class="page-title">红色经典</h2>
<div class="classic-grid">
<div class="classic-item" v-for="item in classics" :key="item.id" @click="viewClassic(item)">
<div class="classic-cover">
<img :src="item.cover" :alt="item.title" />
<div class="classic-type">{{ item.type }}</div>
</div>
<div class="classic-info">
<h3>{{ item.title }}</h3>
<p class="classic-author">{{ item.author }}</p>
<p class="classic-description">{{ item.description }}</p>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const classics = ref<any[]>([]);
onMounted(() => {
// TODO: 加载红色经典数据
});
function viewClassic(item: any) {
router.push(`/resource/classic/${item.id}`);
}
</script>
<style lang="scss" scoped>
.red-classic {
background: white;
padding: 40px;
border-radius: 8px;
}
.page-title {
font-size: 28px;
font-weight: 600;
color: #141F38;
margin-bottom: 32px;
}
.classic-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 24px;
}
.classic-item {
cursor: pointer;
transition: transform 0.3s;
&:hover {
transform: translateY(-8px);
}
}
.classic-cover {
position: relative;
width: 100%;
height: 300px;
background: #f5f5f5;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.classic-type {
position: absolute;
top: 12px;
left: 12px;
padding: 4px 12px;
background: rgba(198, 40, 40, 0.9);
color: white;
border-radius: 4px;
font-size: 12px;
}
.classic-info {
margin-top: 12px;
h3 {
font-size: 16px;
font-weight: 600;
color: #141F38;
margin-bottom: 4px;
}
}
.classic-author {
font-size: 14px;
color: #999;
margin-bottom: 8px;
}
.classic-description {
font-size: 13px;
color: #666;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>

View File

@@ -0,0 +1,104 @@
<template>
<div class="resource-center-page">
<!-- 导航栏 -->
<div class="resource-nav">
<div
class="nav-tab"
v-for="tab in tabs"
:key="tab.key"
:class="{ active: activeTab === tab.key }"
@click="activeTab = tab.key"
>
{{ tab.label }}
</div>
</div>
<!-- 内容区域 -->
<div class="resource-content">
<component :is="currentComponent" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import MediaArchive from './components/MediaArchive.vue';
import PartyHistoryLearning from './PartyHistoryLearningView.vue';
import PolicySpeech from './components/PolicySpeech.vue';
import PolicyInterpretation from './PolicyInterpretationView.vue';
import RedClassic from './RedClassicView.vue';
import SpecialReport from './SpecialReportView.vue';
import WorldCase from './WorldCaseView.vue';
const activeTab = ref('media');
const tabs = [
{ key: 'media', label: '媒体档案' },
{ key: 'party-history', label: '党史学习' },
{ key: 'policy-speech', label: '政策讲话' },
{ key: 'policy-interpretation', label: '政策解读' },
{ key: 'red-classic', label: '红色经典' },
{ key: 'special-report', label: '专题报告' },
{ key: 'world-case', label: '世界案例' }
];
const componentMap: Record<string, any> = {
'media': MediaArchive,
'party-history': PartyHistoryLearning,
'policy-speech': PolicySpeech,
'policy-interpretation': PolicyInterpretation,
'red-classic': RedClassic,
'special-report': SpecialReport,
'world-case': WorldCase
};
const currentComponent = computed(() => componentMap[activeTab.value]);
</script>
<style lang="scss" scoped>
.resource-center-page {
min-height: 100vh;
background: #f5f5f5;
}
.resource-nav {
background: white;
padding: 0 40px;
display: flex;
gap: 8px;
border-bottom: 1px solid #e0e0e0;
}
.nav-tab {
padding: 16px 24px;
cursor: pointer;
font-size: 16px;
color: #666;
position: relative;
transition: all 0.3s;
&:hover {
color: #C62828;
}
&.active {
color: #C62828;
font-weight: 600;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 3px;
background: #C62828;
}
}
}
.resource-content {
padding: 20px;
}
</style>

View File

@@ -0,0 +1,135 @@
<template>
<div class="special-report">
<h2 class="page-title">专题报告</h2>
<div class="report-list">
<div class="report-item" v-for="report in reports" :key="report.id" @click="viewReport(report)">
<div class="report-banner">
<img :src="report.banner" :alt="report.title" />
</div>
<div class="report-content">
<h3>{{ report.title }}</h3>
<div class="report-tags">
<span class="tag" v-for="tag in report.tags" :key="tag">{{ tag }}</span>
</div>
<p class="report-summary">{{ report.summary }}</p>
<div class="report-footer">
<span class="report-speaker">主讲人{{ report.speaker }}</span>
<span class="report-date">{{ report.date }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const reports = ref<any[]>([]);
onMounted(() => {
// TODO: 加载专题报告数据
});
function viewReport(report: any) {
router.push(`/resource/report/${report.id}`);
}
</script>
<style lang="scss" scoped>
.special-report {
background: white;
padding: 40px;
border-radius: 8px;
}
.page-title {
font-size: 28px;
font-weight: 600;
color: #141F38;
margin-bottom: 32px;
}
.report-list {
display: flex;
flex-direction: column;
gap: 24px;
}
.report-item {
display: flex;
gap: 24px;
padding: 20px;
border: 1px solid #e0e0e0;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
&:hover {
border-color: #C62828;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
}
.report-banner {
width: 320px;
height: 200px;
flex-shrink: 0;
background: #f5f5f5;
border-radius: 8px;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.report-content {
flex: 1;
display: flex;
flex-direction: column;
h3 {
font-size: 22px;
font-weight: 600;
color: #141F38;
margin-bottom: 12px;
}
}
.report-tags {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.tag {
padding: 4px 12px;
background: #f5f5f5;
border-radius: 4px;
font-size: 12px;
color: #666;
}
.report-summary {
font-size: 14px;
color: #666;
line-height: 1.6;
margin-bottom: auto;
}
.report-footer {
display: flex;
justify-content: space-between;
padding-top: 16px;
margin-top: 16px;
border-top: 1px solid #f0f0f0;
font-size: 14px;
color: #999;
}
</style>

View File

@@ -0,0 +1,181 @@
<template>
<div class="world-case">
<h2 class="page-title">世界案例</h2>
<!-- 地区筛选 -->
<div class="region-filter">
<div
class="region-tab"
v-for="region in regions"
:key="region.id"
:class="{ active: activeRegion === region.id }"
@click="activeRegion = region.id"
>
{{ region.name }}
</div>
</div>
<!-- 案例列表 -->
<div class="case-grid">
<div class="case-item" v-for="caseItem in cases" :key="caseItem.id" @click="viewCase(caseItem)">
<div class="case-image">
<img :src="caseItem.image" :alt="caseItem.title" />
<div class="case-country">{{ caseItem.country }}</div>
</div>
<div class="case-content">
<h3>{{ caseItem.title }}</h3>
<p class="case-summary">{{ caseItem.summary }}</p>
<div class="case-footer">
<span class="case-category">{{ caseItem.category }}</span>
<span class="case-date">{{ caseItem.publishDate }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const activeRegion = ref('all');
const cases = ref<any[]>([]);
const regions = [
{ id: 'all', name: '全部' },
{ id: 'asia', name: '亚洲' },
{ id: 'europe', name: '欧洲' },
{ id: 'americas', name: '美洲' },
{ id: 'africa', name: '非洲' },
{ id: 'oceania', name: '大洋洲' }
];
onMounted(() => {
// TODO: 加载世界案例数据
});
function viewCase(caseItem: any) {
router.push(`/resource/case/${caseItem.id}`);
}
</script>
<style lang="scss" scoped>
.world-case {
background: white;
padding: 40px;
border-radius: 8px;
}
.page-title {
font-size: 28px;
font-weight: 600;
color: #141F38;
margin-bottom: 32px;
}
.region-filter {
display: flex;
gap: 12px;
margin-bottom: 32px;
}
.region-tab {
padding: 8px 20px;
background: #f5f5f5;
border-radius: 20px;
font-size: 14px;
color: #666;
cursor: pointer;
transition: all 0.3s;
&:hover {
background: #ffe6e6;
color: #C62828;
}
&.active {
background: #C62828;
color: white;
}
}
.case-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 24px;
}
.case-item {
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s;
&:hover {
border-color: #C62828;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
}
.case-image {
position: relative;
width: 100%;
height: 200px;
background: #f5f5f5;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.case-country {
position: absolute;
bottom: 12px;
right: 12px;
padding: 4px 12px;
background: rgba(198, 40, 40, 0.9);
color: white;
border-radius: 4px;
font-size: 12px;
}
.case-content {
padding: 20px;
h3 {
font-size: 18px;
font-weight: 600;
color: #141F38;
margin-bottom: 12px;
}
}
.case-summary {
font-size: 14px;
color: #666;
line-height: 1.6;
margin-bottom: 16px;
}
.case-footer {
display: flex;
justify-content: space-between;
padding-top: 12px;
border-top: 1px solid #f0f0f0;
font-size: 13px;
}
.case-category {
color: #C62828;
}
.case-date {
color: #999;
}
</style>

View File

@@ -0,0 +1,119 @@
<template>
<div class="media-archive">
<h2 class="page-title">媒体档案</h2>
<div class="media-grid">
<div class="media-item" v-for="item in mediaList" :key="item.id" @click="viewMedia(item)">
<div class="media-thumbnail">
<img :src="item.thumbnail" :alt="item.title" />
<div class="media-type">{{ item.type }}</div>
</div>
<div class="media-info">
<h3>{{ item.title }}</h3>
<p class="media-description">{{ item.description }}</p>
<div class="media-meta">
<span>{{ item.publishDate }}</span>
<span>{{ item.views }} 观看</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const mediaList = ref<any[]>([]);
onMounted(() => {
// TODO: 加载媒体档案数据
});
function viewMedia(item: any) {
router.push(`/resource/media/${item.id}`);
}
</script>
<style lang="scss" scoped>
.media-archive {
background: white;
padding: 40px;
border-radius: 8px;
}
.page-title {
font-size: 28px;
font-weight: 600;
color: #141F38;
margin-bottom: 32px;
}
.media-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 24px;
}
.media-item {
cursor: pointer;
transition: transform 0.3s;
&:hover {
transform: translateY(-5px);
}
}
.media-thumbnail {
position: relative;
width: 100%;
height: 200px;
background: #f5f5f5;
border-radius: 8px;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.media-type {
position: absolute;
top: 12px;
right: 12px;
padding: 4px 12px;
background: rgba(198, 40, 40, 0.9);
color: white;
border-radius: 4px;
font-size: 12px;
}
.media-info {
margin-top: 12px;
h3 {
font-size: 16px;
font-weight: 600;
color: #141F38;
margin-bottom: 8px;
}
}
.media-description {
font-size: 14px;
color: #666;
line-height: 1.5;
margin-bottom: 12px;
}
.media-meta {
display: flex;
gap: 16px;
font-size: 12px;
color: #999;
}
</style>

View File

@@ -0,0 +1,101 @@
<template>
<div class="policy-speech">
<h2 class="page-title">政策讲话</h2>
<div class="speech-list">
<div class="speech-item" v-for="speech in speeches" :key="speech.id" @click="viewSpeech(speech)">
<div class="speech-header">
<h3>{{ speech.title }}</h3>
<span class="speech-date">{{ speech.date }}</span>
</div>
<div class="speech-info">
<span class="speaker">讲话人{{ speech.speaker }}</span>
<span class="occasion">场合{{ speech.occasion }}</span>
</div>
<p class="speech-summary">{{ speech.summary }}</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const speeches = ref<any[]>([]);
onMounted(() => {
// TODO: 加载政策讲话数据
});
function viewSpeech(speech: any) {
router.push(`/resource/speech/${speech.id}`);
}
</script>
<style lang="scss" scoped>
.policy-speech {
background: white;
padding: 40px;
border-radius: 8px;
}
.page-title {
font-size: 28px;
font-weight: 600;
color: #141F38;
margin-bottom: 32px;
}
.speech-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.speech-item {
padding: 24px;
border: 1px solid #e0e0e0;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
&:hover {
border-color: #C62828;
box-shadow: 0 2px 8px rgba(198, 40, 40, 0.1);
}
}
.speech-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
h3 {
font-size: 18px;
font-weight: 600;
color: #141F38;
}
}
.speech-date {
font-size: 14px;
color: #999;
}
.speech-info {
display: flex;
gap: 24px;
margin-bottom: 12px;
font-size: 14px;
color: #666;
}
.speech-summary {
font-size: 14px;
color: #666;
line-height: 1.6;
}
</style>

View File

@@ -0,0 +1,227 @@
<template>
<div class="course-center">
<div class="course-header">
<h2>课程中心</h2>
<div class="course-categories">
<div
class="category-tab"
v-for="category in categories"
:key="category.id"
:class="{ active: activeCategory === category.id }"
@click="activeCategory = category.id"
>
{{ category.name }}
</div>
</div>
</div>
<div class="course-grid">
<div class="course-card" v-for="course in filteredCourses" :key="course.id" @click="viewCourse(course)">
<div class="course-cover">
<img :src="course.cover" :alt="course.title" />
<div class="course-duration">{{ course.duration }}</div>
</div>
<div class="course-info">
<h3>{{ course.title }}</h3>
<p class="course-description">{{ course.description }}</p>
<div class="course-meta">
<span class="course-teacher">
<i class="icon">👨🏫</i>
{{ course.teacher }}
</span>
<span class="course-students">
<i class="icon">👥</i>
{{ course.students }} 人学习
</span>
</div>
<div class="course-footer">
<div class="course-rating">
<span class="rating-stars"></span>
<span class="rating-score">{{ course.rating }}</span>
</div>
<el-button type="primary" size="small">开始学习</el-button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { ElButton } from 'element-plus';
const router = useRouter();
const activeCategory = ref('all');
const courses = ref<any[]>([]);
const categories = [
{ id: 'all', name: '全部课程' },
{ id: 'party-history', name: '党史教育' },
{ id: 'theory', name: '理论学习' },
{ id: 'policy', name: '政策解读' },
{ id: 'ethics', name: '道德修养' }
];
const filteredCourses = computed(() => {
if (activeCategory.value === 'all') return courses.value;
return courses.value.filter(course => course.category === activeCategory.value);
});
onMounted(() => {
// TODO: 加载课程数据
});
function viewCourse(course: any) {
router.push(`/study/course/${course.id}`);
}
</script>
<style lang="scss" scoped>
.course-center {
padding: 40px;
}
.course-header {
margin-bottom: 32px;
h2 {
font-size: 24px;
font-weight: 600;
color: #141F38;
margin-bottom: 20px;
}
}
.course-categories {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.category-tab {
padding: 8px 20px;
background: #f5f5f5;
border-radius: 20px;
font-size: 14px;
color: #666;
cursor: pointer;
transition: all 0.3s;
&:hover {
background: #ffe6e6;
color: #C62828;
}
&.active {
background: #C62828;
color: white;
}
}
.course-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 24px;
}
.course-card {
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s;
&:hover {
border-color: #C62828;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-4px);
}
}
.course-cover {
position: relative;
width: 100%;
height: 200px;
background: #f5f5f5;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.course-duration {
position: absolute;
bottom: 12px;
right: 12px;
padding: 4px 12px;
background: rgba(0, 0, 0, 0.7);
color: white;
border-radius: 4px;
font-size: 12px;
}
.course-info {
padding: 20px;
h3 {
font-size: 18px;
font-weight: 600;
color: #141F38;
margin-bottom: 12px;
line-height: 1.4;
}
}
.course-description {
font-size: 14px;
color: #666;
line-height: 1.6;
margin-bottom: 16px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.course-meta {
display: flex;
gap: 20px;
margin-bottom: 16px;
font-size: 13px;
color: #999;
.icon {
margin-right: 4px;
}
}
.course-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
}
.course-rating {
display: flex;
align-items: center;
gap: 8px;
}
.rating-stars {
color: #FFB400;
font-size: 14px;
}
.rating-score {
font-size: 14px;
font-weight: 600;
color: #141F38;
}
</style>

View File

@@ -0,0 +1,113 @@
<template>
<div class="study-plan-page">
<div class="page-header">
<h1>学习计划</h1>
<p class="page-description">制定学习计划完成学习任务提升思政素养</p>
</div>
<div class="plan-tabs">
<div
class="plan-tab"
v-for="tab in tabs"
:key="tab.key"
:class="{ active: activeTab === tab.key }"
@click="activeTab = tab.key"
>
{{ tab.label }}
</div>
</div>
<div class="plan-content">
<component :is="currentComponent" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import StudyTasks from './components/StudyTasks.vue';
import CourseCenter from './components/CourseCenter.vue';
const activeTab = ref('tasks');
const tabs = [
{ key: 'tasks', label: '学习任务' },
{ key: 'courses', label: '课程中心' }
];
const componentMap: Record<string, any> = {
'tasks': StudyTasks,
'courses': CourseCenter
};
const currentComponent = computed(() => componentMap[activeTab.value]);
</script>
<style lang="scss" scoped>
.study-plan-page {
min-height: 100vh;
background: #f5f5f5;
padding: 20px;
}
.page-header {
background: white;
padding: 40px;
border-radius: 8px;
margin-bottom: 20px;
h1 {
font-size: 32px;
font-weight: 600;
color: #141F38;
margin-bottom: 8px;
}
}
.page-description {
font-size: 16px;
color: #666;
}
.plan-tabs {
background: white;
padding: 0 40px;
display: flex;
gap: 8px;
border-radius: 8px 8px 0 0;
}
.plan-tab {
padding: 16px 24px;
cursor: pointer;
font-size: 16px;
color: #666;
position: relative;
transition: all 0.3s;
&:hover {
color: #C62828;
}
&.active {
color: #C62828;
font-weight: 600;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 3px;
background: #C62828;
}
}
}
.plan-content {
background: white;
border-radius: 0 0 8px 8px;
}
</style>

View File

@@ -0,0 +1,297 @@
<template>
<div class="study-tasks">
<!-- 任务统计 -->
<div class="task-statistics">
<div class="stat-card" v-for="stat in statistics" :key="stat.label">
<div class="stat-value">{{ stat.value }}</div>
<div class="stat-label">{{ stat.label }}</div>
</div>
</div>
<!-- 任务列表 -->
<div class="task-section">
<div class="section-header">
<h2>任务列表</h2>
<div class="filter-tabs">
<div
class="filter-tab"
v-for="filter in filters"
:key="filter.key"
:class="{ active: activeFilter === filter.key }"
@click="activeFilter = filter.key"
>
{{ filter.label }}
</div>
</div>
</div>
<div class="task-list">
<div class="task-item" v-for="task in filteredTasks" :key="task.id">
<div class="task-icon" :class="`status-${task.status}`">
<i :class="getTaskIcon(task.status)"></i>
</div>
<div class="task-content">
<h3>{{ task.title }}</h3>
<p class="task-description">{{ task.description }}</p>
<div class="task-meta">
<span class="task-type">{{ task.type }}</span>
<span class="task-deadline">截止时间{{ task.deadline }}</span>
</div>
</div>
<div class="task-progress">
<div class="progress-bar">
<div class="progress-fill" :style="{ width: task.progress + '%' }"></div>
</div>
<span class="progress-text">{{ task.progress }}%</span>
</div>
<div class="task-actions">
<el-button
type="primary"
size="small"
@click="goToTask(task)"
v-if="task.status !== 'completed'"
>
{{ task.status === 'not-started' ? '开始学习' : '继续学习' }}
</el-button>
<el-button
size="small"
@click="viewTaskDetail(task)"
>
查看详情
</el-button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { ElButton } from 'element-plus';
const router = useRouter();
const activeFilter = ref('all');
const tasks = ref<any[]>([]);
const statistics = ref([
{ label: '总任务数', value: 0 },
{ label: '进行中', value: 0 },
{ label: '已完成', value: 0 },
{ label: '完成率', value: '0%' }
]);
const filters = [
{ key: 'all', label: '全部任务' },
{ key: 'not-started', label: '未开始' },
{ key: 'in-progress', label: '进行中' },
{ key: 'completed', label: '已完成' }
];
const filteredTasks = computed(() => {
if (activeFilter.value === 'all') return tasks.value;
return tasks.value.filter(task => task.status === activeFilter.value);
});
onMounted(() => {
// TODO: 加载学习任务数据
});
function getTaskIcon(status: string) {
const iconMap: Record<string, string> = {
'not-started': '⭕',
'in-progress': '⏳',
'completed': '✅'
};
return iconMap[status] || '⭕';
}
function goToTask(task: any) {
router.push(`/study/task/${task.id}`);
}
function viewTaskDetail(task: any) {
router.push(`/study/task/${task.id}/detail`);
}
</script>
<style lang="scss" scoped>
.study-tasks {
padding: 40px;
}
.task-statistics {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin-bottom: 40px;
}
.stat-card {
padding: 24px;
background: linear-gradient(135deg, #C62828, #E53935);
border-radius: 8px;
color: white;
text-align: center;
}
.stat-value {
font-size: 32px;
font-weight: 600;
margin-bottom: 8px;
}
.stat-label {
font-size: 14px;
opacity: 0.9;
}
.task-section {
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
h2 {
font-size: 24px;
font-weight: 600;
color: #141F38;
}
}
}
.filter-tabs {
display: flex;
gap: 8px;
}
.filter-tab {
padding: 8px 16px;
background: #f5f5f5;
border-radius: 20px;
font-size: 14px;
color: #666;
cursor: pointer;
transition: all 0.3s;
&:hover {
background: #ffe6e6;
color: #C62828;
}
&.active {
background: #C62828;
color: white;
}
}
.task-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.task-item {
display: flex;
align-items: center;
gap: 20px;
padding: 20px;
border: 1px solid #e0e0e0;
border-radius: 8px;
transition: all 0.3s;
&:hover {
border-color: #C62828;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
}
.task-icon {
width: 48px;
height: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
flex-shrink: 0;
&.status-not-started {
background: #f5f5f5;
}
&.status-in-progress {
background: #fff3e0;
}
&.status-completed {
background: #e8f5e9;
}
}
.task-content {
flex: 1;
h3 {
font-size: 18px;
font-weight: 600;
color: #141F38;
margin-bottom: 8px;
}
}
.task-description {
font-size: 14px;
color: #666;
margin-bottom: 12px;
}
.task-meta {
display: flex;
gap: 16px;
font-size: 13px;
}
.task-type {
color: #C62828;
font-weight: 500;
}
.task-deadline {
color: #999;
}
.task-progress {
width: 150px;
flex-shrink: 0;
.progress-bar {
width: 100%;
height: 8px;
background: #f5f5f5;
border-radius: 4px;
overflow: hidden;
margin-bottom: 4px;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #C62828, #E53935);
transition: width 0.3s;
}
.progress-text {
font-size: 12px;
color: #666;
}
}
.task-actions {
display: flex;
gap: 8px;
flex-shrink: 0;
}
</style>

View File

@@ -0,0 +1,177 @@
<template>
<div class="learning-records">
<div class="records-header">
<h2>学习记录</h2>
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
@change="handleDateChange"
/>
</div>
<div class="records-list">
<div class="record-item" v-for="record in records" :key="record.id">
<div class="record-icon">
<i :class="getRecordIcon(record.type)"></i>
</div>
<div class="record-content">
<h3>{{ record.title }}</h3>
<p class="record-description">{{ record.description }}</p>
<div class="record-meta">
<span class="record-type">{{ record.typeName }}</span>
<span class="record-duration">学习时长{{ record.duration }}分钟</span>
<span class="record-date">{{ record.learnDate }}</span>
</div>
</div>
<div class="record-progress">
<div class="progress-circle" :class="`progress-${record.status}`">
<span>{{ record.progress }}%</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ElDatePicker } from 'element-plus';
const dateRange = ref<[Date, Date] | null>(null);
const records = ref<any[]>([]);
onMounted(() => {
// TODO: 加载学习记录
});
function handleDateChange() {
// TODO: 根据日期筛选记录
}
function getRecordIcon(type: string) {
const iconMap: Record<string, string> = {
'article': '📄',
'video': '🎥',
'audio': '🎵',
'course': '📚'
};
return iconMap[type] || '📄';
}
</script>
<style lang="scss" scoped>
.learning-records {
.records-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
h2 {
font-size: 24px;
font-weight: 600;
color: #141F38;
}
}
}
.records-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.record-item {
display: flex;
align-items: center;
gap: 20px;
padding: 20px;
border: 1px solid #e0e0e0;
border-radius: 8px;
transition: all 0.3s;
&:hover {
border-color: #C62828;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
}
.record-icon {
width: 48px;
height: 48px;
background: #f5f5f5;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
flex-shrink: 0;
}
.record-content {
flex: 1;
h3 {
font-size: 18px;
font-weight: 600;
color: #141F38;
margin-bottom: 8px;
}
}
.record-description {
font-size: 14px;
color: #666;
margin-bottom: 12px;
}
.record-meta {
display: flex;
gap: 16px;
font-size: 13px;
}
.record-type {
color: #C62828;
font-weight: 500;
}
.record-duration,
.record-date {
color: #999;
}
.record-progress {
flex-shrink: 0;
}
.progress-circle {
width: 60px;
height: 60px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 600;
&.progress-completed {
background: #e8f5e9;
color: #4caf50;
}
&.progress-in-progress {
background: #fff3e0;
color: #ff9800;
}
&.progress-not-started {
background: #f5f5f5;
color: #999;
}
}
</style>

View File

@@ -0,0 +1,200 @@
<template>
<div class="my-achievements">
<div class="achievements-header">
<h2>我的成就</h2>
<div class="achievement-stats">
<span>已获得 <strong>{{ earnedCount }}</strong> / {{ totalCount }} 个成就</span>
</div>
</div>
<div class="achievements-grid">
<div
class="achievement-item"
v-for="achievement in achievements"
:key="achievement.id"
:class="{ earned: achievement.earned, locked: !achievement.earned }"
>
<div class="achievement-icon">
<img :src="achievement.icon" :alt="achievement.name" />
<div class="achievement-badge" v-if="achievement.earned"></div>
</div>
<div class="achievement-info">
<h3>{{ achievement.name }}</h3>
<p class="achievement-description">{{ achievement.description }}</p>
<div class="achievement-progress" v-if="!achievement.earned && achievement.progress">
<div class="progress-bar">
<div class="progress-fill" :style="{ width: achievement.progress + '%' }"></div>
</div>
<span class="progress-text">{{ achievement.progress }}%</span>
</div>
<div class="achievement-date" v-if="achievement.earned">
获得时间{{ achievement.earnedDate }}
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
const achievements = ref<any[]>([]);
const earnedCount = computed(() => {
return achievements.value.filter(a => a.earned).length;
});
const totalCount = computed(() => {
return achievements.value.length;
});
onMounted(() => {
// TODO: 加载成就数据
});
</script>
<style lang="scss" scoped>
.my-achievements {
.achievements-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
h2 {
font-size: 24px;
font-weight: 600;
color: #141F38;
}
}
}
.achievement-stats {
font-size: 16px;
color: #666;
strong {
color: #C62828;
font-size: 20px;
}
}
.achievements-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
}
.achievement-item {
display: flex;
gap: 16px;
padding: 20px;
border: 2px solid #e0e0e0;
border-radius: 8px;
transition: all 0.3s;
&.earned {
border-color: #C62828;
background: linear-gradient(135deg, #fff5f5, #ffffff);
.achievement-icon {
img {
filter: none;
}
}
}
&.locked {
opacity: 0.6;
.achievement-icon {
img {
filter: grayscale(100%);
}
}
}
&:hover.earned {
box-shadow: 0 4px 12px rgba(198, 40, 40, 0.2);
}
}
.achievement-icon {
position: relative;
width: 64px;
height: 64px;
flex-shrink: 0;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
.achievement-badge {
position: absolute;
bottom: -4px;
right: -4px;
width: 24px;
height: 24px;
background: #4caf50;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 600;
border: 2px solid white;
}
.achievement-info {
flex: 1;
h3 {
font-size: 16px;
font-weight: 600;
color: #141F38;
margin-bottom: 8px;
}
}
.achievement-description {
font-size: 14px;
color: #666;
line-height: 1.5;
margin-bottom: 12px;
}
.achievement-progress {
margin-bottom: 8px;
.progress-bar {
width: 100%;
height: 6px;
background: #f5f5f5;
border-radius: 3px;
overflow: hidden;
margin-bottom: 4px;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #C62828, #E53935);
transition: width 0.3s;
}
.progress-text {
font-size: 12px;
color: #999;
}
}
.achievement-date {
font-size: 12px;
color: #999;
}
</style>

View File

@@ -0,0 +1,197 @@
<template>
<div class="my-favorites">
<div class="favorites-header">
<h2>我的收藏</h2>
<div class="filter-tabs">
<div
class="filter-tab"
v-for="filter in filters"
:key="filter.key"
:class="{ active: activeFilter === filter.key }"
@click="activeFilter = filter.key"
>
{{ filter.label }}
</div>
</div>
</div>
<div class="favorites-grid">
<div class="favorite-item" v-for="item in filteredFavorites" :key="item.id">
<div class="item-thumbnail">
<img :src="item.thumbnail" :alt="item.title" />
<div class="item-type">{{ item.typeName }}</div>
</div>
<div class="item-info">
<h3>{{ item.title }}</h3>
<p class="item-summary">{{ item.summary }}</p>
<div class="item-footer">
<span class="item-date">收藏于 {{ item.favoriteDate }}</span>
<div class="item-actions">
<el-button size="small" @click="viewItem(item)">查看</el-button>
<el-button size="small" type="danger" @click="removeFavorite(item)">取消收藏</el-button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { ElButton, ElMessage } from 'element-plus';
const activeFilter = ref('all');
const favorites = ref<any[]>([]);
const filters = [
{ key: 'all', label: '全部' },
{ key: 'article', label: '文章' },
{ key: 'video', label: '视频' },
{ key: 'audio', label: '音频' },
{ key: 'course', label: '课程' }
];
const filteredFavorites = computed(() => {
if (activeFilter.value === 'all') return favorites.value;
return favorites.value.filter(item => item.type === activeFilter.value);
});
onMounted(() => {
// TODO: 加载收藏数据
});
function viewItem(item: any) {
// TODO: 跳转到详情页
}
function removeFavorite(item: any) {
// TODO: 取消收藏
ElMessage.success('已取消收藏');
}
</script>
<style lang="scss" scoped>
.my-favorites {
.favorites-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
h2 {
font-size: 24px;
font-weight: 600;
color: #141F38;
}
}
}
.filter-tabs {
display: flex;
gap: 8px;
}
.filter-tab {
padding: 8px 16px;
background: #f5f5f5;
border-radius: 20px;
font-size: 14px;
color: #666;
cursor: pointer;
transition: all 0.3s;
&:hover {
background: #ffe6e6;
color: #C62828;
}
&.active {
background: #C62828;
color: white;
}
}
.favorites-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 24px;
}
.favorite-item {
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
transition: all 0.3s;
&:hover {
border-color: #C62828;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
}
.item-thumbnail {
position: relative;
width: 100%;
height: 180px;
background: #f5f5f5;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.item-type {
position: absolute;
top: 12px;
left: 12px;
padding: 4px 12px;
background: rgba(198, 40, 40, 0.9);
color: white;
border-radius: 4px;
font-size: 12px;
}
.item-info {
padding: 16px;
h3 {
font-size: 16px;
font-weight: 600;
color: #141F38;
margin-bottom: 8px;
}
}
.item-summary {
font-size: 14px;
color: #666;
line-height: 1.5;
margin-bottom: 16px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.item-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 12px;
border-top: 1px solid #f0f0f0;
}
.item-date {
font-size: 12px;
color: #999;
}
.item-actions {
display: flex;
gap: 8px;
}
</style>

View File

@@ -0,0 +1,185 @@
<template>
<div class="user-center-page">
<div class="user-banner">
<div class="user-avatar">
<img :src="userInfo.avatar" alt="用户头像" />
</div>
<div class="user-basic">
<h2>{{ userInfo.name }}</h2>
<p>{{ userInfo.bio }}</p>
</div>
<div class="user-stats">
<div class="stat-item" v-for="stat in stats" :key="stat.label">
<div class="stat-value">{{ stat.value }}</div>
<div class="stat-label">{{ stat.label }}</div>
</div>
</div>
</div>
<div class="center-tabs">
<div
class="center-tab"
v-for="tab in tabs"
:key="tab.key"
:class="{ active: activeTab === tab.key }"
@click="activeTab = tab.key"
>
{{ tab.label }}
</div>
</div>
<div class="center-content">
<component :is="currentComponent" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { useStore } from 'vuex';
import LearningRecords from './components/LearningRecords.vue';
import MyFavorites from './components/MyFavorites.vue';
import MyAchievements from './components/MyAchievements.vue';
const store = useStore();
const activeTab = ref('records');
const userInfo = computed(() => store.getters['auth/userInfo']);
const stats = ref([
{ label: '学习天数', value: 0 },
{ label: '学习时长', value: '0h' },
{ label: '完成任务', value: 0 },
{ label: '获得成就', value: 0 }
]);
const tabs = [
{ key: 'records', label: '学习记录' },
{ key: 'favorites', label: '我的收藏' },
{ key: 'achievements', label: '我的成就' }
];
const componentMap: Record<string, any> = {
'records': LearningRecords,
'favorites': MyFavorites,
'achievements': MyAchievements
};
const currentComponent = computed(() => componentMap[activeTab.value]);
onMounted(() => {
// TODO: 加载用户统计数据
});
</script>
<style lang="scss" scoped>
.user-center-page {
min-height: 100vh;
background: #f5f5f5;
padding: 20px;
}
.user-banner {
background: linear-gradient(135deg, #C62828, #E53935);
padding: 40px;
border-radius: 8px;
display: flex;
align-items: center;
gap: 32px;
margin-bottom: 20px;
color: white;
}
.user-avatar {
width: 100px;
height: 100px;
border-radius: 50%;
overflow: hidden;
border: 4px solid white;
flex-shrink: 0;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.user-basic {
flex: 1;
h2 {
font-size: 28px;
font-weight: 600;
margin-bottom: 8px;
}
p {
font-size: 14px;
opacity: 0.9;
}
}
.user-stats {
display: flex;
gap: 40px;
}
.stat-item {
text-align: center;
.stat-value {
font-size: 28px;
font-weight: 600;
margin-bottom: 4px;
}
.stat-label {
font-size: 14px;
opacity: 0.9;
}
}
.center-tabs {
background: white;
padding: 0 40px;
display: flex;
gap: 8px;
border-radius: 8px 8px 0 0;
}
.center-tab {
padding: 16px 24px;
cursor: pointer;
font-size: 16px;
color: #666;
position: relative;
transition: all 0.3s;
&:hover {
color: #C62828;
}
&.active {
color: #C62828;
font-weight: 600;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 3px;
background: #C62828;
}
}
}
.center-content {
background: white;
border-radius: 0 0 8px 8px;
padding: 40px;
}
</style>