mirror of
https://github.com/yangjian102621/geekai.git
synced 2025-09-17 08:46:38 +08:00
redeem export function is ready
This commit is contained in:
parent
97e81a7dcc
commit
2e1bad387c
@ -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/")
|
||||
|
@ -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(
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
}
|
@ -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,7 +41,8 @@
|
||||
|
||||
<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>
|
||||
@ -57,7 +60,6 @@
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<el-container class="video-container" v-loading="loading" element-loading-background="rgba(100,100,100,0.3)">
|
||||
<h2 class="h-title">你的作品</h2>
|
||||
|
||||
@ -80,7 +82,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
任务执行失败:{{ item.err_msg }},任务提示词:{{ item.prompt }}
|
||||
</div>
|
||||
<div class="prompt" v-else>{{ item.prompt }}</div>
|
||||
</div>
|
||||
<div class="right" v-if="item.progress === 100">
|
||||
@ -129,7 +133,8 @@
|
||||
</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>
|
||||
@ -139,7 +144,7 @@
|
||||
<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"
|
||||
|
@ -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>
|
||||
|
||||
@ -107,10 +112,10 @@
|
||||
|
||||
<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";
|
||||
|
||||
@ -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(() => {
|
||||
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user