zxl
2025-08-15 cd24728b34f86f552b0270791f26fdbe9051b994
抽奖活动
2个文件已添加
865 ■■■■■ 已修改文件
manager/src/api/activity-prize.js 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
manager/src/views/activity-prize/index.vue 830 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
manager/src/api/activity-prize.js
New file
@@ -0,0 +1,35 @@
import service from "../libs/axios";
export const getPage = (params) =>{
  return service({
    url: "/lmk/activity-prize",
    method: "GET",
    params: params
  })
}
export const detail = (params) =>{
  return service({
    url: "/lmk/activity-prize/" +params,
    method: "GET",
  })
}
export const edit = (params) =>{
  return service({
    url: "/lmk/activity-prize",
    method: "PUT",
    data: params
  })
}
export const add = (params) =>{
  return service({
    url: "/lmk/activity-prize",
    method: "POST",
    data: params
  })
}
export const del = (params) =>{
  return service({
    url: "/lmk/activity-prize/" +params,
    method: "DELETE",
  })
}
manager/src/views/activity-prize/index.vue
New file
@@ -0,0 +1,830 @@
<template>
  <div>
    <Card>
      <!-- 搜索表单 -->
      <Form
        ref="searchForm"
        @keydown.enter.native="handleSearch"
        :model="searchForm"
        inline
        :label-width="80"
        class="search-form"
      >
        <FormItem label="活动名称" prop="activityName">
          <Input
            type="text"
            v-model="searchForm.activityName"
            placeholder="请输入活动名称"
            clearable
            @on-clear="handleSearch"
            style="width: 180px"
          />
        </FormItem>
        <FormItem label="报名开始时间" prop="beginTime">
          <DatePicker
            :value="searchForm.beginTime"
            type="datetime"
            placeholder="选择开始时间"
            style="width: 180px"
            value-format="YYYY-MM-DD HH:mm:ss"
            @on-change="handleSearch('beginTime', $event)"
            @on-clear="handleSearch"
          ></DatePicker>
        </FormItem>
        <FormItem label="报名结束时间" prop="endTime">
          <DatePicker
            value-format="YYYY-MM-DD HH:mm:ss"
            :value="searchForm.endTime"
            type="datetime"
            placeholder="选择结束时间"
            style="width: 180px"
            @on-clear="handleSearch"
            @on-change="handleSearch('endTime', $event)"
          ></DatePicker>
        </FormItem>
        <Button
          @click="handleSearch"
          type="primary"
          icon="ios-search"
          class="search-btn"
        >搜索</Button>
        <Button
          @click="resetSearch"
          icon="md-refresh"
          style="margin-left: 8px"
        >重置</Button>
      </Form>
      <!-- 操作按钮 -->
      <Row class="operation">
        <Button @click="openAdd" type="primary" icon="md-add">新增抽奖活动</Button>
      </Row>
      <!-- 活动表格 -->
      <Table
        :loading="loading"
        border
        :columns="columns"
        :data="activityList"
        ref="table"
        class="activity-table"
      >
        <!-- 操作按钮插槽 -->
        <template slot-scope="{ row }" slot="action">
          <div class="action-btns">
            <Button
              type="primary"
              size="small"
              @click="changeStatus(row, row.enableStatus === 'on' ? '关闭' : '开启')"
              :loading="row.statusLoading"
            >
              {{row.enableStatus === 'on' ? '关闭' : '开启'}}
            </Button>
            <Button
              type="info"
              size="small"
              @click="detail(row)"
            >详情</Button>
            <Button
              type="info"
              size="small"
              @click="setPrize(row)"
            >奖品设置</Button>
            <Button
              type="info"
              size="small"
              @click="openEdit(row)"
            >编辑</Button>
            <Button
              type="error"
              size="small"
              @click="delById(row)"
            >删除</Button>
          </div>
        </template>
      </Table>
      <!-- 分页 -->
      <Row type="flex" justify="end" class="page-footer">
        <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"
        @on-cancel="modelClose"
        width="800"
        :mask-closable="false"
      >
        <Form ref="form" :model="activityFrom" :label-width="100" :rules="rules" >
          <Row :gutter="16">
            <Col span="12">
              <FormItem label="活动名称" prop="activityName">
                <Input
                  v-model="activityFrom.activityName"
                  placeholder="请输入活动名称"
                  clearable
                />
              </FormItem>
            </Col>
            <Col span="12">
              <FormItem label="活动描述" prop="activityDes" :label-width="100">
                <Input v-model="activityFrom.activityDes" type="textarea" :autosize="{minRows: 2,maxRows: 5}" placeholder="请输入"></Input>
              </FormItem>
            </Col>
            <Col span="12">
              <FormItem label="开始时间" prop="beginTime">
                <DatePicker
                  v-model="activityFrom.beginTime"
                  type="datetime"
                  placeholder="选择开始时间"
                  style="width: 180px"
                ></DatePicker>
              </FormItem>
            </Col>
            <Col span="12">
              <FormItem label="结束时间" prop="endTime">
                <DatePicker
                  v-model="activityFrom.endTime"
                  type="datetime"
                  placeholder="请选择结束时间"
                  style="width: 180px"
                ></DatePicker>
              </FormItem>
            </Col>
            <Col span="12">
              <FormItem label="每日最大抽奖上限" prop="maxPrize">
                <InputNumber
                  v-model="activityFrom.maxPrize"
                  :min="1"
                  placeholder="请输入每日最大抽奖上限"
                  style="width: 180px"
                />
              </FormItem>
            </Col>
            <Col span="12">
              <FormItem label="初始化抽奖次数" prop="prizeNum" >
                <InputNumber
                  :min="1"
                  v-model="activityFrom.prizeNum"
                  placeholder="请输入初始化抽奖次数"
                  style="width: 180px"
                />
              </FormItem>
            </Col>
            <Col span="24">
              <FormItem label="活动图片:" prop="activityImg">
                <Upload
                  :before-upload="(file) => handleBeforeUpload(file, 'content')"
                  :format="['jpg','jpeg','png','gif']"
                  :max-size="20480"
                  action=""
                  accept="image/*"
                >
                  <Button icon="ios-cloud-upload-outline">上传封面图片</Button>
                  <div class="upload-tip">支持图片,最大20MB</div>
                </Upload>
                <div v-if="activityImg" class="upload-file-info">
                  <p>已选文件: {{ activityImg.name }}</p>
                  <img :src="activityImg.activityImgUrl" alt="活动图片" style="max-width: 100%; max-height: 200px;">
                  <Button type="text" @click="handleRemove('content')">删除</Button>
                </div>
                <!-- 基于elementUi的上传组件 el-upload end-->
              </FormItem>
            </Col>
            <Col span="24">
              <FormItem label="活动封面:" prop="activityCover">
                <Upload
                  :before-upload="(file) => handleBeforeUpload(file, 'cover')"
                  :format="['jpg','jpeg','png','gif']"
                  :max-size="20480"
                  action=""
                  accept="image/*"
                >
                  <Button icon="ios-cloud-upload-outline">上传封面图片</Button>
                  <div class="upload-tip">支持图片,最大20MB</div>
                </Upload>
                <div v-if="activityCover" class="upload-file-info">
                  <p>已选文件: {{ activityCover.name }}</p>
                  <img :src="activityCover.activityCoverUrl" alt="活动图片" style="max-width: 100%; max-height: 200px;">
                  <Button type="text" @click="handleRemove('cover')">删除</Button>
                </div>
                <!-- 基于elementUi的上传组件 el-upload end-->
              </FormItem>
            </Col>
          </Row>
        </Form>
        <div slot="footer">
          <Button @click="modelClose">取消</Button>
          <Button type="primary" :loading="submitLoading" @click="saveOrUpdate">提交</Button>
        </div>
      </Modal>
      <!-- 详情-->
      <Modal
        v-model="infoModalShow"
        title="活动详情"
        @on-cancel="infoModelClose"
        width="800"
        :mask-closable="false"
      >
        <div class="detail-container">
        <Row :gutter="16">
          <Col span="12">
            <div class="detail-item">
              <label>活动名称:</label>
              <span>{{ detailData.activityName }}</span>
            </div>
          </Col>
          <Col span="12">
            <div class="detail-item">
              <label>活动描述:</label>
              <span>{{ detailData.activityDes}}</span>
            </div>
          </Col>
          <Col span="12">
            <div class="detail-item">
              <label>开始时间:</label>
              <span>{{ formatDate(detailData.beginTime) }}</span>
            </div>
          </Col>
          <Col span="12">
            <div class="detail-item">
              <label>结束时间:</label>
              <span>{{ formatDate(detailData.endTime) }}</span>
            </div>
          </Col>
          <Col span="12">
            <div class="detail-item">
              <label>每日最大抽奖上限:</label>
              <span>{{ detailData.maxPrize || '0' }}</span>
            </div>
          </Col>
          <Col span="12">
            <div class="detail-item">
              <label>初始化抽奖次数:</label>
              <span>{{ detailData.prizeNum || '0' }}</span>
            </div>
          </Col>
          <Col span="24">
            <div class="detail-item">
              <label>活动状态:</label>
              <Tag :color="detailData.enableStatus === 'on' ? 'success' : 'default'">
                {{ detailData.enableStatus === 'on' ? '已启用' : '未启用' }}
              </Tag>
            </div>
          </Col>
          <Col span="24">
            <div class="detail-item">
              <label>活动图片:</label>
              <div v-if="detailData.activityImg" class="detail-image">
                <img :src="detailData.activityImg" alt="活动图片" style="max-width: 100%; max-height: 200px;">
              </div>
              <span v-else>-</span>
            </div>
          </Col>
          <Col span="24">
            <div class="detail-item">
              <label>活动封面:</label>
              <div v-if="detailData.activityCover" class="detail-image">
                <img
                  :src="detailData.activityCover" alt="活动封面"
                  style="max-width: 100%; max-height: 200px;"
                >
              </div>
              <span v-else>-</span>
            </div>
          </Col>
          <Col span="24">
            <div class="detail-item">
              <label>奖品:</label>
            </div>
          </Col>
        </Row>
      </div>
      </Modal>
    </Card>
  </div>
