redeem export function is ready

This commit is contained in:
RockYang 2024-11-27 11:52:18 +08:00
parent 6aaf607ed7
commit d30d5585c6
9 changed files with 183 additions and 52 deletions

View File

@ -6,6 +6,7 @@
* 功能优化:失败的任务自动退回算力,而不需要在删除的时候再退回 * 功能优化:失败的任务自动退回算力,而不需要在删除的时候再退回
* 功能新增:支持设置一个专门的模型来翻译提示词,提供 Mate 提示词生成功能 * 功能新增:支持设置一个专门的模型来翻译提示词,提供 Mate 提示词生成功能
* Bug修复修复图片对话的时候上下文不起作用的Bug * Bug修复修复图片对话的时候上下文不起作用的Bug
* 功能新增:管理后台新增批量导出兑换码功能
## v4.1.6 ## v4.1.6
* 功能新增:**支持OpenAI实时语音对话功能** :rocket: :rocket: :rocket:, Beta 版,目前没有做算力计费控制,目前只有 VIP 用户可以使用。 * 功能新增:**支持OpenAI实时语音对话功能** :rocket: :rocket: :rocket:, Beta 版,目前没有做算力计费控制,目前只有 VIP 用户可以使用。

View File

@ -8,6 +8,8 @@ package admin
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import ( import (
"encoding/csv"
"fmt"
"geekai/core" "geekai/core"
"geekai/core/types" "geekai/core/types"
"geekai/handler" "geekai/handler"
@ -35,12 +37,10 @@ func (h *RedeemHandler) List(c *gin.Context) {
session := h.DB.Session(&gorm.Session{}) session := h.DB.Session(&gorm.Session{})
if code != "" { if code != "" {
session.Where("code LIKE ?", "%"+code+"%") session = session.Where("code LIKE ?", "%"+code+"%")
} }
if status == 0 { if status >= 0 {
session.Where("redeem_at = ?", 0) session = session.Where("redeemed_at", status)
} else if status == 1 {
session.Where("redeem_at > ?", 0)
} }
var total int64 var total int64
@ -80,6 +80,65 @@ func (h *RedeemHandler) List(c *gin.Context) {
resp.SUCCESS(c, vo.NewPage(total, page, pageSize, items)) 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) { func (h *RedeemHandler) Create(c *gin.Context) {
var data struct { var data struct {
Name string `json:"name"` Name string `json:"name"`

View File

@ -350,6 +350,7 @@ func main() {
group.POST("create", h.Create) group.POST("create", h.Create)
group.POST("set", h.Set) group.POST("set", h.Set)
group.GET("remove", h.Remove) group.GET("remove", h.Remove)
group.POST("export", h.Export)
}), }),
fx.Invoke(func(s *core.AppServer, h *admin.DashboardHandler) { fx.Invoke(func(s *core.AppServer, h *admin.DashboardHandler) {
group := s.Engine.Group("/api/admin/dashboard/") group := s.Engine.Group("/api/admin/dashboard/")

View File

@ -3,7 +3,7 @@
display flex display flex
width 100% width 100%
.el-input,.el-select,.el-switch { .el-input, .el-select, .el-switch {
margin-right 10px margin-right 10px
} }

View File

@ -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
}

View File

@ -6,12 +6,12 @@
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import axios from 'axios' 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.timeout = 180000
axios.defaults.baseURL = process.env.VUE_APP_API_HOST axios.defaults.baseURL = process.env.VUE_APP_API_HOST
axios.defaults.withCredentials = true; axios.defaults.withCredentials = true;
axios.defaults.headers.post['Content-Type'] = 'application/json' //axios.defaults.headers.post['Content-Type'] = 'application/json'
// HTTP拦截器 // HTTP拦截器
axios.interceptors.request.use( axios.interceptors.request.use(
@ -82,3 +82,18 @@ export function httpDownload(url) {
}) })
}) })
} }
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)
})
})
}

View File

