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",