window.INP2P = window.INP2P || {}; window.INP2P.createState = function createState(Vue) { const { ref, computed } = Vue; const buildVersion = ref('20260307-2246'); const tab = ref('dashboard'); const tabs = [ {id:'dashboard',name:'仪表盘'},{id:'nodes',name:'设备管理'},{id:'sdwan',name:'组网'},{id:'p2p',name:'连接调试'}, {id:'apikeys',name:'API 密钥'},{id:'users',name:'用户'},{id:'enroll',name:'注册码'}, {id:'settings',name:'高级设置'},{id:'audit',name:'审计日志'} ]; const loggedIn = ref(false), msg = ref(''), msgType = ref('ok'); const opSummary = ref(''); const debugMode = ref(new URLSearchParams(location.search).get('debug') === '1'); const debugEvents = ref([]); const logEvent = (type, payload={}) => { const item = { id: Date.now() + Math.random(), ts: new Date().toISOString(), type, ...payload }; debugEvents.value = [item, ...debugEvents.value].slice(0, 20); if (debugMode.value) { if (type === 'error' || type === 'api_error') console.error('[INP2P]', item); else console.log('[INP2P]', item); } }; const clearDebugEvents = () => { debugEvents.value = []; }; const loginBusy = ref(false), globalBusy = ref(false); const tabBusy = ref({ sdwan:false, p2p:false, tenants:false, settings:false, audit:false }); const tabState = ref({}); const setTabBusy = (k, v) => { tabBusy.value = { ...tabBusy.value, [k]: !!v }; }; const beginTab = (k) => { setTabBusy(k, true); tabState.value = { ...tabState.value, [k]: 'loading' }; }; const endTabOk = (k, isEmpty=false) => { setTabBusy(k, false); tabState.value = { ...tabState.value, [k]: (isEmpty ? 'empty' : 'success') }; }; const endTabErr = (k) => { setTabBusy(k, false); tabState.value = { ...tabState.value, [k]: 'error' }; }; const role = ref(''), status = ref(1); const loginUser = ref(''), loginPass = ref(''), loginErr = ref(''); const theme = ref(localStorage.getItem('inp2p_theme') || 'b'); const applyTheme = () => { document.documentElement.setAttribute('data-theme', theme.value === 'a' ? 'a' : 'b'); }; const toggleTheme = () => { theme.value = theme.value === 'a' ? 'b' : 'a'; localStorage.setItem('inp2p_theme', theme.value); applyTheme(); }; const refreshSec = ref(15), timer = ref(null); const health = ref({}), stats = ref({}), nodes = ref([]), nodeKeyword = ref(''); const sd = ref({ enabled:false, name:'sdwan-main', gatewayCIDR:'10.10.0.0/24', mode:'mesh', hubNode:'', mtu:1420, nodes:[], routes:['10.10.0.0/24'], subnetProxies:[] }); const connectForm = ref({ from:'', to:'', srcPort:80, dstPort:80, appName:'manual-connect' }); const nodeEditVisible = ref(false); const nodeEditMode = ref('alias'); const nodeEditTitle = ref('编辑设备'); const nodeEditNode = ref(null); const nodeEditValue = ref(''); const tenants = ref([]), activeTenant = ref(1), keys = ref([]), users = ref([]), enrolls = ref([]); const tenantKeyword = ref(localStorage.getItem('inp2p_kw_tenant') || ''); const keyKeyword = ref(localStorage.getItem('inp2p_kw_key') || ''); const userKeyword = ref(localStorage.getItem('inp2p_kw_user') || ''); const enrollKeyword = ref(localStorage.getItem('inp2p_kw_enroll') || ''); const tenantForm = ref({ name:'', admin_password:'', operator_password:'' }); const keyForm = ref({ scope:'all', ttl:0 }); const userForm = ref({ role:'operator', email:'', password:'' }); const selectedKeyIds = ref([]); const selectedUserIds = ref([]); const selectedEnrollIds = ref([]); const bulkFailedKeyIds = ref([]); const bulkFailedUserIds = ref([]); const bulkFailedEnrollIds = ref([]); const keepFailedPool = ref(localStorage.getItem('inp2p_keep_failed_pool') !== '0'); const auditPreset = ref(localStorage.getItem('inp2p_audit_preset') || 'all'); const tokenType = ref(''); const tenantId = ref(0); const subnet = ref(''); const settings = ref({ advanced_impersonate:'0', advanced_force_network:'0', advanced_cross_tenant:'0', advanced_override_enabled:'0', allow_apikey_mutate_sdwan:'0', allow_operator_kick_cross_node:'0', allow_connect_cross_tenant:'0' }); const audit = ref([]); const auditTenant = ref(''); const auditFilters = ref({ action:'', actor_type:'', actor_id:'', target_type:'', target_id:'', from:'', to:'', limit: 100, offset: 0 }); const auditActionOptions = [ 'auth_login_success','auth_login_failed','tenant_create','apikey_create', 'user_create','enroll_create','enroll_status','enroll_consume','enroll_consume_denied', 'node_alias_update','node_ip_update','node_kick','connect_request_denied' ]; const isAdmin = computed(() => role.value === 'admin' && tenantId.value === 1); const roleLabel = computed(() => isAdmin.value ? '系统管理' : '子网管理'); const filteredTabs = computed(() => isAdmin.value ? tabs : tabs.filter(t => !['apikeys','users','enroll','settings','audit'].includes(t.id))); const filteredNodes = computed(() => { const k = (nodeKeyword.value || '').trim().toLowerCase(); if (!k) return nodes.value; return nodes.value.filter(n => (n.name||'').toLowerCase().includes(k) || (n.publicIP||'').toLowerCase().includes(k)); }); const filteredTenants = computed(() => { const k = (tenantKeyword.value || '').trim().toLowerCase(); if (!k) return tenants.value; return (tenants.value || []).filter(t => String(t.id||'').includes(k) || String(t.name||'').toLowerCase().includes(k) || String(t.subnet||'').toLowerCase().includes(k) ); }); const filteredKeys = computed(() => { const k = (keyKeyword.value || '').trim().toLowerCase(); if (!k) return keys.value; return (keys.value || []).filter(x => String(x.id||'').includes(k) || String(x.scope||'').toLowerCase().includes(k) ); }); const filteredUsers = computed(() => { const k = (userKeyword.value || '').trim().toLowerCase(); if (!k) return users.value; return (users.value || []).filter(u => String(u.id||'').includes(k) || String(u.role||'').toLowerCase().includes(k) || String(u.email||'').toLowerCase().includes(k) ); }); const filteredEnrolls = computed(() => { const k = (enrollKeyword.value || '').trim().toLowerCase(); if (!k) return enrolls.value; return (enrolls.value || []).filter(e => String(e.id||'').includes(k) || String(e.code||'').toLowerCase().includes(k) ); }); const onlineMap = computed(() => new Set((nodes.value || []).map(n => n.name))); const nodeStatus = (name) => (name && onlineMap.value.has(name)) ? '在线' : '离线'; const isOnline = (name) => (name && onlineMap.value.has(name)); const nodeOnline = (n) => { if (!n) return false; if (typeof n.online === 'boolean') return n.online; if (n.status === 1 || String(n.status).toLowerCase() === 'online') return true; if (n.status === 0 || String(n.status).toLowerCase() === 'offline') return false; if (n.lastSeen && (Date.now() - new Date(n.lastSeen).getTime()) > 120000) return false; return !!(n.name && onlineMap.value.has(n.name)); }; const nodeStatusTextOf = (n) => nodeOnline(n) ? '在线' : '离线'; const nodeStatusClassOf = (n) => nodeOnline(n) ? 'status-online' : 'status-offline'; const humanizeError = (msg) => { const t = String(msg || '').trim(); if (!t) return '请求失败,请稍后重试'; if (/unauthorized|invalid credentials|token/i.test(t)) return '认证已失效,请重新登录'; if (/forbidden|permission|denied/i.test(t)) return '权限不足,当前角色不可执行此操作'; if (/timeout|timed out/i.test(t)) return '请求超时,请检查网络后重试'; if (/network|failed to fetch|load failed/i.test(t)) return '网络异常,请稍后再试'; if (/bad request|invalid/i.test(t)) return '参数不正确,请检查输入项'; return t; }; const toast = (text, t='ok') => { msg.value = text; msgType.value = t; setTimeout(() => { if (msg.value === text) msg.value = ''; }, 2500); }; const validateUsername = (v) => { const x = (v || '').trim(); if (!/^[A-Za-z]{6,}$/.test(x)) return '用户名需仅字母且≥6位'; return ''; }; const validateRequired = (v, label='字段') => { if (v === null || v === undefined || String(v).trim() === '') return `${label}不能为空`; return ''; }; const validateSDWAN = (sdwan) => { const x = sdwan || {}; if (!x.gatewayCIDR || !String(x.gatewayCIDR).includes('/')) return '网段格式不正确(如 10.10.0.0/24)'; const dup = new Set(); for (const n of (x.nodes || [])) { if (!n || !n.node) continue; if (dup.has(n.node)) return `SDWAN 节点重复:${n.node}`; dup.add(n.node); if (n.ip && !/^\d+\.\d+\.\d+\.\d+$/.test(String(n.ip))) return `节点IP格式错误:${n.ip}`; } return ''; }; const tenantFormErr = computed(() => { const n = (tenantForm.value.name || '').trim(); return n ? '' : '租户名不能为空'; }); const keyFormErr = computed(() => { const sc = (keyForm.value.scope || '').trim(); return sc ? '' : 'scope 不能为空'; }); const userFormErr = computed(() => { if (!(userForm.value.email || '').trim()) return 'email/username 不能为空'; if (!(userForm.value.password || '').trim()) return 'password 不能为空'; if (String(userForm.value.password || '').length < 6) return 'password 至少 6 位'; return ''; }); const sdwanFormErr = computed(() => validateSDWAN(sd.value)); const natText = (v) => { const x = String(v || '').toLowerCase(); if (!x) return 'Unknown'; if (x.includes('cone')) return 'Cone'; if (x.includes('symmetric')) return 'Symmetric'; if (x.includes('easy')) return 'Easy'; return x; }; const uptime = ts => { if(!ts) return '-'; const sec = Math.max(0, Math.floor((Date.now() - new Date(ts).getTime()) / 1000)); if(sec < 60) return sec + 's'; if(sec < 3600) return Math.floor(sec/60) + 'm'; if(sec < 86400) return Math.floor(sec/3600) + 'h'; return Math.floor(sec/86400) + 'd'; }; const fmtTime = t => t ? new Date(t).toLocaleString() : '-'; return { buildVersion, tab, tabs, loggedIn, msg, msgType, opSummary, loginBusy, globalBusy, tabBusy, tabState, setTabBusy, beginTab, endTabOk, endTabErr, role, status, loginUser, loginPass, loginErr, refreshSec, timer, theme, applyTheme, toggleTheme, health, stats, nodes, nodeKeyword, sd, connectForm, nodeEditVisible, nodeEditMode, nodeEditTitle, nodeEditNode, nodeEditValue, tenants, activeTenant, keys, users, enrolls, tenantForm, keyForm, userForm, selectedKeyIds, selectedUserIds, selectedEnrollIds, bulkFailedKeyIds, bulkFailedUserIds, bulkFailedEnrollIds, keepFailedPool, auditPreset, tenantKeyword, keyKeyword, userKeyword, enrollKeyword, tokenType, tenantId, subnet, settings, audit, auditTenant, auditFilters, auditActionOptions, isAdmin, roleLabel, filteredTabs, filteredNodes, filteredTenants, filteredKeys, filteredUsers, filteredEnrolls, onlineMap, nodeStatus, isOnline, nodeOnline, nodeStatusTextOf, nodeStatusClassOf, debugMode, debugEvents, logEvent, clearDebugEvents, validateUsername, validateRequired, validateSDWAN, tenantFormErr, keyFormErr, userFormErr, sdwanFormErr, humanizeError, toast, natText, uptime, fmtTime }; };