peng
2025-10-11 52f60fcfc348ad26185c22a074b8551429ac5149
Merge remote-tracking branch 'origin/send_coupon' into send_coupon
2个文件已修改
4个文件已添加
1301 ■■■■■ 已修改文件
manager/src/api/store-tag.js 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
manager/src/views/seller/shop/shopDetail.vue 115 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
manager/src/views/store-tag/store-tag.vue 272 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
seller/src/api/goods-customeize-template.js 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
seller/src/views/store-coupon/coupon_store.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
seller/src/views/template/goodsCustomizeTemplate.vue 790 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
manager/src/api/store-tag.js
New file
@@ -0,0 +1,66 @@
import service from "@/libs/axios";
export const getPageList = (data) =>{
  return service({
    url: "/lmk/store-tag/page",
    method: "GET",
    params: data
  })
}
export const getTag = (data) =>{
  return service({
    url: "/lmk/store-tag/list",
    method: "GET",
    params: data
  })
}
export const addStoreTag =(data) =>{
  return service({
    url: "/lmk/store-tag",
    method: "POST",
    data: data
  })
}
export const bind =(data) =>{
  return service({
    url: "/lmk/store-tag/bind",
    method: "POST",
    data: data
  })
}
export const editTag = (data)=>{
  return service({
    url: "/lmk/store-tag",
    method: "PUT",
    data: data
  })
}
export const delTag = (data)=>{
  return service({
    url: "/lmk/store-tag/" + data,
    method: "DELETE",
  })
}
export const removeBatch = (data) =>{
  return service({
    url: "/lmk/store-tag/batch",
    method: "DELETE",
    data:data
  })
}
export const getStoreTagsById =(data) =>{
  return service({
    url: "/lmk/store-tag/storeTags/" + data,
    method: "GET",
  })
}
export const delTagByStoreTagRefId = (data)=>{
  return service({
    url: "/lmk/store-tag/delTagByStoreTagRefId/" + data,
    method: "DELETE",
  })
}
manager/src/views/seller/shop/shopDetail.vue
@@ -121,6 +121,29 @@
            </span>
          </p>
          <p class="item">
            <span class="label">店铺标签:</span>
            <span class="info">
              <template v-if="tagList?.length > 0">
              <!-- 遍历标签列表 -->
                <span v-for="(tag, index) in tagList" :key="tag.id">
                  {{ tag.tagName }}
                    <button
                      style="width: 20px; height: 20px; line-height: 1; border: none; background: #f0f0f0; border-radius: 50%; cursor: pointer; margin-left: 4px; font-size: 12px; display: inline-flex; align-items: center; justify-content: center;"
                      @click.stop="handleDeleteTag(tag.id, index)"
                      title="删除标签"
                    >
                    ×
                  </button>
                  <template v-if="index !== tagList.length - 1">、</template>
                </span>
              </template>
            <template v-else>暂无标签</template>
            </span>
          </p>
          <div style="display: flex;align-items: center;justify-content: center;">
            <Button type="primary"  @click="addStoreTag(storeInfo)">新增标签</Button>
          </div>
          <p class="item">
            <span class="label">店铺简介:</span>
            <span class="info">
              {{storeInfo.storeDesc?storeInfo.storeDesc:'暂未完善'}}
@@ -559,6 +582,22 @@
            </Row>
          </div>
        </TabPane>
        <Modal
          v-model="showModal"
          title="新增标签"
          width="800"
          :mask-closable="false"
        >
          <Select v-model="tagForm.storeTagId" placeholder="请选择" @on-query-change="searchChange" filterable
                  clearable style="width: 150px">
            <Option v-for="item in tagOptions" :value="item.id" :key="item.id">{{ item.tagName }}</Option>
          </Select>
          <div slot="footer">
            <Button @click="showModal = false">取消</Button>
            <Button type="primary" @click="handleSubmit" :loading="submitLoading">确定</Button>
          </div>
        </Modal>
      </Tabs>
    </Card>
