定时任务修正

This commit is contained in:
2025-11-17 15:16:11 +08:00
parent 4b167058b6
commit 6e9057f6ee
16 changed files with 444 additions and 496 deletions

View File

@@ -59,11 +59,12 @@
<div class="stat-views">浏览量</div>
<div class="stat-likes">点赞数</div>
<div class="stat-time">发布时间</div>
<div class="stat-actions">操作</div>
</div>
<div
v-for="(article, index) in hotArticles"
:key="article.resourceID"
:key="article.resource?.resourceID"
class="hot-article-row"
:class="{ 'top-three': index < 3 }"
>
@@ -73,28 +74,50 @@
</div>
</div>
<div class="stat-title">
<span class="article-title" :title="article.title">{{ article.title }}</span>
<span class="article-title" :title="article.resource?.title">{{ article.resource?.title }}</span>
</div>
<div class="stat-tag">
<span v-if="article.tagID" class="tag-name">{{ getTagName(article.tagID) }}</span>
<span v-if="article.resource?.tagID" class="tag-name">{{ getTagName(article.resource.tagID) }}</span>
<span v-else class="tag-empty">-</span>
</div>
<div class="stat-author">
<span>{{ article.author || '-' }}</span>
<span>{{ article.resource?.author || '-' }}</span>
</div>
<div class="stat-views">
<div class="stat-number hot">
<img src="@/assets/imgs/hot.svg" alt="浏览" class="stat-icon" />
{{ formatNumber(article.viewCount) }}
{{ formatNumber(article.resource?.viewCount) }}
</div>
</div>
<div class="stat-likes">
<div class="stat-number">
{{ formatNumber(article.likeCount) }}
{{ formatNumber(article.resource?.likeCount) }}
</div>
</div>
<div class="stat-time">
<span class="time-text">{{ formatDate(article.publishTime) }}</span>
<span class="time-text">{{ formatDate(article.resource?.publishTime) }}</span>
</div>
<div class="stat-actions">
<button
v-if="article.resource?.resourceID && !article.isTopRecommend"
class="add-to-top-btn"
@click="addToTopRecommends(article.resource.resourceID)"
:disabled="!!article.resource?.resourceID && addingToTop.has(article.resource.resourceID)"
>
{{ article.resource?.resourceID && addingToTop.has(article.resource.resourceID) ? '添加中...' : '添加至TOP' }}
</button>
<span v-else-if="article.isTopRecommend" class="already-top">已在TOP</span>
<!-- 添加至思政 -->
<button
v-if="article.resource?.resourceID && !article.isIdeologicalRecommend"
class="add-to-ideological-btn"
@click="addToIdeologicalRecommends(article.resource.resourceID)"
:disabled="!!article.resource?.resourceID && addingToIdeological.has(article.resource.resourceID)"
>
{{ article.resource?.resourceID && addingToIdeological.has(article.resource.resourceID) ? '添加中...' : '添加至思政' }}
</button>
<span v-else-if="article.isIdeologicalRecommend" class="already-ideological">已在思政</span>
</div>
</div>
@@ -338,7 +361,7 @@ import { ElDialog, ElInput, ElButton, ElCheckbox, ElIcon, ElForm, ElFormItem, El
import { Search } from '@element-plus/icons-vue';
import { AdminLayout } from '@/views/admin';
import { resourceRecommendApi, resourceApi, resourceTagApi } from '@/apis/resource';
import type { ResourceRecommendVO, Resource, Tag } from '@/types';
import type { ResourceRecommendVO, Resource, ResourceVO, Tag } from '@/types';
defineOptions({
name: 'ColumnManagementView'
@@ -356,9 +379,15 @@ const topRecommends = ref<ResourceRecommendVO[]>([]);
const ideologicalRecommends = ref<ResourceRecommendVO[]>([]);
// 热门文章列表
const hotArticles = ref<Resource[]>([]);
const hotArticles = ref<ResourceVO[]>([]);
const hotArticleLimit = ref<number>(20);
// 添加至TOP的加载状态
const addingToTop = ref<Set<string>>(new Set());
// 添加至思政的加载状态
const addingToIdeological = ref<Set<string>>(new Set());
// 标签列表
const tags = ref<Tag[]>([]);
@@ -381,7 +410,7 @@ const editForm = ref({
orderNum: 1
});
// 挂载时加载标签和当前tab的数据
// 挂载时加载标签数据和默认tab的数据
onMounted(() => {
loadTags();
loadTabData(activeTab.value);
@@ -445,7 +474,7 @@ async function loadHotArticles() {
loading.value = true;
try {
// 获取已发布的文章,按浏览量排序
const result = await resourceApi.getResourcePage(
const result = await resourceApi.getResourcePageOrderByViewCount(
{
pageNumber: 1,
pageSize: hotArticleLimit.value
@@ -458,7 +487,7 @@ async function loadHotArticles() {
if (result.success && result.pageDomain?.dataList) {
// 按浏览量降序排序
hotArticles.value = result.pageDomain.dataList
.sort((a, b) => (b.viewCount || 0) - (a.viewCount || 0));
.sort((a, b) => (b.resource?.viewCount || 0) - (a.resource?.viewCount || 0));
}
} catch (error) {
console.error('加载热门文章失败:', error);
@@ -510,7 +539,7 @@ async function showAddResourceDialog(recommendType: 1 | 2) {
async function searchResources() {
searchLoading.value = true;
try {
const result = await resourceApi.getResourcePage(
const result = await resourceApi.getResourcePageOrderByViewCount(
{ pageNumber: 1, pageSize: 50 },
{
keyword: searchKeyword.value,
@@ -524,9 +553,9 @@ async function searchResources() {
? topRecommends.value.map(r => r.resourceID)
: ideologicalRecommends.value.map(r => r.resourceID);
availableResources.value = result.pageDomain.dataList.filter(
r => !existingIds.includes(r.resourceID)
);
availableResources.value = result.pageDomain.dataList
.map(vo => vo.resource)
.filter(r => r && !existingIds.includes(r.resourceID)) as Resource[];
}
} catch (error) {
console.error('搜索资源失败:', error);
@@ -655,6 +684,79 @@ async function deleteRecommend(item: ResourceRecommendVO) {
}
}
}
// 检查资源是否已在TOP推荐中
function isInTopRecommends(resourceID?: string): boolean {
if (!resourceID) return false;
return topRecommends.value.some(r => r.resourceID === resourceID);
}
// 检查资源是否已在思政推荐中
function isInIdeologicalRecommends(resourceID?: string): boolean {
if (!resourceID) return false;
return ideologicalRecommends.value.some(r => r.resourceID === resourceID);
}
// 根据ResourceVO获取resourceID
function getResourceID(articleVO: ResourceVO): string | undefined {
return articleVO.resource?.resourceID;
}
// 添加至TOP推荐
async function addToTopRecommends(resourceID?: string) {
if (!resourceID) return;
addingToTop.value.add(resourceID);
try {
await resourceRecommendApi.batchAddRecommends([resourceID], 1);
ElMessage.success('添加至TOP推荐成功');
// 重新加载TOP推荐列表
loadedTabs.value.delete('top-resources');
const topResult = await resourceRecommendApi.getRecommendsByType(1);
if (topResult.success && topResult.dataList) {
topRecommends.value = topResult.dataList.sort((a, b) => (a.orderNum || 0) - (b.orderNum || 0));
}
// 重新加载热门文章统计以更新按钮状态
loadedTabs.value.delete('hot-articles');
await loadHotArticles();
} catch (error) {
console.error('添加至TOP推荐失败:', error);
ElMessage.error('添加至TOP推荐失败');
} finally {
addingToTop.value.delete(resourceID);
}
}
// 添加至思政推荐
async function addToIdeologicalRecommends(resourceID?: string) {
if (!resourceID) return;
addingToIdeological.value.add(resourceID);
try {
await resourceRecommendApi.batchAddRecommends([resourceID], 2);
ElMessage.success('添加至思政推荐成功');
// 重新加载思政推荐列表
loadedTabs.value.delete('ideological');
const ideologicalResult = await resourceRecommendApi.getRecommendsByType(2);
if (ideologicalResult.success && ideologicalResult.dataList) {
ideologicalRecommends.value = ideologicalResult.dataList.sort((a, b) => (a.orderNum || 0) - (b.orderNum || 0));
}
// 重新加载热门文章统计以更新按钮状态
loadedTabs.value.delete('hot-articles');
await loadHotArticles();
} catch (error) {
console.error('添加至思政推荐失败:', error);
ElMessage.error('添加至思政推荐失败');
} finally {
addingToIdeological.value.delete(resourceID);
}
}
</script>
<style lang="scss" scoped>
@@ -906,6 +1008,7 @@ async function deleteRecommend(item: ResourceRecommendVO) {
.filter-controls {
display: flex;
gap: 12px;
min-width: 150px;
}
// 热门文章列表
@@ -918,7 +1021,7 @@ async function deleteRecommend(item: ResourceRecommendVO) {
.statistics-header {
display: grid;
grid-template-columns: 80px 1fr 120px 120px 120px 100px 120px;
grid-template-columns: 80px 1fr 120px 120px 120px 100px 120px 150px;
gap: 16px;
padding: 12px 16px;
background: #F9FAFB;
@@ -931,7 +1034,7 @@ async function deleteRecommend(item: ResourceRecommendVO) {
.hot-article-row {
display: grid;
grid-template-columns: 80px 1fr 120px 120px 120px 100px 120px;
grid-template-columns: 80px 1fr 120px 120px 120px 100px 120px 150px;
gap: 16px;
padding: 16px;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
@@ -952,6 +1055,13 @@ async function deleteRecommend(item: ResourceRecommendVO) {
align-items: center;
}
.stat-actions {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.rank-badge {
display: flex;
align-items: center;
@@ -1035,6 +1145,62 @@ async function deleteRecommend(item: ResourceRecommendVO) {
font-size: 13px;
color: #6B7280;
}
.add-to-top-btn {
padding: 6px 12px;
background: #E7000B;
border: none;
border-radius: 6px;
color: #FFFFFF;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
&:hover:not(:disabled) {
background: #c00009;
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
.already-top {
font-size: 13px;
color: #10B981;
font-weight: 500;
}
.add-to-ideological-btn {
padding: 6px 12px;
background: #E7000B;
border: none;
border-radius: 6px;
color: #FFFFFF;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
&:hover:not(:disabled) {
background: #c00009;
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
.already-ideological {
font-size: 13px;
color: #10B981;
font-weight: 500;
}
}
// 对话框样式

View File

@@ -1,30 +0,0 @@
<template>
<AdminLayout
title="内容管理"
subtitle="管理网站横幅、栏目、标签等内容信息"
>
<div class="content-management">
<el-empty description="请使用顶部标签页切换到Banner管理、标签管理或栏目管理" />
</div>
</AdminLayout>
</template>
<script setup lang="ts">
import { AdminLayout } from '@/views/admin';
defineOptions({
name: 'ContentManagementView'
});
</script>
<style lang="scss" scoped>
.content-management {
background: #FFFFFF;
padding: 24px;
border-radius: 14px;
min-height: 400px;
display: flex;
align-items: center;
justify-content: center;
}
</style>

View File

@@ -362,8 +362,10 @@ const formData = reactive<Partial<CrontabTask>>({
});
// 监听模板选择变化
watch(selectedTemplate, (newTemplate) => {
if (newTemplate) {
watch(selectedTemplate, (newTemplate, oldTemplate) => {
// 只在用户手动切换模板时重置oldTemplate存在且不为null时才重置
// 编辑回填时oldTemplate为null不会触发重置
if (newTemplate && oldTemplate) {
selectedMethod.value = null;
dynamicParams.value = {};
}
@@ -476,6 +478,11 @@ function handleEdit(row: CrontabTask) {
isEdit.value = true;
Object.assign(formData, row);
// 重置选择
selectedTemplate.value = null;
selectedMethod.value = null;
dynamicParams.value = {};
// 尝试解析methodParams来回填表单
if (row.methodParams) {
try {
@@ -486,13 +493,20 @@ function handleEdit(row: CrontabTask) {
t.methods.some(m => m.path === params.scriptPath)
);
if (template) {
selectedTemplate.value = template;
const method = template.methods.find(m => m.path === params.scriptPath);
if (method) {
// 先设置template和method触发watch填充默认值
selectedTemplate.value = template;
selectedMethod.value = method;
// 回填动态参数
// 然后使用nextTick确保watch执行完后再覆盖为实际值
// 回填动态参数排除scriptPath
const { scriptPath, ...restParams } = params;
dynamicParams.value = restParams;
// 延迟设置确保watch先执行完
setTimeout(() => {
dynamicParams.value = restParams;
console.log('📝 编辑回填 - template:', template.name, 'method:', method.name, 'params:', restParams);
}, 0);
}
}
}
@@ -645,19 +659,20 @@ async function handleSubmit() {
submitting.value = true;
try {
// 传递taskGroup和methodName中文名后端根据这两个name查找配置并填充beanName、methodName和scriptPath
const data = {
...formData,
taskGroup: selectedTemplate.value.name, // 第一层name作为taskGroup
methodName: selectedMethod.value.name, // 第二层name作为methodName
taskGroup: selectedTemplate.value.name, // 模板名称(中文)
methodName: selectedMethod.value.name, // 方法名称(中文)
methodParams: JSON.stringify({
scriptPath: selectedMethod.value.path,
...dynamicParams.value
...dynamicParams.value // 只传用户输入的参数scriptPath由后端填充
})
};
console.log('📤 准备提交的数据:', data);
console.log('📤 taskGroup (模板名称):', data.taskGroup);
console.log('📤 methodName (方法名称):', data.methodName);
console.log('📤 taskGroup:', selectedTemplate.value.name);
console.log('📤 methodName:', selectedMethod.value.name);
console.log('📤 动态参数:', dynamicParams.value);
let result;
if (isEdit.value) {

View File

@@ -70,133 +70,75 @@
</div>
<!-- 日志表格 -->
<div class="log-table-wrapper">
<table class="log-table">
<thead>
<tr>
<th width="10%">操作人</th>
<th width="10%">操作模块</th>
<th width="10%">操作类型</th>
<th width="10%">请求链接</th>
<th width="10%">IP地址</th>
<th width="10%">操作描述</th>
<!-- <th width="100">耗时(ms)</th> -->
<!-- <th width="100">状态</th> -->
<th width="10%">操作时间</th>
<th width="10%">操作</th>
</tr>
</thead>
<tbody v-if="loading">
<tr>
<td colspan="9" class="loading-cell">
<div class="loading-spinner"></div>
<span>加载中...</span>
</td>
</tr>
</tbody>
<tbody v-else-if="logs.length === 0">
<tr>
<td colspan="9" class="empty-cell">
<div class="empty-icon">📋</div>
<p>暂无操作日志</p>
</td>
</tr>
</tbody>
<tbody v-else>
<tr v-for="log in logs" :key="log.id" class="table-row">
<td>{{ log.username || '-' }}</td>
<td>{{ log.module || '-' }}</td>
<td>
<span class="status-tag" :class="getOperationClass(log.operation)">
{{ getOperationText(log.operation) }}
</span>
</td>
<td class="log-desc">
<div class="desc-text">{{ log.requestUrl || '-' }}</div>
</td>
<td>{{ log.ipAddress || '-' }}</td>
<td class="log-desc">
<div class="desc-text">{{ truncateText(log.responseData, 50) }}</div>
</td>
<!-- <td>
<span class="status-tag" :class="log.status === 'success' ? 'status-success' : 'status-failed'">
{{ log.status === 'success' ? '成功' : '失败' }}
</span>
</td> -->
<td>{{ formatTime(log?.createTime || '') || '-' }}</td>
<el-table
:data="logs"
v-loading="loading"
style="width: 100%"
height="calc(100vh - 340px)"
stripe
>
<el-table-column prop="username" label="操作人" width="120" show-overflow-tooltip>
<template #default="{ row }">
{{ row.username || '-' }}
</template>
</el-table-column>
<td class="action-cell">
<button class="btn-link btn-primary" @click="viewDetail(log)">详情</button>
</td>
</tr>
</tbody>
</table>
</div>
<el-table-column prop="module" label="操作模块" width="150" show-overflow-tooltip>
<template #default="{ row }">
{{ row.module || '-' }}
</template>
</el-table-column>
<el-table-column prop="operation" label="操作类型" width="120">
<template #default="{ row }">
<span class="status-tag" :class="getOperationClass(row.operation)">
{{ getOperationText(row.operation) }}
</span>
</template>
</el-table-column>
<el-table-column prop="requestUrl" label="请求链接" min-width="200" show-overflow-tooltip>
<template #default="{ row }">
{{ row.requestUrl || '-' }}
</template>
</el-table-column>
<el-table-column prop="ipAddress" label="IP地址" width="150" show-overflow-tooltip>
<template #default="{ row }">
{{ row.ipAddress || '-' }}
</template>
</el-table-column>
<el-table-column prop="responseData" label="操作描述" min-width="200" show-overflow-tooltip>
<template #default="{ row }">
{{ truncateText(row.responseData, 50) }}
</template>
</el-table-column>
<el-table-column prop="createTime" label="操作时间" width="180" show-overflow-tooltip>
<template #default="{ row }">
{{ formatTime(row?.createTime || '') || '-' }}
</template>
</el-table-column>
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="viewDetail(row)">详情</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div v-if="!loading && logs.length > 0" class="pagination">
<div class="pagination-info">
{{ total }} 条数据每页 {{ pageSize }}
</div>
<div class="pagination-controls">
<button
class="page-btn"
:disabled="currentPage === 1"
@click="handlePageChange(1)"
>
首页
</button>
<button
class="page-btn"
:disabled="currentPage === 1"
@click="handlePageChange(currentPage - 1)"
>
上一页
</button>
<div class="page-numbers">
<button
v-for="page in displayPages"
:key="page"
class="page-number"
:class="{ active: page === currentPage }"
@click="handlePageChange(page)"
:disabled="page === -1"
>
{{ page === -1 ? '...' : page }}
</button>
</div>
<button
class="page-btn"
:disabled="currentPage === totalPages"
@click="handlePageChange(currentPage + 1)"
>
下一页
</button>
<button
class="page-btn"
:disabled="currentPage === totalPages"
@click="handlePageChange(totalPages)"
>
末页
</button>
</div>
<div class="pagination-jump">
<span>跳转到</span>
<input
v-model.number="jumpPage"
type="number"
class="jump-input"
@keyup.enter="handleJumpPage"
/>
<span></span>
<button class="jump-btn" @click="handleJumpPage">跳转</button>
</div>
<div class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
</div>
@@ -314,49 +256,9 @@ const currentPage = ref(1);
const pageSize = ref(20);
const total = ref(0);
const logs = ref<OperationLog[]>([]);
const jumpPage = ref<number>();
const detailVisible = ref(false);
const currentLog = ref<OperationLog | null>(null);
// 计算总页数
const totalPages = computed(() => Math.ceil(total.value / pageSize.value) || 1);
// 计算显示的页码
const displayPages = computed(() => {
const pages: number[] = [];
const maxDisplay = 7;
if (totalPages.value <= maxDisplay) {
for (let i = 1; i <= totalPages.value; i++) {
pages.push(i);
}
} else {
if (currentPage.value <= 4) {
for (let i = 1; i <= 5; i++) {
pages.push(i);
}
pages.push(-1);
pages.push(totalPages.value);
} else if (currentPage.value >= totalPages.value - 3) {
pages.push(1);
pages.push(-1);
for (let i = totalPages.value - 4; i <= totalPages.value; i++) {
pages.push(i);
}
} else {
pages.push(1);
pages.push(-1);
for (let i = currentPage.value - 1; i <= currentPage.value + 1; i++) {
pages.push(i);
}
pages.push(-1);
pages.push(totalPages.value);
}
}
return pages;
});
onMounted(() => {
loadLogs();
});
@@ -467,20 +369,16 @@ function viewDetail(log: OperationLog) {
* 页码改变
*/
function handlePageChange(page: number) {
if (page < 1 || page > totalPages.value || page === -1) return;
currentPage.value = page;
loadLogs();
}
/**
* 跳转页面
* 每页条数改变
*/
function handleJumpPage() {
if (!jumpPage.value || jumpPage.value < 1 || jumpPage.value > totalPages.value) {
alert('请输入有效的页码');
return;
}
currentPage.value = jumpPage.value;
function handleSizeChange(size: number) {
pageSize.value = size;
currentPage.value = 1; // 重置到第一页
loadLogs();
}
</script>
@@ -585,60 +483,7 @@ function handleJumpPage() {
}
}
.log-table-wrapper {
background: #fff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.log-table {
width: 100%;
border-collapse: collapse;
th {
background: #f5f7fa;
color: #606266;
font-weight: 600;
font-size: 14px;
padding: 12px 16px;
text-align: left;
border-bottom: 2px solid #e0e0e0;
}
td {
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
font-size: 14px;
color: #606266;
}
.table-row {
transition: background-color 0.3s;
&:hover {
background: #f5f7fa;
}
&:last-child td {
border-bottom: none;
}
}
}
.log-desc {
max-width: 300px;
}
.desc-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.align-right {
text-align: right;
}
// el-table会自动处理样式不需要自定义table样式
.status-tag {
display: inline-block;
@@ -678,154 +523,15 @@ function handleJumpPage() {
}
}
.action-cell {
// el-table和el-button会自动处理样式和交互
.pagination-container {
display: flex;
justify-content: center;
align-items: center;
}
.btn-link {
border: none;
padding: 6px 12px;
font-size: 13px;
cursor: pointer;
transition: all 0.3s;
border-radius: 4px;
min-width: 64px;
text-align: center;
color: #fff;
&:hover {
opacity: 0.8;
}
&.btn-primary {
background: #409eff;
}
}
.loading-cell,
.empty-cell {
text-align: center;
padding: 60px 20px !important;
color: #909399;
}
.loading-spinner {
display: inline-block;
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #409eff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 12px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.empty-icon {
font-size: 48px;
margin-bottom: 12px;
}
.pagination {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 20px;
padding: 20px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
flex-wrap: wrap;
gap: 16px;
}
.pagination-info {
font-size: 14px;
color: #606266;
}
.pagination-controls {
display: flex;
gap: 8px;
align-items: center;
}
.page-btn,
.page-number {
padding: 6px 12px;
border: 1px solid #dcdfe6;
background: #fff;
color: #606266;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
&:hover:not(:disabled) {
color: #409eff;
border-color: #409eff;
}
&:disabled {
cursor: not-allowed;
opacity: 0.5;
}
&.active {
background: #409eff;
color: #fff;
border-color: #409eff;
}
}
.page-numbers {
display: flex;
gap: 4px;
}
.pagination-jump {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #606266;
.jump-input {
width: 60px;
padding: 6px 8px;
border: 1px solid #dcdfe6;
border-radius: 4px;
font-size: 14px;
text-align: center;
&:focus {
outline: none;
border-color: #409eff;
}
}
.jump-btn {
padding: 6px 12px;
border: 1px solid #dcdfe6;
background: #fff;
color: #606266;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
&:hover {
color: #409eff;
border-color: #409eff;
}
}
}
// 模态框样式
.modal-overlay {