@@ -0,0 +1,26 @@ | |||
.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 |
@@ -0,0 +1,83 @@ | |||
# 考勤工具 | |||
这是一个基于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 |
@@ -0,0 +1,44 @@ | |||
{ | |||
"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" | |||
} |
@@ -0,0 +1,124 @@ | |||
<!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> |
@@ -0,0 +1,49 @@ | |||
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(); | |||
} | |||
}); |
@@ -0,0 +1,72 @@ | |||
{ | |||
"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" | |||
} | |||
} |
@@ -0,0 +1,224 @@ | |||
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(); | |||
} | |||
}); |