@@ -566,7 +605,7 @@
</template>
<script>
  import { getTag,bind,getStoreTagsById,delTagByStoreTagRefId } from "@/api/store-tag"
  import ossManage from "@/views/sys/oss-manage/ossManage";
  import * as RegExp from '@/libs/RegExp.js';
  import {getCategoryTree} from "@/api/goods";
@@ -577,15 +616,23 @@
  export default {
    name: "member",
    components: {
      ossManage,
    },
    data() {
      return {
        tagOptions:[],
        submitLoading:false,
        tagForm:{
          storeId:null,
          storeTagId:"",
        },
        showModal:false,
        id: "",//店铺id
        categories: [], //店铺静音范围
        loading: true, // 表单加载状态
        storeInfo: {},//店铺信息
        storeInfo: {
        },//店铺信息
        tagList:[],
        checkAllGroup: [], //选中的经营分类
        selectDate: null, // 申请时间
@@ -846,7 +893,69 @@
      };
    },
    methods: {
      handleDeleteTag(storeTagRefId){
        console.log(storeTagRefId)
        delTagByStoreTagRefId(storeTagRefId).then(res=>{
          if (res.code === 200){
            this.$Message.success("删除成功")
          }
          this.getStoreTags();
        })
      },
      getStoreTags(){
        getStoreTagsById(this.id).then(res =>{
          if (res.code === 200){
            this.$set(this, "tagList", res.data);
          }
        })
      },
      searchChange(val){
        this.getStoreTag(val)
      },
      getStoreTag(val){
        const params = {
          tagName: ''
        }
        if (val) {
          params.tagName = val;
        } else {
          params.tagName = ''
        }
        getTag(params).then(res =>{
          if (res.code ===200){
            this.tagOptions = res.data
          }
        })
      },
      handleSubmit(){
        if (this.tagForm.storeTagId === null || this.tagForm.storeTagId ===""){
          this.$Message.error("请选择标签")
         return
        }
        this.showModal = false;
          bind(this.tagForm).then(res =>{
            if (res.code === 200){
          }
          this.getStoreTags();
            this.getStoreTag();
        })
      },
      addStoreTag(info){
        console.log(info)
        this.tagForm.storeId = info.storeId;
        this.tagForm.storeTagId = "";
        this.showModal = true;
      },
      init() {
        this.getStoreTags();
        this.getStoreTag();
        //查店铺基本信息
        this.getStoreInfo();
        //查询店铺分类
manager/src/views/store-tag/store-tag.vue
New file
@@ -0,0 +1,272 @@
<template>
  <div>
    <card>
      <Form
        ref="searchForm"
        @keydown.enter.native="handleSearch"
        :model="searchForm"
        inline
        :label-width="70"
        class="search-form"
      >
        <Form-item label="标签名称" prop="tagName">
          <Input
            type="text"
            v-model="searchForm.tagName"
            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="tagList"
        ref="table"
        sortable="custom"
        @on-sort-change="changeSort"
        @on-selection-change="showSelect"
      >
        <template slot-scope="{ row, index }" slot="action">
          <Button type="info" size="small" style="margin-right: 5px" @click="openEdit(row)">编辑标签</Button>
          <Button type="error" size="small" style="margin-right: 5px" @click="del(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"
      >
        <Form ref="form" :model="form" :label-width="70" :rules="rules">
          <FormItem label="标签名称" prop="tagName">
            <Input v-model="form.tagName" autocomplete="off"/>
          </FormItem>
        </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 { getPageList,addStoreTag,editTag,delTag,removeBatch } from "@/api/store-tag"
  export default {
    name: "store-tag",
    data(){
      return{
        loading:false,
        columns:[
          {
            type: 'selection',
            width: 60,
            align: 'center'
          },
          {
            title:'标签名称',
            key: 'tagName',
            minWidth: 60,
            tooltip: true,
          },
          {
            title: '操作',
            key: 'action',
            slot: 'action',
            minWidth: 150,
            align: 'center'
          }
        ],
        total:0,
        tagList:[],
        searchForm: {
          // 搜索框初始化对象
          pageNumber: 1, // 当前页数
          pageSize: 10, // 页面大小
          tagName: '', // 标签名称
        },
        // 对话框标题
        modelTitle:'',
        modelShow:false,
        submitLoading:false,
        // 表单
        form: {
          id: '',
          tagName: '',
        },
        rules: {
          tagName: [
            {required: true, message: "标签名称不能为空", trigger: "blur"}
          ],
        },
        //多选
        selectCount: 0, // 已选数量
        selectList: [], // 已选数据列表
      }
    },
    mounted(){
      this.init();
    },
    methods:{
      init(){
        this.getTagList()
      },
      openAdd(){
        this.modelTitle = "新增标签";
        this.modelShow = true;
        this.form={
          id: '',
          tagName: '',
        }
      },
      openEdit(row){
        this.form.id = row.id;
        this.form.tagName = row.tagName;
        this.modelTitle= "修改标签";
        this.modelShow = true;
      },
      saveOrUpdate() {
        this.$refs.form.validate(valid => {
          if (valid) {
            this.submitLoading = true
            if (this.form.id) {
              // 修改
              editTag(this.form).then(res => {
                if (res.code === 200) {
                  this.$Message.success("修改成功");
                  this.modelClose()
                  this.getTagList()
                }
              })
            } else {
              // 新增
              addStoreTag(this.form).then(res => {
                if (res.code === 200) {
                  this.$Message.success("添加成功");
                  this.modelClose()
                  this.getTagList()
                }
              })
            }
          }
        });
      },
      del(row){
        delTag(row.id).then(res =>{
          if (res.code === 200){
            this.$Message.success("删除成功")
          }
          this.getTagList();
        })
      },
      delBatch() {
        if (this.selectCount <= 0) {
          this.$Message.warning("您还未选择要删除的数据");
          return;
        }
        // 显示确认对话框
        this.$Modal.confirm({
          title: '确认删除',
          content: `您确定要删除选中的 ${this.selectCount} 条数据吗?`,
          onOk: () => {
            // 用户点击确认后执行删除
            this.removeBatch();
          },
          onCancel: () => {
            this.$Message.info('已取消删除');
          }
        });
      },
      async removeBatch() {
        try {
          const res = await removeBatch(this.selectList);
          if (res.code === 200) {
            this.getTagList();
            this.selectedRows = [];
            this.selectCount = 0;
            this.$Message.success('删除成功');
          } else {
            this.$Message.error(res.message || '删除失败');
          }
        } catch (err) {
          console.log(err);
          this.$Message.error('请求失败,请重试');
        }
      },
      // 关闭弹窗
      modelClose() {
        this.submitLoading = false
        this.modelShow = false
      },
      getTagList(){
        this.loading = true;
        getPageList(this.searchForm).then((res) => {
          this.loading = false;
          if (res.code === 200) {
            this.tagList = res.data;
            this.total = res.total;
          }
        });
        this.loading = false;
      },
      handleSearch(){
        this.searchForm.pageNumber = 1;
        this.searchForm.pageSize = 10;
        this.getTagList();
      },
      // 分页 改变页码
      changePage(v) {
        this.searchForm.pageNumber = v;
        this.getTagList();
      },
      // 分页 改变页数
      changePageSize(v) {
        this.searchForm.pageNumber = 1;
        this.searchForm.pageSize = v;
        this.getTagList();
      },
      changeSort(){
      },
      showSelect(e){
        this.selectList = e.map(d => d.id);
        this.selectCount = e.length;
      },
    },
  }
</script>
<style scoped lang="scss">
</style>
seller/src/api/goods-customeize-template.js
New file
@@ -0,0 +1,47 @@
import service from "../libs/axios";
export const getPage = (params) =>{
  return service({
    url: "/lmk/goodsCustomizeTemplate",
    method: "GET",
    params: params
  })
}
export const edit = (params) =>{
  return service({
    url: "/lmk/goodsCustomizeTemplate",
    method: "PUT",
    data: params
  })
}
export const add = (params) =>{
  return service({
    url: "/lmk/goodsCustomizeTemplate",
    method: "POST",
    data: params
  })
}
export const del = (params) =>{
  return service({
    url: "/lmk/goodsCustomizeTemplate/" +params,
    method: "DELETE",
  })
}
export const changeStatus = (params) =>{
  return service({
    url: "/lmk/goodsCustomizeTemplate/changeStatus/" +params,
    method: "PUT",
  })
}
export const detail = (params) =>{
  return service({
    url: "/lmk/goodsCustomizeTemplate/" +params,
    method: "GET",
  })
}
seller/src/views/store-coupon/coupon_store.vue
@@ -28,17 +28,6 @@
              <Option value="GENERATE">生成</Option>
            </Select>
          </FormItem>
          <FormItem label="启用状态:">
            <Select
              v-model="listQuery.status"
              placeholder="全部状态"
              clearable
              style="width: 180px"
            >
              <Option value="ENABLE">启用</Option>
              <Option value="DISABLE">禁用</Option>
            </Select>
          </FormItem>
        </Form>
      </div>
    </Card>
seller/src/views/template/goodsCustomizeTemplate.vue
New file
@@ -0,0 +1,790 @@
<template>
  <div class="coupon-management">
    <!-- 搜索区域 -->
    <Card class="filter-container">
      <div class="filter-header">
        <Icon type="ios-search" size="18" />
        <span class="filter-title">筛选搜索</span>
        <div class="filter-actions">
          <Button type="primary" @click="getList" size="small">查询搜索</Button>
          <Button @click="handleResetSearch" size="small" style="margin-left: 10px;">重置</Button>
          <Button type="primary" @click="addModal" size="small" style="margin-left: 10px;">新增</Button>
        </div>
      </div>
      <div class="filter-content">
        <Form
          :model="listQuery"
          :label-width="90"
          class="search-form"
          inline
        >
          <FormItem label="模板名称">
            <Input
              v-model="listQuery.templateName"
              style="width: 180px;"
              clearable
              placeholder="请输入模板名称"
            >
            </Input>
          </FormItem>
          <FormItem label="启用状态:">
            <Select
              v-model="listQuery.status"
              placeholder="全部状态"
              clearable
              style="width: 180px"
            >
              <Option value="DISABLE">未启用</Option>
              <Option value="ENABLE">已起用</Option>
            </Select>
          </FormItem>
        </Form>
      </div>
    </Card>
    <!-- 表格区域 -->
    <Card class="table-container">
      <Table
        :loading="listLoading"
        border
        :columns="tableColumns"
        :data="list"
        ref="table"
        class="coupon-table"
      >
        <template slot-scope="{ row }" slot="action">
          <Button
            type="primary"
            size="small"
            style="margin-right: 5px"
            @click="delTemplate(row)"
          >删除</Button
          >
          <Button
            type="primary"
            size="small"
            style="margin-right: 5px"
            @click="updateTemplate(row)"
          >修改</Button
          >
          <Button
            type="primary"
            size="small"
            style="margin-right: 5px"
            @click="changeStatus(row)"
          >{{ row.status ==='ENABLE' ? '禁用':'开启'}}</Button
          >
        </template>
      </Table>
    </Card>
    <!-- 分页区域 -->
    <div class="pagination-container">
      <Page
        :current="listQuery.pageNumber"
        :page-size="listQuery.pageSize"
        :total="total"
        :page-size-opts="[10, 20, 30, 50]"
        show-elevator
        show-sizer
        show-total
        @on-change="handleCurrentChange"
        @on-page-size-change="handleSizeChange"
      />
    </div>
    <Modal
      v-model="showModal"
      :title="modalTitle"
      width="800"
      :mask-closable="false"
    >
      <Form
        ref="form"
        :model="form"
        :label-width="90"
        class="search-form"
        :rules="formRules"
        inline>
        <Row>
          <Col :span="24">
            <FormItem label="模板名称" prop="templateName">
              <Input v-model="form.templateName" autocomplete="off"/>
            </FormItem>
          </Col>
          <Col :span="24">
            <FormItem label="选择新增标题">
              <Button type="primary" @click="addTitle('TEXT')">添加文字标题</Button>
              <Button type="primary" style="margin-left: 10px" @click="addTitle('IMAGE')">添加图片标题</Button>
            </FormItem>
          </Col>
          <Col :span="24">
            <!-- 已添加的标题列表 -->
            <div v-for="(title, index) in form.titles" :key="index" class="title-item">
              <card class="title-text">
                <Input v-model="title.templateTitle" placeholder="请输入文本标题" style="width: 300px; margin-top: 8px;"/>
                <Button  class="delete-btn" type="text" @click="removeTitle(index)" style="color: #ff4d4f;">删除</Button>
              </card>
<!--              <card v-if="title.contentType === 'IMAGE'" class="title-image">-->
<!--                <Upload-->
<!--                  v-if="!title.imgTempUrl"-->
<!--                  :before-upload="(file) => handleBeforeUpload(file,title)"-->
<!--                  :format="['jpg','jpeg','png','gif']"-->
<!--                  :max-size="20480"-->
<!--                  action=""-->
<!--                  accept="image/*"-->
<!--                >-->
<!--                  <Button icon="ios-cloud-upload-outline">上传封面图片</Button>-->
<!--                </Upload>-->
<!--                <div v-else class="upload-file-info">-->
<!--                  <Col :span="24">-->
<!--                    <img :src="title.imgTempUrl" alt="活动图片" class="preview-image-limit">-->
<!--                  </Col>-->
<!--                  <Button type="text" @click="handleRemoveImage(title)">删除</Button>-->
<!--                </div>-->
<!--                <Button class="delete-btn" type="text" @click="removeTitle(index)" style="color: #ff4d4f;">删除</Button>-->
<!--              </card>-->
            </div>
          </Col>
          <Col :span="24">
            <FormItem  label="模板图片" >
              <div style="display: flex; flex-wrap: wrap;">
                <vuedraggable :animation="200" :list="showListImages">
                  <div v-for="(item, __index) in showListImages" :key="__index"
                       class="demo-upload-list">
                    <template>
                      <img :src="item"/>
                      <div class="demo-upload-list-cover">
                        <div>
                          <Icon size="30" type="md-search" @click.native="handleViewGoodsPicture(item)"></Icon>
                          <Icon size="30" type="md-trash" @click.native="handleRemoveGoodsPicture(item)"></Icon>
                        </div>
                      </div>
                    </template>
                  </div>
                </vuedraggable>
              </div>
              <div style="width: 100%;display: flex;justify-content: start;margin-top: 10px;">
                <Button @click="handleCLickImg('goodsGalleryFiles')" type="primary">上传图片</Button>
              </div>
              <Modal v-model="goodsPictureVisible" title="View Image">
                <img v-if="goodsPictureVisible" :src="previewGoodsPicture" style="width: 100%"/>
              </Modal>
            </FormItem>
          </Col>
        </Row>
      </Form>
      <div slot="footer">
        <Button type="text" @click="showModal = false">关闭</Button>
        <Button type="primary" :loading="submitLoading" @click="saveOrUpdate">确认</Button>
      </div>
    </Modal>
    <Modal v-model="picModelFlag" width="1200px">
      <div class="demo-upload-list" v-for="(item,index) in showListImages">
        <template>
          <img :src="item">
          <div class="demo-upload-list-cover">
            <Icon type="ios-eye-outline" @click.native="handleView(item)"></Icon>
            <Icon type="ios-trash-outline" @click.native="handleRemove(null,index)"></Icon>
          </div>
        </template>
      </div>
      <div class="demo-upload-list">
        <Upload
          :before-upload="upLoadImg"
          accept="image/*"
          action="-"
          type="drag"
          style=" display: inline-block;width: 58px"
        >
          <div style="width: 58px;height:58px;line-height: 58px;">
            <Icon type="ios-camera" size="20"></Icon>
          </div>
        </Upload>
      </div>
    </Modal>
    <Modal v-model="visible" title="预览图片">
      <img v-if="visible" :src="previewPicture" style="width: 100%">
    </Modal>
  </div>
</template>
<script>
import { getPage,edit,add,del,changeStatus,detail } from "@/api/goods-customeize-template.js"
import vuedraggable from "vuedraggable";
import COS from "cos-js-sdk-v5";
import {getFileKey} from "@/utils/file.js";
import {getFilePreview, getSts} from "@/api/file";
export default {
  name: 'GoodsCustomizeTemplate',
  components: {vuedraggable},
  data() {
    return {
      // 预览图片路径
      previewPicture: "",
      visible:false,
      picModelFlag: false, // 图片选择器
      goodsPictureVisible: false,
      showListImages: [],
      listImages: [],
      previewGoodsPicture: "",
      selectedFormBtnName: "", // 点击图片绑定form
      //新增
      modalTitle:"",
      showModal:false,
      list: [],
      total: 0,
      listLoading: false,
      submitLoading: false,
      listQuery: {
        pageNumber: 1,
        pageSize: 10,
        templateName:"",
        status:"",
      },
      form:{
        titles: []
      },
      // 表单验证规则
      formRules: {
        templateName:[
          {required:true,message:"模板名不能为空",trigger:"blur"}
        ]
      },
      // 表头配置
      tableColumns: [
        {
          type: 'selection',
          width: 60,
          align: 'center'
        },
        {
          title: '模板名称',
          key: 'templateName',
          align: 'center',
          ellipsis: true,
          tooltip: true
        },
        {
          title: '启用状态',
          key: 'status',
          width: 120,
          align: 'center',
          render: (h, params) => {
            const status = params.row.status;
            const color = status === 'ENABLE' ? 'success' : status === 'DISABLE' ? 'default' : 'warning';
            const text = status === 'ENABLE' ? '启用' : status === 'DISABLE' ? '未启用' : '未知';
            return h('Tag', {
              props: {
                color: color
              }
            }, text);
          }
        },
        {
          title: '操作',
          slot: 'action',
          width: 200,
          align: 'center'
        },
      ]
    }
  },
  methods: {
    changeStatus(row){
      changeStatus(row.id).then(res =>{
        if (res.code ===200 ){
          this.$Message.success(res.msg)
        }
        this.getList()
      })
    },
    async updateTemplate(row){
      this.showModal = true;
      this.modalTitle = "修改模板";
      await detail(row.id).then(res =>{
        if (res.code === 200){
          this.form = {
            id: res.data.id,
            templateName:  res.data.templateName || '', // 确保不为undefined
            titles:  res.data.titles || [] // 标题列表
          };
          this.form.titles.map((i) =>{
            i.id = null;
            return i
          })
          this.listImages = res.data.listImages.map((i) => {
            return i.imgUrl
          });
        }
      })
      const stsInfo = await getSts();
      const endpoint = stsInfo.data.endpoint;
      console.log()
      this.showListImages = this.listImages.map((i) => {
        if (i!=null&&i.indexOf('http')===-1)
          return endpoint+'/'+i;
        else return i;
      })
    },
    delTemplate(row){
      del(row.id).then(res =>{
        if (res.code === 200){
          this.$Message.success(res.msg)
        }
        this.getList()
      })
    },
    async upLoadImg(file) {
      console.log("打印上传1")
      if (this.listImages.length >= 10) {
        this.$Message.error("图片上传不能超过10个");
        return;
      }
      console.log("打印上传2")
      try {
        // 获取文件上传临时密钥
        const sts = await getSts();
        const cos = new COS({
          getAuthorization: async function (options, callback) {
            callback({
              TmpSecretId: sts.data.tmpSecretId,
              TmpSecretKey: sts.data.tmpSecretKey,
              SecurityToken: sts.data.sessionToken,
              // 建议返回服务器时间作为签名的开始时间,避免客户端本地时间偏差过大导致签名错误
              StartTime: sts.data.stsStartTime, // 时间戳,单位秒,如:1580000000
              ExpiredTime: sts.data.stsEndTime,// 时间戳,单位秒,如:1580000000
              ScopeLimit: true, // 细粒度控制权限需要设为 true,会限制密钥只在相同请求时重复使用
            });
          }
        })
        const fileKey = getFileKey(file.name)
        const upData = await cos.uploadFile({
          Bucket: sts.data.bucket,
          Region: sts.data.region,
          Key: fileKey,
          Body: file, // 要上传的文件对象。
          SliceSize: 1024 * 1024 * 5,
          onProgress: function (progressData) {
            console.log('上传进度:', progressData);
          },
        });
        console.log("上传成功", upData)
        this.$nextTick(() => {
          this.listImages.push(fileKey);
          this.showListImages.push(sts.data.endpoint + "/" + fileKey);
        })
      } catch (e) {
        console.log("上传失败", upData)
        return false;
      } finally {
      }
      return false;
    },
    handleView(url) {
      this.previewPicture = url;
      this.visible = true;
    },
    handleRemove(item, index) {
      if (!item) {
        this.listImages.splice(index, 1);
        this.showListImages.splice(index, 1);
      } else {
        console.log('移除测试',item, index);
        item.splice(index, 1)
      }
      this.previewPicture = "";
    },
    handleCLickImg(val, index) {
      this.picModelFlag = true;
      this.selectedFormBtnName = val;
    },
    handleViewGoodsPicture(url) {
      this.previewGoodsPicture = url;
      this.goodsPictureVisible = true;
    },
    // 移除商品图片
    handleRemoveGoodsPicture(file) {
      // this.baseInfoForm.goodsGalleryFiles =
      //   this.baseInfoForm.goodsGalleryFiles.filter((i) => i !== file);
    },
    addTitle(type){
      this.form.titles.push(
        {
          contentType: type,
          templateTitle: '',
          imgTempUrl:null,//图片临时地址
          file:null,
        });
    },
    // 移除标题
    removeTitle(index) {
      this.form.titles.splice(index, 1);
    },
    saveOrUpdate() {
      const submitData = {
        ...this.form,
        listImages: this.listImages,
      };
      console.log(submitData)
      const isTitlesValid = (titles) => {
        console.log("进入判断")
        // 数组本身为null/undefined或空数组
        if (!titles || titles.length === 0) return false;
        // 检查每个元素的有效性
        return titles.every(title => {
          // 基础校验:必须是对象且包含contentType
          if (!title || typeof title !== 'object' || !title.contentType) return false;
            return title.templateTitle !== null
              && title.templateTitle !== undefined
              && title.templateTitle.trim() !== '';
          // 未知类型
          return false;
        });
      };
      const isImagesValid = (images) => {
        if (!images || images.length === 0) return false;
        return images.some(img => img && (typeof img === 'string' ? img.trim() !== '' : true));
      };
      this.$refs.form.validate(async (valid) => {
        if (valid) {
          try {
            // 先处理所有图片上传(等待异步完成)
            this.submitLoading = true;
            // 上传完成后再验证
            if (!isTitlesValid(submitData.titles)) {
              this.$Message.error('请确保所有标题都填写完整(文本标题不能为空,图片标题需上传成功)');
              this.submitLoading = false;
              return;
            }
            if (!isImagesValid(submitData.listImages)) {
              this.$Message.error('请添加有效的图片(至少一张图片)');
              this.submitLoading = false;
              return;
            }
            // 验证通过,提交数据
            const api = this.form.id ? edit : add;
            const res = await api(submitData);
            if (res.code === 200) {
              this.$Message.success(res.msg);
              this.modelShow = false;
              this.getList();
            } else {
              this.$Message.error(res.msg);
            }
          } catch (error) {
            this.$Message.error('提交失败,请重试');
            console.error(error);
          } finally {
            this.submitLoading = false;
            this.showModal = false;
          }
        }
      });
    },
    addModal(){
      this.form = {
        titles: []
      };
      this.showModal = true;
      this.modalTitle = "新增模板";
      this.showListImages =[];
      this.listImages =[];
    },
    detail(row){
      console.log(row)
    },
    // 获取列表数据
    getList() {
      this.listLoading = true;
      // 模拟API调用
      getPage(this.listQuery).then(res =>{
        this.listLoading = false;
        this.list = res.data;
        this.total = res.total;
      })
    },
    // 重置搜索条件
    handleResetSearch() {
      this.listQuery = {
        pageNumber: 1,
        pageSize: 10,
        storeName: "",
        couponName: "",
        generateStatus: "",
        status: ""
      };
      this.getList();
    },
    // 分页大小改变
    handleSizeChange(size) {
      this.listQuery.pageSize = size;
      this.listQuery.pageNumber = 1;
      this.getList();
    },
    // 当前页码改变
    handleCurrentChange(pageNumber) {
      this.listQuery.pageNumber = pageNumber;
      this.getList();
    },
    // 提交表单
    handleSubmit() {
      this.$refs.dataForm.validate((valid) => {
        if (valid) {
        }
      });
    }
  },
  mounted() {
    this.getList();
  }
}
</script>
<style scoped lang="less">
.preview-image-limit{
  max-width: 100%;
  max-height: 200px;
  object-fit: contain;
  border-radius: 4px;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  display: block;
}
.delete-btn {
  position: absolute; /* 绝对定位 */
  top: 12px; /* 距离顶部的距离 */
  right: 12px; /* 距离右侧的距离 */
  padding: 4px 8px; /* 调整按钮大小 */
  line-height: 1; /* 调整行高 */
}
.title-item {
  margin-bottom: 12px;
  padding-bottom: 12px;
  border-bottom: 1px dashed #e9e9e9;
}
.title-type-selector {
  display: flex;
  justify-content: center;
  padding: 20px 0;
}
.type-option {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin: 0 20px;
  cursor: pointer;
  padding: 15px;
  border-radius: 4px;
  transition: all 0.3s;
  &:hover {
    background-color: #f5f7fa;
  }
  span {
    margin-top: 10px;
  }
}
.coupon-management {
  padding: 16px;
  background: #f5f7f9;
  min-height: 100vh;
}
.filter-container {
  margin-bottom: 16px;
  .filter-header {
    display: flex;
    align-items: center;
    margin-bottom: 16px;
    .filter-title {
      margin-left: 8px;
      font-weight: 600;
      font-size: 16px;
    }
    .filter-actions {
      margin-left: auto;
    }
  }
  .filter-content {
    .search-form {
      /deep/ .ivu-form-item {
        margin-bottom: 16px;
        margin-right: 16px;
      }
    }
  }
}
.operation-container {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 16px;
  padding: 12px 16px;
  background: #fff;
  border-radius: 4px;
  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
  .operation-info {
    color: #999;
    font-size: 14px;
  }
}
.table-container {
  margin-bottom: 16px;
  .coupon-table {
    /deep/ .ivu-table-cell {
      padding: 8px 12px;
    }
    .action-btns {
      display: flex;
      justify-content: center;
      button {
        margin: 0 2px;
      }
    }
  }
}
.pagination-container {
  display: flex;
  justify-content: flex-end;
  background: #fff;
  padding: 12px 16px;
  border-radius: 4px;
  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
}
// 响应式调整
@media (max-width: 768px) {
  .coupon-management {
    padding: 8px;
  }
  .filter-content .search-form {
    /deep/ .ivu-form-item {
      width: 100%;
      margin-right: 0;
      .ivu-form-item-content {
        width: 100%;
        .ivu-input, .ivu-select {
          width: 100% !important;
        }
      }
    }
  }
  .operation-container {
    flex-direction: column;
    align-items: flex-start;
    .add-btn {
      margin-bottom: 8px;
    }
  }
  .action-btns {
    flex-direction: column;
    button {
      margin: 2px 0 !important;
      width: 100%;
    }
  }
}
.demo-upload-list {
  display: inline-block;
  width: 60px;
  height: 60px;
  text-align: center;
  line-height: 60px;
  border: 1px solid transparent;
  border-radius: 4px;
  overflow: hidden;
  background: #fff;
  position: relative;
  box-shadow: 0 1px 1px rgba(0, 0, 0, .2);
  margin-right: 4px;
}
.demo-upload-list img {
  width: 100%;
  height: 100%;
}
.demo-upload-list-cover {
  display: none;
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  background: rgba(0, 0, 0, .6);
}
.demo-upload-list:hover .demo-upload-list-cover {
  display: block;
}
.demo-upload-list-cover i {
  color: #fff;
  font-size: 20px;
  cursor: pointer;
  margin: 0 2px;
}
</style>