.DS_Store | |||||
node_modules/ | |||||
unpackage/ | |||||
dist/ | |||||
# local env files | |||||
.env.local | |||||
.env.*.local | |||||
# Log files | |||||
npm-debug.log* | |||||
yarn-debug.log* | |||||
yarn-error.log* | |||||
# Editor directories and files | |||||
.project | |||||
.idea | |||||
.vscode | |||||
*.suo | |||||
*.ntvs* | |||||
*.njsproj | |||||
*.sln | |||||
*.sw* | |||||
.gitattributes | |||||
*.log |
# 考勤工具 | |||||
这是一个基于Electron和Playwright的考勤打卡工具,可以帮助无法使用老版IE的用户实现上下班打卡功能。 | |||||
## 安装包 | |||||
`Attendance Tool Setup 1.0.0.exe` | |||||
## 功能特点 | |||||
- 账号密码保存功能 | |||||
- 自动登录考勤系统 | |||||
- 一键上班打卡 | |||||
- 一键下班打卡 | |||||
- 实时显示打卡状态 | |||||
- 定时刷新考勤信息 | |||||
## 技术栈 | |||||
- Electron: 跨平台桌面应用开发框架 | |||||
- Playwright: 用于浏览器自动化操作 | |||||
- Node.js: JavaScript运行环境 | |||||
## 项目结构 | |||||
``` | |||||
attendance-tool/ | |||||
├── main.js // Electron主进程 | |||||
├── renderer.js // 渲染进程,处理UI交互和打卡操作 | |||||
├── index.html // 应用界面 | |||||
├── icon.ico // 应用图标 | |||||
├── package.json // 项目配置和依赖 | |||||
└── electron-builder.json // 打包配置 | |||||
``` | |||||
## 安装与运行 | |||||
### 开发环境 | |||||
1. 克隆项目到本地 | |||||
2. 安装依赖 | |||||
``` | |||||
npm install | |||||
``` | |||||
3. 运行应用 | |||||
``` | |||||
npm start | |||||
``` | |||||
### 打包应用 | |||||
打包为便携版本: | |||||
``` | |||||
npm run dist | |||||
``` | |||||
打包为安装版本: | |||||
``` | |||||
npm run dist:installer | |||||
``` | |||||
## 使用说明 | |||||
1. 启动应用后,在登录界面输入您的考勤系统用户名和密码 | |||||
2. 如需保存账号密码,勾选"记住账号密码"选项 | |||||
3. 点击"登录"按钮进入系统 | |||||
4. 登录成功后,可以看到您的姓名和当前打卡状态 | |||||
5. 根据需要,点击"上班打卡"或"下班打卡"按钮完成打卡操作 | |||||
6. 系统会自动更新打卡状态 | |||||
## 注意事项 | |||||
- 此工具仅适用于特定考勤系统(http://hy.zhushitrade.cn/groupware) | |||||
- 请确保网络连接正常 | |||||
- 打卡成功后,按钮会自动禁用,防止重复打卡 | |||||
## 环境要求 | |||||
- Windows 系统 | |||||
- Node.js 14.0 或以上 | |||||
## 许可证 | |||||
ISC |
{ | |||||
"appId": "com.attendance.tool", | |||||
"productName": "Attendance Tool", | |||||
"directories": { | |||||
"output": "dist" | |||||
}, | |||||
"files": [ | |||||
"**/*", | |||||
"!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme}", | |||||
"!**/node_modules/*/{test,__tests__,tests,powered-test,example,examples}", | |||||
"!**/node_modules/*.d.ts", | |||||
"!**/node_modules/.bin", | |||||
"!**/*.{iml,o,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,xproj}", | |||||
"!.editorconfig", | |||||
"!**/._*", | |||||
"!**/{.DS_Store,.git,.hg,.svn,CVS,RCS,SCCS,.gitignore,.gitattributes}", | |||||
"!**/{__pycache__,thumbs.db,.flowconfig,.idea,.vs,.nyc_output}", | |||||
"!**/{appveyor.yml,.travis.yml,circle.yml}", | |||||
"!**/{npm-debug.log,yarn.lock,.yarn-integrity,.yarn-metadata.json}" | |||||
], | |||||
"extraResources": [ | |||||
{ | |||||
"from": "C:/Users/WIN/AppData/Local/ms-playwright/chromium-1155", | |||||
"to": "ms-playwright/chromium-1155" | |||||
}, | |||||
{ | |||||
"from": "C:/Users/WIN/AppData/Local/ms-playwright/chromium_headless_shell-1155", | |||||
"to": "ms-playwright/chromium_headless_shell-1155" | |||||
}, | |||||
{ | |||||
"from": "C:/Users/WIN/AppData/Local/ms-playwright/ffmpeg-1011", | |||||
"to": "ms-playwright/ffmpeg-1011" | |||||
} | |||||
], | |||||
"win": { | |||||
"target": "portable", | |||||
"icon": "icon.ico", | |||||
"signingHashAlgorithms": null, | |||||
"signAndEditExecutable": false | |||||
}, | |||||
"asar": true, | |||||
"forceCodeSigning": false, | |||||
"compression": "maximum" | |||||
} |
<!DOCTYPE html> | |||||
<html> | |||||
<head> | |||||
<title>考勤工具</title> | |||||
<style> | |||||
body { | |||||
font-family: Arial, sans-serif; | |||||
padding: 20px; | |||||
background-color: #f0f2f5; | |||||
} | |||||
.container { | |||||
max-width: 400px; | |||||
margin: 0 auto; | |||||
background: white; | |||||
padding: 20px; | |||||
border-radius: 8px; | |||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |||||
} | |||||
.form-group { | |||||
margin-bottom: 15px; | |||||
} | |||||
label { | |||||
display: block; | |||||
margin-bottom: 5px; | |||||
} | |||||
input[type="text"], | |||||
input[type="password"] { | |||||
width: 100%; | |||||
padding: 8px; | |||||
border: 1px solid #ddd; | |||||
border-radius: 4px; | |||||
box-sizing: border-box; | |||||
} | |||||
.checkbox-group { | |||||
margin: 10px 0; | |||||
} | |||||
button { | |||||
background-color: #1890ff; | |||||
color: white; | |||||
padding: 10px 15px; | |||||
border: none; | |||||
border-radius: 4px; | |||||
cursor: pointer; | |||||
width: 100%; | |||||
margin-bottom: 10px; | |||||
} | |||||
button:hover { | |||||
background-color: #40a9ff; | |||||
} | |||||
.attendance-buttons { | |||||
display: none; | |||||
margin-top: 20px; | |||||
} | |||||
#checkInBtn { | |||||
background-color: #52c41a; | |||||
} | |||||
#checkOutBtn { | |||||
background-color: #f5222d; | |||||
} | |||||
.status { | |||||
margin-top: 10px; | |||||
text-align: center; | |||||
color: #666; | |||||
} | |||||
.user-info { | |||||
display: none; | |||||
margin: 15px 0; | |||||
padding: 10px; | |||||
background-color: #f8f9fa; | |||||
border-radius: 4px; | |||||
} | |||||
.user-info p { | |||||
margin: 5px 0; | |||||
color: #333; | |||||
} | |||||
.attendance-status { | |||||
margin-top: 10px; | |||||
font-size: 0.9em; | |||||
color: #666; | |||||
} | |||||
.time-info { | |||||
color: #1890ff; | |||||
font-weight: bold; | |||||
} | |||||
</style> | |||||
</head> | |||||
<body> | |||||
<div class="container"> | |||||
<h2>考勤系统</h2> | |||||
<div id="loginForm"> | |||||
<div class="form-group"> | |||||
<label for="username">用户名:</label> | |||||
<input type="text" id="username" required> | |||||
</div> | |||||
<div class="form-group"> | |||||
<label for="password">密码:</label> | |||||
<input type="password" id="password" required> | |||||
</div> | |||||
<div class="checkbox-group"> | |||||
<input type="checkbox" id="remember"> | |||||
<label for="remember">记住账号密码</label> | |||||
</div> | |||||
<button id="loginBtn">登录</button> | |||||
</div> | |||||
<div id="userInfo" class="user-info"> | |||||
<p>欢迎,<span id="userName">-</span></p> | |||||
<div class="attendance-status"> | |||||
<p>上班打卡时间: <span id="checkInTime" class="time-info">-</span></p> | |||||
<p>下班打卡时间: <span id="checkOutTime" class="time-info">-</span></p> | |||||
</div> | |||||
</div> | |||||
<div id="attendanceButtons" class="attendance-buttons"> | |||||
<button id="checkInBtn">上班打卡</button> | |||||
<button id="checkOutBtn">下班打卡</button> | |||||
</div> | |||||
<div id="status" class="status"></div> | |||||
</div> | |||||
<script src="renderer.js"></script> | |||||
</body> | |||||
</html> |
const { app, BrowserWindow, ipcMain } = require('electron'); | |||||
const path = require('path'); | |||||
const Store = require('electron-store'); | |||||
const store = new Store(); | |||||
// 设置 Playwright 浏览器路径 | |||||
if (app.isPackaged) { | |||||
process.env.PLAYWRIGHT_BROWSERS_PATH = path.join(process.resourcesPath, 'ms-playwright'); | |||||
} else { | |||||
process.env.PLAYWRIGHT_BROWSERS_PATH = path.join(process.env.LOCALAPPDATA, 'ms-playwright'); | |||||
} | |||||
let mainWindow; | |||||
function createWindow() { | |||||
mainWindow = new BrowserWindow({ | |||||
width: 800, | |||||
height: 600, | |||||
webPreferences: { | |||||
nodeIntegration: true, | |||||
contextIsolation: false | |||||
} | |||||
}); | |||||
mainWindow.loadFile('index.html'); | |||||
} | |||||
// 处理 IPC 消息 | |||||
ipcMain.handle('get-credentials', async () => { | |||||
return store.get('credentials'); | |||||
}); | |||||
ipcMain.handle('save-credentials', async (event, credentials) => { | |||||
store.set('credentials', credentials); | |||||
}); | |||||
app.whenReady().then(createWindow); | |||||
app.on('window-all-closed', () => { | |||||
if (process.platform !== 'darwin') { | |||||
app.quit(); | |||||
} | |||||
}); | |||||
app.on('activate', () => { | |||||
if (BrowserWindow.getAllWindows().length === 0) { | |||||
createWindow(); | |||||
} | |||||
}); |
{ | |||||
"name": "attendance-tool", | |||||
"version": "1.0.0", | |||||
"author": "Lizhuang", | |||||
"description": "考勤自动化工具", | |||||
"main": "main.js", | |||||
"scripts": { | |||||
"start": "electron .", | |||||
"postinstall": "playwright install chromium", | |||||
"pack": "electron-builder --dir", | |||||
"dist": "electron-builder --win portable --config electron-builder.json", | |||||
"dist:installer": "electron-builder --win nsis --config electron-builder.json", | |||||
"package": "electron-packager . AttendanceTool --platform=win32 --arch=x64 --icon=icon.ico --out=dist" | |||||
}, | |||||
"build": { | |||||
"appId": "com.attendance.tool", | |||||
"productName": "考勤工具", | |||||
"directories": { | |||||
"output": "dist" | |||||
}, | |||||
"win": { | |||||
"target": [ | |||||
{ | |||||
"target": "portable", | |||||
"arch": ["x64"] | |||||
}, | |||||
{ | |||||
"target": "nsis", | |||||
"arch": ["x64"] | |||||
} | |||||
], | |||||
"icon": "icon.ico" | |||||
}, | |||||
"nsis": { | |||||
"oneClick": false, | |||||
"allowToChangeInstallationDirectory": true, | |||||
"createDesktopShortcut": true, | |||||
"createStartMenuShortcut": true, | |||||
"shortcutName": "考勤工具", | |||||
"installerIcon": "icon.ico", | |||||
"uninstallerIcon": "icon.ico", | |||||
"installerHeaderIcon": "icon.ico", | |||||
"language": "2052" | |||||
}, | |||||
"asar": false, | |||||
"files": [ | |||||
"**/*", | |||||
"!**/node_modules/*/{test,__tests__,tests,powered-test,example,examples}", | |||||
"!**/node_modules/*.d.ts", | |||||
"!**/node_modules/.bin", | |||||
"!**/*.{iml,o,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,xproj}", | |||||
"!.editorconfig", | |||||
"!**/._*", | |||||
"!**/{.DS_Store,.git,.hg,.svn,CVS,RCS,SCCS,__pycache__,thumbs.db,.gitignore,.gitattributes}", | |||||
"!**/{__pycache__,*.py[cod],*.egg,*.egg-info,*.spec}", | |||||
"!**/{.env,.env.*,.venv,.venv.*}", | |||||
"!**/{bower_components,vendor}", | |||||
"!**/{.babelrc,.eslintrc,.eslintignore,.eslintcache,*.config.js}" | |||||
] | |||||
}, | |||||
"keywords": [], | |||||
"license": "ISC", | |||||
"dependencies": { | |||||
"electron-store": "^8.1.0", | |||||
"playwright": "^1.41.2" | |||||
}, | |||||
"devDependencies": { | |||||
"electron": "^28.1.0", | |||||
"electron-builder": "^24.9.1", | |||||
"electron-packager": "^17.1.2" | |||||
} | |||||
} |
const { ipcRenderer } = require('electron'); | |||||
const { chromium } = require('playwright'); | |||||
let browser = null; | |||||
let page = null; | |||||
const loginForm = document.getElementById('loginForm'); | |||||
const attendanceButtons = document.getElementById('attendanceButtons'); | |||||
const userInfo = document.getElementById('userInfo'); | |||||
const statusDiv = document.getElementById('status'); | |||||
const usernameInput = document.getElementById('username'); | |||||
const passwordInput = document.getElementById('password'); | |||||
const rememberCheckbox = document.getElementById('remember'); | |||||
// User info elements | |||||
const userNameSpan = document.getElementById('userName'); | |||||
const checkInTimeSpan = document.getElementById('checkInTime'); | |||||
const checkOutTimeSpan = document.getElementById('checkOutTime'); | |||||
// Load saved credentials | |||||
async function loadSavedCredentials() { | |||||
const credentials = await ipcRenderer.invoke('get-credentials'); | |||||
if (credentials) { | |||||
usernameInput.value = credentials.username || ''; | |||||
passwordInput.value = credentials.password || ''; | |||||
rememberCheckbox.checked = true; | |||||
} | |||||
} | |||||
loadSavedCredentials(); | |||||
async function initBrowser() { | |||||
if (!browser) { | |||||
browser = await chromium.launch({ | |||||
headless: true, // 使用无头模式 | |||||
slowMo: 50 // 保留较小的延迟以确保稳定性 | |||||
}); | |||||
} | |||||
if (!page) { | |||||
page = await browser.newPage(); | |||||
await page.setDefaultTimeout(60000); | |||||
await page.setDefaultNavigationTimeout(60000); | |||||
} | |||||
} | |||||
async function updateAttendanceStatus() { | |||||
try { | |||||
const userInfo = await page.evaluate(() => { | |||||
const rows = document.querySelectorAll('#Table1 tbody tr'); | |||||
if (rows.length > 1) { | |||||
const row = rows[1]; | |||||
return { | |||||
name: row.children[1].textContent.trim(), | |||||
checkInTime: row.children[2].children[0].children[0].children[0].children[2].children[1].textContent.trim(), | |||||
checkOutTime: row.children[2].children[0].children[0].children[0].children[3].children[1].textContent.trim() | |||||
}; | |||||
} | |||||
return null; | |||||
}); | |||||
if (userInfo) { | |||||
userNameSpan.textContent = userInfo.name; | |||||
checkInTimeSpan.textContent = userInfo.checkInTime; | |||||
checkOutTimeSpan.textContent = userInfo.checkOutTime; | |||||
// 更新按钮状态 | |||||
const checkInBtn = document.getElementById('checkInBtn'); | |||||
const checkOutBtn = document.getElementById('checkOutBtn'); | |||||
if (userInfo.checkInTime === '00:00' || userInfo.checkInTime === '') { | |||||
checkInBtn.disabled = false; | |||||
checkInBtn.textContent = '上班打卡'; | |||||
} else { | |||||
checkInBtn.disabled = true; | |||||
checkInBtn.textContent = '已打卡'; | |||||
} | |||||
if (userInfo.checkOutTime === '00:00' && userInfo.checkInTime !== '00:00') { | |||||
checkOutBtn.disabled = false; | |||||
checkOutBtn.textContent = '下班打卡'; | |||||
} else { | |||||
checkOutBtn.disabled = true; | |||||
checkOutBtn.textContent = userInfo.checkOutTime === '00:00' ? '未到下班时间' : '已打卡'; | |||||
} | |||||
} | |||||
} catch (error) { | |||||
console.error('获取考勤状态失败:', error); | |||||
} | |||||
} | |||||
async function login() { | |||||
try { | |||||
await initBrowser(); | |||||
statusDiv.textContent = '正在登录...'; | |||||
// 导航到登录页面 | |||||
await page.goto('http://hy.zhushitrade.cn/groupware/index.asp', { | |||||
waitUntil: 'networkidle', | |||||
timeout: 60000 | |||||
}); | |||||
// 等待输入框出现 | |||||
await page.waitForSelector('#userId', { timeout: 60000 }); | |||||
await page.waitForSelector('#userPassword', { timeout: 60000 }); | |||||
// 清除现有输入 | |||||
await page.evaluate(() => { | |||||
document.getElementById('userId').value = ''; | |||||
document.getElementById('userPassword').value = ''; | |||||
}); | |||||
// 输入凭据 | |||||
await page.fill('#userId', usernameInput.value); | |||||
await page.fill('#userPassword', passwordInput.value); | |||||
if (rememberCheckbox.checked) { | |||||
await ipcRenderer.invoke('save-credentials', { | |||||
username: usernameInput.value, | |||||
password: passwordInput.value | |||||
}); | |||||
} | |||||
// 点击登录并等待响应 | |||||
const navigationPromise = page.waitForNavigation({ | |||||
waitUntil: 'networkidle', | |||||
timeout: 60000 | |||||
}); | |||||
await page.click('#btn_login'); | |||||
await navigationPromise; | |||||
// 验证登录是否成功 | |||||
const currentUrl = page.url(); | |||||
if (currentUrl.includes('index.asp')) { | |||||
statusDiv.textContent = '登录失败: 可能是用户名或密码错误'; | |||||
return; | |||||
} | |||||
// 更新用户信息和考勤状态 | |||||
await updateAttendanceStatus(); | |||||
statusDiv.textContent = '登录成功'; | |||||
loginForm.style.display = 'none'; | |||||
userInfo.style.display = 'block'; | |||||
attendanceButtons.style.display = 'block'; | |||||
} catch (error) { | |||||
console.error('登录错误:', error); | |||||
statusDiv.textContent = '登录失败: ' + (error.message || '网络连接问题,请检查网络后重试'); | |||||
// 如果是超时错误,尝试重新初始化浏览器 | |||||
if (error.name === 'TimeoutError') { | |||||
try { | |||||
if (browser) { | |||||
await browser.close(); | |||||
} | |||||
browser = null; | |||||
page = null; | |||||
} catch (closeError) { | |||||
console.error('关闭浏览器失败:', closeError); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
async function checkIn() { | |||||
try { | |||||
if (!page) { | |||||
statusDiv.textContent = '请先登录'; | |||||
return; | |||||
} | |||||
statusDiv.textContent = '正在打卡...'; | |||||
await page.goto('http://hy.zhushitrade.cn/groupware/editCheckWork.asp?action=onDuty', { | |||||
waitUntil: 'networkidle', | |||||
timeout: 60000 | |||||
}); | |||||
await page.waitForTimeout(2000); | |||||
await updateAttendanceStatus(); | |||||
statusDiv.textContent = '上班打卡成功'; | |||||
} catch (error) { | |||||
console.error('打卡错误:', error); | |||||
statusDiv.textContent = '打卡失败: ' + (error.message || '请检查网络连接'); | |||||
} | |||||
} | |||||
async function checkOut() { | |||||
try { | |||||
if (!page) { | |||||
statusDiv.textContent = '请先登录'; | |||||
return; | |||||
} | |||||
statusDiv.textContent = '正在打卡...'; | |||||
await page.goto('http://hy.zhushitrade.cn/groupware/editcheckwork.asp?action=offDuty', { | |||||
waitUntil: 'networkidle', | |||||
timeout: 60000 | |||||
}); | |||||
await page.waitForTimeout(2000); | |||||
await updateAttendanceStatus(); | |||||
statusDiv.textContent = '下班打卡成功'; | |||||
} catch (error) { | |||||
console.error('打卡错误:', error); | |||||
statusDiv.textContent = '打卡失败: ' + (error.message || '请检查网络连接'); | |||||
} | |||||
} | |||||
// 定时更新考勤状态 | |||||
setInterval(async () => { | |||||
if (page && userInfo.style.display === 'block') { | |||||
await updateAttendanceStatus(); | |||||
} | |||||
}, 60000); // 每分钟更新一次 | |||||
// Event Listeners | |||||
document.getElementById('loginBtn').addEventListener('click', login); | |||||
document.getElementById('checkInBtn').addEventListener('click', checkIn); | |||||
document.getElementById('checkOutBtn').addEventListener('click', checkOut); | |||||
// Cleanup on window close | |||||
window.addEventListener('beforeunload', async () => { | |||||
if (browser) { | |||||
await browser.close(); | |||||
} | |||||
}); |