feat: 支持尊享包渠道

This commit is contained in:
ccnetcore
2025-12-31 00:02:25 +08:00
parent 411a9058ca
commit 70ae2fab44
28 changed files with 1666 additions and 34 deletions

View File

@@ -0,0 +1,100 @@
import { del, get, post, put } from '@/utils/request';
import type {
AiAppDto,
AiAppCreateInput,
AiAppUpdateInput,
AiAppGetListInput,
AiModelDto,
AiModelCreateInput,
AiModelUpdateInput,
AiModelGetListInput,
PagedResultDto,
} from './types';
// ==================== AI应用管理 ====================
// 获取AI应用列表
export function getAppList(params?: AiAppGetListInput) {
const queryParams = new URLSearchParams();
if (params?.searchKey) {
queryParams.append('SearchKey', params.searchKey);
}
if (params?.skipCount !== undefined) {
queryParams.append('SkipCount', params.skipCount.toString());
}
if (params?.maxResultCount !== undefined) {
queryParams.append('MaxResultCount', params.maxResultCount.toString());
}
const queryString = queryParams.toString();
const url = queryString ? `/channel/app?${queryString}` : '/channel/app';
return get<PagedResultDto<AiAppDto>>(url).json();
}
// 根据ID获取AI应用
export function getAppById(id: string) {
return get<AiAppDto>(`/channel/app/${id}`).json();
}
// 创建AI应用
export function createApp(data: AiAppCreateInput) {
return post<AiAppDto>('/channel/app', data).json();
}
// 更新AI应用
export function updateApp(data: AiAppUpdateInput) {
return put<AiAppDto>('/channel/app', data).json();
}
// 删除AI应用
export function deleteApp(id: string) {
return del(`/channel/app/${id}`).json();
}
// ==================== AI模型管理 ====================
// 获取AI模型列表
export function getModelList(params?: AiModelGetListInput) {
const queryParams = new URLSearchParams();
if (params?.searchKey) {
queryParams.append('SearchKey', params.searchKey);
}
if (params?.aiAppId) {
queryParams.append('AiAppId', params.aiAppId);
}
if (params?.isPremiumOnly !== undefined) {
queryParams.append('IsPremiumOnly', params.isPremiumOnly.toString());
}
if (params?.skipCount !== undefined) {
queryParams.append('SkipCount', params.skipCount.toString());
}
if (params?.maxResultCount !== undefined) {
queryParams.append('MaxResultCount', params.maxResultCount.toString());
}
const queryString = queryParams.toString();
const url = queryString ? `/channel/model?${queryString}` : '/channel/model';
return get<PagedResultDto<AiModelDto>>(url).json();
}
// 根据ID获取AI模型
export function getModelById(id: string) {
return get<AiModelDto>(`/channel/model/${id}`).json();
}
// 创建AI模型
export function createModel(data: AiModelCreateInput) {
return post<AiModelDto>('/channel/model', data).json();
}
// 更新AI模型
export function updateModel(data: AiModelUpdateInput) {
return put<AiModelDto>('/channel/model', data).json();
}
// 删除AI模型
export function deleteModel(id: string) {
return del(`/channel/model/${id}`).json();
}

View File

