import { computed, reactive, ref } from "vue";
import { useQuasar } from "quasar";
import type { AdminMfaPayload, AuthApplicationPageItem } from "../../../models/AuthModels";
import { AdminApiService, type ApplicationFormPayload } from "../../../services/AdminApiService";
import { useAdminActions } from "./useAdminActions";

export interface ApplicationFormModel {
    id: string;
    clientId: string;
    clientSecret: string;
    displayName: string;
    logo: string;
    applicationType: string;
    clientType: string;
    redirectUrisText: string;
    postLogoutRedirectUri: string;
    permissions: string[];
    customPermission: string;
}

const confidentialClientType = "confidential";

export function useApplicationForm(id?: string) {
    const $q = useQuasar();
    const api = new AdminApiService();
    const { runWithState } = useAdminActions();
    const loading = ref(false);
    const saving = ref(false);
    const errorMessage = ref("");
    const model = reactive<ApplicationFormModel>({
        id: id ?? "",
        clientId: "",
        clientSecret: "",
        displayName: "",
        logo: "",
        applicationType: "web",
        clientType: confidentialClientType,
        redirectUrisText: "",
        postLogoutRedirectUri: "",
        permissions: [
            "ept:authorization",
            "ept:token",
            "gt:authorization_code",
            "gt:refresh_token",
            "rst:code",
            "scp:openid",
            "scp:profile",
        ],
        customPermission: "",
    });

    const isEdit = computed(() => Boolean(id));

    async function load() {
        if (!id) {
            return;
        }
        loading.value = true;
        errorMessage.value = "";
        try {
            const result = await api.applicationDetail(id);
            if (!result.success || !result.data) {
                errorMessage.value = result.message ?? "加载应用失败";
                return;
            }
            hydrate(result.data);
        } catch (error) {
            errorMessage.value = error instanceof Error ? error.message : "加载应用失败";
        } finally {
            loading.value = false;
        }
    }

    function hydrate(item: AuthApplicationPageItem) {
        model.id = item.id;
        model.clientId = item.clientId;
        model.displayName = item.displayName ?? "";
        model.logo = item.logo ?? "";
        model.applicationType = item.applicationType ?? "web";
        model.clientType = item.clientType ?? confidentialClientType;
        model.redirectUrisText = item.redirectUris.join("\n");
        model.postLogoutRedirectUri = item.postLogoutRedirectUri ?? "";
        model.permissions = [...item.permissions];
    }

    function addCustomPermission() {
        const value = model.customPermission.trim();
        if (!value || model.permissions.includes(value)) {
            model.customPermission = "";
            return;
        }
        model.permissions.push(value);
        model.customPermission = "";
    }

    function validate() {
        if (!model.clientId.trim()) {
            return "请填写 ClientId";
        }
        if (!model.displayName.trim()) {
            return "请填写显示名称";
        }
        if (!model.clientType.trim()) {
            return "请选择客户端类型";
        }
        if (
            !isEdit.value &&
            model.clientType === confidentialClientType &&
            !model.clientSecret.trim()
        ) {
            return "机密客户端需填写 ClientSecret";
        }
        return "";
    }

    async function save() {
        const validationMessage = validate();
        if (validationMessage) {
            $q.notify({ type: "warning", message: validationMessage });
            return false;
        }

        saving.value = true;
        errorMessage.value = "";
        try {
            const payload = toPayload();
            return await runWithState(
                isEdit.value ? `update-application:${payload.id}` : "create-application",
                (mfa) => {
                    const mfaPayload = { ...payload, mfa };
                    return isEdit.value
                        ? api.updateApplication(mfaPayload)
                        : api.createApplication(mfaPayload);
                },
                "保存成功",
            );
        } catch (error) {
            errorMessage.value = error instanceof Error ? error.message : "保存失败";
            $q.notify({ type: "negative", message: errorMessage.value });
            return false;
        } finally {
            saving.value = false;
        }
    }

    function toPayload(mfa?: AdminMfaPayload): ApplicationFormPayload {
        return {
            id: model.id,
            clientId: model.clientId.trim(),
            clientSecret: model.clientSecret.trim(),
            displayName: model.displayName.trim(),
            logo: model.logo.trim(),
            applicationType: model.applicationType.trim(),
            clientType: model.clientType.trim(),
            redirectUris: parseLines(model.redirectUrisText),
            postLogoutRedirectUri: model.postLogoutRedirectUri.trim(),
            permissions: [...model.permissions],
            mfa,
        };
    }

    return {
        model,
        loading,
        saving,
        errorMessage,
        isEdit,
        load,
        save,
        addCustomPermission,
    };
}

