feat: 新增AI模型配置功能,优化前端组件与代码规范

1. 新增Redis AI_MODEL_CONFIG枚举与后端AI模型配置CRUD接口
2. 升级vue-img-cutter到3.1.1版本,更新前端依赖
3. 重构前端多处ElMessage提示逻辑,统一由拦截器处理
4. 替换ElDrawer为FaDrawer组件,统一弹窗组件库
5. 重构文章详情、评论组件,新增租户切换器与AI配置面板
6. 优化代码生成模板、菜单树表格逻辑与代码高亮样式
7. 修复前端路由与组件命名问题,更新快速入口配置
This commit is contained in:
zhangtao
2026-06-25 00:14:51 +08:00
parent 18ac9eda84
commit fa680c03d3
56 changed files with 4066 additions and 1380 deletions
@@ -295,7 +295,6 @@ import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue"
import type { FormItem } from "@/components/forms/fa-form/index.vue";
import type FaForm from "@/components/forms/fa-form/index.vue";
import FaStatusTag from "@/components/others/fa-status-tag/index.vue";
import { ElMessage } from "element-plus";
defineOptions({ name: "Email" });
@@ -1004,7 +1003,7 @@ async function handleSubmitSend() {
sendSubmitting.value = true;
try {
await EmailAPI.sendEmail(sendFormData.value);
ElMessage.success("发送成功");
// 成功 / 失败提示由 axios 拦截器统一处理
sendVisible.value = false;
} catch {
/* ignore */
@@ -1057,7 +1056,7 @@ async function handleSubmitTest() {
config_id: testFormData.value.config_id,
to_email: testFormData.value.to_email,
});
ElMessage.success("测试邮件已发送");
// 成功 / 失败提示由 axios 拦截器统一处理
testVisible.value = false;
} catch {
/* ignore */
@@ -619,14 +619,13 @@ async function deletePkgRow(id: number) {
async function togglePkgStatus(row: PackageTable) {
const newStatus = row.status === 0 ? 1 : 0;
const label = newStatus === 0 ? "启用" : "禁用";
try {
await confirmToggleStatus(newStatus);
await PackageAPI.batchPackageStatus({ ids: [row.id!], status: Number(newStatus) });
ElMessage.success(`${label}成功`);
// 成功提示由 axios 拦截器统一处理
await refreshData();
} catch {
// 用户取消
// 用户取消 / 接口错误(已由拦截器提示)
}
}
@@ -686,7 +685,6 @@ async function handleSaveMenus() {
menuSaveLoading.value = true;
try {
await PackageAPI.setPackageMenus(currentMenuPkgId.value, checkedIds);
ElMessage.success("菜单权限保存成功");
menuDialogVisible.value = false;
} catch {
// 错误由全局拦截处理
@@ -259,15 +259,7 @@ import PluginAPI, { type PluginForm, type PluginTable } from "@/api/module_platf
import { useAuth } from "@/hooks/core/useAuth";
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
import type { FormItem } from "@/components/forms/fa-form/index.vue";
import {
ElTag,
ElMessage,
ElButton,
ElIcon,
ElDropdown,
ElDropdownMenu,
ElDropdownItem,
} from "element-plus";
import { ElTag, ElButton, ElIcon, ElDropdown, ElDropdownMenu, ElDropdownItem } from "element-plus";
import { Plus, Edit, Delete, MoreFilled } from "@element-plus/icons-vue";
defineOptions({
@@ -421,10 +413,10 @@ async function doInstall(row: PluginTable) {
if (!row.id) return;
try {
await PluginAPI.install(row.id);
ElMessage.success("安装成功");
// 成功 / 失败提示由 axios 拦截器统一处理
row.installed = true;
} catch {
ElMessage.error("安装失败");
/* 接口错误已由拦截器提示 */
}
}
@@ -432,10 +424,10 @@ async function doUninstall(row: PluginTable) {
if (!row.id) return;
try {
await PluginAPI.uninstall(row.id);
ElMessage.success("卸载成功");
// 成功 / 失败提示由 axios 拦截器统一处理
row.installed = false;
} catch {
ElMessage.error("卸载失败");
/* 接口错误已由拦截器提示 */
}
}
@@ -443,10 +435,10 @@ async function doToggle(row: PluginTable) {
if (!row.id) return;
try {
await PluginAPI.toggle(row.id);
ElMessage.success(row.status === 0 ? "已禁用" : "已启用");
// 成功 / 失败提示由 axios 拦截器统一处理
row.status = row.status === 0 ? 1 : 0;
} catch {
ElMessage.error("操作失败");
/* 接口错误已由拦截器提示 */
}
}
@@ -138,7 +138,9 @@
:show-tip="true"
:enable-preview="true"
:enable-crop="true"
v-bind="brandCropBind('tenant_logo')"
crop-dialog-title="裁剪站点 Logo"
crop-inner-title="调整 Logo"
crop-preview-title="预览"
/>
</ElFormItem>
</ElCol>
@@ -152,7 +154,9 @@
:show-tip="true"
:enable-preview="true"
:enable-crop="true"
v-bind="brandCropBind('tenant_favicon')"
crop-dialog-title="裁剪网站图标"
crop-inner-title="调整图标"
crop-preview-title="预览"
/>
</ElFormItem>
</ElCol>
@@ -166,7 +170,9 @@
:show-tip="true"
:enable-preview="true"
:enable-crop="true"
v-bind="brandCropBind('tenant_login_bg')"
crop-dialog-title="裁剪登录背景"
crop-inner-title="调整背景图"
crop-preview-title="预览"
/>
</ElFormItem>
</ElCol>
@@ -214,6 +220,7 @@ import TenantAPI, {
type TenantTable,
type TenantUpdateForm,
} from "@/api/module_platform/tenant";
import PackageAPI from "@/api/module_platform/package";
import { useAuth } from "@/hooks/core/useAuth";
import { renderTableOperationCell, type TableOperationAction, resolveStatusColumns } from "@utils";
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
@@ -221,7 +228,7 @@ import type FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
import type { FormItem } from "@/components/forms/fa-form/index.vue";
import type FaForm from "@/components/forms/fa-form/index.vue";
import { ElMessage, ElTabs, ElTabPane, ElForm, ElFormItem, ElRow, ElCol } from "element-plus";
import { h, ref } from "vue";
import { h, ref, computed, onMounted } from "vue";
defineOptions({
name: "Tenant",
@@ -365,7 +372,7 @@ async function toggleTenantStatus(id: number) {
}
await refreshData();
} catch {
ElMessage.error("状态切换失败");
/* 接口错误已由拦截器提示 */
}
}
@@ -551,44 +558,6 @@ const dataFormRef = ref<InstanceType<typeof FaForm> | null>(null);
const submitLoading = ref(false);
const tenantFormRenderKey = ref(0);
function brandCropBind(key: string) {
switch (key) {
case "tenant_favicon":
return {
cropCutWidth: 64,
cropCutHeight: 64,
cropBoxWidth: 380,
cropBoxHeight: 320,
cropDialogTitle: "裁剪网站图标",
cropInnerTitle: "调整图标",
cropPreviewTitle: "预览",
};
case "tenant_logo":
return {
cropCutWidth: 320,
cropCutHeight: 96,
cropBoxWidth: 520,
cropBoxHeight: 360,
cropDialogTitle: "裁剪站点 Logo",
cropInnerTitle: "调整 Logo",
cropPreviewTitle: "预览",
};
case "tenant_login_bg":
return {
cropCutWidth: 960,
cropCutHeight: 540,
cropBoxWidth: 560,
cropBoxHeight: 380,
cropDialogTitle: "裁剪登录背景",
cropInnerTitle: "调整背景图",
cropPreviewTitle: "预览",
cropFileType: "jpeg" as const,
};
default:
return {};
}
}
async function handleAdd() {
createLoading.value = true;
try {
@@ -628,7 +597,27 @@ async function handleCloseDialog() {
const activeTab = ref("basic");
const basicFormItems: FormItem[] = [
const packageOptions = ref<{ label: string; value: number }[]>([]);
const packageLoading = ref(false);
async function fetchPackageOptions() {
packageLoading.value = true;
try {
const res = await PackageAPI.listPackage({ page_no: 1, page_size: 100 });
const list = (res.data?.data?.items ?? res.data?.data ?? []) as { id: number; name: string }[];
packageOptions.value = list.map((p) => ({ label: p.name, value: p.id }));
} catch {
packageOptions.value = [];
} finally {
packageLoading.value = false;
}
}
onMounted(() => {
fetchPackageOptions();
});
const basicFormItems = computed<FormItem[]>(() => [
{
label: "租户名称",
key: "name",
@@ -659,10 +648,16 @@ const basicFormItems: FormItem[] = [
},
},
{
label: "关联套餐ID",
label: "关联套餐",
key: "package_id",
type: "number",
props: { placeholder: "选填", min: 1, style: { width: "100%" } },
type: "select",
props: {
placeholder: "请选择套餐",
options: packageOptions.value,
loading: packageLoading.value,
clearable: true,
style: { width: "100%" },
},
},
{
label: "排序",
@@ -726,7 +721,7 @@ const basicFormItems: FormItem[] = [
valueFormat: "YYYY-MM-DD HH:mm:ss",
},
},
];
]);
const websiteFormItems: FormItem[] = [
{