@ -5,7 +5,9 @@
<template v-for="(img, index) in images" :key="img"> <template v-for="(img, index) in images" :key="img">
<div class="item"> <div class="item">
<el-image :src="replaceImg(img)" fit="cover"/> <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>
<div class="btn-swap" v-if="images.length === 2 && index === 0"> <div class="btn-swap" v-if="images.length === 2 && index === 0">
<i class="iconfont icon-exchange" @click="switchReverse"></i> <i class="iconfont icon-exchange" @click="switchReverse"></i>
@ -39,25 +41,25 @@
<div class="params"> <div class="params">
<div class="item-group"> <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> <i class="iconfont icon-chuangzuo" style="margin-right: 5px"></i>
<span>生成AI视频提示词</span> <span>生成AI视频提示词</span>
</el-button> </el-button>
</div> </div>
<div class="item-group"> <div class="item-group">
<span class="label">循环参考图</span> <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>
<div class="item-group"> <div class="item-group">
<span class="label">提示词优化</span> <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>
</div> </div>
</div> </div>
<el-container class="video-container" v-loading="loading" element-loading-background="rgba(100,100,100,0.3)"> <el-container class="video-container" v-loading="loading" element-loading-background="rgba(100,100,100,0.3)">
<h2 class="h-title">你的作品</h2> <h2 class="h-title">你的作品</h2>
@ -74,26 +76,28 @@
<img src="/images/play.svg" alt=""/> <img src="/images/play.svg" alt=""/>
</button> </button>
</div> </div>
<el-image :src="item.cover_url" fit="cover" v-else-if="item.progress > 100" /> <el-image :src="item.cover_url" fit="cover" v-else-if="item.progress > 100"/>
<generating message="正在生成视频" v-else /> <generating message="正在生成视频" v-else/>
</div> </div>
</div> </div>
<div class="center"> <div class="center">
<div class="failed" v-if="item.progress === 101">任务执行失败{{item.err_msg}}任务提示词{{item.prompt}}</div> <div class="failed" v-if="item.progress === 101">
<div class="prompt" v-else>{{item.prompt}}</div> 任务执行失败{{ item.err_msg }}任务提示词{{ item.prompt }}
</div>
<div class="prompt" v-else>{{ item.prompt }}</div>
</div> </div>
<div class="right" v-if="item.progress === 100"> <div class="right" v-if="item.progress === 100">
<div class="tools"> <div class="tools">
<button class="btn btn-publish"> <button class="btn btn-publish">
<span class="text">发布</span> <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> </button>
<el-tooltip effect="light" content="下载视频" placement="top"> <el-tooltip effect="light" content="下载视频" placement="top">
<button class="btn btn-icon" @click="download(item)" :disabled="item.downloading"> <button class="btn btn-icon" @click="download(item)" :disabled="item.downloading">
<i class="iconfont icon-download" v-if="!item.downloading"></i> <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> </button>
</el-tooltip> </el-tooltip>
<el-tooltip effect="light" content="删除" placement="top"> <el-tooltip effect="light" content="删除" placement="top">
@ -111,7 +115,7 @@
</div> </div>
</div> </div>
</div> </div>
<el-empty :image-size="100" description="没有任何作品,赶紧去创作吧!" v-else /> <el-empty :image-size="100" description="没有任何作品,赶紧去创作吧!" v-else/>
<div class="pagination"> <div class="pagination">
<el-pagination v-if="total > pageSize" background <el-pagination v-if="total > pageSize" background
@ -129,7 +133,8 @@
</div> </div>
</el-container> </el-container>
<black-dialog v-model:show="showDialog" title="预览视频" hide-footer @cancal="showDialog = false" width="auto"> <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> </video>
</black-dialog> </black-dialog>
@ -139,10 +144,10 @@
<script setup> <script setup>
import {onMounted, onUnmounted, reactive, ref} from "vue"; import {onMounted, onUnmounted, reactive, ref} from "vue";
import {CircleCloseFilled} from "@element-plus/icons-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 {checkSession, getClientId} from "@/store/cache";
import {showMessageError, showMessageOK} from "@/utils/dialog"; import {showMessageError, showMessageOK} from "@/utils/dialog";
import { replaceImg } from "@/utils/libs" import {replaceImg} from "@/utils/libs"
import {ElMessage, ElMessageBox} from "element-plus"; import {ElMessage, ElMessageBox} from "element-plus";
import BlackSwitch from "@/components/ui/BlackSwitch.vue"; import BlackSwitch from "@/components/ui/BlackSwitch.vue";
import Generating from "@/components/ui/Generating.vue"; import Generating from "@/components/ui/Generating.vue";
@ -164,12 +169,12 @@ const formData = reactive({
}) })
const store = useSharedStore() const store = useSharedStore()
onMounted(()=>{ onMounted(() => {
checkSession().then(() => { checkSession().then(() => {
fetchData(1) fetchData(1)
}) })
store.addMessageHandler("luma",(data) => { store.addMessageHandler("luma", (data) => {
// //
if (data.channel !== "luma" || data.clientId !== getClientId()) { if (data.channel !== "luma" || data.clientId !== getClientId()) {
return return
@ -234,7 +239,7 @@ const removeJob = (item) => {
} }
const publishJob = (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("操作成功") ElMessage.success("操作成功")
}).catch(e => { }).catch(e => {
ElMessage.error("操作失败:" + e.message) ElMessage.error("操作失败:" + e.message)
@ -270,7 +275,7 @@ const fetchData = (_page) => {
if (_page) { if (_page) {
page.value = _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 total.value = res.data.total
loading.value = false loading.value = false
list.value = res.data.items list.value = res.data.items
@ -285,9 +290,9 @@ const fetchData = (_page) => {
const create = () => { const create = () => {
const len = images.value.length; const len = images.value.length;
if(len){ if (len) {
formData.first_frame_img = images.value[0] formData.first_frame_img = images.value[0]
if(len === 2){ if (len === 2) {
formData.end_frame_img = images.value[1] formData.end_frame_img = images.value[1]
} }
} }
@ -296,7 +301,7 @@ const create = () => {
fetchData(1) fetchData(1)
showMessageOK("创建任务成功") showMessageOK("创建任务成功")
}).catch(e => { }).catch(e => {
showMessageError("创建任务失败:"+e.message) showMessageError("创建任务失败:" + e.message)
}) })
} }
@ -310,7 +315,7 @@ const generatePrompt = () => {
formData.prompt = res.data formData.prompt = res.data
isGenerating.value = false isGenerating.value = false
}).catch(e => { }).catch(e => {
showMessageError("生成提示词失败:"+e.message) showMessageError("生成提示词失败:" + e.message)
isGenerating.value = false isGenerating.value = false
}) })
} }

View File

@ -49,7 +49,7 @@ watch(() => store.adminTheme, (val) => {
</script> </script>
<style scoped lang="stylus"> <style lang="stylus">
@import '@/assets/css/color-dark.styl'; @import '@/assets/css/color-dark.styl';
@import '@/assets/css/main.styl'; @import '@/assets/css/main.styl';
@import '@/assets/iconfont/iconfont.css'; @import '@/assets/iconfont/iconfont.css';

View File

@ -12,10 +12,14 @@
</el-select> </el-select>
<el-button type="primary" :icon="Search" @click="fetchData">搜索</el-button> <el-button type="primary" :icon="Search" @click="fetchData">搜索</el-button>
<el-button type="success" :icon="Plus" @click="add">添加兑换码</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> </div>
<el-row> <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="name" label="名称"/>
<el-table-column prop="code" label="兑换码"> <el-table-column prop="code" label="兑换码">
<template #default="scope"> <template #default="scope">
@ -48,7 +52,8 @@
<el-table-column prop="enabled" label="启用状态"> <el-table-column prop="enabled" label="启用状态">
<template #default="scope"> <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> </template>
</el-table-column> </el-table-column>
@ -90,7 +95,7 @@
</el-form-item> </el-form-item>
<el-form-item label="生成数量:" prop="num"> <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-item>
</el-form> </el-form>
</template> </template>
@ -107,17 +112,17 @@
<script setup> <script setup>
import {onMounted, onUnmounted, ref} from "vue"; 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 {ElMessage} from "element-plus";
import {dateFormat, removeArrayItem, substr} from "@/utils/libs"; import {dateFormat, substr, UUID} from "@/utils/libs";
import {Delete, DocumentCopy, Plus, Search, UploadFilled} from "@element-plus/icons-vue"; import {DocumentCopy, Plus, Search} from "@element-plus/icons-vue";
import {showMessageError} from "@/utils/dialog"; import {showMessageError} from "@/utils/dialog";
import ClipboardJS from "clipboard"; import ClipboardJS from "clipboard";
// //
const items = ref([]) const items = ref([])
const loading = ref(true) const loading = ref(true)
const query = ref({code:"",status:-1}) const query = ref({code: "", status: -1})
const redeemStatus = ref([ const redeemStatus = ref([
{value: -1, label: "全部"}, {value: -1, label: "全部"},
{value: 0, label: "未核销"}, {value: 0, label: "未核销"},
@ -126,6 +131,8 @@ const redeemStatus = ref([
const showDialog = ref(false) const showDialog = ref(false)
const dialogLoading = ref(false) const dialogLoading = ref(false)
const item = ref({name: "", power: 0, num: 1}) const item = ref({name: "", power: 0, num: 1})
const itemIds = ref([])
const exporting = ref(false)
const clipboard = ref(null) const clipboard = ref(null)
onMounted(() => { onMounted(() => {
@ -152,10 +159,10 @@ const add = () => {
} }
const save = () => { const save = () => {
if (item.value.name ===""){ if (item.value.name === "") {
return showMessageError("请输入兑换码名称") return showMessageError("请输入兑换码名称")
} }
if (item.value.power === 0){ if (item.value.power === 0) {
return showMessageError("请输入算力额度") return showMessageError("请输入算力额度")
} }
if (item.value.num <= 0) { if (item.value.num <= 0) {
@ -207,12 +214,39 @@ const remove = function (row) {
ElMessage.error("删除失败:" + e.message) 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> </script>
<style lang="stylus" scoped> <style lang="stylus" scoped>
.list { .list {
.handle-box { .handle-box {
margin-bottom 20px margin-bottom 20px
.handle-input { .handle-input {
max-width 150px; max-width 150px;
margin-right 10px; margin-right 10px;