function parseLines(value: string) {
    return value
        .split(/[\n;]/)
        .map((item) => item.trim())
        .filter(Boolean);
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

这段代码是一个 Vue 3 组合式函数(composable) useApplicationForm,主要用于管理某个“应用”的创建和编辑表单。它结合了 Quasar UI 框架的提示和交互逻辑,也调用了后台 Admin API 服务。

下面我从整体到细节逐步解释。


1. 用途

这个 composable 在管理后台中用于处理“应用程序”的表单逻辑,比如:

  • 新建应用时,默认给出一个空表单;
  • 编辑已有应用时,通过 id 加载已有数据并回填表单;
  • 对表单字段进行校验;
  • 将表单数据组装成 API 需要的 payload;
  • 创建或更新到后台,并在成功后给出提示。

2. 导入部分

import { computed, reactive, ref } from "vue";
import { useQuasar } from "quasar";
import type { AdminMfaPayload, AuthApplicationPageItem } from "../../../models/AuthModels";
import { AdminApiService, type ApplicationFormPayload } from "../../../services/AdminApiService";
import { useAdminActions } from "./useAdminActions";
  • Vue 的响应式 API:computed, reactive, ref
  • useQuasar:获取 Quasar 的全局实例,用于弹出通知提示($q.notify)。
  • AdminApiService:和后台 API 交互的服务类。
  • useAdminActions:自定义组合函数,提供 runWithState 方法,用于处理与 MFA/操作状态有关的统一逻辑。
  • 类型定义:AdminMfaPayload, AuthApplicationPageItem, ApplicationFormPayload

3. 类型与常量

export interface ApplicationFormModel {
    id: string;
    clientId: string;
    clientSecret: string;
    displayName: string;
    logo: string;
    applicationType: string;
    clientType: string;
    redirectUrisText: string;
    postLogoutRedirectUri: string;
    permissions: string[];
    customPermission: string;
}

ApplicationFormModel 描述了表单内部使用的中文/中间状态数据结构。注意:它不是直接对应 API 的 payload,而是为了在表单里维护 redirectUrisText(多行文本)和 customPermission 这种 UI 字段。

const confidentialClientType = "confidential";

定义了一个常量,表示“机密客户端”类型,后面用于条件校验以及默认值。


4. 组合式函数主体

export function useApplicationForm(id?: string) {

对外暴露一个函数 useApplicationForm,参数 id 是可选的。如果传入了 id,则认为是编辑模式;否则是新建模式。


4.1 初始化依赖

const $q = useQuasar();
const api = new AdminApiService();
const { runWithState } = useAdminActions();
  • $q 用来调用 Quasar 的通知、确认框等 UI。
  • api 用来调用后台接口。
  • runWithState 是一个高阶方法,用来包装创建/更新请求,处理 MFA(多因素认证)验证和按钮 loading 状态。

4.2 响应式状态

const loading = ref(false);
const saving = ref(false);
const errorMessage = ref("");

三个 ref

  • loading:是否正在加载已有应用详情。
  • saving:是否正在保存(创建/更新)。
  • errorMessage:错误消息文本,可展示在页面上。

4.3 模型数据

const model = reactive<ApplicationFormModel>({
    id: id ?? "",
    clientId: "",
    clientSecret: "",
    displayName: "",
    logo: "",
    applicationType: "web",
    clientType: confidentialClientType,
    redirectUrisText: "",
    postLogoutRedirectUri: "",
    permissions: [
        "ept:authorization",
        "ept:token",
        "gt:authorization_code",
        "gt:refresh_token",
        "rst:code",
        "scp:openid",
        "scp:profile",
    ],
    customPermission: "",
});

model 是一个 reactive 对象,初始值如下:

  • id 默认为传入的 id(或空字符串)。
  • 应用类型默认 web
  • 客户端类型默认 confidentialClientType
  • permissions 是一组默认授权范围。
  • redirectUrisText 为空,用于让用户粘贴多行重定向 URI。

这里用 reactive 是为了让表单数据双向绑定到模板更自然。


4.4 计算属性:是否编辑模式

const isEdit = computed(() => Boolean(id));

根据是否有 id 判断当前是编辑还是新建。


4.5 加载应用详情

async function load() {
    if (!id) {
        return;
    }
    loading.value = true;
    errorMessage.value = "";
    try {
        const result = await api.applicationDetail(id);
        if (!result.success || !result.data) {
            errorMessage.value = result.message ?? "加载应用失败";
            return;
        }
        hydrate(result.data);
    } catch (error) {
        errorMessage.value = error instanceof Error ? error.message : "加载应用失败";
    } finally {
        loading.value = false;
    }
}
  • 没有 id 直接返回,因为新建模式无需加载。
  • loading 阶段清空错误。
  • 调用后台接口 api.applicationDetail(id)
  • 如果接口返回 success: false,则显示错误信息。
  • 如果成功,则调用 hydrate(result.data) 把返回的数据写入 model
  • finally 确保停止 loading。

4.6 从后台数据填充表单

function hydrate(item: AuthApplicationPageItem) {
    model.id = item.id;
    model.clientId = item.clientId;
    model.displayName = item.displayName ?? "";
    model.logo = item.logo ?? "";
    model.applicationType = item.applicationType ?? "web";
    model.clientType = item.clientType ?? confidentialClientType;
    model.redirectUrisText = item.redirectUris.join("\n");
    model.postLogoutRedirectUri = item.postLogoutRedirectUri ?? "";
    model.permissions = [...item.permissions];
}

该方法把接口返回的 AuthApplicationPageItem 映射到 model

  • redirectUris 是一个字符串数组,变成用换行符连接的文本,方便在 <textarea> 中编辑。
  • 未提供字段时用空字符串或默认值兜底。
  • permissions 用拷贝的方式展开,避免直接引用后台对象。

4.7 添加自定义权限

function addCustomPermission() {
    const value = model.customPermission.trim();
    if (!value || model.permissions.includes(value)) {
        model.customPermission = "";
        return;
    }
    model.permissions.push(value);
    model.customPermission = "";
}

处理输入自定义权限的逻辑:

  • 如果输入为空,或已存在于 permissions,则清空输入框但不重复添加。
  • 否则加入数组,并清空输入框。

4.8 表单校验

function validate() {
    if (!model.clientId.trim()) {
        return "请填写 ClientId";
    }
    if (!model.displayName.trim()) {
        return "请填写显示名称";
    }
    if (!model.clientType.trim()) {
        return "请选择客户端类型";
    }
    if (
        !isEdit.value &&
        model.clientType === confidentialClientType &&
        !model.clientSecret.trim()
    ) {
        return "机密客户端需填写 ClientSecret";
    }
    return "";
}

按顺序校验关键字段:

  • clientId 必填。
  • displayName 必填。
  • clientType 必填。
  • 新建模式下,如果选择了机密客户端类型且没有填写 ClientSecret,则提醒必须填写。编辑模式下,因为可以保持原 Secret 不变,所以不强制要求每次都填写。

如果校验失败,返回错误消息字符串;否则返回空字符串。


4.9 保存逻辑

async function save() {
    const validationMessage = validate();
    if (validationMessage) {
        $q.notify({ type: "warning", message: validationMessage });
        return false;
    }

    saving.value = true;
    errorMessage.value = "";
    try {
        const payload = toPayload();
        return await runWithState(
            isEdit.value ? `update-application:${payload.id}` : "create-application",
            (mfa) => {
                const mfaPayload = { ...payload, mfa };
                return isEdit.value
                    ? api.updateApplication(mfaPayload)
                    : api.createApplication(mfaPayload);
            },
            "保存成功",
        );
    } catch (error) {
        errorMessage.value = error instanceof Error ? error.message : "保存失败";
        $q.notify({ type: "negative", message: errorMessage.value });
        return false;
    } finally {
        saving.value = false;
    }
}

保存函数流程:

  1. 先调用 validate(),若出错则用 Quasar 通知展示 warning 提示,并返回 false
  2. 设置 saving 为 true,清空错误。
  3. 调用 toPayload() 生成 API 参数。
  4. 调用 runWithState 来执行创建/更新:
    • runWithState 内部可能会弹出 MFA 验证对话框,并在验证通过后执行回调。
    • 它接收一个“操作键名”,例如 update-application:<id>create-application,用于防止重复提交、管理 loading 等。
    • 第二个参数是一个回调 (mfa) => { ... },因为 MFA 结果可能需要作为额外的 payload 字段传给后台,所以代码中把 mfa 合并进 payload。
    • 第三个参数是成功后的提示文本。
  5. 如果成功,则返回 runWithState 的结果(通常是 true)。
  6. 如果抛异常,则记录错误并在页面上弹出 negative 提示。
  7. 最后 finally 里恢复 saving = false

4.10 组装 payload

function toPayload(mfa?: AdminMfaPayload): ApplicationFormPayload {
    return {
        id: model.id,
        clientId: model.clientId.trim(),
        clientSecret: model.clientSecret.trim(),
        displayName: model.displayName.trim(),
        logo: model.logo.trim(),
        applicationType: model.applicationType.trim(),
        clientType: model.clientType.trim(),
        redirectUris: parseLines(model.redirectUrisText),
        postLogoutRedirectUri: model.postLogoutRedirectUri.trim(),
        permissions: [...model.permissions],
        mfa,
    };
}
  • 将表单数据转换为后端需要的 ApplicationFormPayload 结构。
  • redirectUris 从多行文本框内容解析成数组。
  • mfa 参数用于合并 MFA 验证结果。
  • 所有字符串字段都做了 .trim() 去除首尾空格。

4.11 返回值

return {
    model,
    loading,
    saving,
    errorMessage,
    isEdit,
    load,
    save,
    addCustomPermission,
};

对外暴露:

  • model:表单所有字段。
  • loading / saving / errorMessage:状态。
  • isEdit:是否为编辑状态。
  • load:加载详情的方法。
  • save:保存方法。
  • addCustomPermission:添加自定义权限方法。

使用时可在组件中这样调用:

const { model, loading, saving, isEdit, load, save, addCustomPermission } = useApplicationForm(props.id);

5. 辅助函数

function parseLines(value: string) {
    return value
        .split(/[\n;]/)
        .map((item) => item.trim())
        .filter(Boolean);
}

这是文件末尾的独立工具函数,作用是把“多行文本”或者“分号隔开”的字符串转换为字符串数组,丢弃空行、清理空格。例如输入:

https://a.com
https://b.com

会得到 ["https://a.com", "https://b.com"]


6. 整体架构与注意事项

  • 该 composable 把“创建”和“编辑”的差异隐藏在内部,对外接口基本统一。
  • 编辑模式下 load() 会自动填充表单;新建模式下不调用 load()
  • MFA 验证流程被封装在 runWithState 中,保存时通过回调参数获取 MFA 结果并合并到 payload,确保后台的 MFA 校验能通过。
  • 校验逻辑是轻量级的,但关键字段都有校验,尤其对机密客户端和新建场景做了特殊要求,避免无 Secret 就保存。
  • model 使用 reactive,在模板中可以使用 v-model 直接绑定 model.clientIdmodel.displayName 等字段,非常方便。

总而言之,useApplicationForm 是管理后台应用管理模块中一个非常核心可复用的逻辑层,负责从表单展示、数据加载、校验、保存到错误提示等完整链路。

评论加载中...