</template>
<script>
import {
  getPage,
  detail,
  edit,
  add,
  del
} from '@/api/activity-prize.js'
import {delByKey, uploadFileByLmk} from "../../api/common";
export default {
  name: "activityPrize",
  data() {
    return {
      infoModalShow:false,
      detailData: {},
      modelShow:false,
      modelTitle:'',
      rules: {
        activityDes:[
          { required: true, message: '请输入活动描述', trigger: 'blur' },
          { max: 100, message: '长度不能超过100个字符', trigger: 'blur' }
        ],
        activityName: [
          { required: true, message: '请输入活动名称', trigger: 'blur' },
          { max: 50, message: '长度不能超过50个字符', trigger: 'blur' }
        ],
        beginTime: [
          { type:'date',required: true, message: '请选择开始时间', trigger: 'change'},
          {
            trigger: ['change' ],
            validator: this.validateBeginTime
          }
        ],
        endTime: [
          { type:'date',required: true, message: '请选择结束时间', trigger: 'change'},
          {  trigger: ['change'] ,
            validator: this.validateEndTime
          }
        ],
        maxPrize: [
          {required: true, type: 'number', message: '请输入每日最大抽奖上限', trigger: 'blur'},
          {type: 'number', min: 1, message: '必须大于0', trigger: 'blur'}
        ],
        prizeNum: [
          {required: true, type: 'number', message: '请输入初始化抽奖次数', trigger: 'blur'},
          {type: 'number', min: 1, message: '必须大于0', trigger: 'blur'}
        ],
        activityCover: [
          {required: true, message: '请选择活动封面', trigger: 'blur'}
        ],
        activityImg: [
          {required: true, message: '请选择活动图片', trigger: 'blur'}
        ],
      },
      activityFrom:{
        id:'',
        activityName:'',
        activityDes:'',
        beginTime:'',
        endTime:'',
        maxPrize:0,
        prizeNum:0,
        activityImg:'',
        activityCover:'',
        enableStatus:'off'
      },
      loading:false,
      columns:[
        {
          title: '活动名称',
          key: 'activityName',
          minWidth: 100,
        },
        {
          title: '活动描述',
          key: 'activityDes',
          minWidth: 100,
        },
        {
          title: '开始时间',
          key: 'beginTime',
          minWidth: 100,
          render: (h, params) => {
            return h('div', [
              h('div', this.formatDate(params.row.beginTime)),
            ])
          }
        },
        {
          title: '结束时间',
          key: 'endTime',
          minWidth: 100,
          render: (h, params) => {
            return h('div', [
              h('div', this.formatDate(params.row.endTime)),
            ])
          }
        },
        {
          title: '每日最大抽奖上限',
          key: 'maxPrize',
          minWidth: 100,
        },
        {
          title: '初始化抽奖次数',
          key: 'prizeNum',
          minWidth: 100,
        },
        {
          title: '是否开启活动',
          key: 'enableStatus',
          minWidth: 100,
          render: (h, params) => {
            return h('Tag', {
              props: {
                color: params.row.enableStatus  === 'on' ? 'green' : 'default'
              }
            }, params.row.enableStatus === 'on' ? '开启' : '关闭')
          }
        },
        {
          title: '操作',
          slot: 'action',
          width: 280,
          align: 'center',
          fixed: 'right'
        },
      ],
      activityList:[],
      total:0,
      searchForm:{
        pageSize:10,
        pageNumber:1,
        activityName:'',
        endTime:'',
        beginTime:'',
      },
      submitLoading:false,
      activityCover:null,
      activityImg:null,
    }
  },
  // 在组件创建前注册
  beforeCreate() {
  },
  mounted() {
    this.init();
  },
  methods: {
    validateBeginTime(rule, value, callback) {
      console.log("触发验证")
      // 校验必填
      if (!value) {
        return callback(new Error('请选择开始时间')); // ✅ 正确提示
      }
      console.log("表单开始验证开始时间")
      console.log(value)
      console.log(this.activityFrom.beginTime)
      // 校验开始时间不能晚于结束时间
      if (new Date(this.activityFrom.beginTime) > new Date(this.activityFrom.endTime) ) {
        return callback(new Error('开始时间不能晚于结束时间'));
      }
      callback(); // 验证通过
    },
    validateEndTime(rule, value, callback) {
      // 校验必填
      if (!value) {
        return callback(new Error('请选择结束时间')); // ✅ 正确提示
      }
      // 校验结束时间不能早于开始时间
      if (new Date(this.activityFrom.endTime) < new Date(this.activityFrom.beginTime)) {
        return callback(new Error('结束时间不能早于开始时间'));
      }
      callback(); // 验证通过
    },
    setPrize(row){
    },
    formatDate(date, format = 'YYYY-MM-DD HH:mm:ss') {
      if (!date) return '';
      const d = new Date(date);
      if (isNaN(d.getTime())) return '';
      const padZero = (num) => String(num).padStart(2, '0'); // 更可靠的补零方法
      const year = d.getFullYear();
      const month = padZero(d.getMonth() + 1); // 月份 0-11 → +1
      const day = padZero(d.getDate());
      const hours = padZero(d.getHours());
      const minutes = padZero(d.getMinutes());
      const seconds = padZero(d.getSeconds());
      return format
        .replace('YYYY', year)
        .replace('MM', month)
        .replace('DD', day)
        .replace('HH', hours)
        .replace('mm', minutes)
        .replace('ss', seconds);
    },
    resetSearch(){
      this.$refs.searchForm.resetFields()
      this.searchForm.pageNumber = 1
      this.getPage()
    },
    handleBeforeUpload(file,type){
      if ("content" === type){
        this.activityImg = file
        this.submitLoading = true
        const formData = new FormData()
        formData.append('file', this.activityImg)
        uploadFileByLmk(formData).then(res => {
          this.submitLoading = false
          if (res.code === 200) {
            this.activityFrom.activityImg = res.data.fileKey;
            this.activityImg.activityImgUrl = res.data.url;
            this.$Message.success('上传成功')
          }else{
            this.$Message.error(res.msg)
          }
        }).catch(() => {
          this.submitLoading = false
        })
      }else if ("cover" === type){
        this.activityCover = file
        this.submitLoading = true
        const formData = new FormData()
        formData.append('file', this.activityCover)
        uploadFileByLmk(formData).then(res => {
          this.submitLoading = false
          if (res.code === 200) {
            this.activityFrom.activityCover = res.data.fileKey
            this.activityCover.activityCoverUrl = res.data.url;
            this.$Message.success('上传成功')
          }else{
            this.$Message.error(res.msg)
          }
        }).catch(() => {
          this.submitLoading = false
        })
      }
    },
    // 删除文件
    handleRemove(type) {
      if ("content" === type){
        //点击关闭窗口时确保文件已被清除
        if (this.activityImg === null) {
          return;
        }
        if (!this.activityFrom.activityImg) {
          this.activityImg = null
          return
        }
        delByKey(this.activityFrom.activityImg).then(res => {
          if (res.code === 200) {
            this.activityImg = null
            this.activityFrom.activityImg = ''
          }else{
            this.$Message.error(res.msg)
          }
        })
      }else if ("cover" === type){
        if (this.activityCover === null) {
          return;
        }
        if (!this.activityFrom.activityCover) {
          this.activityCover = null
          return
        }
        delByKey(this.activityFrom.activityCover).then(res => {
          if (res.code === 200) {
            this.activityCover = null
            this.activityFrom.activityCover = ''
          }else{
            this.$Message.error(res.msg)
          }
        })
      }
    },
    saveOrUpdate(){
      const submitData = {
        ...this.activityFrom,
        beginTime: this.formatDate(this.activityFrom.beginTime, 'YYYY-MM-DD HH:mm:ss'),
        endTime: this.formatDate(this.activityFrom.endTime, 'YYYY-MM-DD HH:mm:ss')
      };
      console.log(submitData)
      this.$refs.form.validate(valid => {
        if (valid) {
          this.submitLoading = true
          const api = this.activityFrom.id ? edit : add
          api(submitData).then(res => {
            this.submitLoading = false
            if (res.code === 200) {
              this.$Message.success(res.msg)
              this.modelClose()
              this.getPage()
            }else{
              this.$Message.error(res.msg)
            }
          }).catch(() => {
            this.submitLoading = false
          })
        }
      })
    },
    infoModelClose(){
      this.infoModalShow = false;
    },
    modelClose(){
      this.modelShow = false
      this.submitLoading = false
      this.handleRemove("content");
      this.handleRemove("cover");
      this.$refs.form.resetFields()
    },
    getPage(){
      this.loading = true;
      getPage(this.searchForm).then(res =>{
        this.loading = false;
        if (res.code === 200){
          this.activityList = res.data;
        }else {
          this.$Message.error(res.msg)
        }
      })
    },
    detail(row){
      this.infoModalShow = true;
      this.detailData = {...row};
    },
    openEdit(row){
      this.modelShow = true;
      this.modelTitle = "编辑活动";
      this.activityFrom= {
        id:row.id,
        activityName:row.activityName,
        activityDes:row.activityDes,
        beginTime:this.formatDate(row.beginTime, 'YYYY-MM-DD HH:mm:ss'),
        endTime: this.formatDate(row.endTime, 'YYYY-MM-DD HH:mm:ss'),
        maxPrize:row.maxPrize,
        prizeNum:row.prizeNum,
        activityImg:row.activityImg,
        activityCover:row.activityCover,
        enableStatus:row.enableStatus,
      }
    },
    handleSearch(type,value){
      if (type === 'beginTime') {
        this.searchForm.beginTime = value
      } else if (type === 'endTime') {
        this.searchForm.endTime = value
      }
      this.getPage()
    },
    openAdd(){
      this.modelShow = true;
      this.modelTitle = "新增活动";
    },
    delById(row){
      del(row.id).then(res=>{
        if (res.code === 200){
          this.$Message.success(res.msg)
        }else {
          this.$Message.error(res.msg)
        }
        this.getPage()
      })
    },
    // 获取富文本编辑器的内容
    // 初始化数据
    init() {
      this.getPage()
    },
    changePage(){
      this.searchForm.pageNumber = 1
      this.searchForm.pageSize = pageSize
      this.getPage()
    },
    changePageSize(){
      this.searchForm.pageNumber = page
      this.getPage()
    },
    changeStatus(){
    },
  },
}
</script>
<style lang="scss" scoped>
.detail-container {
  padding: 16px;
}
.detail-item {
  margin-bottom: 16px;
  line-height: 1.5;
}
.detail-item label {
  display: inline-block;
  width: 120px;
  font-weight: bold;
  color: #515a6e;
  vertical-align: top;
}
.detail-item span {
  display: inline-block;
  max-width: calc(100% - 120px);
}
.detail-image {
  display: inline-block;
  margin-top: 8px;
  border: 1px dashed #dcdee2;
  padding: 4px;
  border-radius: 4px;
  background: #f8f8f9;
}
.search-form {
  padding: 16px;
  background: #f8f8f9;
  border-radius: 4px;
  margin-bottom: 16px;
  .ivu-form-item {
    margin-bottom: 16px;
    margin-right: 16px;
  }
  .search-btn {
    margin-left: 8px;
  }
}
.operation {
  margin-bottom: 16px;
  .ivu-btn {
    margin-right: 8px;
  }
}
.activity-table {
  .media-container {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100px;
    .thumbnail {
      max-width: 100%;
      max-height: 100%;
      object-fit: contain;
      cursor: pointer;
      transition: all 0.3s;
      &:hover {
        transform: scale(1.05);
        box-shadow: 0 0 8px rgba(0, 0, 0, 0.2);
      }
    }
    .video-player {
      max-width: 100%;
      max-height: 100px;
      background: #000;
    }
    .text-cover {
      padding: 8px;
      background: #f8f8f9;
      border-radius: 4px;
      max-width: 100%;
      word-break: break-all;
    }
  }
  .action-btns {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    .ivu-btn {
      margin: 4px;
      font-size: 12px;
      padding: 2px 6px;
      min-width: 60px;
    }
  }
}
</style>