148 lines
4.2 KiB
JavaScript
148 lines
4.2 KiB
JavaScript
import { mkdir, writeFile } from "node:fs/promises";
|
|
import { existsSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
const args = process.argv.slice(2);
|
|
const featureName = args.find((arg) => !arg.startsWith("--"));
|
|
|
|
if (!featureName) {
|
|
console.error("Usage: pnpm scaffold:feature <feature-name> --label=模块名称 --path=/path --menu-code=menu-code --permission=module:view");
|
|
process.exit(1);
|
|
}
|
|
|
|
const options = Object.fromEntries(
|
|
args
|
|
.filter((arg) => arg.startsWith("--"))
|
|
.map((arg) => {
|
|
const [key, ...valueParts] = arg.slice(2).split("=");
|
|
return [key, valueParts.join("=")];
|
|
})
|
|
);
|
|
|
|
const kebabName = toKebabCase(featureName);
|
|
const camelName = toCamelCase(kebabName);
|
|
const pascalName = toPascalCase(kebabName);
|
|
const routePath = options.path || `/${kebabName}`;
|
|
const label = options.label || pascalName;
|
|
const menuCode = options["menu-code"] || kebabName;
|
|
const permission = options.permission || `${kebabName}:view`;
|
|
const featureDir = path.join(rootDir, "src/features", kebabName);
|
|
|
|
const files = new Map([
|
|
[
|
|
"api.ts",
|
|
`import { apiRequest } from "@/shared/api/request";
|
|
|
|
export function list${pascalName}(query?: Record<string, string | number>) {
|
|
return apiRequest("${routePath}", { query });
|
|
}
|
|
`
|
|
],
|
|
[
|
|
"routes.js",
|
|
`export const ${camelName}Routes = [
|
|
{
|
|
label: ${JSON.stringify(label)},
|
|
loader: () => import("./pages/${pascalName}Page.jsx").then((module) => module.${pascalName}Page),
|
|
menuCode: ${JSON.stringify(menuCode)},
|
|
pageKey: ${JSON.stringify(kebabName)},
|
|
path: ${JSON.stringify(routePath)},
|
|
permission: ${JSON.stringify(permission)}
|
|
}
|
|
];
|
|
`
|
|
],
|
|
[
|
|
"permissions.js",
|
|
`import { useAuth } from "@/app/auth/AuthProvider.jsx";
|
|
|
|
export function use${pascalName}Abilities() {
|
|
const { can } = useAuth();
|
|
|
|
return {
|
|
canView: can(${JSON.stringify(permission)})
|
|
};
|
|
}
|
|
`
|
|
],
|
|
[
|
|
`hooks/use${pascalName}Page.js`,
|
|
`import { useCallback } from "react";
|
|
import { useAdminQuery } from "@/shared/hooks/useAdminQuery.js";
|
|
import { list${pascalName} } from "@/features/${kebabName}/api";
|
|
import { use${pascalName}Abilities } from "@/features/${kebabName}/permissions.js";
|
|
|
|
const emptyData = { items: [], total: 0, page: 1, pageSize: 10 };
|
|
|
|
export function use${pascalName}Page() {
|
|
const abilities = use${pascalName}Abilities();
|
|
const queryFn = useCallback(() => list${pascalName}({ page: 1, page_size: 10 }), []);
|
|
const { data, error, loading, reload } = useAdminQuery(queryFn, {
|
|
errorMessage: "加载失败",
|
|
initialData: emptyData,
|
|
queryKey: [${JSON.stringify(kebabName)}]
|
|
});
|
|
|
|
return { abilities, data, error, loading, reload };
|
|
}
|
|
`
|
|
],
|
|
[
|
|
`pages/${pascalName}Page.jsx`,
|
|
`import { DataState } from "@/shared/ui/DataState.jsx";
|
|
import { use${pascalName}Page } from "@/features/${kebabName}/hooks/use${pascalName}Page.js";
|
|
import styles from "@/features/${kebabName}/${kebabName}.module.css";
|
|
|
|
export function ${pascalName}Page() {
|
|
const page = use${pascalName}Page();
|
|
|
|
return (
|
|
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
|
<section className={styles.root} />
|
|
</DataState>
|
|
);
|
|
}
|
|
`
|
|
],
|
|
[
|
|
`${kebabName}.module.css`,
|
|
`.root {
|
|
min-height: 100%;
|
|
}
|
|
`
|
|
],
|
|
["components/.gitkeep", ""]
|
|
]);
|
|
|
|
await mkdir(featureDir, { recursive: true });
|
|
|
|
for (const [relativePath, content] of files) {
|
|
const absolutePath = path.join(featureDir, relativePath);
|
|
await mkdir(path.dirname(absolutePath), { recursive: true });
|
|
if (existsSync(absolutePath)) {
|
|
console.log(`Skipped existing ${path.relative(rootDir, absolutePath)}`);
|
|
continue;
|
|
}
|
|
await writeFile(absolutePath, content);
|
|
console.log(`Created ${path.relative(rootDir, absolutePath)}`);
|
|
}
|
|
|
|
function toKebabCase(value) {
|
|
return value
|
|
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
.replace(/[^a-zA-Z0-9]+/g, "-")
|
|
.replace(/^-|-$/g, "")
|
|
.toLowerCase();
|
|
}
|
|
|
|
function toCamelCase(value) {
|
|
return value.replace(/-([a-z0-9])/g, (_, letter) => letter.toUpperCase());
|
|
}
|
|
|
|
function toPascalCase(value) {
|
|
const camel = toCamelCase(value);
|
|
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
|
}
|