diff --git a/Moka_全量API接口.xlsx b/Moka_全量API接口.xlsx new file mode 100644 index 0000000..d35cfc2 Binary files /dev/null and b/Moka_全量API接口.xlsx differ diff --git a/client/src/components/ApiDebugger.jsx b/client/src/components/ApiDebugger.jsx index 62e3be5..c2646f6 100644 --- a/client/src/components/ApiDebugger.jsx +++ b/client/src/components/ApiDebugger.jsx @@ -32,6 +32,10 @@ const ApiDebugger = ({ onTitleChange, storageKey = 'tab_default' }) => { const [currentMatchIndex, setCurrentMatchIndex] = useState(0); const [matchCount, setMatchCount] = useState(0); + // Endpoint search state + const [endpointSearchQuery, setEndpointSearchQuery] = useState(''); + const [isEndpointDropdownOpen, setIsEndpointDropdownOpen] = useState(false); + // UX state const [copyResText, setCopyResText] = useState('复制结果'); const [copyCurlText, setCopyCurlText] = useState('复制 CURL'); @@ -102,7 +106,16 @@ const ApiDebugger = ({ onTitleChange, storageKey = 'tab_default' }) => { }; fetchInitialData(); - }, []); + + // Close dropdown when clicking outside + const handleClickOutside = (e) => { + if (isEndpointDropdownOpen && !e.target.closest('.form-group')) { + setIsEndpointDropdownOpen(false); + } + }; + document.addEventListener('click', handleClickOutside); + return () => document.removeEventListener('click', handleClickOutside); + }, [isEndpointDropdownOpen]); useEffect(() => { const tenant = tenants.find(t => t.id === parseInt(selectedTenantId)); @@ -428,10 +441,11 @@ const ApiDebugger = ({ onTitleChange, storageKey = 'tab_default' }) => { setQueryParamsError(''); }; - const handleEndpointChange = (e) => { - const endpointId = Number(e.target.value); + const handleEndpointChange = (endpointId) => { setSelectedEndpointId(endpointId); localStorage.setItem(`${storageKey}_endpointId`, endpointId); + setIsEndpointDropdownOpen(false); + setEndpointSearchQuery(''); const endpoint = endpoints.find(ep => ep.id === endpointId); if (endpoint) { @@ -452,6 +466,28 @@ const ApiDebugger = ({ onTitleChange, storageKey = 'tab_default' }) => { } }; + // Filter endpoints by search query + const searchFilteredEndpoints = useMemo(() => { + if (!endpointSearchQuery.trim()) return filteredEndpoints; + const query = endpointSearchQuery.toLowerCase(); + return filteredEndpoints.filter(ep => + ep.name.toLowerCase().includes(query) || + ep.method.toLowerCase().includes(query) || + (ep.module && ep.module.toLowerCase().includes(query)) + ); + }, [filteredEndpoints, endpointSearchQuery]); + + // Group endpoints by module + const groupedEndpoints = useMemo(() => { + const groups = {}; + searchFilteredEndpoints.forEach(ep => { + const module = ep.module || '其他'; + if (!groups[module]) groups[module] = []; + groups[module].push(ep); + }); + return groups; + }, [searchFilteredEndpoints]); + return (
@@ -470,12 +506,130 @@ const ApiDebugger = ({ onTitleChange, storageKey = 'tab_default' }) => {
- +
+
setIsEndpointDropdownOpen(!isEndpointDropdownOpen)} + style={{ + padding: '0.5rem', + border: '1px solid var(--border)', + borderRadius: '4px', + cursor: 'pointer', + backgroundColor: 'var(--bg)', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center' + }} + > + + {selectedEndpointId + ? `[${endpoints.find(e => e.id === parseInt(selectedEndpointId))?.method}] ${endpoints.find(e => e.id === parseInt(selectedEndpointId))?.name}` + : '-- 请选择接口 --' + } + + +
+ {isEndpointDropdownOpen && ( +
+
+ setEndpointSearchQuery(e.target.value)} + onClick={(e) => e.stopPropagation()} + style={{ + width: '100%', + padding: '0.5rem', + border: '1px solid var(--border)', + borderRadius: '4px', + fontSize: '0.875rem' + }} + autoFocus + /> +
+
+ {Object.keys(groupedEndpoints).length === 0 ? ( +
+ 未找到匹配的接口 +
+ ) : ( + Object.entries(groupedEndpoints).map(([module, eps]) => ( +
+
+ {module} +
+ {eps.map(ep => ( +
handleEndpointChange(ep.id)} + style={{ + padding: '0.5rem 1rem', + cursor: 'pointer', + backgroundColor: selectedEndpointId === ep.id ? 'rgba(59, 130, 246, 0.1)' : 'transparent', + borderLeft: selectedEndpointId === ep.id ? '3px solid #3b82f6' : '3px solid transparent' + }} + onMouseEnter={(e) => { + if (selectedEndpointId !== ep.id) { + e.currentTarget.style.backgroundColor = 'rgba(0,0,0,0.05)'; + } + }} + onMouseLeave={(e) => { + if (selectedEndpointId !== ep.id) { + e.currentTarget.style.backgroundColor = 'transparent'; + } + }} + > +
+ + {ep.method} + + {ep.name} +
+
+ ))} +
+ )) + )} +
+
+ )} +
diff --git a/server/import_templates.js b/server/import_templates.js new file mode 100644 index 0000000..a579179 --- /dev/null +++ b/server/import_templates.js @@ -0,0 +1,77 @@ +const XLSX = require('xlsx'); +const pool = require('./db'); +const path = require('path'); + +async function importTemplates() { + try { + const filePath = path.join(__dirname, '../Moka_全量API接口.xlsx'); + console.log('读取文件:', filePath); + + const workbook = XLSX.readFile(filePath); + console.log(`共 ${workbook.SheetNames.length} 个 Sheet\n`); + + let totalSuccess = 0; + let totalError = 0; + + for (const sheetName of workbook.SheetNames) { + console.log(`\n=== 处理 Sheet: ${sheetName} ===`); + const sheet = workbook.Sheets[sheetName]; + const data = XLSX.utils.sheet_to_json(sheet); + console.log(`共 ${data.length} 条记录`); + + let successCount = 0; + let errorCount = 0; + + for (const row of data) { + try { + // Excel 列名映射 + const module = sheetName; // 使用 Sheet 名称作为业务模块 + const name = row['接口名称'] || ''; + const description = row['接口说明'] || null; + const url = row['请求地址'] || ''; + const body = row['请求体 Request Body'] || null; + + // 默认值 + const category = '旗舰版PP'; // 默认分类 + const method = 'POST'; // 默认 POST + const apiCode = null; + const userName = null; + + if (!name || !url) { + console.log(' 跳过无效记录 (缺少名称或URL):', name || '未命名'); + errorCount++; + continue; + } + + await pool.query( + `INSERT INTO endpoint_templates + (name, category, module, api_code, user_name, url, method, body, description) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [name, category, module, apiCode, userName, url, method, body, description] + ); + + successCount++; + console.log(` ✓ ${name}`); + } catch (err) { + errorCount++; + console.error(` ✗ 导入失败:`, err.message); + } + } + + console.log(`${sheetName}: 成功 ${successCount} 条, 失败 ${errorCount} 条`); + totalSuccess += successCount; + totalError += errorCount; + } + + console.log('\n========== 导入完成 =========='); + console.log(`总成功: ${totalSuccess} 条`); + console.log(`总失败: ${totalError} 条`); + + process.exit(0); + } catch (error) { + console.error('导入失败:', error); + process.exit(1); + } +} + +importTemplates(); diff --git a/server/init_admin_endpoints.js b/server/init_admin_endpoints.js new file mode 100644 index 0000000..2686b04 --- /dev/null +++ b/server/init_admin_endpoints.js @@ -0,0 +1,69 @@ +require('dotenv').config(); +const pool = require('./db'); + +async function initAdminEndpoints() { + try { + console.log('开始为 admin 账号初始化接口...\n'); + + // 模块名称到分类的映射 + const moduleToCategory = { + '组织接口API': '组织接口', + '职位职务接口API': '职位职务接口', + '人事接口API': '人事接口', + '假勤接口API': '假勤接口', + '薪酬接口API': '薪酬接口', + '绩效接口API': '绩效接口' + }; + + // 查询所有模板接口 + const [templates] = await pool.query( + 'SELECT * FROM endpoint_templates ORDER BY module, id' + ); + + console.log(`共找到 ${templates.length} 条模板接口\n`); + + let successCount = 0; + let errorCount = 0; + + for (const template of templates) { + try { + const category = moduleToCategory[template.module] || template.module; + + await pool.query( + `INSERT INTO endpoints + (user_id, name, category, module, api_code, user_name, url, method, body, description) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [ + 'admin', + template.name, + '标品PP', // 接口类型固定为标品PP + category, // 使用映射后的分类 + template.api_code, + template.user_name, + template.url, + template.method, + template.body, + template.description + ] + ); + + successCount++; + console.log(`✓ [${category}] ${template.name}`); + } catch (err) { + errorCount++; + console.error(`✗ 导入失败: ${template.name}`, err.message); + } + } + + console.log('\n========== 初始化完成 =========='); + console.log(`成功: ${successCount} 条`); + console.log(`失败: ${errorCount} 条`); + + process.exit(0); + } catch (error) { + console.error('初始化失败:', error); + process.exit(1); + } +} + +initAdminEndpoints(); diff --git a/server/package-lock.json b/server/package-lock.json index 183de09..d405238 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -16,7 +16,8 @@ "dotenv": "^17.3.1", "express": "^5.2.1", "jsonwebtoken": "^9.0.3", - "mysql2": "^3.19.0" + "mysql2": "^3.19.0", + "xlsx": "^0.18.5" }, "devDependencies": { "nodemon": "^3.1.14" @@ -45,6 +46,15 @@ "node": ">= 0.6" } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -211,6 +221,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -236,6 +259,15 @@ "fsevents": "~2.3.2" } }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -305,6 +337,18 @@ "url": "https://opencollective.com/express" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -602,6 +646,15 @@ "node": ">= 0.6" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fresh": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", @@ -1493,6 +1546,18 @@ "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" } }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -1593,11 +1658,50 @@ "node": ">= 0.8" } }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" + }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } } } } diff --git a/server/package.json b/server/package.json index 51e50d8..5b51171 100644 --- a/server/package.json +++ b/server/package.json @@ -7,7 +7,8 @@ "dotenv": "^17.3.1", "express": "^5.2.1", "jsonwebtoken": "^9.0.3", - "mysql2": "^3.19.0" + "mysql2": "^3.19.0", + "xlsx": "^0.18.5" }, "name": "server", "version": "1.0.0",