301 lines
12 KiB
JavaScript
301 lines
12 KiB
JavaScript
; (async () => {
|
||
/**
|
||
* @typedef {Object} Gateway
|
||
* @property {string} devName - device name
|
||
* @property {Element} btn - button element
|
||
* @property {string} sn - serial number
|
||
*/
|
||
|
||
/**
|
||
* Centralised timeout/interval constants (milliseconds).
|
||
*/
|
||
const WAIT = {
|
||
POLL_INTERVAL: 100,
|
||
PANEL_FADE: 100,
|
||
PANEL_VISIBLE_TIMEOUT: 600,
|
||
BUTTON_FIND: 5000,
|
||
CONFIG_DIALOG_VISIBLE: 8000,
|
||
TEMPLATE_LIST_RENDER: 200,
|
||
DROPDOWN_CLOSE: 100,
|
||
MSGBOX_APPEAR: 2000,
|
||
DIALOG_CLOSE: 8000,
|
||
DETAIL_PAGE_RENDER: 15000,
|
||
TABLE_ROWS_RENDER: 15000,
|
||
}
|
||
|
||
/**
|
||
* Clicks through: 参数信息 → 参数配置 → 参数模板配置
|
||
*/
|
||
async function openTemplateConfig() {
|
||
// 1. Click the "参数信息" tab
|
||
const paramTab = document.getElementById('tab-parameterInfo')
|
||
|| [...document.querySelectorAll('.el-tabs__item')]
|
||
.find(el => el.textContent.trim() === '参数信息')
|
||
if (!paramTab) throw new Error('无法找到"参数信息"标签')
|
||
if (!paramTab.classList.contains('is-active')) {
|
||
// Some implementations bind click on the inner <div id="step1">,
|
||
// so try it first, then fall back to the wrapper.
|
||
const inner = paramTab.querySelector('#step1, .el-tabs-padding8')
|
||
; (inner ?? paramTab).click()
|
||
// wait for its panel to become visible
|
||
await new Promise(res => {
|
||
const panel = document.getElementById('pane-parameterInfo')
|
||
const mo = new MutationObserver(() => {
|
||
if (panel.style.display !== 'none') {
|
||
mo.disconnect()
|
||
// give any fade-in a moment
|
||
setTimeout(res, WAIT.PANEL_FADE)
|
||
}
|
||
})
|
||
mo.observe(panel, { attributes: true, attributeFilter: ['style'] })
|
||
// fallback
|
||
setTimeout(() => { mo.disconnect(); res() }, WAIT.PANEL_VISIBLE_TIMEOUT)
|
||
})
|
||
}
|
||
|
||
// 2. Wait for并点击 "参数配置" 按钮
|
||
const cfgBtn = await waitFor(() =>
|
||
[...document.querySelectorAll('#pane-parameterInfo button')]
|
||
.find(b => b.textContent.trim() === '参数配置'),
|
||
WAIT.BUTTON_FIND,
|
||
)
|
||
cfgBtn.click()
|
||
|
||
// 3. Wait for the configuration dialog to appear
|
||
await waitFor(() => {
|
||
const dlgWrapper = document.querySelector('.configurationDialog')
|
||
return dlgWrapper && getComputedStyle(dlgWrapper).display !== 'none'
|
||
}, WAIT.CONFIG_DIALOG_VISIBLE)
|
||
|
||
// 4. Click the "参数模板配置" radio
|
||
const radio = [...document.querySelectorAll('.configurationDialog .el-radio-button')]
|
||
.find(r => r.textContent.trim() === '参数模板配置')
|
||
if (!radio) throw new Error('无法找到"参数模板配置"项')
|
||
radio.click()
|
||
}
|
||
|
||
/**
|
||
* After 参数模板配置 is active, select a named template, apply it, and confirm the message box.
|
||
* @param {string} templateName — e.g. "gw_template_01"
|
||
*/
|
||
async function applyParameterTemplate(templateName = 'gw_template_01') {
|
||
// 1. Open the template-config UI
|
||
await openTemplateConfig() // assumes you've already defined openTemplateConfig()
|
||
|
||
// 2. Click the 参数配置 dropdown
|
||
const selectInput = document.querySelector('.configurationDialog input[placeholder*="参数模板"]')
|
||
if (!selectInput) throw new Error('找不到参数模板下拉框')
|
||
selectInput.click()
|
||
|
||
// 3. Wait a moment for the list to render
|
||
await new Promise(r => setTimeout(r, WAIT.TEMPLATE_LIST_RENDER))
|
||
|
||
// 4. Pick the desired template
|
||
const option = Array.from(document.querySelectorAll('.el-select-dropdown__item'))
|
||
.find(li => li.textContent.trim() === templateName)
|
||
if (!option) throw new Error(`模板 "${templateName}" 不存在`)
|
||
option.click()
|
||
|
||
// 5. Wait for the dropdown to close
|
||
await new Promise(r => setTimeout(r, WAIT.DROPDOWN_CLOSE))
|
||
|
||
// 6. Click the "配置" button in the dialog footer
|
||
const applyBtn = document.querySelector('.configurationDialog .el-dialog__footer .el-button.el-button--primary')
|
||
if (!applyBtn) throw new Error('找不到"配置"按钮')
|
||
applyBtn.click()
|
||
|
||
// 7. Wait for the confirmation message box to appear
|
||
await new Promise(resolve => {
|
||
const selector = '.el-message-box__wrapper[aria-label="提示"]'
|
||
const check = () => {
|
||
const mb = document.querySelector(selector)
|
||
if (mb && getComputedStyle(mb).display !== 'none') {
|
||
return setTimeout(resolve, WAIT.MSGBOX_APPEAR)
|
||
}
|
||
return false
|
||
}
|
||
if (check()) return
|
||
const mo = new MutationObserver(() => {
|
||
if (check()) {
|
||
mo.disconnect()
|
||
}
|
||
})
|
||
mo.observe(document.body, { childList: true, subtree: true })
|
||
// fallback timeout
|
||
setTimeout(() => { mo.disconnect(); resolve() }, WAIT.MSGBOX_APPEAR)
|
||
})
|
||
|
||
// 8. Click the "好的" button in the message-box footer
|
||
const okBtn = Array.from(document.querySelectorAll('.el-message-box__btns .el-button'))
|
||
.find(b => b.textContent.trim().startsWith('好的'))
|
||
if (!okBtn) throw new Error('找不到消息框中的"好的"按钮')
|
||
okBtn.click()
|
||
|
||
// 9. Wait for both message box and configuration dialog to disappear
|
||
await waitFor(() => {
|
||
const msgWrap = document.querySelector('.el-message-box__wrapper[aria-label="提示"]')
|
||
const cfgDlg = document.querySelector('.configurationDialog')
|
||
const msgHidden = !msgWrap || getComputedStyle(msgWrap).display === 'none'
|
||
const dlgHidden = cfgDlg && getComputedStyle(cfgDlg).display === 'none'
|
||
return msgHidden && dlgHidden
|
||
}, WAIT.DIALOG_CLOSE)
|
||
}
|
||
|
||
/**
|
||
* @returns {boolean}
|
||
*/
|
||
function isConsoleLogNative() {
|
||
try {
|
||
return console.log.toString().includes('native code')
|
||
} catch (e) {
|
||
return false
|
||
}
|
||
}
|
||
/**
|
||
* @description create a clean log function from iframe
|
||
* @returns {Function}
|
||
*/
|
||
function createCleanLog() {
|
||
const f = document.createElement('iframe')
|
||
f.style.display = 'none'
|
||
document.body.appendChild(f)
|
||
return f.contentWindow.console.log.bind(f.contentWindow.console)
|
||
}
|
||
|
||
|
||
/**
|
||
* @returns {Gateway[]}
|
||
*/
|
||
function getGateways() {
|
||
/**
|
||
* @type {Gateway[]}
|
||
*/
|
||
const gateways = []
|
||
|
||
const rows = document.querySelectorAll('.el-table__body .el-table__row')
|
||
rows.forEach(row => {
|
||
const devNameEl = row.querySelector('.DevNameCls')
|
||
const snEl = row.querySelectorAll('td')[3] // 4th column is SN
|
||
|
||
if (devNameEl && snEl) {
|
||
const btn = devNameEl
|
||
const devName = devNameEl.textContent.trim()
|
||
const sn = snEl.textContent.trim()
|
||
gateways.push({ devName, btn, sn })
|
||
}
|
||
})
|
||
return gateways
|
||
}
|
||
|
||
/**
|
||
* Wait until a predicate returns a truthy value.
|
||
* @template T
|
||
* @param {() => T} predicate – polling function
|
||
* @param {number} [timeout=5000] – max wait in ms
|
||
* @param {number} [interval=100] – polling interval in ms
|
||
* @returns {Promise<T>}
|
||
*/
|
||
function waitFor(predicate, timeout = 5000, interval = WAIT.POLL_INTERVAL) {
|
||
const start = Date.now()
|
||
return new Promise((resolve, reject) => {
|
||
const check = () => {
|
||
let res
|
||
try { res = predicate() } catch { /* ignore */ }
|
||
if (res) return resolve(res)
|
||
if (Date.now() - start >= timeout) return reject(new Error('waitFor timeout'))
|
||
setTimeout(check, interval)
|
||
}
|
||
check()
|
||
})
|
||
}
|
||
|
||
/**
|
||
* Waits for a DOM element matching the selector.
|
||
* @param {string} selector
|
||
* @param {number} [timeout=5000]
|
||
* @returns {Promise<Element>}
|
||
*/
|
||
function waitForElement(selector, timeout = 5000) {
|
||
return waitFor(() => document.querySelector(selector), timeout)
|
||
}
|
||
|
||
/**
|
||
* Opens gateway details, applies the template, and navigates back.
|
||
* @param {string} devName – gateway display name
|
||
* @param {string} [templateName='gw_template_01']
|
||
*/
|
||
async function processGateway(devName, templateName = 'gw_template_01') {
|
||
console.group(`Processing gateway: ${devName}`)
|
||
|
||
// Wait for the 参数信息 tab (or any detail-specific element) to appear
|
||
await waitFor(() => document.querySelector('#tab-parameterInfo'), WAIT.DETAIL_PAGE_RENDER)
|
||
|
||
// 2. Run the existing template-application workflow
|
||
await applyParameterTemplate(templateName)
|
||
|
||
// 3. Navigate back to the gateway list via breadcrumb
|
||
let listLink = [...document.querySelectorAll('.CompBreadcrumb a')]
|
||
.find(a => a.textContent.trim() === '网关列表')
|
||
if (!listLink) {
|
||
// 有时文字在 <span role="link"> 中
|
||
listLink = [...document.querySelectorAll('.CompBreadcrumb [role="link"]')]
|
||
.find(el => el.textContent.trim() === '网关列表')
|
||
}
|
||
if (!listLink) throw new Error('无法找到"网关列表"面包屑链接')
|
||
listLink.click()
|
||
|
||
// 4. Wait until the table rows reappear
|
||
await waitFor(() => document.querySelectorAll('.el-table__body .el-table__row').length > 0, WAIT.TABLE_ROWS_RENDER)
|
||
console.groupEnd()
|
||
}
|
||
|
||
/**
|
||
* Entrypoint – iterates over every gateway and applies the template.
|
||
*/
|
||
async function init() {
|
||
// Expose helpers for manual debugging
|
||
Object.assign(window, {
|
||
openTemplateConfig,
|
||
applyParameterTemplate,
|
||
processGateway,
|
||
waitFor,
|
||
waitForElement,
|
||
getGateways,
|
||
})
|
||
if (!isConsoleLogNative()) {
|
||
const cleanLog = createCleanLog()
|
||
console.log = cleanLog
|
||
console.info("re-override console.log")
|
||
}
|
||
const initialGateways = getGateways()
|
||
const names = initialGateways.map(g => g.devName)
|
||
window.userState = { names }
|
||
console.info('Gateways detected:', names)
|
||
|
||
for (const devName of names) {
|
||
try {
|
||
// Re-query each round in case DOM has been re-rendered
|
||
const row = [...document.querySelectorAll('.el-table__body .el-table__row')]
|
||
.find(r => r.querySelector('.DevNameCls')?.textContent.trim() === devName)
|
||
if (!row) {
|
||
console.warn(`Row for "${devName}" not found; skipping.`)
|
||
continue
|
||
}
|
||
// Prefer anchor/button inside the DevName cell, else the cell div itself
|
||
const btn = row.querySelector('.DevNameCls a, .DevNameCls button') || row.querySelector('.DevNameCls')
|
||
if (!btn) {
|
||
console.warn(`Gateway button for "${devName}" not found; skipping.`)
|
||
continue
|
||
}
|
||
btn.click()
|
||
await processGateway(devName, 'gw_template_01')
|
||
} catch (e) {
|
||
console.error(`Failed to process gateway "${devName}"`, e)
|
||
}
|
||
}
|
||
|
||
console.info('Automation complete.')
|
||
}
|
||
await init()
|
||
})()
|