This commit is contained in:
2025-10-27 19:05:56 +08:00
parent 0033ac10ec
commit 98c73632bd
25 changed files with 1223 additions and 133 deletions

View File

@@ -0,0 +1,11 @@
<svg width="23" height="23" viewBox="0 0 23 23" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_526_9105)">
<path d="M11.5049 1.95605C15.1879 2.34994 17.6732 4.0702 18.9621 7.11758C20.251 10.165 20.0176 13.1884 18.2608 16.1887L19.8499 18.0915L17.7405 20.2121L16.0259 18.4791C13.873 19.9515 11.4081 20.4009 8.63182 19.828C7.24893 19.5422 6.08134 19.0585 5.12899 18.376L5.00279 18.4798L4.96142 18.4349C4.97613 18.5177 4.9846 18.6034 4.9846 18.6905C4.9846 19.4531 4.38781 20.0717 3.65148 20.0717C2.91516 20.0717 2.31836 19.4537 2.31836 18.6905C2.31836 17.928 2.91516 17.3101 3.65076 17.3101C3.77065 17.3101 3.88642 17.3262 3.99646 17.3564C3.55662 16.8608 3.18947 16.3052 2.90603 15.7063L4.25806 14.276C5.74542 15.672 7.32043 16.5363 8.98318 16.8691C10.6458 17.2026 12.2511 16.9758 13.7987 16.188L8.09044 10.2085L6.35757 11.9021L3.96843 9.11733L8.33936 4.7682L8.34784 4.77451L8.32821 4.74987C8.73208 4.9149 9.10235 5.00763 9.43827 5.02799L9.51122 5.01393L9.63604 4.9809C10.0989 4.83913 10.5919 4.58564 11.1178 4.22052L11.0673 4.25493L11.1045 4.20292L12.4593 5.5806L12.4565 5.58204L12.4733 5.5995L10.3289 7.95873L16.0091 13.986L10.3324 7.9588L10.3289 7.96366L16.0259 14.0141C17.2518 12.7404 17.4537 10.7568 16.6311 8.06406C15.8085 5.37203 14.0996 3.33576 11.5049 1.95605Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_526_9105">
<rect width="18.256" height="18.256" fill="white" transform="translate(1.95605 1.95605)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,267 @@
<template>
<div class="custom-carousel">
<div class="carousel-container">
<!-- 轮播内容 -->
<div
class="carousel-track"
:style="{ transform: `translateX(-${currentIndex * 100}%)` }"
>
<div
v-for="(item, index) in items"
:key="index"
class="carousel-item"
>
<slot :item="item" :index="index"></slot>
</div>
</div>
<!-- 左右箭头 -->
<button
v-if="showArrows"
class="carousel-arrow carousel-arrow-left"
@click="prev"
:disabled="currentIndex === 0 && !loop"
>
<el-icon><ArrowLeft /></el-icon>
</button>
<button
v-if="showArrows"
class="carousel-arrow carousel-arrow-right"
@click="next"
:disabled="currentIndex === items.length - 1 && !loop"
>
<el-icon><ArrowRight /></el-icon>
</button>
</div>
<!-- 指示器 -->
<div
v-if="showIndicators"
class="carousel-indicators"
:class="indicatorPosition"
>
<button
v-for="(item, index) in items"
:key="index"
class="indicator-item"
:class="{ active: currentIndex === index }"
@click="goTo(index)"
>
<img
v-if="currentIndex === index && activeIcon"
:src="activeIcon"
class="indicator-icon"
/>
<span v-else class="indicator-dot"></span>
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue';
interface Props {
items: any[];
autoplay?: boolean;
interval?: number;
loop?: boolean;
showArrows?: boolean;
showIndicators?: boolean;
indicatorPosition?: 'bottom-center' | 'bottom-right' | 'bottom-left';
activeIcon?: string;
inactiveIcon?: string;
}
const props = withDefaults(defineProps<Props>(), {
autoplay: true,
interval: 5000,
loop: true,
showArrows: true,
showIndicators: true,
indicatorPosition: 'bottom-right',
});
const currentIndex = ref(0);
let timer: ReturnType<typeof setInterval> | null = null;
function next() {
if (currentIndex.value < props.items.length - 1) {
currentIndex.value++;
} else if (props.loop) {
currentIndex.value = 0;
}
}
function prev() {
if (currentIndex.value > 0) {
currentIndex.value--;
} else if (props.loop) {
currentIndex.value = props.items.length - 1;
}
}
function goTo(index: number) {
currentIndex.value = index;
resetTimer();
}
function startAutoplay() {
if (props.autoplay) {
timer = setInterval(() => {
next();
}, props.interval);
}
}
function stopAutoplay() {
if (timer) {
clearInterval(timer);
timer = null;
}
}
function resetTimer() {
stopAutoplay();
startAutoplay();
}
onMounted(() => {
startAutoplay();
});
onUnmounted(() => {
stopAutoplay();
});
defineExpose({
next,
prev,
goTo,
});
</script>
<style lang="scss" scoped>
.custom-carousel {
width: 100%;
height: 100%;
position: relative;
.carousel-container {
width: 100%;
height: 100%;
overflow: hidden;
position: relative;
.carousel-track {
display: flex;
height: 100%;
transition: transform 0.5s ease-in-out;
.carousel-item {
min-width: 100%;
height: 100%;
flex-shrink: 0;
}
}
.carousel-arrow {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 40px;
height: 40px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.3);
backdrop-filter: blur(6.4px);
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
z-index: 10;
color: #FFFFFF;
&:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.5);
}
&:disabled {
opacity: 0.3;
cursor: not-allowed;
}
&.carousel-arrow-left {
left: 20px;
}
&.carousel-arrow-right {
right: 20px;
}
.el-icon {
font-size: 20px;
}
}
}
.carousel-indicators {
display: flex;
gap: 13px;
position: absolute;
z-index: 10;
&.bottom-center {
bottom: 32px;
left: 50%;
transform: translateX(-50%);
}
&.bottom-right {
bottom: 32px;
right: 120px;
}
&.bottom-left {
bottom: 32px;
left: 120px;
}
.indicator-item {
width: 22px;
height: 22px;
border: none;
background: transparent;
cursor: pointer;
padding: 0;
transition: all 0.3s;
.indicator-dot {
display: block;
width: 100%;
height: 100%;
border-radius: 50%;
background: rgba(191, 191, 191, 0.5);
backdrop-filter: blur(6.4px);
}
.indicator-icon {
width: 100%;
height: 100%;
object-fit: contain;
}
&:hover .indicator-dot {
background: rgba(255, 255, 255, 0.6);
}
&.active .indicator-dot {
background: rgba(255, 255, 255, 0.8);
}
}
}
}
</style>

