Generate TS interfaces from YApi with robust nested array and anonymous node handling (I prefix, remove List, skip method prefix)
Ekde
// ==UserScript==
// @name Generate TS Interface
// @namespace http://tampermonkey.net/
// @version 1.8.0
// @description Generate TS interfaces from YApi with robust nested array and anonymous node handling (I prefix, remove List, skip method prefix)
// @author ihopeful
// @match *://*/project/*/interface/api/*
// @grant GM_setClipboard
// @icon https://yapi.pro/image/favicon.png
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const typeMap = {
string: 'string',
number: 'number',
integer: 'number',
boolean: 'boolean',
object: 'Record<string, any>',
'object []': 'Record<string, any>[]',
array: 'any[]'
};
const toTsType = t => {
if (!t) return 'any';
const lower = t.toLowerCase();
if (lower.includes('array')) return 'any[]';
if (lower.includes('object')) return 'Record<string, any>';
return typeMap[lower] || 'any';
};
const capitalize = str => str.charAt(0).toUpperCase() + str.slice(1);
const ignoreNames = ['List', 'Data', 'Response'];
let anonCount = 1;
const interfaces = [];
/** 自动展开所有折叠行 */
function expandAll(callback) {
const icons = Array.from(document.querySelectorAll('span.ant-table-row-expand-icon'))
.filter(el => el.className.includes('ant-table-row-collapsed'));
if (icons.length === 0) {
if (callback) callback();
return;
}
icons.forEach(icon => {
icon.scrollIntoView();
icon.click();
});
setTimeout(() => expandAll(callback), 500);
}
/** 安全生成接口名 */
function safeInterfaceName(name, parentName, isArray) {
let base;
if (!name) {
// 匿名对象用父名 + DTO
base = `${parentName.replace(/^I/, '')}DTO`;
} else {
base = capitalize(name);
if (base.endsWith('List')) base = base.slice(0, -4);
}
let interfaceName = 'I' + base;
// 重名检测
if (interfaces.some(i => i.startsWith(`interface ${interfaceName} `))) {
interfaceName += anonCount++;
}
return interfaceName;
}
/** 解析请求参数 */
function parseRequest(prefix) {
const rows = document.querySelectorAll('.colQuery table tbody tr');
if (!rows.length) return null;
const props = Array.from(rows).map(tr => {
const tds = tr.querySelectorAll('td');
const name = tds[0]?.innerText.trim();
const required = (tds[1]?.innerText || '').includes('是');
const comment = tds[3]?.innerText.trim() || name;
const type = 'string | number';
return ` /** ${comment} */\n ${name}${required ? '' : '?'}: ${type};`;
});
return `interface ${prefix}RequestQuery {\n${props.join('\n')}\n}`;
}
/** 解析响应参数 */
function parseResponse(prefix) {
const rows = document.querySelectorAll('h2.interface-title + div table tbody tr');
if (!rows.length) return null;
const root = { name: 'Response', children: [] };
const stack = [root];
Array.from(rows).forEach(tr => {
const levelClass = Array.from(tr.classList).find(c => c.includes('ant-table-row-level-'));
const level = levelClass ? parseInt(levelClass.split('-').pop(), 10) : 0;
const tds = tr.querySelectorAll('td');
const name = tds[0]?.innerText.trim();
const typeRaw = tds[1]?.innerText.trim();
const required = (tds[2]?.innerText || '').includes('必须');
const comment = tds[4]?.innerText.trim() || name;
const type = toTsType(typeRaw);
const node = { name, type, required, comment, children: [], rawType: typeRaw || '' };
while (stack.length > level + 1) stack.pop();
stack[stack.length - 1].children.push(node);
stack.push(node);
});
function genInterface(nodes, parentName, rootPrefix = parentName.replace(/^I/, '')) {
let res = '';
nodes.forEach(n => {
const pad = ' ';
const comment = `/** ${n.comment || ''} */\n`;
const isArray = n.rawType.includes('[]');
const hasChildren = n.children.length > 0;
if (hasChildren) {
// ✅ 若为数组 + 匿名对象子项 → 提升为 DTO[]
if (isArray && n.children.length === 1 && (!n.children[0].name || n.children[0].name === '')) {
const child = n.children[0];
const dtoName = `I${rootPrefix}${capitalize(n.name.replace(/List$/, ''))}DTO`;
interfaces.push(`interface ${dtoName} {\n${genInterface(child.children, dtoName, rootPrefix)}\n}`);
n.type = `${dtoName}[]`;
res += `${pad}${comment}${n.name}${n.required ? '' : '?'}: ${n.type};\n`;
} else {
// ✅ 子对象用 rootPrefix + 当前字段名 生成
const interfaceName = `I${rootPrefix}${capitalize(n.name.replace(/List$/, ''))}`;
interfaces.push(`interface ${interfaceName} {\n${genInterface(n.children, interfaceName, rootPrefix)}\n}`);
n.type = interfaceName + (isArray ? '[]' : '');
res += `${pad}${comment}${n.name}${n.required ? '' : '?'}: ${n.type};\n`;
}
} else {
res += `${pad}${comment}${n.name}${n.required ? '' : '?'}: ${n.type};\n`;
}
});
return res.trim();
}
interfaces.push(`interface ${prefix}Response {\n${genInterface(root.children, prefix)}\n}`);
return interfaces.join('\n\n');
}
/** 获取接口名和 URL */
function parseInterfaceName() {
const nameEl = document.querySelector('.panel-view .colName');
return nameEl?.innerText.trim() || '接口名称';
}
function parseApiUrl() {
const urlEl = document.querySelector('.panel-view .colValue .colValue:nth-child(2)');
if (!urlEl) return { method: 'get', url: '' };
const method = urlEl.previousElementSibling?.innerText.toLowerCase() || 'get';
let url = urlEl.innerText.trim();
url = url.replace(/^\/admin/, '');
return { method, url };
}
/** 生成函数名(去掉method前缀) */
function genFuncName(url) {
return url
.split('/')
.filter(Boolean)
.map((s, i) => (i === 0 ? s.toLowerCase() : capitalize(s)))
.join('');
}
/** 生成并插入 TS Interface */
function generateInterface() {
const apiName = parseInterfaceName();
const { method, url } = parseApiUrl();
const funcName = genFuncName(url);
const prefix = 'I' + capitalize(funcName);
const req = parseRequest(prefix);
const res = parseResponse(prefix);
if (!res) return;
const paramsStr = req ? `params: ${prefix}RequestQuery` : 'params?: Record<string, any>';
const func = `
/**
* ${apiName}
*/
export async function ${funcName}(${paramsStr}): Promise<${prefix}Response> {
return await ${method}(\`${url}\`, ${req ? 'params' : 'params'});
}
`;
const result = [req, res, func].filter(Boolean).join('\n\n');
const panelView = document.querySelector('.panel-view');
if (panelView) {
const pre = document.getElementById('ts-interface-generate');
if (pre) pre.remove();
const preEl = document.createElement('pre');
preEl.id = 'ts-interface-generate';
preEl.style = 'background:#f0f0f0;padding:10px;border-radius:6px;margin:10px 0;white-space:pre-wrap;position:relative;';
preEl.textContent = result;
const btn = document.createElement('button');
btn.textContent = '复制';
btn.style = 'position:absolute;top:5px;right:5px;padding:2px 6px;font-size:12px;cursor:pointer;';
btn.onclick = () => {
GM_setClipboard(result);
alert('复制成功✅');
};
preEl.appendChild(btn);
panelView.parentNode.insertBefore(preEl, panelView.nextSibling);
}
}
// 自动展开并生成
setTimeout(() => expandAll(generateInterface), 1500);
})();