Files
clean-chrome/popup.js

542 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
document.addEventListener('DOMContentLoaded', () => {
const addBtn = document.getElementById('add-btn');
const addForm = document.getElementById('add-form');
const cancelBtn = document.getElementById('cancel-btn');
const saveBtn = document.getElementById('save-btn');
const envSelect = document.getElementById('env-select');
const clientInput = document.getElementById('client-input');
const usernameInput = document.getElementById('username-input');
const passwordInput = document.getElementById('password-input');
const rulesList = document.getElementById('rules-list');
const rulesContainer = document.querySelector('.rules-container');
const formTitle = document.getElementById('form-title');
const editingIdInput = document.getElementById('editing-id');
const loginView = document.getElementById('login-view');
const registerView = document.getElementById('register-view');
const appView = document.getElementById('app-view');
const authLoginBtn = document.getElementById('auth-login-btn');
const authRegBtn = document.getElementById('auth-reg-btn');
const goToRegister = document.getElementById('go-to-register');
const goToLogin = document.getElementById('go-to-login');
const logoutBtn = document.getElementById('logout-btn');
const archiveToggle = document.getElementById('archive-toggle');
const backToMain = document.getElementById('back-to-main');
const mainViewContent = document.getElementById('main-view-content');
const archiveViewContent = document.getElementById('archive-view-content');
const archiveList = document.getElementById('archive-list');
if (logoutBtn) {
logoutBtn.addEventListener('click', (e) => {
e.preventDefault();
if (confirm('确定要退出当前同步账号吗?')) {
chrome.storage.local.remove(['quickPurgeUser'], () => {
currentUser = null;
rules = [];
renderRules(); // 立即清空界面显示的 rules
showView('login');
});
}
});
}
const ENV_CONFIG = {
DD_PP: {
domain: 'app135148.dingtalkoxm.com',
url: 'https://app135148.dingtalkoxm.com/user/login'
},
DD_ATS: {
domain: 'app135149.dingtalkoxm.com',
url: 'https://app135149.dingtalkoxm.com/login'
},
STD_PP: {
domain: 'core.mokahr.com',
url: 'https://core.mokahr.com'
},
STD_ATS: {
domain: 'app.mokahr.com',
url: 'https://app.mokahr.com'
}
};
const API_BASE = 'https://sqd.zhouzishen.cn/api'; // 已更新为您的生产服务器地址
let currentUser = null;
let rules = [];
let dragStartIndex = -1;
// --- 工具函数 ---
function hasChinese(str) {
return /[\u4E00-\u9FA5]/.test(str);
}
const ICON_SET = [
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>', // User
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="2" width="16" height="20" rx="2" ry="2"/><line x1="9" y1="22" x2="9" y2="22"/><line x1="15" y1="22" x2="15" y2="22"/><line x1="12" y1="18" x2="12" y2="18"/><line x1="9" y1="14" x2="9" y2="14"/><line x1="15" y1="14" x2="15" y2="14"/><line x1="9" y1="10" x2="9" y2="10"/><line x1="15" y1="10" x2="15" y2="10"/><line x1="9" y1="6" x2="9" y2="6"/><line x1="15" y1="6" x2="15" y2="6"/></svg>', // Building
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>', // Shield
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3m-3-3l-2.5-2.5"/></svg>', // Key
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>', // Globe
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="7" width="20" height="14" rx="2" ry="2"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/></svg>', // Briefcase
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>', // Zap
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg>' // Database
];
function getRandomIcon(seed) {
// Use a simple hash of the seed (rule ID) to pick a stable icon
let hash = 0;
for (let i = 0; i < seed.length; i++) {
hash = seed.charCodeAt(i) + ((hash << 5) - hash);
}
return ICON_SET[Math.abs(hash) % ICON_SET.length];
}
// --- Auth View Controllers ---
function showView(viewName) {
if (loginView) loginView.classList.add('hidden');
if (registerView) registerView.classList.add('hidden');
if (appView) appView.classList.add('hidden');
if (viewName === 'app') {
if (appView) appView.classList.remove('hidden');
} else {
if (viewName === 'login' && loginView) loginView.classList.remove('hidden');
else if (viewName === 'register' && registerView) registerView.classList.remove('hidden');
}
}
if (goToRegister) goToRegister.addEventListener('click', (e) => { e.preventDefault(); showView('register'); });
if (goToLogin) goToLogin.addEventListener('click', (e) => { e.preventDefault(); showView('login'); });
if (authRegBtn) {
authRegBtn.addEventListener('click', async () => {
const username = document.getElementById('auth-reg-username').value.trim();
const password = document.getElementById('auth-reg-password').value;
const inviteCode = document.getElementById('auth-reg-invite').value.trim();
if (!username || !password || !inviteCode) return alert('请填写完整信息');
if (hasChinese(username) || hasChinese(password) || hasChinese(inviteCode)) {
return alert('账号、密码和邀请码不允许包含中文字符');
}
authRegBtn.textContent = '注册中...';
try {
const res = await fetch(`${API_BASE}/register`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password, inviteCode })
});
const data = await res.json();
if (data.code === 0) {
alert('注册成功,请使用该账号登录');
document.getElementById('auth-login-username').value = username;
document.getElementById('auth-login-password').value = password;
showView('login');
} else alert(data.msg);
} catch (e) { alert('请求失败,请确保本地服务器已启动运行。' + e); }
finally { authRegBtn.textContent = '注册'; }
});
}
if (authLoginBtn) {
authLoginBtn.addEventListener('click', async () => {
const username = document.getElementById('auth-login-username').value.trim();
const password = document.getElementById('auth-login-password').value;
if (!username || !password) return alert('请输入账号和密码');
if (hasChinese(username) || hasChinese(password)) {
return alert('账号和密码格式不正确(不能包含中文)');
}
authLoginBtn.textContent = '登录中...';
try {
const res = await fetch(`${API_BASE}/login`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await res.json();
if (data.code === 0) {
currentUser = { username, password };
rules = processLoadedRules(data.data.rules || []);
chrome.storage.local.set({ quickPurgeUser: { username, password, rules } }, () => {
showView('app');
renderRules();
});
} else alert(data.msg);
} catch (e) { alert('请求失败,请确保本地服务器已启动运行。' + e); }
finally { authLoginBtn.textContent = '登录'; }
});
}
// Load Init from local cache
function loadAndInitBaseApp() {
if (typeof chrome !== 'undefined' && chrome.storage) {
chrome.storage.local.get(['quickPurgeUser'], async (result) => {
const user = result.quickPurgeUser;
if (user && user.username && user.password) {
currentUser = user;
rules = processLoadedRules(user.rules || []);
showView('app'); // Proceed to app UI
renderRules();
// 静默发起登录获取云端最新数据
try {
const res = await fetch(`${API_BASE}/login`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: user.username, password: user.password })
});
const data = await res.json();
if (data.code === 0) {
rules = processLoadedRules(data.data.rules || []);
chrome.storage.local.set({ quickPurgeUser: { ...currentUser, rules } });
renderRules();
}
} catch (e) {
console.log("Offline mode, using local storage cache fallback.");
}
} else {
showView('login'); // Not logged in
}
});
} else {
showView('login');
}
}
function processLoadedRules(rawRules) {
return rawRules.map(rule => {
if (rule.env === 'PP') rule.env = 'DD_PP';
if (rule.env === 'ATS') rule.env = 'DD_ATS';
return rule;
});
}
function saveRules() {
if (typeof chrome !== 'undefined' && chrome.storage && currentUser) {
chrome.storage.local.set({ quickPurgeUser: { ...currentUser, rules } }, () => {
renderRules();
// 异步防抖提交到自建服务云端
fetch(`${API_BASE}/sync`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: currentUser.username, rules })
}).catch(e => console.error("Sync error:", e));
});
} else {
renderRules();
}
}
function toggleAddForm(mode = 'add', ruleId = null) {
if (mode === 'edit') {
const rule = rules.find(r => r.id === ruleId);
if (rule) {
formTitle.textContent = '编辑配置';
editingIdInput.value = rule.id;
envSelect.value = rule.env;
clientInput.value = rule.clientName;
usernameInput.value = rule.username;
passwordInput.value = rule.password;
addForm.classList.remove('hidden');
rulesContainer.classList.add('hidden');
addBtn.classList.add('hidden'); // Optional: hide add btn while editing
}
} else {
formTitle.textContent = '添加配置';
editingIdInput.value = '';
resetForm();
addForm.classList.toggle('hidden');
if (addForm.classList.contains('hidden')) {
rulesContainer.classList.remove('hidden');
addBtn.classList.remove('hidden');
} else {
rulesContainer.classList.add('hidden');
addBtn.classList.add('hidden');
}
}
if (!addForm.classList.contains('hidden')) {
clientInput.focus();
}
}
addBtn.addEventListener('click', () => toggleAddForm('add'));
cancelBtn.addEventListener('click', () => {
addForm.classList.add('hidden');
rulesContainer.classList.remove('hidden');
addBtn.classList.remove('hidden');
resetForm();
});
function resetForm() {
editingIdInput.value = '';
envSelect.value = 'DD_PP';
clientInput.value = '';
usernameInput.value = '';
passwordInput.value = '';
}
saveBtn.addEventListener('click', () => {
const env = envSelect.value;
const clientName = clientInput.value.trim();
const username = usernameInput.value.trim();
const password = passwordInput.value;
const editingId = editingIdInput.value;
if (!clientName || !username || !password) {
alert('请填写完整的客户名称、账号和密码!');
return;
}
if (editingId) {
// Update existing rule
const index = rules.findIndex(r => r.id === editingId);
if (index !== -1) {
rules[index] = {
...rules[index],
env: env,
clientName: clientName,
username: username,
password: password
};
}
} else {
// Add new rule
rules.push({
id: Date.now().toString(),
env: env,
clientName: clientName,
username: username,
password: password
});
}
saveRules();
addForm.classList.add('hidden');
rulesContainer.classList.remove('hidden');
addBtn.classList.remove('hidden');
resetForm();
});
function renderRules() {
rulesList.innerHTML = '';
const activeRules = rules.filter(r => !r.archived);
if (activeRules.length === 0) {
rulesList.innerHTML = `
<div class="empty-state">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<line x1="9" y1="9" x2="15" y2="15"></line>
<line x1="15" y1="9" x2="9" y2="15"></line>
</svg>
<span>暂无账号配置,点击上方 "+" 添加</span>
</div>`;
return;
}
activeRules.forEach((rule, index) => {
const li = document.createElement('li');
li.className = 'rule-item';
li.setAttribute('draggable', 'true');
li.setAttribute('data-index', index.toString());
li.innerHTML = `
<div class="drag-handle" title="拖拽排序">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M8 9h8M8 15h8" /></svg>
</div>
<div class="rule-icon-box">${getRandomIcon(rule.id)}</div>
<div class="rule-info" data-id="${rule.id}" title="点击执行一键登录">
<div class="rule-domain">
${rule.clientName}
<span class="rule-env env-${rule.env.toLowerCase().replace('_', '-')}">${rule.env.replace('_', ' ')}</span>
</div>
</div>
<div class="rule-actions">
<button class="icon-btn btn-edit" data-id="${rule.id}" title="编辑此配置">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</button>
<button class="icon-btn btn-delete" data-id="${rule.id}" title="归档此配置">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M20 7l-8 8-4-4m12 4v5a2 2 0 01-2 2H6a2 2 0 01-2-2V9a2 2 0 012-2h5" />
</svg>
</button>
</div>
`;
addDragEvents(li);
rulesList.appendChild(li);
});
attachRuleEvents();
}
function renderArchive() {
archiveList.innerHTML = '';
const archivedRules = rules.filter(r => r.archived);
if (archivedRules.length === 0) {
archiveList.innerHTML = '<div class="empty-state" style="padding: 20px 0;"><span>暂无归档内容</span></div>';
return;
}
archivedRules.forEach(rule => {
const li = document.createElement('li');
li.className = 'rule-item';
li.innerHTML = `
<div class="rule-icon-box">${getRandomIcon(rule.id)}</div>
<div class="rule-info">
<div class="rule-domain">${rule.clientName} <span class="rule-env env-${rule.env.toLowerCase().replace('_', '-')}">${rule.env.replace('_', ' ')}</span></div>
</div>
<div class="rule-actions">
<button class="icon-btn btn-restore" data-id="${rule.id}" title="恢复到主列表">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
</button>
<button class="icon-btn btn-delete-perm" data-id="${rule.id}" title="彻底删除">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
</button>
</div>
`;
archiveList.appendChild(li);
});
document.querySelectorAll('.btn-restore').forEach(btn => {
btn.addEventListener('click', (e) => {
const id = e.currentTarget.getAttribute('data-id');
const index = rules.findIndex(r => r.id === id);
if (index !== -1) {
rules[index].archived = false;
saveRules();
renderArchive();
}
});
});
document.querySelectorAll('.btn-delete-perm').forEach(btn => {
btn.addEventListener('click', (e) => {
const id = e.currentTarget.getAttribute('data-id');
if (confirm('确定要彻底删除此配置吗?此操作不可撤销。')) {
rules = rules.filter(r => r.id !== id);
saveRules();
renderArchive();
}
});
});
}
function attachRuleEvents() {
document.querySelectorAll('.rule-info').forEach(infoArea => {
infoArea.addEventListener('click', (e) => {
const id = e.currentTarget.getAttribute('data-id');
const rule = rules.find(r => r.id === id);
if (rule) executePurgeAndLogin(rule, e.currentTarget);
});
});
document.querySelectorAll('.btn-edit').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
toggleAddForm('edit', e.currentTarget.getAttribute('data-id'));
});
});
document.querySelectorAll('.btn-delete').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const id = e.currentTarget.getAttribute('data-id');
const index = rules.findIndex(r => r.id === id);
if (index !== -1) {
rules[index].archived = true;
saveRules();
}
});
});
}
if (archiveToggle) {
archiveToggle.addEventListener('click', () => {
mainViewContent.classList.add('hidden');
archiveViewContent.classList.remove('hidden');
renderArchive();
});
}
if (backToMain) {
backToMain.addEventListener('click', () => {
archiveViewContent.classList.add('hidden');
mainViewContent.classList.remove('hidden');
renderRules();
});
}
// --- Drag and Drop functionality ---
function addDragEvents(item) {
item.addEventListener('dragstart', (e) => {
dragStartIndex = parseInt(e.currentTarget.getAttribute('data-index'));
e.currentTarget.classList.add('dragging');
// Set empty ghost image
if (e.dataTransfer) {
e.dataTransfer.effectAllowed = 'move';
// Need to set data to allow dragging in Firefox
e.dataTransfer.setData('text/plain', dragStartIndex);
}
});
item.addEventListener('dragend', (e) => {
e.currentTarget.classList.remove('dragging');
});
item.addEventListener('dragover', (e) => {
e.preventDefault();
// Allow drop
return false;
});
item.addEventListener('dragenter', (e) => {
e.preventDefault();
});
item.addEventListener('drop', (e) => {
e.stopPropagation();
const dragEndIndex = parseInt(e.currentTarget.closest('.rule-item').getAttribute('data-index'));
if (dragStartIndex !== dragEndIndex) {
swapItems(dragStartIndex, dragEndIndex);
}
return false;
});
}
function swapItems(fromIndex, toIndex) {
// Array manipulation for drag sorting
const itemOne = rules[fromIndex];
rules.splice(fromIndex, 1);
rules.splice(toIndex, 0, itemOne);
saveRules();
}
function executePurgeAndLogin(rule, element) {
const originalContent = element.innerHTML;
element.innerHTML = `<div class="loading-state"><svg class="loading" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 2v4m0 12v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83"/></svg><span>清理中...</span></div>`;
if (typeof chrome !== 'undefined' && chrome.runtime) {
const config = ENV_CONFIG[rule.env];
chrome.runtime.sendMessage({
action: "purgeAndRedirect",
domain: config.domain,
targetUrl: config.url,
username: rule.username,
password: rule.password
}, (response) => {
// 提供短暂的“清理中...”视觉反馈后关闭插件
setTimeout(() => {
window.close();
}, 1500); // 保持 1500ms 反馈后关闭,也可改为更短如 200ms为保持原逻辑体验暂设 1500ms
});
}
}
loadAndInitBaseApp();
});