@@ -0,0 +1,121 @@
// 模型类型枚举
export enum ModelTypeEnum {
Chat = 0,
Image = 1,
Embedding = 2,
PremiumChat = 3,
}
// 模型API类型枚举
export enum ModelApiTypeEnum {
OpenAi = 0,
Claude = 1,
}
// AI应用DTO
export interface AiAppDto {
id: string;
name: string;
endpoint: string;
extraUrl?: string;
apiKey: string;
orderNum: number;
creationTime: string;
}
// 创建AI应用输入
export interface AiAppCreateInput {
name: string;
endpoint: string;
extraUrl?: string;
apiKey: string;
orderNum: number;
}
// 更新AI应用输入
export interface AiAppUpdateInput {
id: string;
name: string;
endpoint: string;
extraUrl?: string;
apiKey: string;
orderNum: number;
}
// 获取AI应用列表输入
export interface AiAppGetListInput {
searchKey?: string;
skipCount?: number;
maxResultCount?: number;
}
// AI模型DTO
export interface AiModelDto {
id: string;
handlerName: string;
modelId: string;
name: string;
description?: string;
orderNum: number;
aiAppId: string;
extraInfo?: string;
modelType: ModelTypeEnum;
modelApiType: ModelApiTypeEnum;
multiplier: number;
multiplierShow: number;
providerName?: string;
iconUrl?: string;
isPremium: boolean;
}
// 创建AI模型输入
export interface AiModelCreateInput {
handlerName: string;
modelId: string;
name: string;
description?: string;
orderNum: number;
aiAppId: string;
extraInfo?: string;
modelType: ModelTypeEnum;
modelApiType: ModelApiTypeEnum;
multiplier: number;
multiplierShow: number;
providerName?: string;
iconUrl?: string;
isPremium: boolean;
}
// 更新AI模型输入
export interface AiModelUpdateInput {
id: string;
handlerName: string;
modelId: string;
name: string;
description?: string;
orderNum: number;
aiAppId: string;
extraInfo?: string;
modelType: ModelTypeEnum;
modelApiType: ModelApiTypeEnum;
multiplier: number;
multiplierShow: number;
providerName?: string;
iconUrl?: string;
isPremium: boolean;
}
// 获取AI模型列表输入
export interface AiModelGetListInput {
searchKey?: string;
aiAppId?: string;
isPremiumOnly?: boolean;
skipCount?: number;
maxResultCount?: number;
}
// 分页结果
export interface PagedResultDto<T> {
items: T[];
totalCount: number;
}

View File

