ai_images/admin-ui/src/views/ai/order/index.vue

663 lines
20 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="app-container">
<el-form
:model="queryParams"
ref="queryForm"
size="small"
:inline="true"
v-show="showSearch"
label-width="68px"
>
<el-form-item label="订单编号" prop="orderNum">
<el-input
v-model="queryParams.orderNum"
placeholder="请输入订单编号"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="用户ID" prop="uuid">
<el-input
v-model="queryParams.uuid"
placeholder="请输入用户ID"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="操作类型" prop="type">
<el-select v-model="queryParams.type" placeholder="请选择操作类型" clearable>
<el-option
v-for="dict in dict.type.ai_function_type"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="是否置顶" prop="isTop">
<el-select v-model="queryParams.isTop" placeholder="请选择" clearable>
<el-option
v-for="dict in dict.type.sys_yes_no"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="请选择状态"
filterable
clearable
style="width: 200px; margin-bottom: 16px;"
>
<el-option
v-for="(label, value) in statusMap"
:key="value"
:label="label"
:value="Number(value)"
/>
</el-select>
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker
v-model="dateRange"
style="width: 240px"
value-format="yyyy-MM-dd"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
></el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
<el-button
type="warning"
plain
icon="el-icon-download"
size="mini"
@click="handleExport"
v-hasPermi="['ai:order:export']"
>导出</el-button>
</el-form-item>
</el-form>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
<el-table v-loading="loading" :data="orderList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="主键ID" align="center" prop="id" width="60" />
<!-- <el-table-column label="备注" align="center" prop="remark" /> -->
<el-table-column label="订单编号" align="center" prop="orderNum" width="150"/>
<el-table-column label="用户ID" align="center" prop="uuid" width="100" />
<el-table-column label="操作类型" align="center" prop="type" width="90" >
<template slot-scope="scope">
<dict-tag :options="dict.type.ai_function_type" :value="scope.row.type" />
</template>
</el-table-column>
<el-table-column label="金额" align="center" prop="amount" width="100" />
<el-table-column label="生成结果" align="center" width="200">
<template slot-scope="scope">
<!-- 判断是否为链接 -->
<template v-if="isUrl(scope.row.result)">
<!-- 图片链接 -->
<img
v-if="isImage(scope.row.result)"
:src="scope.row.result"
class="preview-img"
@click="viewImage(scope.row.result)"
alt="预览图片"
/>
<!-- 视频链接 -->
<video
v-else-if="isVideo(scope.row.result)"
:src="scope.row.result"
class="preview-video"
@click="playVideo(scope.row.result)"
/>
</template>
<!-- 视频任务失败等场景result 为 VideoTaskError JSON表格内简短悬停看全文 -->
<template v-else-if="parseVolcTaskErrorJson(scope.row.result)">
<span
v-for="err in [parseVolcTaskErrorJson(scope.row.result)]"
:key="'oe-' + scope.row.id"
class="order-result-error-wrap"
>
<el-tooltip
placement="top-start"
effect="dark"
:open-delay="200"
popper-class="order-result-error-tooltip-popper"
>
<div slot="content" class="order-result-error-tooltip-body">
<div class="order-result-error-tooltip-row">
<span class="order-result-error-tooltip-k">错误编号:</span>
<span class="order-result-error-tooltip-v">{{ err.code || '—' }}</span>
</div>
<div
v-if="volcTaskErrorHintForCode(err.code)"
class="order-result-error-tooltip-row"
>
<span class="order-result-error-tooltip-k">中文说明:</span>
<span class="order-result-error-tooltip-v">{{
volcTaskErrorHintForCode(err.code)
}}</span>
</div>
<div class="order-result-error-tooltip-row order-result-error-tooltip-row--msg">
<span class="order-result-error-tooltip-k">信息:</span>
<span class="order-result-error-tooltip-v">{{ err.message || '—' }}</span>
</div>
</div>
<span class="order-result-error-compact">{{ volcTaskErrorCellSummary(err) }}</span>
</el-tooltip>
</span>
</template>
<!-- 非链接内容(如任务 id可点击尝试拉取视频 -->
<span v-else @click="handleOtherEvent(scope.row)">{{ scope.row.result }}</span>
</template>
</el-table-column>
<el-table-column label="文字描述" align="center" prop="text">
<template slot-scope="scope">
<!-- 核心:给 popover 加 ref触发元素绑定 v-popover -->
<el-popover
ref="promptPopover"
placement="top"
width="400"
trigger="hover"
:content="scope.row.text || '无提示词'"
></el-popover>
<!-- 触发元素:绑定 v-popover 到 popover 的 ref -->
<div
class="text-ellipsis-two-lines"
v-popover:promptPopover
style="cursor: pointer;"
>{{ scope.row.text}}</div>
</template>
</el-table-column>
<el-table-column label="置顶" align="center" key="status" width="60">
<template slot-scope="scope">
<el-switch
v-model="scope.row.isTop"
active-value="Y"
inactive-value="N"
@change="handleIsTopChange(scope.row)"
></el-switch>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="150"/>
<el-table-column label="状态" align="center" prop="status" :formatter="formatStatus" width="70"/>
<el-table-column label="来源" align="center" prop="source" width="70"/>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<!-- 添加或修改订单管理对话框 -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="订单编号" prop="orderNum">
<el-input v-model="form.orderNum" placeholder="请输入订单编号" />
</el-form-item>
<el-form-item label="用户ID" prop="userId">
<el-input v-model="form.userId" placeholder="请输入用户ID" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select
v-model="form.status"
placeholder="请选择状态"
filterable
clearable
style="width: 200px;"
>
<el-option
v-for="(label, value) in statusMap"
:key="value"
:label="label"
:value="Number(value)"
/>
</el-select>
</el-form-item>
<el-form-item label="金额" prop="amount">
<el-input v-model="form.amount" placeholder="请输入金额" />
</el-form-item>
<el-form-item label="生成结果" prop="result">
<el-input v-model="form.result" placeholder="请输入生成结果" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</el-dialog>
<!-- 图片预览弹窗 -->
<el-dialog :visible.sync="imageDialogVisible" title="图片预览" width="60%">
<img :src="previewImageUrl" class="dialog-img" alt="大图预览" />
</el-dialog>
<!-- 新增视频弹窗 -->
<el-dialog :visible.sync="videoDialogVisible" title="视频播放" width="60%">
<video :src="previewVideoUrl" controls class="dialog-video"></video>
</el-dialog>
</div>
</template>
<script>
import {
listOrder,
getOrder,
delOrder,
addOrder,
downloadVideo,
changeIsTop,
updateOrder
} from "@/api/ai/order";
export default {
name: "Order",
dicts: ["ai_function_type", "sys_yes_no"],
data() {
return {
imageDialogVisible: false,
videoDialogVisible: false,
previewVideoUrl: "",
previewImageUrl: "",
dateRange: [],
statusMap: {
0: "进行中",
1: "已完成",
2: "已失败"
},
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 订单管理表格数据
orderList: [],
// 弹出层标题
title: "",
// 是否显示弹出层
open: false,
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
orderNum: null,
userId: null,
type: null,
amount: null,
result: null,
status: null
},
// 表单参数
form: {},
// 表单校验
rules: {
delFlag: [
{ required: true, message: "删除标志不能为空", trigger: "blur" }
]
}
};
},
created() {
this.getList();
},
methods: {
handleIsTopChange(row) {
let text = row.isTop === "Y" ? "置顶" : "取消置顶";
this.$modal
.confirm('确认要"' + text + '""' + row.id + '"作品吗?')
.then(function() {
return changeIsTop(row.id, row.isTop);
})
.then(() => {
this.$modal.msgSuccess(text + "成功");
})
.catch(function() {
row.isTop = row.isTop === "Y" ? "N" : "Y";
});
},
// 判断是否为 http(s) URL与 portal-ui 生成视频模块一致)
isUrl(str) {
const value = String(str || "").trim();
return /^https?:\/\//i.test(value);
},
/** 已知错误码对应中文说明(与门户 VideoGen 一致) */
volcTaskErrorHintForCode(code) {
const c = String(code || "").trim();
const hints = {
OutputVideoSensitiveContentDetected: "输出视频可能包含敏感信息",
InvalidParameter: "请求参数无效"
};
return hints[c] || "";
},
/** 表格内摘要:已知码只显示中文,未知码显示 code */
volcTaskErrorCellSummary(err) {
if (!err) return "—";
const code = String(err.code || "").trim();
const hint = this.volcTaskErrorHintForCode(code);
if (hint) return hint;
if (code) return code;
const msg = String(err.message || "").trim();
return msg ? msg.slice(0, 32) + (msg.length > 32 ? "…" : "") : "—";
},
/** 已知火山错误码后附中文说明(括号),导出等场景可用 */
volcFailureCodeWithHint(code) {
const c = String(code || "").trim();
if (!c) return "";
const hint = this.volcTaskErrorHintForCode(c);
return hint ? `${c}${hint}` : c;
},
/** result 为火山回调失败写入的 VideoTaskError JSON 时解析为 { code, message } */
parseVolcTaskErrorJson(str) {
const s = String(str || "").trim();
if (!s || s[0] !== "{") return null;
try {
const o = JSON.parse(s);
if (
o &&
typeof o === "object" &&
!Array.isArray(o) &&
("code" in o || "message" in o)
) {
return {
code: o.code != null ? String(o.code) : "",
message: o.message != null ? String(o.message) : ""
};
}
} catch (_) {
/* ignore */
}
return null;
},
// 判断是否为图片结果(与 portal-ui GeneratedAssets 一致)
isImage(url) {
const value = String(url || "").trim();
if (!value) return false;
return /\.(jpeg|jpg|png|gif|webp|bmp)(\?.*)?$/i.test(value);
},
// 判断是否为视频结果(与 portal-ui VideoGen/GeneratedAssets 一致)
isVideo(url) {
const value = String(url || "").trim();
if (!value) return false;
return /\.(mp4|mov|webm|ogg|m4v|avi|mkv)(\?.*)?$/i.test(value);
},
// 查看图片
viewImage(url) {
this.previewImageUrl = url;
this.imageDialogVisible = true;
},
// 播放视频
playVideo(url) {
this.previewVideoUrl = url;
this.videoDialogVisible = true;
},
// 非链接内容点击事件
async handleOtherEvent(row) {
if (this.parseVolcTaskErrorJson(row.result)) return;
// 防止重复点击
if (row.isDownloading) return;
const originalResult = row.result;
try {
// 标记为下载中状态
row.isDownloading = true;
// 保存原始内容
// 修改显示内容为“下载中...”
row.result = "下载中...";
// 执行下载操作
const resultId = originalResult; // 假设result存储的是ID
var res = await downloadVideo(resultId);
if (res.status === "succeeded") {
row.result = res.content.video_url;
} else {
row.result = "视频生成中"
}
} catch (error) {
console.error('下载失败:', error);
// 失败后恢复原始内容
row.result = originalResult || row.result.replace("下载中...", "");
this.$message.error('下载失败,请重试');
} finally {
// 重置下载状态
row.isDownloading = false;
}
},
formatStatus(row) {
// 如果有匹配的类型则显示对应文字,否则显示"未知"
return this.statusMap[row.status] || "未知";
},
/** 查询订单管理列表 */
getList() {
this.loading = true;
listOrder(this.addDateRange(this.queryParams, this.dateRange)).then(
response => {
this.orderList = response.rows;
this.total = response.total;
this.loading = false;
}
);
},
// 取消按钮
cancel() {
this.open = false;
this.reset();
},
// 表单重置
reset() {
this.form = {
id: null,
delFlag: null,
createBy: null,
createTime: null,
updateBy: null,
updateTime: null,
remark: null,
orderNum: null,
userId: null,
type: null,
amount: null,
result: null,
status: null
};
this.resetForm("form");
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.dateRange = [];
this.resetForm("queryForm");
this.handleQuery();
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.id);
this.single = selection.length !== 1;
this.multiple = !selection.length;
},
/** 新增按钮操作 */
handleAdd() {
this.reset();
this.open = true;
this.title = "添加订单管理";
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset();
const id = row.id || this.ids;
getOrder(id).then(response => {
this.form = response.data;
this.open = true;
this.title = "修改订单管理";
});
},
/** 提交按钮 */
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
if (this.form.id != null) {
updateOrder(this.form).then(response => {
this.$modal.msgSuccess("修改成功");
this.open = false;
this.getList();
});
} else {
addOrder(this.form).then(response => {
this.$modal.msgSuccess("新增成功");
this.open = false;
this.getList();
});
}
}
});
},
/** 删除按钮操作 */
handleDelete(row) {
const ids = row.id || this.ids;
this.$modal
.confirm('是否确认删除订单管理编号为"' + ids + '"的数据项?')
.then(function() {
return delOrder(ids);
})
.then(() => {
this.getList();
this.$modal.msgSuccess("删除成功");
})
.catch(() => {});
},
/** 导出按钮操作 */
handleExport() {
this.download(
"ai/order/export",
{
...this.queryParams
},
`order_${new Date().getTime()}.xlsx`
);
}
}
};
</script>
<style>
.dialog-video {
width: 100%;
height: auto;
}
.preview-img {
width: 80px;
height: 60px;
object-fit: cover;
cursor: pointer;
border-radius: 4px;
}
.preview-video {
width: 120px;
height: 80px;
border-radius: 4px;
}
.dialog-img {
width: 100%;
height: auto;
}
.order-result-error-wrap {
display: inline-block;
}
.order-result-error-compact {
display: inline-block;
max-width: 200px;
margin: 0 auto;
font-size: 12px;
line-height: 1.35;
color: #c45656;
cursor: help;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: middle;
}
.order-result-error-tooltip-body {
max-width: 420px;
text-align: left;
line-height: 1.5;
font-size: 12px;
}
.order-result-error-tooltip-row {
margin-bottom: 6px;
}
.order-result-error-tooltip-row:last-child {
margin-bottom: 0;
}
.order-result-error-tooltip-row--msg .order-result-error-tooltip-v {
display: block;
margin-top: 2px;
white-space: pre-wrap;
word-break: break-word;
}
.order-result-error-tooltip-k {
font-weight: 600;
color: #fde2e2;
}
.order-result-error-tooltip-v {
color: #fff;
}
.text-ellipsis-two-lines {
/* 必须设置宽度(继承表格列宽,也可手动指定) */
width: 100%;
/* 核心属性:多行文本溢出隐藏 */
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2; /* 显示2行 */
line-clamp: 2; /* 标准属性,兼容现代浏览器 */
overflow: hidden;
/* 可选:调整行高,优化显示 */
line-height: 1.5;
max-height: 3em; /* 行高1.5 × 2行 = 3em防止高度溢出 */
/* 可选:文本对齐 */
text-align: center;
/* 解决scoped样式穿透如果需要 */
/deep/ & {
box-sizing: border-box;
padding: 0 4px;
}
}
</style>