更新模型配置和订阅页面
- 修改模型模块的字段名称,从 `keyword` 更改为 `modelName`,以提高一致性。 - 添加模型类型和访问类型的选择功能,增强用户交互体验。 - 移除不必要的调试日志,优化代码整洁性。 - 更新订阅页面的错误处理逻辑,确保用户在加载失败时获得清晰反馈。
This commit is contained in:
@@ -468,7 +468,6 @@
|
||||
|
||||
try {
|
||||
const token = getToken();
|
||||
console.log('[subscribe] token:', token);
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -483,7 +482,6 @@
|
||||
|
||||
// 先获取文本,检查是否为有效JSON
|
||||
const text = await response.text();
|
||||
console.log('[subscribe] response text:', text);
|
||||
|
||||
let result;
|
||||
try {
|
||||
@@ -502,7 +500,6 @@
|
||||
renderTypeList(tenantModuleTypes);
|
||||
renderSkuList(assetData.skus || []);
|
||||
} catch (error) {
|
||||
console.error('加载失败:', error);
|
||||
showError(error.message || '加载套餐信息失败,请稍后重试');
|
||||
} finally {
|
||||
showLoading(false);
|
||||
@@ -692,8 +689,6 @@
|
||||
|
||||
// 延迟跳转回原页面
|
||||
const targetUrl = decodeURIComponent(returnUrl);
|
||||
// console.log('[subscribe] 开通成功,即将跳转到:', targetUrl);
|
||||
// console.log('[subscribe] 原始 returnUrl:', returnUrl);
|
||||
|
||||
setTimeout(() => {
|
||||
let finalUrl;
|
||||
|
||||
@@ -3,7 +3,7 @@ import request from '/@/utils/request';
|
||||
export interface ModelModuleListParams {
|
||||
pageNum?: number;
|
||||
pageSize?: number;
|
||||
keyword?: string;
|
||||
modelName?: string;
|
||||
}
|
||||
|
||||
export interface ModelFormItem {
|
||||
@@ -13,6 +13,54 @@ export interface ModelFormItem {
|
||||
type: 'input' | 'number' | 'textarea' | 'switch' | string;
|
||||
}
|
||||
|
||||
export interface ModelFormEntry {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
/** 模型类型(listType 接口项,字段名以后端为准,前端做兼容解析) */
|
||||
export interface ModelTypeListItem {
|
||||
id?: number | string;
|
||||
typeId?: number | string;
|
||||
modelsType?: number | string;
|
||||
name?: string;
|
||||
typeName?: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
/** listType 标准返回:data.type 为 Record<id, 名称>,如 { "1": "推理模型", "2": "图片模型" } */
|
||||
export function normalizeModelTypeOptions(res: { data?: unknown }): Array<{ id: number | string; label: string }> {
|
||||
const data = res?.data as { type?: Record<string, string> } | undefined;
|
||||
const typeMap = data?.type;
|
||||
if (typeMap && typeof typeMap === 'object' && !Array.isArray(typeMap)) {
|
||||
return Object.entries(typeMap)
|
||||
.map(([key, label]) => {
|
||||
const n = Number(key);
|
||||
const id = Number.isNaN(n) ? key : n;
|
||||
return { id, label: String(label ?? '') };
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const na = Number(a.id);
|
||||
const nb = Number(b.id);
|
||||
if (!Number.isNaN(na) && !Number.isNaN(nb)) {
|
||||
return na - nb;
|
||||
}
|
||||
return String(a.id).localeCompare(String(b.id));
|
||||
});
|
||||
}
|
||||
|
||||
/** 兼容旧结构:data 为数组或 data.list */
|
||||
const raw = res?.data;
|
||||
const arr: ModelTypeListItem[] = Array.isArray(raw) ? raw : ((raw as { list?: ModelTypeListItem[] })?.list ?? []);
|
||||
return arr
|
||||
.map((item) => {
|
||||
const id = item.id ?? item.typeId ?? item.modelsType;
|
||||
const label = item.name ?? item.typeName ?? item.label ?? (id != null && id !== '' ? String(id) : '');
|
||||
return { id: id as number | string, label: label || String(id) };
|
||||
})
|
||||
.filter((x) => x.id !== undefined && x.id !== null && x.id !== '');
|
||||
}
|
||||
|
||||
export interface ModelModuleItem {
|
||||
id: number | string;
|
||||
tenantId?: number;
|
||||
@@ -23,18 +71,28 @@ export interface ModelModuleItem {
|
||||
deletedAt?: string | null;
|
||||
isDeleted?: boolean;
|
||||
modelName: string;
|
||||
/** 模型类型 ID,与 listType 返回项对应 */
|
||||
modelsType?: number | string;
|
||||
baseUrl: string;
|
||||
route: string;
|
||||
route?: string;
|
||||
httpMethod: string;
|
||||
apiKey?: string;
|
||||
isPrivate?: number;
|
||||
isChatModel?: number;
|
||||
enabled: number;
|
||||
maxConcurrency: number;
|
||||
queueLimit: number;
|
||||
timeoutMs: number;
|
||||
timeoutMs?: number;
|
||||
timeoutSeconds?: number;
|
||||
expectedSeconds?: number;
|
||||
retryTimes: number;
|
||||
retryQueueMaxSeconds: number;
|
||||
autoCleanSeconds: number;
|
||||
remark?: string;
|
||||
headMsg?: string;
|
||||
form?: ModelFormEntry[] | Record<string, { value: string }>;
|
||||
requestMapping?: Record<string, unknown>;
|
||||
responseMapping?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface ModelModuleListResponse {
|
||||
@@ -48,17 +106,23 @@ export interface ModelModuleListResponse {
|
||||
|
||||
export interface CreateModelParams {
|
||||
modelName: string;
|
||||
modelsType: number;
|
||||
/** 与 listType 返回的类型 id 一致,可能为数字或字符串 */
|
||||
modelsType: number | string;
|
||||
baseUrl: string;
|
||||
route: string;
|
||||
httpMethod: string;
|
||||
httpMethod?: string;
|
||||
headMsg?: string;
|
||||
isPrivate: number;
|
||||
enabled: number;
|
||||
maxConcurrency: number;
|
||||
queueLimit: number;
|
||||
isChatModel: number;
|
||||
apiKey?: string;
|
||||
form: ModelFormEntry[];
|
||||
requestMapping?: Record<string, unknown>;
|
||||
responseMapping?: Record<string, unknown>;
|
||||
maxConcurrency?: number;
|
||||
queueLimit?: number;
|
||||
timeoutSeconds: number;
|
||||
expectedSeconds: number;
|
||||
retryTimes: number;
|
||||
retryTimes?: number;
|
||||
retryQueueMaxSeconds: number;
|
||||
autoCleanSeconds: number;
|
||||
remark?: string;
|
||||
@@ -93,6 +157,16 @@ export function getModelModuleList(params?: ModelModuleListParams) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模型类型列表(用于下拉与列表回显)
|
||||
*/
|
||||
export function getModelTypeList() {
|
||||
return request({
|
||||
url: '/model-gateway/model/listType',
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增模型配置
|
||||
*/
|
||||
@@ -110,7 +184,7 @@ export function addModelModule(data: CreateModelParams) {
|
||||
export function updateModelModule(data: Partial<CreateModelParams> & { id: number | string }) {
|
||||
return request({
|
||||
url: '/model-gateway/model/updateModel',
|
||||
method: 'post',
|
||||
method: 'put',
|
||||
data,
|
||||
});
|
||||
}
|
||||
@@ -121,17 +195,18 @@ export function updateModelModule(data: Partial<CreateModelParams> & { id: numbe
|
||||
export function deleteModelModule(id: number | string) {
|
||||
return request({
|
||||
url: '/model-gateway/model/deleteModel',
|
||||
method: 'post',
|
||||
method: 'delete',
|
||||
data: { id },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模型配置(按类型分组)
|
||||
* 获取单条模型配置详情(编辑用,需传 id)
|
||||
*/
|
||||
export function getModelConfig() {
|
||||
export function getModelModuleDetail(id: number | string) {
|
||||
return request({
|
||||
url: '/model-gateway/model/getModel',
|
||||
method: 'get',
|
||||
params: { id },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
<el-dialog v-model="visible" title="选择模型" width="1000px" :close-on-click-modal="false" @close="handleClose">
|
||||
<div class="model-selector-header">
|
||||
<div class="search-bar">
|
||||
<el-input v-model="searchParams.keyword" placeholder="搜索模型名称" clearable @clear="handleSearch">
|
||||
<template #prefix><el-icon><Search /></el-icon></template>
|
||||
<el-input v-model="searchParams.modelName" placeholder="搜索模型名称" clearable @clear="handleSearch">
|
||||
<template #prefix
|
||||
><el-icon><Search /></el-icon
|
||||
></template>
|
||||
</el-input>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
</div>
|
||||
@@ -94,7 +96,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const visible = ref(false);
|
||||
const searchParams = reactive({ keyword: '' });
|
||||
const searchParams = reactive({ modelName: '' });
|
||||
const pagination = reactive({ pageNum: 1, pageSize: 10, total: 0 });
|
||||
const modelList = ref<ModelItem[]>([]);
|
||||
const loading = ref(false);
|
||||
@@ -133,7 +135,7 @@ const fetchModelList = async () => {
|
||||
const params = {
|
||||
pageNum: pagination.pageNum,
|
||||
pageSize: pagination.pageSize,
|
||||
keyword: searchParams.keyword || undefined,
|
||||
modelName: searchParams.modelName || undefined,
|
||||
};
|
||||
const res = await getModelModuleList(params, { errorMode: 'message' });
|
||||
modelList.value = res.data?.list || [];
|
||||
|
||||
@@ -41,7 +41,6 @@ export function redirectToSubscribePage(assetId: string) {
|
||||
const returnUrl = encodeURIComponent(window.location.href);
|
||||
// 构建跳转URL
|
||||
const url = `${SUBSCRIBE_PAGE_URL}?assetId=${assetId}&returnUrl=${returnUrl}`;
|
||||
console.log('[redirectToSubscribePage] 跳转到开通页面:', url);
|
||||
window.location.href = url;
|
||||
}
|
||||
|
||||
@@ -49,17 +48,13 @@ export function redirectToSubscribePage(assetId: string) {
|
||||
* 处理 402 错误码(模块未开通)
|
||||
*/
|
||||
export function handleModuleNotEnabled(routePath: string): boolean {
|
||||
console.log('[模块未开通] 当前路由路径:', routePath);
|
||||
const assetInfo = getAssetInfoByRoute(routePath);
|
||||
console.log('[模块未开通] 匹配到的资产信息:', assetInfo);
|
||||
|
||||
if (assetInfo) {
|
||||
redirectToSubscribePage(assetInfo.assetId);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 如果没有匹配到路由,尝试使用默认的资产管理
|
||||
console.warn('[模块未开通] 未匹配到路由,使用默认资产管理');
|
||||
redirectToSubscribePage('696b4acd1be1c8b76c4b4c15');
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -209,7 +209,6 @@ const responseInterceptor = (response: AxiosResponse) => {
|
||||
} else {
|
||||
// 未知的 code,统一提示后端异常
|
||||
errorMsg = '后端异常,请联系管理员';
|
||||
console.error(`未知的业务错误码: ${code}, 原始消息: ${message}`);
|
||||
}
|
||||
|
||||
showErrorMessage(errorMsg, config);
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="资产分类" prop="categoryId">
|
||||
<el-cascader
|
||||
v-model="ruleForm.categoryId"
|
||||
@@ -33,7 +33,6 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
</el-row>
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="8">
|
||||
@@ -98,12 +97,7 @@
|
||||
class="w100"
|
||||
clearable
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in attr.options"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
<el-option v-for="opt in attr.options" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||
</el-select>
|
||||
<!-- 多选类型 -->
|
||||
<el-select
|
||||
@@ -114,12 +108,7 @@
|
||||
multiple
|
||||
clearable
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in attr.options"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
<el-option v-for="opt in attr.options" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||
</el-select>
|
||||
<!-- 文本类型 -->
|
||||
<el-input
|
||||
@@ -128,11 +117,7 @@
|
||||
:placeholder="'请输入' + getAttrLabel(attr)"
|
||||
/>
|
||||
<!-- 数字类型 -->
|
||||
<el-input-number
|
||||
v-else-if="attr.type === 'number'"
|
||||
v-model="ruleForm.metadata[getAttrKey(attr)]"
|
||||
class="w100"
|
||||
/>
|
||||
<el-input-number v-else-if="attr.type === 'number'" v-model="ruleForm.metadata[getAttrKey(attr)]" class="w100" />
|
||||
<!-- 日期类型 -->
|
||||
<el-date-picker
|
||||
v-else-if="attr.type === 'date'"
|
||||
@@ -142,10 +127,7 @@
|
||||
class="w100"
|
||||
/>
|
||||
<!-- 布尔类型 -->
|
||||
<el-switch
|
||||
v-else-if="attr.type === 'boolean'"
|
||||
v-model="ruleForm.metadata[getAttrKey(attr)]"
|
||||
/>
|
||||
<el-switch v-else-if="attr.type === 'boolean'" v-model="ruleForm.metadata[getAttrKey(attr)]" />
|
||||
<!-- 图片类型 -->
|
||||
<div v-else-if="attr.type === 'image'" class="w100">
|
||||
<el-upload
|
||||
@@ -185,13 +167,7 @@
|
||||
<el-col :span="8">
|
||||
<el-form-item label="主图" prop="mainImage">
|
||||
<div class="w100">
|
||||
<el-upload
|
||||
class="avatar-uploader"
|
||||
:show-file-list="false"
|
||||
:auto-upload="true"
|
||||
:http-request="handleMainImageUpload"
|
||||
accept="image/*"
|
||||
>
|
||||
<el-upload class="avatar-uploader" :show-file-list="false" :auto-upload="true" :http-request="handleMainImageUpload" accept="image/*">
|
||||
<img v-if="mainImagePreview" :src="mainImagePreview" class="avatar" />
|
||||
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
|
||||
</el-upload>
|
||||
@@ -220,7 +196,6 @@
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
|
||||
<!-- 资产描述 -->
|
||||
<el-divider content-position="left">资产描述</el-divider>
|
||||
<el-form-item label="描述内容" label-width="100px">
|
||||
@@ -229,7 +204,6 @@
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
|
||||
<!-- 实物资产配置 -->
|
||||
<template v-if="ruleForm.type === 'physical'">
|
||||
<el-divider content-position="left">实物资产配置</el-divider>
|
||||
@@ -302,9 +276,17 @@
|
||||
<el-input v-model="item.key" placeholder="Key" style="width: 200px" />
|
||||
<span class="separator">:</span>
|
||||
<el-input v-model="item.value" placeholder="Value" style="width: 200px" />
|
||||
<el-button type="danger" :icon="Delete" circle size="small" @click="removeKeyValuePair(ruleForm.virtualAssetConfig.apiConfig.headers, index)" />
|
||||
<el-button
|
||||
type="danger"
|
||||
:icon="Delete"
|
||||
circle
|
||||
size="small"
|
||||
@click="removeKeyValuePair(ruleForm.virtualAssetConfig.apiConfig.headers, index)"
|
||||
/>
|
||||
</div>
|
||||
<el-button type="primary" :icon="Plus" size="small" @click="addKeyValuePair(ruleForm.virtualAssetConfig.apiConfig.headers)">添加请求头</el-button>
|
||||
<el-button type="primary" :icon="Plus" size="small" @click="addKeyValuePair(ruleForm.virtualAssetConfig.apiConfig.headers)"
|
||||
>添加请求头</el-button
|
||||
>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -317,9 +299,17 @@
|
||||
<el-input v-model="item.key" placeholder="Key" style="width: 200px" />
|
||||
<span class="separator">:</span>
|
||||
<el-input v-model="item.value" placeholder="Value" style="width: 200px" />
|
||||
<el-button type="danger" :icon="Delete" circle size="small" @click="removeKeyValuePair(ruleForm.virtualAssetConfig.apiConfig.params, index)" />
|
||||
<el-button
|
||||
type="danger"
|
||||
:icon="Delete"
|
||||
circle
|
||||
size="small"
|
||||
@click="removeKeyValuePair(ruleForm.virtualAssetConfig.apiConfig.params, index)"
|
||||
/>
|
||||
</div>
|
||||
<el-button type="primary" :icon="Plus" size="small" @click="addKeyValuePair(ruleForm.virtualAssetConfig.apiConfig.params)">添加请求参数</el-button>
|
||||
<el-button type="primary" :icon="Plus" size="small" @click="addKeyValuePair(ruleForm.virtualAssetConfig.apiConfig.params)"
|
||||
>添加请求参数</el-button
|
||||
>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -337,7 +327,13 @@
|
||||
</el-col>
|
||||
<el-col :span="16" v-if="ruleForm.virtualAssetConfig.apiConfig.authType !== 'none'">
|
||||
<el-form-item label="认证配置" prop="virtualAssetConfig.apiConfig.authConfig">
|
||||
<el-input v-model="ruleForm.virtualAssetConfig.apiConfig.authConfig" type="textarea" :rows="2" placeholder="请输入认证配置" class="w100" />
|
||||
<el-input
|
||||
v-model="ruleForm.virtualAssetConfig.apiConfig.authConfig"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="请输入认证配置"
|
||||
class="w100"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@@ -386,9 +382,13 @@
|
||||
<!-- 时间段配置 -->
|
||||
<el-form-item label="服务时间" prop="serviceAssetConfig.serviceAssetArrivalConfig.schedule.timeSlots">
|
||||
<div class="config-list-container">
|
||||
<div v-for="(slot, index) in ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule.timeSlots" :key="index" class="config-list-item">
|
||||
<div
|
||||
v-for="(slot, index) in ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule.timeSlots"
|
||||
:key="index"
|
||||
class="config-list-item"
|
||||
>
|
||||
<el-select v-model="slot.dayOfWeek" placeholder="星期" style="width: 100px">
|
||||
<el-option v-for="d in 7" :key="d" :label="'周' + ['一','二','三','四','五','六','日'][d-1]" :value="String(d)" />
|
||||
<el-option v-for="d in 7" :key="d" :label="'周' + ['一', '二', '三', '四', '五', '六', '日'][d - 1]" :value="String(d)" />
|
||||
</el-select>
|
||||
<el-time-picker v-model="slot.startTime" format="HH:mm" value-format="HH:mm" placeholder="开始" style="width: 100px" />
|
||||
<span class="separator">-</span>
|
||||
@@ -396,29 +396,33 @@
|
||||
<el-input-number v-model="slot.capacity" :min="1" placeholder="容量" style="width: 100px" controls-position="right" />
|
||||
<el-button type="danger" :icon="Delete" circle size="small" @click="removeTimeSlot(index)" />
|
||||
</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
:icon="Plus"
|
||||
size="small"
|
||||
@click="addTimeSlot"
|
||||
:disabled="isTimeSlotLimitReached"
|
||||
>
|
||||
添加时间段
|
||||
</el-button>
|
||||
<el-button type="primary" :icon="Plus" size="small" @click="addTimeSlot" :disabled="isTimeSlotLimitReached"> 添加时间段 </el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 例外日期配置 -->
|
||||
<el-form-item label="休息时间">
|
||||
<div class="config-list-container">
|
||||
<div v-for="(exc, index) in ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule.exceptions" :key="index" class="config-list-item">
|
||||
<div
|
||||
v-for="(exc, index) in ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule.exceptions"
|
||||
:key="index"
|
||||
class="config-list-item"
|
||||
>
|
||||
<el-select v-model="exc.exceptionType" placeholder="类型" style="width: 100px" @change="onExceptionTypeChange(exc)">
|
||||
<el-option label="指定日期" value="date" />
|
||||
<el-option label="指定星期" value="dayOfWeek" />
|
||||
</el-select>
|
||||
<el-date-picker v-if="exc.exceptionType === 'date'" v-model="exc.date" type="date" format="YYYY-MM-DD" value-format="YYYY-MM-DD" placeholder="日期" style="width: 130px" />
|
||||
<el-date-picker
|
||||
v-if="exc.exceptionType === 'date'"
|
||||
v-model="exc.date"
|
||||
type="date"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="日期"
|
||||
style="width: 130px"
|
||||
/>
|
||||
<el-select v-if="exc.exceptionType === 'dayOfWeek'" v-model="exc.dayOfWeek" placeholder="星期" style="width: 100px">
|
||||
<el-option v-for="d in 7" :key="d" :label="'周' + ['一','二','三','四','五','六','日'][d-1]" :value="String(d)" />
|
||||
<el-option v-for="d in 7" :key="d" :label="'周' + ['一', '二', '三', '四', '五', '六', '日'][d - 1]" :value="String(d)" />
|
||||
</el-select>
|
||||
<el-select v-model="exc.status" placeholder="状态" style="width: 90px">
|
||||
<el-option label="可用" :value="1" />
|
||||
@@ -591,8 +595,6 @@ const assetFormDiff = createFormDiff<Record<string, any>>();
|
||||
|
||||
// 获取租户ID
|
||||
const tenantId = ref(Session.get('userInfo')?.tenantId || '');
|
||||
console.log(tenantId.value,'租户id');
|
||||
|
||||
|
||||
const formatImageUrl = (url?: string) => {
|
||||
if (!url) return '';
|
||||
@@ -1252,9 +1254,7 @@ const buildRequestBody = async (): Promise<any> => {
|
||||
// 只有单选和多选类型才传递 options,且只传递选中的值对应的选项
|
||||
if ((attr.type === 'select' || attr.type === 'multi_select') && attr.options && value) {
|
||||
const selectedValues = Array.isArray(value) ? value : [value];
|
||||
metaItem.options = attr.options.filter((opt: { label: string; value: string }) =>
|
||||
selectedValues.includes(opt.value)
|
||||
);
|
||||
metaItem.options = attr.options.filter((opt: { label: string; value: string }) => selectedValues.includes(opt.value));
|
||||
}
|
||||
|
||||
metadataArray.push(metaItem);
|
||||
@@ -1296,7 +1296,7 @@ const onSubmit = async () => {
|
||||
closeDialog();
|
||||
emit('getAssetList');
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error);
|
||||
ElMessage.error('提交失败');
|
||||
} finally {
|
||||
submitLoading.value = false;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<el-dialog v-model="dialogVisible" :title="`SKU管理 - ${assetName}`" width="1200px" :close-on-click-modal="false" @close="onClose">
|
||||
<el-dialog v-model="dialogVisible" :title="`SKU管理 - ${assetName}`" width="1200px" :close-on-click-modal="false" @close="onClose">
|
||||
<!-- 搜索区域 -->
|
||||
<div class="sku-search mb15">
|
||||
<el-button type="primary" @click="onOpenAddSku">
|
||||
@@ -28,10 +28,15 @@
|
||||
{{ item.name }}: {{ item.options?.[0]?.label || (Array.isArray(item.value) ? item.value[0] : item.value) }}
|
||||
</el-tag>
|
||||
</span>
|
||||
<span v-else-if="scope.row.specValues && typeof scope.row.specValues === 'object' && !Array.isArray(scope.row.specValues) && Object.keys(scope.row.specValues).length > 0">
|
||||
<el-tag v-for="(value, key) in scope.row.specValues" :key="key" size="small" style="margin-right: 4px">
|
||||
{{ key }}: {{ value }}
|
||||
</el-tag>
|
||||
<span
|
||||
v-else-if="
|
||||
scope.row.specValues &&
|
||||
typeof scope.row.specValues === 'object' &&
|
||||
!Array.isArray(scope.row.specValues) &&
|
||||
Object.keys(scope.row.specValues).length > 0
|
||||
"
|
||||
>
|
||||
<el-tag v-for="(value, key) in scope.row.specValues" :key="key" size="small" style="margin-right: 4px"> {{ key }}: {{ value }} </el-tag>
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
@@ -47,9 +52,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="price" label="价格" width="90" align="center">
|
||||
<template #default="scope">
|
||||
¥{{ (scope.row.price / 100).toFixed(2) }}
|
||||
</template>
|
||||
<template #default="scope"> ¥{{ (scope.row.price / 100).toFixed(2) }} </template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="stock" label="库存数量" width="100" align="center">
|
||||
<template #default="scope">
|
||||
@@ -122,7 +125,15 @@
|
||||
<div class="spec-values-container">
|
||||
<div v-for="attr in assetSpecAttrs" :key="attr.name" class="spec-item">
|
||||
<span class="spec-label">{{ attr.name }}:</span>
|
||||
<el-select v-if="attr.options && attr.options.length > 0" v-model="specValuesMap[attr.name]" placeholder="请选择" style="width: 120px" filterable allow-create clearable>
|
||||
<el-select
|
||||
v-if="attr.options && attr.options.length > 0"
|
||||
v-model="specValuesMap[attr.name]"
|
||||
placeholder="请选择"
|
||||
style="width: 120px"
|
||||
filterable
|
||||
allow-create
|
||||
clearable
|
||||
>
|
||||
<el-option v-for="opt in attr.options" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||
</el-select>
|
||||
<el-input v-else v-model="specValuesMap[attr.name]" placeholder="请输入" style="width: 120px" />
|
||||
@@ -144,12 +155,7 @@
|
||||
<span style="margin-left: 8px; color: #909399; font-size: 12px">数值越小越靠前</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="图片" prop="imageUrl" required>
|
||||
<el-upload
|
||||
class="sku-image-uploader"
|
||||
:show-file-list="false"
|
||||
:http-request="handleSkuImageUpload"
|
||||
accept="image/*"
|
||||
>
|
||||
<el-upload class="sku-image-uploader" :show-file-list="false" :http-request="handleSkuImageUpload" accept="image/*">
|
||||
<img v-if="skuImagePreview" :src="skuImagePreview" class="sku-image" />
|
||||
<el-icon v-else class="sku-image-uploader-icon"><ele-Plus /></el-icon>
|
||||
</el-upload>
|
||||
@@ -194,13 +200,7 @@
|
||||
style="width: 200px"
|
||||
/>
|
||||
<!-- 文本类型 -->
|
||||
<el-input
|
||||
v-else
|
||||
v-model="stockForm[field.name]"
|
||||
:maxlength="field.maxLength"
|
||||
placeholder="请输入"
|
||||
style="width: 200px"
|
||||
/>
|
||||
<el-input v-else v-model="stockForm[field.name]" :maxlength="field.maxLength" placeholder="请输入" style="width: 200px" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
@@ -217,7 +217,18 @@
|
||||
import { ref, reactive } from 'vue';
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import { listAssetSkus, createAssetSku, updateAssetSku, deleteAssetSku, getAssetSku, getAsset, uploadAssetImage, getSpecsUnitOptions, getStockFormFields, stockOperation } from '/@/api/assets/asset';
|
||||
import {
|
||||
listAssetSkus,
|
||||
createAssetSku,
|
||||
updateAssetSku,
|
||||
deleteAssetSku,
|
||||
getAssetSku,
|
||||
getAsset,
|
||||
uploadAssetImage,
|
||||
getSpecsUnitOptions,
|
||||
getStockFormFields,
|
||||
stockOperation,
|
||||
} from '/@/api/assets/asset';
|
||||
import { createFormDiff } from '/@/utils/diffUtils';
|
||||
import OperationLogDialog from '../../component/operationLogDialog.vue';
|
||||
import type { UploadRequestOptions, UploadUserFile } from 'element-plus';
|
||||
@@ -307,11 +318,11 @@ const skuRules: FormRules = {
|
||||
specsUnit: [{ required: true, message: '请选择规格单位', trigger: 'change' }],
|
||||
specsCount: [
|
||||
{ required: true, message: '请输入规格数量', trigger: 'blur' },
|
||||
{ type: 'number', min: 1, message: '规格数量必须大于0', trigger: 'blur' }
|
||||
{ type: 'number', min: 1, message: '规格数量必须大于0', trigger: 'blur' },
|
||||
],
|
||||
price: [
|
||||
{ required: true, message: '请输入价格', trigger: 'blur' },
|
||||
{ type: 'number', min: 0.01, message: '价格必须大于0', trigger: 'blur' }
|
||||
{ type: 'number', min: 0.01, message: '价格必须大于0', trigger: 'blur' },
|
||||
],
|
||||
stock: [
|
||||
{ required: true, message: '请输入库存数量', trigger: 'blur' },
|
||||
@@ -325,11 +336,10 @@ const skuRules: FormRules = {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
imageUrl: [{ required: true, message: '请上传SKU图片', trigger: 'change' }],
|
||||
|
||||
};
|
||||
|
||||
// 打开弹窗
|
||||
@@ -584,7 +594,6 @@ const onGenerateStock = async (row: any) => {
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取库存表单字段失败:', error);
|
||||
ElMessage.error('获取库存表单字段失败');
|
||||
stockFormVisible.value = false;
|
||||
} finally {
|
||||
@@ -619,7 +628,7 @@ const onSubmitStock = async () => {
|
||||
stockFormVisible.value = false;
|
||||
getSkuList();
|
||||
} catch (error) {
|
||||
console.error('库存操作失败:', error);
|
||||
ElMessage.error('库存操作失败');
|
||||
} finally {
|
||||
stockSubmitLoading.value = false;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
<template>
|
||||
<div class="system-edit-module-container">
|
||||
<el-dialog :title="state.dialog.title" v-model="state.dialog.isShowDialog" width="900px">
|
||||
<el-form ref="editModuleFormRef" :model="state.ruleForm" :rules="state.rules" size="default" label-width="140px">
|
||||
<el-form
|
||||
ref="editModuleFormRef"
|
||||
v-loading="state.dialog.detailLoading"
|
||||
:model="state.ruleForm"
|
||||
:rules="state.rules"
|
||||
size="default"
|
||||
label-width="140px"
|
||||
>
|
||||
<!-- 基础配置 -->
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
|
||||
@@ -12,20 +19,18 @@
|
||||
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
|
||||
<el-form-item label="模型类型" prop="modelsType">
|
||||
<el-select v-model="state.ruleForm.modelsType" placeholder="请选择模型类型" clearable style="width: 100%">
|
||||
<el-option label="图片模型" :value="1"></el-option>
|
||||
<el-option label="语音模型" :value="2"></el-option>
|
||||
<el-option label="推理模型" :value="3"></el-option>
|
||||
<el-option
|
||||
v-for="t in modelTypeOptions"
|
||||
:key="String(t.id)"
|
||||
:label="t.label"
|
||||
:value="typeOptionValue(t.id)"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
|
||||
<el-form-item label="模型地址" prop="baseUrl">
|
||||
<el-input v-model="state.ruleForm.baseUrl" placeholder="请输入模型地址" clearable></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
|
||||
<el-form-item label="路由" prop="route">
|
||||
<el-input v-model="state.ruleForm.route" placeholder="请输入路由" clearable></el-input>
|
||||
<el-form-item label="模型服务地址" prop="baseUrl">
|
||||
<el-input v-model="state.ruleForm.baseUrl" placeholder="请输入模型服务地址" clearable></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
|
||||
@@ -38,6 +43,33 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
|
||||
<el-form-item label="访问类型" prop="isPrivate">
|
||||
<el-radio-group v-model="state.ruleForm.isPrivate" @change="onIsPrivateChange">
|
||||
<el-radio :label="0">私有</el-radio>
|
||||
<el-radio :label="1">公共</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-if="state.ruleForm.isPrivate === 1" :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
|
||||
<el-form-item label="API 密钥" prop="apiKey">
|
||||
<el-input
|
||||
v-model="state.ruleForm.apiKey"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="请输入 API 密钥字符串"
|
||||
clearable
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
|
||||
<el-form-item label="是否对话模型" prop="isChatModel">
|
||||
<el-radio-group v-model="state.ruleForm.isChatModel">
|
||||
<el-radio :label="1">是</el-radio>
|
||||
<el-radio :label="0">否</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
|
||||
<el-form-item label="请求头绑定" prop="headMsg">
|
||||
<el-button @click="showHeaderDialog = true" style="width: 100%"> 配置请求头 ({{ state.headers.length }}) </el-button>
|
||||
@@ -110,13 +142,43 @@
|
||||
<el-input-number v-model="state.ruleForm.autoCleanSeconds" :min="0" :max="86400" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
|
||||
<el-form-item label="请求映射" prop="requestMappingJson">
|
||||
<el-input
|
||||
v-model="state.ruleForm.requestMappingJson"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder='JSON 对象,例如 {}'
|
||||
clearable
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
|
||||
<el-form-item label="响应映射" prop="responseMappingJson">
|
||||
<el-input
|
||||
v-model="state.ruleForm.responseMappingJson"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder='JSON 对象,例如 {}'
|
||||
clearable
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-collapse-transition>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="onCancel" size="default">取 消</el-button>
|
||||
<el-button type="primary" @click="onSubmit" size="default" :loading="state.dialog.loading">{{ state.dialog.submitTxt }}</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="onSubmit"
|
||||
size="default"
|
||||
:loading="state.dialog.loading"
|
||||
:disabled="state.dialog.detailLoading"
|
||||
>
|
||||
{{ state.dialog.submitTxt }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@@ -161,11 +223,32 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="systemEditModule">
|
||||
import { reactive, ref } from 'vue';
|
||||
import { reactive, ref, computed } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import { ArrowUp, ArrowDown } from '@element-plus/icons-vue';
|
||||
import { addModelModule, updateModelModule } from '/@/api/digitalHuman/modelConfig/modelModule/index';
|
||||
import {
|
||||
addModelModule,
|
||||
updateModelModule,
|
||||
getModelModuleDetail,
|
||||
type ModelFormEntry,
|
||||
} from '/@/api/digitalHuman/modelConfig/modelModule/index';
|
||||
|
||||
export type ModelTypeOption = { id: number | string; label: string };
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelTypes?: ModelTypeOption[];
|
||||
}>(),
|
||||
{ modelTypes: () => [] as ModelTypeOption[] }
|
||||
);
|
||||
|
||||
const modelTypeOptions = computed(() => props.modelTypes);
|
||||
|
||||
const typeOptionValue = (id: number | string): number | string => {
|
||||
const n = Number(id);
|
||||
return Number.isNaN(n) ? id : n;
|
||||
};
|
||||
|
||||
const editModuleFormRef = ref();
|
||||
const emit = defineEmits(['refresh']);
|
||||
@@ -175,12 +258,14 @@ const state = reactive({
|
||||
ruleForm: {
|
||||
id: '',
|
||||
modelName: '',
|
||||
modelsType: 1,
|
||||
modelsType: null as number | string | null,
|
||||
baseUrl: '',
|
||||
route: '',
|
||||
httpMethod: 'POST',
|
||||
headMsg: '',
|
||||
isPrivate: 0,
|
||||
apiKey: '',
|
||||
enabled: 1,
|
||||
isChatModel: 0,
|
||||
maxConcurrency: 10,
|
||||
queueLimit: 100,
|
||||
timeoutSeconds: 30,
|
||||
@@ -189,13 +274,67 @@ const state = reactive({
|
||||
retryQueueMaxSeconds: 60,
|
||||
autoCleanSeconds: 300,
|
||||
remark: '',
|
||||
requestMappingJson: '{}',
|
||||
responseMappingJson: '{}',
|
||||
},
|
||||
rules: {
|
||||
modelName: [{ required: true, message: '请输入模型名称', trigger: 'blur' }],
|
||||
modelsType: [{ required: true, message: '请选择模型类型', trigger: 'change' }],
|
||||
baseUrl: [{ required: true, message: '请输入模型地址', trigger: 'blur' }],
|
||||
route: [{ required: true, message: '请输入路由', trigger: 'blur' }],
|
||||
modelsType: [
|
||||
{
|
||||
validator: (_rule: unknown, value: unknown, callback: (e?: Error) => void) => {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
callback(new Error('请选择模型类型'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
trigger: 'change',
|
||||
},
|
||||
],
|
||||
baseUrl: [{ required: true, message: '请输入模型服务地址', trigger: 'blur' }],
|
||||
httpMethod: [{ required: true, message: '请选择请求方式', trigger: 'change' }],
|
||||
requestMappingJson: [
|
||||
{
|
||||
validator: (_rule: unknown, value: string, callback: (e?: Error) => void) => {
|
||||
if (!value || !String(value).trim()) {
|
||||
callback(new Error('请输入请求映射 JSON'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const o = JSON.parse(value);
|
||||
if (o === null || typeof o !== 'object' || Array.isArray(o)) {
|
||||
callback(new Error('请求映射须为 JSON 对象'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
} catch {
|
||||
callback(new Error('请求映射 JSON 格式无效'));
|
||||
}
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
responseMappingJson: [
|
||||
{
|
||||
validator: (_rule: unknown, value: string, callback: (e?: Error) => void) => {
|
||||
if (!value || !String(value).trim()) {
|
||||
callback(new Error('请输入响应映射 JSON'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const o = JSON.parse(value);
|
||||
if (o === null || typeof o !== 'object' || Array.isArray(o)) {
|
||||
callback(new Error('响应映射须为 JSON 对象'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
} catch {
|
||||
callback(new Error('响应映射 JSON 格式无效'));
|
||||
}
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
maxConcurrency: [{ required: true, message: '请输入最大并发数', trigger: 'blur' }],
|
||||
queueLimit: [{ required: true, message: '请输入排队队列上限', trigger: 'blur' }],
|
||||
timeoutSeconds: [{ required: true, message: '请输入请求超时时间', trigger: 'blur' }],
|
||||
@@ -207,58 +346,85 @@ const state = reactive({
|
||||
title: '',
|
||||
submitTxt: '',
|
||||
loading: false,
|
||||
detailLoading: false,
|
||||
},
|
||||
showAdvanced: false, // 是否展开高级配置
|
||||
headers: [] as Array<{ key: string; value: string }>, // 请求头列表
|
||||
showAdvanced: false,
|
||||
headers: [] as Array<{ key: string; value: string }>,
|
||||
formFields: [] as Array<{ key: string; value: string }>,
|
||||
});
|
||||
|
||||
// 解析 headMsg 字符串为数组
|
||||
const parseHeaders = (headMsg: string) => {
|
||||
if (!headMsg) return [];
|
||||
const parseHeaders = (headMsg: string) => parseKeyValueString(headMsg);
|
||||
// 解析 form:支持数组 [{ key, value }] 或历史对象 { k: { value } }
|
||||
const parseFormFields = (form: unknown) => {
|
||||
if (!form) return [];
|
||||
if (Array.isArray(form)) {
|
||||
return (form as ModelFormEntry[])
|
||||
.filter((item) => item && (item.key !== undefined || item.value !== undefined))
|
||||
.map((item) => ({
|
||||
key: String(item.key ?? '').trim(),
|
||||
value: String(item.value ?? '').trim(),
|
||||
}));
|
||||
}
|
||||
if (typeof form === 'object') {
|
||||
const fields: Array<{ key: string; value: string }> = [];
|
||||
Object.keys(form as Record<string, unknown>).forEach((key) => {
|
||||
const v = (form as Record<string, { value?: string }>)[key];
|
||||
if (v && typeof v === 'object' && v.value !== undefined) {
|
||||
fields.push({ key, value: String(v.value) });
|
||||
}
|
||||
});
|
||||
return fields;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const buildFormArray = (): ModelFormEntry[] => {
|
||||
return state.formFields
|
||||
.filter((field) => field.key?.trim() && field.value?.trim())
|
||||
.map((field) => ({ key: field.key.trim(), value: field.value.trim() }));
|
||||
};
|
||||
|
||||
const parseKeyValueString = (raw: string) => {
|
||||
if (!raw) return [];
|
||||
const headers: Array<{ key: string; value: string }> = [];
|
||||
const pairs = headMsg.split(',');
|
||||
const pairs = raw.split(',');
|
||||
pairs.forEach((pair) => {
|
||||
const [key, value] = pair.split(':');
|
||||
if (key && value) {
|
||||
headers.push({ key: key.trim(), value: value.trim() });
|
||||
const idx = pair.indexOf(':');
|
||||
if (idx === -1) return;
|
||||
const key = pair.slice(0, idx).trim();
|
||||
const value = pair.slice(idx + 1).trim();
|
||||
if (key) {
|
||||
headers.push({ key, value });
|
||||
}
|
||||
});
|
||||
return headers;
|
||||
};
|
||||
// 解析 form 对象为数组
|
||||
const parseFormFields = (form: any) => {
|
||||
if (!form || typeof form !== 'object') return [];
|
||||
const fields: Array<{ key: string; value: string }> = [];
|
||||
Object.keys(form).forEach((key) => {
|
||||
if (form[key] && typeof form[key] === 'object' && form[key].value !== undefined) {
|
||||
fields.push({ key, value: form[key].value });
|
||||
}
|
||||
});
|
||||
return fields;
|
||||
};
|
||||
|
||||
// 将数组转换为 form 对象
|
||||
const stringifyFormFields = () => {
|
||||
const formObj: Record<string, { value: string }> = {};
|
||||
state.formFields.forEach((field) => {
|
||||
const key = field.key?.trim();
|
||||
const value = field.value?.trim();
|
||||
if (key && value) {
|
||||
formObj[key] = { value: value };
|
||||
}
|
||||
});
|
||||
return formObj;
|
||||
};
|
||||
|
||||
// 将数组转换为 headMsg 字符串
|
||||
const stringifyHeaders = () => {
|
||||
return state.headers
|
||||
.filter((h) => h.key && h.value)
|
||||
.map((h) => `${h.key}:${h.value}`)
|
||||
.filter((h) => h.key?.trim() && h.value?.trim())
|
||||
.map((h) => `${h.key.trim()}:${h.value.trim()}`)
|
||||
.join(',');
|
||||
};
|
||||
|
||||
const onIsPrivateChange = (val: string | number | boolean | undefined) => {
|
||||
if (val === 0 || val === '0') {
|
||||
state.ruleForm.apiKey = '';
|
||||
}
|
||||
};
|
||||
|
||||
const parseJsonObjectField = (raw: string, fallback: Record<string, unknown>) => {
|
||||
try {
|
||||
const o = JSON.parse(raw || '{}');
|
||||
if (o && typeof o === 'object' && !Array.isArray(o)) {
|
||||
return o as Record<string, unknown>;
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return fallback;
|
||||
};
|
||||
|
||||
// 添加请求头
|
||||
const addHeader = () => {
|
||||
state.headers.push({ key: '', value: '' });
|
||||
@@ -289,43 +455,121 @@ const confirmFormFields = () => {
|
||||
showFormDialog.value = false;
|
||||
};
|
||||
|
||||
// 打开弹窗
|
||||
const openDialog = (type: string, row?: any) => {
|
||||
const ensureKeyValueRows = (rows: Array<{ key: string; value: string }>) => (rows.length ? rows : [{ key: '', value: '' }]);
|
||||
|
||||
/** 从 getModel 返回的 data 中取出单条模型对象 */
|
||||
const unwrapModelDetailPayload = (data: unknown): Record<string, unknown> | null => {
|
||||
if (data == null) return null;
|
||||
if (Array.isArray(data)) return null;
|
||||
if (typeof data !== 'object') return null;
|
||||
const o = data as Record<string, unknown>;
|
||||
if (typeof o.modelName === 'string') return o;
|
||||
if (o.model && typeof o.model === 'object' && !Array.isArray(o.model)) {
|
||||
return o.model as Record<string, unknown>;
|
||||
}
|
||||
if (o.detail && typeof o.detail === 'object' && !Array.isArray(o.detail)) {
|
||||
return o.detail as Record<string, unknown>;
|
||||
}
|
||||
if (o.info && typeof o.info === 'object' && !Array.isArray(o.info)) {
|
||||
return o.info as Record<string, unknown>;
|
||||
}
|
||||
return o;
|
||||
};
|
||||
|
||||
const fillFormFromDetailRow = (row: Record<string, unknown>) => {
|
||||
const timeoutSeconds =
|
||||
row.timeoutSeconds != null && row.timeoutSeconds !== ''
|
||||
? Number(row.timeoutSeconds)
|
||||
: row.timeoutMs != null
|
||||
? Math.floor(Number(row.timeoutMs) / 1000)
|
||||
: 30;
|
||||
const isPrivate = row.isPrivate !== undefined && row.isPrivate !== null ? Number(row.isPrivate) : 0;
|
||||
state.ruleForm = {
|
||||
id: row.id as string,
|
||||
modelName: String(row.modelName ?? ''),
|
||||
modelsType:
|
||||
row.modelsType !== undefined && row.modelsType !== null
|
||||
? typeOptionValue(row.modelsType as number | string)
|
||||
: null,
|
||||
baseUrl: String(row.baseUrl ?? ''),
|
||||
httpMethod: String(row.httpMethod || 'POST'),
|
||||
headMsg: String(row.headMsg || ''),
|
||||
isPrivate,
|
||||
apiKey: isPrivate === 1 ? String(row.apiKey ?? '') : '',
|
||||
enabled: Number(row.enabled ?? 1),
|
||||
isChatModel: row.isChatModel !== undefined && row.isChatModel !== null ? Number(row.isChatModel) : 0,
|
||||
maxConcurrency: Number(row.maxConcurrency ?? 10),
|
||||
queueLimit: Number(row.queueLimit ?? 100),
|
||||
timeoutSeconds,
|
||||
expectedSeconds: Number(row.expectedSeconds ?? 15),
|
||||
retryTimes: Number(row.retryTimes ?? 3),
|
||||
retryQueueMaxSeconds: Number(row.retryQueueMaxSeconds ?? 60),
|
||||
autoCleanSeconds: Number(row.autoCleanSeconds ?? 300),
|
||||
remark: String(row.remark || ''),
|
||||
requestMappingJson: JSON.stringify(
|
||||
row.requestMapping && typeof row.requestMapping === 'object' ? row.requestMapping : {},
|
||||
null,
|
||||
2
|
||||
),
|
||||
responseMappingJson: JSON.stringify(
|
||||
row.responseMapping && typeof row.responseMapping === 'object' ? row.responseMapping : {},
|
||||
null,
|
||||
2
|
||||
),
|
||||
};
|
||||
state.headers = ensureKeyValueRows(parseHeaders(String(row.headMsg || '')));
|
||||
state.formFields = ensureKeyValueRows(parseFormFields(row.form));
|
||||
};
|
||||
|
||||
// 打开弹窗(编辑时会请求 /model/getModel 详情)
|
||||
const openDialog = async (type: string, row?: Record<string, unknown>) => {
|
||||
state.dialog.type = type;
|
||||
state.dialog.isShowDialog = true;
|
||||
state.showAdvanced = false;
|
||||
state.dialog.detailLoading = false;
|
||||
|
||||
if (type === 'edit') {
|
||||
// 编辑时需要转换字段
|
||||
state.ruleForm = {
|
||||
id: row.id,
|
||||
modelName: row.modelName,
|
||||
modelsType: row.modelsType || 1,
|
||||
baseUrl: row.baseUrl,
|
||||
route: row.route,
|
||||
httpMethod: row.httpMethod,
|
||||
headMsg: row.headMsg || '',
|
||||
enabled: row.enabled,
|
||||
maxConcurrency: row.maxConcurrency,
|
||||
queueLimit: row.queueLimit,
|
||||
timeoutSeconds: Math.floor(row.timeoutMs / 1000), // 毫秒转秒
|
||||
expectedSeconds: row.expectedSeconds || 15,
|
||||
retryTimes: row.retryTimes,
|
||||
retryQueueMaxSeconds: row.retryQueueMaxSeconds,
|
||||
autoCleanSeconds: row.autoCleanSeconds,
|
||||
remark: row.remark || '',
|
||||
};
|
||||
// 解析请求头
|
||||
state.headers = parseHeaders(row.headMsg || '');
|
||||
state.formFields = parseFormFields(row.form);
|
||||
const listRowId = row?.id;
|
||||
if (listRowId === undefined || listRowId === null || listRowId === '') {
|
||||
ElMessage.error('缺少模型 ID');
|
||||
state.dialog.isShowDialog = false;
|
||||
return;
|
||||
}
|
||||
state.dialog.title = '修改模型配置';
|
||||
state.dialog.submitTxt = '修 改';
|
||||
state.dialog.detailLoading = true;
|
||||
try {
|
||||
const res: any = await getModelModuleDetail(listRowId as string | number);
|
||||
if (res.code !== 0) {
|
||||
ElMessage.error(res.message || '获取模型详情失败');
|
||||
state.dialog.isShowDialog = false;
|
||||
return;
|
||||
}
|
||||
const detail = unwrapModelDetailPayload(res.data) ?? row ?? null;
|
||||
if (!detail || typeof detail !== 'object') {
|
||||
ElMessage.error('获取模型详情失败');
|
||||
state.dialog.isShowDialog = false;
|
||||
return;
|
||||
}
|
||||
fillFormFromDetailRow(detail as Record<string, unknown>);
|
||||
} catch {
|
||||
ElMessage.error('获取模型详情失败');
|
||||
state.dialog.isShowDialog = false;
|
||||
} finally {
|
||||
state.dialog.detailLoading = false;
|
||||
}
|
||||
} else {
|
||||
state.ruleForm = {
|
||||
id: '',
|
||||
modelName: '',
|
||||
modelsType: 1,
|
||||
modelsType: null,
|
||||
baseUrl: '',
|
||||
route: '',
|
||||
httpMethod: 'POST',
|
||||
headMsg: '',
|
||||
isPrivate: 0,
|
||||
apiKey: '',
|
||||
enabled: 1,
|
||||
isChatModel: 0,
|
||||
maxConcurrency: 10,
|
||||
queueLimit: 100,
|
||||
timeoutSeconds: 30,
|
||||
@@ -334,21 +578,20 @@ const openDialog = (type: string, row?: any) => {
|
||||
retryQueueMaxSeconds: 60,
|
||||
autoCleanSeconds: 300,
|
||||
remark: '',
|
||||
requestMappingJson: '{}',
|
||||
responseMappingJson: '{}',
|
||||
};
|
||||
// 初始化一个空的请求头
|
||||
state.headers = [{ key: '', value: '' }];
|
||||
state.formFields = [{ key: '', value: '' }];
|
||||
state.dialog.title = '新增模型配置';
|
||||
state.dialog.submitTxt = '新 增';
|
||||
}
|
||||
state.dialog.type = type;
|
||||
state.dialog.isShowDialog = true;
|
||||
state.showAdvanced = false; // 每次打开弹窗时重置为收起状态
|
||||
};
|
||||
|
||||
// 关闭弹窗
|
||||
const closeDialog = () => {
|
||||
state.dialog.isShowDialog = false;
|
||||
state.dialog.detailLoading = false;
|
||||
editModuleFormRef.value?.resetFields();
|
||||
};
|
||||
|
||||
@@ -364,19 +607,34 @@ const onSubmit = () => {
|
||||
|
||||
state.dialog.loading = true;
|
||||
try {
|
||||
// 将 headMsg 转换为 form 的 JSON 格式
|
||||
// headMsg: "X-API-Key:xxx,operation:true"
|
||||
// 转换为 form: { "X-API-Key": { "value": "xxx" }, "operation": { "value": "true" } }
|
||||
const formObj = stringifyFormFields();
|
||||
|
||||
// 提交数据
|
||||
state.ruleForm.headMsg = stringifyHeaders();
|
||||
const requestMapping = parseJsonObjectField(state.ruleForm.requestMappingJson, {});
|
||||
const responseMapping = parseJsonObjectField(state.ruleForm.responseMappingJson, {});
|
||||
const submitData = {
|
||||
...state.ruleForm,
|
||||
form: formObj,
|
||||
modelName: state.ruleForm.modelName,
|
||||
modelsType: state.ruleForm.modelsType as number | string,
|
||||
baseUrl: state.ruleForm.baseUrl,
|
||||
httpMethod: state.ruleForm.httpMethod || 'POST',
|
||||
headMsg: state.ruleForm.headMsg,
|
||||
isPrivate: state.ruleForm.isPrivate,
|
||||
enabled: state.ruleForm.enabled,
|
||||
isChatModel: state.ruleForm.isChatModel,
|
||||
apiKey: state.ruleForm.isPrivate === 1 ? String(state.ruleForm.apiKey ?? '').trim() : '',
|
||||
form: buildFormArray(),
|
||||
requestMapping,
|
||||
responseMapping,
|
||||
maxConcurrency: state.ruleForm.maxConcurrency,
|
||||
queueLimit: state.ruleForm.queueLimit,
|
||||
timeoutSeconds: state.ruleForm.timeoutSeconds,
|
||||
expectedSeconds: state.ruleForm.expectedSeconds,
|
||||
retryTimes: state.ruleForm.retryTimes,
|
||||
retryQueueMaxSeconds: state.ruleForm.retryQueueMaxSeconds,
|
||||
autoCleanSeconds: state.ruleForm.autoCleanSeconds,
|
||||
remark: state.ruleForm.remark || '',
|
||||
};
|
||||
|
||||
if (state.dialog.type === 'edit') {
|
||||
await updateModelModule(submitData);
|
||||
await updateModelModule({ ...submitData, id: state.ruleForm.id });
|
||||
ElMessage.success('修改成功');
|
||||
} else {
|
||||
await addModelModule(submitData);
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
<div class="system-user-container layout-padding">
|
||||
<el-card shadow="hover" class="layout-padding-auto">
|
||||
<div class="system-user-search mb15">
|
||||
<el-input v-model="state.tableData.param.keyword" size="default" placeholder="请输入模型名称" style="max-width: 180px" clearable> </el-input>
|
||||
<el-input v-model="state.tableData.param.modelName" size="default" placeholder="请输入模型名称" style="max-width: 180px" clearable>
|
||||
</el-input>
|
||||
<el-button size="default" type="primary" class="ml10" @click="getTableData">
|
||||
<el-icon>
|
||||
<ele-Search />
|
||||
@@ -19,8 +20,17 @@
|
||||
<el-table :data="state.tableData.data" v-loading="state.tableData.loading" style="width: 100%">
|
||||
<el-table-column type="index" label="序号" width="60" />
|
||||
<el-table-column prop="modelName" label="模型名称" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="baseUrl" label="模型地址" show-overflow-tooltip width="200"></el-table-column>
|
||||
<el-table-column prop="route" label="路由" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="模型类型" width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ resolveModelTypeLabel(row.modelsType) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- <el-table-column prop="baseUrl" label="模型服务地址" show-overflow-tooltip width="200"></el-table-column> -->
|
||||
<el-table-column prop="isPrivate" label="访问类型" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.isPrivate === 1 ? 'primary' : 'info'">{{ scope.row.isPrivate === 1 ? '公共' : '私有' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="httpMethod" label="请求方式" width="100"></el-table-column>
|
||||
<el-table-column prop="enabled" label="状态" width="100">
|
||||
<template #default="scope">
|
||||
@@ -31,6 +41,7 @@
|
||||
<el-table-column prop="queueLimit" label="队列上限" width="100"></el-table-column>
|
||||
<el-table-column prop="remark" label="备注" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="createdAt" label="创建时间" show-overflow-tooltip width="160"></el-table-column>
|
||||
<el-table-column prop="updatedAt" label="修改时间" show-overflow-tooltip width="160"></el-table-column>
|
||||
<el-table-column label="操作" width="150" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button size="small" text type="primary" @click="onOpenEditModule('edit', scope.row)">修改</el-button>
|
||||
@@ -52,19 +63,25 @@
|
||||
>
|
||||
</el-pagination>
|
||||
</el-card>
|
||||
<EditModule ref="editModuleRef" @refresh="getTableData()" />
|
||||
<EditModule ref="editModuleRef" :model-types="state.modelTypes" @refresh="getTableData()" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="digitalHumanModelModule">
|
||||
import { defineAsyncComponent, reactive, onMounted, ref } from 'vue';
|
||||
import { ElMessageBox, ElMessage } from 'element-plus';
|
||||
import { getModelModuleList, deleteModelModule } from '/@/api/digitalHuman/modelConfig/modelModule/index';
|
||||
import {
|
||||
getModelModuleList,
|
||||
getModelTypeList,
|
||||
deleteModelModule,
|
||||
normalizeModelTypeOptions,
|
||||
} from '/@/api/digitalHuman/modelConfig/modelModule/index';
|
||||
|
||||
const EditModule = defineAsyncComponent(() => import('/@/views/digitalHuman/modelConfig/modelModule/component/editModule.vue'));
|
||||
|
||||
const editModuleRef = ref();
|
||||
const state = reactive({
|
||||
modelTypes: [] as Array<{ id: number | string; label: string }>,
|
||||
tableData: {
|
||||
data: [],
|
||||
total: 0,
|
||||
@@ -72,11 +89,30 @@ const state = reactive({
|
||||
param: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
keyword: '',
|
||||
modelName: '',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const resolveModelTypeLabel = (modelsType: number | string | undefined | null) => {
|
||||
if (modelsType === undefined || modelsType === null || modelsType === '') {
|
||||
return '—';
|
||||
}
|
||||
const hit = state.modelTypes.find((t) => String(t.id) === String(modelsType));
|
||||
return hit?.label ?? String(modelsType);
|
||||
};
|
||||
|
||||
const loadModelTypes = async () => {
|
||||
try {
|
||||
const res: any = await getModelTypeList();
|
||||
if (res.code === 0) {
|
||||
state.modelTypes = normalizeModelTypeOptions(res);
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error('获取模型类型失败:');
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化表格数据
|
||||
const getTableData = async () => {
|
||||
state.tableData.loading = true;
|
||||
@@ -87,7 +123,6 @@ const getTableData = async () => {
|
||||
state.tableData.total = res.data.total || 0;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取模型列表失败:', error);
|
||||
ElMessage.error('获取模型列表失败');
|
||||
} finally {
|
||||
state.tableData.loading = false;
|
||||
@@ -117,7 +152,6 @@ const onRowDel = (row: any) => {
|
||||
ElMessage.success('删除成功');
|
||||
getTableData();
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error);
|
||||
ElMessage.error('删除失败');
|
||||
}
|
||||
})
|
||||
@@ -137,7 +171,8 @@ const onHandleCurrentChange = (val: number) => {
|
||||
};
|
||||
|
||||
// 页面加载时
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
await loadModelTypes();
|
||||
getTableData();
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user