ModuleConfig.tsx 10.7 KB
import { useEffect, useMemo, useState } from "react";
import {
  Button,
  Form,
  Input,
  InputNumber,
  Select,
  Tree,
  Checkbox,
  App as AntApp,
  Popconfirm,
} from "antd";
import {
  PlusOutlined,
  EditOutlined,
  SaveOutlined,
  CloseOutlined,
  DeleteOutlined,
  ReloadOutlined,
} from "@ant-design/icons";
import type { DataNode } from "antd/es/tree";
import {
  listModules,
  createModule,
  updateModule,
  deleteModule,
  type ModuleTreeVO,
  type ModuleDTO,
} from "@/api/module";
import { MODULE_DISPLAY_TYPES } from "@/utils/data";

type Mode = "view" | "edit" | "new";

interface FormShape {
  sDisplayType: string;
  sModuleNameZh: string;
  sManageDeptEn: string;
  sModuleType: string;
  sProcedureName: string;
  iSortOrder: number;
  iParentId: number | null;
  bShowPermission: boolean;
}

export default function ModuleConfig() {
  const { message } = AntApp.useApp();
  const [form] = Form.useForm<FormShape>();
  const [tree, setTree] = useState<ModuleTreeVO[]>([]);
  const [loading, setLoading] = useState(false);
  const [selectedId, setSelectedId] = useState<number | null>(null);
  const [mode, setMode] = useState<Mode>("view");
  const [submitting, setSubmitting] = useState(false);

  const reload = async () => {
    setLoading(true);
    try {
      const data = await listModules();
      setTree(data);
    } catch {
      // interceptor
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    void reload();
  }, []);

  const flat = useMemo(() => flatten(tree), [tree]);
  const treeData = useMemo(() => toTreeData(tree), [tree]);
  const selected = flat.find((m) => m.iIncrement === selectedId) ?? null;

  useEffect(() => {
    if (selected && mode === "view") {
      form.setFieldsValue({
        sDisplayType: selected.sDisplayType,
        sModuleNameZh: selected.sModuleNameZh,
        sManageDeptEn: selected.sManageDeptEn,
        sModuleType: "",
        sProcedureName: "",
        iSortOrder: selected.iSortOrder,
        iParentId: selected.iParentId ?? null,
        bShowPermission: false,
      });
    }
  }, [selected, mode, form]);

  const startEdit = () => {
    if (!selected) return;
    setMode("edit");
  };

  const startNew = () => {
    form.setFieldsValue({
      sDisplayType: "前端业务",
      sModuleNameZh: "",
      sManageDeptEn: "",
      sModuleType: "",
      sProcedureName: "",
      iSortOrder: 1,
      iParentId: selectedId,
      bShowPermission: false,
    });
    setMode("new");
  };

  const cancel = () => {
    setMode("view");
  };

  const save = async () => {
    try {
      const values = await form.validateFields();
      const dto: ModuleDTO = {
        sDisplayType: values.sDisplayType,
        sModuleNameZh: values.sModuleNameZh,
        sManageDeptEn: values.sManageDeptEn,
        sModuleType: values.sModuleType,
        sProcedureName: values.sProcedureName,
        iSortOrder: values.iSortOrder,
        iParentId: values.iParentId,
        bShowPermission: values.bShowPermission,
      };
      setSubmitting(true);
      if (mode === "new") {
        const res = await createModule(dto);
        message.success("新增成功");
        await reload();
        setSelectedId(res.iIncrement);
        setMode("view");
      } else if (selectedId) {
        await updateModule(selectedId, dto);
        message.success("保存成功");
        await reload();
        setMode("view");
      }
    } catch {
      // validation or interceptor
    } finally {
      setSubmitting(false);
    }
  };

  const onDelete = async () => {
    if (!selectedId) return;
    try {
      await deleteModule(selectedId);
      message.success("删除成功");
      setSelectedId(null);
      await reload();
    } catch {
      // interceptor
    }
  };

  const disabled = mode === "view";

  return (
    <div
      style={{
        display: "flex",
        flexDirection: "column",
        height: "100%",
        background: "var(--bg-app)",
      }}
    >
      <div className="blue-toolbar">
        <Button icon={<ReloadOutlined />} onClick={reload} size="small">
          刷新
        </Button>
        <Button
          icon={<PlusOutlined />}
          type="primary"
          onClick={startNew}
          disabled={mode !== "view"}
          size="small"
        >
          添加节点
        </Button>
        <Button
          icon={<EditOutlined />}
          type="primary"
          onClick={startEdit}
          disabled={mode !== "view" || !selected}
          size="small"
        >
          修改
        </Button>
        <Button
          icon={<SaveOutlined />}
          onClick={save}
          loading={submitting}
          disabled={mode === "view"}
          type="primary"
          size="small"
        >
          保存
        </Button>
        <Button
          icon={<CloseOutlined />}
          onClick={cancel}
          disabled={mode === "view"}
          size="small"
        >
          取消
        </Button>
        <Popconfirm
          title="确认删除该模块?"
          onConfirm={onDelete}
          disabled={!selected || mode !== "view"}
        >
          <Button
            icon={<DeleteOutlined />}
            danger
            disabled={!selected || mode !== "view"}
            size="small"
          >
            删除节点
          </Button>
        </Popconfirm>
        <div style={{ flex: 1 }} />
        <span
          style={{
            padding: "2px 8px",
            fontSize: 11,
            background:
              mode === "view"
                ? "rgba(39,165,103,0.12)"
                : "rgba(217,142,31,0.18)",
            color: mode === "view" ? "var(--success)" : "var(--warning)",
            border: "1px solid var(--border)",
          }}
        >
          状态:{mode === "view" ? "只读" : mode === "new" ? "新增中" : "编辑中"}
        </span>
      </div>

      <div style={{ flex: 1, display: "flex", overflow: "hidden", minHeight: 0 }}>
        <div
          style={{
            width: 280,
            flex: "none",
            background: "#fff",
            borderRight: "1px solid var(--border)",
            overflow: "auto",
          }}
        >
          <Tree
            treeData={treeData}
            selectedKeys={selectedId ? [selectedId] : []}
            onSelect={(keys) => {
              if (keys.length) setSelectedId(Number(keys[0]));
            }}
            blockNode
            defaultExpandAll
            disabled={loading || mode !== "view"}
            style={{ padding: 6, fontSize: 12 }}
          />
          {tree.length === 0 && !loading && (
            <div
              style={{
                padding: 16,
                color: "var(--text-faint)",
                fontSize: 12,
                textAlign: "center",
              }}
            >
              暂无模块数据
            </div>
          )}
        </div>

        <div style={{ flex: 1, overflow: "auto", padding: 14, minWidth: 0 }}>
          <div
            style={{
              background: "#fff",
              padding: "14px 14px 16px",
              border: "1px solid var(--border)",
            }}
          >
            <Form
              form={form}
              layout="vertical"
              disabled={disabled}
              style={{
                display: "grid",
                gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
                rowGap: 8,
                columnGap: 14,
              }}
            >
              <Form.Item
                label="显示类别"
                name="sDisplayType"
                rules={[{ required: true }]}
              >
                <Select options={MODULE_DISPLAY_TYPES.map((t) => ({ value: t, label: t }))} />
              </Form.Item>
              <Form.Item
                label="模块中文名"
                name="sModuleNameZh"
                rules={[{ required: true, max: 100 }]}
              >
                <Input />
              </Form.Item>
              <Form.Item
                label="管理部门英文"
                name="sManageDeptEn"
                rules={[{ required: true, max: 50 }]}
              >
                <Input />
              </Form.Item>
              <Form.Item
                label="模块类型"
                name="sModuleType"
                rules={[{ required: true, max: 50 }]}
              >
                <Input />
              </Form.Item>
              <Form.Item
                label="存储过程名"
                name="sProcedureName"
                rules={[
                  { required: mode === "new", max: 100 },
                ]}
              >
                <Input className="mono" />
              </Form.Item>
              <Form.Item label="序号" name="iSortOrder" initialValue={1}>
                <InputNumber style={{ width: "100%" }} />
              </Form.Item>
              <Form.Item label="父级" name="iParentId">
                <Select
                  allowClear
                  placeholder="无(根节点)"
                  options={flat.map((m) => ({ value: m.iIncrement, label: m.sModuleNameZh }))}
                />
              </Form.Item>
              <Form.Item
                label="权限是否显示"
                name="bShowPermission"
                valuePropName="checked"
              >
                <Checkbox />
              </Form.Item>
            </Form>
          </div>
          <div
            style={{
              marginTop: 8,
              padding: "6px 12px",
              background: "#fff",
              border: "1px solid var(--border)",
              fontSize: 11,
              color: "var(--text-muted)",
              display: "flex",
              justifyContent: "space-between",
            }}
          >
            <span>
              当前节点:
              {selected ? `${selected.iIncrement} ${selected.sModuleNameZh}` : "未选择"}
            </span>
            <span>
              状态:
              <span
                style={{
                  color: mode === "view" ? "var(--success)" : "var(--warning)",
                  fontWeight: 500,
                }}
              >
                {mode === "view" ? "只读" : mode === "new" ? "新增中" : "编辑中"}
              </span>
            </span>
          </div>
        </div>
      </div>
    </div>
  );
}

function flatten(nodes: ModuleTreeVO[]): ModuleTreeVO[] {
  const out: ModuleTreeVO[] = [];
  const walk = (ns: ModuleTreeVO[]) => {
    for (const n of ns) {
      out.push(n);
      if (n.children?.length) walk(n.children);
    }
  };
  walk(nodes);
  return out;
}

function toTreeData(nodes: ModuleTreeVO[]): DataNode[] {
  return nodes.map((n) => ({
    key: n.iIncrement,
    title: n.sModuleNameZh,
    children: n.children?.length ? toTreeData(n.children) : undefined,
  }));
}