|
|
@@ -1,18 +1,12 @@
|
|
|
<template>
|
|
|
<div class="p-2 food-safety-page">
|
|
|
- <!-- 页面标题 -->
|
|
|
- <div class="page-header">
|
|
|
- <h2 class="page-title">食品安全宣传</h2>
|
|
|
- <p class="page-desc">开展各类食品安全宣传活动并归集宣传资料</p>
|
|
|
- </div>
|
|
|
-
|
|
|
<!-- ① 顶部数据看板 -->
|
|
|
<el-row :gutter="20" class="mb-2">
|
|
|
<el-col :span="6">
|
|
|
<el-card shadow="hover" class="stat-card">
|
|
|
<div class="stat-card-content">
|
|
|
<div class="stat-info">
|
|
|
- <div class="stat-title">总宣传活动</div>
|
|
|
+ <div class="stat-title">宣传宣传活动</div>
|
|
|
<div class="stat-value">{{ stats.totalCount || 0 }}</div>
|
|
|
<div class="stat-extra stat-extra-up" v-if="stats.totalRate !== undefined">
|
|
|
<el-icon v-if="stats.totalRate >= 0"><CaretTop /></el-icon>
|
|
|
@@ -21,7 +15,7 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="stat-icon stat-icon-blue">
|
|
|
- <el-icon><Calendar /></el-icon>
|
|
|
+ <el-icon><Microphone /></el-icon>
|
|
|
</div>
|
|
|
</div>
|
|
|
</el-card>
|
|
|
@@ -32,8 +26,10 @@
|
|
|
<div class="stat-info">
|
|
|
<div class="stat-title">参与人次</div>
|
|
|
<div class="stat-value">{{ stats.participantsTotal || 0 }}</div>
|
|
|
- <div class="stat-extra stat-extra-up">
|
|
|
- <span>累计参与人数</span>
|
|
|
+ <div class="stat-extra stat-extra-up" v-if="stats.participantsRate !== undefined">
|
|
|
+ <el-icon v-if="stats.participantsRate >= 0"><CaretTop /></el-icon>
|
|
|
+ <el-icon v-else><CaretBottom /></el-icon>
|
|
|
+ <span>{{ Math.abs(stats.participantsRate) }}% 本月</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="stat-icon stat-icon-green">
|
|
|
@@ -48,8 +44,10 @@
|
|
|
<div class="stat-info">
|
|
|
<div class="stat-title">宣传资料数量</div>
|
|
|
<div class="stat-value">{{ stats.materialTotal || 0 }}</div>
|
|
|
- <div class="stat-extra stat-extra-up">
|
|
|
- <span>全部上传资料</span>
|
|
|
+ <div class="stat-extra stat-extra-up" v-if="stats.materialRate !== undefined">
|
|
|
+ <el-icon v-if="stats.materialRate >= 0"><CaretTop /></el-icon>
|
|
|
+ <el-icon v-else><CaretBottom /></el-icon>
|
|
|
+ <span>新增{{ Math.abs(stats.materialRate) }}份 本月</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="stat-icon stat-icon-purple">
|
|
|
@@ -65,7 +63,7 @@
|
|
|
<div class="stat-title">即将进行</div>
|
|
|
<div class="stat-value">{{ stats.notStartedCount || 0 }}</div>
|
|
|
<div class="stat-extra stat-extra-warning">
|
|
|
- <span>未开始状态活动</span>
|
|
|
+ <span>未来一周内</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="stat-icon stat-icon-orange">
|
|
|
@@ -77,109 +75,223 @@
|
|
|
</el-row>
|
|
|
|
|
|
<!-- ② 宣传活动列表 -->
|
|
|
- <el-card shadow="hover">
|
|
|
- <template #header>
|
|
|
- <div class="section-header">
|
|
|
- <span class="section-title">宣传活动列表</span>
|
|
|
- <el-button type="primary" :icon="Plus" @click="handleAdd">新增活动</el-button>
|
|
|
+ <el-card shadow="hover" class="list-card">
|
|
|
+ <!-- 工具栏:左侧按钮 / 右侧筛选 -->
|
|
|
+ <div class="list-toolbar">
|
|
|
+ <div class="toolbar-left">
|
|
|
+ <el-button type="primary" :icon="Plus" @click="handleAdd">新增宣传活动</el-button>
|
|
|
+ <el-button :icon="Download" @click="handleExport">导出数据</el-button>
|
|
|
</div>
|
|
|
- </template>
|
|
|
-
|
|
|
- <!-- 筛选 -->
|
|
|
- <el-form ref="queryFormRef" :model="queryParams" :inline="true" class="filter-bar">
|
|
|
- <el-form-item label="活动名称">
|
|
|
- <el-input
|
|
|
- v-model="queryParams.keyword"
|
|
|
- placeholder="搜索活动名称/编号/负责人"
|
|
|
- clearable
|
|
|
- style="width: 240px"
|
|
|
- @keyup.enter="handleQuery"
|
|
|
- />
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="活动类型">
|
|
|
+ <div class="toolbar-right">
|
|
|
<el-select
|
|
|
- v-model="queryParams.campaignType"
|
|
|
- placeholder="全部类型"
|
|
|
+ v-model="queryParams.status"
|
|
|
+ placeholder="全部状态"
|
|
|
clearable
|
|
|
- style="width: 200px"
|
|
|
+ style="width: 140px"
|
|
|
@change="handleQuery"
|
|
|
>
|
|
|
- <el-option label="全部类型" value="" />
|
|
|
+ <el-option label="全部状态" value="" />
|
|
|
<el-option
|
|
|
- v-for="d in campaignTypeDict"
|
|
|
+ v-for="d in campaignStatusDict"
|
|
|
:key="d.value"
|
|
|
:label="d.label"
|
|
|
:value="d.value"
|
|
|
/>
|
|
|
</el-select>
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="状态">
|
|
|
<el-select
|
|
|
- v-model="queryParams.status"
|
|
|
- placeholder="全部状态"
|
|
|
+ v-model="queryParams.campaignType"
|
|
|
+ placeholder="全部类型"
|
|
|
clearable
|
|
|
- style="width: 160px"
|
|
|
+ style="width: 200px"
|
|
|
@change="handleQuery"
|
|
|
>
|
|
|
- <el-option label="全部状态" value="" />
|
|
|
+ <el-option label="全部类型" value="" />
|
|
|
<el-option
|
|
|
- v-for="d in campaignStatusDict"
|
|
|
+ v-for="d in campaignTypeDict"
|
|
|
:key="d.value"
|
|
|
:label="d.label"
|
|
|
:value="d.value"
|
|
|
/>
|
|
|
</el-select>
|
|
|
- </el-form-item>
|
|
|
- <el-form-item>
|
|
|
- <el-button @click="resetQuery">重置</el-button>
|
|
|
- <el-button type="primary" @click="handleQuery">搜索</el-button>
|
|
|
- </el-form-item>
|
|
|
- </el-form>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
|
|
|
- <el-table v-loading="loading" :data="campaignList" border stripe>
|
|
|
- <el-table-column label="活动编号" align="center" prop="campaignCode" width="160" :show-overflow-tooltip="true" />
|
|
|
- <el-table-column label="活动名称" align="center" prop="campaignName" min-width="180" :show-overflow-tooltip="true" />
|
|
|
- <el-table-column label="活动类型" align="center" prop="campaignType" width="200">
|
|
|
+ <el-table v-loading="loading" :data="campaignList">
|
|
|
+ <el-table-column label="宣传动名称" align="left" min-width="240" :show-overflow-tooltip="true">
|
|
|
<template #default="scope">
|
|
|
- {{ getDictLabel(campaignTypeDict, scope.row.campaignType) }}
|
|
|
+ <div class="campaign-name-cell">
|
|
|
+ <div class="campaign-name">{{ scope.row.campaignName }}</div>
|
|
|
+ <div class="campaign-id">#{{ scope.row.campaignCode }}</div>
|
|
|
+ </div>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
- <el-table-column label="开始时间" align="center" prop="startTime" width="170">
|
|
|
- <template #default="scope">{{ formatDateTime(scope.row.startTime) }}</template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column label="结束时间" align="center" prop="endTime" width="170">
|
|
|
- <template #default="scope">{{ formatDateTime(scope.row.endTime) }}</template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column label="参与人数" align="center" width="160">
|
|
|
+ <el-table-column label="类型" align="center" prop="campaignType" width="140">
|
|
|
<template #default="scope">
|
|
|
- <span>实际 {{ scope.row.actualParticipants || 0 }} / 预计 {{ scope.row.expectedParticipants || 0 }}</span>
|
|
|
+ {{ getDictLabel(campaignTypeDict, scope.row.campaignType) }}
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
- <el-table-column label="负责人" align="center" prop="principalName" width="100" />
|
|
|
+ <el-table-column label="开始时间" align="center" prop="startTime" width="120">
|
|
|
+ <template #default="scope">{{ formatDate(scope.row.startTime) }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="结束时间" align="center" prop="endTime" width="120">
|
|
|
+ <template #default="scope">{{ formatDate(scope.row.endTime) }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="参与人数" align="center" width="100" prop="actualParticipants" />
|
|
|
<el-table-column label="状态" align="center" width="100">
|
|
|
<template #default="scope">
|
|
|
<el-tag :type="getStatusTag(scope.row.status)">{{ scope.row.status || '-' }}</el-tag>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
- <el-table-column label="操作" align="center" width="320" fixed="right" class-name="small-padding fixed-width">
|
|
|
+ <el-table-column label="操作" align="center" width="280" fixed="right" class-name="small-padding fixed-width">
|
|
|
<template #default="scope">
|
|
|
- <el-button link type="primary" @click="handleUploadMaterial(scope.row)">上传资料</el-button>
|
|
|
<el-button link type="primary" @click="handleEdit(scope.row)">编辑</el-button>
|
|
|
- <el-button link type="success" v-if="scope.row.status === '未开始'" @click="handleStart(scope.row)">活动开始</el-button>
|
|
|
- <el-button link type="warning" v-if="scope.row.status === '进行中'" @click="openFinishDialog(scope.row)">活动结束</el-button>
|
|
|
+ <el-button
|
|
|
+ v-if="scope.row.status === '未开始'"
|
|
|
+ link
|
|
|
+ type="success"
|
|
|
+ @click="handleStart(scope.row)"
|
|
|
+ >活动开始</el-button>
|
|
|
+ <el-button
|
|
|
+ v-if="scope.row.status === '进行中'"
|
|
|
+ link
|
|
|
+ type="warning"
|
|
|
+ @click="openFinishDialog(scope.row)"
|
|
|
+ >活动结束</el-button>
|
|
|
<el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
</el-table>
|
|
|
|
|
|
+ <div v-if="total > 0" class="pagination-wrapper">
|
|
|
+ <span class="total-text">共 {{ total }} 条记录</span>
|
|
|
+ <pagination
|
|
|
+ v-model:page="queryParams.pageNum"
|
|
|
+ v-model:limit="queryParams.pageSize"
|
|
|
+ :total="total"
|
|
|
+ :layout="'prev, pager, next'"
|
|
|
+ @pagination="getList"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- ②最新宣传资料 -->
|
|
|
+ <el-card shadow="hover" class="mb-2 mt-2">
|
|
|
+ <template #header>
|
|
|
+ <div class="section-header">
|
|
|
+ <span class="section-title">最新宣传资料</span>
|
|
|
+ <div class="section-actions">
|
|
|
+ <el-button type="primary" :icon="UploadFilled" @click="openUploadMaterialDialog">上传资料</el-button>
|
|
|
+ <el-button link type="primary" @click="openAllMaterials">查看全部 ></el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <div v-if="recentMaterials.length === 0" class="empty-tip">暂无宣传资料,请点击列表“上传资料”按钮添加</div>
|
|
|
+
|
|
|
+ <el-row v-else :gutter="20">
|
|
|
+ <el-col
|
|
|
+ v-for="mat in recentMaterials"
|
|
|
+ :key="mat.id"
|
|
|
+ :span="8"
|
|
|
+ class="material-col"
|
|
|
+ >
|
|
|
+ <div class="material-card" @click="handleOpenMaterial(mat)">
|
|
|
+ <div class="material-cover">
|
|
|
+ <el-image
|
|
|
+ v-if="mat.coverUrl"
|
|
|
+ :src="mat.coverUrl"
|
|
|
+ fit="cover"
|
|
|
+ lazy
|
|
|
+ :preview-src-list="[mat.coverUrl]"
|
|
|
+ hide-on-click-modal
|
|
|
+ >
|
|
|
+ <template #error>
|
|
|
+ <div class="material-cover-fallback">无封面</div>
|
|
|
+ </template>
|
|
|
+ </el-image>
|
|
|
+ <div v-else class="material-cover-fallback">无封面</div>
|
|
|
+ </div>
|
|
|
+ <div class="material-info">
|
|
|
+ <div class="material-name" :title="mat.materialName">{{ mat.materialName || '未命名资料' }}</div>
|
|
|
+ <div class="material-summary" :title="mat.materialSummary">{{ mat.materialSummary || '暂无描述' }}</div>
|
|
|
+ <div class="material-meta">
|
|
|
+ <span>更新于 {{ formatDateTime(mat.createTime) }}</span>
|
|
|
+ <el-button
|
|
|
+ link
|
|
|
+ type="primary"
|
|
|
+ size="small"
|
|
|
+ :disabled="!mat.attachmentUrl"
|
|
|
+ @click.stop="handleDownloadMaterial(mat)"
|
|
|
+ >下载</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- ②② 全部宣传资料列表弹窗 -->
|
|
|
+ <el-dialog
|
|
|
+ v-model="allMaterialsDialog.visible"
|
|
|
+ title="全部宣传资料"
|
|
|
+ width="960px"
|
|
|
+ append-to-body
|
|
|
+ :close-on-click-modal="false"
|
|
|
+ >
|
|
|
+ <el-form ref="allMatQueryFormRef" :model="allMatQueryParams" :inline="true" class="filter-bar">
|
|
|
+ <el-form-item label="资料名称">
|
|
|
+ <el-input
|
|
|
+ v-model="allMatQueryParams.keyword"
|
|
|
+ placeholder="搜索资料名称/编号"
|
|
|
+ clearable
|
|
|
+ style="width: 240px"
|
|
|
+ @keyup.enter="loadAllMaterials"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <el-button @click="resetAllMatQuery">重置</el-button>
|
|
|
+ <el-button type="primary" @click="loadAllMaterials">搜索</el-button>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <el-table v-loading="allMaterialsDialog.loading" :data="allMaterials" border stripe>
|
|
|
+ <el-table-column label="封面" align="center" width="100">
|
|
|
+ <template #default="scope">
|
|
|
+ <el-image
|
|
|
+ v-if="scope.row.coverUrl"
|
|
|
+ :src="scope.row.coverUrl"
|
|
|
+ fit="cover"
|
|
|
+ style="width: 60px; height: 40px; border-radius: 4px"
|
|
|
+ :preview-src-list="[scope.row.coverUrl]"
|
|
|
+ hide-on-click-modal
|
|
|
+ />
|
|
|
+ <span v-else class="muted">—</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="资料编号" align="center" prop="materialCode" width="180" :show-overflow-tooltip="true" />
|
|
|
+ <el-table-column label="资料名称" align="center" prop="materialName" min-width="180" :show-overflow-tooltip="true" />
|
|
|
+ <el-table-column label="类型" align="center" prop="materialType" width="120">
|
|
|
+ <template #default="scope">
|
|
|
+ {{ getDictLabel(materialTypeDict, scope.row.materialType) }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="上传人" align="center" prop="uploaderName" width="100" />
|
|
|
+ <el-table-column label="上传时间" align="center" prop="createTime" width="170">
|
|
|
+ <template #default="scope">{{ formatDateTime(scope.row.createTime) }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" align="center" width="160" fixed="right">
|
|
|
+ <template #default="scope">
|
|
|
+ <el-button link type="primary" @click="handleDownloadMaterial(scope.row)">下载</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
<pagination
|
|
|
- v-show="total > 0"
|
|
|
- v-model:page="queryParams.pageNum"
|
|
|
- v-model:limit="queryParams.pageSize"
|
|
|
- :total="total"
|
|
|
- @pagination="getList"
|
|
|
+ v-show="allMatTotal > 0"
|
|
|
+ v-model:page="allMatQueryParams.pageNum"
|
|
|
+ v-model:limit="allMatQueryParams.pageSize"
|
|
|
+ :total="allMatTotal"
|
|
|
+ @pagination="loadAllMaterials"
|
|
|
/>
|
|
|
- </el-card>
|
|
|
+ </el-dialog>
|
|
|
|
|
|
<!-- ③ 新增/编辑活动弹窗 -->
|
|
|
<el-dialog
|
|
|
@@ -190,73 +302,52 @@
|
|
|
:close-on-click-modal="false"
|
|
|
@close="closeAddDialog"
|
|
|
>
|
|
|
- <el-form ref="addFormRef" :model="addForm" :rules="addRules" label-width="100px">
|
|
|
+ <el-form ref="addFormRef" :model="addForm" :rules="addRules" label-width="110px">
|
|
|
<el-form-item label="活动名称" prop="campaignName">
|
|
|
<el-input v-model="addForm.campaignName" placeholder="请输入活动名称" maxlength="200" show-word-limit />
|
|
|
</el-form-item>
|
|
|
- <el-row :gutter="20">
|
|
|
- <el-col :span="12">
|
|
|
- <el-form-item label="活动类型" prop="campaignType">
|
|
|
- <el-select v-model="addForm.campaignType" placeholder="请选择活动类型" style="width: 100%">
|
|
|
- <el-option
|
|
|
- v-for="d in campaignTypeDict"
|
|
|
- :key="d.value"
|
|
|
- :label="d.label"
|
|
|
- :value="d.value"
|
|
|
- />
|
|
|
- </el-select>
|
|
|
- </el-form-item>
|
|
|
- </el-col>
|
|
|
- <el-col :span="12">
|
|
|
- <el-form-item label="负责人" prop="principalName">
|
|
|
- <el-input v-model="addForm.principalName" placeholder="请输入负责人姓名" maxlength="50" />
|
|
|
- </el-form-item>
|
|
|
- </el-col>
|
|
|
- </el-row>
|
|
|
- <el-row :gutter="20">
|
|
|
- <el-col :span="12">
|
|
|
- <el-form-item label="开始时间" prop="startTime">
|
|
|
- <el-date-picker
|
|
|
- v-model="addForm.startTime"
|
|
|
- type="datetime"
|
|
|
- placeholder="请选择开始时间"
|
|
|
- value-format="YYYY-MM-DD HH:mm:ss"
|
|
|
- format="YYYY-MM-DD HH:mm"
|
|
|
- style="width: 100%"
|
|
|
- />
|
|
|
- </el-form-item>
|
|
|
- </el-col>
|
|
|
- <el-col :span="12">
|
|
|
- <el-form-item label="结束时间" prop="endTime">
|
|
|
- <el-date-picker
|
|
|
- v-model="addForm.endTime"
|
|
|
- type="datetime"
|
|
|
- placeholder="请选择结束时间"
|
|
|
- value-format="YYYY-MM-DD HH:mm:ss"
|
|
|
- format="YYYY-MM-DD HH:mm"
|
|
|
- style="width: 100%"
|
|
|
- />
|
|
|
- </el-form-item>
|
|
|
- </el-col>
|
|
|
- </el-row>
|
|
|
- <el-row :gutter="20">
|
|
|
- <el-col :span="12">
|
|
|
- <el-form-item label="活动地点" prop="location">
|
|
|
- <el-input v-model="addForm.location" placeholder="请输入活动地点" maxlength="200" />
|
|
|
- </el-form-item>
|
|
|
- </el-col>
|
|
|
- <el-col :span="12">
|
|
|
- <el-form-item label="预计参与人数" prop="expectedParticipants">
|
|
|
- <el-input-number
|
|
|
- v-model="addForm.expectedParticipants"
|
|
|
- :min="0"
|
|
|
- :max="99999"
|
|
|
- placeholder="请输入预计参与人数"
|
|
|
- style="width: 100%"
|
|
|
- />
|
|
|
- </el-form-item>
|
|
|
- </el-col>
|
|
|
- </el-row>
|
|
|
+ <el-form-item label="活动类型" prop="campaignType">
|
|
|
+ <el-select v-model="addForm.campaignType" placeholder="请选择活动类型" style="width: 100%">
|
|
|
+ <el-option
|
|
|
+ v-for="d in campaignTypeDict"
|
|
|
+ :key="d.value"
|
|
|
+ :label="d.label"
|
|
|
+ :value="d.value"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="开始时间" prop="startTime">
|
|
|
+ <el-date-picker
|
|
|
+ v-model="addForm.startTime"
|
|
|
+ type="datetime"
|
|
|
+ placeholder="请选择开始时间"
|
|
|
+ value-format="YYYY-MM-DD HH:mm:ss"
|
|
|
+ format="YYYY-MM-DD HH:mm"
|
|
|
+ style="width: 100%"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="结束时间" prop="endTime">
|
|
|
+ <el-date-picker
|
|
|
+ v-model="addForm.endTime"
|
|
|
+ type="datetime"
|
|
|
+ placeholder="请选择结束时间"
|
|
|
+ value-format="YYYY-MM-DD HH:mm:ss"
|
|
|
+ format="YYYY-MM-DD HH:mm"
|
|
|
+ style="width: 100%"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="预计参与人数" prop="expectedParticipants">
|
|
|
+ <el-input-number
|
|
|
+ v-model="addForm.expectedParticipants"
|
|
|
+ :min="0"
|
|
|
+ :max="99999"
|
|
|
+ placeholder="请输入预计参与人数"
|
|
|
+ style="width: 100%"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="活动地点" prop="location">
|
|
|
+ <el-input v-model="addForm.location" placeholder="请输入活动地点" maxlength="200" />
|
|
|
+ </el-form-item>
|
|
|
<el-form-item label="活动描述" prop="description">
|
|
|
<el-input
|
|
|
v-model="addForm.description"
|
|
|
@@ -267,8 +358,8 @@
|
|
|
show-word-limit
|
|
|
/>
|
|
|
</el-form-item>
|
|
|
- <el-form-item label="备注" prop="remark">
|
|
|
- <el-input v-model="addForm.remark" placeholder="备注(选填)" maxlength="500" />
|
|
|
+ <el-form-item label="负责人" prop="principalName">
|
|
|
+ <el-input v-model="addForm.principalName" placeholder="请输入负责人姓名" maxlength="50" />
|
|
|
</el-form-item>
|
|
|
</el-form>
|
|
|
<template #footer>
|
|
|
@@ -283,11 +374,11 @@
|
|
|
<el-dialog
|
|
|
v-model="finishDialog.visible"
|
|
|
title="活动结束"
|
|
|
- width="560px"
|
|
|
+ width="640px"
|
|
|
append-to-body
|
|
|
:close-on-click-modal="false"
|
|
|
>
|
|
|
- <el-form ref="finishFormRef" :model="finishForm" :rules="finishRules" label-width="100px">
|
|
|
+ <el-form ref="finishFormRef" :model="finishForm" :rules="finishRules" label-width="110px">
|
|
|
<el-form-item label="活动编号">
|
|
|
<span class="readonly-text">{{ finishForm.campaignCode }}</span>
|
|
|
</el-form-item>
|
|
|
@@ -317,7 +408,7 @@
|
|
|
<template #footer>
|
|
|
<div class="dialog-footer">
|
|
|
<el-button @click="finishDialog.visible = false">取 消</el-button>
|
|
|
- <el-button type="primary" @click="submitFinishForm">确 定 结束</el-button>
|
|
|
+ <el-button type="primary" @click="submitFinishForm">确定结束</el-button>
|
|
|
</div>
|
|
|
</template>
|
|
|
</el-dialog>
|
|
|
@@ -332,9 +423,6 @@
|
|
|
@close="closeMaterialDialog"
|
|
|
>
|
|
|
<el-form ref="materialFormRef" :model="materialForm" :rules="materialRules" label-width="100px">
|
|
|
- <el-form-item label="关联活动">
|
|
|
- <span class="readonly-text">{{ currentCampaign.campaignName || '—' }} <span class="muted">({{ currentCampaign.campaignCode || '无编号' }})</span></span>
|
|
|
- </el-form-item>
|
|
|
<el-form-item label="资料名称" prop="materialName">
|
|
|
<el-input v-model="materialForm.materialName" placeholder="请输入资料名称" maxlength="200" show-word-limit />
|
|
|
</el-form-item>
|
|
|
@@ -427,7 +515,8 @@ import {
|
|
|
finishFoodSafetyCampaign,
|
|
|
getFoodSafetyCampaignStats
|
|
|
} from '@/api/risk/foodSafetyCampaign';
|
|
|
-import { addFoodSafetyMaterial } from '@/api/risk/foodSafetyMaterial';
|
|
|
+import { addFoodSafetyMaterial, listFoodSafetyMaterial } from '@/api/risk/foodSafetyMaterial';
|
|
|
+import type { RiskFoodSafetyMaterialVo } from '@/api/risk/types';
|
|
|
import { uploadFile } from '@/api/risk/file';
|
|
|
import { getDicts } from '@/api/system/dict/data';
|
|
|
import type { DictDataVO } from '@/api/system/dict/data/types';
|
|
|
@@ -438,7 +527,7 @@ import type {
|
|
|
RiskFoodSafetyMaterialBo
|
|
|
} from '@/api/risk/types';
|
|
|
import { ElMessage, FormInstance, UploadUserFile } from 'element-plus';
|
|
|
-import { Calendar, User, Files, Bell, CaretTop, CaretBottom, Plus, UploadFilled } from '@element-plus/icons-vue';
|
|
|
+import { Calendar, User, Files, Bell, CaretTop, CaretBottom, Plus, UploadFilled, Download, VideoPlay, CircleClose, Microphone } from '@element-plus/icons-vue';
|
|
|
import type { ComponentInternalInstance } from 'vue';
|
|
|
|
|
|
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
|
|
@@ -467,8 +556,14 @@ const campaignTypeDict = ref<Array<{ label: string; value: string }>>([]);
|
|
|
const campaignStatusDict = ref<Array<{ label: string; value: string }>>([]);
|
|
|
const materialTypeDict = ref<Array<{ label: string; value: string }>>([]);
|
|
|
|
|
|
-const mapDictData = (list: DictDataVO[]) => {
|
|
|
- return (list || [])
|
|
|
+const mapDictData = (resOrList: any) => {
|
|
|
+ // 字典接口被 axios 拦截器自动解包,返回值为数组;兼容部分场景被再包一层 .data 的情况
|
|
|
+ const list: any[] = Array.isArray(resOrList)
|
|
|
+ ? resOrList
|
|
|
+ : Array.isArray(resOrList?.data)
|
|
|
+ ? resOrList.data
|
|
|
+ : [];
|
|
|
+ return list
|
|
|
.filter((d: any) => d.status === '0' || d.status === undefined)
|
|
|
.sort((a, b) => (a.dictSort || 0) - (b.dictSort || 0))
|
|
|
.map((d) => ({ label: d.dictLabel, value: d.dictValue }));
|
|
|
@@ -481,9 +576,9 @@ const loadDicts = async () => {
|
|
|
getDicts('risk_food_safety_campaign_status'),
|
|
|
getDicts('risk_food_safety_material_type')
|
|
|
]);
|
|
|
- campaignTypeDict.value = mapDictData(typeRes.data || []);
|
|
|
- campaignStatusDict.value = mapDictData(statusRes.data || []);
|
|
|
- materialTypeDict.value = mapDictData(materialRes.data || []);
|
|
|
+ campaignTypeDict.value = mapDictData(typeRes as any);
|
|
|
+ campaignStatusDict.value = mapDictData(statusRes as any);
|
|
|
+ materialTypeDict.value = mapDictData(materialRes as any);
|
|
|
} catch (e) {
|
|
|
console.error('加载食品安全宣传字典失败', e);
|
|
|
}
|
|
|
@@ -534,6 +629,7 @@ const addRules = {
|
|
|
campaignType: [{ required: true, message: '请选择活动类型', trigger: 'change' }],
|
|
|
startTime: [{ required: true, message: '请选择开始时间', trigger: 'change' }],
|
|
|
endTime: [{ required: true, message: '请选择结束时间', trigger: 'change' }],
|
|
|
+ expectedParticipants: [{ required: true, type: 'number', message: '请输入预计参与人数', trigger: 'blur' }],
|
|
|
principalName: [{ required: true, message: '请输入负责人姓名', trigger: 'blur' }]
|
|
|
};
|
|
|
|
|
|
@@ -613,7 +709,6 @@ const handleQuery = () => {
|
|
|
};
|
|
|
|
|
|
const resetQuery = () => {
|
|
|
- queryFormRef.value?.resetFields();
|
|
|
queryParams.value.keyword = '';
|
|
|
queryParams.value.campaignType = '';
|
|
|
queryParams.value.status = '';
|
|
|
@@ -621,8 +716,137 @@ const resetQuery = () => {
|
|
|
getList();
|
|
|
};
|
|
|
|
|
|
+// ============ 导出数据 ============
|
|
|
+const handleExport = async () => {
|
|
|
+ try {
|
|
|
+ ElMessage.info('正在导出数据…');
|
|
|
+ const all = await listFoodSafetyCampaign({
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 5000,
|
|
|
+ keyword: queryParams.value.keyword || undefined,
|
|
|
+ campaignType: queryParams.value.campaignType || undefined,
|
|
|
+ status: queryParams.value.status || undefined
|
|
|
+ } as any);
|
|
|
+ const rows = all?.rows || [];
|
|
|
+ if (rows.length === 0) {
|
|
|
+ ElMessage.warning('暂无可导出数据');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const headers = ['活动编号', '活动名称', '活动类型', '开始时间', '结束时间', '参与人数', '负责人', '状态'];
|
|
|
+ const csv = [headers.join(',')];
|
|
|
+ for (const r of rows) {
|
|
|
+ const typeLabel = getDictLabel(campaignTypeDict.value, r.campaignType) || '';
|
|
|
+ const cells = [
|
|
|
+ r.campaignCode || '',
|
|
|
+ `"${(r.campaignName || '').replace(/"/g, '""')}"`,
|
|
|
+ typeLabel,
|
|
|
+ formatDateTime(r.startTime),
|
|
|
+ formatDateTime(r.endTime),
|
|
|
+ r.actualParticipants ?? 0,
|
|
|
+ `"${(r.principalName || '').replace(/"/g, '""')}"`,
|
|
|
+ r.status || ''
|
|
|
+ ];
|
|
|
+ csv.push(cells.join(','));
|
|
|
+ }
|
|
|
+ // 加 BOM 避免 Excel 打开中文乱码
|
|
|
+ const blob = new Blob(['\uFEFF' + csv.join('\n')], { type: 'text/csv;charset=utf-8;' });
|
|
|
+ const url = URL.createObjectURL(blob);
|
|
|
+ const link = document.createElement('a');
|
|
|
+ const stamp = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14);
|
|
|
+ link.href = url;
|
|
|
+ link.download = `宣传活动列表_${stamp}.csv`;
|
|
|
+ document.body.appendChild(link);
|
|
|
+ link.click();
|
|
|
+ document.body.removeChild(link);
|
|
|
+ URL.revokeObjectURL(url);
|
|
|
+ ElMessage.success(`已导出 ${rows.length} 条数据`);
|
|
|
+ } catch (e: any) {
|
|
|
+ ElMessage.error(e?.msg || '导出失败');
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
const loadAll = async () => {
|
|
|
- await Promise.all([loadStats(), getList()]);
|
|
|
+ await Promise.all([loadStats(), getList(), loadRecentMaterials()]);
|
|
|
+};
|
|
|
+
|
|
|
+// ============ 最新宣传资料 ============
|
|
|
+const recentMaterials = ref<RiskFoodSafetyMaterialVo[]>([]);
|
|
|
+
|
|
|
+const loadRecentMaterials = async () => {
|
|
|
+ try {
|
|
|
+ const res: any = await listFoodSafetyMaterial({ pageNum: 1, pageSize: 3 } as any);
|
|
|
+ // list 接口被 axios 拦截器自动解包,rows 才是真正的表格数据
|
|
|
+ const rows: any[] = Array.isArray(res?.rows) ? res.rows : Array.isArray(res) ? res : [];
|
|
|
+ // 按 createTime 倒序补齐(后端默认按 id 倒序但需要保证一致)
|
|
|
+ recentMaterials.value = [...rows]
|
|
|
+ .sort((a, b) => new Date(b.createTime).getTime() - new Date(a.createTime).getTime())
|
|
|
+ .slice(0, 3);
|
|
|
+ } catch (e) {
|
|
|
+ console.warn('加载最新宣传资料失败', e);
|
|
|
+ recentMaterials.value = [];
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 点击封面:预览大图
|
|
|
+const handleOpenMaterial = (mat: RiskFoodSafetyMaterialVo) => {
|
|
|
+ if (mat.attachmentUrl) {
|
|
|
+ handleDownloadMaterial(mat);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 下载:在浏览器打开附件(OSS 链接可直接下载)
|
|
|
+const handleDownloadMaterial = async (mat: RiskFoodSafetyMaterialVo) => {
|
|
|
+ if (!mat.attachmentUrl) {
|
|
|
+ ElMessage.warning('该资料未上传附件');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const a = document.createElement('a');
|
|
|
+ a.href = mat.attachmentUrl;
|
|
|
+ a.download = mat.attachmentName || (mat.materialName ? `${mat.materialName}` : 'material');
|
|
|
+ a.target = '_blank';
|
|
|
+ a.rel = 'noopener';
|
|
|
+ document.body.appendChild(a);
|
|
|
+ a.click();
|
|
|
+ document.body.removeChild(a);
|
|
|
+};
|
|
|
+
|
|
|
+// ============ 全部宣传资料列表弹窗 ============
|
|
|
+const allMaterialsDialog = reactive({ visible: false, loading: false });
|
|
|
+const allMatQueryFormRef = ref<FormInstance>();
|
|
|
+const allMatQueryParams = ref({
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ keyword: ''
|
|
|
+});
|
|
|
+const allMaterials = ref<RiskFoodSafetyMaterialVo[]>([]);
|
|
|
+const allMatTotal = ref(0);
|
|
|
+
|
|
|
+const loadAllMaterials = async () => {
|
|
|
+ allMaterialsDialog.loading = true;
|
|
|
+ try {
|
|
|
+ const res: any = await listFoodSafetyMaterial(allMatQueryParams.value as any);
|
|
|
+ const payload = res?.rows ? res : { rows: Array.isArray(res) ? res : [], total: 0 };
|
|
|
+ allMaterials.value = payload.rows || [];
|
|
|
+ allMatTotal.value = Number(payload.total) || 0;
|
|
|
+ } catch (e) {
|
|
|
+ ElMessage.error('加载宣传资料失败');
|
|
|
+ allMaterials.value = [];
|
|
|
+ allMatTotal.value = 0;
|
|
|
+ } finally {
|
|
|
+ allMaterialsDialog.loading = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const resetAllMatQuery = () => {
|
|
|
+ allMatQueryFormRef.value?.resetFields();
|
|
|
+ allMatQueryParams.value.keyword = '';
|
|
|
+ allMatQueryParams.value.pageNum = 1;
|
|
|
+ loadAllMaterials();
|
|
|
+};
|
|
|
+
|
|
|
+const openAllMaterials = () => {
|
|
|
+ allMaterialsDialog.visible = true;
|
|
|
+ loadAllMaterials();
|
|
|
};
|
|
|
|
|
|
// ============ 新增/编辑活动 ============
|
|
|
@@ -731,7 +955,10 @@ const submitFinishForm = () => {
|
|
|
finishFormRef.value?.validate(async (valid: boolean) => {
|
|
|
if (!valid) return;
|
|
|
try {
|
|
|
- await finishFoodSafetyCampaign(finishForm.id as number, { summary: finishForm.summary });
|
|
|
+ await finishFoodSafetyCampaign(finishForm.id as number, {
|
|
|
+ summary: finishForm.summary,
|
|
|
+ actualParticipants: Number(finishForm.actualParticipants) || 0
|
|
|
+ });
|
|
|
ElMessage.success('活动已结束');
|
|
|
finishDialog.visible = false;
|
|
|
await loadAll();
|
|
|
@@ -787,6 +1014,24 @@ const handleUploadMaterial = (row: RiskFoodSafetyCampaignVo) => {
|
|
|
materialDialog.visible = true;
|
|
|
};
|
|
|
|
|
|
+// ============ 顶部入口:打开上传资料弹窗(不预选活动) ============
|
|
|
+const openUploadMaterialDialog = () => {
|
|
|
+ materialForm.value = {
|
|
|
+ campaignId: undefined,
|
|
|
+ materialName: '',
|
|
|
+ materialType: '',
|
|
|
+ materialSummary: '',
|
|
|
+ coverUrl: '',
|
|
|
+ coverName: '',
|
|
|
+ attachmentUrl: '',
|
|
|
+ attachmentName: '',
|
|
|
+ attachmentSize: 0
|
|
|
+ } as RiskFoodSafetyMaterialBo;
|
|
|
+ coverFileList.value = [];
|
|
|
+ attachmentFileList.value = [];
|
|
|
+ materialDialog.visible = true;
|
|
|
+};
|
|
|
+
|
|
|
const closeMaterialDialog = () => {
|
|
|
materialDialog.visible = false;
|
|
|
resetMaterialForm();
|
|
|
@@ -796,21 +1041,24 @@ const submitMaterialForm = () => {
|
|
|
materialFormRef.value?.validate(async (valid: boolean) => {
|
|
|
if (!valid) return;
|
|
|
try {
|
|
|
- // 提取封面
|
|
|
+ // 提取封面(包装返回值 + 旧式 response.data 兼容)
|
|
|
const cover: any = coverFileList.value.find((f: any) => f.status === 'success');
|
|
|
+ const coverPayload = cover?.response?.response || cover?.response || null;
|
|
|
if (cover) {
|
|
|
- materialForm.value.coverUrl = cover.url || cover.response?.data?.url || cover.response?.data?.ossId || '';
|
|
|
- materialForm.value.coverName = cover.name || cover.response?.data?.originalName || '';
|
|
|
+ materialForm.value.coverUrl =
|
|
|
+ cover.url || coverPayload?.data?.url || coverPayload?.data?.ossId || '';
|
|
|
+ materialForm.value.coverName = cover.name || coverPayload?.data?.originalName || '';
|
|
|
} else {
|
|
|
materialForm.value.coverUrl = '';
|
|
|
materialForm.value.coverName = '';
|
|
|
}
|
|
|
// 提取附件
|
|
|
const attach: any = attachmentFileList.value.find((f: any) => f.status === 'success');
|
|
|
+ const attachPayload = attach?.response?.response || attach?.response || null;
|
|
|
if (attach) {
|
|
|
materialForm.value.attachmentUrl =
|
|
|
- attach.url || attach.response?.data?.url || attach.response?.data?.ossId || '';
|
|
|
- materialForm.value.attachmentName = attach.name || attach.response?.data?.originalName || '';
|
|
|
+ attach.url || attachPayload?.data?.url || attachPayload?.data?.ossId || '';
|
|
|
+ materialForm.value.attachmentName = attach.name || attachPayload?.data?.originalName || '';
|
|
|
} else {
|
|
|
materialForm.value.attachmentUrl = '';
|
|
|
materialForm.value.attachmentName = '';
|
|
|
@@ -834,18 +1082,23 @@ const ATTACH_MAX_SIZE = 50 * 1024 * 1024; // 50MB
|
|
|
|
|
|
/**
|
|
|
* 自定义上传(走 axios baseURL,避免跨域/405)
|
|
|
+ *
|
|
|
+ * 注:只返回 success/false,el-upload 内部会根据返回值自动触发 on-success 或 on-error。
|
|
|
+ * 不要再手动调用 options.onSuccess / options.onError,否则会导致同一文件同时弹
|
|
|
+ * 成功+失败两条 toast(经典的双回调 bug)。
|
|
|
*/
|
|
|
const customUpload = async (options: any) => {
|
|
|
try {
|
|
|
const res: any = await uploadFile(options.file);
|
|
|
- if (res && res.code === 200 && res.data) {
|
|
|
+ if (res && res.code === 200 && res.data && res.data.url) {
|
|
|
options.file.url = res.data.url;
|
|
|
- options.onSuccess(res, options.file);
|
|
|
+ // 响应包装成 el-upload 期望的格式,on-success 里通过 response.data.url 取值
|
|
|
+ return { success: true, response: res, file: options.file };
|
|
|
} else {
|
|
|
- options.onError(new Error(res?.msg || '上传失败'));
|
|
|
+ return { success: false, error: new Error(res?.msg || '上传失败') };
|
|
|
}
|
|
|
} catch (err: any) {
|
|
|
- options.onError(err);
|
|
|
+ return { success: false, error: err };
|
|
|
}
|
|
|
};
|
|
|
|
|
|
@@ -876,24 +1129,35 @@ const beforeAttachmentUpload = (file: any) => {
|
|
|
};
|
|
|
|
|
|
const handleCoverSuccess = (response: any, file: any) => {
|
|
|
- if (response && response.code === 200) {
|
|
|
+ // customUpload 返回的是包装对象 {success, response, file},response.code 可能是 undefined
|
|
|
+ const realRes = response?.response || response;
|
|
|
+ if (realRes && realRes.code === 200) {
|
|
|
+ // 同步写入表单,避免 prop="coverUrl" 校验一直报红
|
|
|
+ materialForm.value.coverUrl = realRes.data?.url || realRes.data?.ossId || '';
|
|
|
+ materialForm.value.coverName = realRes.data?.originalName || file.name;
|
|
|
ElMessage.success(`封面 ${file.name} 上传成功`);
|
|
|
} else {
|
|
|
- ElMessage.error(`封面 ${file.name} 上传失败: ${response?.msg || '未知错误'}`);
|
|
|
+ ElMessage.error(`封面 ${file.name} 上传失败: ${realRes?.msg || response?.error?.message || '未知错误'}`);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
const handleAttachmentSuccess = (response: any, file: any) => {
|
|
|
- if (response && response.code === 200) {
|
|
|
+ const realRes = response?.response || response;
|
|
|
+ if (realRes && realRes.code === 200) {
|
|
|
+ // 同步写入表单,避免 prop="attachmentUrl" 校验一直报红
|
|
|
+ materialForm.value.attachmentUrl = realRes.data?.url || realRes.data?.ossId || '';
|
|
|
+ materialForm.value.attachmentName = realRes.data?.originalName || file.name;
|
|
|
ElMessage.success(`附件 ${file.name} 上传成功`);
|
|
|
} else {
|
|
|
- ElMessage.error(`附件 ${file.name} 上传失败: ${response?.msg || '未知错误'}`);
|
|
|
+ ElMessage.error(`附件 ${file.name} 上传失败: ${realRes?.msg || response?.error?.message || '未知错误'}`);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
const handleUploadError = (error: any) => {
|
|
|
- console.error('文件上传失败', error);
|
|
|
- ElMessage.error(`文件上传失败: ${error?.message || '网络异常'}`);
|
|
|
+ // 关键:这里的 error 可能是包装对象 {success:false, error},也可能是 Error
|
|
|
+ const realErr = error?.error || error;
|
|
|
+ console.error('文件上传失败', realErr);
|
|
|
+ ElMessage.error(`文件上传失败: ${realErr?.message || '网络异常'}`);
|
|
|
};
|
|
|
|
|
|
const handleCoverExceed = () => {
|
|
|
@@ -917,9 +1181,9 @@ const handleAttachmentRemove = () => {
|
|
|
// ============ 工具 ============
|
|
|
const getStatusTag = (status: string): 'primary' | 'success' | 'warning' | 'info' | 'danger' => {
|
|
|
const map: Record<string, 'primary' | 'success' | 'warning' | 'info' | 'danger'> = {
|
|
|
- 未开始: 'info',
|
|
|
- 进行中: 'warning',
|
|
|
- 已结束: 'success'
|
|
|
+ 未开始: 'warning',
|
|
|
+ 进行中: 'primary',
|
|
|
+ 已结束: 'info'
|
|
|
};
|
|
|
return map[status] || 'info';
|
|
|
};
|
|
|
@@ -932,6 +1196,14 @@ const formatDateTime = (val: any): string => {
|
|
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
|
};
|
|
|
|
|
|
+const formatDate = (val: any): string => {
|
|
|
+ if (!val) return '';
|
|
|
+ const d = new Date(val);
|
|
|
+ if (isNaN(d.getTime())) return String(val);
|
|
|
+ const pad = (n: number) => String(n).padStart(2, '0');
|
|
|
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
|
+};
|
|
|
+
|
|
|
// ============ 生命周期 ============
|
|
|
onMounted(async () => {
|
|
|
await loadDicts();
|
|
|
@@ -973,7 +1245,71 @@ onMounted(async () => {
|
|
|
font-weight: 600;
|
|
|
color: #303133;
|
|
|
}
|
|
|
+.section-actions {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
|
|
|
+/* 列表工具栏 */
|
|
|
+.list-toolbar {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding-bottom: 16px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+.toolbar-left {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+}
|
|
|
+.toolbar-right {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+}
|
|
|
+
|
|
|
+/* 表格活动名称单元格 */
|
|
|
+.campaign-name-cell {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: flex-start;
|
|
|
+ padding: 6px 0;
|
|
|
+ line-height: 1.4;
|
|
|
+}
|
|
|
+.campaign-name {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #303133;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+.campaign-id {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+ margin-top: 2px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 分页区 */
|
|
|
+.pagination-wrapper {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding-top: 16px;
|
|
|
+}
|
|
|
+.total-text {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #606266;
|
|
|
+}
|
|
|
+
|
|
|
+/* 弹窗底部左右布局 */
|
|
|
+.dialog-footer {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
.filter-bar {
|
|
|
margin-bottom: 16px;
|
|
|
}
|
|
|
@@ -1078,4 +1414,112 @@ onMounted(async () => {
|
|
|
line-height: 1.5;
|
|
|
margin-top: 4px;
|
|
|
}
|
|
|
+
|
|
|
+/* 最新宣传资料 3 张卡片 */
|
|
|
+.material-col {
|
|
|
+ margin-bottom: 12px;
|
|
|
+}
|
|
|
+.material-card {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ border-radius: 8px;
|
|
|
+ border: 1px solid #ebeef5;
|
|
|
+ background: #fff;
|
|
|
+ overflow: hidden;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: box-shadow 0.2s ease, transform 0.2s ease;
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+.material-card:hover {
|
|
|
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
|
+ transform: translateY(-2px);
|
|
|
+}
|
|
|
+.material-cover {
|
|
|
+ width: 100%;
|
|
|
+ height: 160px;
|
|
|
+ background: #f5f7fa;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+.material-cover :deep(.el-image) {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+.material-cover-fallback {
|
|
|
+ color: #c0c4cc;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+.material-info {
|
|
|
+ padding: 12px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 6px;
|
|
|
+}
|
|
|
+.material-name {
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+.material-summary {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #606266;
|
|
|
+ line-height: 1.5;
|
|
|
+ display: -webkit-box;
|
|
|
+ -webkit-line-clamp: 2;
|
|
|
+ -webkit-box-orient: vertical;
|
|
|
+ overflow: hidden;
|
|
|
+ min-height: 36px;
|
|
|
+}
|
|
|
+.material-meta {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+ border-top: 1px dashed #ebeef5;
|
|
|
+ padding-top: 6px;
|
|
|
+ margin-top: 2px;
|
|
|
+}
|
|
|
+.empty-tip {
|
|
|
+ color: #909399;
|
|
|
+ font-size: 14px;
|
|
|
+ text-align: center;
|
|
|
+ padding: 24px 0;
|
|
|
+}
|
|
|
+.muted {
|
|
|
+ color: #c0c4cc;
|
|
|
+}
|
|
|
+
|
|
|
+/* 表格卡片化 */
|
|
|
+.list-card {
|
|
|
+ border-radius: 8px;
|
|
|
+ border: none;
|
|
|
+ :deep(.el-card__body) {
|
|
|
+ padding: 16px 20px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 表格表头浅色背景 + 行悬停高亮 */
|
|
|
+:deep(.el-table) {
|
|
|
+ border-radius: 4px;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+:deep(.el-table__header-wrapper th.el-table__cell) {
|
|
|
+ background-color: #f5f7fa !important;
|
|
|
+ color: #303133;
|
|
|
+ font-weight: 600;
|
|
|
+ border-bottom: 1px solid #ebeef5;
|
|
|
+ height: 44px;
|
|
|
+}
|
|
|
+:deep(.el-table__row) {
|
|
|
+ transition: background-color 0.2s;
|
|
|
+}
|
|
|
+:deep(.el-table__row:hover > td.el-table__cell) {
|
|
|
+ background-color: #f5f9ff !important;
|
|
|
+}
|
|
|
</style>
|