Bladeren bron

first commit

master
lizhuang 2 weken geleden
commit
9190b7dfb3
9 gewijzigde bestanden met toevoegingen van 5636 en 0 verwijderingen
  1. 26
    0
      .gitignore
  2. 83
    0
      README.md
  3. 44
    0
      electron-builder.json
  4. BIN
      icon.ico
  5. 124
    0
      index.html
  6. 49
    0
      main.js
  7. 5014
    0
      package-lock.json
  8. 72
    0
      package.json
  9. 224
    0
      renderer.js

+ 26
- 0
.gitignore Bestand weergeven

@@ -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

+ 83
- 0
README.md Bestand weergeven

@@ -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

+ 44
- 0
electron-builder.json Bestand weergeven

@@ -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"
}

BIN
icon.ico Bestand weergeven


+ 124
- 0
index.html Bestand weergeven

@@ -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>

+ 49
- 0
main.js Bestand weergeven

@@ -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();
}
});

+ 5014
- 0
package-lock.json
Diff onderdrukt omdat het te groot bestand
Bestand weergeven


+ 72
- 0
package.json Bestand weergeven

@@ -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"
}
}

+ 224
- 0
renderer.js Bestand weergeven

@@ -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();
}
});

Laden…
Annuleren
Opslaan