/** * 营销系统数据导出工具 - 主入口 * @description 使用模块化架构重构,提升可读性和扩展性 */ ;(function() { 'use strict'; const { createApp, reactive } = Vue; // ==================== 模块引用 ==================== const { CONSTANTS, getDatasourceOptions, getDatasourceLabel, getMainTable, getOrderTypeOptions, getDefaultOrderType, getSceneOptions, getSceneLabel, getOrderTypeLabel, VISIBILITY_OPTIONS, FORMAT_OPTIONS, getDefaultFields } = window.AppConfig; const { showMessage, formatDateTime, getMonthRange, clampDialogWidth, parseWidth, calculateJobProgress } = window.AppUtils; const { fieldsManager, TreeUtils } = window.FieldsModule; const { createAppState, ValidationRules } = window.StateModule; const Api = window.ApiService; // ==================== Vue 应用 ==================== const app = createApp({ setup() { // ==================== 状态初始化 ==================== const state = reactive(createAppState()); // ==================== 计算属性 ==================== const hasUserId = Vue.computed(() => !!Api.getUserId()); const currentUserId = Vue.computed(() => { const userId = Api.getUserId(); return userId ? Number(userId) : null; }); // ==================== 字段元数据管理 ==================== const metaTableLabels = Vue.ref({}); const recommendedMeta = Vue.ref([]); const metadataVersion = Vue.ref(0); // 用于触发树重新计算的响应式变量 /** * 加载字段元数据 * @param {string} datasource - 数据源 * @param {number} orderType - 订单类型 * @returns {Promise} 推荐字段列表 */ const loadFieldsMetadata = async (datasource, orderType) => { try { const { tables, recommended } = await Api.fetchFieldsMetadata(datasource, orderType); fieldsManager.updateMetadata(tables, recommended); metaTableLabels.value = fieldsManager.tableLabels; recommendedMeta.value = recommended; metadataVersion.value++; // 触发树重新计算 return recommended; } catch (error) { console.error('加载字段元数据失败:', error); fieldsManager.updateMetadata([], []); metaTableLabels.value = {}; recommendedMeta.value = []; metadataVersion.value++; // 即使失败也触发更新 return []; } }; // ==================== 树形选择器数据 ==================== const fieldTreeData = Vue.computed(() => { // 引用 metadataVersion 以确保元数据更新时重新计算 const _version = metadataVersion.value; return fieldsManager.buildFieldTree( state.form.datasource, state.form.orderType ); }); const editFieldTreeData = Vue.computed(() => { // 引用 metadataVersion 以确保元数据更新时重新计算 const _version = metadataVersion.value; return fieldsManager.buildFieldTree( state.edit.datasource, state.edit.orderType, state.edit.main_table ); }); // ==================== 选项配置 ==================== const datasourceOptions = getDatasourceOptions(); const visibilityOptions = VISIBILITY_OPTIONS; const formatOptions = FORMAT_OPTIONS; const sceneOptions = Vue.computed(() => getSceneOptions(state.form.datasource)); const editSceneOptions = Vue.computed(() => getSceneOptions(state.edit.datasource)); /** * 获取订单类型选项 * @param {string} datasource - 数据源 * @returns {Array} 订单类型选项 */ const orderTypeOptionsFor = (datasource) => getOrderTypeOptions(datasource); /** * 获取数据源标签 * @param {string} value - 数据源值 * @returns {string} 数据源标签 */ const dsLabel = (value) => getDatasourceLabel(value); // ==================== 树形选择器引用 ==================== const createFieldsTree = Vue.ref(null); const editFieldsTree = Vue.ref(null); /** * 树形选择器复选框变化处理 * @param {string} kind - 操作类型: 'create' | 'edit' */ const onTreeCheck = (kind) => { if (kind !== 'create' && kind !== 'edit') { console.warn('onTreeCheck: 无效的 kind 参数', kind); return; } const tree = kind === 'create' ? createFieldsTree.value : editFieldsTree.value; if (!tree) return; const checkedKeys = tree.getCheckedKeys(); const treeData = kind === 'create' ? fieldTreeData.value : editFieldTreeData.value; const paths = TreeUtils.checkedKeysToLeafPaths(checkedKeys, treeData); if (kind === 'create') { state.form.fieldsSel = paths; } else { state.edit.fieldsSel = paths; } }; /** * 设置树形选择器选中状态 * @param {string} kind - 操作类型 * @param {Array} values - 选中值 */ const setTreeChecked = (kind, values) => { const tree = kind === 'create' ? createFieldsTree.value : editFieldsTree.value; if (!tree || !values || !Array.isArray(values)) { console.log(`[设置树勾选] 跳过: kind=${kind}, tree存在=${!!tree}, values类型=${Array.isArray(values) ? 'array' : typeof values}`); return; } const treeData = kind === 'create' ? fieldTreeData.value : editFieldTreeData.value; console.log(`[设置树勾选] kind=${kind}, values数量=${values.length}, 树数据存在=${!!treeData}`); const keys = TreeUtils.pathsToNodeKeys(values, treeData); console.log(`[设置树勾选] 转换后keys数量=${keys.length}`); Vue.nextTick(() => { setTimeout(() => { try { if (keys.length > 0 && tree) { console.log(`[设置树勾选] 调用 setCheckedKeys,keys数量: ${keys.length}`); tree.setCheckedKeys(keys); console.log(`[设置树勾选] setCheckedKeys 调用成功`); } else { console.warn(`[设置树勾选] 未调用 setCheckedKeys: keys.length=${keys.length}, tree存在=${!!tree}`); } } catch (error) { console.warn('[设置树勾选] 设置树形选择器选中状态失败:', error); } }, CONSTANTS.TREE_RENDER_DELAY); }); }; // 监听字段选择变化 Vue.watch(() => state.form.fieldsSel, (newVal) => { if (newVal?.length > 0) { Vue.nextTick(() => setTreeChecked('create', newVal)); } }); Vue.watch(() => state.edit.fieldsSel, (newVal) => { if (newVal?.length > 0) { Vue.nextTick(() => setTreeChecked('edit', newVal)); } }); // ==================== 表单验证规则 ==================== const createRules = ValidationRules.createTemplateRules(); const editRules = ValidationRules.createEditRules(); const exportRules = ValidationRules.createExportRules(); // ==================== 表单引用 ==================== const createFormRef = Vue.ref(null); const editFormRef = Vue.ref(null); const exportFormRef = Vue.ref(null); // ==================== 选项数据 ==================== const creatorOptions = Vue.ref([]); const ymtCreatorOptions = Vue.ref([]); const resellerOptions = Vue.ref([]); const planOptions = Vue.ref([]); const ymtMerchantOptions = Vue.ref([]); const ymtActivityOptions = Vue.ref([]); /** * 加载创建者列表 */ const loadCreators = async () => { try { creatorOptions.value = await Api.fetchCreators(); } catch (error) { console.error('加载创建者列表失败:', error); creatorOptions.value = []; } }; /** * 加载易码通用户列表 */ const loadYmtCreators = async () => { try { const userId = Api.getUserId(); ymtCreatorOptions.value = await Api.fetchYmtUsers(userId || undefined); } catch (error) { console.error('加载易码通用户列表失败:', error); ymtCreatorOptions.value = []; } }; /** * 加载分销商列表 */ const loadResellers = async () => { const ids = state.exportForm.creatorIds; if (!ids?.length) { resellerOptions.value = []; return; } try { resellerOptions.value = await Api.fetchResellers(ids); } catch (error) { console.error('加载分销商列表失败:', error); resellerOptions.value = []; } }; /** * 加载易码通客户列表 */ const loadYmtMerchants = async () => { const userId = state.exportForm.ymtCreatorId; if (!userId) { ymtMerchantOptions.value = []; return; } try { ymtMerchantOptions.value = await Api.fetchYmtMerchants(userId); } catch (error) { console.error('加载客户列表失败:', error); ymtMerchantOptions.value = []; } }; /** * 加载易码通活动列表 */ const loadYmtActivities = async () => { const merchantId = state.exportForm.ymtMerchantId; if (!merchantId) { ymtActivityOptions.value = []; return; } try { ymtActivityOptions.value = await Api.fetchYmtActivities(merchantId); } catch (error) { console.error('加载活动列表失败:', error); ymtActivityOptions.value = []; } }; /** * 加载计划列表 */ const loadPlans = async () => { const resellerId = state.exportForm.resellerId; if (!resellerId) { planOptions.value = []; return; } try { planOptions.value = await Api.fetchPlans(resellerId); } catch (error) { console.error('加载计划列表失败:', error); planOptions.value = []; } }; // ==================== 导出相关计算属性 ==================== const hasCreators = Vue.computed(() => state.exportForm.creatorIds?.length > 0); const hasReseller = Vue.computed(() => !!state.exportForm.resellerId); const hasPlan = Vue.computed(() => !!state.exportForm.planId); const hasKeyBatch = Vue.computed(() => !!state.exportForm.keyBatchId); const hasCodeBatch = Vue.computed(() => !!state.exportForm.codeBatchId); const exportType = Vue.computed(() => { const filters = state.exportTpl?.filters; if (!filters) return null; if (filters.type_eq != null) return Number(filters.type_eq); if (Array.isArray(filters.type_in) && filters.type_in.length === 1) { return Number(filters.type_in[0]); } return null; }); const exportTypeList = Vue.computed(() => { const filters = state.exportTpl?.filters; if (!filters) return []; if (Array.isArray(filters.type_in) && filters.type_in.length) { return filters.type_in.map(Number); } if (filters.type_eq != null) return [Number(filters.type_eq)]; return []; }); const isOrder = Vue.computed(() => { const mainTable = state.exportTpl?.main_table; return mainTable === 'order' || mainTable === 'order_info'; }); const exportTitle = Vue.computed(() => { let title = '执行导出'; const mainTable = state.exportTpl?.main_table; if (mainTable) { title += ' - ' + getSceneLabel(mainTable); if (mainTable === 'order' || mainTable === 'order_info') { const datasource = state.exportTpl?.datasource; const labels = exportTypeList.value .map(type => getOrderTypeLabel(datasource, type)) .filter(Boolean); if (labels.length) { title += ' - 订单类型:' + labels.join('、'); } } } return title; }); // ==================== 模板管理 ==================== /** * 加载模板列表 */ const loadTemplates = async () => { try { state.templates = await Api.fetchTemplates(); } catch (error) { showMessage('加载模板失败', 'error'); state.templates = []; } }; /** * 创建模板 */ const createTemplate = async () => { const formRef = createFormRef.value; const valid = formRef ? await formRef.validate().catch(() => false) : true; if (!valid) { showMessage('请完善必填项', 'error'); return; } let fields = []; const { datasource, main_table, fieldsSel, fieldsRaw, orderType, name, file_format, visibility } = state.form; if (fieldsSel?.length) { fields = fieldsManager.convertPathsToFields(fieldsSel, datasource, main_table); } else { const recommended = recommendedMeta.value; if (recommended?.length) { fields = recommended; } else { fields = getDefaultFields(datasource); } } const payload = { name, datasource, main_table: getMainTable(datasource), fields, filters: { type_eq: Number(orderType) }, file_format, visibility, owner_id: Api.getUserId() ? Number(Api.getUserId()) : 0 }; try { await Api.createTemplate(payload); showMessage('创建成功'); state.createVisible = false; loadTemplates(); } catch (error) { showMessage(error.message || '创建失败', 'error'); } }; /** * 打开编辑对话框 * @param {Object} row - 模板行数据 */ const openEdit = async (row) => { state.edit.id = row.id; try { const template = await Api.fetchTemplateDetail(row.id); state.edit.name = template.name || row.name || ''; state.edit.datasource = template.datasource || row.datasource || 'marketing'; state.edit.main_table = template.main_table || row.main_table || 'order'; state.edit.file_format = template.file_format || row.file_format || 'xlsx'; state.edit.visibility = template.visibility || row.visibility || 'private'; const filters = template.filters || {}; if (filters.type_eq != null) { state.edit.orderType = Number(filters.type_eq); } else if (Array.isArray(filters.type_in) && filters.type_in.length === 1) { state.edit.orderType = Number(filters.type_in[0]); } else { state.edit.orderType = getDefaultOrderType(state.edit.datasource); } const fields = Array.isArray(template.fields) ? template.fields : []; state.editVisible = true; await Vue.nextTick(); await loadFieldsMetadata(state.edit.datasource, state.edit.orderType); await Vue.nextTick(); const mainTable = state.edit.main_table || 'order'; const paths = fieldsManager.deduplicatePaths( fieldsManager.convertFieldsToPaths(fields, state.edit.datasource, mainTable) ); state.edit.fieldsSel = paths; await Vue.nextTick(); setTimeout(() => setTreeChecked('edit', paths), CONSTANTS.TREE_EDIT_RENDER_DELAY); } catch (error) { console.error('加载模板详情失败:', error); state.edit.name = row.name; state.edit.datasource = row.datasource || 'marketing'; state.edit.main_table = row.main_table || 'order'; state.edit.file_format = row.file_format || 'xlsx'; state.edit.visibility = row.visibility || 'private'; state.edit.orderType = getDefaultOrderType(state.edit.datasource); state.edit.fieldsSel = []; state.editVisible = true; } }; /** * 保存编辑 */ const saveEdit = async () => { const formRef = editFormRef.value; const valid = formRef ? await formRef.validate().catch(() => false) : true; if (!valid) { showMessage('请完善必填项', 'error'); return; } const { id, name, datasource, main_table, fieldsSel, visibility, file_format, orderType } = state.edit; let fields = []; if (fieldsSel?.length) { fields = fieldsManager.convertPathsToFields(fieldsSel, datasource, main_table || 'order'); } else { fields = getDefaultFields(datasource); } if (!fields.length) { showMessage('请至少选择一个字段', 'error'); return; } const payload = { name, visibility, file_format, fields, filters: { type_eq: Number(orderType || 1) }, main_table: 'order' }; try { await Api.updateTemplate(id, payload); showMessage('保存成功'); state.editVisible = false; loadTemplates(); } catch (error) { showMessage(error.message || '保存失败', 'error'); } }; /** * 删除模板(带二次确认) * @param {number} id - 模板 ID */ const removeTemplate = async (id) => { // 第一次:弹出确认框,避免误删 try { if (window.ElementPlus?.ElMessageBox) { await window.ElementPlus.ElMessageBox.confirm( '删除后不可恢复,确认要删除该导出模板吗?', '删除确认', { type: 'warning', confirmButtonText: '确认删除', cancelButtonText: '再想想' } ); } else { const ok = window.confirm('删除后不可恢复,确认要删除该导出模板吗?'); if (!ok) return; } } catch { // 用户取消 return; } // 第二次:真正调用后端删除 try { await Api.deleteTemplate(id); showMessage('删除成功'); loadTemplates(); } catch (error) { showMessage(error.message || '删除失败', 'error'); } }; // ==================== 导出任务 ==================== /** * 打开导出对话框 * @param {Object} row - 模板行数据 */ const openExport = async (row) => { state.exportForm.tplId = row.id; try { const template = await Api.fetchTemplateDetail(row.id); state.exportTpl = template; } catch (error) { console.error('加载模板详情失败:', error); state.exportTpl = { id: null, filters: {}, main_table: '', fields: [], datasource: '', file_format: '' }; } state.exportForm.datasource = state.exportTpl.datasource || row.datasource || 'marketing'; state.exportForm.file_format = state.exportTpl.file_format || row.file_format || 'xlsx'; if (state.exportForm.datasource === 'marketing') { loadCreators(); const userId = Api.getUserId(); if (userId) { const parts = String(userId).split(',').map(s => s.trim()).filter(Boolean); state.exportForm.creatorIds = parts.length > 1 ? parts.map(Number) : [Number(userId)]; } } if (state.exportForm.datasource === 'ymt') { await loadYmtCreators(); await loadYmtMerchants(); await loadYmtActivities(); const userId = Api.getUserId(); if (userId) { const first = String(userId).split(',').map(s => s.trim()).filter(Boolean)[0]; if (first) { state.exportForm.ymtCreatorId = Number(first); } } } if (!state.exportForm.dateRange?.length) { state.exportForm.dateRange = getMonthRange(-1); } state.exportVisible = true; }; /** * 提交导出任务 */ const submitExport = async () => { const formRef = exportFormRef.value; const valid = formRef ? await formRef.validate().catch(() => false) : true; if (!valid) { showMessage('请完善必填项', 'error'); return; } state.exportSubmitting = true; showMessage('估算中', 'info'); try { const { tplId, dateRange, datasource, file_format, planId, resellerId, voucherChannelActivityId, creatorIds, creatorIdsRaw, ymtCreatorId, ymtMerchantId, ymtActivityId } = state.exportForm; const filters = {}; const typeValue = exportType.value; if (typeValue != null) { filters.type_eq = Number(typeValue); } if (dateRange?.length === 2) { filters.create_time_between = [dateRange[0], dateRange[1]]; } if (planId) filters.plan_id_eq = Number(planId); if (resellerId) filters.reseller_id_eq = Number(resellerId); if (voucherChannelActivityId) { const type = exportType.value; if ((datasource === 'marketing' && type === 2) || (datasource === 'ymt' && type === 3)) { filters.order_voucher_channel_activity_id_eq = voucherChannelActivityId; } } if (creatorIds?.length) { filters.creator_in = creatorIds.map(Number); } else if (creatorIdsRaw) { const arr = String(creatorIdsRaw).split(',').map(s => s.trim()).filter(Boolean); if (arr.length) filters.creator_in = arr; } if (datasource === 'ymt') { if (String(ymtCreatorId).trim()) { filters.creator_in = [Number(ymtCreatorId)]; } if (String(ymtMerchantId).trim()) { filters.reseller_id_eq = Number(ymtMerchantId); } if (String(ymtActivityId).trim()) { filters.plan_id_eq = Number(ymtActivityId); } } const payload = { template_id: Number(tplId), requested_by: 1, permission: {}, options: {}, filters, file_format, datasource }; const result = await Api.createExportJob(payload); const jobId = result?.data?.id ?? result?.id; state.exportVisible = false; if (jobId) { state.jobsTplId = Number(tplId); state.jobsVisible = true; loadJobs(1); startJobsPolling(); } else { showMessage('任务创建返回异常', 'error'); } } catch (error) { showMessage(error.message || '导出失败', 'error'); } finally { state.exportSubmitting = false; } }; // ==================== 任务管理 ==================== let jobsPollTimer = null; /** * 加载任务列表 * @param {number} [page] - 页码 */ const loadJobs = async (page) => { page = page || state.jobsPage; try { const { items, total, page: currentPage } = await Api.fetchJobs({ page, pageSize: state.jobsPageSize, templateId: state.jobsTplId }); state.jobs = items; state.jobsTotal = total; state.jobsPage = currentPage; } catch (error) { console.error('加载任务列表失败:', error); state.jobs = []; } }; /** * 检查是否所有任务都已完成 */ const checkAndStopPollingIfComplete = () => { const hasRunningJob = state.jobs.some( job => job.status === 'queued' || job.status === 'running' ); if (!hasRunningJob && state.jobs.length > 0) { stopJobsPolling(); } }; /** * 启动任务轮询 */ const startJobsPolling = () => { if (jobsPollTimer) return; jobsPollTimer = setInterval(async () => { if (state.jobsVisible) { await loadJobs(state.jobsPage); checkAndStopPollingIfComplete(); } }, CONSTANTS.JOBS_POLL_INTERVAL); }; /** * 停止任务轮询 */ const stopJobsPolling = () => { if (jobsPollTimer) { clearInterval(jobsPollTimer); jobsPollTimer = null; } }; /** * 打开任务列表 * @param {Object} row - 模板行数据 */ const openJobs = (row) => { state.jobsTplId = row.id; state.jobsVisible = true; loadJobs(1); startJobsPolling(); }; /** * 关闭任务列表 */ const closeJobs = () => { state.jobsVisible = false; stopJobsPolling(); }; /** * 计算任务进度 * @param {Object} row - 任务行数据 * @returns {string} 进度描述 */ const jobPercent = (row) => calculateJobProgress(row); /** * 下载文件 * @param {number} id - 任务 ID */ const download = (id) => { window.open(Api.getDownloadUrl(id), '_blank'); }; /** * 打开 SQL 预览 * @param {number} id - 任务 ID */ const openSQL = async (id) => { try { state.sqlExplainDesc = ''; state.sqlText = await Api.fetchJobSql(id); try { const job = await Api.fetchJobDetail(id); state.sqlExplainDesc = job?.eval_desc || job?.eval_status || ''; } catch { const row = state.jobs.find(r => Number(r.id) === Number(id)); state.sqlExplainDesc = row?.eval_desc || row?.eval_status || ''; } state.sqlVisible = true; } catch (error) { console.error('加载SQL失败:', error); state.sqlText = ''; state.sqlExplainDesc = ''; state.sqlVisible = false; showMessage('加载SQL失败', 'error'); } }; // ==================== 对话框尺寸管理 ==================== /** * 调整对话框尺寸 * @param {string} kind - 对话框类型 * @param {number} delta - 变化量 */ const resizeDialog = (kind, delta) => { if (kind === 'create') { const current = parseWidth(state.createWidth, CONSTANTS.DIALOG_DEFAULT_WIDTH); const next = clampDialogWidth(current + delta); state.createWidth = next; localStorage.setItem('tplDialogWidth', next); } else if (kind === 'edit') { const current = parseWidth(state.editWidth, CONSTANTS.DIALOG_EDIT_DEFAULT_WIDTH); const next = clampDialogWidth(current + delta); state.editWidth = next; localStorage.setItem('tplEditDialogWidth', next); } }; // ==================== 监听器 ==================== // 数据源变化 Vue.watch(() => state.form.datasource, async (datasource) => { state.form.fieldsSel = []; state.form.main_table = getMainTable(datasource); state.form.orderType = getDefaultOrderType(datasource); await loadFieldsMetadata(datasource, state.form.orderType); }); // 订单类型变化 Vue.watch(() => state.form.orderType, async () => { state.form.fieldsSel = []; await loadFieldsMetadata(state.form.datasource, state.form.orderType); }); // 编辑数据源变化 Vue.watch(() => state.edit.datasource, async (datasource) => { state.edit.fieldsSel = []; state.edit.main_table = getMainTable(datasource); if (!state.edit.orderType) { state.edit.orderType = getDefaultOrderType(datasource); } await loadFieldsMetadata(datasource, state.edit.orderType); }); // 编辑订单类型变化 Vue.watch(() => state.edit.orderType, async () => { state.edit.fieldsSel = []; await loadFieldsMetadata(state.edit.datasource, state.edit.orderType); }); // 导出筛选条件变化 Vue.watch(() => state.exportForm.creatorIds, () => { state.exportForm.resellerId = null; state.exportForm.planId = null; state.exportForm.keyBatchId = null; state.exportForm.codeBatchId = null; loadResellers(); }); Vue.watch(() => state.exportForm.ymtCreatorId, () => { state.exportForm.ymtMerchantId = null; state.exportForm.ymtActivityId = null; loadYmtMerchants(); }); Vue.watch(() => state.exportForm.ymtMerchantId, () => { state.exportForm.ymtActivityId = null; loadYmtActivities(); }); Vue.watch(() => state.exportForm.resellerId, () => { state.exportForm.planId = null; state.exportForm.keyBatchId = null; state.exportForm.codeBatchId = null; loadPlans(); }); Vue.watch(() => state.exportForm.planId, () => { state.exportForm.keyBatchId = null; state.exportForm.codeBatchId = null; }); Vue.watch(() => state.exportForm.keyBatchId, () => { state.exportForm.codeBatchId = null; }); // ==================== 初始化 ==================== loadTemplates(); loadFieldsMetadata(state.form.datasource, state.form.orderType); // 组件销毁时清理 Vue.onUnmounted(() => { stopJobsPolling(); }); // ==================== 返回 ==================== return { ...Vue.toRefs(state), // 选项配置 visibilityOptions, formatOptions, datasourceOptions, sceneOptions, editSceneOptions, // 模板管理 loadTemplates, createTemplate, openEdit, saveEdit, removeTemplate, // 导出管理 openExport, submitExport, // 任务管理 loadJobs, openJobs, closeJobs, download, openSQL, jobPercent, // 对话框 resizeDialog, // 验证规则 createRules, editRules, exportRules, // 表单引用 createFormRef, editFormRef, exportFormRef, // 树形选择器 createFieldsTree, editFieldsTree, fieldTreeData, editFieldTreeData, onTreeCheck, setTreeChecked, // 工具函数 dsLabel, orderTypeOptionsFor, fmtDT: formatDateTime, // 计算属性 exportType, isOrder, exportTitle, hasUserId, currentUserId, hasCreators, hasReseller, hasPlan, hasKeyBatch, hasCodeBatch, // 选项数据 creatorOptions, ymtCreatorOptions, ymtMerchantOptions, ymtActivityOptions, resellerOptions, planOptions }; } }); app.use(ElementPlus); app.mount('#app'); })();