Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c286f639e | ||
|
|
56131a4662 | ||
|
|
7209cd3ba2 | ||
|
|
a5262941ea | ||
|
|
490abd60e9 | ||
|
|
aa7c8856fb | ||
|
|
3f9b5e59a8 | ||
|
|
fbe2bcfee6 | ||
|
|
bdbf989b5e | ||
|
|
c2244f50b1 | ||
|
|
43d4392873 | ||
|
|
0ec961e210 | ||
|
|
3b0369850f | ||
|
|
42b6e75b77 | ||
|
|
d62f79a407 |
+2
-2
@@ -1,5 +1,5 @@
|
||||
# ==================== 第一阶段:构建前端 ====================
|
||||
FROM docker.m.daocloud.io/node:18-alpine AS builder
|
||||
FROM node:18-alpine AS builder
|
||||
WORKDIR /app
|
||||
# 配置Alpine国内镜像源
|
||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
|
||||
@@ -13,7 +13,7 @@ COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# ==================== 第二阶段:部署到Nginx ====================
|
||||
FROM docker.m.daocloud.io/nginx:alpine
|
||||
FROM nginx:alpine
|
||||
|
||||
# 复制构建产物
|
||||
COPY --from=builder /app/dist/ /usr/share/nginx/html/
|
||||
|
||||
@@ -78,6 +78,7 @@ server {
|
||||
server {
|
||||
listen 80;
|
||||
server_name gitea.redpowerfuture.com;
|
||||
client_max_body_size 500M;
|
||||
if ($badbot) {
|
||||
return 444;
|
||||
}
|
||||
@@ -88,6 +89,7 @@ server {
|
||||
server {
|
||||
listen 80;
|
||||
server_name minio.redpowerfuture.com;
|
||||
client_max_body_size 500M;
|
||||
location / {
|
||||
proxy_pass http://__API_HOST__:9001;
|
||||
proxy_http_version 1.1;
|
||||
@@ -106,6 +108,7 @@ server {
|
||||
server {
|
||||
listen 80;
|
||||
server_name consul.redpowerfuture.com;
|
||||
client_max_body_size 500M;
|
||||
location / {
|
||||
proxy_pass http://__API_HOST__:8500;
|
||||
proxy_http_version 1.1;
|
||||
@@ -120,6 +123,7 @@ server {
|
||||
server {
|
||||
listen 80;
|
||||
server_name meilisearch.redpowerfuture.com;
|
||||
client_max_body_size 500M;
|
||||
location / {
|
||||
proxy_pass http://__API_HOST__:7700;
|
||||
proxy_http_version 1.1;
|
||||
@@ -133,6 +137,7 @@ server {
|
||||
server {
|
||||
listen 80;
|
||||
server_name pm.redpowerfuture.com;
|
||||
client_max_body_size 500M;
|
||||
location / {
|
||||
proxy_pass http://__API_HOST__:8080;
|
||||
proxy_http_version 1.1;
|
||||
@@ -146,6 +151,7 @@ server {
|
||||
server {
|
||||
listen 80;
|
||||
server_name jaeger.redpowerfuture.com;
|
||||
client_max_body_size 500M;
|
||||
location / {
|
||||
proxy_pass http://__API_HOST__:16686;
|
||||
proxy_http_version 1.1;
|
||||
@@ -159,6 +165,7 @@ server {
|
||||
server {
|
||||
listen 80;
|
||||
server_name observer.redpowerfuture.com;
|
||||
client_max_body_size 500M;
|
||||
location / {
|
||||
proxy_pass http://__API_HOST__:18080;
|
||||
proxy_http_version 1.1;
|
||||
@@ -242,6 +249,7 @@ server {
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name gitea.redpowerfuture.com;
|
||||
client_max_body_size 500M;
|
||||
|
||||
ssl_certificate /etc/nginx/ssl/gitea.redpowerfuture.com.pem;
|
||||
ssl_certificate_key /etc/nginx/ssl/gitea.redpowerfuture.com.key;
|
||||
|
||||
@@ -1,213 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
/* ========== 全局重置 ========== */
|
||||
/* 去掉所有元素的自带边距和内边距,让布局从零开始 */
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
/* ========== 画布(视频输出尺寸) ========== */
|
||||
/* 这是最终视频的分辨率:宽1080px × 高1920px(竖屏 9:16,手机全屏) */
|
||||
/* 如果你想改输出尺寸,改这里的 width 和 height 就行 */
|
||||
body {
|
||||
width: 1080px; /* 画布宽度 ← 改成 720 就是 720×1280 */
|
||||
height: 1920px; /* 画布高度 ← 改成 1280 就是 720p */
|
||||
overflow: hidden; /* 超出画布的部分裁掉,不显示滚动条 */
|
||||
background: #000; /* 画布背景色(黑色),视频没铺满时露出的颜色 */
|
||||
}
|
||||
|
||||
/* ========== 所有元素的定位基础 ========== */
|
||||
/* 给所有带 clip 类的元素开启绝对定位,可以用 left/top 精确控制位置 */
|
||||
.clip {
|
||||
position: absolute; /* 绝对定位,相对于父容器(body)定位 */
|
||||
}
|
||||
|
||||
/* ========== 文字元素的通用样式 ========== */
|
||||
.text-element {
|
||||
font-family: Arial, Helvetica, sans-serif; /* 字体:首选Arial,不行就Helvetica,再不行用系统无衬线体 */
|
||||
text-align: center; /* 文字水平居中 */
|
||||
display: flex; /* 启用弹性盒子布局 */
|
||||
align-items: center; /* 垂直居中(配合flex使用) */
|
||||
justify-content: center; /* 水平居中(配合flex使用) */
|
||||
white-space: pre-wrap; /* 保留换行,文字超出宽度自动换行 */
|
||||
word-break: break-word; /* 长单词或URL撑破容器时强制换行 */
|
||||
}
|
||||
|
||||
/* ========== 入场动画定义 ========== */
|
||||
/* 想改动画时长,改后面的秒数(如 0.5s → 1s 就是慢一倍) */
|
||||
|
||||
/* 淡入:从完全透明 → 完全可见 */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; } /* 开始:透明(完全看不见) */
|
||||
to { opacity: 1; } /* 结束:不透明(完全可见) */
|
||||
}
|
||||
|
||||
/* 上滑:从下方40px处边向上移动边显现 */
|
||||
@keyframes slideUp {
|
||||
from { opacity: 0; transform: translateY(40px); } /* 开始:透明 + 在下方40px */
|
||||
to { opacity: 1; transform: translateY(0); } /* 结束:可见 + 回到原位 */
|
||||
}
|
||||
|
||||
/* 左滑:从右侧40px处边向左移动边显现(视觉上像从右边滑入) */
|
||||
@keyframes slideLeft {
|
||||
from { opacity: 0; transform: translateX(40px); } /* 开始:透明 + 在右边40px */
|
||||
to { opacity: 1; transform: translateX(0); } /* 结束:可见 + 回到原位 */
|
||||
}
|
||||
|
||||
/* 放大:从缩小到80%边放大边显现 */
|
||||
@keyframes scaleIn {
|
||||
from { opacity: 0; transform: scale(0.8); } /* 开始:透明 + 缩到80%大小 */
|
||||
to { opacity: 1; transform: scale(1); } /* 结束:可见 + 100%正常大小 */
|
||||
}
|
||||
|
||||
/* 脉冲:一直循环缩放,像呼吸一样忽大忽小(一般用于强调/吸引注意) */
|
||||
@keyframes pulse {
|
||||
0% { transform: scale(1); } /* 开始:正常大小 */
|
||||
50% { transform: scale(1.05); } /* 中间:放大到1.05倍(105%)*/
|
||||
100% { transform: scale(1); } /* 结束:回到正常大小 */
|
||||
}
|
||||
|
||||
/* ========== 动画类名 ========== */
|
||||
/* 给元素加上 class="anim-xxx" 就能用上面的动画,用法看下面的HTML元素 */
|
||||
|
||||
/* 淡入 0.5秒,先快后慢 */
|
||||
.anim-fadeIn { animation: fadeIn 0.5s ease-out; }
|
||||
|
||||
/* 上滑 0.6秒,先快后慢 */
|
||||
.anim-slideUp { animation: slideUp 0.6s ease-out; }
|
||||
|
||||
/* 左滑 0.6秒,先快后慢 */
|
||||
.anim-slideLeft { animation: slideLeft 0.6s ease-out; }
|
||||
|
||||
/* 放大入场 0.5秒,先快后慢 */
|
||||
.anim-scaleIn { animation: scaleIn 0.5s ease-out; }
|
||||
|
||||
/* 脉冲 1.5秒一个循环,一直重复(infinite) */
|
||||
.anim-pulse { animation: pulse 1.5s ease-in-out infinite; }
|
||||
|
||||
/* ── 动画参数怎么改? ── */
|
||||
/* • 0.5s → 改成 1s 动画就慢一倍,改 0.2s 就快一倍 */
|
||||
/* • ease-out → 开始快结尾慢,换成 linear 就是匀速,换成 ease-in 就是开始慢结尾快 */
|
||||
/* • infinite → 一直重复,去掉就不重复只播一次 */
|
||||
</style>
|
||||
|
||||
<!-- GSAP:专业动画引擎,用于精确控制时间轴(当前脚本只用了初始化,没做复杂动画) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ====================================================================== -->
|
||||
<!-- 舞台容器:所有元素都放在这里面 -->
|
||||
<!-- data-composition-id 是内部标识,不要改 -->
|
||||
<!-- data-width / data-height 需要和上面的 body 宽高一致 -->
|
||||
<!-- ====================================================================== -->
|
||||
<div id="stage" data-composition-id="main" data-start="0" data-width="1080" data-height="1920">
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- data-key 属性:这个元素的内容会被程序自动替换 -->
|
||||
<!-- 比如 data-key="title",程序就会用"标题"对应的文字替换掉这个元素的内容 -->
|
||||
<!-- 所以里面的"标题文字"只是占位,实际显示什么由数据决定 -->
|
||||
<!-- ================================================================== -->
|
||||
|
||||
<!-- ======================================================== -->
|
||||
<!-- 标题:红色大标题,靠上居中 -->
|
||||
<!-- ======================================================== -->
|
||||
<!--
|
||||
data-key="title" → 程序会用标题数据替换此元素内容
|
||||
data-start="0" → 第0秒开始显示(单位:秒)
|
||||
data-duration="58.000" → 持续显示58秒
|
||||
class="clip" → 绝对定位,可以用 left/top 控制位置
|
||||
class="text-element" → 文字居中、自动换行
|
||||
class="anim-slideUp" → 入场动画:从下方滑入,0.6秒
|
||||
-->
|
||||
<div data-key="title" data-start="0" data-duration="58.000" data-track-index="1"
|
||||
class="clip text-element anim-slideUp"
|
||||
style="
|
||||
left: 50%; /* 水平居中定位(配合 translateX 使用) */
|
||||
top: 70px; /* 距离顶部70px ← 改这个数字上下移动 */
|
||||
transform: translateX(-50%); /* 把元素向左拉回自身宽度的一半,实现真居中 */
|
||||
width: 972px; /* 文字区域宽度 ← 改窄一点文字就缩短 */
|
||||
max-width: 972px; /* 最大宽度,防止被撑破 */
|
||||
font-size: 48px; /* 字号 ← 改大/改小 */
|
||||
color: #D82828; /* 文字颜色(红色)← 改成 #FFFFFF 就是白色 */
|
||||
">标题文字</div>
|
||||
|
||||
<!-- ======================================================== -->
|
||||
<!-- 产品图片:左上角位置,图片链接由程序自动替换 -->
|
||||
<!-- ======================================================== -->
|
||||
<!--
|
||||
这是 <img> 标签,src 属性会被程序替换成真实的图片 URL
|
||||
data-key="product_image" → 程序用产品图片的URL替换 src
|
||||
-->
|
||||
<img data-key="product_image" data-start="0" data-duration="58.000" data-track-index="2"
|
||||
class="clip anim-fadeIn"
|
||||
style="
|
||||
width: 200px; /* 图片宽度 ← 改这个调大小 */
|
||||
height: 200px; /* 图片高度 ← 改这个调大小 */
|
||||
left: 0px; /* 距离左边0px(贴左边缘)← 改大往右移 */
|
||||
top: 200px; /* 距离顶部200px ← 改大往下移 */
|
||||
"
|
||||
src="占位图片.jpg" />
|
||||
|
||||
<!-- ======================================================== -->
|
||||
<!-- 产品卖点描述:居中偏下位置 -->
|
||||
<!-- ======================================================== -->
|
||||
<div data-key="product_desc" data-start="0" data-duration="58.000" data-track-index="3"
|
||||
class="clip text-element"
|
||||
style="
|
||||
left: 50%; /* 水平居中 */
|
||||
top: 830px; /* 距离顶部830px ← 改大就往下移 */
|
||||
transform: translateX(-50%); /* 水平居中补偿 */
|
||||
width: 972px; /* 文本宽度 */
|
||||
max-width: 972px; /* 最大宽度 */
|
||||
font-size: 34px; /* 字号 ← 改大/改小 */
|
||||
color: #333333; /* 文字颜色(深灰色)← 改成 #000 就是黑色 */
|
||||
">产品卖点描述</div>
|
||||
|
||||
<!-- ======================================================== -->
|
||||
<!-- 底部免责声明:背景半透明黑底白字,圆角,贴底部 -->
|
||||
<!-- ======================================================== -->
|
||||
<div data-key="disclaimer" data-start="0" data-duration="58.000" data-track-index="5"
|
||||
class="clip text-element"
|
||||
style="
|
||||
left: 50%; /* 水平居中 */
|
||||
top: calc(100% - 45px); /* 距离底部45px(100%是父容器高度)← 改45调距离 */
|
||||
transform: translateX(-50%); /* 水平居中补偿 */
|
||||
width: 972px; /* 文本宽度 */
|
||||
max-width: 972px;
|
||||
font-size: 20px; /* 字号 */
|
||||
color: #FFFFFF; /* 文字颜色(白色) */
|
||||
">
|
||||
<!--
|
||||
内部 span:负责背景和间距
|
||||
background: rgba(0,0,0,0.70) → 黑色半透明背景,70%不透明度
|
||||
← 改最后一个数字(0~1)调透明度:0=全透,1=全黑
|
||||
padding: 12px 24px → 内边距:上下12px,左右24px
|
||||
← 改这个让背景范围变大/变小
|
||||
border-radius: 8px → 圆角大小
|
||||
← 改大更圆(如 20px),改 0 就是直角
|
||||
-->
|
||||
<span style="background:rgba(0,0,0,0.70); padding:12px 24px; border-radius:8px;">
|
||||
免责声明文字
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<!-- GSAP 时间轴初始化脚本 -->
|
||||
<!-- 当前只初始化了一个空时间轴,给程序框架用,一般不需要改 -->
|
||||
<!-- ================================================================ -->
|
||||
<script>
|
||||
(function() {
|
||||
var tl = gsap.timeline({ paused: true }); // 建一个暂停的时间轴
|
||||
tl.to('#stage', { duration: 0, opacity: 1 }, 0); // 舞台立刻显示(第0秒)
|
||||
window.__timelines = window.__timelines || {}; // 存到全局,供程序调用
|
||||
window.__timelines['main'] = tl; // 时间轴名字叫 'main'
|
||||
})();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -24,6 +24,8 @@ export interface VOSessionInfoResult {
|
||||
totalTokens: number;
|
||||
totalFee: number;
|
||||
errorMsg: string;
|
||||
// 失败详细原因(工作流节点失败详情,如「执行工作流失败: ...节点:生成视频, 失败原因:...」),比 errorMsg 更具体
|
||||
error?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,22 @@ export function deleteUser(ids: number[]) {
|
||||
});
|
||||
}
|
||||
|
||||
export function recharge(data: object) {
|
||||
return request({
|
||||
url: '/shop-user-trade/account/controller/recharge',
|
||||
method: 'post',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function getWalletLogs(userId: number, pageNum: number, pageSize: number) {
|
||||
return request({
|
||||
url: '/shop-user-trade/account/controller/logs',
|
||||
method: 'get',
|
||||
params: { userId, pageNum, pageSize },
|
||||
});
|
||||
}
|
||||
|
||||
export function checkIsSuperAdmin() {
|
||||
return request({
|
||||
url: '/admin-go/api/v1/system/user/checkIsSuperAdmin',
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import request from '/@/utils/request';
|
||||
|
||||
/** 计价对象枚举 */
|
||||
export function getSubjects() {
|
||||
return request({
|
||||
url: '/shop-user-trade/pricing/controller/subjects',
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/** 计价配置详情 */
|
||||
export function getPricingConfig(params: { subjectType: string; subjectId: string }) {
|
||||
return request({
|
||||
url: '/shop-user-trade/pricing/controller/config/get',
|
||||
method: 'get',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/** 计价对象可选计费方式(workflow/business 使用;model 计价方式由配置规则决定,不调用) */
|
||||
export function getChargeModes(params: { subjectType: string; subjectId: string }) {
|
||||
return request({
|
||||
url: '/shop-user-trade/pricing/controller/subjects/charge-modes',
|
||||
method: 'get',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/** 新增/更新计价配置(id > 0 更新,0 新增;rules 传费率对象;id 兼容雪花字符串避免精度丢失) */
|
||||
export function savePricingConfig(data: {
|
||||
id: string | number;
|
||||
subjectType: string;
|
||||
subjectId: string;
|
||||
rules: object;
|
||||
minBalance?: number;
|
||||
currency?: string;
|
||||
enabled?: number;
|
||||
}) {
|
||||
return request({
|
||||
url: '/shop-user-trade/pricing/controller/config/save',
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
}
|
||||
@@ -166,7 +166,7 @@ function nextImageKey(template: PatchTemplate): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
const DEFAULT_TEMPLATE_URL = 'https://cdn.redpowerfuture.com/tenantid-1/template_example1.html';
|
||||
const DEFAULT_TEMPLATE_URL = 'https://cdn.redpowerfuture.com/tenantid-1/vertical_template_example.html';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: PatchTemplate[];
|
||||
@@ -290,14 +290,24 @@ const handleImageUpload = async (templateIndex: number, imgIdx: number, file: an
|
||||
}
|
||||
};
|
||||
|
||||
// 下载参考模板文件
|
||||
const handleDownload = (_e?: Event) => {
|
||||
const a = document.createElement('a');
|
||||
a.href = '/template_example.html';
|
||||
a.download = 'template_example.html';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
// 下载参考模板文件:统一走 CDN,转 blob 触发下载(避免跨域 <a download> 直接打开页面)
|
||||
const handleDownload = async (_e?: Event) => {
|
||||
try {
|
||||
const res = await fetch(DEFAULT_TEMPLATE_URL);
|
||||
if (!res.ok) throw new Error('参考模板获取失败');
|
||||
const blob = await res.blob();
|
||||
const fileName = decodeURIComponent(DEFAULT_TEMPLATE_URL.split('/').pop() || 'vertical_template_example.html');
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = blobUrl;
|
||||
a.download = fileName;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || '参考模板下载失败');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -203,11 +203,12 @@ export default defineComponent({
|
||||
}
|
||||
},
|
||||
})
|
||||
.then(async () => {
|
||||
.then(() => {
|
||||
// 手动退出登录也只清理登录态缓存,保留主题、语言等本地配置。
|
||||
Session.clearAuth();
|
||||
// 显式回到登录页,避免保留之前受保护页面的重定向参数
|
||||
await router.replace('/login');
|
||||
// 整页跳转登录页(刷新重建路由实例):SPA 内 router.replace 不会重置已注册的动态路由,
|
||||
// 重新登录时 setAddRoute 的 hasRoute 判断会跳过新菜单注册,出现"菜单显示但点击 404"(如价格管理页)
|
||||
window.location.href = import.meta.env.BASE_URL + '#/login';
|
||||
})
|
||||
.catch(() => {});
|
||||
} else if (path === 'wareHouse') {
|
||||
|
||||
@@ -31,6 +31,10 @@
|
||||
color: #f56c6c !important;
|
||||
}
|
||||
|
||||
.op-btn-recharge {
|
||||
color: #67c23a !important; // 充值 绿色
|
||||
}
|
||||
|
||||
[data-theme='dark'] {
|
||||
// textarea - css vars
|
||||
--w-e-textarea-bg-color: var(--el-color-white) !important;
|
||||
@@ -49,6 +53,10 @@
|
||||
color: #f56c6c !important; // 删除 红色
|
||||
}
|
||||
|
||||
.op-btn-recharge {
|
||||
color: #67c23a !important; // 充值 绿色
|
||||
}
|
||||
|
||||
// toolbar - css vars
|
||||
--w-e-toolbar-color: var(--el-text-color-primary) !important;
|
||||
--w-e-toolbar-bg-color: var(--el-color-white) !important;
|
||||
|
||||
@@ -33,6 +33,12 @@
|
||||
|
||||
<div v-show="!collapsed" class="workflow-form-scroll">
|
||||
<el-form label-position="top" class="workflow-form">
|
||||
<!-- 计费方式:必选、不设置默认值;可选项来自计价对象 charge-modes 接口(workflow) -->
|
||||
<el-form-item label="计费方式" required>
|
||||
<el-select v-model="chargeMode" :disabled="isDisabled" placeholder="请选择计费方式" class="w100">
|
||||
<el-option v-for="opt in chargeModes" :key="opt.mode" :label="opt.name" :value="opt.mode" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<template v-if="detail?.nodeInputParams">
|
||||
<template v-for="node in detail.nodeInputParams" :key="node.id || node.nodeCode">
|
||||
<template v-if="hasFormConfig(node)">
|
||||
@@ -165,19 +171,26 @@
|
||||
|
||||
<!-- 底部操作区:editing 提示 / running 执行中 / done·failed 状态标签(执行统一由 InputBar 发送按钮触发) -->
|
||||
<div class="workflow-form-footer">
|
||||
<div v-if="isFailed" class="form-error-text">{{ formError }}</div>
|
||||
<!-- 执行中:节点文本进度推进(后端 node_start/node_complete 事件),无进度时兜底「执行中...」;
|
||||
停止统一由 InputBar 发送按钮(变停止)触发,卡片内不再提供取消入口 -->
|
||||
<div v-if="submitting" class="executing-progress">
|
||||
<span class="exec-spinner" />
|
||||
<span class="exec-text">{{ progressText }}</span>
|
||||
</div>
|
||||
<!-- 已执行卡片(done/failed,含可编辑的最后一张):显示状态标签,参数是否可编辑由 editable 决定 -->
|
||||
<el-tag v-else-if="formStatus === 'done' || formStatus === 'failed'" :type="isFailed ? 'danger' : 'success'" size="small" effect="light">
|
||||
{{ isFailed ? '执行失败' : '执行完成' }}
|
||||
</el-tag>
|
||||
<!-- 已执行卡片(done/failed,含可编辑的最后一张):状态标签 + 失败原因按钮(默认收起,点击手动展开) -->
|
||||
<div v-else-if="formStatus === 'done' || formStatus === 'failed'" class="form-status-row">
|
||||
<el-tag :type="isFailed ? 'danger' : 'success'" size="small" effect="light">
|
||||
{{ isFailed ? '执行失败' : '执行完成' }}
|
||||
</el-tag>
|
||||
<el-button v-if="isFailed && formError" text size="small" class="form-error-toggle" @click="showError = !showError">
|
||||
{{ showError ? '收起失败原因' : '查看失败原因' }}
|
||||
<el-icon class="error-arrow" :class="{ 'is-expanded': showError }"><ArrowDown /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<!-- 编辑态:无执行按钮,提示通过下方发送按钮执行 -->
|
||||
<span v-else class="form-edit-tip">填写参数后,点击下方发送按钮执行工作流</span>
|
||||
<!-- 失败原因详情:默认收起,点「查看失败原因」展开(error 字段可能含多行详细原因) -->
|
||||
<div v-if="isFailed && showError && formError" class="form-error-text">{{ formError }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -188,6 +201,7 @@ import { ElMessage } from 'element-plus';
|
||||
import { ArrowDown, CircleCheckFilled, CircleCloseFilled } from '@element-plus/icons-vue';
|
||||
import PatchTemplateEditor from '/@/components/patchTemplate/PatchTemplateEditor.vue';
|
||||
import { uploadFile } from '/@/api/common/upload';
|
||||
import { getChargeModes } from '/@/api/trade/pricing';
|
||||
import { collectHomeFormFields } from '../utils/flowDsl';
|
||||
import type { WorkflowNodeStep } from '../utils/wsMessage';
|
||||
|
||||
@@ -209,7 +223,7 @@ interface Props {
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'submit', payload: { formValues: Record<string, any>; formFileNames: Record<string, string | string[]>; templates: any[] }): void;
|
||||
(e: 'submit', payload: { formValues: Record<string, any>; formFileNames: Record<string, string | string[]>; templates: any[]; chargeMode: string }): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -229,11 +243,47 @@ const formFileNames = reactive<Record<string, string | string[]>>({});
|
||||
const uploadingFields = reactive<Record<string, boolean>>({});
|
||||
const templates = ref<any[]>([]);
|
||||
|
||||
// ===== 计费方式:必选、不设置默认值(新建为空强制选择;回显重跑恢复上次选择) =====
|
||||
interface ChargeModeOption {
|
||||
mode: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const chargeMode = ref('');
|
||||
const chargeModes = ref<ChargeModeOption[]>([]);
|
||||
/** 计费方式可选项缓存:workflow 计价对象固定(subjectType/subjectId 均 workflow),模块级共享避免每张卡片重复请求 */
|
||||
let workflowChargeModesCache: ChargeModeOption[] | null = null;
|
||||
|
||||
const loadChargeModes = async () => {
|
||||
if (workflowChargeModesCache) {
|
||||
chargeModes.value = workflowChargeModesCache;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res: any = await getChargeModes({ subjectType: 'workflow', subjectId: 'workflow' });
|
||||
const list: ChargeModeOption[] = res?.data?.chargeModes || [];
|
||||
chargeModes.value = list;
|
||||
workflowChargeModesCache = list;
|
||||
} catch {
|
||||
// 计费方式拉取失败:全局拦截器已提示,下拉留空(必填校验会拦截提交)
|
||||
chargeModes.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
// 状态派生:失败 = 携带 formError;禁用编辑 = 定格(done/failed)卡片默认只读,
|
||||
// 但最后一张已执行卡片(editable)允许改参;执行中(submitting)一律禁用
|
||||
const isFailed = computed(() => !!props.formError);
|
||||
const isDisabled = computed(() => (props.readonly && !props.editable) || props.submitting);
|
||||
|
||||
// 失败原因默认收起:新失败时重置为不展示,用户点「查看失败原因」才展开
|
||||
const showError = ref(false);
|
||||
watch(
|
||||
() => props.formError,
|
||||
() => {
|
||||
showError.value = false;
|
||||
}
|
||||
);
|
||||
|
||||
// 表单收起/展开:editing(待提交/回显)默认展开;提交(running)自动收起;
|
||||
// done/failed 保持提交时状态(默认收起,用户可手动展开查看已填值)
|
||||
const collapsed = ref(false);
|
||||
@@ -435,6 +485,11 @@ const hasFormFields = computed(() => {
|
||||
});
|
||||
|
||||
const validateFormFields = (): boolean => {
|
||||
// 计费方式必选、无默认值:未选择时阻止提交(含无表单参数的工作流)
|
||||
if (!chargeMode.value) {
|
||||
ElMessage.warning('请选择计费方式');
|
||||
return false;
|
||||
}
|
||||
if (!props.detail?.nodeInputParams) return true;
|
||||
for (const node of props.detail.nodeInputParams as any[]) {
|
||||
if (String(node?.nodeCode || '').toLowerCase() !== '__start__') continue;
|
||||
@@ -462,6 +517,10 @@ watch(
|
||||
Object.keys(fieldFiles).forEach((key) => delete fieldFiles[key]);
|
||||
Object.keys(formFileNames).forEach((key) => delete formFileNames[key]);
|
||||
Object.keys(uploadingFields).forEach((key) => delete uploadingFields[key]);
|
||||
// 计费方式:新建(handleWorkflowSelect 已清空后端自带 chargeMode)为空强制选择;
|
||||
// 回显重跑卡片从 flowContent.chargeMode 恢复上次选择
|
||||
chargeMode.value = detail?.flowContent?.chargeMode || '';
|
||||
loadChargeModes();
|
||||
// 尝试从执行详情恢复贴片模板
|
||||
// 提交路径:templates 写入开启贴片布局的节点内部(patchLayout === true 的 node.templates),顶层已删除;
|
||||
// 回显时 detail.nodeInputParams 即提交时的 nodes,从该处优先恢复;兼容旧数据(extension / detail / flowContent 顶层)
|
||||
@@ -543,6 +602,7 @@ const handleSubmit = () => {
|
||||
formValues: JSON.parse(JSON.stringify(formValues)),
|
||||
formFileNames: JSON.parse(JSON.stringify(formFileNames)),
|
||||
templates: JSON.parse(JSON.stringify(templates.value || [])),
|
||||
chargeMode: chargeMode.value,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -754,19 +814,41 @@ watch(
|
||||
/* 底部操作区 */
|
||||
.workflow-form-footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap; /* 失败原因详情换行到状态行下方 */
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 20px;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
background: #fafbfc;
|
||||
flex-shrink: 0;
|
||||
.form-error-text {
|
||||
.form-status-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.form-error-toggle {
|
||||
padding: 0;
|
||||
height: auto;
|
||||
font-size: 12px;
|
||||
color: #ef4444;
|
||||
.error-arrow {
|
||||
transition: transform 0.2s;
|
||||
&.is-expanded { transform: rotate(180deg); }
|
||||
}
|
||||
}
|
||||
.form-error-text {
|
||||
flex-basis: 100%; /* 占整行,显示在状态行下方 */
|
||||
font-size: 12px;
|
||||
color: #ef4444;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap; /* 失败详情可能含换行(error 字段多行原因),允许换行展示 */
|
||||
word-break: break-word;
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fee2e2;
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
.form-edit-tip {
|
||||
flex: 1;
|
||||
|
||||
@@ -105,7 +105,7 @@ import { applyHomeFormValues, buildOutputsFromUrls } from './utils/flowDsl';
|
||||
import type { WorkflowOutput } from './utils/flowDsl';
|
||||
import { getChatModel, listModelManage } from '/@/api/settings/modelConfigV2';
|
||||
import { connectSessionSocket, sendAgentStart, sendWorkflowStart, sendCancel } from './utils/wsExecute';
|
||||
import { parseWsMessage, getDelta, getAnswer, getRecordId, getErrorText, getNodeEvent, getToolCallName, getToolResultText, type WorkflowNodeProgress } from './utils/wsMessage';
|
||||
import { parseWsMessage, getDelta, getAnswer, getRecordId, getErrorText, getErrorDetail, getNodeEvent, getToolCallName, getToolResultText, type WorkflowNodeProgress } from './utils/wsMessage';
|
||||
import { getApiErrorMessage } from '/@/utils/request';
|
||||
import {
|
||||
getWorkflowDetail,
|
||||
@@ -690,6 +690,8 @@ const handleWorkflowSelect = async (workflowId: string | null, isTemplate?: bool
|
||||
removeDraftFormCard();
|
||||
const res = await getWorkflowDetail(workflowId);
|
||||
selectedWorkflowDetail.value = res.data || null;
|
||||
// 新建表单不沿用后端工作流详情自带的 chargeMode(如缺省 per_item):计费方式必选且无默认值,强制用户选择
|
||||
if (res.data?.flowContent) delete res.data.flowContent.chargeMode;
|
||||
// 对话卡片化:选中工作流即推送一张可填表单卡片进入消息流
|
||||
if (res.data) pushFormCard(res.data);
|
||||
} catch {
|
||||
@@ -1042,7 +1044,7 @@ const runWorkflow = async (
|
||||
sid: string,
|
||||
sessionId: string,
|
||||
detail: any,
|
||||
opts: { formValues: Record<string, any>; formFileNames: Record<string, string | string[]>; templates?: any[] },
|
||||
opts: { formValues: Record<string, any>; formFileNames: Record<string, string | string[]>; templates?: any[]; chargeMode?: string },
|
||||
formMsg: ChatMessage
|
||||
) => {
|
||||
const curSession = historyList.value.find((h) => h.id === sid);
|
||||
@@ -1119,6 +1121,8 @@ const runWorkflow = async (
|
||||
// 构建 flowContent:templates 不再放最外层(删除 detail.flowContent 可能遗留的顶层 templates,统一走节点)
|
||||
const updatedFlowContent: any = { ...detail.flowContent, nodes: nodeInputParams };
|
||||
delete updatedFlowContent.templates;
|
||||
// 计费方式写入 flowContent(与 version 平级),后端按此计费;表单必填已拦截未选,此处仅兜底
|
||||
if (opts.chargeMode) updatedFlowContent.chargeMode = opts.chargeMode;
|
||||
|
||||
// 3. 工作流模型由后端按 flow 配置决定,无需前端取模型 id(普通对话取模型见 runChat)
|
||||
|
||||
@@ -1136,13 +1140,14 @@ const runWorkflow = async (
|
||||
formMsg.recordType = 'workflow';
|
||||
}
|
||||
} else if (msg.type === 'error') {
|
||||
// 失败:执行中的节点标记为失败,执行过程区展示中断位置
|
||||
// 失败:执行中的节点标记为失败,执行过程区展示中断位置;
|
||||
// 失败原因绑定 error 字段(节点失败详情),供卡片「查看失败原因」手动展开
|
||||
if (formMsg.formProgress) {
|
||||
formMsg.formProgress.nodes.forEach((n) => {
|
||||
if (n.status === 'running') n.status = 'failed';
|
||||
});
|
||||
}
|
||||
finishExec(false, getErrorText(msg));
|
||||
finishExec(false, getErrorDetail(msg));
|
||||
} else if (msg.type === 'flow_complete') {
|
||||
// flow_complete 为后端新增的工作流完成信号,事件自带产出文件 URL 列表 → 直接渲染产出卡片;
|
||||
// 以 round_start 首帧填充的 roundRecordId 作 backendId,保证实时产出可删除(与回显路径一致)
|
||||
@@ -1234,9 +1239,20 @@ const snapshotFormValuesToMsg = (msg: ChatMessage, formValues: Record<string, an
|
||||
});
|
||||
};
|
||||
|
||||
// 提交时把用户填的贴片模板写回消息对象 patchLayout 节点(与表单值快照同理):
|
||||
// 会话认领(virtual→UUID 触发 MainContent 重建)/ 历史回显重挂载时,WorkflowFormCard 从 msg.form
|
||||
// 恢复贴片模板能取到本次填写值,避免新建会话首次执行后展开贴片模板回显空白
|
||||
const snapshotTemplatesToMsg = (msg: ChatMessage, templates: any[]) => {
|
||||
const nodes = msg.form?.nodeInputParams;
|
||||
if (!Array.isArray(nodes)) return;
|
||||
nodes.forEach((n: any) => {
|
||||
if (n.patchLayout === true) n.templates = templates;
|
||||
});
|
||||
};
|
||||
|
||||
const startWorkflowFromCard = async (
|
||||
msg: ChatMessage,
|
||||
payload: { formValues: Record<string, any>; formFileNames: Record<string, string | string[]>; templates?: any[] }
|
||||
payload: { formValues: Record<string, any>; formFileNames: Record<string, string | string[]>; templates?: any[]; chargeMode?: string }
|
||||
) => {
|
||||
// 定位卡片所在会话
|
||||
let sid = '';
|
||||
@@ -1254,6 +1270,16 @@ const startWorkflowFromCard = async (
|
||||
// 会话认领(virtual→UUID 触发 MainContent 重建)/ 历史回显重挂载时能从消息对象恢复已填值,
|
||||
// 避免执行后表单被清空
|
||||
snapshotFormValuesToMsg(msg, payload.formValues);
|
||||
// 贴片模板同样写回消息对象 patchLayout 节点(保持与后端执行副本一致),重建/重挂载后能恢复回显
|
||||
if (Array.isArray(payload.templates)) {
|
||||
snapshotTemplatesToMsg(msg, payload.templates);
|
||||
}
|
||||
// 计费方式写回消息对象 flowContent(快照):会话认领(virtual→UUID 触发 MainContent 重建)/历史回显重挂载时,
|
||||
// WorkflowFormCard 从 flowContent.chargeMode 恢复本次选择,避免重挂载后计费方式被清空
|
||||
if (payload.chargeMode) {
|
||||
if (!msg.form.flowContent || typeof msg.form.flowContent !== 'object') msg.form.flowContent = {};
|
||||
msg.form.flowContent.chargeMode = payload.chargeMode;
|
||||
}
|
||||
|
||||
// 标记本轮发送中:工作流执行期间 InputBar 显示停止按钮(isGenerating 依赖 sendingSessions)
|
||||
sendingSessions[sid] = true;
|
||||
@@ -1632,8 +1658,8 @@ const loadWorkflowSessionMessages = async (sid: string, results: VOSessionInfoRe
|
||||
if (q) msgs.push({ id: 'rq-' + r.id, content: String(q), time: r.createdAt || '', isUser: true, recordId: String(r.id), recordType: r.type });
|
||||
if (r.resultContent) msgs.push({ id: 'ra-' + r.id, content: String(r.resultContent), time: r.createdAt || '', isUser: false, recordId: String(r.id), recordType: r.type });
|
||||
} else if (r.type === 'workflow') {
|
||||
// 失败判断:以 errorMsg 为准(部分失败记录 status 仍是 1,仅凭 status 会误判为成功)
|
||||
const failed = !!r.errorMsg;
|
||||
// 失败判断:error / errorMsg 任一有值即失败(部分失败记录 status 仍是 1,仅凭 status 会误判为成功)
|
||||
const failed = !!(r.errorMsg || r.error);
|
||||
// 每次执行都渲染带值表单卡片(requestParams.nodes 即该次提交的表单值快照);
|
||||
// form 补 id=flowId,供失败卡片「重新编辑并执行」时作为启动 flowId
|
||||
if (Array.isArray(r.requestParams?.nodes)) {
|
||||
@@ -1650,7 +1676,8 @@ const loadWorkflowSessionMessages = async (sid: string, results: VOSessionInfoRe
|
||||
flowContent: r.requestParams,
|
||||
},
|
||||
formStatus: failed ? 'failed' : 'done',
|
||||
formError: failed ? r.errorMsg || '执行失败' : '',
|
||||
// 失败原因绑定 error 字段(节点失败详情);errorMsg(如「用户已终止执行」)仅作兜底
|
||||
formError: failed ? r.error || r.errorMsg || '执行失败' : '',
|
||||
});
|
||||
}
|
||||
// 结果消息:成功记录直接带产出(session/get 记录自带的 resultFileUrl),不再调 execution/get
|
||||
|
||||
@@ -169,3 +169,16 @@ export function getErrorText(msg: WsStreamMessage): string {
|
||||
}
|
||||
return '执行失败,请重试';
|
||||
}
|
||||
|
||||
/** error 详细原因(error 字段优先展示失败详情;message 仅在 error 缺失时兜底)——供工作流失败卡片「查看失败原因」使用 */
|
||||
export function getErrorDetail(msg: WsStreamMessage): string {
|
||||
if (typeof msg?.error === 'string' && msg.error) return msg.error;
|
||||
if (typeof msg?.message === 'string' && msg.message) return msg.message;
|
||||
const d = msg?.data;
|
||||
if (typeof d === 'string') return d;
|
||||
if (d && typeof d === 'object') {
|
||||
const nested = d.message ?? d.error ?? d.msg;
|
||||
if (typeof nested === 'string' && nested) return nested;
|
||||
}
|
||||
return '执行失败,请重试';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
<template>
|
||||
<div class="editor-body">
|
||||
<div class="editor-col">
|
||||
<!-- 通用配置 -->
|
||||
<div class="card">
|
||||
<div class="card-head">
|
||||
<span class="card-title">通用配置</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="cfg-row">
|
||||
<div class="cfg-item">
|
||||
<div class="cfg-label">门禁金额(元)</div>
|
||||
<el-input-number v-model="minBalance" :min="0" :precision="5" :step="0.01" :controls="false" class="cfg-min-balance" />
|
||||
</div>
|
||||
<div class="cfg-item">
|
||||
<div class="cfg-label">币种</div>
|
||||
<el-select v-model="currency" class="cfg-currency">
|
||||
<el-option label="人民币" value="CNY" />
|
||||
<el-option label="美元" value="USD" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="cfg-item">
|
||||
<div class="cfg-label">启用</div>
|
||||
<el-switch v-model="enabled" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor-col">
|
||||
<!-- 周期订阅 -->
|
||||
<div class="card">
|
||||
<div class="card-head">
|
||||
<span class="card-title">周期订阅</span>
|
||||
<span class="head-hint">按周期一次性收取,与用量无关</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<el-form label-position="top" size="default">
|
||||
<el-form-item label="周期">
|
||||
<el-select v-model="period" class="ctrl-select">
|
||||
<el-option label="按年" value="year" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="每周期价格(元)">
|
||||
<el-input-number
|
||||
v-model="price"
|
||||
:min="0"
|
||||
:precision="5"
|
||||
:step="0.01"
|
||||
:controls="false"
|
||||
class="ctrl-input"
|
||||
/>
|
||||
</el-form-item>
|
||||
<div class="form-hint">本期支持按年订阅;订阅购买 / 续费流程接入另期。</div>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import {
|
||||
type BusinessRules,
|
||||
type Currency,
|
||||
type EditorInitConfig,
|
||||
type Subject,
|
||||
} from '../mock';
|
||||
|
||||
/**
|
||||
* 业务模块计价编辑面板。
|
||||
* 周期订阅 per_period:period 固定 year,价格 + 公共配置。
|
||||
* initConfig 传入后端已保存配置(parse 后)时初始化表单;无配置时用默认值。
|
||||
*/
|
||||
const props = defineProps<{
|
||||
subject: Subject;
|
||||
initConfig: EditorInitConfig | null;
|
||||
}>();
|
||||
|
||||
const period = ref<'year'>('year');
|
||||
const price = ref(1999);
|
||||
const minBalance = ref(0);
|
||||
const currency = ref<Currency>('CNY');
|
||||
const enabled = ref(true);
|
||||
|
||||
/** 用后端已保存配置初始化表单 */
|
||||
function applyConfig(cfg: EditorInitConfig) {
|
||||
const r = cfg.rules as BusinessRules;
|
||||
period.value = r.period;
|
||||
price.value = r.price;
|
||||
minBalance.value = cfg.minBalance;
|
||||
currency.value = cfg.currency;
|
||||
enabled.value = cfg.enabled;
|
||||
}
|
||||
|
||||
/** 重置为默认(无配置) */
|
||||
function resetDefault() {
|
||||
period.value = 'year';
|
||||
price.value = 1999;
|
||||
minBalance.value = 0;
|
||||
currency.value = 'CNY';
|
||||
enabled.value = true;
|
||||
}
|
||||
|
||||
function save(): { rules: BusinessRules; minBalance: number; currency: Currency; enabled: boolean } {
|
||||
return {
|
||||
rules: { period: period.value, price: price.value },
|
||||
minBalance: minBalance.value,
|
||||
currency: currency.value,
|
||||
enabled: enabled.value,
|
||||
};
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.initConfig,
|
||||
(cfg) => {
|
||||
if (cfg && cfg.rules && (cfg.rules as BusinessRules).period) {
|
||||
applyConfig(cfg);
|
||||
} else {
|
||||
resetDefault();
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
defineExpose({ save });
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.editor-body {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.editor-col {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
overflow: hidden;
|
||||
|
||||
.card-head {
|
||||
background: #fafafa;
|
||||
padding: 10px 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.head-hint {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
|
||||
.cfg-row {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 40px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.cfg-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.cfg-label {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.cfg-min-balance {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.cfg-currency {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.ctrl-input {
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.ctrl-select {
|
||||
width: 220px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,616 @@
|
||||
<template>
|
||||
<div class="editor-body">
|
||||
<div class="editor-col">
|
||||
<!-- 通用配置 -->
|
||||
<div class="card">
|
||||
<div class="card-head">
|
||||
<span class="card-title">通用配置</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="cfg-row">
|
||||
<div class="cfg-item">
|
||||
<div class="cfg-label">门禁金额(元)</div>
|
||||
<el-input-number v-model="minBalance" :min="0" :precision="5" :step="0.01" :controls="false" class="cfg-min-balance" />
|
||||
</div>
|
||||
<div class="cfg-item">
|
||||
<div class="cfg-label">币种</div>
|
||||
<el-select v-model="currency" class="cfg-currency">
|
||||
<el-option label="人民币" value="CNY" />
|
||||
<el-option label="美元" value="USD" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="cfg-item">
|
||||
<div class="cfg-label">启用</div>
|
||||
<el-switch v-model="enabled" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor-col">
|
||||
<!-- 基础计费 -->
|
||||
<div class="card">
|
||||
<div class="card-head">
|
||||
<span class="card-title">基础计费</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<el-form label-position="top" size="default">
|
||||
<el-form-item label="计费单位">
|
||||
<el-select v-model="unit" class="ctrl-select" @change="onUnitChange">
|
||||
<el-option v-for="o in UNIT_OPTIONS" :key="o.value" :label="o.label" :value="o.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="阶梯分档">
|
||||
<el-switch v-model="tiered" :disabled="!isTokenUnit" />
|
||||
</el-form-item>
|
||||
<div class="form-hint">阶梯=按输入 token 分档,<b>命中哪档整单按哪档价(不累加)</b>。档位区间相邻不重叠(下档 max+1 = 上档 min)。仅 token 单位(per_1K / per_1M)可用。</div>
|
||||
<div class="money-note">{{ priceNote }}</div>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 计费规则 -->
|
||||
<div class="card">
|
||||
<div class="card-head">
|
||||
<span class="card-title">计费规则</span>
|
||||
<span class="head-select">
|
||||
<el-select :model-value="exampleIndex" size="small" placeholder="选择示例填充" @change="loadExample">
|
||||
<el-option v-for="(e, i) in examples" :key="i" :label="e.label" :value="i" />
|
||||
</el-select>
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div v-for="(r, i) in modelRules" :key="i" class="rule-card">
|
||||
<div class="rule-head">
|
||||
<span class="rule-index">#{{ i + 1 }}</span>
|
||||
<el-input v-model="r.name" placeholder="规则名(如:有声-1080p-输入含视频)" class="rule-name" />
|
||||
<el-button size="small" type="danger" text @click="removeRule(i)">删除</el-button>
|
||||
</div>
|
||||
|
||||
<div class="rc-sec match">命中条件</div>
|
||||
<el-form label-position="top" size="small">
|
||||
<div class="rc-grid">
|
||||
<template v-if="subject.modelType === 'reason'">
|
||||
<el-form-item label="思考模式">
|
||||
<el-select v-model="r.thinking" class="rc-control">
|
||||
<el-option v-for="o in THINKING_OPTIONS" :key="o.value" :label="o.label" :value="o.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="输入长度下界">
|
||||
<el-input-number v-model="r.inLenMin" :min="0" :precision="0" :controls="false" class="rc-control" />
|
||||
</el-form-item>
|
||||
<el-form-item label="输入长度上界">
|
||||
<el-input-number v-model="r.inLenMax" :min="0" :precision="0" :controls="false" class="rc-control" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
<template v-if="subject.modelType === 'video'">
|
||||
<el-form-item label="输出音频">
|
||||
<el-select v-model="r.outputAudio" class="rc-control">
|
||||
<el-option v-for="o in OUTPUT_AUDIO_OPTIONS" :key="o.value" :label="o.label" :value="o.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="输出分辨率">
|
||||
<el-select v-model="r.outputResolution" class="rc-control">
|
||||
<el-option v-for="o in RESOLUTION_OPTIONS.video" :key="o.value" :label="o.label" :value="o.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<template v-if="subject.modelType === 'image'">
|
||||
<el-form-item label="输出分辨率">
|
||||
<el-select v-model="r.outputResolution" class="rc-control">
|
||||
<el-option v-for="o in RESOLUTION_OPTIONS.image" :key="o.value" :label="o.label" :value="o.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</div>
|
||||
</el-form>
|
||||
|
||||
<div class="rc-sec price">计价项</div>
|
||||
<el-form label-position="top" size="small">
|
||||
<div class="rc-grid">
|
||||
<template v-if="isTokenUnit">
|
||||
<el-form-item label="输入单价">
|
||||
<el-input-number v-model="r.input" :min="0" :precision="5" :step="0.0001" :controls="false" class="rc-control" />
|
||||
</el-form-item>
|
||||
<el-form-item label="输出单价">
|
||||
<el-input-number v-model="r.output" :min="0" :precision="5" :step="0.0001" :controls="false" class="rc-control" />
|
||||
</el-form-item>
|
||||
<el-form-item label="缓存命中">
|
||||
<el-input-number v-model="r.cacheHit" :min="0" :precision="5" :step="0.0001" :controls="false" class="rc-control" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-form-item label="每单位单价">
|
||||
<el-input-number v-model="r.unitPrice" :min="0" :precision="5" :step="0.0001" :controls="false" class="rc-control" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
</div>
|
||||
</el-form>
|
||||
|
||||
<!-- 媒体价格模板(仅 token 单位):媒体命中条件已移除,改为对特定媒体类型设独立覆盖价 mediaPrices -->
|
||||
<div v-if="isTokenUnit" class="media-price-section">
|
||||
<div class="rc-sec price">媒体价格模板(可选)</div>
|
||||
<div v-for="(mp, mIdx) in r.mediaPrices" :key="mIdx" class="media-price-row">
|
||||
<el-select v-model="mp.mediaType" class="mp-media" placeholder="媒体类型">
|
||||
<el-option v-for="o in getAvailableMediaOptions(r, mIdx)" :key="o.value" :label="o.label" :value="o.value" />
|
||||
</el-select>
|
||||
<div class="mp-field">
|
||||
<span class="mp-label">输入价</span>
|
||||
<el-input-number v-model="mp.input" :min="0" :precision="5" :step="0.0001" :controls="false" class="mp-num" />
|
||||
</div>
|
||||
<div class="mp-field">
|
||||
<span class="mp-label">输出价</span>
|
||||
<el-input-number v-model="mp.output" :min="0" :precision="5" :step="0.0001" :controls="false" class="mp-num" />
|
||||
</div>
|
||||
<div class="mp-field">
|
||||
<span class="mp-label">缓存命中价</span>
|
||||
<el-input-number v-model="mp.cacheHit" :min="0" :precision="5" :step="0.0001" :controls="false" class="mp-num" />
|
||||
</div>
|
||||
<el-button type="danger" text size="small" @click="removeMediaPrice(r, mIdx)">删除</el-button>
|
||||
</div>
|
||||
<el-button size="small" class="media-add-btn" @click="addMediaPrice(r)">+ 添加媒体价格</el-button>
|
||||
<div class="mp-tip">基础价(input/output/cacheHit)为默认价;媒体模板仅覆盖指定媒体类型的价,未选媒体类型的行保存时丢弃。</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-button size="small" class="add-btn" @click="addRule">+ 添加规则</el-button>
|
||||
<div class="note mt10">
|
||||
结算入参(ChargeUsage 扩展,本期备用):<code>{promptTokens, completionTokens, cachedTokens, charCount, imageCount, mediaType, thinking, outputAudio, outputResolution}</code>。匹配 = 全部条件满足的<b>首条规则</b>命中,无命中报错。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import {
|
||||
EXAMPLES,
|
||||
MEDIA_PRICE_OPTIONS,
|
||||
OUTPUT_AUDIO_OPTIONS,
|
||||
RESOLUTION_OPTIONS,
|
||||
THINKING_OPTIONS,
|
||||
UNIT_OPTIONS,
|
||||
type BuiltModelRule,
|
||||
type BuiltModelRuleMatch,
|
||||
type Currency,
|
||||
type EditorInitConfig,
|
||||
type MediaPriceRow,
|
||||
type ModelRule,
|
||||
type ModelRules,
|
||||
type Subject,
|
||||
type Unit,
|
||||
} from '../mock';
|
||||
|
||||
/**
|
||||
* 模型计价编辑面板(按模型类型自适应)。
|
||||
* 计费单位决定计价项(token 单位 input/output/cacheHit,非 token 单位 unitPrice);
|
||||
* 命中条件 match 字段随模型类型变化(reason/video/audio/image)。
|
||||
* initConfig 传入后端已保存配置(parse 后)时初始化表单;无配置时自动加载本地示例。
|
||||
*/
|
||||
const props = defineProps<{
|
||||
subject: Subject;
|
||||
initConfig: EditorInitConfig | null;
|
||||
}>();
|
||||
|
||||
const examples = computed(() => EXAMPLES[props.subject.modelType!] ?? []);
|
||||
|
||||
const unit = ref<Unit>('per_second');
|
||||
const tiered = ref(false);
|
||||
const currency = ref<'CNY' | 'USD'>('CNY');
|
||||
const enabled = ref(true);
|
||||
const minBalance = ref(0);
|
||||
const modelRules = ref<ModelRule[]>([]);
|
||||
const exampleIndex = ref(0);
|
||||
|
||||
const isTokenUnit = computed(() => unit.value === 'per_1K' || unit.value === 'per_1M');
|
||||
|
||||
const priceNote = computed(() =>
|
||||
isTokenUnit.value
|
||||
? 'token 单位价格按 元/基准(per_1M=每百万 token)计:input=输入单价、output=输出单价、cacheHit=缓存命中输入单价。'
|
||||
: '非 token 单位价格按 元/单位 计:unitPrice=每单位单价(秒/分钟/小时/字/张)。',
|
||||
);
|
||||
|
||||
function emptyRule(): ModelRule {
|
||||
return {
|
||||
name: '',
|
||||
thinking: '',
|
||||
outputAudio: '',
|
||||
outputResolution: '',
|
||||
inLenMin: undefined,
|
||||
inLenMax: undefined,
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheHit: 0,
|
||||
unitPrice: 0,
|
||||
mediaPrices: [],
|
||||
};
|
||||
}
|
||||
|
||||
function loadExample(index: number) {
|
||||
const ex = examples.value[index];
|
||||
if (!ex) return;
|
||||
exampleIndex.value = index;
|
||||
unit.value = ex.unit;
|
||||
tiered.value = !!ex.tiered;
|
||||
if (ex.currency) currency.value = ex.currency;
|
||||
if (ex.minBalance != null) minBalance.value = ex.minBalance;
|
||||
modelRules.value = ex.rules.map((r) => ({
|
||||
name: r.name,
|
||||
thinking: (r.thinking == null ? '' : String(r.thinking)) as ModelRule['thinking'],
|
||||
outputAudio: (r.outputAudio == null ? '' : String(r.outputAudio)) as ModelRule['outputAudio'],
|
||||
outputResolution: r.outputResolution || '',
|
||||
inLenMin: r.inLenMin == null ? undefined : r.inLenMin,
|
||||
inLenMax: r.inLenMax == null ? undefined : r.inLenMax,
|
||||
input: r.input ?? 0,
|
||||
output: r.output ?? 0,
|
||||
cacheHit: r.cacheHit ?? 0,
|
||||
unitPrice: r.unitPrice ?? 0,
|
||||
mediaPrices: mediaPricesToRows(r.mediaPrices),
|
||||
}));
|
||||
onUnitChange();
|
||||
}
|
||||
|
||||
function onUnitChange() {
|
||||
// 非 token 单位强制关闭阶梯
|
||||
if (!isTokenUnit.value) tiered.value = false;
|
||||
}
|
||||
|
||||
function addRule() {
|
||||
modelRules.value.push(emptyRule());
|
||||
}
|
||||
|
||||
function removeRule(index: number) {
|
||||
modelRules.value.splice(index, 1);
|
||||
}
|
||||
|
||||
/** 构建态规则 → 编辑器行态(parse 反向映射) */
|
||||
function fromBuiltRule(r: BuiltModelRule): ModelRule {
|
||||
const m = r.match;
|
||||
return {
|
||||
name: r.name,
|
||||
thinking: (m?.thinking == null ? '' : String(m.thinking)) as ModelRule['thinking'],
|
||||
outputAudio: (m?.outputAudio == null ? '' : String(m.outputAudio)) as ModelRule['outputAudio'],
|
||||
outputResolution: m?.outputResolution || '',
|
||||
inLenMin: m?.inputLengthMin,
|
||||
inLenMax: m?.inputLengthMax,
|
||||
input: r.price.input ?? 0,
|
||||
output: r.price.output ?? 0,
|
||||
cacheHit: r.price.cacheHit ?? 0,
|
||||
unitPrice: r.price.unitPrice ?? 0,
|
||||
mediaPrices: mediaPricesToRows(r.mediaPrices),
|
||||
};
|
||||
}
|
||||
|
||||
/** 用后端已保存配置初始化表单 */
|
||||
function applyInit(cfg: EditorInitConfig) {
|
||||
const r = cfg.rules as ModelRules;
|
||||
unit.value = r.unit;
|
||||
tiered.value = !!r.tiered;
|
||||
currency.value = r.currency || cfg.currency;
|
||||
enabled.value = cfg.enabled;
|
||||
minBalance.value = cfg.minBalance;
|
||||
modelRules.value = (r.rules || []).map(fromBuiltRule);
|
||||
exampleIndex.value = -1;
|
||||
if (!isTokenUnit.value) tiered.value = false;
|
||||
}
|
||||
|
||||
/** 过滤空白规则:规则名或任一命中/价格字段非空才保留 */
|
||||
function keepRule(r: ModelRule): boolean {
|
||||
if (r.name.trim()) return true;
|
||||
if (r.thinking || r.outputAudio || r.outputResolution) return true;
|
||||
if (r.inLenMin != null || r.inLenMax != null) return true;
|
||||
if (r.input > 0 || r.output > 0 || r.cacheHit > 0 || r.unitPrice > 0) return true;
|
||||
// 仅填写了媒体价格模板也算有效规则
|
||||
return !!buildMediaPrices(r.mediaPrices);
|
||||
}
|
||||
|
||||
/** 编辑器行态 → 构建态规则(match 只收录非空字段) */
|
||||
function buildBuiltRule(r: ModelRule): BuiltModelRule {
|
||||
const match: BuiltModelRuleMatch = {};
|
||||
if (r.thinking === 'true' || r.thinking === 'false') match.thinking = r.thinking === 'true';
|
||||
if (r.outputAudio === 'true' || r.outputAudio === 'false') match.outputAudio = r.outputAudio === 'true';
|
||||
if (r.outputResolution) match.outputResolution = r.outputResolution;
|
||||
if (r.inLenMin != null) match.inputLengthMin = r.inLenMin;
|
||||
if (r.inLenMax != null) match.inputLengthMax = r.inLenMax;
|
||||
const price: Record<string, number> = isTokenUnit.value
|
||||
? { input: r.input ?? 0, output: r.output ?? 0, cacheHit: r.cacheHit ?? 0 }
|
||||
: { unitPrice: r.unitPrice ?? 0 };
|
||||
return {
|
||||
name: r.name,
|
||||
match: Object.keys(match).length ? match : undefined,
|
||||
price,
|
||||
mediaPrices: buildMediaPrices(r.mediaPrices),
|
||||
};
|
||||
}
|
||||
|
||||
/** 媒体价格行 → mediaPrices 对象(仅保留填写了价格的媒体;无覆盖返回 undefined 不写入) */
|
||||
function buildMediaPrices(rows: MediaPriceRow[]): Record<string, Record<string, number>> | undefined {
|
||||
if (!rows?.length) return undefined;
|
||||
const map: Record<string, Record<string, number>> = {};
|
||||
rows.forEach((row) => {
|
||||
if (!row.mediaType) return;
|
||||
const prices: Record<string, number> = {};
|
||||
if (row.input > 0) prices.input = row.input;
|
||||
if (row.output > 0) prices.output = row.output;
|
||||
if (row.cacheHit > 0) prices.cacheHit = row.cacheHit;
|
||||
if (Object.keys(prices).length) map[row.mediaType] = prices;
|
||||
});
|
||||
return Object.keys(map).length ? map : undefined;
|
||||
}
|
||||
|
||||
/** mediaPrices 对象 → 编辑态媒体价格行(兼容后端两种结构:按媒体分组的内层对象,或空/缺失) */
|
||||
function mediaPricesToRows(mediaPrices?: unknown): MediaPriceRow[] {
|
||||
if (!mediaPrices || typeof mediaPrices !== 'object') return [];
|
||||
return Object.entries(mediaPrices as Record<string, Record<string, number | undefined>>)
|
||||
.filter(([, prices]) => prices && typeof prices === 'object')
|
||||
.map(([mediaType, prices]) => ({
|
||||
mediaType,
|
||||
input: prices.input ?? 0,
|
||||
output: prices.output ?? 0,
|
||||
cacheHit: prices.cacheHit ?? 0,
|
||||
}));
|
||||
}
|
||||
|
||||
/** 添加一条媒体价格行 */
|
||||
function addMediaPrice(r: ModelRule) {
|
||||
r.mediaPrices.push({ mediaType: '', input: 0, output: 0, cacheHit: 0 });
|
||||
}
|
||||
|
||||
/** 删除一条媒体价格行 */
|
||||
function removeMediaPrice(r: ModelRule, index: number) {
|
||||
r.mediaPrices.splice(index, 1);
|
||||
}
|
||||
|
||||
/** 媒体价格行的可选媒体:排除本规则其他行已选中的媒体,避免同一条规则重复选择同一媒体 */
|
||||
function getAvailableMediaOptions(r: ModelRule, curIndex: number) {
|
||||
const selected = new Set(r.mediaPrices.filter((_, i) => i !== curIndex).map((row) => row.mediaType));
|
||||
return MEDIA_PRICE_OPTIONS.filter((opt) => !selected.has(opt.value) || r.mediaPrices[curIndex]?.mediaType === opt.value);
|
||||
}
|
||||
|
||||
/** 构建模型费率 JSON(对齐后端结构,由父组件 stringify 提交) */
|
||||
function buildRules(): ModelRules {
|
||||
return {
|
||||
unit: unit.value,
|
||||
tiered: tiered.value,
|
||||
currency: currency.value,
|
||||
rules: modelRules.value.filter(keepRule).map(buildBuiltRule),
|
||||
};
|
||||
}
|
||||
|
||||
function save(): { rules: ModelRules; minBalance: number; currency: Currency; enabled: boolean } {
|
||||
return {
|
||||
rules: buildRules(),
|
||||
minBalance: minBalance.value,
|
||||
currency: currency.value,
|
||||
enabled: enabled.value,
|
||||
};
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.initConfig,
|
||||
(cfg) => {
|
||||
if (cfg && (cfg.rules as ModelRules).unit) {
|
||||
// 有后端已保存配置 → 用它初始化
|
||||
applyInit(cfg);
|
||||
} else if (examples.value.length) {
|
||||
// 无配置 → 自动加载该类型第一个示例(对齐原型行为)
|
||||
loadExample(0);
|
||||
} else {
|
||||
modelRules.value = [emptyRule()];
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
defineExpose({ save });
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.editor-body {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.editor-col {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
overflow: hidden;
|
||||
|
||||
.card-head {
|
||||
background: #fafafa;
|
||||
padding: 10px 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.head-select {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.money-note {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.mt10 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
|
||||
.rule-card {
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
padding: 12px;
|
||||
|
||||
.rule-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.rule-index {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.rule-name {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rc-sec {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin: 10px 0 6px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
width: 3px;
|
||||
height: 12px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&.match::before {
|
||||
background: #e6a23c;
|
||||
}
|
||||
|
||||
&.price::before {
|
||||
background: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
.rc-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 0 12px;
|
||||
|
||||
.rc-control {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 媒体价格模板区(token 单位):基础价下方的媒体覆盖价动态行 */
|
||||
.media-price-section {
|
||||
margin-top: 12px;
|
||||
border-top: 1px dashed #e4e7ed;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.media-price-row {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.mp-media {
|
||||
width: 120px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mp-field {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
|
||||
.mp-label {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.mp-num {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.media-add-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mp-tip {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
line-height: 1.7;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.note {
|
||||
border: 1px solid #e6a23c;
|
||||
background: #fdf6ec;
|
||||
color: #b88230;
|
||||
border-radius: 6px;
|
||||
padding: 10px 14px;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.cfg-row {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 40px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.cfg-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.cfg-label {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.cfg-min-balance {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.cfg-currency {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.ctrl-select {
|
||||
width: 220px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,211 @@
|
||||
<template>
|
||||
<div class="subject-list">
|
||||
<div v-if="loading" class="subject-loading">加载计价对象…</div>
|
||||
<template v-else>
|
||||
<!-- 工作流 -->
|
||||
<div class="grp-head">
|
||||
<span class="grp-name">工作流</span>
|
||||
<span class="grp-count">{{ workflowSubjects.length }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-for="s in workflowSubjects"
|
||||
:key="`${s.type}:${s.id}`"
|
||||
class="subj-item"
|
||||
:class="{ active: `${s.type}:${s.id}` === currentKey }"
|
||||
@click="emit('select', s)"
|
||||
>
|
||||
<span class="type-badge t-workflow">工作流</span>
|
||||
<span class="subj-name" :title="s.name">{{ s.name }}</span>
|
||||
<span
|
||||
class="status-dot"
|
||||
:class="s.hasConfig ? 'on' : 'off'"
|
||||
:title="s.hasConfig ? '已配置' : '未配置'"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 模型 -->
|
||||
<div class="grp-head">
|
||||
<span class="grp-name">模型</span>
|
||||
<span class="grp-count">{{ modelSubjects.length }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-for="s in modelSubjects"
|
||||
:key="`${s.type}:${s.id}`"
|
||||
class="subj-item"
|
||||
:class="{ active: `${s.type}:${s.id}` === currentKey }"
|
||||
@click="emit('select', s)"
|
||||
>
|
||||
<span class="type-badge" :class="`t-${s.modelType}`">{{ s.typeLabel }}</span>
|
||||
<span class="subj-name" :title="s.name">{{ s.name }}</span>
|
||||
<span
|
||||
class="status-dot"
|
||||
:class="s.hasConfig ? 'on' : 'off'"
|
||||
:title="s.hasConfig ? '已配置' : '未配置'"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="!modelSubjects.length" class="empty-tip">暂无模型</div>
|
||||
|
||||
<!-- 业务模块 -->
|
||||
<div class="grp-head">
|
||||
<span class="grp-name">业务模块</span>
|
||||
<span class="grp-count">{{ businessSubjects.length }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-for="s in businessSubjects"
|
||||
:key="`${s.type}:${s.id}`"
|
||||
class="subj-item"
|
||||
:class="{ active: `${s.type}:${s.id}` === currentKey }"
|
||||
@click="emit('select', s)"
|
||||
>
|
||||
<span class="type-badge t-business">业务</span>
|
||||
<span class="subj-name" :title="s.name">{{ s.name }}</span>
|
||||
<span
|
||||
class="status-dot"
|
||||
:class="s.hasConfig ? 'on' : 'off'"
|
||||
:title="s.hasConfig ? '已配置' : '未配置'"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import type { Subject } from '../mock';
|
||||
|
||||
/**
|
||||
* 左侧计价对象枚举列表。
|
||||
* 分三组渲染(工作流 / 模型 / 业务模块);
|
||||
* 配置状态以实心(已配置)/ 空心(未配置)圆点呈现。
|
||||
*/
|
||||
const props = defineProps<{
|
||||
subjects: Subject[];
|
||||
currentKey: string;
|
||||
loading?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{ select: [subject: Subject] }>();
|
||||
|
||||
const workflowSubjects = computed(() => props.subjects.filter((s) => s.type === 'workflow'));
|
||||
const modelSubjects = computed(() => props.subjects.filter((s) => s.type === 'model'));
|
||||
const businessSubjects = computed(() => props.subjects.filter((s) => s.type === 'business'));
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.subject-list {
|
||||
.grp-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 14px 16px 6px;
|
||||
|
||||
.grp-name {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #909399;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.grp-count {
|
||||
font-size: 11px;
|
||||
background: #f4f4f5;
|
||||
color: #909399;
|
||||
border-radius: 8px;
|
||||
padding: 0 6px;
|
||||
line-height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.subject-loading {
|
||||
padding: 20px 16px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
padding: 10px 16px;
|
||||
font-size: 12px;
|
||||
color: #c0c4cc;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.subj-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 9px 16px;
|
||||
cursor: pointer;
|
||||
border-left: 3px solid transparent;
|
||||
transition: all 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #ecf5ff;
|
||||
border-left-color: #409eff;
|
||||
}
|
||||
|
||||
.subj-name {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.type-badge {
|
||||
font-size: 11px;
|
||||
border-radius: 3px;
|
||||
padding: 1px 6px;
|
||||
color: #fff;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.t-workflow {
|
||||
background: #8e44ad;
|
||||
}
|
||||
|
||||
&.t-reason {
|
||||
background: #e6a23c;
|
||||
}
|
||||
|
||||
&.t-video {
|
||||
background: #909399;
|
||||
}
|
||||
|
||||
&.t-audio {
|
||||
background: #67c23a;
|
||||
}
|
||||
|
||||
&.t-image {
|
||||
background: #00bcd4;
|
||||
}
|
||||
|
||||
&.t-business {
|
||||
background: #e74c3c;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.on {
|
||||
background: #67c23a;
|
||||
box-shadow: 0 0 0 2px rgba(103, 194, 58, 0.2);
|
||||
}
|
||||
|
||||
&.off {
|
||||
background: #fff;
|
||||
border: 1px solid #c0c4cc;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,343 @@
|
||||
<template>
|
||||
<div class="editor-body">
|
||||
<div class="editor-col">
|
||||
<!-- 通用配置 -->
|
||||
<div class="card">
|
||||
<div class="card-head">
|
||||
<span class="card-title">通用配置</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="cfg-row">
|
||||
<div class="cfg-item">
|
||||
<div class="cfg-label">门禁金额(元)</div>
|
||||
<el-input-number v-model="minBalance" :min="0" :precision="5" :step="0.01" :controls="false" class="cfg-min-balance" />
|
||||
</div>
|
||||
<div class="cfg-item">
|
||||
<div class="cfg-label">币种</div>
|
||||
<el-select v-model="currency" class="cfg-currency">
|
||||
<el-option label="人民币" value="CNY" />
|
||||
<el-option label="美元" value="USD" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="cfg-item">
|
||||
<div class="cfg-label">启用</div>
|
||||
<el-switch v-model="enabled" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor-col">
|
||||
<!-- 计费模式 -->
|
||||
<div class="card">
|
||||
<div class="card-head">
|
||||
<span class="card-title">计费模式</span>
|
||||
<span class="head-hint">可同时开启多套计费</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- 按条计费 per_item -->
|
||||
<div class="mode-item">
|
||||
<div class="mode-item-head">
|
||||
<span class="mode-name">按条计费</span>
|
||||
<el-switch v-model="modes.per_item" />
|
||||
</div>
|
||||
<div v-if="modes.per_item" class="mode-item-body">
|
||||
<el-form label-position="top" size="default">
|
||||
<el-form-item label="档位列表(上不封顶,命中首个档位上限 ≥ 时长的档)">
|
||||
<div class="tier-list">
|
||||
<div v-for="(tier, i) in tiers" :key="i" class="tier-row">
|
||||
<el-input-number
|
||||
v-model="tier.maxSec"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
:controls="false"
|
||||
placeholder="档位上限(秒)"
|
||||
class="tier-input"
|
||||
/>
|
||||
<el-input-number
|
||||
v-model="tier.price"
|
||||
:min="0"
|
||||
:precision="5"
|
||||
:controls="false"
|
||||
placeholder="价格(元)"
|
||||
class="tier-input"
|
||||
/>
|
||||
<el-button size="small" type="danger" text @click="removeTier(i)">删除</el-button>
|
||||
</div>
|
||||
<el-button size="small" class="add-btn" @click="addTier">+ 添加档位</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="超出最大档单价(元/秒)">
|
||||
<el-input-number
|
||||
v-model="overflowUnitPrice"
|
||||
:min="0"
|
||||
:precision="5"
|
||||
:step="0.01"
|
||||
:controls="false"
|
||||
class="ctrl-input"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 按秒计费 per_second -->
|
||||
<div class="mode-item">
|
||||
<div class="mode-item-head">
|
||||
<span class="mode-name">按秒计费</span>
|
||||
<el-switch v-model="modes.per_second" />
|
||||
</div>
|
||||
<div v-if="modes.per_second" class="mode-item-body">
|
||||
<el-form label-position="top" size="default">
|
||||
<el-form-item label="单价(元/秒,不满 1 秒向上取整)">
|
||||
<el-input-number
|
||||
v-model="perSecondUnitPrice"
|
||||
:min="0"
|
||||
:precision="5"
|
||||
:step="0.01"
|
||||
:controls="false"
|
||||
class="ctrl-input"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 按 token 计费 per_token -->
|
||||
<div class="mode-item">
|
||||
<div class="mode-item-head">
|
||||
<span class="mode-name">按 Token 计费</span>
|
||||
<el-switch v-model="modes.per_token" />
|
||||
</div>
|
||||
<div v-if="modes.per_token" class="mode-item-body">
|
||||
<div class="form-hint">
|
||||
价格<b>取自各模型自己的计价配置</b>(模型列表里配的那份),不在此重复配置。未配价的模型按 0
|
||||
计。⚠️ per_token 费率不冻结在快照,结算时按模型实时价。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, watch } from 'vue';
|
||||
import {
|
||||
WORKFLOW_DEFAULT_TIERS,
|
||||
type Currency,
|
||||
type EditorInitConfig,
|
||||
type Subject,
|
||||
type WorkflowRules,
|
||||
type WorkflowTier,
|
||||
} from '../mock';
|
||||
|
||||
/**
|
||||
* 工作流计价编辑面板。
|
||||
* 三套计费模式开关(per_item 档位表 / per_second 单价 / per_token 占位)。
|
||||
* initConfig 传入后端已保存配置(parse 后)时初始化表单;无配置时用默认档位。
|
||||
*/
|
||||
const props = defineProps<{
|
||||
subject: Subject;
|
||||
initConfig: EditorInitConfig | null;
|
||||
}>();
|
||||
|
||||
const modes = reactive({ per_item: true, per_second: false, per_token: false });
|
||||
const tiers = ref<WorkflowTier[]>(WORKFLOW_DEFAULT_TIERS.map((t) => ({ ...t })));
|
||||
const overflowUnitPrice = ref(0.9);
|
||||
const perSecondUnitPrice = ref(0.9);
|
||||
const minBalance = ref(0);
|
||||
const currency = ref<Currency>('CNY');
|
||||
const enabled = ref(true);
|
||||
|
||||
function addTier() {
|
||||
tiers.value.push({ maxSec: 60, price: 89 });
|
||||
}
|
||||
|
||||
function removeTier(index: number) {
|
||||
tiers.value.splice(index, 1);
|
||||
}
|
||||
|
||||
/** 用后端已保存配置初始化表单 */
|
||||
function applyConfig(cfg: EditorInitConfig) {
|
||||
const r = cfg.rules as WorkflowRules;
|
||||
modes.per_item = !!r.per_item;
|
||||
modes.per_second = !!r.per_second;
|
||||
modes.per_token = !!r.per_token;
|
||||
tiers.value = r.per_item?.tiers?.map((t) => ({ ...t })) ?? WORKFLOW_DEFAULT_TIERS.map((t) => ({ ...t }));
|
||||
overflowUnitPrice.value = r.per_item?.overflowUnitPrice ?? 0.9;
|
||||
perSecondUnitPrice.value = r.per_second?.unitPrice ?? 0.9;
|
||||
minBalance.value = cfg.minBalance;
|
||||
currency.value = cfg.currency;
|
||||
enabled.value = cfg.enabled;
|
||||
}
|
||||
|
||||
/** 重置为默认(无配置) */
|
||||
function resetDefault() {
|
||||
modes.per_item = true;
|
||||
modes.per_second = false;
|
||||
modes.per_token = false;
|
||||
tiers.value = WORKFLOW_DEFAULT_TIERS.map((t) => ({ ...t }));
|
||||
overflowUnitPrice.value = 0.9;
|
||||
perSecondUnitPrice.value = 0.9;
|
||||
minBalance.value = 0;
|
||||
currency.value = 'CNY';
|
||||
enabled.value = true;
|
||||
}
|
||||
|
||||
function save(): { rules: WorkflowRules; minBalance: number; currency: Currency; enabled: boolean } {
|
||||
const rules: WorkflowRules = {};
|
||||
// 只把开启的模式写入费率 JSON
|
||||
if (modes.per_item) {
|
||||
rules.per_item = { tiers: tiers.value.filter((t) => t.maxSec > 0), overflowUnitPrice: overflowUnitPrice.value };
|
||||
}
|
||||
if (modes.per_second) rules.per_second = { unitPrice: perSecondUnitPrice.value };
|
||||
if (modes.per_token) rules.per_token = {};
|
||||
return {
|
||||
rules,
|
||||
minBalance: minBalance.value,
|
||||
currency: currency.value,
|
||||
enabled: enabled.value,
|
||||
};
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.initConfig,
|
||||
(cfg) => {
|
||||
if (cfg && cfg.rules && !('unit' in (cfg.rules as Record<string, unknown>))) {
|
||||
applyConfig(cfg);
|
||||
} else {
|
||||
resetDefault();
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
defineExpose({ save });
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.editor-body {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.editor-col {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
overflow: hidden;
|
||||
|
||||
.card-head {
|
||||
background: #fafafa;
|
||||
padding: 10px 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.head-hint {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.mode-item {
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.mode-item-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 12px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 6px 6px 0 0;
|
||||
|
||||
.mode-name {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.mode-item-body {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.tier-list {
|
||||
.tier-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.tier-input {
|
||||
width: 130px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.ctrl-input {
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.cfg-row {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 40px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.cfg-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.cfg-label {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.cfg-min-balance {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.cfg-currency {
|
||||
width: 200px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,449 @@
|
||||
<template>
|
||||
<div class="system-price-container">
|
||||
<el-card shadow="hover" class="price-card">
|
||||
<!-- 顶部操作栏 -->
|
||||
<div class="page-toolbar">
|
||||
<div class="page-toolbar-left">
|
||||
<div class="page-title">计价配置管理</div>
|
||||
<div class="page-subtitle">统一管理工作流 / 模型 / 业务模块的计费规则与门禁</div>
|
||||
</div>
|
||||
<div class="page-toolbar-actions">
|
||||
<el-button :loading="loading" :disabled="!subjects.length" @click="handleRefresh">刷新</el-button>
|
||||
<el-button type="primary" :loading="saving" :disabled="!current" @click="handleSave">保存配置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="price-layout">
|
||||
<!-- 左侧:计价对象枚举 -->
|
||||
<div class="subject-panel">
|
||||
<div class="subject-head">
|
||||
<span class="subject-title">计价对象</span>
|
||||
<span class="subject-count">{{ subjects.length }} 项</span>
|
||||
</div>
|
||||
<SubjectList :subjects="subjects" :current-key="currentKey" :loading="loading" @select="onSelect" />
|
||||
</div>
|
||||
|
||||
<!-- 右侧:配置编辑 -->
|
||||
<div class="editor-panel">
|
||||
<div class="editor-overview">
|
||||
<div class="overview-main">
|
||||
<div class="overview-title">
|
||||
{{ current?.name ?? '计价对象' }}
|
||||
<span v-if="current?.type === 'model'" class="type-tag" :class="`t-${current.modelType}`">
|
||||
{{ current.typeLabel }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="overview-meta">
|
||||
<span v-if="current" class="conf-status" :class="current.hasConfig ? 'ok' : 'no'">
|
||||
{{ current.hasConfig ? '已配置' : '未配置' }}
|
||||
</span>
|
||||
<span v-if="current && lastUpdated" class="update-time">更新于 {{ lastUpdated }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overview-sub">{{ headerSub }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 面板切换:v-if 每次切换重建,initConfig 驱动回显后端已保存配置 -->
|
||||
<template v-if="current">
|
||||
<WorkflowEditor
|
||||
v-if="current.type === 'workflow'"
|
||||
ref="workflowRef"
|
||||
:subject="current"
|
||||
:init-config="initConfig"
|
||||
/>
|
||||
<ModelEditor
|
||||
v-else-if="current.type === 'model'"
|
||||
ref="modelRef"
|
||||
:key="currentKey"
|
||||
:subject="current"
|
||||
:init-config="initConfig"
|
||||
/>
|
||||
<BusinessEditor
|
||||
v-else-if="current.type === 'business'"
|
||||
ref="businessRef"
|
||||
:subject="current"
|
||||
:init-config="initConfig"
|
||||
/>
|
||||
</template>
|
||||
<div v-else class="editor-empty">请选择左侧计价对象</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import SubjectList from './component/subjectList.vue';
|
||||
import WorkflowEditor from './component/workflowEditor.vue';
|
||||
import ModelEditor from './component/modelEditor.vue';
|
||||
import BusinessEditor from './component/businessEditor.vue';
|
||||
import { getPricingConfig, getSubjects, savePricingConfig } from '/@/api/trade/pricing';
|
||||
import {
|
||||
toSubject,
|
||||
type ApiSubjectItem,
|
||||
type BusinessRules,
|
||||
type Currency,
|
||||
type EditorInitConfig,
|
||||
type ModelRules,
|
||||
type Subject,
|
||||
type WorkflowRules,
|
||||
} from './mock';
|
||||
|
||||
/**
|
||||
* 价格管理(计价配置管理)页面。
|
||||
* 左侧计价对象枚举来自后端 subjects 接口,右侧按对象加载 config/get 详情并编辑;
|
||||
* 保存时把编辑器构建的费率对象 stringify 为 rules 字符串提交 config/save。
|
||||
*/
|
||||
|
||||
/** 后端 config/get 返回的配置记录(rules 为费率对象;兼容历史字符串形态) */
|
||||
interface PricingDetailRaw {
|
||||
/** 配置记录 id(雪花 id 超 JS 安全整数,用字符串承载避免精度丢失) */
|
||||
id: string | number;
|
||||
subjectType: string;
|
||||
subjectId: string;
|
||||
/** rules 为费率对象;旧接口可能返回 JSON 字符串,解析时兼容 */
|
||||
rules: string | Record<string, unknown>;
|
||||
minBalance: number;
|
||||
currency: string;
|
||||
enabled: number;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
/** 编辑器 save() 的返回结构(父组件组装提交) */
|
||||
type SaveResult = {
|
||||
rules: WorkflowRules | ModelRules | BusinessRules;
|
||||
minBalance: number;
|
||||
currency: Currency;
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
const subjects = ref<Subject[]>([]);
|
||||
const currentKey = ref('');
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
/** 当前配置记录 id(>0 更新,0 新增;雪花 id 用字符串承载,避免精度丢失) */
|
||||
const currentConfigId = ref<string | number>(0);
|
||||
/** 当前对象已保存配置(parse 后),传给编辑器回显;null 表示无配置 */
|
||||
const initConfig = ref<EditorInitConfig | null>(null);
|
||||
/** 当前配置最近更新时间(展示用) */
|
||||
const lastUpdated = ref('');
|
||||
|
||||
const workflowRef = ref<InstanceType<typeof WorkflowEditor>>();
|
||||
const modelRef = ref<InstanceType<typeof ModelEditor>>();
|
||||
const businessRef = ref<InstanceType<typeof BusinessEditor>>();
|
||||
|
||||
const current = computed<Subject | undefined>(() =>
|
||||
subjects.value.find((s) => `${s.type}:${s.id}` === currentKey.value),
|
||||
);
|
||||
|
||||
const headerSub = computed(() => {
|
||||
const s = current.value;
|
||||
if (!s) return '加载计价对象…';
|
||||
if (s.type === 'workflow') return `工作流对象 · 标识:${s.id}`;
|
||||
if (s.type === 'model') return `模型对象(${s.typeLabel})· 标识:${s.id}`;
|
||||
return `业务模块对象 · 标识:${s.id}`;
|
||||
});
|
||||
|
||||
/** 详情请求序号,防止快速切换时旧响应覆盖当前对象 */
|
||||
let detailSeq = 0;
|
||||
|
||||
function onSelect(subject: Subject) {
|
||||
// 先清空 initConfig,避免 v-if 重建的编辑器拿到上一个对象的配置
|
||||
initConfig.value = null;
|
||||
currentConfigId.value = 0;
|
||||
currentKey.value = `${subject.type}:${subject.id}`;
|
||||
loadDetail(subject);
|
||||
}
|
||||
|
||||
/** 拉取某对象已保存的计价配置,填充编辑器回显并刷新"已配置"状态 */
|
||||
async function loadDetail(subject: Subject) {
|
||||
const seq = ++detailSeq;
|
||||
try {
|
||||
const res = await getPricingConfig({ subjectType: subject.type, subjectId: subject.id });
|
||||
const data = res?.data as PricingDetailRaw | undefined;
|
||||
if (data && data.rules) {
|
||||
let parsed: EditorInitConfig['rules'];
|
||||
try {
|
||||
// 后端 config/get 的 rules 为费率对象;兼容旧接口返回的 JSON 字符串形态
|
||||
parsed =
|
||||
typeof data.rules === 'string'
|
||||
? JSON.parse(data.rules)
|
||||
: (data.rules as unknown as EditorInitConfig['rules']);
|
||||
} catch {
|
||||
parsed = undefined as unknown as EditorInitConfig['rules'];
|
||||
}
|
||||
if (parsed) {
|
||||
if (seq !== detailSeq) return;
|
||||
subject.hasConfig = true;
|
||||
currentConfigId.value = data.id;
|
||||
lastUpdated.value = formatTime(data.updatedAt);
|
||||
initConfig.value = {
|
||||
id: data.id,
|
||||
minBalance: data.minBalance ?? 0,
|
||||
currency: (data.currency as Currency) || 'CNY',
|
||||
enabled: data.enabled === 1,
|
||||
rules: parsed,
|
||||
};
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (seq !== detailSeq) return;
|
||||
subject.hasConfig = false;
|
||||
currentConfigId.value = 0;
|
||||
lastUpdated.value = '';
|
||||
initConfig.value = null;
|
||||
} catch {
|
||||
// 详情拉取失败按无配置处理(错误提示由全局拦截器负责)
|
||||
if (seq !== detailSeq) return;
|
||||
subject.hasConfig = false;
|
||||
currentConfigId.value = 0;
|
||||
lastUpdated.value = '';
|
||||
initConfig.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSubjects() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getSubjects();
|
||||
const list: ApiSubjectItem[] = res?.data?.subjects ?? [];
|
||||
subjects.value = list.map(toSubject);
|
||||
// 默认选中第一个模型(对齐原型默认打开模型),无模型则第一个对象
|
||||
const first = subjects.value.find((s) => s.type === 'model') ?? subjects.value[0];
|
||||
if (first) onSelect(first);
|
||||
} catch {
|
||||
// 枚举拉取失败:全局拦截器已提示,留空列表
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 刷新枚举:保留当前选中(若对象仍存在),否则回退到第一个模型 */
|
||||
async function handleRefresh() {
|
||||
const keepKey = currentKey.value;
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getSubjects();
|
||||
const list: ApiSubjectItem[] = res?.data?.subjects ?? [];
|
||||
subjects.value = list.map(toSubject);
|
||||
const kept = subjects.value.find((s) => `${s.type}:${s.id}` === keepKey);
|
||||
if (kept) {
|
||||
initConfig.value = null;
|
||||
currentConfigId.value = 0;
|
||||
lastUpdated.value = '';
|
||||
await loadDetail(kept);
|
||||
} else {
|
||||
const first = subjects.value.find((s) => s.type === 'model') ?? subjects.value[0];
|
||||
if (first) onSelect(first);
|
||||
}
|
||||
} catch {
|
||||
// 枚举拉取失败:全局拦截器已提示
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
const s = current.value;
|
||||
if (!s) return;
|
||||
// v-if 保证当前对象对应编辑器已挂载,ref 非空
|
||||
let result: SaveResult;
|
||||
if (s.type === 'workflow') result = workflowRef.value!.save();
|
||||
else if (s.type === 'model') result = modelRef.value!.save();
|
||||
else result = businessRef.value!.save();
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
await savePricingConfig({
|
||||
id: currentConfigId.value,
|
||||
subjectType: s.type,
|
||||
subjectId: s.id,
|
||||
rules: result.rules,
|
||||
minBalance: result.minBalance,
|
||||
currency: result.currency,
|
||||
enabled: result.enabled ? 1 : 0,
|
||||
});
|
||||
ElMessage.success('计价配置保存成功');
|
||||
s.hasConfig = true;
|
||||
// 保存后重拉详情,刷新配置 id 与回显(新增场景 id 以后端返回为准)
|
||||
await loadDetail(s);
|
||||
} catch {
|
||||
// 保存失败:错误提示由全局拦截器负责
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 时间格式化:YYYY-MM-DD HH:mm */
|
||||
function formatTime(t?: string): string {
|
||||
if (!t) return '';
|
||||
const d = new Date(t);
|
||||
if (Number.isNaN(d.getTime())) return t;
|
||||
const p = (n: number) => String(n).padStart(2, '0');
|
||||
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
|
||||
}
|
||||
|
||||
onMounted(loadSubjects);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.system-price-container {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.price-card {
|
||||
:deep(.el-card__body) {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 顶部操作栏 */
|
||||
.page-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.page-toolbar-left {
|
||||
.page-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.page-toolbar-actions {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.price-layout {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.subject-panel {
|
||||
width: 320px;
|
||||
flex-shrink: 0;
|
||||
border-right: 1px solid #ebeef5;
|
||||
|
||||
.subject-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 4px 16px 12px;
|
||||
|
||||
.subject-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.subject-count {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.editor-panel {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.editor-overview {
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.overview-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
|
||||
.overview-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.type-tag {
|
||||
font-size: 11px;
|
||||
border-radius: 3px;
|
||||
padding: 1px 6px;
|
||||
color: #fff;
|
||||
font-weight: 400;
|
||||
|
||||
&.t-reason {
|
||||
background: #e6a23c;
|
||||
}
|
||||
|
||||
&.t-video {
|
||||
background: #909399;
|
||||
}
|
||||
|
||||
&.t-audio {
|
||||
background: #67c23a;
|
||||
}
|
||||
|
||||
&.t-image {
|
||||
background: #00bcd4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.overview-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
.conf-status {
|
||||
font-size: 12px;
|
||||
padding: 1px 8px;
|
||||
border-radius: 10px;
|
||||
|
||||
&.ok {
|
||||
background: #f0f9eb;
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
&.no {
|
||||
background: #f4f4f5;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.update-time {
|
||||
font-size: 12px;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.overview-sub {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.editor-empty {
|
||||
padding: 60px 0;
|
||||
text-align: center;
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,366 @@
|
||||
/**
|
||||
* 价格管理(计价配置管理)—— 类型定义、接口映射与本地示例数据
|
||||
*
|
||||
* 说明:
|
||||
* - 计价对象枚举来自后端 GET .../subjects(见 src/api/trade/pricing/index.ts),
|
||||
* 本文件提供后端项 → 页面 Subject 的映射(中文 modelType/modelTypeCode → 英文枚举等)
|
||||
* - 计价配置读取/提交后端的 rules 均为对象(config/get 返回对象,save 也传对象)
|
||||
* - 模型类型编码:reason=100推理 / image=200图片 / audio=300音频 / video=600视频
|
||||
* - EXAMPLES 仅为编辑器本地"示例填充" UX,不参与后端数据
|
||||
*/
|
||||
|
||||
/* ================= 枚举类型 ================= */
|
||||
|
||||
export type SubjectType = 'workflow' | 'model' | 'business';
|
||||
|
||||
export type ModelType = 'reason' | 'video' | 'audio' | 'image';
|
||||
|
||||
export type Unit =
|
||||
| 'per_1M'
|
||||
| 'per_1K'
|
||||
| 'per_1'
|
||||
| 'per_second'
|
||||
| 'per_minute'
|
||||
| 'per_hour'
|
||||
| 'per_char';
|
||||
|
||||
export type Currency = 'CNY' | 'USD';
|
||||
|
||||
/* ================= 后端枚举接口 ================= */
|
||||
|
||||
/** 后端 GET .../subjects 返回的计价对象项 */
|
||||
export interface ApiSubjectItem {
|
||||
subjectType: SubjectType;
|
||||
subjectId: string;
|
||||
name: string;
|
||||
/** 仅 model:中文类型(推理/视频/音频/图片) */
|
||||
modelType?: string;
|
||||
/** 仅 model:模型类型编码(100推理/200图片/300音频/600视频) */
|
||||
modelTypeCode?: number;
|
||||
/** 该对象支持的计费模式(workflow/business 由后端 subjects.chargeModes 下发;model 无此字段) */
|
||||
chargeModes?: string[];
|
||||
}
|
||||
|
||||
/** modelTypeCode → 英文模型类型 */
|
||||
export const MODEL_TYPE_CODE_TO_EN: Record<number, ModelType> = {
|
||||
100: 'reason',
|
||||
200: 'image',
|
||||
300: 'audio',
|
||||
600: 'video',
|
||||
};
|
||||
|
||||
/** 后端枚举项 → 页面 Subject */
|
||||
export function toSubject(item: ApiSubjectItem): Subject {
|
||||
if (item.subjectType === 'model') {
|
||||
const modelType = MODEL_TYPE_CODE_TO_EN[item.modelTypeCode ?? -1] ?? 'reason';
|
||||
return {
|
||||
type: 'model',
|
||||
id: item.subjectId,
|
||||
name: item.name,
|
||||
modelType,
|
||||
modelTypeCode: item.modelTypeCode,
|
||||
typeLabel: item.modelType || modelType,
|
||||
hasConfig: false,
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: item.subjectType,
|
||||
id: item.subjectId,
|
||||
name: item.name,
|
||||
modes: item.chargeModes ?? [],
|
||||
hasConfig: false,
|
||||
};
|
||||
}
|
||||
|
||||
/* ================= 计价对象枚举(页面态) ================= */
|
||||
|
||||
export interface Subject {
|
||||
type: SubjectType;
|
||||
/** subjectId */
|
||||
id: string;
|
||||
/** 显示名称 */
|
||||
name: string;
|
||||
/** 仅 model:模型类型(英文枚举,由 modelTypeCode 映射) */
|
||||
modelType?: ModelType;
|
||||
/** 仅 model:模型类型编码 */
|
||||
modelTypeCode?: number;
|
||||
/** 仅 model:模型中文类型(推理/视频/音频/图片) */
|
||||
typeLabel?: string;
|
||||
/** workflow/business:支持的计费模式(后端 subjects.chargeModes 下发) */
|
||||
modes?: string[];
|
||||
/** 是否已配置计价(选中后由 config/get 判断) */
|
||||
hasConfig: boolean;
|
||||
}
|
||||
|
||||
/* ================= 工作流费率 JSON ================= */
|
||||
|
||||
export interface WorkflowTier {
|
||||
/** 档位最大秒数 */
|
||||
maxSec: number;
|
||||
/** 档位价格(元) */
|
||||
price: number;
|
||||
}
|
||||
|
||||
export interface WorkflowRules {
|
||||
/** 按条计费 */
|
||||
per_item?: {
|
||||
tiers: WorkflowTier[];
|
||||
/** 超出最大档单价(元/秒) */
|
||||
overflowUnitPrice: number;
|
||||
};
|
||||
/** 按秒计费 */
|
||||
per_second?: {
|
||||
unitPrice: number;
|
||||
};
|
||||
/** 按 token 计费(价格取自各模型,仅占位空对象) */
|
||||
per_token?: Record<string, never>;
|
||||
}
|
||||
|
||||
/* ================= 模型费率 JSON ================= */
|
||||
|
||||
/** 媒体价格模板行(token 单位):对特定媒体类型设置独立覆盖价(mediaPrices) */
|
||||
export interface MediaPriceRow {
|
||||
/** 媒体类型:text / audio / video / image */
|
||||
mediaType: string;
|
||||
/** 输入单价 */
|
||||
input: number;
|
||||
/** 输出单价 */
|
||||
output: number;
|
||||
/** 缓存命中输入单价 */
|
||||
cacheHit: number;
|
||||
}
|
||||
|
||||
/** 编辑器内部规则行态(thinking/outputAudio 用字符串承载"未设置";媒体命中条件已移除,媒体改走价格模板) */
|
||||
export interface ModelRule {
|
||||
name: string;
|
||||
thinking: '' | 'true' | 'false';
|
||||
outputAudio: '' | 'true' | 'false';
|
||||
outputResolution: string;
|
||||
inLenMin?: number;
|
||||
inLenMax?: number;
|
||||
/** token 单位:输入单价 */
|
||||
input: number;
|
||||
/** token 单位:输出单价 */
|
||||
output: number;
|
||||
/** token 单位:缓存命中输入单价 */
|
||||
cacheHit: number;
|
||||
/** 非 token 单位:每单位单价 */
|
||||
unitPrice: number;
|
||||
/** 媒体价格模板行(token 单位;空数组=无媒体覆盖价) */
|
||||
mediaPrices: MediaPriceRow[];
|
||||
}
|
||||
|
||||
/** 示例数据源规则(thinking/outputAudio 为布尔,由 loadExample 转换) */
|
||||
export interface ExampleRule {
|
||||
name: string;
|
||||
thinking?: boolean;
|
||||
outputAudio?: boolean;
|
||||
outputResolution?: string;
|
||||
inLenMin?: number;
|
||||
inLenMax?: number;
|
||||
input?: number;
|
||||
output?: number;
|
||||
cacheHit?: number;
|
||||
unitPrice?: number;
|
||||
/** 媒体价格模板示例(可选):媒体类型 → 覆盖价 */
|
||||
mediaPrices?: Record<string, { input?: number; output?: number; cacheHit?: number }>;
|
||||
}
|
||||
|
||||
/** 构建态:规则命中条件(媒体类型已改为价格模板,不再作为命中条件) */
|
||||
export interface BuiltModelRuleMatch {
|
||||
thinking?: boolean;
|
||||
outputAudio?: boolean;
|
||||
outputResolution?: string;
|
||||
inputLengthMin?: number;
|
||||
inputLengthMax?: number;
|
||||
}
|
||||
|
||||
/** 构建态:单条规则 */
|
||||
export interface BuiltModelRule {
|
||||
name: string;
|
||||
match?: BuiltModelRuleMatch;
|
||||
price: Record<string, number>;
|
||||
/** 媒体价格模板(token 单位):媒体类型 → 该媒体独立覆盖价;无覆盖时省略 */
|
||||
mediaPrices?: Record<string, Record<string, number>>;
|
||||
}
|
||||
|
||||
/** 模型费率 JSON 结构(对齐后端,含内嵌币种) */
|
||||
export interface ModelRules {
|
||||
unit: Unit;
|
||||
tiered: boolean;
|
||||
rules: BuiltModelRule[];
|
||||
currency: Currency;
|
||||
}
|
||||
|
||||
/** 模型示例 */
|
||||
export interface ModelExample {
|
||||
label: string;
|
||||
unit: Unit;
|
||||
tiered: boolean;
|
||||
currency?: Currency;
|
||||
minBalance?: number;
|
||||
rules: ExampleRule[];
|
||||
}
|
||||
|
||||
export type ModelExampleMap = Record<ModelType, ModelExample[]>;
|
||||
|
||||
/* ================= 业务模块费率 JSON ================= */
|
||||
|
||||
export interface BusinessRules {
|
||||
period: 'year';
|
||||
price: number;
|
||||
}
|
||||
|
||||
/* ================= 编辑器初始化配置 ================= */
|
||||
|
||||
/** 后端 config/get 解析后的编辑器初始化数据(公共字段 + 已 parse 的费率 JSON) */
|
||||
export interface EditorInitConfig {
|
||||
/** 配置记录 id(>0 更新,0 新增;雪花 id 超安全整数,用字符串承载避免精度丢失) */
|
||||
id: string | number;
|
||||
minBalance: number;
|
||||
currency: Currency;
|
||||
enabled: boolean;
|
||||
/** 已 parse 的费率 JSON(编辑器按 subjectType 断言具体结构) */
|
||||
rules: WorkflowRules | ModelRules | BusinessRules;
|
||||
}
|
||||
|
||||
/* ================= 常量 ================= */
|
||||
|
||||
export const WORKFLOW_DEFAULT_TIERS: WorkflowTier[] = [
|
||||
{ maxSec: 15, price: 29 },
|
||||
{ maxSec: 30, price: 49 },
|
||||
{ maxSec: 60, price: 89 },
|
||||
];
|
||||
|
||||
export const MODEL_TYPE_CODES: Record<ModelType, number> = {
|
||||
reason: 100,
|
||||
image: 200,
|
||||
audio: 300,
|
||||
video: 600,
|
||||
};
|
||||
|
||||
export const UNIT_OPTIONS: { value: Unit; label: string }[] = [
|
||||
{ value: 'per_1M', label: '每百万 Token' },
|
||||
{ value: 'per_1K', label: '每千 Token' },
|
||||
{ value: 'per_1', label: '每个 / 每张图' },
|
||||
{ value: 'per_second', label: '每秒' },
|
||||
{ value: 'per_minute', label: '每分钟' },
|
||||
{ value: 'per_hour', label: '每小时' },
|
||||
{ value: 'per_char', label: '每字' },
|
||||
];
|
||||
|
||||
/** 媒体价格模板可选媒体类型(媒体命中条件已移除,媒体改按模板定价) */
|
||||
export const MEDIA_PRICE_OPTIONS: { value: 'text' | 'audio' | 'video' | 'image'; label: string }[] = [
|
||||
{ value: 'text', label: '文本' },
|
||||
{ value: 'audio', label: '音频' },
|
||||
{ value: 'video', label: '视频' },
|
||||
{ value: 'image', label: '图片' },
|
||||
];
|
||||
|
||||
/** outputAudio 命中条件(video):给用户展示中文(有声/无声) */
|
||||
export const OUTPUT_AUDIO_OPTIONS: { value: '' | 'true' | 'false'; label: string }[] = [
|
||||
{ value: '', label: '不限制' },
|
||||
{ value: 'true', label: '有声' },
|
||||
{ value: 'false', label: '无声' },
|
||||
];
|
||||
|
||||
/** thinking 命中条件(reason):给用户展示中文(是/否) */
|
||||
export const THINKING_OPTIONS: { value: '' | 'true' | 'false'; label: string }[] = [
|
||||
{ value: '', label: '不限制' },
|
||||
{ value: 'true', label: '是' },
|
||||
{ value: 'false', label: '否' },
|
||||
];
|
||||
|
||||
/** 输出分辨率命中条件下拉选项(按模型类型区分:video 横版 / image 方形) */
|
||||
export const RESOLUTION_OPTIONS: Record<'video' | 'image', { value: string; label: string }[]> = {
|
||||
video: [
|
||||
{ value: '', label: '不限制' },
|
||||
{ value: '480p', label: '480p' },
|
||||
{ value: '720p', label: '720p' },
|
||||
{ value: '1080p', label: '1080p' },
|
||||
{ value: '2k', label: '2k' },
|
||||
{ value: '4k', label: '4k' },
|
||||
],
|
||||
image: [
|
||||
{ value: '', label: '不限制' },
|
||||
{ value: '512x512', label: '512x512' },
|
||||
{ value: '1024x1024', label: '1024x1024' },
|
||||
{ value: '2048x2048', label: '2048x2048' },
|
||||
],
|
||||
};
|
||||
|
||||
/** 模型示例(本地填充 UX,逐字对应原型定价配置) */
|
||||
export const EXAMPLES: ModelExampleMap = {
|
||||
video: [
|
||||
{
|
||||
label: '视频·按秒(分辨率×有声/无声)',
|
||||
unit: 'per_second',
|
||||
tiered: false,
|
||||
rules: [
|
||||
{ name: '无声-720p', outputAudio: false, outputResolution: '720p', unitPrice: 0.8 },
|
||||
{ name: '有声-1080p', outputAudio: true, outputResolution: '1080p', unitPrice: 1.5 },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '视频·按token(百万)',
|
||||
unit: 'per_1M',
|
||||
tiered: false,
|
||||
rules: [
|
||||
{ name: '无声-1080p', outputAudio: false, outputResolution: '1080p', input: 0.6, output: 3.6, cacheHit: 0.12 },
|
||||
{ name: '有声-1080p', outputAudio: true, outputResolution: '1080p', input: 9, output: 27, cacheHit: 1.8 },
|
||||
],
|
||||
},
|
||||
],
|
||||
reason: [
|
||||
{
|
||||
label: '推理·阶梯(思考×输入长度档×媒体模板)',
|
||||
unit: 'per_1M',
|
||||
tiered: true,
|
||||
rules: [
|
||||
{ name: '思考-输入长度[0,32k]', thinking: true, inLenMax: 32000, input: 0.6, output: 3.6, cacheHit: 0.12 },
|
||||
{
|
||||
name: '思考-输入长度(32k,128k]',
|
||||
thinking: true,
|
||||
inLenMin: 32001,
|
||||
inLenMax: 128000,
|
||||
input: 0.9,
|
||||
output: 5.4,
|
||||
cacheHit: 0.18,
|
||||
mediaPrices: { audio: { input: 9, output: 3.6, cacheHit: 1.8 } },
|
||||
},
|
||||
{ name: '非思考-统一价', thinking: false, input: 0.3, output: 1.2, cacheHit: 0.06 },
|
||||
],
|
||||
},
|
||||
],
|
||||
audio: [
|
||||
{
|
||||
label: '音频·按字数',
|
||||
unit: 'per_char',
|
||||
tiered: false,
|
||||
rules: [{ name: '语音-统一价(万字符5元)', unitPrice: 0.0005 }],
|
||||
},
|
||||
{
|
||||
label: '音频·按分钟',
|
||||
unit: 'per_minute',
|
||||
tiered: false,
|
||||
rules: [{ name: '语音-统一价', unitPrice: 2 }],
|
||||
},
|
||||
{
|
||||
label: '音频·按token(百万)',
|
||||
unit: 'per_1M',
|
||||
tiered: false,
|
||||
rules: [{ name: '统一价', input: 500, output: 500, cacheHit: 0 }],
|
||||
},
|
||||
],
|
||||
image: [
|
||||
{
|
||||
label: '图片·按张数×分辨率',
|
||||
unit: 'per_1',
|
||||
tiered: false,
|
||||
rules: [
|
||||
{ name: '512x512', outputResolution: '512x512', unitPrice: 0.2 },
|
||||
{ name: '1024x1024', outputResolution: '1024x1024', unitPrice: 0.5 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<div class="system-recharge-user-container">
|
||||
<el-dialog title="用户充值" v-model="isShowDialog" width="520px">
|
||||
<el-form ref="formRef" :model="ruleForm" :rules="rules" size="default" label-width="110px">
|
||||
<el-form-item label="账户名称" prop="userName">
|
||||
<el-input v-model="ruleForm.userName" disabled placeholder="请输入账户名称"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="充值金额" prop="amount">
|
||||
<el-input v-model="ruleForm.amount" placeholder="请输入充值金额(单位:元)" clearable></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="外部订单号" prop="orderNo">
|
||||
<el-input v-model="ruleForm.orderNo" placeholder="请输入外部订单号(选填)" clearable></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" prop="description">
|
||||
<el-input v-model="ruleForm.description" type="textarea" placeholder="请输入描述(选填)" maxlength="150"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="onCancel" size="default">取 消</el-button>
|
||||
<el-button type="primary" @click="onSubmit" size="default">确 定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { reactive, toRefs, defineComponent, ref, unref } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { recharge } from '/@/api/system/user';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'systemRechargeUser',
|
||||
setup() {
|
||||
const formRef = ref<HTMLElement | null>(null);
|
||||
const state = reactive({
|
||||
isShowDialog: false,
|
||||
ruleForm: {
|
||||
userId: 0,
|
||||
userName: '',
|
||||
amount: '',
|
||||
orderNo: '',
|
||||
description: '',
|
||||
},
|
||||
//表单校验
|
||||
rules: {
|
||||
amount: [
|
||||
{ required: true, message: '充值金额不能为空', trigger: 'blur' },
|
||||
{
|
||||
validator: (_rule: any, value: any, callback: any) => {
|
||||
if (!/^\d+(\.\d{1,2})?$/.test(value) || Number(value) <= 0) {
|
||||
callback(new Error('请输入大于 0 的金额(单位:元,最多两位小数)'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
// 打开弹窗
|
||||
const openDialog = (row: any) => {
|
||||
state.ruleForm = {
|
||||
userId: row.id,
|
||||
userName: row.userName,
|
||||
amount: '',
|
||||
orderNo: '',
|
||||
description: '',
|
||||
};
|
||||
state.isShowDialog = true;
|
||||
};
|
||||
// 关闭弹窗
|
||||
const closeDialog = () => {
|
||||
state.isShowDialog = false;
|
||||
};
|
||||
// 取消
|
||||
const onCancel = () => {
|
||||
closeDialog();
|
||||
};
|
||||
// 提交
|
||||
const onSubmit = () => {
|
||||
const formWrap = unref(formRef) as any;
|
||||
if (!formWrap) return;
|
||||
formWrap.validate((valid: boolean) => {
|
||||
if (valid) {
|
||||
recharge({
|
||||
userId: state.ruleForm.userId,
|
||||
userName: state.ruleForm.userName,
|
||||
amount: Number(state.ruleForm.amount),
|
||||
orderNo: state.ruleForm.orderNo,
|
||||
description: state.ruleForm.description,
|
||||
}).then(() => {
|
||||
ElMessage.success('充值成功');
|
||||
closeDialog();
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
return {
|
||||
openDialog,
|
||||
closeDialog,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
formRef,
|
||||
...toRefs(state),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<div class="system-wallet-log-container">
|
||||
<el-dialog title="钱包流水信息" v-model="isShowDialog" width="1000px">
|
||||
<el-table :data="logs" style="width: 100%" :max-height="420">
|
||||
<el-table-column prop="orderNo" label="业务单据号" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="transactionNo" label="账务流水号" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="type" label="类型" width="80" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.type === 'income' ? 'success' : 'danger'">{{ scope.row.type === 'income' ? '充值' : '扣费' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="amount" label="变动金额(元)" width="120" align="right"></el-table-column>
|
||||
<el-table-column prop="balanceBefore" label="变动前余额(元)" width="130" align="right"></el-table-column>
|
||||
<el-table-column prop="balanceAfter" label="变动后余额(元)" width="130" align="right"></el-table-column>
|
||||
<el-table-column prop="currency" label="货币" width="80" align="center"></el-table-column>
|
||||
<el-table-column prop="description" label="描述" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="createdAt" label="创建时间" width="170"></el-table-column>
|
||||
</el-table>
|
||||
<div class="system-wallet-log-pagination">
|
||||
<pagination :total="total" v-model:page="pageNum" v-model:limit="pageSize" @pagination="handlePagination" />
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { reactive, toRefs, defineComponent } from 'vue';
|
||||
import { getWalletLogs } from '/@/api/system/user';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'systemWalletLog',
|
||||
setup() {
|
||||
const state = reactive({
|
||||
isShowDialog: false,
|
||||
userId: 0,
|
||||
logs: [] as any[],
|
||||
total: 0,
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
});
|
||||
// 请求流水数据(后端分页)
|
||||
const getLogs = () => {
|
||||
getWalletLogs(state.userId, state.pageNum, state.pageSize).then((res: any) => {
|
||||
state.logs = res.data.logs ?? [];
|
||||
state.total = res.data.total ?? 0;
|
||||
});
|
||||
};
|
||||
// 打开弹窗并加载流水
|
||||
const openDialog = (userId: number) => {
|
||||
state.userId = userId;
|
||||
state.logs = [];
|
||||
state.total = 0;
|
||||
state.pageNum = 1;
|
||||
state.isShowDialog = true;
|
||||
getLogs();
|
||||
};
|
||||
// 分页事件(pageNum/pageSize 已由 v-model 更新,重新请求当前页)
|
||||
const handlePagination = () => {
|
||||
getLogs();
|
||||
};
|
||||
return {
|
||||
openDialog,
|
||||
handlePagination,
|
||||
...toRefs(state),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -117,7 +117,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="创建时间" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<el-table-column label="操作" width="320">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
size="small"
|
||||
@@ -140,6 +140,25 @@
|
||||
>删除</el-button
|
||||
>
|
||||
<el-button size="small" text type="primary" v-debounce @click="handleResetPwd(scope.row)">重置</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
text
|
||||
type="primary"
|
||||
class="op-btn-recharge"
|
||||
:disabled="!scope.row.isOperation"
|
||||
v-debounce
|
||||
@click="onOpenRecharge(scope.row)"
|
||||
>充值</el-button
|
||||
>
|
||||
<el-button
|
||||
size="small"
|
||||
text
|
||||
type="primary"
|
||||
:disabled="!scope.row.isOperation"
|
||||
v-debounce
|
||||
@click="onOpenWalletLog(scope.row)"
|
||||
>流水信息</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -154,6 +173,8 @@
|
||||
</el-col>
|
||||
</el-row>
|
||||
<EditUser ref="editUserRef" :dept-data="deptData" :gender-data="sys_user_sex" @getUserList="userList" />
|
||||
<RechargeUser ref="rechargeUserRef" />
|
||||
<WalletLog ref="walletLogRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -162,6 +183,8 @@ import { toRefs, reactive, onMounted, ref, defineComponent, watch, getCurrentIns
|
||||
import { ElMessageBox, ElMessage, ElTree, FormInstance } from 'element-plus';
|
||||
import { Search } from '@element-plus/icons-vue';
|
||||
import EditUser from '/@/views/system/user/component/editUser.vue';
|
||||
import RechargeUser from '/@/views/system/user/component/rechargeUser.vue';
|
||||
import WalletLog from '/@/views/system/user/component/walletLog.vue';
|
||||
import { getUserList, getDeptTree, resetUserPwd, changeUserStatus, deleteUser } from '/@/api/system/user/index';
|
||||
|
||||
interface TableDataState {
|
||||
@@ -186,11 +209,13 @@ interface TableDataState {
|
||||
|
||||
export default defineComponent({
|
||||
name: 'systemUser',
|
||||
components: { EditUser },
|
||||
components: { EditUser, RechargeUser, WalletLog },
|
||||
setup() {
|
||||
const { proxy } = <any>getCurrentInstance();
|
||||
const { sys_user_sex } = proxy.useDict('sys_user_sex');
|
||||
const editUserRef = ref();
|
||||
const rechargeUserRef = ref();
|
||||
const walletLogRef = ref();
|
||||
const queryRef = ref();
|
||||
const filterText = ref('');
|
||||
const treeRef = ref<InstanceType<typeof ElTree>>();
|
||||
@@ -259,6 +284,14 @@ export default defineComponent({
|
||||
const onOpenEditUser = (row: any) => {
|
||||
editUserRef.value.openDialog(row);
|
||||
};
|
||||
// 打开充值弹窗
|
||||
const onOpenRecharge = (row: any) => {
|
||||
rechargeUserRef.value.openDialog(row);
|
||||
};
|
||||
// 打开流水信息弹窗
|
||||
const onOpenWalletLog = (row: any) => {
|
||||
walletLogRef.value.openDialog(row.id);
|
||||
};
|
||||
// 删除用户
|
||||
const onRowDel = (row: any) => {
|
||||
let msg = '你确定要删除所选用户?';
|
||||
@@ -358,8 +391,12 @@ export default defineComponent({
|
||||
return {
|
||||
queryRef,
|
||||
editUserRef,
|
||||
rechargeUserRef,
|
||||
walletLogRef,
|
||||
onOpenAddUser,
|
||||
onOpenEditUser,
|
||||
onOpenRecharge,
|
||||
onOpenWalletLog,
|
||||
onRowDel,
|
||||
onHandleSizeChange,
|
||||
onHandleCurrentChange,
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
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
|
||||
-----END RSA PRIVATE KEY-----
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIGJTCCBQ2gAwIBAgIQCXpUlFT9Cue3+TsPpxvnWjANBgkqhkiG9w0BAQsFADBu
|
||||
MIIGJTCCBQ2gAwIBAgIQA0m9UBf/1I217Y9jcPp0vTANBgkqhkiG9w0BAQsFADBu
|
||||
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
|
||||
d3cuZGlnaWNlcnQuY29tMS0wKwYDVQQDEyRFbmNyeXB0aW9uIEV2ZXJ5d2hlcmUg
|
||||
RFYgVExTIENBIC0gRzIwHhcNMjYwNjEwMDAwMDAwWhcNMjYwOTA3MjM1OTU5WjAj
|
||||
RFYgVExTIENBIC0gRzIwHhcNMjYwOTA3MDAwMDAwWhcNMjYxMjA1MjM1OTU5WjAj
|
||||
MSEwHwYDVQQDExhnaXRlYS5yZWRwb3dlcmZ1dHVyZS5jb20wggEiMA0GCSqGSIb3
|
||||
DQEBAQUAA4IBDwAwggEKAoIBAQCqVsfwTHPJbcPzuCUOCNLPoigpaZXWJmHfcMhi
|
||||
YMXNtoap0FmBbJbhNZsGf2G5UOuCVKGN4gSMB18rcFwcq/j/GH5eMaPOn8bK8qSC
|
||||
AsYHYi1zdarDGXICGWbu2BTRuZAY+7/4Kci+ayzuUpbH1vkv15hT5OhTev8Y4/iO
|
||||
4apxRgcvKFZkOPLZ+M7km7j9bRTU5xouGjVJqmf0QVcUiMnlFuKI4XxBWjwTPcRt
|
||||
X8SxCjBoi+k6iotPUYYFF3qzP8vVMyWLDpQ9wZ35NNoANOHg/FKSVixequjS+y1+
|
||||
3SSK4ASVqHcfDddkw2JF8ColW+Fzz+ld0DqQfgW5kp/EAXwLAgMBAAGjggMIMIID
|
||||
BDAfBgNVHSMEGDAWgBR435GQX+7erPbFdevVTFVT7yRKtjAdBgNVHQ4EFgQUOiq0
|
||||
OCRofHA6BrxoM9do+AWnNgowQQYDVR0RBDowOIIYZ2l0ZWEucmVkcG93ZXJmdXR1
|
||||
DQEBAQUAA4IBDwAwggEKAoIBAQCUvl+uz6tWWTAea9IYnFT35vJzbfPF2wutwVBL
|
||||
l4QVTNBUPK7Ptpmpvc1AzJNGgZwuxRhEfJBZkx2x7O7XES6a80v0THUGqHf9SUoM
|
||||
QFUX/PJWtaGF9RQvl53GWJY2vDCYexfUUbL4xpVXIHzNXvXnrIZy8KrtB8A01Pqq
|
||||
2W2KiCo7qswpaBP/2F0Bn4U9BonaEUeyDPbRNaAqRLZWRnW2asDHAgjpoUEvKqSS
|
||||
Y6cMWwjCtbzOQaKctWmq8jqjIceA71UqD5MxgtWyykrCage5IQJFBVvbRvWOk+J+
|
||||
yybGg9jGRjz1objhWMYO5YNTXW1tTvc9oSdltFnT5zBJAJcVAgMBAAGjggMIMIID
|
||||
BDAfBgNVHSMEGDAWgBR435GQX+7erPbFdevVTFVT7yRKtjAdBgNVHQ4EFgQUYPN9
|
||||
3He9hDMC/uFm78xCWWEF4/owQQYDVR0RBDowOIIYZ2l0ZWEucmVkcG93ZXJmdXR1
|
||||
cmUuY29tghx3d3cuZ2l0ZWEucmVkcG93ZXJmdXR1cmUuY29tMD4GA1UdIAQ3MDUw
|
||||
MwYGZ4EMAQIBMCkwJwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQuY29t
|
||||
L0NQUzAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUF
|
||||
@@ -19,19 +19,19 @@ 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
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEqjCCA5KgAwIBAgIQDeD/te5iy2EQn2CMnO1e0zANBgkqhkiG9w0BAQsFADBh
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
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
|
||||
-----END RSA PRIVATE KEY-----
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIGJTCCBQ2gAwIBAgIQAWng8l3t5rBYcCmLpCDdJTANBgkqhkiG9w0BAQsFADBu
|
||||
MIIGJDCCBQygAwIBAgIQCG68erA4eBvEiFO3I/+fkDANBgkqhkiG9w0BAQsFADBu
|
||||
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
|
||||
d3cuZGlnaWNlcnQuY29tMS0wKwYDVQQDEyRFbmNyeXB0aW9uIEV2ZXJ5d2hlcmUg
|
||||
RFYgVExTIENBIC0gRzIwHhcNMjYwNjEyMDAwMDAwWhcNMjYwOTA5MjM1OTU5WjAj
|
||||
RFYgVExTIENBIC0gRzIwHhcNMjYwOTA3MDAwMDAwWhcNMjYxMjA1MjM1OTU5WjAj
|
||||
MSEwHwYDVQQDExhtaW5pby5yZWRwb3dlcmZ1dHVyZS5jb20wggEiMA0GCSqGSIb3
|
||||
DQEBAQUAA4IBDwAwggEKAoIBAQC+gxSvGFHa9gL+3IZmWmWQA2m2YV+pyCWd34RO
|
||||
TWmkT+0zKsg/EEu+M3UEhxsEWQe2G6/9S391KC1LSFsjnZnVswAJsnlJ08tMFnM2
|
||||
leAdZdQd/9I6xAHyzuCwJ6RxWXLdN/V6axfFzJ5dDu7z9ms0QAoZnBPfGQC+217T
|
||||
U2ol5ogx11jWj+euVWGBXoK8O0IAUtFW/DannNz9V6Nz8wsRdXiX2DdUg4N9whsu
|
||||
xzNSaxP3zM4s7DCBWywJBx2cDXZPVfdTixfhTQ+KfQx7FPkvz8rHekdtqOlAr4gP
|
||||
RMOmY/hAp/lCQAcnEzunLi7UQc0GjeeSvFY099lNOeFu+jCPAgMBAAGjggMIMIID
|
||||
BDAfBgNVHSMEGDAWgBR435GQX+7erPbFdevVTFVT7yRKtjAdBgNVHQ4EFgQUm+W1
|
||||
VEi2C0CdxtuncyBFtX9cS5UwQQYDVR0RBDowOIIYbWluaW8ucmVkcG93ZXJmdXR1
|
||||
DQEBAQUAA4IBDwAwggEKAoIBAQCkmgtAQ/KZv8wrvYOiih8xozGCaoTWiW5ye/Hf
|
||||
tnX07Sxt1IaAfFUMHtaoEzsVBLwuTALYINWsCTrolvrgy6i/WvpsTGbZn7mkLjmK
|
||||
T8db1ToBueW/eePdHXg2Q1Pgi/Cy5/cwkp29WXvxRsbiINP4jqsma+4vuqPuXV6W
|
||||
o6HfXL3xddPl/ap0ScjCe+aZKB/hWwR3O4QNSmQFLY6jb7tgH3hqwRJzjOnqkQHA
|
||||
H2Tcdx/gUYYUErPqyyqjhFJW5uDvh1Ha5jcbU2XphOiBgI3wElCkmfAqt4WZGU0z
|
||||
kIluYkwZDxe3nN7iSZl9SbmgwTmlO1g04rC3T5WRQlk08dVdAgMBAAGjggMHMIID
|
||||
AzAfBgNVHSMEGDAWgBR435GQX+7erPbFdevVTFVT7yRKtjAdBgNVHQ4EFgQUbMUu
|
||||
74Ronh+u0yO+sQpoSQ4TbFMwQQYDVR0RBDowOIIYbWluaW8ucmVkcG93ZXJmdXR1
|
||||
cmUuY29tghx3d3cubWluaW8ucmVkcG93ZXJmdXR1cmUuY29tMD4GA1UdIAQ3MDUw
|
||||
MwYGZ4EMAQIBMCkwJwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQuY29t
|
||||
L0NQUzAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUF
|
||||
BwMCMIGABggrBgEFBQcBAQR0MHIwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRp
|
||||
Z2ljZXJ0LmNvbTBKBggrBgEFBQcwAoY+aHR0cDovL2NhY2VydHMuZGlnaWNlcnQu
|
||||
Y29tL0VuY3J5cHRpb25FdmVyeXdoZXJlRFZUTFNDQS1HMi5jcnQwDAYDVR0TAQH/
|
||||
BAIwADCCAX0GCisGAQQB1nkCBAIEggFtBIIBaQFnAHUAwjF+V0UZo0XufzjespBB
|
||||
68fCIVoiv3/Vta12mtkOUs0AAAGeucefAgAABAMARjBEAiBdD793ndAhFw/xk4gS
|
||||
Pycc/U4IGSbSFNg1wXMMBkma9gIgSK8LNW9lqHn4wEYFRQrBfmYMUkJhUR51JmOb
|
||||
rUESBu8AdgDXbX0Q0af1d8LH6V/XAL/5gskzWmXh0LMBcxfAyMVpdwAAAZ65x57r
|
||||
AAAEAwBHMEUCIALrNS+sHrfHVl72YGHkqSb4quYzH6xZ0JNCTLakfjK4AiEA0jbR
|
||||
ORUL7BLzYbw5geZfMb8NJmD+7CbgXdHNPWmowdYAdgCUTkOH+uzB74HzGSQmqBhl
|
||||
AcfTXzgCAT9yZ31VNy4Z2AAAAZ65x58NAAAEAwBHMEUCIQCn753LSIK5ODnAw8za
|
||||
2BUf7PZQhtwhe1I0fEfrFjwcAAIgPZtnV83cNV8UK9zGQhWso4QX/oK+ziXQMuP5
|
||||
+lApPpcwDQYJKoZIhvcNAQELBQADggEBAKceFp1JWPgaR5TGrMXQNY0qgRPhY0ZC
|
||||
MSVLqtbgWPeODl3p+BTpLJ/taGR1v1mlL/2cM+EU/RQVDobYUu91Td0Tibp1T8vv
|
||||
KKgnGTj8fVTzimTR5YsFHKc0XFqHpbneArZFb9tbMnRLheDjAqJ3Xs5H5t/XNDSi
|
||||
vasqxdvAYZPP+7JZPRrFqq/fPZR79NRz7kl45YiQX199d/8oPRVFRrq1TGtp41Nk
|
||||
7rP31tq0YZDpBHXXmpYXmOZalWBHoASnZJDq0+H5bSBzNVHR25eOZe0dt5lWMzrf
|
||||
bzFMatWnPKI/aLhc3GGZAxyy7PCRA5pJGB+qnaJAakODvB02VvJey/Y=
|
||||
BAIwADCCAXwGCisGAQQB1nkCBAIEggFsBIIBaAFmAHUAwjF+V0UZo0XufzjespBB
|
||||
68fCIVoiv3/Vta12mtkOUs0AAAGgebrkkgAABAMARjBEAiA3QXzJe/8VFDnVdr/6
|
||||
BRerjq1qVsc61eFPJfMdmgbeCAIgTTMUJI0RnOIX7O0umzAWJa6T4lljD2+2ybyw
|
||||
FNTGoaQAdgDYCVU7lE96/8gWGW+UT4WrsPj8XodVJg8V0S5yu0VLFAAAAaB5uuSb
|
||||
AAAEAwBHMEUCIDb3e4Fb75I9JRnvfM5q8/iPLgs4u/NPdH4brm//kO9KAiEAujgF
|
||||
VeUmRxadsYgF1pVRytH2vURymXUvcTtZfWLbk/YAdQCUTkOH+uzB74HzGSQmqBhl
|
||||
AcfTXzgCAT9yZ31VNy4Z2AAAAaB5uuSbAAAEAwBGMEQCIGyiorlBMD7UARZvFGRz
|
||||
VsPQH+oqh3cEtHq9t/EdDFH7AiAb7dllHHDE1zJrf41OuXOHQZZrA5VBNPpBu1Lj
|
||||
avU6kzANBgkqhkiG9w0BAQsFAAOCAQEADDNB+zpouXcPD43XUYYE8dOpqFCHXGpe
|
||||
MOn44OGPpfU/DvsQgsMZl//bdIcSlmuyHIr/Z5CojN/sCiyCmqnM+vSRMjcjoqxU
|
||||
y7Ly0df9moxXprB20/XiWhCMSy0+bK051Kk4Lh3O0jWKGe2edEUEzwb1vvyhDylV
|
||||
skd347/BUVGEf+PGT4NqI2ImWoUBc73C8oZ6eHe/bHAiESOI1RnUefOUw3G8El2o
|
||||
t5DUflbkT1+Gzi/NgxvnSwbuvsf77mA9DOg8F6wwsPk4je+ekGycrzoVj/hnzy+h
|
||||
4BT6OiBxzBkRa7TLMblQAPiDTXkbP41ZzLNYnCN8XwpXRvrt29Hf6g==
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEqjCCA5KgAwIBAgIQDeD/te5iy2EQn2CMnO1e0zANBgkqhkiG9w0BAQsFADBh
|
||||
|
||||
Reference in New Issue
Block a user