diff --git a/backend/ai/migrations/0003_aimodel_model_type.py b/backend/ai/migrations/0003_aimodel_model_type.py new file mode 100644 index 0000000..9f9ef4b --- /dev/null +++ b/backend/ai/migrations/0003_aimodel_model_type.py @@ -0,0 +1,24 @@ +# Generated by Django 5.2.1 on 2025-07-16 03:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("ai", "0002_alter_chatrole_knowledge_alter_chatrole_tools"), + ] + + operations = [ + migrations.AddField( + model_name="aimodel", + name="model_type", + field=models.CharField( + blank=True, + db_comment="模型类型", + max_length=32, + null=True, + verbose_name="模型类型", + ), + ), + ] diff --git a/backend/ai/models.py b/backend/ai/models.py index c2d3c9a..10f94b6 100644 --- a/backend/ai/models.py +++ b/backend/ai/models.py @@ -46,6 +46,7 @@ class AIModel(CoreModel): on_delete=models.CASCADE, db_comment='API 秘钥编号', verbose_name="API 秘钥编号" ) + model_type = models.CharField(max_length=32, db_comment="模型类型", verbose_name="模型类型", blank=True, null=True) platform = models.CharField(max_length=32, db_comment="模型平台", verbose_name="模型平台") model = models.CharField(max_length=64, db_comment="模型标识", verbose_name="模型标识") diff --git a/backend/system/management/commands/generate_crud.py b/backend/system/management/commands/generate_crud.py index 50e6ec4..10131d5 100644 --- a/backend/system/management/commands/generate_crud.py +++ b/backend/system/management/commands/generate_crud.py @@ -219,38 +219,124 @@ class Command(BaseCommand): field_name = field.name field_label = getattr(field, 'verbose_name', field_name) if field_name == 'status': - return "{ component: 'RadioGroup', componentProps: { buttonStyle: 'solid', options: [{ label: $t('common.enabled'), value: 1 }, { label: $t('common.disabled'), value: 0 }], optionType: 'button' }, defaultValue: 1, fieldName: 'status', label: $t('system.status') }," + return f''' {{ + component: 'RadioGroup', + componentProps: {{ + buttonStyle: 'solid', + options: [ + {{ label: $t('common.enabled'), value: 1 }}, + {{ label: $t('common.disabled'), value: 0 }}, + ], + optionType: 'button', + }}, + defaultValue: 1, + fieldName: 'status', + label: $t('system.status'), + }},''' if isinstance(field, models.CharField): - return f"{{ component: 'Input', fieldName: '{field_name}', label: '{field_label}', rules: z.string().min(1, $t('ui.formRules.required', ['{field_label}'])).max(100, $t('ui.formRules.maxLength', ['{field_label}', 100])) }}," + return f''' {{ + component: 'Input', + fieldName: '{field_name}', + label: '{field_label}', + rules: z.string().min(1, $t('ui.formRules.required', ['{field_label}'])).max(100, $t('ui.formRules.maxLength', ['{field_label}', 100])), + }},''' elif isinstance(field, models.TextField): - return f"{{ component: 'Input', componentProps: {{ rows: 3, showCount: true }}, fieldName: '{field_name}', label: '{field_label}', rules: z.string().max(500, $t('ui.formRules.maxLength', ['{field_label}', 500])).optional() }}," + return f''' {{ + component: 'Input', + componentProps: {{ rows: 3, showCount: true }}, + fieldName: '{field_name}', + label: '{field_label}', + rules: z.string().max(500, $t('ui.formRules.maxLength', ['{field_label}', 500])).optional(), + }},''' elif isinstance(field, models.IntegerField): - return f"{{ component: 'InputNumber', fieldName: '{field_name}', label: '{field_label}' }}," + return f''' {{ + component: 'InputNumber', + fieldName: '{field_name}', + label: '{field_label}', + }},''' elif isinstance(field, models.BooleanField): - return f"{{ component: 'RadioGroup', componentProps: {{ buttonStyle: 'solid', options: [{{ label: '开启', value: 1 }}, {{ label: '关闭', value: 0 }}], optionType: 'button' }}, defaultValue: 1, fieldName: '{field_name}', label: '{field_label}' }}," + return f''' {{ + component: 'RadioGroup', + componentProps: {{ + buttonStyle: 'solid', + options: [ + {{ label: '开启', value: 1 }}, + {{ label: '关闭', value: 0 }}, + ], + optionType: 'button', + }}, + defaultValue: 1, + fieldName: '{field_name}', + label: '{field_label}', + }},''' else: - return f"{{ component: 'Input', fieldName: '{field_name}', label: '{field_label}' }}," + return f''' {{ + component: 'Input', + fieldName: '{field_name}', + label: '{field_label}', + }},''' def generate_grid_form_field(self, field): field_name = field.name field_label = getattr(field, 'verbose_name', field_name) if field_name == 'status': - return "{ component: 'Select', fieldName: 'status', label: '状态', componentProps: { allowClear: true, options: [{ label: '启用', value: 1 }, { label: '禁用', value: 0 }] } }," + return f''' {{ + component: 'Select', + fieldName: 'status', + label: '状态', + componentProps: {{ + allowClear: true, + options: [ + {{ label: '启用', value: 1 }}, + {{ label: '禁用', value: 0 }}, + ], + }}, + }},''' if isinstance(field, models.CharField): - return f"{{ component: 'Input', fieldName: '{field_name}', label: '{field_label}' }}," + return f''' {{ + component: 'Input', + fieldName: '{field_name}', + label: '{field_label}', + }},''' elif isinstance(field, models.IntegerField): - return f"{{ component: 'InputNumber', fieldName: '{field_name}', label: '{field_label}' }}," + return f''' {{ + component: 'InputNumber', + fieldName: '{field_name}', + label: '{field_label}', + }},''' else: - return f"{{ component: 'Input', fieldName: '{field_name}', label: '{field_label}' }}," + return f''' {{ + component: 'Input', + fieldName: '{field_name}', + label: '{field_label}', + }},''' def get_columns_code(self, fields): columns = [] for field in fields: if field.name == 'status': - columns.append("{ field: 'status', title: '状态', cellRender: { name: 'CellTag' } },") + columns.append( + """ { + field: 'status', + title: '状态', + cellRender: { name: 'CellTag' }, + },""" + ) continue if isinstance(field, (models.DateField, models.DateTimeField)): - columns.append(f"{{ field: '{field.name}', title: '{getattr(field, 'verbose_name', field.name)}', width: 150, formatter: ({{ cellValue }}) => format_datetime(cellValue) }},") + columns.append( + f""" {{ + field: '{field.name}', + title: '{getattr(field, 'verbose_name', field.name)}', + width: 150, + formatter: ({{ cellValue }}) => format_datetime(cellValue), + }},""" + ) continue - columns.append(f"{{ field: '{field.name}', title: '{getattr(field, 'verbose_name', field.name)}' }},") + columns.append( + f""" {{ + field: '{field.name}', + title: '{getattr(field, 'verbose_name', field.name)}', + }},""" + ) return columns \ No newline at end of file diff --git a/backend/system/management/commands/tpl/viewset.py.tpl b/backend/system/management/commands/tpl/viewset.py.tpl index 6c6b71a..85a83b8 100644 --- a/backend/system/management/commands/tpl/viewset.py.tpl +++ b/backend/system/management/commands/tpl/viewset.py.tpl @@ -1,6 +1,8 @@ from $app_name.models import $model_name from utils.serializers import CustomModelSerializer from utils.custom_model_viewSet import CustomModelViewSet +from django_filters import rest_framework as filters + class ${model_name}Serializer(CustomModelSerializer): """ @@ -12,13 +14,20 @@ class ${model_name}Serializer(CustomModelSerializer): read_only_fields = ['id', 'create_time', 'update_time'] +class $model_nameFilter(filters.FilterSet): + + class Meta: + model = $model_name + fields = [$filterset_fields] + + class ${model_name}ViewSet(CustomModelViewSet): """ $verbose_name 视图集 """ queryset = $model_name.objects.filter(is_deleted=False).order_by('-id') serializer_class = ${model_name}Serializer - filterset_fields = [$filterset_fields] + filterset_class = [$filterset_fields] search_fields = ['name'] # 根据实际字段调整 ordering_fields = ['create_time', 'id'] ordering = ['-create_time'] diff --git a/backend/system/views/dict_data.py b/backend/system/views/dict_data.py index 92737ea..1f36614 100644 --- a/backend/system/views/dict_data.py +++ b/backend/system/views/dict_data.py @@ -22,7 +22,7 @@ class DictDataFilter(filters.FilterSet): class DictDataLabelValueSerializer(serializers.ModelSerializer): - dict_type_value = serializers.CharField(source='dict_type.value') + dict_type = serializers.CharField(source='dict_type.value') class Meta: model = DictData @@ -39,4 +39,4 @@ class DictDataViewSet(CustomModelViewSet): # 复用filterset_class过滤DictData queryset = self.get_queryset().filter(status=CommonStatus.ENABLED) serializer = DictDataLabelValueSerializer(queryset, many=True) - return Response(serializer.data) \ No newline at end of file + return self._build_response(data=serializer.data) \ No newline at end of file diff --git a/web/apps/web-antd/src/app.vue b/web/apps/web-antd/src/app.vue index bbaccce..7dc7681 100644 --- a/web/apps/web-antd/src/app.vue +++ b/web/apps/web-antd/src/app.vue @@ -7,12 +7,17 @@ import { preferences, usePreferences } from '@vben/preferences'; import { App, ConfigProvider, theme } from 'ant-design-vue'; import { antdLocale } from '#/locales'; +import { useDictStore } from '#/store/dict'; defineOptions({ name: 'App' }); const { isDark } = usePreferences(); const { tokens } = useAntdDesignTokens(); +const dictStore = useDictStore(); + +dictStore.fetchDictData(); + const tokenTheme = computed(() => { const algorithm = isDark.value ? [theme.darkAlgorithm] diff --git a/web/apps/web-antd/src/hooks/useDictOptions.ts b/web/apps/web-antd/src/hooks/useDictOptions.ts new file mode 100644 index 0000000..827fca1 --- /dev/null +++ b/web/apps/web-antd/src/hooks/useDictOptions.ts @@ -0,0 +1,23 @@ +import { useDictStore } from '#/store/dict'; + +export function useDictOptions(dictType: string) { + const dictStore = useDictStore(); + return dictStore.getOptionsByType(dictType); +} + +/** + * 通用字典 value 转 label + * @param dictType 字典类型 + * @param value 字典值 + * @returns label + */ +export function useDictLabel(dictType: string, value: any): string { + const options = useDictOptions(dictType); + const item = options.find((opt) => opt.value === value); + return item ? item.label : value; +} + +export function dictFormatter(dictType: string) { + return ({ cellValue }: { cellValue: any }) => + useDictLabel(dictType, cellValue); +} diff --git a/web/apps/web-antd/src/models/ai/ai_api_key.ts b/web/apps/web-antd/src/models/ai/ai_api_key.ts index c29eb6c..e65a14a 100644 --- a/web/apps/web-antd/src/models/ai/ai_api_key.ts +++ b/web/apps/web-antd/src/models/ai/ai_api_key.ts @@ -11,6 +11,7 @@ export namespace AiAIApiKeyApi { is_deleted: boolean; name: string; platform: string; + model_type: string; api_key: string; url: string; status: number; diff --git a/web/apps/web-antd/src/store/dict.ts b/web/apps/web-antd/src/store/dict.ts new file mode 100644 index 0000000..813d7de --- /dev/null +++ b/web/apps/web-antd/src/store/dict.ts @@ -0,0 +1,18 @@ +// src/store/dict.ts +import { defineStore } from 'pinia'; + +import { getDictDataSimple } from '#/api/system/dict_data'; // 根据实际路径调整 + +export const useDictStore = defineStore('dict', { + state: () => ({ + dictData: [] as any[], + }), + actions: { + async fetchDictData() { + this.dictData = await getDictDataSimple(); // 根据接口返回结构调整 + }, + getOptionsByType(type: string) { + return this.dictData.filter((item) => item.dict_type === type); + }, + }, +}); diff --git a/web/apps/web-antd/src/views/ai/ai_api_key/data.ts b/web/apps/web-antd/src/views/ai/ai_api_key/data.ts index 312627c..dcfb537 100644 --- a/web/apps/web-antd/src/views/ai/ai_api_key/data.ts +++ b/web/apps/web-antd/src/views/ai/ai_api_key/data.ts @@ -28,8 +28,8 @@ export function useSchema(): VbenFormSchema[] { label: '平台', componentProps: { options: PLATFORM_OPTIONS, - style: { minWidth: '180px' }, - dropdownStyle: { minWidth: '180px' }, + class: 'w-full', + placeholder: '请选择', }, rules: z.string(), }, diff --git a/web/apps/web-antd/src/views/ai/ai_model/data.ts b/web/apps/web-antd/src/views/ai/ai_model/data.ts index f340860..7ae1b2b 100644 --- a/web/apps/web-antd/src/views/ai/ai_model/data.ts +++ b/web/apps/web-antd/src/views/ai/ai_model/data.ts @@ -5,6 +5,7 @@ import type { OnActionClickFn } from '#/adapter/vxe-table'; import type { AiAIModelApi } from '#/models/ai/ai_model'; import { z } from '#/adapter/form'; +import { dictFormatter, useDictOptions } from '#/hooks/useDictOptions'; import { $t } from '#/locales'; import { AiAIApiKeyModel } from '#/models/ai/ai_api_key'; import { op } from '#/utils/permission'; @@ -27,15 +28,27 @@ export function useSchema(): VbenFormSchema[] { }, fieldName: 'key', label: 'API 秘钥', + rules: z.number(), }, { - component: 'Input', + component: 'ApiSelect', fieldName: 'platform', + componentProps: { + options: useDictOptions('ai_platform'), + class: 'w-full', + }, label: '模型平台', - rules: z - .string() - .min(1, $t('ui.formRules.required', ['模型平台'])) - .max(100, $t('ui.formRules.maxLength', ['模型平台', 100])), + rules: z.string(), + }, + { + component: 'ApiSelect', + fieldName: 'model_type', + componentProps: { + options: useDictOptions('ai_model_type'), + class: 'w-full', + }, + label: '模型类型', + rules: z.string(), }, { component: 'Input', @@ -59,6 +72,9 @@ export function useSchema(): VbenFormSchema[] { component: 'InputNumber', fieldName: 'sort', label: '排序', + componentProps: { + class: 'w-full', + }, }, { component: 'RadioGroup', @@ -82,12 +98,18 @@ export function useSchema(): VbenFormSchema[] { { component: 'InputNumber', fieldName: 'max_tokens', - label: '回复数 Token 数', + label: '回复Token数', + componentProps: { + class: 'w-full', + }, }, { component: 'InputNumber', fieldName: 'max_contexts', label: '上下文数量', + componentProps: { + class: 'w-full', + }, }, { component: 'Input', @@ -162,6 +184,12 @@ export function useColumns( { field: 'platform', title: '模型平台', + formatter: dictFormatter('ai_platform'), + }, + { + field: 'model_type', + title: '模型类型', + formatter: dictFormatter('ai_model_type'), }, { field: 'model',