View File

@@ -1,4 +1,5 @@
export { default as Breadcrumb } from './Breadcrumb.vue';
export { default as Carousel } from './Carousel.vue';
export { default as FloatingSidebar } from './FloatingSidebar.vue';
export { default as MenuItem } from './MenuItem.vue';
export { default as MenuNav } from './MenuNav.vue';

View File

@@ -248,7 +248,7 @@ const cleanDialogVisible = ref(false);
const cleanDays = ref(30);
// 加载日志列表
const loadLogList = async () => {
async function loadLogList() {
loading.value = true;
try {
const filter: Partial<CrontabLog> = {};
@@ -273,37 +273,37 @@ const loadLogList = async () => {
} finally {
loading.value = false;
}
};
}
// 搜索
const handleSearch = () => {
function handleSearch() {
pageParam.pageNumber = 1;
loadLogList();
};
}
// 重置搜索
const handleReset = () => {
function handleReset() {
searchForm.taskName = '';
searchForm.taskGroup = '';
searchForm.executeStatus = undefined;
pageParam.pageNumber = 1;
loadLogList();
};
}
// 分页变化
const handlePageChange = (page: number) => {
function handlePageChange(page: number) {
pageParam.pageNumber = page;
loadLogList();
};
}
const handleSizeChange = (size: number) => {
function handleSizeChange(size: number) {
pageParam.pageSize = size;
pageParam.pageNumber = 1;
loadLogList();
};
}
// 查看详情
const handleViewDetail = async (row: CrontabLog) => {
async function handleViewDetail(row: CrontabLog) {
try {
const result = await crontabApi.getLogById(row.id!);
if (result.success && result.data) {
@@ -316,10 +316,10 @@ const handleViewDetail = async (row: CrontabLog) => {
console.error('获取日志详情失败:', error);
ElMessage.error('获取日志详情失败');
}
};
}
// 删除日志
const handleDelete = async (row: CrontabLog) => {
async function handleDelete(row: CrontabLog) {
try {
await ElMessageBox.confirm(
'确定要删除这条日志吗?',
@@ -344,15 +344,15 @@ const handleDelete = async (row: CrontabLog) => {
ElMessage.error('删除日志失败');
}
}
};
}
// 清理日志
const handleCleanLogs = () => {
function handleCleanLogs() {
cleanDialogVisible.value = true;
};
}
// 确认清理
const handleConfirmClean = async () => {
async function handleConfirmClean() {
submitting.value = true;
try {
const result = await crontabApi.cleanLogs(cleanDays.value);
@@ -369,7 +369,7 @@ const handleConfirmClean = async () => {
} finally {
submitting.value = false;
}
};
}
// 初始化
onMounted(() => {

View File

@@ -266,10 +266,7 @@
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import {
Plus, Search, Refresh, DocumentCopy, VideoPlay,
VideoPause, Promotion, Edit, Delete
} from '@element-plus/icons-vue';
import { Plus, Search, Refresh, DocumentCopy, VideoPlay, VideoPause, Promotion, Edit, Delete } from '@element-plus/icons-vue';
import { crontabApi } from '@/apis/crontab';
import type { CrontabTask, PageParam } from '@/types';
@@ -310,7 +307,7 @@ const formData = reactive<Partial<CrontabTask>>({
});
// 加载爬虫列表
const loadCrawlerList = async () => {
async function loadCrawlerList() {
loading.value = true;
try {
const filter: Partial<CrontabTask> = {
@@ -337,50 +334,50 @@ const loadCrawlerList = async () => {
} finally {
loading.value = false;
}
};
}
// 搜索
const handleSearch = () => {
function handleSearch() {
pageParam.pageNumber = 1;
loadCrawlerList();
};
}
// 重置搜索
const handleReset = () => {
function handleReset() {
searchForm.taskName = '';
searchForm.status = undefined;
pageParam.pageNumber = 1;
loadCrawlerList();
};
}
// 分页变化
const handlePageChange = (page: number) => {
function handlePageChange(page: number) {
pageParam.pageNumber = page;
loadCrawlerList();
};
}
const handleSizeChange = (size: number) => {
function handleSizeChange(size: number) {
pageParam.pageSize = size;
pageParam.pageNumber = 1;
loadCrawlerList();
};
}
// 新增爬虫
const handleAdd = () => {
function handleAdd() {
isEdit.value = false;
resetFormData();
dialogVisible.value = true;
};
}
// 编辑爬虫
const handleEdit = (row: CrontabTask) => {
function handleEdit(row: CrontabTask) {
isEdit.value = true;
Object.assign(formData, row);
dialogVisible.value = true;
};
}
// 启动爬虫
const handleStart = async (row: CrontabTask) => {
async function handleStart(row: CrontabTask) {
try {
const result = await crontabApi.startTask(row.taskId!);
if (result.success) {
@@ -393,10 +390,10 @@ const handleStart = async (row: CrontabTask) => {
console.error('启动爬虫失败:', error);
ElMessage.error('启动爬虫失败');
}
};
}
// 暂停爬虫
const handlePause = async (row: CrontabTask) => {
async function handlePause(row: CrontabTask) {
try {
const result = await crontabApi.pauseTask(row.taskId!);
if (result.success) {
@@ -409,10 +406,10 @@ const handlePause = async (row: CrontabTask) => {
console.error('暂停爬虫失败:', error);
ElMessage.error('暂停爬虫失败');
}
};
}
// 执行一次
const handleExecute = async (row: CrontabTask) => {
async function handleExecute(row: CrontabTask) {
try {
await ElMessageBox.confirm(
`确定立即执行爬虫"${row.taskName}"吗?`,
@@ -436,10 +433,10 @@ const handleExecute = async (row: CrontabTask) => {
ElMessage.error('执行爬虫失败');
}
}
};
}
// 删除爬虫
const handleDelete = async (row: CrontabTask) => {
async function handleDelete(row: CrontabTask) {
try {
await ElMessageBox.confirm(
`确定要删除爬虫"${row.taskName}"吗?`,
@@ -464,10 +461,10 @@ const handleDelete = async (row: CrontabTask) => {
ElMessage.error('删除爬虫失败');
}
}
};
}
// 验证Cron表达式
const validateCron = async () => {
async function validateCron() {
if (!formData.cronExpression) {
ElMessage.warning('请输入Cron表达式');
return;
@@ -484,10 +481,10 @@ const validateCron = async () => {
console.error('验证Cron表达式失败:', error);
ElMessage.error('验证失败');
}
};
}
// 提交表单
const handleSubmit = async () => {
async function handleSubmit() {
// 表单验证
if (!formData.taskName) {
ElMessage.warning('请输入爬虫名称');
@@ -533,15 +530,15 @@ const handleSubmit = async () => {
} finally {
submitting.value = false;
}
};
}
// 重置表单
const resetForm = () => {
function resetForm() {
resetFormData();
};
}
// 重置表单数据
const resetFormData = () => {
function resetFormData() {
Object.assign(formData, {
taskName: '',
taskGroup: 'NEWS_CRAWLER',
@@ -554,7 +551,7 @@ const resetFormData = () => {
misfirePolicy: 3,
description: ''
});
};
}
// 初始化
onMounted(() => {

View File

@@ -315,48 +315,48 @@ const loadTaskList = async () => {
};
// 搜索
const handleSearch = () => {
function handleSearch() {
pageParam.pageNumber = 1;
loadTaskList();
};
}
// 重置搜索
const handleReset = () => {
function handleReset() {
searchForm.taskName = '';
searchForm.taskGroup = '';
searchForm.status = undefined;
pageParam.pageNumber = 1;
loadTaskList();
};
}
// 分页变化
const handlePageChange = (page: number) => {
function handlePageChange(page: number) {
pageParam.pageNumber = page;
loadTaskList();
};
}
const handleSizeChange = (size: number) => {
function handleSizeChange(size: number) {
pageParam.pageSize = size;
pageParam.pageNumber = 1;
loadTaskList();
};
}
// 新增任务
const handleAdd = () => {
function handleAdd() {
isEdit.value = false;
resetFormData();
dialogVisible.value = true;
};
}
// 编辑任务
const handleEdit = (row: CrontabTask) => {
function handleEdit(row: CrontabTask) {
isEdit.value = true;
Object.assign(formData, row);
dialogVisible.value = true;
};
}
// 启动任务
const handleStart = async (row: CrontabTask) => {
async function handleStart(row: CrontabTask) {
try {
const result = await crontabApi.startTask(row.taskId!);
if (result.success) {
@@ -369,10 +369,10 @@ const handleStart = async (row: CrontabTask) => {
console.error('启动任务失败:', error);
ElMessage.error('启动任务失败');
}
};
}
// 暂停任务
const handlePause = async (row: CrontabTask) => {
async function handlePause(row: CrontabTask) {
try {
const result = await crontabApi.pauseTask(row.taskId!);
if (result.success) {
@@ -385,10 +385,10 @@ const handlePause = async (row: CrontabTask) => {
console.error('暂停任务失败:', error);
ElMessage.error('暂停任务失败');
}
};
}
// 执行一次
const handleExecute = async (row: CrontabTask) => {
async function handleExecute(row: CrontabTask) {
try {
await ElMessageBox.confirm(
`确定立即执行任务"${row.taskName}"吗?`,
@@ -412,10 +412,10 @@ const handleExecute = async (row: CrontabTask) => {
ElMessage.error('执行任务失败');
}
}
};
}
// 删除任务
const handleDelete = async (row: CrontabTask) => {
async function handleDelete(row: CrontabTask) {
try {
await ElMessageBox.confirm(
`确定要删除任务"${row.taskName}"吗?`,
@@ -440,10 +440,10 @@ const handleDelete = async (row: CrontabTask) => {
ElMessage.error('删除任务失败');
}
}
};
}
// 验证Cron表达式
const validateCron = async () => {
async function validateCron() {
if (!formData.cronExpression) {
ElMessage.warning('请输入Cron表达式');
return;
@@ -460,10 +460,10 @@ const validateCron = async () => {
console.error('验证Cron表达式失败:', error);
ElMessage.error('验证失败');
}
};
}
// 提交表单
const handleSubmit = async () => {
async function handleSubmit() {
// 表单验证
if (!formData.taskName) {
ElMessage.warning('请输入任务名称');
@@ -510,15 +510,15 @@ const handleSubmit = async () => {
} finally {
submitting.value = false;
}
};
}
// 重置表单
const resetForm = () => {
function resetForm() {
resetFormData();
};
}
// 重置表单数据
const resetFormData = () => {
function resetFormData() {
Object.assign(formData, {
taskName: '',
taskGroup: 'DEFAULT',
@@ -531,7 +531,7 @@ const resetFormData = () => {
misfirePolicy: 2,
description: ''
});
};
}
// 初始化
onMounted(() => {

View File

@@ -455,7 +455,7 @@ const filteredModules = computed(() => {
});
// 加载模块列表
const loadModuleList = async () => {
async function loadModuleList() {
moduleLoading.value = true;
try {
const result = await moduleApi.getModuleList();
@@ -470,10 +470,10 @@ const loadModuleList = async () => {
} finally {
moduleLoading.value = false;
}
};
}
// 加载权限列表
const loadPermissions = async (moduleID: string) => {
async function loadPermissions(moduleID: string) {
permissionLoading.value = true;
try {
const result = await moduleApi.getModulePermissions(moduleID);
@@ -489,32 +489,32 @@ const loadPermissions = async (moduleID: string) => {
} finally {
permissionLoading.value = false;
}
};
}
// 选择模块
const handleSelectModule = (module: SysModule) => {
function handleSelectModule(module: SysModule) {
currentModule.value = module;
if (module.moduleID) {
loadPermissions(module.moduleID);
}
};
}
// 新增模块
const handleAddModule = () => {
function handleAddModule() {
isEditModule.value = false;
resetModuleForm();
moduleDialogVisible.value = true;
};
}
// 编辑模块
const handleEditModule = (module: SysModule) => {
function handleEditModule(module: SysModule) {
isEditModule.value = true;
Object.assign(moduleForm, module);
moduleDialogVisible.value = true;
};
}
// 删除模块
const handleDeleteModule = async (module: SysModule) => {
async function handleDeleteModule(module: SysModule) {
try {
await ElMessageBox.confirm(
`确定要删除模块"${module.name}"吗?删除后该模块下的所有权限也会被删除。`,
@@ -543,10 +543,10 @@ const handleDeleteModule = async (module: SysModule) => {
ElMessage.error('删除模块失败');
}
}
};
}
// 提交模块表单
const handleSubmitModule = async () => {
async function handleSubmitModule() {
if (!moduleForm.name) {
ElMessage.warning('请输入模块名称');
return;
@@ -582,24 +582,24 @@ const handleSubmitModule = async () => {
} finally {
submitting.value = false;
}
};
}
// 新增权限
const handleAddPermission = () => {
function handleAddPermission() {
isEditPermission.value = false;
resetPermissionForm();
permissionDialogVisible.value = true;
};
}
// 编辑权限
const handleEditPermission = (permission: SysPermission) => {
function handleEditPermission(permission: SysPermission) {
isEditPermission.value = true;
Object.assign(permissionForm, permission);
permissionDialogVisible.value = true;
};
}
// 删除权限
const handleDeletePermission = async (permission: SysPermission) => {
async function handleDeletePermission(permission: SysPermission) {
if (!currentModule.value) return;
try {
@@ -630,10 +630,10 @@ const handleDeletePermission = async (permission: SysPermission) => {
ElMessage.error('删除权限失败');
}
}
};
}
// 提交权限表单
const handleSubmitPermission = async () => {
async function handleSubmitPermission() {
if (!permissionForm.name) {
ElMessage.warning('请输入权限名称');
return;
@@ -674,10 +674,10 @@ const handleSubmitPermission = async () => {
} finally {
submitting.value = false;
}
};
}
// 绑定菜单
const handleBindMenu = async (permission: SysPermission) => {
async function handleBindMenu(permission: SysPermission) {
currentPermission.value = permission;
permission.bindType = 'menu';
@@ -695,10 +695,10 @@ const handleBindMenu = async (permission: SysPermission) => {
console.error('获取菜单绑定信息失败:', error);
ElMessage.error('获取菜单绑定信息失败');
}
};
}
// 绑定角色
const handleBindRole = async (permission: SysPermission) => {
async function handleBindRole(permission: SysPermission) {
currentPermission.value = permission;
permission.bindType = 'role';
@@ -716,14 +716,14 @@ const handleBindRole = async (permission: SysPermission) => {
console.error('获取角色绑定信息失败:', error);
ElMessage.error('获取角色绑定信息失败');
}
};
}
// 菜单选择相关
const isMenuSelected = (menuID: string | undefined): boolean => {
function isMenuSelected(menuID: string | undefined): boolean {
return menuID ? selectedMenus.value.includes(menuID) : false;
};
}
const toggleMenuSelection = (menu: SysMenu) => {
function toggleMenuSelection(menu: SysMenu) {
if (!menu.menuID) return;
const index = selectedMenus.value.indexOf(menu.menuID);
if (index > -1) {
@@ -731,14 +731,14 @@ const toggleMenuSelection = (menu: SysMenu) => {
} else {
selectedMenus.value.push(menu.menuID);
}
};
}
// 角色选择相关
const isRoleSelected = (roleID: string | undefined): boolean => {
function isRoleSelected(roleID: string | undefined): boolean {
return roleID ? selectedRoles.value.includes(roleID) : false;
};
}
const toggleRoleSelection = (role: SysRole) => {
function toggleRoleSelection(role: SysRole) {
if (!role.roleID) return;
const index = selectedRoles.value.indexOf(role.roleID);
if (index > -1) {
@@ -746,10 +746,10 @@ const toggleRoleSelection = (role: SysRole) => {
} else {
selectedRoles.value.push(role.roleID);
}
};
}
// 保存菜单绑定
const saveMenuBinding = async () => {
async function saveMenuBinding() {
if (!currentPermission.value?.permissionID) {
ElMessage.error('权限信息不完整');
return;
@@ -792,10 +792,10 @@ const saveMenuBinding = async () => {
} finally {
submitting.value = false;
}
};
}
// 保存角色绑定
const saveRoleBinding = async () => {
async function saveRoleBinding() {
if (!currentPermission.value?.permissionID) {
ElMessage.error('权限信息不完整');
return;
@@ -837,10 +837,10 @@ const saveRoleBinding = async () => {
} finally {
submitting.value = false;
}
};
}
// 重置表单
const resetModuleForm = () => {
function resetModuleForm() {
Object.assign(moduleForm, {
name: '',
code: '',
@@ -849,21 +849,21 @@ const resetModuleForm = () => {
orderNum: 0,
status: 1
});
};
}
const resetPermissionForm = () => {
function resetPermissionForm() {
Object.assign(permissionForm, {
name: '',
code: '',
description: ''
});
};
}
const resetBindList = () => {
function resetBindList() {
selectedMenus.value = [];
selectedRoles.value = [];
currentPermission.value = null;
};
}
// 初始化
onMounted(() => {

View File

@@ -0,0 +1,160 @@
<template>
<div class="article-card">
<div class="article-image">
<div class="image-placeholder"></div>
<div class="article-tag">精选文章</div>
</div>
<div class="article-content">
<h3 class="article-title">新时代中国特色社会主义发展历程</h3>
<p class="article-desc">
习近平新时代中国特色社会主义思想是当代中国马克思主义二十一世纪马克思主义是中华文化和中国精神的时代精华其核心要义与实践要求内涵丰富意义深远
</p>
<div class="article-footer">
<div class="meta-tag">
<el-icon><Document /></el-icon>
<span>热门文章</span>
</div>
<span class="view-count">2.1w次浏览</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { Document } from '@element-plus/icons-vue';
</script>
<style lang="scss" scoped>
.article-card {
background: #FFFFFF;
border-radius: 0.625em;
overflow: hidden;
box-shadow: 0px 0.5em 1.25em 0px rgba(164, 182, 199, 0.2);
transition: all 0.3s;
cursor: pointer;
display: flex;
flex-direction: column;
&:hover {
transform: translateY(-0.25em);
box-shadow: 0px 0.75em 1.75em 0px rgba(164, 182, 199, 0.3);
}
.article-image {
width: 100%;
aspect-ratio: 384 / 221;
position: relative;
overflow: hidden;
flex-shrink: 0;
.image-placeholder {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
position: relative;
&::after {
content: '';
position: absolute;
width: 100%;
height: 100%;
background: url('data:image/svg+xml,<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg"><defs><pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse"><path d="M 40 0 L 0 0 0 40" fill="none" stroke="rgba(255,255,255,0.1)" stroke-width="1"/></pattern></defs><rect width="100%" height="100%" fill="url(%23grid)" /></svg>');
}
}
.article-tag {
position: absolute;
top: 0;
left: 0;
background: #D1AD79;
border-radius: 0px 0px 0.625em 0px;
padding: 0.2em 1.6em;
min-width: 5.4em;
min-height: 2.1em;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
font-family: 'PingFang SC';
font-weight: 600;
font-size: 0.875em;
line-height: 1.57;
color: #FFFFFF;
}
}
.article-content {
padding: 5.2% 5.7% 5.7% 5.7%;
flex: 1;
display: flex;
flex-direction: column;
.article-title {
font-family: 'PingFang SC';
font-weight: 600;
font-size: 1.25em;
line-height: 1.4;
color: #141F38;
margin: 0 0 0.2em 0;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1;
line-clamp: 1;
-webkit-box-orient: vertical;
}
.article-desc {
font-family: 'PingFang SC';
font-weight: 400;
font-size: 0.875em;
line-height: 1.57;
color: rgba(0, 0, 0, 0.3);
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 4;
line-clamp: 4;
-webkit-box-orient: vertical;
}
.article-footer {
margin-top: 1.25em;
display: flex;
justify-content: space-between;
align-items: center;
.meta-tag {
display: flex;
align-items: center;
gap: 0.25em;
.el-icon {
width: 1em;
height: 1em;
color: #FFFFFF;
}
span {
font-family: 'PingFang SC';
font-weight: 500;
font-size: 0.875em;
line-height: 1.57;
color: #C62828;
}
}
.view-count {
font-family: 'PingFang SC';
font-weight: 400;
font-size: 0.875em;
line-height: 1.57;
color: rgba(0, 0, 0, 0.3);
}
}
}
}
</style>

View File

@@ -0,0 +1,137 @@
<template>
<div class="ideological-card">
<div class="card-image">
<div class="image-placeholder"></div>
</div>
<div class="date-box">
<div class="day">10</div>
<div class="month">2025.10</div>
</div>
<div class="card-content">
<h3 class="card-title">学校召开"习近平新时代中国特色社会主义思想概论"课程集体备课会</h3>
<p class="card-desc">
深入贯彻习近平总书记关于思政课建设的重要论述持续推进思政课教学改革创新
</p>
</div>
</div>
</template>
<script setup lang="ts">
</script>
<style lang="scss" scoped>
.ideological-card {
background: #FFFFFF;
border-radius: 0.625em;
overflow: hidden;
box-shadow: 0px 0.5em 1.25em 0px rgba(164, 182, 199, 0.2);
transition: all 0.3s;
cursor: pointer;
display: flex;
flex-direction: column;
position: relative;
&:hover {
transform: translateY(-0.25em);
box-shadow: 0px 0.75em 1.75em 0px rgba(164, 182, 199, 0.3);
}
.card-image {
width: 100%;
aspect-ratio: 384 / 221;
flex-shrink: 0;
overflow: hidden;
.image-placeholder {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
position: relative;
&::after {
content: '';
position: absolute;
width: 100%;
height: 100%;
background: url('data:image/svg+xml,<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg"><defs><pattern id="dots" width="20" height="20" patternUnits="userSpaceOnUse"><circle cx="10" cy="10" r="1.5" fill="rgba(255,255,255,0.2)"/></pattern></defs><rect width="100%" height="100%" fill="url(%23dots)" /></svg>');
}
}
}
.date-box {
position: absolute;
top: calc(57.55% - 3.5em);
left: 5.7%;
width: 18.75%;
aspect-ratio: 1 / 1;
background: #C62828;
border-radius: 0.25em;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 0.3em;
padding: 0.4em 0.3em;
box-sizing: border-box;
z-index: 10;
.day {
font-family: 'PingFang SC';
font-weight: 600;
font-size: 1.875em;
line-height: 0.73;
color: #FFFFFF;
text-align: center;
width: 100%;
}
.month {
font-family: 'PingFang SC';
font-weight: 400;
font-size: 0.875em;
line-height: 1.57;
color: #FFFFFF;
text-align: center;
width: 100%;
}
}
.card-content {
padding: 17.4% 5.7% 5.7% 5.7%;
flex: 1;
display: flex;
flex-direction: column;
.card-title {
font-family: 'PingFang SC';
font-weight: 600;
font-size: 1.25em;
line-height: 1.4;
color: #141F38;
margin: 0 0 0.25em 0;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
}
.card-desc {
font-family: 'PingFang SC';
font-weight: 400;
font-size: 0.875em;
line-height: 1.57;
color: rgba(0, 0, 0, 0.3);
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
}
}
}
</style>

View File

@@ -0,0 +1,2 @@
export { default as HotArticleCard } from './HotArticleCard.vue';
export { default as IdeologicalArticleCard } from './IdeologicalArticleCard.vue';

View File

@@ -1,2 +1,3 @@
export { default as ArticleAddView } from './ArticleAddView.vue';
export { default as ArticleShowView } from './ArticleShowView.vue';
export * from './card';

View File

@@ -0,0 +1,10 @@
<template>
<div class="banner-add"></div>
</template>
<script setup lang="ts">
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,88 @@
<template>
<div class="banner-card">
<div class="banner-content" @click="handleLearn">
<!-- <img :src="FILE_DOWNLOAD_URL + props.banner.imageUrl" alt="banner" class="banner-image"> -->
<span>test</span>
</div>
</div>
</template>
<script setup lang="ts">
import type { Banner } from '@/types';
import { FILE_DOWNLOAD_URL } from '@/config';
import { useRouter } from 'vue-router';
const router = useRouter();
const props = defineProps<{
banner: Banner;
}>();
function handleLearn() {
if (props.banner.linkType === 1) {
router.push(`/resource/${props.banner.linkID}`);
} else if (props.banner.linkType === 2) {
router.push(`/course/${props.banner.linkID}`);
} else if (props.banner.linkType === 3) {
window.open(props.banner.linkUrl, '_blank');
}
}
</script>
<style lang="scss" scoped>
.banner-card {
width: 100%;
height: 100%;
.banner-content {
width: 100%;
height: 100%;
overflow: hidden;
position: relative;
background-color: red;
.banner-image {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
display: block;
}
.banner-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 60px 120px;
display: flex;
align-items: flex-end;
background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.3) 100%);
.learn-btn {
width: 120px;
height: 39px;
background: #FFFFFF;
border-radius: 12px;
border: none;
font-family: 'PingFang SC';
font-weight: 600;
font-size: 16px;
line-height: 22.4px;
color: #C62828;
cursor: pointer;
transition: all 0.3s;
&:hover {
background: rgba(255, 255, 255, 0.9);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
&:active {
transform: translateY(0);
}
}
}
}
}
</style>

View File

@@ -0,0 +1,3 @@
// 导出目录下组件
export { default as BannerAdd } from './BannerAdd.vue';
export { default as BannerCard } from './BannerCard.vue';

View File

@@ -0,0 +1 @@
export { default as RichTextEditorView } from './RichTextEditorView.vue'

View File

@@ -0,0 +1,4 @@
// 导出错误页面
export { default as Error403 } from './403.vue'
export { default as Error404 } from './404.vue'
export { default as Error500 } from './500.vue'

View File

@@ -0,0 +1,7 @@
export * from './article'
export * from './banner'
export * from './course'
export * from './editor'
export * from './error'
export * from './login'
export * from './task'

View File

@@ -0,0 +1,3 @@
export { default as Login } from './Login.vue'
export { default as Register } from './Register.vue'
export { default as ForgotPassword } from './ForgotPassword.vue'

View File

@@ -0,0 +1,262 @@
<template>
<div class="learning-progress">
<div class="progress-header">
<div class="header-left">
<h3 class="progress-title">学习进度</h3>
<p class="update-time">更新时间2025-09-25 18:30:00</p>
</div>
<div class="tab-buttons">
<button v-for="(tab, index) in tabs" :key="index" :class="['tab-btn', { active: activeTab === index }]"
@click="handleTabChange(index)">
{{ tab }}
</button>
</div>
</div>
<div class="chart-container" ref="chartRef"></div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
import * as echarts from 'echarts';
import type { EChartsOption } from 'echarts';
const tabs = ref(['今日', '近一周', '近一月']);
const activeTab = ref(0);
const chartRef = ref<HTMLElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
// 模拟不同时间段的数据
const chartDataMap = {
0: [
{ name: '党史', value: 43, highlight: false },
{ name: '理论', value: 58, highlight: false },
{ name: '政策', value: 34, highlight: false },
{ name: '思政', value: 20, highlight: false },
{ name: '文化', value: 52, highlight: true }
],
1: [
{ name: '党史', value: 68, highlight: true },
{ name: '理论', value: 52, highlight: false },
{ name: '政策', value: 45, highlight: false },
{ name: '思政', value: 38, highlight: false },
{ name: '文化', value: 41, highlight: false }
],
2: [
{ name: '党史', value: 85, highlight: true },
{ name: '理论', value: 72, highlight: false },
{ name: '政策', value: 58, highlight: false },
{ name: '思政', value: 65, highlight: false },
{ name: '文化', value: 43, highlight: false }
]
};
const chartData = computed(() => chartDataMap[activeTab.value as keyof typeof chartDataMap]);
// 图表配置
function getChartOption(): EChartsOption {
const data = chartData.value;
return {
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(0, 0, 0, 0.8)',
borderColor: 'transparent',
textStyle: {
color: '#fff',
fontSize: 12
},
formatter: function(params: any) {
const item = params[0];
return `${item.name}: ${item.value}%`;
}
},
grid: {
left: '0',
right: '20px',
top: '20px',
bottom: '40px',
containLabel: true
},
xAxis: {
type: 'category',
data: data.map(item => item.name),
axisLine: {
show: false
},
axisTick: {
show: false
},
axisLabel: {
color: '#979797',
fontSize: 14,
fontFamily: 'PingFang SC'
}
},
yAxis: {
type: 'value',
min: 0,
max: 100,
interval: 20,
axisLine: {
show: false
},
axisTick: {
show: false
},
axisLabel: {
color: '#979797',
fontSize: 14,
fontFamily: 'PingFang SC'
},
splitLine: {
lineStyle: {
color: '#D4D4D5',
type: 'dashed'
}
}
},
series: [{
type: 'bar',
data: data.map(item => ({
value: item.value,
itemStyle: {
color: item.highlight ? '#FF6B6B' : '#C62828',
borderRadius: [8, 8, 0, 0]
}
})),
barWidth: 40,
label: {
show: false
}
}]
};
}
const chartOption = computed(getChartOption);
// 初始化图表
function initChart() {
if (!chartRef.value) return;
chartInstance = echarts.init(chartRef.value);
chartInstance.setOption(chartOption.value);
// 自动调整大小
window.addEventListener('resize', handleResize);
}
// 处理窗口大小变化
function handleResize() {
if (chartInstance) {
chartInstance.resize();
}
}
// 处理标签切换
function handleTabChange(index: number) {
activeTab.value = index;
if (chartInstance) {
chartInstance.setOption(chartOption.value, true);
}
}
// 监听配置变化
watch(() => chartOption.value, () => {
if (chartInstance) {
chartInstance.setOption(chartOption.value, true);
}
}, { deep: true });
onMounted(() => {
initChart();
});
onUnmounted(() => {
if (chartInstance) {
chartInstance.dispose();
}
window.removeEventListener('resize', handleResize);
});
</script>
<style lang="scss" scoped>
.learning-progress {
width: 100%;
height: 371px;
background: #FFFFFF;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 10px;
padding: 30px 30px 40px 30px;
.progress-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 30px;
.header-left {
display: flex;
align-items: center;
gap: 8px;
}
.progress-title {
font-family: 'PingFang SC';
font-weight: 600;
font-size: 20px;
line-height: 38px;
color: #141F38;
margin: 0;
}
.update-time {
font-family: 'PingFang SC';
font-weight: 400;
font-size: 14px;
line-height: 22px;
color: #B4B8BF;
margin: 0;
}
}
.tab-buttons {
display: flex;
gap: 4px;
background: #F9F9F9;
border: 1px solid #EBEBEB;
border-radius: 8px;
padding: 4px;
.tab-btn {
padding: 8px;
min-width: 71px;
height: 40px;
background: transparent;
border: none;
border-radius: 6px;
font-family: 'PingFang SC';
font-weight: 600;
font-size: 14px;
line-height: 22px;
color: #B4B8BF;
cursor: pointer;
transition: all 0.3s;
&.active {
background: #FFFFFF;
color: #C62828;
}
&:hover:not(.active) {
color: #141F38;
}
}
}
}
.chart-container {
height: 243px;
}
</style>

View File

@@ -434,8 +434,8 @@ async function loadTaskList() {
try {
const pageParam: PageParam = {
page: pagination.value.current,
size: pagination.value.pageSize
pageNumber: pagination.value.current,
pageSize: pagination.value.pageSize
};
const filter: any = {};

View File

@@ -1,3 +1,4 @@
export { default as LearningTaskAdd } from './LearningTaskAdd.vue';
export { default as LearningTaskList } from './LearningTaskList.vue';
export { default as LearingTaskDetail } from './LearingTaskDetail.vue';
export { default as LearingTaskDetail } from './LearningTaskDetail.vue';
export { default as LearningProgress } from './LearningProgress.vue';

View File

@@ -1,7 +1,142 @@
<template>
<div class="home-view"></div>
<div class="home-view">
<!-- 轮播横幅区域 -->
<div class="banner-section">
<Carousel
:items="banners"
:interval="5000"
:active-icon="dangIcon"
indicator-position="bottom-right"
>
<template #default="{ item }">
<BannerCard :banner="item" />
</template>
</Carousel>
</div>
<!-- 热门资源推荐 -->
<div class="section">
<div class="section-header">
<h2 class="section-title">热门资源推荐</h2>
<div class="more-link">
<span>查看更多</span>
<el-icon><ArrowRight /></el-icon>
</div>
</div>
<div class="article-grid">
<HotArticleCard v-for="item in 3" :key="item" />
</div>
</div>
<!-- 思政新闻概览 -->
<div class="section">
<div class="section-header">
<h2 class="section-title">思政新闻概览</h2>
<div class="more-link">
<span>查看更多</span>
<el-icon><ArrowRight /></el-icon>
</div>
</div>
<div class="article-grid">
<IdeologicalArticleCard v-for="item in 3" :key="item" />
</div>
</div>
<!-- 我的学习数据 -->
<div class="section">
<div class="section-header">
<h2 class="section-title">我的学习数据</h2>
<div class="more-link">
<span>查看更多</span>
<el-icon><ArrowRight /></el-icon>
</div>
</div>
<LearningProgress />
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { BannerCard, LearningProgress } from '@/views/public/';
import { HotArticleCard, IdeologicalArticleCard } from '@/views/public/article';
import { Carousel } from '@/components/base';
import { ArrowRight } from '@element-plus/icons-vue';
import dangIcon from '@/assets/imgs/dang.svg';
// 模拟轮播数据,实际应该从接口获取
const banners = ref([
{ id: 1, imageUrl: '', linkType: 1, linkID: '', linkUrl: '' },
{ id: 2, imageUrl: '', linkType: 1, linkID: '', linkUrl: '' },
]);
</script>
<style lang="scss" scoped>
.home-view {
background-color: #F9F9F9;
min-height: 100vh;
padding-bottom: 60px;
}
.banner-section {
width: 100%;
height: 30vh;
}
.section {
max-width: 1440px;
margin: 0 auto;
padding: 0 120px;
margin-top: 60px;
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
.section-title {
font-family: 'PingFang SC';
font-weight: 600;
font-size: 28px;
line-height: 38px;
color: #141F38;
margin: 0;
}
.more-link {
display: flex;
align-items: center;
gap: 2px;
cursor: pointer;
transition: color 0.3s;
span {
font-family: 'PingFang SC';
font-weight: 400;
font-size: 16px;
line-height: 24px;
color: rgba(20, 31, 56, 0.3);
}
.el-icon {
width: 17px;
height: 17px;
color: rgba(20, 31, 56, 0.3);
}
&:hover {
span, .el-icon {
color: rgba(20, 31, 56, 0.6);
}
}
}
}
.article-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
}
}
</style>

View File

@@ -143,10 +143,10 @@ onMounted(() => {
});
// 获取当前用户ID
const getUserID = () => {
function getUserID() {
const userInfo = store.getters['auth/user'];
return userInfo?.id || '';
};
}
// 加载任务列表(用户视角)
async function loadTaskList() {
@@ -161,8 +161,8 @@ async function loadTaskList() {
// 调用用户任务分页接口
const pageParam = {
page: 1,
size: 100 // 获取所有任务,不做分页
pageNumber: 1,
pageSize: 100 // 获取所有任务,不做分页
};
const filter: TaskItemVO = {