zxl
2025-05-21 0e0e55ae0f7b15fee74f9bebe7ed0ac80f3e2430
活动管理(列表,推荐,发布)
2个文件已修改
2个文件已添加
859 ■■■■■ 已修改文件
manager/src/api/activity.js 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
manager/src/api/common.js 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
manager/src/api/customer.js 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
manager/src/views/activity/index.vue 770 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
manager/src/api/activity.js
New file
@@ -0,0 +1,57 @@
import service from "../libs/axios";
//获取活动列表
export const getActivityList = (params) =>{
  return service({
    url: "/activity/page",
    method: "GET",
    params: params
  })
}
// 添加活动
export const addActivity = (params) =>{
  return service({
    url: "/activity",
    method: "POST",
    data: params
  })
}
// 修改活动
export const editActivity = (params) =>{
  return service({
    url: "/activity",
    method: "PUT",
    data: params
  })
}
// 删除活动
export const delActivityById = (params) => {
  return service({
    url: "/activity/" + params,
    method: "DELETE",
  })
}
// 活动详情
export const activityInfoById = (params) => {
  return service({
    url: "/activity/" + params,
    method: "GET",
  })
}
export const activityChangeStatus = (params) => {
  return service({
    url: "/activity/changeStatus",
    method: "PUT",
    data:params,
  })
}
export const activityChangeRecommend = (params) => {
  return service({
    url: "/activity/changeRecommend",
    method: "PUT",
    data:params,
  })
}
manager/src/api/common.js
@@ -1,4 +1,6 @@
import {commonUrl, getRequest, getRequestWithNoToken, postRequestWithNoToken,uploadFileRequest,uploadFile} from '@/libs/axios';
import service from "../libs/axios";
import {getStore} from "../libs/storage";
// 通过id获取子地区
export const getChildRegion = (id) => {
@@ -30,3 +32,31 @@
export const upLoadFile = (bold) =>{
  return uploadFileRequest(uploadFile,bold);
}
export const uploadFileByLmk = (params) =>{
  return service({
    url: "/lmk/common/upload",
    method: "POST",
    headers:{'Content-Type': 'multipart/form-data'},
    data: params
  })
}
export const delByKey = (params) =>{
  return service({
    url:  "/lmk/common/delByKey",
    method: "DELETE",
    data: params
  })
}
export const getUrl = (params) =>{
  return service({
    url: "/lmk/common/getUrl/" + params,
    method:"GET",
  })
}
manager/src/api/customer.js
@@ -69,6 +69,8 @@
    method: "DELETE",
  })
}
// 商家下拉
export const getStoreSelectOptions = () =>{
  return service({
    url: '/customerManager/store/selectOption',
manager/src/views/activity/index.vue
New file
@@ -0,0 +1,770 @@
<template>
  <div>
    <card>
      <Form
        ref="searchForm"
        @keydown.enter.native="handleSearch"
        :model="searchForm"
        inline
        :label-width="70"
        class="search-form"
      >
        <Form-item label="活动名" prop="activityName">
          <Input
            type="text"
            v-model="searchForm.activityName"
            clearable
            @on-clear="handleSearch"
            @on-change="handleSearch"
            style="width: 160px"
          />
        </Form-item>
        <Form-item label="活动类型" prop="activityType">
          <Select
            v-model="searchForm.activityType"
            style="width:260px"
            class="custom-select"
            clearable
            @on-clear="handleSearch"
            @on-change="handleSearch"
          >
            <Option
              v-for="item in typeSelect"
              :value="item.value"
              :key="item.id"
            >
              {{ item.value }}
            </Option>
          </Select>
        </Form-item>
        <Form-item label="活动报名开始时间" prop="reportStartTime">
          <DatePicker :value="searchForm.reportStartTime"
                      type="datetime" placeholder="选择日期"
                      @on-change="handleSearch('reportStart',$event)"
                      @on-clear="handleSearch"
                      ></DatePicker>
        </Form-item>
        <Form-item label="活动报名结束时间" prop="reportEndTime">
          <DatePicker :value="searchForm.reportEndTime"
                      type="datetime" placeholder="选择日期"
                      @on-clear="handleSearch"
                      @on-change="handleSearch('reportEnd',$event)"></DatePicker>
        </Form-item>
<!--        <Form-item label="状态" prop="status">-->
<!--          <Input-->
<!--            type="text"-->
<!--            v-model="searchForm.status"-->
<!--            clearable-->
<!--            @on-clear="handleSearch"-->
<!--            @on-change="handleSearch"-->
<!--            style="width: 160px"-->
<!--          />-->
<!--        </Form-item>-->
        <Button
          @click="handleSearch"
          type="primary"
          icon="ios-search"
          class="search-btn"
        >搜索</Button
        >
      </Form>
      <Row class="operation padding-row">
        <Button @click="openAdd" type="info">添加</Button>
        <Button @click="delBatch" type="error">批量删除</Button>
      </Row>
      <Table
        :loading="loading"
        border
        :columns="columns"
        :data="activityList"
        ref="table"
        sortable="custom"
        @on-sort-change="changeSort"
        @on-selection-change="showSelect"
      >
        <template  slot-scope="{ row }" slot="url">
            <!-- 图片类型 -->
            <template v-if="row.coverType === '图片'">
                <img width="300" height="300"
                  :src="row.url"
                  alt="封面"
                  class="thumbnail"
                  @click="previewImage(row.url)"
                  @error="handleImageError"
                >
            </template>
            <!-- 视频类型 -->
            <template v-else-if="row.coverType === '视频'">
                <video width="300"
                       height="300"
                  :src="row.url"
                  class="video-player"
                  controls
                  @error="handleVideoError"
                ></video>
            </template>
            <!-- 文字类型 -->
            <template v-else-if="row.coverType === text">
                {{ row.cover || '暂无文字内容' }}
            </template>
        </template>
        <template slot-scope="{ row, index }" slot="action">
          <Button type="info" size="small" style="margin-right: 5px" @click="changeRecommend(row,row.recommend === true ? '取消推荐' : '推荐')">
            {{ row.recommend === true ? '取消推荐' : '推荐' }}
          </Button>
          <Button type="info" size="small" style="margin-right: 5px" @click="changeStatus(row,row.status === '已发布' ? '下架' : '发布')">
            {{ row.status === '已发布' ? '下架' : '发布' }}
          </Button>
          <Button type="info" size="small" style="margin-right: 5px" @click="openEdit(row)">编辑标签</Button>
          <Button type="error" size="small" style="margin-right: 5px" @click="delById(row)">删除</Button>
        </template>
      </Table>
      <Row type="flex" justify="end" class="mt_10">
        <Page
          :current="searchForm.pageNumber"
          :total="total"
          :page-size="searchForm.pageSize"
          @on-change="changePage"
          @on-page-size-change="changePageSize"
          :page-size-opts="[10, 20, 50]"
          size="small"
          show-total
          show-elevator
          show-sizer
        ></Page>
      </Row>
      <Modal
        v-model="modelShow"
        :title="modelTitle"
        @close="modelClose()"
        width="800"
      >
        <Form ref="form" :model="activityFrom" :label-width="70" :rules="rules" >
          <Row :gutter="16">
            <Col span="12">
              <FormItem label="活动名称" prop="activityName">
                <Input
                  type="text"
                  v-model="activityFrom.activityName"
                  clearable
                  style="width: 300px"
                />
              </FormItem>
            </Col>
            <Col span="12">
              <FormItem label="活动类型" prop="activityType" >
                <Select
                  v-model="activityFrom.activityType"
                  class="custom-select"
                  clearable
                  style="width: 100px;"
                >
                  <Option
                    v-for="item in typeSelect"
                    :value="item.value"
                    :key="item.id"
                  >
                    {{ item.value }}
                  </Option>
                </Select>
              </FormItem>
            </Col>
            <Col span="12">
              <FormItem label="报名时间段" prop="reportTime">
                <DatePicker v-model="activityFrom.reportTime" style="width: 300px"
                            :value="activityFrom.reportTime"
                            @on-change="activityFrom.reportTime =$event"
                            format="yyyy-MM-dd HH:mm:ss"
                            type="datetimerange"  placeholder="请选择"></DatePicker>
              </FormItem>
            </Col>
            <Col span="12">
              <FormItem label="活动时间段" prop="time">
                <DatePicker v-model="activityFrom.time" style="width: 300px"
                            :value="activityFrom.time"
                            @on-change="activityFrom.time =$event"
                            format= "yyyy-MM-dd HH:mm:ss"
                            type="datetimerange"  placeholder="请选择"></DatePicker>
              </FormItem>
            </Col>
            <Col span="24">
              <FormItem label="封面" prop="cover">
                <Select
                  v-model="coverType"
                  class="custom-select"
                  clearable
                  style="width: 150px;"
                >
                  <Option
                    v-for="item in coverTypeOptions"
                    :value="item.value"
                    :key="item.id"
                  >
                    {{ item.value }}
                  </Option>
                </Select>
                <template>
                  <div v-if="coverTypeJudgment('text')">
                    <template>
                      <Input
                        type="text"
                        v-model="activityFrom.cover"
                        clearable
                        style="width: 300px"
                        placeholder="输入文本或选择文件"
                      />
                    </template>
                  </div>
                  <div v-if="coverTypeJudgment('file')">
                    <Upload
                      :before-upload="handleBeforeUpload"
                      action="">
                      <Button icon="ios-cloud-upload-outline">上传</Button>
                    </Upload>
                    <div v-if="file !== null">
                      Upload file: {{ file.name }}
                      <Button type="text" @click="handleRemove" :loading="loadingStatus">
                        {{ loadingStatus ? '上传中' : '删除' }}
                      </Button>
                    </div>
                  </div>
                </template>
            </FormItem>
            </Col>
            <Col span="24">
              <FormItem label="活动内容" prop="activityContent">
                <Input v-model="activityFrom.activityContent" type="textarea" :autosize="{minRows: 2,maxRows: 5}" placeholder="输入" />
              </FormItem>
            </Col>
          </ROW>
        </Form>
        <div slot="footer">
          <Button type="text" @click="modelClose">取消</Button>
          <Button type="primary" :loading="submitLoading" @click="saveOrUpdate">提交</Button>
        </div>
      </Modal>
    </card>
  </div>
</template>
<script>
import JsonExcel from "vue-json-excel";
import {getActivityList,addActivity,editActivity,delActivityById,activityChangeStatus,activityChangeRecommend} from "@/api/activity.js"
import {uploadFileByLmk,delByKey,getUrl} from "@/api/common.js"
export default {
  name:"activity",
  components:{
    "download-excel": JsonExcel,
  },
  data(){
    return{
      loading: false, // 表单加载状态
      typeSelect:[
        {
          id:1,
          value:'线上'
        },
        {
          id:2,
          value:'线下'
        }
      ],
      coverTypeOptions:[
        {
          id:1,
          value: '输入文字封面'
        },
        {
          id:2,
          value: '选择文件封面'
        },
      ],
      coverType:'',
      //查询活动列表请求参数
      searchForm:{
        activityName:'',// 活动名称
        activityType:'',// 活动类型
        recommend:'',// 推荐
        reportStartTime:'',// 活动报名时间
        reportEndTime:'',// 活动结束时间
        pageNumber: 1, // 当前页数
        pageSize: 10, // 页面大小
      },
      //活动列表表头
      columns: [
        {
          type: 'selection',
          width: 60,
          align: 'center'
        },
        {
          title:'活动名',
          key: 'activityName',
          minWidth: 60,
          tooltip: true,
        },
        {
          title:'活动类型',
          key: 'activityType',
          tooltip: true,
        },
        // {
        //   title:'状态',
        //   key: 'status',
        //   tooltip: true,
        // },
        {
          title:'推荐',
          key: 'recommend',
          tooltip: true,
          render: (h, params) => {
            const sexText = params.row.recommend === true ? '是' : '否';
            return h('span', sexText);
          }
        },
        {
          title:'活动报名开始时间',
          key: 'reportStartTime',
          minWidth: 60,
          tooltip: true,
        },
        {
          title:'活动报名结束时间',
          key: 'reportEndTime',
          minWidth: 60,
          tooltip: true,
        },
        {
          title:'活动开始时间',
          key: 'reportEndTime',
          minWidth: 60,
          tooltip: true,
        },
        {
          title:'活动结束时间',
          key: 'reportEndTime',
          minWidth: 60,
          tooltip: true,
        },
        {
          title:'封面',
          key: 'url',
          slot:"url",
          minWidth: 400,
          tooltip: true,
        },
        {
          title:'封面类型',
          key: 'coverType',
          tooltip: true,
        },
        {
          title:'最大报名人数限制',
          key: 'limitUserNum',
          tooltip: true,
        },
        {
          title:'活动地点',
          key: 'activityLocation',
          tooltip: true,
        },
        {
          title:'活动详细内容',
          key: 'activityContent',
          tooltip: true,
        },
        {
          title: '操作',
          key: 'action',
          slot: 'action',
          minWidth: 100,
          align: 'center'
        }
      ],
      //活动列表数据
      activityList:[],
      total:0,
      selectCount: 0, // 已选数量
      selectList: [], // 已选数据列表
      //活动对话框---
      modelShow:false,
      submitLoading:false,
      modelTitle:'',
      activityFrom:{
        id:'',
        activityName:'',
        activityType:'',
        reportTime:[],
        time:[],
        activityContent:'', // 活动内容
        cover:'', //image/2025052014565362541.jpg
        coverType:'',
        status:'', //活动状态
        reportStartTime:'',
        reportEndTime:'',
        startTime:'',
        endTime:'',
        recommend:'',
        // 报名条件
        // 报名费用
      },
      rules: {
        activityName: {required: true, message: "活动名称", trigger: "blur"},
        activityType: {required: true, message: "活动类型", trigger: "blur"},
        reportTime: [{type: 'array',
          required: true,
          message: "报名日期", trigger: "change"
        }],
        // time: [{type: 'array',
        //   required: true,
        //   fields: {
        //     0: {type: 'date', required: true, message: '请选择起止日期'},
        //     1: {type: 'date', required: true, message: '请选择起止日期'}
        //   }
        // }],
        time: [{type: 'array',
          required: true,
          message: "活动日期", trigger: "change"
        }],
        activityContent: {required: true, message: "活动内容", trigger: "blur"},
        cover: {required: true, message: "封面", trigger: "blur"},
      },
      // 上传
      file: null,
      loadingStatus: false,
      text:'文字',
    }
  },
  mounted(){
    this.init();
  },
  methods:{
    changeRecommend(row,recommend){
      const form = {
        ...this.activityFrom
      };
      form.id = row.id;
      if (recommend === "取消推荐"){
        form.recommend = false
      }else if (recommend ==="推荐"){
        form.recommend = true
      }
      activityChangeRecommend(form).then(res =>{
        if (res.code === 200){
          this.getActivityList();
        }
      })
    },
    changeStatus(row,status){
      const form = {
        ...this.activityFrom
      };
      form.id = row.id;
      if (status === "发布"){
        form.status = "已发布"
      }else if (status ==="下架"){
        form.status = "已下架"
      }
      activityChangeStatus(form).then(res =>{
        if (res.code === 200){
          this.getActivityList();
        }
      })
    },
    coverTypeJudgment(type){
      if (this.coverType === '输入文字封面' && type === 'text'){
        this.activityFrom.coverType = this.text;
        return true;
      }
      if (this.coverType === '选择文件封面' && type === 'file'){
        return true;
      }
      return false
    },
    //自动上传 false
    handleBeforeUpload (file) {
      const fileCategory = this.getFileCategory(file.type);
      console.log('文件分类:', fileCategory); // 输出:图片/视频/文档 等
      // 2. 你的其他逻辑(如类型验证)
      if (fileCategory === '未知') {
        this.$Message.error('不支持的文件类型');
        return false;
      }
      this.file = file;
      this.activityFrom.coverType = fileCategory;
      this.upload();
      return false
    },
    getFileCategory(mimeType) {
      // MIME类型前缀映射表
      const mimeMap = {
        'jpg' : '图片',
        'image': '图片',    // image/jpeg, image/png 等
        'video': '视频',    // video/mp4, video/quicktime 等
        'audio': '音频',    // audio/mpeg, audio/wav 等
        'application': '文档' // application/pdf, application/msword 等
      };
      // 获取类型前缀(如 image/png → image)
      const typePrefix = mimeType.split('/')[0];
      // 返回对应分类或未知
      return mimeMap[typePrefix] || '未知';
    },
    //上传图片接口
    upload () {
      this.submitLoading = true;
      this.loadingStatus = true;
      const formData = new FormData()
      formData.append('file', this.file)
      uploadFileByLmk(formData).then(res =>{
        this.loadingStatus = false;
        this.submitLoading = false;
        if (res.code === 200){
          this.activityFrom.cover = res.data.fileKey;
          this.$Message.success(res.msg)
        }
      })
    },
    handleRemove(){
        this.file = null
        this.loadingStatus = true;
        delByKey(this.activityFrom.cover).then(res =>{
          this.loadingStatus = false;
          if (res.code === 200){
            this.activityFrom.cover = null;
          }
        })
    },
    // 获得客户表格信息
    getActivityList(){
      this.loading = true;
      getActivityList(this.searchForm).then(res =>{
        this.loading = false;
        if (res.code === 200) {
          this.activityList = res.data;
          this.total = res.total;
        }
      })
    },
    init(){
      this.getActivityList();
    },
    changeSort(){
    },
    showSelect(){
      this.selectList = e.map(d => d.id);
      this.selectCount = e.length;
    },
    //
    openEdit(row){
      console.log(row)
      this.modelShow = true
      this.modelTitle = "编辑活动"
      this.activityFrom.id = row.id;
      this.activityFrom.activityName = row.activityName;
      this.activityFrom.activityType = row.activityType;
      this.activityFrom.activityContent = row.activityContent;
      // this.activityFrom.cover = row.cover;
      //判断封面类型赋值给前端
      if (row.coverType === this.text){
        this.coverType = '输入文字封面'
        this.activityFrom.cover = row.cover
        this.activityFrom.coverType = row.coverType
      }else{
        this.coverType = '选择文件封面'
        this.activityFrom.cover = row.cover
        this.activityFrom.coverType = row.coverType
      }
      //转换格式
      this.activityFrom.reportTime=
        this.formatDate(new Date(row.reportStartTime))+' - '+ this.formatDate(new Date(row.reportEndTime));
     //转化格式
      this.activityFrom.time =
        this.formatDate(new Date(row.startTime))+' - '+ this.formatDate(new Date(row.endTime));
    },
    validateDateTime(dateTimeString) {
      const dateTimeRegex = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]) ([01]\d|2[0-3]):[0-5]\d:[0-5]\d$/;
      return dateTimeRegex.test(dateTimeString);
    },
    saveOrUpdate(){
      //判断正则格式 ,全部转换成本地时间 并且转换为指定格式
      if (!this.validateDateTime(this.activityFrom.reportTime[0])){
        this.activityFrom.reportStartTime = this.formatDate(this.activityFrom.reportTime[0]);
      }else {
        this.activityFrom.reportStartTime = this.activityFrom.reportTime[0]
      }
      if (!this.validateDateTime(this.activityFrom.reportTime[1])){
        this.activityFrom.reportEndTime =this.formatDate(this.activityFrom.reportTime[1]);
      }else {
        this.activityFrom.reportEndTime = this.activityFrom.reportTime[1]
      }
      if (!this.validateDateTime(this.activityFrom.time[0])){
        this.activityFrom.startTime = this.formatDate(this.activityFrom.time[0]);
      }else {
        this.activityFrom.startTime = this.activityFrom.time[0]
      }
      if (!this.validateDateTime(this.activityFrom.time[1])){
        this.activityFrom.endTime = this.formatDate(this.activityFrom.time[1]);
      }else {
        this.activityFrom.endTime = this.activityFrom.time[1]
      }
      this.$refs.form.validate(valid => {
        if (valid) {
          this.submitLoading = true
          if (this.activityFrom.id) {
            editActivity(this.activityFrom).then(res =>{
              if (res.code === 200) {
                this.$Message.success(res.msg)
                this.modelClose();
                this.getActivityList();
              }
            })
          }else {
            addActivity(this.activityFrom).then(res => {
              if (res.code === 200) {
                this.$Message.success(res.msg)
                this.modelClose();
                this.getActivityList();
              }
            })
          }
        }
      });
    },
    formatDate(date) {
      if (date !== null && date !== undefined  && date !== ''){
        const year = date.getFullYear();
        const month = String(date.getMonth() + 1).padStart(2, '0'); // 补零
        const day = String(date.getDate()).padStart(2, '0');
        const hour = String(date.getHours()).padStart(2, '0');
        const minutes = String(date.getMinutes()).padStart(2, '0');
        const second = String(date.getSeconds()).padStart(2, '0');
        return `${year}-${month}-${day} ${hour}:${minutes}:${second}`;
      }
      return null
    },
    // 搜索
    handleSearch(type,$event){
      if(type === 'reportStart'){
        this.searchForm.reportStartTime = $event;
      }else if(type === 'reportEnd'){
        this.searchForm.reportEndTime = $event;
      }
      this.searchForm.pageNumber = 1;
      this.searchForm.pageSize = 10;
      this.getActivityList();
    },
    // 关闭弹窗
    modelClose() {
      this.file = null
      this.submitLoading = false
      this.modelShow = false
      this.coverType = null
      this.$refs.form.resetFields()
    },
    openAdd(){
      this.modelTitle = "新增活动"
      this.modelShow = true
    },
    // 删除
    delById(row){
      delActivityById(row.id).then(res =>{
        if (res.code === 200){
          this.$Message.success(res.msg)
          this.getActivityList();
        }
      })
    },
    // 批量删除
    delBatch(){
      if (this.selectCount <= 0) {
        this.$Message.warning("您还未选择要删除的数据");
        return;
      }
      this.$Modal.confirm({
        title: "确认删除",
        content: "您确认要删除所选的 " + this.selectCount + " 条数据?",
        loading: true,
        onOk: () => {
        }
      });
    },
    // 页码
    changePage(v){
      this.searchForm.pageNumber = v
      this.getActivityList()
    },
    // 修改size
    changePageSize(v){
      this.searchForm.pageNumber = 1;
      this.searchForm.pageSize = v;
      this.getActivityList()
    },
    handleImageError(){
    },
    handleVideoError(){
    },
    previewImage(){
    }
  }
}
</script>
<style lang="scss" scoped>
.export {
  margin: 10px 20px 10px 0;
}
.export-excel-wrapper {
  display: inline;
}
.order-tab {
  width: 950px;
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  background-color: #f0f0f0;
  padding: 0 10px;
  margin-bottom: 10px;
  div {
    text-align: center;
    padding: 4px 12px;
    border-radius: 4px;
    cursor: pointer;
  }
  .current {
    background-color: #ffffff;
  }
}
</style>