@@ -0,0 +1,548 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { Delete, Edit, Plus, Refresh, View } from '@element-plus/icons-vue';
import type { AiAppDto, AiModelDto } from '@/api/channel/types';
import {
getAppList,
createApp,
updateApp,
deleteApp,
getModelList,
createModel,
updateModel,
deleteModel,
} from '@/api/channel';
// ==================== 应用管理 ====================
const appList = ref<AiAppDto[]>([]);
const appLoading = ref(false);
const selectedAppId = ref<string>('');
// 应用对话框
const appDialogVisible = ref(false);
const appDialogTitle = ref('');
const appForm = ref<Partial<AiAppDto>>({});
const appDetailDialogVisible = ref(false);
const appDetailData = ref<AiAppDto | null>(null);
// 获取应用列表
async function fetchAppList() {
appLoading.value = true;
try {
const res = await getAppList({
skipCount: 0,
maxResultCount: 100,
});
appList.value = res.data.items;
// 默认选中第一个应用
if (appList.value.length > 0 && !selectedAppId.value) {
selectedAppId.value = appList.value[0].id;
fetchModelList();
}
}
catch (error: any) {
ElMessage.error(error.message || '获取应用列表失败');
}
finally {
appLoading.value = false;
}
}
// 选择应用
function handleSelectApp(appId: string) {
selectedAppId.value = appId;
fetchModelList();
}
// 查看应用详情
function handleViewAppDetail(app: AiAppDto) {
appDetailData.value = app;
appDetailDialogVisible.value = true;
}
// 打开应用对话框
function openAppDialog(type: 'create' | 'edit', row?: AiAppDto) {
appDialogTitle.value = type === 'create' ? '创建应用' : '编辑应用';
if (type === 'create') {
appForm.value = {
name: '',
endpoint: '',
extraUrl: '',
apiKey: '',
orderNum: 0,
};
}
else {
appForm.value = { ...row };
}
appDialogVisible.value = true;
}
// 保存应用
async function saveApp() {
try {
if (appForm.value.id) {
await updateApp(appForm.value as any);
ElMessage.success('更新成功');
}
else {
await createApp(appForm.value as any);
ElMessage.success('创建成功');
}
appDialogVisible.value = false;
fetchAppList();
}
catch (error: any) {
ElMessage.error(error.message || '保存失败');
}
}
// 删除应用
async function handleDeleteApp(row: AiAppDto) {
try {
await ElMessageBox.confirm('确定要删除该应用吗?删除后该应用下的所有模型将无法使用。', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await deleteApp(row.id);
ElMessage.success('删除成功');
// 如果删除的是当前选中的应用,清空选中状态
if (selectedAppId.value === row.id) {
selectedAppId.value = '';
modelList.value = [];
}
fetchAppList();
}
catch (error: any) {
if (error !== 'cancel') {
ElMessage.error(error.message || '删除失败');
}
}
}
// ==================== 模型管理 ====================
const modelList = ref<AiModelDto[]>([]);
const modelLoading = ref(false);
const modelSearchKey = ref('');
const modelDialogVisible = ref(false);
const modelDialogTitle = ref('');
const modelForm = ref<Partial<AiModelDto>>({});
// 获取模型列表
async function fetchModelList() {
if (!selectedAppId.value) {
modelList.value = [];
return;
}
modelLoading.value = true;
try {
const res = await getModelList({
aiAppId: selectedAppId.value,
searchKey: modelSearchKey.value,
skipCount: 0,
maxResultCount: 100,
});
modelList.value = res.data.items;
}
catch (error: any) {
ElMessage.error(error.message || '获取模型列表失败');
}
finally {
modelLoading.value = false;
}
}
// 打开模型对话框
function openModelDialog(type: 'create' | 'edit', row?: AiModelDto) {
if (!selectedAppId.value) {
ElMessage.warning('请先选择一个应用');
return;
}
modelDialogTitle.value = type === 'create' ? '创建模型' : '编辑模型';
if (type === 'create') {
modelForm.value = {
handlerName: '',
modelId: '',
name: '',
description: '',
orderNum: 0,
aiAppId: selectedAppId.value,
extraInfo: '',
modelType: 0,
modelApiType: 0,
multiplier: 1,
multiplierShow: 1,
providerName: '',
iconUrl: '',
isPremium: false,
};
}
else {
modelForm.value = { ...row };
}
modelDialogVisible.value = true;
}
// 保存模型
async function saveModel() {
try {
if (modelForm.value.id) {
await updateModel(modelForm.value as any);
ElMessage.success('更新成功');
}
else {
await createModel(modelForm.value as any);
ElMessage.success('创建成功');
}
modelDialogVisible.value = false;
fetchModelList();
}
catch (error: any) {
ElMessage.error(error.message || '保存失败');
}
}
// 删除模型
async function handleDeleteModel(row: AiModelDto) {
try {
await ElMessageBox.confirm('确定要删除该模型吗?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await deleteModel(row.id);
ElMessage.success('删除成功');
fetchModelList();
}
catch (error: any) {
if (error !== 'cancel') {
ElMessage.error(error.message || '删除失败');
}
}
}
// 初始化
onMounted(() => {
fetchAppList();
});
</script>
<template>
<div class="channel-management">
<div class="channel-container">
<!-- 左侧应用列表 -->
<div class="app-list-panel">
<div class="panel-header">
<h3>应用列表</h3>
<el-button type="primary" size="small" :icon="Plus" @click="openAppDialog('create')">
新建
</el-button>
</div>
<el-scrollbar class="app-list-scrollbar">
<div v-loading="appLoading" class="app-list">
<div
v-for="app in appList"
:key="app.id"
class="app-item"
:class="{ active: selectedAppId === app.id }"
@click="handleSelectApp(app.id)"
>
<div class="app-item-content">
<div class="app-name">{{ app.name }}</div>
<div class="app-actions">
<el-button
link
type="primary"
size="small"
:icon="View"
@click.stop="handleViewAppDetail(app)"
>
详情
</el-button>
<el-button
link
type="primary"
size="small"
:icon="Edit"
@click.stop="openAppDialog('edit', app)"
>
编辑
</el-button>
<el-button
link
type="danger"
size="small"
:icon="Delete"
@click.stop="handleDeleteApp(app)"
>
删除
</el-button>
</div>
</div>
</div>
<el-empty v-if="!appLoading && appList.length === 0" description="暂无应用" />
</div>
</el-scrollbar>
</div>
<!-- 右侧模型列表 -->
<div class="model-list-panel">
<div class="panel-header">
<h3>模型列表</h3>
<div class="header-actions">
<el-input
v-model="modelSearchKey"
placeholder="搜索模型"
style="width: 200px; margin-right: 10px"
clearable
@keyup.enter="fetchModelList"
/>
<el-button type="primary" size="small" :icon="Plus" @click="openModelDialog('create')">
新建
</el-button>
<el-button size="small" :icon="Refresh" @click="fetchModelList">
刷新
</el-button>
</div>
</div>
<div v-if="!selectedAppId" class="empty-tip">
<el-empty description="请先选择左侧的应用" />
</div>
<el-table
v-else
v-loading="modelLoading"
:data="modelList"
border
stripe
height="calc(100vh - 220px)"
>
<el-table-column prop="name" label="模型名称" min-width="150" />
<el-table-column prop="modelId" label="模型ID" min-width="200" show-overflow-tooltip />
<el-table-column prop="handlerName" label="处理名" min-width="120" />
<el-table-column prop="providerName" label="供应商" width="100" />
<el-table-column label="是否尊享" width="100">
<template #default="{ row }">
<el-tag :type="row.isPremium ? 'warning' : 'info'">
{{ row.isPremium ? '尊享' : '普通' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="multiplierShow" label="显示倍率" width="100" />
<el-table-column prop="orderNum" label="排序" width="80" />
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button link type="primary" :icon="Edit" @click="openModelDialog('edit', row)">
编辑
</el-button>
<el-button link type="danger" :icon="Delete" @click="handleDeleteModel(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
<!-- 应用详情对话框 -->
<el-dialog v-model="appDetailDialogVisible" title="应用详情" width="600px">
<el-descriptions v-if="appDetailData" :column="1" border>
<el-descriptions-item label="应用名称">{{ appDetailData.name }}</el-descriptions-item>
<el-descriptions-item label="终结点">{{ appDetailData.endpoint }}</el-descriptions-item>
<el-descriptions-item label="额外URL">{{ appDetailData.extraUrl || '-' }}</el-descriptions-item>
<el-descriptions-item label="API Key">
<el-input :model-value="appDetailData.apiKey" type="textarea" readonly />
</el-descriptions-item>
<el-descriptions-item label="排序">{{ appDetailData.orderNum }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ appDetailData.creationTime }}</el-descriptions-item>
</el-descriptions>
</el-dialog>
<!-- 应用编辑对话框 -->
<el-dialog v-model="appDialogVisible" :title="appDialogTitle" width="600px">
<el-form :model="appForm" label-width="120px">
<el-form-item label="应用名称" required>
<el-input v-model="appForm.name" placeholder="请输入应用名称" />
</el-form-item>
<el-form-item label="终结点" required>
<el-input v-model="appForm.endpoint" placeholder="请输入应用终结点URL" />
</el-form-item>
<el-form-item label="额外URL">
<el-input v-model="appForm.extraUrl" placeholder="请输入额外URL可选" />
</el-form-item>
<el-form-item label="API Key" required>
<el-input v-model="appForm.apiKey" type="textarea" placeholder="请输入API Key" />
</el-form-item>
<el-form-item label="排序">
<el-input-number v-model="appForm.orderNum" :min="0" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="appDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveApp">保存</el-button>
</template>
</el-dialog>
<!-- 模型编辑对话框 -->
<el-dialog v-model="modelDialogVisible" :title="modelDialogTitle" width="700px">
<el-form :model="modelForm" label-width="120px">
<el-form-item label="模型名称" required>
<el-input v-model="modelForm.name" placeholder="请输入模型名称" />
</el-form-item>
<el-form-item label="模型ID" required>
<el-input v-model="modelForm.modelId" placeholder="请输入模型ID" />
</el-form-item>
<el-form-item label="处理名" required>
<el-input v-model="modelForm.handlerName" placeholder="请输入处理名" />
</el-form-item>
<el-form-item label="供应商名称">
<el-input v-model="modelForm.providerName" placeholder="如OpenAI、Anthropic等" />
</el-form-item>
<el-form-item label="模型描述">
<el-input v-model="modelForm.description" type="textarea" placeholder="请输入模型描述" />
</el-form-item>
<el-form-item label="是否尊享模型">
<el-switch v-model="modelForm.isPremium" />
</el-form-item>
<el-form-item label="模型倍率">
<el-input-number v-model="modelForm.multiplier" :min="0.01" :step="0.1" />
</el-form-item>
<el-form-item label="显示倍率">
<el-input-number v-model="modelForm.multiplierShow" :min="0.01" :step="0.1" />
</el-form-item>
<el-form-item label="模型类型" required>
<el-select v-model="modelForm.modelType" placeholder="请选择模型类型">
<el-option label="聊天" :value="0" />
<el-option label="图片" :value="1" />
<el-option label="嵌入" :value="2" />
</el-select>
</el-form-item>
<el-form-item label="API类型" required>
<el-select v-model="modelForm.modelApiType" placeholder="请选择API类型">
<el-option label="OpenAI" :value="0" />
<el-option label="Claude" :value="1" />
</el-select>
</el-form-item>
<el-form-item label="图标URL">
<el-input v-model="modelForm.iconUrl" placeholder="请输入模型图标URL" />
</el-form-item>
<el-form-item label="排序">
<el-input-number v-model="modelForm.orderNum" :min="0" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="modelDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveModel">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped lang="scss">
.channel-management {
padding: 20px;
height: calc(100vh - 40px);
.channel-container {
display: flex;
gap: 20px;
height: 100%;
}
.app-list-panel {
width: 350px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
}
.model-list-panel {
flex: 1;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
}
.panel-header {
padding: 16px 20px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.header-actions {
display: flex;
gap: 10px;
}
}
.app-list-scrollbar {
flex: 1;
height: 0;
}
.app-list {
padding: 10px;
}
.app-item {
padding: 12px 16px;
margin-bottom: 8px;
border: 1px solid #e4e7ed;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s;
&:hover {
border-color: #409eff;
background: #f0f9ff;
}
&.active {
border-color: #409eff;
background: #ecf5ff;
}
.app-item-content {
.app-name {
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
color: #303133;
}
.app-actions {
display: flex;
gap: 8px;
}
}
}
.empty-tip {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
}
</style>

View File

@@ -19,6 +19,7 @@ const navItems = [
{ name: 'daily-task', label: '每日任务(限时)', icon: 'Trophy', path: '/console/daily-task' },
{ name: 'invite', label: '每周邀请(限时)', icon: 'Present', path: '/console/invite' },
{ name: 'activation', label: '激活码兑换', icon: 'MagicStick', path: '/console/activation' },
{ name: 'channel', label: '渠道商管理', icon: 'Setting', path: '/console/channel' },
];
// 当前激活的菜单

View File

@@ -207,6 +207,14 @@ export const layoutRouter: RouteRecordRaw[] = [
title: '激活码兑换',
},
},
{
path: 'channel',
name: 'consoleChannel',
component: () => import('@/pages/console/channel/index.vue'),
meta: {
title: '渠道商管理',
},
},
],
},
],