mirror of
				https://github.com/yangjian102621/geekai.git
				synced 2025-11-04 08:13:43 +08:00 
			
		
		
		
	redeem export function is ready
This commit is contained in:
		@@ -6,6 +6,7 @@
 | 
			
		||||
* 功能优化:失败的任务自动退回算力,而不需要在删除的时候再退回
 | 
			
		||||
* 功能新增:支持设置一个专门的模型来翻译提示词,提供 Mate 提示词生成功能
 | 
			
		||||
* Bug修复:修复图片对话的时候,上下文不起作用的Bug
 | 
			
		||||
* 功能新增:管理后台新增批量导出兑换码功能
 | 
			
		||||
 | 
			
		||||
## v4.1.6
 | 
			
		||||
* 功能新增:**支持OpenAI实时语音对话功能** :rocket: :rocket: :rocket:, Beta 版,目前没有做算力计费控制,目前只有 VIP 用户可以使用。
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,8 @@ package admin
 | 
			
		||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/csv"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"geekai/core"
 | 
			
		||||
	"geekai/core/types"
 | 
			
		||||
	"geekai/handler"
 | 
			
		||||
@@ -35,12 +37,10 @@ func (h *RedeemHandler) List(c *gin.Context) {
 | 
			
		||||
 | 
			
		||||
	session := h.DB.Session(&gorm.Session{})
 | 
			
		||||
	if code != "" {
 | 
			
		||||
		session.Where("code LIKE ?", "%"+code+"%")
 | 
			
		||||
		session = session.Where("code LIKE ?", "%"+code+"%")
 | 
			
		||||
	}
 | 
			
		||||
	if status == 0 {
 | 
			
		||||
		session.Where("redeem_at = ?", 0)
 | 
			
		||||
	} else if status == 1 {
 | 
			
		||||
		session.Where("redeem_at > ?", 0)
 | 
			
		||||
	if status >= 0 {
 | 
			
		||||
		session = session.Where("redeemed_at", status)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var total int64
 | 
			
		||||
@@ -80,6 +80,65 @@ func (h *RedeemHandler) List(c *gin.Context) {
 | 
			
		||||
	resp.SUCCESS(c, vo.NewPage(total, page, pageSize, items))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Export 导出 CVS 文件
 | 
			
		||||
func (h *RedeemHandler) Export(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Status int   `json:"status"`
 | 
			
		||||
		Ids    []int `json:"ids"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	session := h.DB.Session(&gorm.Session{})
 | 
			
		||||
	if data.Status >= 0 {
 | 
			
		||||
		session = session.Where("redeemed_at", data.Status)
 | 
			
		||||
	}
 | 
			
		||||
	if len(data.Ids) > 0 {
 | 
			
		||||
		session = session.Where("id IN ?", data.Ids)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var items []model.Redeem
 | 
			
		||||
	err := session.Order("id DESC").Find(&items).Error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 设置响应头,告诉浏览器这是一个附件,需要下载
 | 
			
		||||
	c.Header("Content-Disposition", "attachment; filename=output.csv")
 | 
			
		||||
	c.Header("Content-Type", "text/csv")
 | 
			
		||||
 | 
			
		||||
	// 创建一个 CSV writer
 | 
			
		||||
	writer := csv.NewWriter(c.Writer)
 | 
			
		||||
 | 
			
		||||
	// 写入 CSV 文件的标题行
 | 
			
		||||
	headers := []string{"名称", "兑换码", "算力", "创建时间"}
 | 
			
		||||
	if err := writer.Write(headers); err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 写入数据行
 | 
			
		||||
	records := make([][]string, 0)
 | 
			
		||||
	for _, item := range items {
 | 
			
		||||
		records = append(records, []string{item.Name, item.Code, fmt.Sprintf("%d", item.Power), item.CreatedAt.Format("2006-01-02 15:04:05")})
 | 
			
		||||
	}
 | 
			
		||||
	for _, record := range records {
 | 
			
		||||
		if err := writer.Write(record); err != nil {
 | 
			
		||||
			resp.ERROR(c, err.Error())
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 确保所有数据都已写入响应
 | 
			
		||||
	writer.Flush()
 | 
			
		||||
	if err := writer.Error(); err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *RedeemHandler) Create(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Name  string `json:"name"`
 | 
			
		||||
 
 | 
			
		||||
@@ -350,6 +350,7 @@ func main() {
 | 
			
		||||
			group.POST("create", h.Create)
 | 
			
		||||
			group.POST("set", h.Set)
 | 
			
		||||
			group.GET("remove", h.Remove)
 | 
			
		||||
			group.POST("export", h.Export)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *admin.DashboardHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/admin/dashboard/")
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@
 | 
			
		||||
    display flex
 | 
			
		||||
    width 100%
 | 
			
		||||
 | 
			
		||||
    .el-input,.el-select,.el-switch {
 | 
			
		||||
    .el-input, .el-select, .el-switch {
 | 
			
		||||
      margin-right 10px
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -172,6 +172,22 @@ body {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mr-1 {
 | 
			
		||||
  margin-right 5px
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mr-2 {
 | 
			
		||||
  margin-right 10px
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ml-1 {
 | 
			
		||||
  margin-left 5px
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ml-2 {
 | 
			
		||||
  margin-left 10px
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -6,12 +6,12 @@
 | 
			
		||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
 | 
			
		||||
import axios from 'axios'
 | 
			
		||||
import {getAdminToken, getSessionId, getUserToken, removeAdminToken, removeUserToken} from "@/store/session";
 | 
			
		||||
import {getAdminToken, getUserToken, removeAdminToken, removeUserToken} from "@/store/session";
 | 
			
		||||
 | 
			
		||||
axios.defaults.timeout = 180000
 | 
			
		||||
axios.defaults.baseURL = process.env.VUE_APP_API_HOST
 | 
			
		||||
axios.defaults.withCredentials = true;
 | 
			
		||||
axios.defaults.headers.post['Content-Type'] = 'application/json'
 | 
			
		||||
//axios.defaults.headers.post['Content-Type'] = 'application/json'
 | 
			
		||||
 | 
			
		||||
// HTTP拦截器
 | 
			
		||||
axios.interceptors.request.use(
 | 
			
		||||
@@ -81,4 +81,19 @@ export function httpDownload(url) {
 | 
			
		||||
            reject(err)
 | 
			
		||||
        })
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function httpPostDownload(url, data) {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
        axios({
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            url: url,
 | 
			
		||||
            data: data,
 | 
			
		||||
            responseType: 'blob' // 将响应类型设置为 `blob`
 | 
			
		||||
        }).then(response => {
 | 
			
		||||
            resolve(response)
 | 
			
		||||
        }).catch(err => {
 | 
			
		||||
            reject(err)
 | 
			
		||||
        })
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
@@ -5,7 +5,9 @@
 | 
			
		||||
        <template v-for="(img, index) in images" :key="img">
 | 
			
		||||
          <div class="item">
 | 
			
		||||
            <el-image :src="replaceImg(img)" fit="cover"/>
 | 
			
		||||
            <el-icon @click="remove(img)"><CircleCloseFilled /></el-icon>
 | 
			
		||||
            <el-icon @click="remove(img)">
 | 
			
		||||
              <CircleCloseFilled/>
 | 
			
		||||
            </el-icon>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="btn-swap" v-if="images.length === 2 && index === 0">
 | 
			
		||||
            <i class="iconfont icon-exchange" @click="switchReverse"></i>
 | 
			
		||||
@@ -39,25 +41,25 @@
 | 
			
		||||
 | 
			
		||||
        <div class="params">
 | 
			
		||||
          <div class="item-group">
 | 
			
		||||
            <el-button class="generate-btn" size="small" @click="generatePrompt" color="#5865f2" :disabled="isGenerating">
 | 
			
		||||
            <el-button class="generate-btn" size="small" @click="generatePrompt" color="#5865f2"
 | 
			
		||||
                       :disabled="isGenerating">
 | 
			
		||||
              <i class="iconfont icon-chuangzuo" style="margin-right: 5px"></i>
 | 
			
		||||
              <span>生成AI视频提示词</span>
 | 
			
		||||
            </el-button>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="item-group">
 | 
			
		||||
            <span class="label">循环参考图</span>
 | 
			
		||||
            <el-switch  v-model="formData.loop" size="small" style="--el-switch-on-color:#BF78BF;" />
 | 
			
		||||
            <el-switch v-model="formData.loop" size="small" style="--el-switch-on-color:#BF78BF;"/>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="item-group">
 | 
			
		||||
            <span class="label">提示词优化</span>
 | 
			
		||||
            <el-switch  v-model="formData.expand_prompt" size="small" style="--el-switch-on-color:#BF78BF;" />
 | 
			
		||||
            <el-switch v-model="formData.expand_prompt" size="small" style="--el-switch-on-color:#BF78BF;"/>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    <el-container class="video-container" v-loading="loading" element-loading-background="rgba(100,100,100,0.3)">
 | 
			
		||||
      <h2 class="h-title">你的作品</h2>
 | 
			
		||||
 | 
			
		||||
@@ -67,33 +69,35 @@
 | 
			
		||||
            <div class="left">
 | 
			
		||||
              <div class="container">
 | 
			
		||||
                <div v-if="item.progress === 100">
 | 
			
		||||
                  <video class="video" :src="replaceImg(item.video_url)"  preload="auto" loop="loop" muted="muted">
 | 
			
		||||
                  <video class="video" :src="replaceImg(item.video_url)" preload="auto" loop="loop" muted="muted">
 | 
			
		||||
                    您的浏览器不支持视频播放
 | 
			
		||||
                  </video>
 | 
			
		||||
                  <button class="play" @click="play(item)">
 | 
			
		||||
                    <img src="/images/play.svg" alt=""/>
 | 
			
		||||
                  </button>
 | 
			
		||||
                </div>
 | 
			
		||||
                <el-image :src="item.cover_url" fit="cover" v-else-if="item.progress > 100" />
 | 
			
		||||
                <generating message="正在生成视频" v-else />
 | 
			
		||||
                <el-image :src="item.cover_url" fit="cover" v-else-if="item.progress > 100"/>
 | 
			
		||||
                <generating message="正在生成视频" v-else/>
 | 
			
		||||
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="center">
 | 
			
		||||
              <div class="failed" v-if="item.progress === 101">任务执行失败:{{item.err_msg}},任务提示词:{{item.prompt}}</div>
 | 
			
		||||
              <div class="prompt" v-else>{{item.prompt}}</div>
 | 
			
		||||
              <div class="failed" v-if="item.progress === 101">
 | 
			
		||||
                任务执行失败:{{ item.err_msg }},任务提示词:{{ item.prompt }}
 | 
			
		||||
              </div>
 | 
			
		||||
              <div class="prompt" v-else>{{ item.prompt }}</div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="right" v-if="item.progress === 100">
 | 
			
		||||
              <div class="tools">
 | 
			
		||||
                <button class="btn btn-publish">
 | 
			
		||||
                  <span class="text">发布</span>
 | 
			
		||||
                  <black-switch v-model:value="item.publish" @change="publishJob(item)" size="small" />
 | 
			
		||||
                  <black-switch v-model:value="item.publish" @change="publishJob(item)" size="small"/>
 | 
			
		||||
                </button>
 | 
			
		||||
 | 
			
		||||
                <el-tooltip effect="light" content="下载视频" placement="top">
 | 
			
		||||
                  <button class="btn btn-icon" @click="download(item)" :disabled="item.downloading">
 | 
			
		||||
                    <i class="iconfont icon-download" v-if="!item.downloading"></i>
 | 
			
		||||
                    <el-image src="/images/loading.gif" class="downloading" fit="cover" v-else />
 | 
			
		||||
                    <el-image src="/images/loading.gif" class="downloading" fit="cover" v-else/>
 | 
			
		||||
                  </button>
 | 
			
		||||
                </el-tooltip>
 | 
			
		||||
                <el-tooltip effect="light" content="删除" placement="top">
 | 
			
		||||
@@ -111,38 +115,39 @@
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <el-empty :image-size="100" description="没有任何作品,赶紧去创作吧!" v-else />
 | 
			
		||||
      <el-empty :image-size="100" description="没有任何作品,赶紧去创作吧!" v-else/>
 | 
			
		||||
 | 
			
		||||
      <div class="pagination">
 | 
			
		||||
        <el-pagination v-if="total > pageSize" background
 | 
			
		||||
          style="--el-pagination-button-bg-color:#414141;
 | 
			
		||||
                       style="--el-pagination-button-bg-color:#414141;
 | 
			
		||||
          --el-pagination-button-color:#d1d1d1;
 | 
			
		||||
          --el-disabled-bg-color:#414141;
 | 
			
		||||
          --el-color-primary:#666666;
 | 
			
		||||
          --el-pagination-hover-color:#e1e1e1"
 | 
			
		||||
          layout="total,prev, pager, next"
 | 
			
		||||
          :hide-on-single-page="true"
 | 
			
		||||
          v-model:current-page="page"
 | 
			
		||||
          v-model:page-size="pageSize"
 | 
			
		||||
          @current-change="fetchData(page)"
 | 
			
		||||
          :total="total"/>
 | 
			
		||||
                       layout="total,prev, pager, next"
 | 
			
		||||
                       :hide-on-single-page="true"
 | 
			
		||||
                       v-model:current-page="page"
 | 
			
		||||
                       v-model:page-size="pageSize"
 | 
			
		||||
                       @current-change="fetchData(page)"
 | 
			
		||||
                       :total="total"/>
 | 
			
		||||
      </div>
 | 
			
		||||
    </el-container>
 | 
			
		||||
    <black-dialog v-model:show="showDialog" title="预览视频" hide-footer @cancal="showDialog = false" width="auto">
 | 
			
		||||
      <video style="width: 100%; max-height: 90vh;" :src="currentVideoUrl"  preload="auto" :autoplay="true" loop="loop" muted="muted" v-show="showDialog">
 | 
			
		||||
      <video style="width: 100%; max-height: 90vh;" :src="currentVideoUrl" preload="auto" :autoplay="true" loop="loop"
 | 
			
		||||
             muted="muted" v-show="showDialog">
 | 
			
		||||
        您的浏览器不支持视频播放
 | 
			
		||||
      </video>
 | 
			
		||||
    </black-dialog>    
 | 
			
		||||
    </black-dialog>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
import {onMounted, onUnmounted, reactive, ref} from "vue";
 | 
			
		||||
import {CircleCloseFilled} from "@element-plus/icons-vue";
 | 
			
		||||
import {httpDownload, httpPost, httpGet} from "@/utils/http";
 | 
			
		||||
import {httpDownload, httpGet, httpPost} from "@/utils/http";
 | 
			
		||||
import {checkSession, getClientId} from "@/store/cache";
 | 
			
		||||
import {showMessageError, showMessageOK} from "@/utils/dialog";
 | 
			
		||||
import { replaceImg } from "@/utils/libs"
 | 
			
		||||
import {replaceImg} from "@/utils/libs"
 | 
			
		||||
import {ElMessage, ElMessageBox} from "element-plus";
 | 
			
		||||
import BlackSwitch from "@/components/ui/BlackSwitch.vue";
 | 
			
		||||
import Generating from "@/components/ui/Generating.vue";
 | 
			
		||||
@@ -164,12 +169,12 @@ const formData = reactive({
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const store = useSharedStore()
 | 
			
		||||
onMounted(()=>{
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  checkSession().then(() => {
 | 
			
		||||
    fetchData(1)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  store.addMessageHandler("luma",(data) => {
 | 
			
		||||
  store.addMessageHandler("luma", (data) => {
 | 
			
		||||
    // 丢弃无关消息
 | 
			
		||||
    if (data.channel !== "luma" || data.clientId !== getClientId()) {
 | 
			
		||||
      return
 | 
			
		||||
@@ -192,7 +197,7 @@ const download = (item) => {
 | 
			
		||||
  const urlObj = new URL(url);
 | 
			
		||||
  const fileName = urlObj.pathname.split('/').pop();
 | 
			
		||||
  item.downloading = true
 | 
			
		||||
  httpDownload(downloadURL).then(response  => {
 | 
			
		||||
  httpDownload(downloadURL).then(response => {
 | 
			
		||||
    const blob = new Blob([response.data]);
 | 
			
		||||
    const link = document.createElement('a');
 | 
			
		||||
    link.href = URL.createObjectURL(blob);
 | 
			
		||||
@@ -234,7 +239,7 @@ const removeJob = (item) => {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const publishJob = (item) => {
 | 
			
		||||
  httpGet("/api/video/publish", {id: item.id, publish:item.publish}).then(() => {
 | 
			
		||||
  httpGet("/api/video/publish", {id: item.id, publish: item.publish}).then(() => {
 | 
			
		||||
    ElMessage.success("操作成功")
 | 
			
		||||
  }).catch(e => {
 | 
			
		||||
    ElMessage.error("操作失败:" + e.message)
 | 
			
		||||
@@ -270,7 +275,7 @@ const fetchData = (_page) => {
 | 
			
		||||
  if (_page) {
 | 
			
		||||
    page.value = _page
 | 
			
		||||
  }
 | 
			
		||||
  httpGet("/api/video/list",{page:page.value, page_size:pageSize.value, type: 'luma'}).then(res => {
 | 
			
		||||
  httpGet("/api/video/list", {page: page.value, page_size: pageSize.value, type: 'luma'}).then(res => {
 | 
			
		||||
    total.value = res.data.total
 | 
			
		||||
    loading.value = false
 | 
			
		||||
    list.value = res.data.items
 | 
			
		||||
@@ -284,10 +289,10 @@ const fetchData = (_page) => {
 | 
			
		||||
// 创建视频
 | 
			
		||||
const create = () => {
 | 
			
		||||
 | 
			
		||||
  const len =  images.value.length;
 | 
			
		||||
  if(len){
 | 
			
		||||
  const len = images.value.length;
 | 
			
		||||
  if (len) {
 | 
			
		||||
    formData.first_frame_img = images.value[0]
 | 
			
		||||
    if(len === 2){
 | 
			
		||||
    if (len === 2) {
 | 
			
		||||
      formData.end_frame_img = images.value[1]
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -296,7 +301,7 @@ const create = () => {
 | 
			
		||||
    fetchData(1)
 | 
			
		||||
    showMessageOK("创建任务成功")
 | 
			
		||||
  }).catch(e => {
 | 
			
		||||
    showMessageError("创建任务失败:"+e.message)
 | 
			
		||||
    showMessageError("创建任务失败:" + e.message)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -310,7 +315,7 @@ const generatePrompt = () => {
 | 
			
		||||
    formData.prompt = res.data
 | 
			
		||||
    isGenerating.value = false
 | 
			
		||||
  }).catch(e => {
 | 
			
		||||
    showMessageError("生成提示词失败:"+e.message)
 | 
			
		||||
    showMessageError("生成提示词失败:" + e.message)
 | 
			
		||||
    isGenerating.value = false
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -49,7 +49,7 @@ watch(() => store.adminTheme, (val) => {
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="stylus">
 | 
			
		||||
<style lang="stylus">
 | 
			
		||||
@import '@/assets/css/color-dark.styl';
 | 
			
		||||
@import '@/assets/css/main.styl';
 | 
			
		||||
@import '@/assets/iconfont/iconfont.css';
 | 
			
		||||
 
 | 
			
		||||
@@ -12,10 +12,14 @@
 | 
			
		||||
      </el-select>
 | 
			
		||||
      <el-button type="primary" :icon="Search" @click="fetchData">搜索</el-button>
 | 
			
		||||
      <el-button type="success" :icon="Plus" @click="add">添加兑换码</el-button>
 | 
			
		||||
      <el-button type="primary" @click="exportItems" :loading="exporting"><i class="iconfont icon-export mr-1"></i> 导出
 | 
			
		||||
      </el-button>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <el-row>
 | 
			
		||||
      <el-table :data="items" :row-key="row => row.id">
 | 
			
		||||
      <el-table :data="items" :row-key="row => row.id"
 | 
			
		||||
                @selection-change="handleSelectionChange" table-layout="auto">
 | 
			
		||||
        <el-table-column type="selection" width="38"></el-table-column>
 | 
			
		||||
        <el-table-column prop="name" label="名称"/>
 | 
			
		||||
        <el-table-column prop="code" label="兑换码">
 | 
			
		||||
          <template #default="scope">
 | 
			
		||||
@@ -48,7 +52,8 @@
 | 
			
		||||
 | 
			
		||||
        <el-table-column prop="enabled" label="启用状态">
 | 
			
		||||
          <template #default="scope">
 | 
			
		||||
            <el-switch v-model="scope.row['enabled']" @change="set('enabled',scope.row)" :disabled="scope.row['redeemed_at']>0"/>
 | 
			
		||||
            <el-switch v-model="scope.row['enabled']" @change="set('enabled',scope.row)"
 | 
			
		||||
                       :disabled="scope.row['redeemed_at']>0"/>
 | 
			
		||||
          </template>
 | 
			
		||||
        </el-table-column>
 | 
			
		||||
 | 
			
		||||
@@ -90,7 +95,7 @@
 | 
			
		||||
          </el-form-item>
 | 
			
		||||
 | 
			
		||||
          <el-form-item label="生成数量:" prop="num">
 | 
			
		||||
            <el-input v-model.number="item.num" />
 | 
			
		||||
            <el-input v-model.number="item.num"/>
 | 
			
		||||
          </el-form-item>
 | 
			
		||||
        </el-form>
 | 
			
		||||
      </template>
 | 
			
		||||
@@ -107,17 +112,17 @@
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
import {onMounted, onUnmounted, ref} from "vue";
 | 
			
		||||
import {httpGet, httpPost} from "@/utils/http";
 | 
			
		||||
import {httpGet, httpPost, httpPostDownload} from "@/utils/http";
 | 
			
		||||
import {ElMessage} from "element-plus";
 | 
			
		||||
import {dateFormat, removeArrayItem, substr} from "@/utils/libs";
 | 
			
		||||
import {Delete, DocumentCopy, Plus, Search, UploadFilled} from "@element-plus/icons-vue";
 | 
			
		||||
import {dateFormat, substr, UUID} from "@/utils/libs";
 | 
			
		||||
import {DocumentCopy, Plus, Search} from "@element-plus/icons-vue";
 | 
			
		||||
import {showMessageError} from "@/utils/dialog";
 | 
			
		||||
import ClipboardJS from "clipboard";
 | 
			
		||||
 | 
			
		||||
// 变量定义
 | 
			
		||||
const items = ref([])
 | 
			
		||||
const loading = ref(true)
 | 
			
		||||
const query = ref({code:"",status:-1})
 | 
			
		||||
const query = ref({code: "", status: -1})
 | 
			
		||||
const redeemStatus = ref([
 | 
			
		||||
  {value: -1, label: "全部"},
 | 
			
		||||
  {value: 0, label: "未核销"},
 | 
			
		||||
@@ -126,6 +131,8 @@ const redeemStatus = ref([
 | 
			
		||||
const showDialog = ref(false)
 | 
			
		||||
const dialogLoading = ref(false)
 | 
			
		||||
const item = ref({name: "", power: 0, num: 1})
 | 
			
		||||
const itemIds = ref([])
 | 
			
		||||
const exporting = ref(false)
 | 
			
		||||
 | 
			
		||||
const clipboard = ref(null)
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
@@ -152,10 +159,10 @@ const add = () => {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const save = () => {
 | 
			
		||||
  if (item.value.name ===""){
 | 
			
		||||
  if (item.value.name === "") {
 | 
			
		||||
    return showMessageError("请输入兑换码名称")
 | 
			
		||||
  }
 | 
			
		||||
  if (item.value.power === 0){
 | 
			
		||||
  if (item.value.power === 0) {
 | 
			
		||||
    return showMessageError("请输入算力额度")
 | 
			
		||||
  }
 | 
			
		||||
  if (item.value.num <= 0) {
 | 
			
		||||
@@ -207,12 +214,39 @@ const remove = function (row) {
 | 
			
		||||
    ElMessage.error("删除失败:" + e.message)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const handleSelectionChange = (items) => {
 | 
			
		||||
  itemIds.value = items.map(item => item.id)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const exportItems = () => {
 | 
			
		||||
  query.value.ids = itemIds.value
 | 
			
		||||
  exporting.value = true
 | 
			
		||||
  httpPostDownload("/api/admin/redeem/export", query.value).then(response => {
 | 
			
		||||
    const url = window.URL.createObjectURL(new Blob([response.data]));
 | 
			
		||||
    const link = document.createElement('a');
 | 
			
		||||
    link.href = url;
 | 
			
		||||
    link.setAttribute('download', UUID() + ".csv"); // 设置下载文件的名称
 | 
			
		||||
    document.body.appendChild(link);
 | 
			
		||||
    link.click();
 | 
			
		||||
 | 
			
		||||
    // 移除 <a> 标签
 | 
			
		||||
    document.body.removeChild(link);
 | 
			
		||||
    window.URL.revokeObjectURL(url);
 | 
			
		||||
    exporting.value = false
 | 
			
		||||
  }).catch(() => {
 | 
			
		||||
    exporting.value = false
 | 
			
		||||
    showMessageError("下载失败")
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="stylus" scoped>
 | 
			
		||||
.list {
 | 
			
		||||
  .handle-box {
 | 
			
		||||
    margin-bottom 20px
 | 
			
		||||
 | 
			
		||||
    .handle-input {
 | 
			
		||||
      max-width 150px;
 | 
			
		||||
      margin-right 10px;
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user