/** * TalkingQ Platform - RBAC Permission Management Page */ registerPage('rbac', function(container) { const DEFAULT_ROLE_COMPANIES = getDefaultCompanyIds(); const PERMISSION_CATALOG = [ { key: 'dashboard', label: '数据仪表盘', desc: '查看公司维度统计数据' }, { key: 'agent-dev', label: '配置应用', desc: '配置智能体模型、Prompt、技能和运行体验' }, { key: 'agent-manage', label: '应用管理', desc: '创建、发布、回收应用' }, { key: 'session', label: '会话分析', desc: '查看设备会话和热门话题' }, { key: 'badcase', label: 'Bad Case 管理', desc: '标注、处理和关闭问题案例' }, { key: 'device', label: '设备管理', desc: '查看和维护公司设备' }, { key: 'ota', label: 'OTA 升级', desc: '上传固件和创建升级任务' }, { key: 'rbac', label: '权限管理', desc: '管理用户、角色和授权范围' }, { key: 'settings', label: '系统设置', desc: '管理 API Key 和平台配置' }, ]; const DEFAULT_ROLE_CONFIGS = [ { id: 'admin', name: 'Admin', desc: '超级管理员,拥有平台全部权限和全部公司数据访问权', color: '#722ed1', companyScope: 'all', permissions: PERMISSION_CATALOG.map(item => item.key), locked: true, }, { id: 'worker', name: 'Worker', desc: '内部运营 / 研发人员,可配置应用并处理运营问题', color: '#1890ff', companyScope: 'all', permissions: ['dashboard', 'agent-dev', 'agent-manage', 'session', 'badcase', 'device', 'ota'], locked: false, }, { id: 'client', name: 'Client', desc: 'B 端客户,只能访问自己公司的客户可见数据', color: '#52c41a', companyScope: 'single', permissions: ['dashboard', 'session', 'device'], locked: true, }, ]; const DEFAULT_USERS = [ { id: 'u001', displayName: '张明华', loginName: 'zhangmh@talkingq.com', status: 'active', lastLogin: '2026-05-12 14:10', role: 'admin', remark: '超级管理员' }, { id: 'u002', displayName: '李思远', loginName: 'lisiy@talkingq.com', status: 'active', lastLogin: '2026-05-12 11:30', role: 'worker', remark: '运营人员' }, { id: 'u003', displayName: '王晓燕', loginName: 'wangxy@partner.com', status: 'active', lastLogin: '2026-05-11 09:00', role: 'client', companyId: 'company_a', allowedCompanies: ['company_a'], remark: '智慧星科技-客户' }, { id: 'u004', displayName: '刘亚楠', loginName: 'liuyn@partner.com', status: 'inactive', lastLogin: '2026-04-28 16:00', role: 'client', companyId: 'company_b', allowedCompanies: ['company_b'], remark: '未来玩具厂-客户' }, { id: 'u005', displayName: '陈志强', loginName: 'chenzq@talkingq.com', status: 'active', lastLogin: '2026-05-10 09:15', role: 'worker', remark: '开发人员' }, ]; const DEFAULT_AUDIT_LOGS = [ { id: 'a001', time: '2026-05-12 14:22:05', user: '张明华', action: '登录', target: '系统', ip: '192.168.1.10', ok: true }, { id: 'a002', time: '2026-05-12 14:18:30', user: '张明华', action: '修改 Prompt', target: '小Q故事机器人 v2.3', ip: '192.168.1.10', ok: true }, { id: 'a003', time: '2026-05-12 13:55:12', user: '李思远', action: '关闭 Bad Case', target: 'BC-2026-004', ip: '10.0.2.15', ok: true }, { id: 'a004', time: '2026-05-12 11:30:00', user: 'system', action: '自动触发内容安全', target: 'sess-3f7a', ip: 'internal', ok: false }, { id: 'a005', time: '2026-05-12 09:15:42', user: '张明华', action: '创建 OTA 任务', target: 'OTA-2026-002', ip: '192.168.1.10', ok: true }, ]; let activeTab = window.__rbacActiveTab || 'users'; let roleConfigs = window.__rbacRoleConfigs || cloneRoleConfigs(DEFAULT_ROLE_CONFIGS); let users = loadUsers(); let auditLogs = window.__rbacAuditLogs || DEFAULT_AUDIT_LOGS.map(item => ({ ...item })); window.__rbacRoleConfigs = roleConfigs; window.__rbacUsers = users; window.__rbacAuditLogs = auditLogs; window.__lookupRbacUserByLoginName = function(loginName) { const normalized = String(loginName || '').trim().toLowerCase(); return users.find(user => user.loginName === normalized) || null; }; window.__getRoleConfig = function(role) { return getRoleConfig(role); }; function getDefaultCompanyIds() { const ids = (window.companies || []) .filter(company => company.id !== 'all') .map(company => company.id); return ids.length ? ids : ['company_a', 'company_b', 'company_c']; } function cloneRoleConfigs(list) { return list.map(role => ({ ...role, permissions: [...role.permissions], allowedCompanyIds: role.allowedCompanyIds ? [...role.allowedCompanyIds] : DEFAULT_ROLE_COMPANIES.slice(), })); } function cloneUsers(list) { return list.map(item => ({ ...item, allowedCompanies: Array.isArray(item.allowedCompanies) ? [...item.allowedCompanies] : item.allowedCompanies, permissions: Array.isArray(item.permissions) ? [...item.permissions] : item.permissions, })); } function loadUsers() { try { localStorage.removeItem('talkingq_rbac_users_v1'); if (Array.isArray(window.__rbacUsers) && window.__rbacUsers.length) { return window.__rbacUsers.map((user, index) => normalizeUser(user, index)); } } catch (error) { console.warn('Failed to reset legacy RBAC storage', error); } return cloneUsers(DEFAULT_USERS).map((user, index) => normalizeUser(user, index)); } function saveUsers(nextUsers = users) { users = nextUsers; window.__rbacUsers = users; } function saveRoles(nextRoles = roleConfigs) { roleConfigs = nextRoles; window.__rbacRoleConfigs = roleConfigs; } function saveAudit(nextLogs = auditLogs) { auditLogs = nextLogs.slice(0, 80); window.__rbacAuditLogs = auditLogs; } function normalizeRole(role) { const raw = String(role || '').toLowerCase(); if (raw === 'admin' || raw === 'administrator') return 'admin'; if (raw === 'worker' || raw === 'employee') return 'worker'; if (raw === 'client' || raw === 'customer') return 'client'; return 'worker'; } function inferRoleFromRemark(remark) { const text = String(remark || ''); if (text.includes('超级管理员')) return 'admin'; if (text.includes('客户')) return 'client'; return 'worker'; } function getRoleConfig(role) { const roleId = normalizeRole(role); return roleConfigs.find(item => item.id === roleId) || roleConfigs.find(item => item.id === 'worker'); } function getRoleDefaultPermissions(role) { return [...(getRoleConfig(role)?.permissions || [])]; } function normalizeAllowedCompanies(value, companyId, role) { if (role !== 'client') return DEFAULT_ROLE_COMPANIES.slice(); if (Array.isArray(value) && value.length) return value; return companyId ? [companyId] : []; } function normalizeUser(user, index = 0) { const role = normalizeRole(user.role || inferRoleFromRemark(user.remark)); const fallbackCompanyId = DEFAULT_ROLE_COMPANIES[index % DEFAULT_ROLE_COMPANIES.length]; const companyId = role === 'client' ? (user.companyId || user.allowedCompanies?.[0] || fallbackCompanyId) : null; return { id: user.id || `u${String(index + 1).padStart(3, '0')}`, displayName: user.displayName || user.loginName || '未命名用户', loginName: String(user.loginName || '').trim().toLowerCase(), status: user.status || 'active', lastLogin: user.lastLogin || '-', role, companyId, allowedCompanies: normalizeAllowedCompanies(user.allowedCompanies, companyId, role), permissions: Array.isArray(user.permissions) && user.permissions.length ? [...user.permissions] : getRoleDefaultPermissions(role), remark: user.remark || (role === 'client' ? `${getCompanyName(companyId)}-客户` : role === 'admin' ? '超级管理员' : '运营人员'), }; } function escapeHtml(value) { return String(value ?? '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function getNowText() { const now = new Date(); const pad = value => String(value).padStart(2, '0'); return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`; } function getCompanyName(companyId) { return window.__getCompanyNameById ? window.__getCompanyNameById(companyId) : companyId; } function getCompanyOptionsHtml(selectedCompanyId = '') { const companies = (window.companies || []).filter(company => company.id !== 'all'); return companies.map(company => ` `).join(''); } function renderCompanyCheckboxes(selectedIds = DEFAULT_ROLE_COMPANIES) { const selected = new Set(selectedIds); const companies = (window.companies || []).filter(company => company.id !== 'all'); return companies.map(company => ` `).join(''); } function getUserById(userId) { return users.find(user => user.id === userId); } function getUserRoleLabel(user) { if (user.role === 'admin') return '管理员'; if (user.role === 'client') return '客户'; return '运营人员'; } function getCompanyScopeText(user) { if (user.role !== 'client') return '全部公司'; return user.companyId ? getCompanyName(user.companyId) : '未绑定公司'; } function getStatusLabel(status) { return status === 'inactive' ? '已禁用' : '启用中'; } function getStatusBadge(status) { return status === 'inactive' ? 'badge-default' : 'badge-success'; } function getActorName() { try { const user = JSON.parse(sessionStorage.getItem('talkingq_user') || 'null'); return user?.displayName || user?.username || '张明华'; } catch (error) { return '张明华'; } } function addAuditLog(action, target, ok = true) { saveAudit([ { id: `a${Date.now()}`, time: getNowText(), user: getActorName(), action, target, ip: 'local-demo', ok, }, ...auditLogs, ]); } function getPermissionLabel(key) { return PERMISSION_CATALOG.find(item => item.key === key)?.label || key; } function renderPermissionBadges(permissionKeys) { const keys = permissionKeys.length ? permissionKeys : []; const shown = keys.slice(0, 8); return `
${shown.map(key => `${escapeHtml(getPermissionLabel(key))}`).join('')} ${keys.length > shown.length ? `+${keys.length - shown.length}` : ''}
`; } function renderPermissionCheckboxes(roleId, selectedKeys, editable = true) { const role = getRoleConfig(roleId); const selected = new Set(selectedKeys || role.permissions); const locked = role.id === 'admin' || role.id === 'client' || !editable; return PERMISSION_CATALOG.map(permission => { const checked = role.id === 'admin' ? true : selected.has(permission.key); const disabled = locked ? 'disabled' : ''; return ` `; }).join(''); } function collectCheckedValues(root, selector) { return [...root.querySelectorAll(selector)] .filter(input => input.checked) .map(input => input.value); } function isLastActiveAdmin(userId) { const activeAdmins = users.filter(user => user.role === 'admin' && user.status === 'active'); return activeAdmins.length <= 1 && activeAdmins.some(user => user.id === userId); } function renderPage() { activeTab = activeTab || 'users'; window.__rbacActiveTab = activeTab; container.innerHTML = `
用户管理
角色配置
审计日志
${renderUsersTab()}
${renderRolesTab()}
${renderAuditTab()}
`; bindEvents(); lucide.createIcons({ elements: [container] }); } function renderUsersTab() { return `
${users.map(user => renderUserRow(user)).join('')}
显示名称 登录名称 用户类型 数据范围 状态 备注 操作
`; } function renderUserRow(user) { const role = getRoleConfig(user.role); const safeName = escapeHtml(user.displayName); return `
${safeName} ${user.status === 'active' ? '' : ''}
${escapeHtml(user.loginName)} ${escapeHtml(getUserRoleLabel(user))} ${escapeHtml(getCompanyScopeText(user))} ${getStatusLabel(user.status)} ${escapeHtml(user.remark || '-')} `; } function renderRolesTab() { return `
${roleConfigs.map(role => `
${escapeHtml(role.name)} ${role.id === 'admin' ? '全部' : role.permissions.length} 项权限
${escapeHtml(role.desc)}
可访问功能:
${renderPermissionBadges(role.id === 'admin' ? PERMISSION_CATALOG.map(item => item.key) : role.permissions)}
`).join('')}
RBAC 接口预留:GET /api/v1/rbac/rolesPUT /api/v1/rbac/roles/{id}POST /api/v1/rbac/users/assign-role
`; } function renderAuditTab() { return `
审计日志
记录权限、角色、用户状态等关键变更
${auditLogs.map(log => ` `).join('')}
时间操作人操作类型目标IP 地址结果
${escapeHtml(log.time)} ${escapeHtml(log.user)} ${escapeHtml(log.action)} ${escapeHtml(log.target)} ${escapeHtml(log.ip)} ${log.ok ? '成功' : '告警'}
`; } function bindEvents() { container.querySelectorAll('#rbacTabs .tab-item').forEach(tab => { tab.addEventListener('click', () => { activeTab = tab.dataset.tab; renderPage(); }); }); container.querySelector('#addUserBtn')?.addEventListener('click', openAddUserModal); container.querySelector('#refreshAuditBtn')?.addEventListener('click', () => { addAuditLog('刷新审计日志', '权限管理 (RBAC)'); activeTab = 'audit'; renderPage(); Toast.success('审计日志已刷新'); }); } function openAddUserModal() { const modal = createModal({ title: '添加用户', size: 'modal-lg', content: `
`, onConfirm: async () => { const displayName = modal.overlay.querySelector('#newUserDisplayName')?.value.trim(); const loginName = modal.overlay.querySelector('#newUserLoginName')?.value.trim().toLowerCase(); const role = modal.overlay.querySelector('#newUserRole')?.value; const status = modal.overlay.querySelector('#newUserStatus')?.value || 'active'; const companyId = modal.overlay.querySelector('#newUserCompany')?.value; const password = modal.overlay.querySelector('#newUserPassword')?.value; const remark = modal.overlay.querySelector('#newUserRemark')?.value.trim(); if (!displayName || !loginName || !role || !password) { Toast.warning('请填写用户必填信息'); return false; } if (users.some(user => user.loginName === loginName)) { Toast.warning('登录名称已存在'); return false; } if (role === 'client' && !companyId) { Toast.warning('Client 角色必须选择所属公司'); return false; } const nextUser = normalizeUser({ id: `u${Date.now()}`, displayName, loginName, role, status, companyId: role === 'client' ? companyId : null, allowedCompanies: role === 'client' ? [companyId] : DEFAULT_ROLE_COMPANIES.slice(), remark: remark || (role === 'client' ? `${getCompanyName(companyId)}-客户` : role === 'admin' ? '超级管理员' : '运营人员'), }, users.length); saveUsers([nextUser, ...users]); addAuditLog('创建用户', `${nextUser.displayName} / ${getUserRoleLabel(nextUser)}`); try { await window.API?.rbac?.createUser?.({ displayName, loginName, role, companyId, status }); } catch (error) { console.warn('Mock create user failed:', error); } Toast.success('用户创建成功'); activeTab = 'users'; renderPage(); }, confirmText: '创建用户', }); bindRoleCompanyToggle(modal.overlay, '#newUserRole', '#newCompanyGroup', '#newUserCompany'); } function bindRoleCompanyToggle(root, roleSelector, groupSelector, companySelector) { const roleSelect = root.querySelector(roleSelector); const group = root.querySelector(groupSelector); const companySelect = root.querySelector(companySelector); const sync = () => { const isClient = roleSelect?.value === 'client'; if (group) group.style.display = isClient ? '' : 'none'; if (companySelect) companySelect.disabled = !isClient; }; roleSelect?.addEventListener('change', sync); sync(); } window.editUser = function(userId) { const user = getUserById(userId); if (!user) return; const modal = createModal({ title: '编辑用户', size: 'modal-lg', content: `
Client 角色登录后只显示该公司客户可见内容。
`, onConfirm: async () => { const displayName = modal.overlay.querySelector('#editUserDisplayName')?.value.trim(); const role = modal.overlay.querySelector('#editUserRole')?.value || 'worker'; const status = modal.overlay.querySelector('#editUserStatus')?.value || 'active'; const companyId = modal.overlay.querySelector('#editUserCompany')?.value; const remark = modal.overlay.querySelector('#editUserRemark')?.value.trim(); if (!displayName) { Toast.warning('请填写显示名称'); return false; } if (role === 'client' && !companyId) { Toast.warning('Client 角色必须选择所属公司'); return false; } if (status === 'inactive' && isLastActiveAdmin(user.id)) { Toast.warning('至少需要保留一个启用中的管理员'); return false; } const nextUser = normalizeUser({ ...user, displayName, role, status, companyId: role === 'client' ? companyId : null, allowedCompanies: role === 'client' ? [companyId] : DEFAULT_ROLE_COMPANIES.slice(), permissions: role === user.role ? user.permissions : getRoleDefaultPermissions(role), remark: remark || (role === 'client' ? `${getCompanyName(companyId)}-客户` : role === 'admin' ? '超级管理员' : '运营人员'), }); saveUsers(users.map(item => item.id === user.id ? nextUser : item)); addAuditLog('编辑用户', `${nextUser.displayName} / ${getUserRoleLabel(nextUser)}`); try { await window.API?.rbac?.updateUser?.(user.id, nextUser); } catch (error) { console.warn('Mock update user failed:', error); } Toast.success(`用户 ${nextUser.displayName} 信息已更新`); activeTab = 'users'; renderPage(); }, confirmText: '保存', }); bindRoleCompanyToggle(modal.overlay, '#editUserRole', '#editCompanyGroup', '#editUserCompany'); }; window.managePermissions = function(userId) { const user = getUserById(userId); if (!user) return; const modal = createModal({ title: `权限管理 - ${escapeHtml(user.displayName)}`, size: 'modal-lg', content: `
Client 角色仅可访问所选公司。
细粒度权限
后端接口预留:POST /api/v1/rbac/users/assign-rolePUT /api/v1/rbac/users/{id}/permissions
`, onConfirm: async () => { const nextRole = modal.overlay.querySelector('#permRoleSelect')?.value || user.role; const companyId = modal.overlay.querySelector('#permCompanySelect')?.value; if (nextRole === 'client' && !companyId) { Toast.warning('Client 角色必须选择所属公司'); return false; } const role = getRoleConfig(nextRole); const permissions = role.locked ? getRoleDefaultPermissions(nextRole) : collectCheckedValues(modal.overlay, '.rbac-permission-checkbox'); if (!permissions.length) { Toast.warning('请至少保留一项权限'); return false; } const nextUser = normalizeUser({ ...user, role: nextRole, companyId: nextRole === 'client' ? companyId : null, allowedCompanies: nextRole === 'client' ? [companyId] : DEFAULT_ROLE_COMPANIES.slice(), permissions, remark: nextRole === 'client' ? `${getCompanyName(companyId)}-客户` : nextRole === 'admin' ? '超级管理员' : '运营人员', }); saveUsers(users.map(item => item.id === user.id ? nextUser : item)); addAuditLog('更新用户权限', `${nextUser.displayName} / ${getUserRoleLabel(nextUser)}`); try { await window.API?.rbac?.assignRole?.(user.id, nextRole); await window.API?.rbac?.updateUser?.(user.id, nextUser); } catch (error) { console.warn('Mock update permissions failed:', error); } Toast.success(`${nextUser.displayName} 的权限已更新`); activeTab = 'users'; renderPage(); }, confirmText: '保存权限', }); const roleSelect = modal.overlay.querySelector('#permRoleSelect'); const companyGroup = modal.overlay.querySelector('#permCompanyGroup'); const companySelect = modal.overlay.querySelector('#permCompanySelect'); const grid = modal.overlay.querySelector('#permCheckboxGrid'); const sync = () => { const roleId = roleSelect?.value || user.role; const role = getRoleConfig(roleId); if (companyGroup) companyGroup.style.display = roleId === 'client' ? '' : 'none'; if (companySelect) companySelect.disabled = roleId !== 'client'; if (grid) grid.innerHTML = renderPermissionCheckboxes(roleId, roleId === user.role ? user.permissions : role.permissions, !role.locked); lucide.createIcons({ elements: [modal.overlay] }); }; roleSelect?.addEventListener('change', sync); sync(); }; window.toggleUserStatus = function(userId) { const user = getUserById(userId); if (!user) return; const nextStatus = user.status === 'active' ? 'inactive' : 'active'; createModal({ title: `${nextStatus === 'active' ? '启用' : '禁用'}用户`, content: `

确认${nextStatus === 'active' ? '启用' : '禁用'} ${escapeHtml(user.displayName)}?

${nextStatus === 'active' ? '启用后用户可重新登录平台。' : '禁用后用户将无法登录平台。'}

`, onConfirm: async () => { if (nextStatus === 'inactive' && isLastActiveAdmin(user.id)) { Toast.warning('至少需要保留一个启用中的管理员'); return false; } const nextUser = { ...user, status: nextStatus }; saveUsers(users.map(item => item.id === user.id ? nextUser : item)); addAuditLog(nextStatus === 'active' ? '启用用户' : '禁用用户', user.displayName); try { await window.API?.rbac?.updateUser?.(user.id, { status: nextStatus }); } catch (error) { console.warn('Mock update status failed:', error); } Toast.success(`用户 ${user.displayName} 已${nextStatus === 'active' ? '启用' : '禁用'}`); renderPage(); }, confirmText: nextStatus === 'active' ? '确认启用' : '确认禁用', }); lucide.createIcons(); }; window.deleteUser = function(userId) { const user = getUserById(userId); if (!user) return; createModal({ title: '确认删除', content: `

确认删除用户 ${escapeHtml(user.displayName)}?

此操作不可撤销,删除后该用户将无法登录系统。

`, onConfirm: async () => { if (isLastActiveAdmin(user.id)) { Toast.warning('至少需要保留一个启用中的管理员'); return false; } saveUsers(users.filter(item => item.id !== user.id)); addAuditLog('删除用户', user.displayName); try { await window.API?.rbac?.deleteUser?.(user.id); } catch (error) { console.warn('Mock delete user failed:', error); } Toast.success(`用户 ${user.displayName} 已删除`); activeTab = 'users'; renderPage(); }, confirmText: '确认删除', cancelText: '取消', }); lucide.createIcons(); }; window.editRolePermissions = function(roleId) { const role = getRoleConfig(roleId); if (!role) return; const modal = createModal({ title: `编辑 ${escapeHtml(role.name)} 角色权限`, size: 'modal-lg', content: `
角色说明
${escapeHtml(role.desc)}
${renderPermissionCheckboxes(role.id, role.permissions, !role.locked)}
${role.locked ? '
Admin / Client 为平台内置角色,功能范围固定,只能通过用户授权控制公司范围。
' : ''}
${role.id === 'admin' ? '
Admin 默认可访问全部公司。
' : renderCompanyCheckboxes(role.allowedCompanyIds || DEFAULT_ROLE_COMPANIES)}
这里控制该角色可被分配到哪些公司;Client 用户仍需在用户权限中选择具体所属公司。
后端接口预留:PUT /api/v1/rbac/roles/${escapeHtml(role.id)}
`, onConfirm: async () => { const permissions = role.locked ? getRoleDefaultPermissions(role.id) : collectCheckedValues(modal.overlay, '.rbac-permission-checkbox'); const allowedCompanyIds = role.id === 'admin' ? DEFAULT_ROLE_COMPANIES.slice() : collectCheckedValues(modal.overlay, '.rbac-company-checkbox'); if (!permissions.length) { Toast.warning('请至少保留一项权限'); return false; } if (role.id !== 'admin' && !allowedCompanyIds.length) { Toast.warning('请至少选择一个可分配公司'); return false; } const nextRole = { ...role, permissions, allowedCompanyIds, }; saveRoles(roleConfigs.map(item => item.id === role.id ? nextRole : item)); addAuditLog('编辑角色权限', role.name); try { await window.API?.rbac?.updateRole?.(role.id, nextRole); } catch (error) { console.warn('Mock update role failed:', error); } Toast.success(`${role.name} 角色权限已更新`); activeTab = 'roles'; renderPage(); }, confirmText: '保存', }); lucide.createIcons({ elements: [modal.overlay] }); }; window.viewRoleUsers = function(roleId) { const role = getRoleConfig(roleId); if (!role) return; const roleUsers = users.filter(user => user.role === role.id); createModal({ title: `${escapeHtml(role.name)} 角色下的用户`, size: 'modal-lg', content: `
共 ${roleUsers.length} 个用户
${roleUsers.length ? roleUsers.map(user => `
${escapeHtml(user.displayName)}
${escapeHtml(user.loginName)}
${getStatusLabel(user.status)} ${escapeHtml(getCompanyScopeText(user))}
`).join('') : `
暂无用户使用该角色
`}
`, confirmText: '关闭', showCancel: false, }); }; renderPage(); });