feat: 接口选择改为支持搜索的下拉组件,按模块分组显示
This commit is contained in:
BIN
Moka_全量API接口.xlsx
Normal file
BIN
Moka_全量API接口.xlsx
Normal file
Binary file not shown.
@@ -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 (
|
||||
<div style={{ width: '100%', maxWidth: '1600px', margin: '0 auto', height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<header style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem' }}>
|
||||
@@ -470,12 +506,130 @@ const ApiDebugger = ({ onTitleChange, storageKey = 'tab_default' }) => {
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>选择接口</label>
|
||||
<select value={selectedEndpointId} onChange={handleEndpointChange}>
|
||||
<option value="">-- 请选择接口 --</option>
|
||||
{filteredEndpoints
|
||||
.map(ep => <option key={ep.id} value={ep.id}>[{ep.method}] {ep.name}</option>)
|
||||
}
|
||||
</select>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div
|
||||
onClick={() => 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'
|
||||
}}
|
||||
>
|
||||
<span style={{ color: selectedEndpointId ? 'inherit' : '#999' }}>
|
||||
{selectedEndpointId
|
||||
? `[${endpoints.find(e => e.id === parseInt(selectedEndpointId))?.method}] ${endpoints.find(e => e.id === parseInt(selectedEndpointId))?.name}`
|
||||
: '-- 请选择接口 --'
|
||||
}
|
||||
</span>
|
||||
<span style={{ fontSize: '0.75rem' }}>▼</span>
|
||||
</div>
|
||||
{isEndpointDropdownOpen && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '100%',
|
||||
left: 0,
|
||||
right: 0,
|
||||
marginTop: '4px',
|
||||
backgroundColor: 'var(--bg)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: '4px',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
|
||||
zIndex: 1000,
|
||||
maxHeight: '400px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}
|
||||
>
|
||||
<div style={{ padding: '0.5rem', borderBottom: '1px solid var(--border)', backgroundColor: 'var(--bg)' }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索接口名称、方法或分类..."
|
||||
value={endpointSearchQuery}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
</div>
|
||||
<div style={{ overflowY: 'auto', maxHeight: '340px' }}>
|
||||
{Object.keys(groupedEndpoints).length === 0 ? (
|
||||
<div style={{ padding: '1rem', textAlign: 'center', color: '#999' }}>
|
||||
未找到匹配的接口
|
||||
</div>
|
||||
) : (
|
||||
Object.entries(groupedEndpoints).map(([module, eps]) => (
|
||||
<div key={module}>
|
||||
<div style={{
|
||||
padding: '0.5rem',
|
||||
backgroundColor: 'var(--bg)',
|
||||
fontWeight: 'bold',
|
||||
fontSize: '0.875rem',
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 2,
|
||||
borderBottom: '1px solid var(--border)'
|
||||
}}>
|
||||
{module}
|
||||
</div>
|
||||
{eps.map(ep => (
|
||||
<div
|
||||
key={ep.id}
|
||||
onClick={() => 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';
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: '0.875rem' }}>
|
||||
<span style={{
|
||||
display: 'inline-block',
|
||||
padding: '2px 6px',
|
||||
backgroundColor: ep.method === 'POST' ? '#10b981' : ep.method === 'GET' ? '#3b82f6' : '#f59e0b',
|
||||
color: 'white',
|
||||
borderRadius: '3px',
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: 'bold',
|
||||
marginRight: '0.5rem',
|
||||
minWidth: '45px',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
{ep.method}
|
||||
</span>
|
||||
{ep.name}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>接口地址 (URL)</label>
|
||||
|
||||
77
server/import_templates.js
Normal file
77
server/import_templates.js
Normal file
@@ -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();
|
||||
69
server/init_admin_endpoints.js
Normal file
69
server/init_admin_endpoints.js
Normal file
@@ -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();
|
||||
106
server/package-lock.json
generated
106
server/package-lock.json
generated
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user