From 26989cc391a96cc36f93d3c7f0bceeb154258319 Mon Sep 17 00:00:00 2001 From: zhouyonggao <1971162852@qq.com> Date: Wed, 17 Dec 2025 10:03:26 +0800 Subject: [PATCH] =?UTF-8?q?refactor(main):=20=E9=87=8D=E6=9E=84=E4=B8=BB?= =?UTF-8?q?=E5=85=A5=E5=8F=A3=E4=BB=A3=E7=A0=81=E6=8F=90=E5=8D=87=E5=8F=AF?= =?UTF-8?q?=E8=AF=BB=E6=80=A7=E5=92=8C=E5=8F=AF=E7=BB=B4=E6=8A=A4=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 使用模块化架构替代内联常量和工具函数引用提升结构清晰度 - 统一调用外部 Api 服务封装,简化接口调用流程 - 优化字段元数据加载逻辑,支持缓存及异常处理恢复 - 重构树形选择器操作,提升选中状态处理的可靠性和性能 - 采用状态管理模块创建响应式状态,拆分表单验证规则实现 - 使用计算属性优化表单及导出相关的动态选项展示 - 重新实现异步数据加载函数,统一错误处理并清空异常状态 - 清理和合并废弃或重复代码,改善代码整洁度和可维护性 --- web/index.html | 2 +- web/main.js | 1642 +++++++++++++--------------------------- web/main.refactored.js | 914 ---------------------- 3 files changed, 523 insertions(+), 2035 deletions(-) delete mode 100644 web/main.refactored.js diff --git a/web/index.html b/web/index.html index 2c5a219..00da11c 100644 --- a/web/index.html +++ b/web/index.html @@ -318,6 +318,6 @@ - + diff --git a/web/main.js b/web/main.js index 66171a5..dc5e31a 100644 --- a/web/main.js +++ b/web/main.js @@ -1,373 +1,101 @@ +/** + * 营销系统数据导出工具 - 主入口 + * @description 使用模块化架构重构,提升可读性和扩展性 + */ + +;(function() { +'use strict'; + const { createApp, reactive } = Vue; -// ==================== 常量定义 ==================== -const CONSTANTS = { - // 轮询间隔(毫秒) - JOBS_POLL_INTERVAL: 1000, - // 树形选择器渲染延迟(毫秒) - TREE_RENDER_DELAY: 100, - // 编辑模式树渲染延迟(毫秒) - TREE_EDIT_RENDER_DELAY: 300, - // 对话框最小宽度 - DIALOG_MIN_WIDTH: 500, - // 对话框最大宽度 - DIALOG_MAX_WIDTH: 1400, - // 默认对话框宽度 - DIALOG_DEFAULT_WIDTH: 900, - // 默认编辑对话框宽度 - DIALOG_EDIT_DEFAULT_WIDTH: 900 -}; +// ==================== 模块引用 ==================== +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 API_BASE = (window.__API_BASE__ && String(window.__API_BASE__).trim()) - ? String(window.__API_BASE__).trim() - : (typeof location !== 'undefined' ? location.origin : 'http://localhost:8077'); - - const getUserId = () => { - const sp = new URLSearchParams(window.location.search || ''); - const v = sp.get('userId') || sp.get('userid') || sp.get('user_id'); - return v && String(v).trim() ? String(v).trim() : ''; - }; + // ==================== 状态初始化 ==================== + const state = reactive(createAppState()); - const getMerchantId = () => { - const sp = new URLSearchParams(window.location.search || ''); - const v = sp.get('merchantId') || sp.get('merchantid') || sp.get('merchant_id'); - return v && String(v).trim() ? String(v).trim() : ''; - }; - - const qsUser = () => { - const uid = getUserId(); - const mid = getMerchantId(); - const parts = []; - if (uid) parts.push('userId=' + encodeURIComponent(uid)); - if (mid) parts.push('merchantId=' + encodeURIComponent(mid)); - return parts.length ? ('?' + parts.join('&')) : ''; - }; - - const msg = (text, type = 'success') => ElementPlus.ElMessage({ message: text, type }); - - const fmtDT = (d) => { - const pad = (n) => String(n).padStart(2, '0'); - return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; - }; - - const monthRange = (offsetMonths = -1) => { - const now = new Date(); - const base = new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0); - const start = new Date(base.getFullYear(), base.getMonth() + offsetMonths, 1, 0, 0, 0); - const end = new Date(start.getFullYear(), start.getMonth() + 1, 0, 23, 59, 59); - return [fmtDT(start), fmtDT(end)]; - }; - - // ==================== 状态管理 ==================== - const state = reactive({ - templates: [], - jobs: [], - jobsVisible: false, - jobsTplId: null, - jobsPage: 1, - jobsPageSize: 10, - jobsTotal: 0, - sqlVisible: false, - sqlText: '', - sqlExplainDesc: '', - job: {}, - form: { - name: '', - datasource: 'marketing', - main_table: 'order', - orderType: 1, - fieldsRaw: 'order_number,creator,out_trade_no,type,status,contract_price,num,total,pay_amount,create_time', - fieldsSel: [], - file_format: 'xlsx', - visibility: 'private' - }, - createVisible: false, - editVisible: false, - exportVisible: false, - exportSubmitting: false, - createWidth: localStorage.getItem('tplDialogWidth') || CONSTANTS.DIALOG_DEFAULT_WIDTH + 'px', - editWidth: localStorage.getItem('tplEditDialogWidth') || CONSTANTS.DIALOG_EDIT_DEFAULT_WIDTH + 'px', - edit: { - id: null, - name: '', - datasource: 'marketing', - main_table: 'order', - orderType: 1, - fieldsSel: [], - visibility: 'private', - file_format: 'xlsx' - }, - exportForm: { - tplId: null, - datasource: 'marketing', - file_format: 'xlsx', - dateRange: [], - creatorIds: [], - creatorIdsRaw: '', - resellerId: null, - planId: null, - keyBatchId: null, - codeBatchId: null, - voucherChannelActivityId: '', - ymtCreatorId: '', - ymtMerchantId: '', - ymtActivityId: '' - }, - exportTpl: { - id: null, - filters: {}, - main_table: '', - fields: [], - datasource: '', - file_format: '' - } - }); - // ==================== 计算属性 ==================== - const hasUserId = Vue.computed(() => !!getUserId()); + const hasUserId = Vue.computed(() => !!Api.getUserId()); const currentUserId = Vue.computed(() => { - const v = getUserId(); - return v ? Number(v) : null; + const userId = Api.getUserId(); + return userId ? Number(userId) : null; }); - + // ==================== 字段元数据管理 ==================== - const metaFM = Vue.ref({}); const metaTableLabels = Vue.ref({}); const recommendedMeta = Vue.ref([]); - + /** - * 加载字段元数据和推荐字段(合并为单次 API 调用) - * @param {string} ds - 数据源 - * @param {number} type - 订单类型 + * 加载字段元数据 + * @param {string} datasource - 数据源 + * @param {number} orderType - 订单类型 * @returns {Promise} 推荐字段列表 */ - const loadFieldsMetaAndRecommended = async (ds, type) => { + const loadFieldsMetadata = async (datasource, orderType) => { try { - const res = await fetch(API_BASE + '/api/metadata/fields?datasource=' + encodeURIComponent(ds) + '&order_type=' + encodeURIComponent(String(type || 0))); - if (!res.ok) { - throw new Error(`加载字段元数据失败: ${res.status}`); - } - const data = await res.json(); - - // 处理表字段映射 - const tables = Array.isArray(data?.data?.tables) ? data.data.tables : (Array.isArray(data?.tables) ? data.tables : []); - const m = {}; - const tblLabels = {}; - tables.forEach(t => { - const arr = Array.isArray(t.fields) ? t.fields : []; - const visibleFields = arr.filter(it => !it.hidden); - m[t.table] = visibleFields.map(it => ({ value: it.field, label: it.label })); - if (t.label) tblLabels[t.table] = t.label; - }); - metaFM.value = m; - metaTableLabels.value = tblLabels; - - // 处理推荐字段 - const rec = Array.isArray(data?.data?.recommended) ? data.data.recommended : (Array.isArray(data?.recommended) ? data.recommended : []); - recommendedMeta.value = rec; - return rec; - } catch (e) { - console.error('加载字段元数据失败:', e); - metaFM.value = {}; + const { tables, recommended } = await Api.fetchFieldsMetadata(datasource, orderType); + fieldsManager.updateMetadata(tables, recommended); + metaTableLabels.value = fieldsManager.tableLabels; + recommendedMeta.value = recommended; + return recommended; + } catch (error) { + console.error('加载字段元数据失败:', error); + fieldsManager.updateMetadata([], []); metaTableLabels.value = {}; recommendedMeta.value = []; return []; } }; - - // 兼容旧接口 - const loadFieldsMeta = (ds, type) => loadFieldsMetaAndRecommended(ds, type); - const loadRecommendedFields = (ds, orderType) => loadFieldsMetaAndRecommended(ds, orderType); - - const getFieldsMap = (ds) => metaFM.value || {}; - - // ==================== 表标签映射 ==================== - const TABLE_LABELS = { - order: '订单主表', - order_detail: '订单详情', - order_cash: '红包订单', - order_voucher: '立减金订单', - plan: '活动计划', - key_batch: 'key批次', - code_batch: '兑换码批次', - voucher: '立减金', - voucher_batch: '立减金批次', - merchant_key_send: '开放平台发放记录', - order_digit: '直充卡密订单', - merchant: '客户', - activity: '活动', - goods_voucher_batch: '立减金批次表', - goods_voucher_subject_config: '立减金主体配置' - }; - const tableLabel = (table) => metaTableLabels.value[table] || TABLE_LABELS[table] || table; - - // ==================== 字段选项构建 ==================== - const buildFieldNode = (table, children = []) => ({ - value: table, - label: tableLabel(table), - children - }); - - const buildYmtOrderChildren = (FM, type) => { - const orderFields = FM['order'] || []; - const ch = [...orderFields.map(f => ({ value: f.value, label: f.label }))]; - ch.push(buildFieldNode('merchant', FM['merchant'] || [])); - ch.push(buildFieldNode('activity', FM['activity'] || [])); - - if (type === 2) { - ch.push(buildFieldNode('order_digit', FM['order_digit'] || [])); - } else if (type === 3) { - ch.push(buildFieldNode('order_voucher', FM['order_voucher'] || [])); - ch.push(buildFieldNode('goods_voucher_batch', FM['goods_voucher_batch'] || [])); - ch.push(buildFieldNode('goods_voucher_subject_config', FM['goods_voucher_subject_config'] || [])); - } else if (type === 1) { - ch.push(buildFieldNode('order_cash', FM['order_cash'] || [])); - } else { - ch.push(buildFieldNode('order_voucher', FM['order_voucher'] || [])); - ch.push(buildFieldNode('order_cash', FM['order_cash'] || [])); - ch.push(buildFieldNode('order_digit', FM['order_digit'] || [])); - ch.push(buildFieldNode('goods_voucher_batch', FM['goods_voucher_batch'] || [])); - ch.push(buildFieldNode('goods_voucher_subject_config', FM['goods_voucher_subject_config'] || [])); - } - return ch; - }; - - const buildMarketingOrderChildren = (FM, type) => { - const orderFields = FM['order'] || []; - const ch = [...orderFields.map(f => ({ value: f.value, label: f.label }))]; - ch.push(buildFieldNode('order_detail', FM['order_detail'] || [])); - - const planChildren = [ - ...(FM['plan'] || []).map(f => ({ value: f.value, label: f.label })), - buildFieldNode('key_batch', [ - ...(FM['key_batch'] || []).map(f => ({ value: f.value, label: f.label })), - buildFieldNode('code_batch', FM['code_batch'] || []) - ]) - ]; - - const voucherChildren = [ - ...(FM['order_voucher'] || []).map(f => ({ value: f.value, label: f.label })), - buildFieldNode('voucher', [ - ...(FM['voucher'] || []).map(f => ({ value: f.value, label: f.label })), - buildFieldNode('voucher_batch', FM['voucher_batch'] || []) - ]) - ]; - - if (type === 1) { - ch.push(buildFieldNode('plan', planChildren)); - ch.push(buildFieldNode('merchant_key_send', FM['merchant_key_send'] || [])); - } else if (type === 2) { - ch.push(buildFieldNode('order_voucher', voucherChildren)); - ch.push(buildFieldNode('plan', planChildren)); - } else if (type === 3) { - ch.push(buildFieldNode('order_cash', FM['order_cash'] || [])); - ch.push(buildFieldNode('plan', planChildren)); - } else { - ch.push(buildFieldNode('order_cash', FM['order_cash'] || [])); - ch.push(buildFieldNode('order_voucher', voucherChildren)); - ch.push(buildFieldNode('plan', planChildren)); - ch.push(buildFieldNode('merchant_key_send', FM['merchant_key_send'] || [])); - } - return ch; - }; - - // 树形选择器数据(与级联选择器兼容) + // ==================== 树形选择器数据 ==================== const fieldTreeData = Vue.computed(() => { - const ds = state.form.datasource; - const FM = getFieldsMap(ds); - const type = Number(state.form.orderType || 0); - - if (ds === 'ymt') { - const orderNode = buildFieldNode('order', buildYmtOrderChildren(FM, type)); - return type ? [orderNode] : [{ value: 'scene_order', label: '订单数据', children: [orderNode] }]; - } else { - const orderNode = buildFieldNode('order', buildMarketingOrderChildren(FM, type)); - return type ? [orderNode] : [{ value: 'scene_order', label: '订单数据', children: [orderNode] }]; - } + return fieldsManager.buildFieldTree( + state.form.datasource, + state.form.orderType + ); }); - + const editFieldTreeData = Vue.computed(() => { - const ds = state.edit.datasource; - const FM = getFieldsMap(ds); - const type = Number(state.edit.orderType || 0); - const mainTable = state.edit.main_table || 'order'; - const actualMainTable = (ds === 'ymt' && mainTable === 'order_info') ? 'order' : mainTable; - - if (ds === 'ymt') { - const orderFields = FM[actualMainTable] || []; - const ch = [...orderFields.map(f => ({ value: f.value, label: f.label }))]; - const ymtChildren = buildYmtOrderChildren(FM, type); - ch.push(...ymtChildren.slice(orderFields.length)); - return [buildFieldNode(actualMainTable, ch)]; - } else { - return [buildFieldNode(mainTable, buildMarketingOrderChildren(FM, type))]; - } + return fieldsManager.buildFieldTree( + state.edit.datasource, + state.edit.orderType, + state.edit.main_table + ); }); - - // 保留级联选择器的选项(用于兼容) - const fieldOptionsDynamic = fieldTreeData; - const editFieldOptionsDynamic = editFieldTreeData; - + // ==================== 选项配置 ==================== - const sceneOptions = Vue.computed(() => { - const ds = state.form.datasource; - return [{ label: '订单数据', value: ds === 'ymt' ? 'order_info' : 'order' }]; - }); - - const editSceneOptions = Vue.computed(() => { - const ds = state.edit.datasource; - return [{ label: '订单数据', value: ds === 'ymt' ? 'order_info' : 'order' }]; - }); - - const visibilityOptions = [ - { label: '个人', value: 'private' }, - { label: '公共', value: 'public' } - ]; - - const formatOptions = [ - { label: 'XLSX', value: 'xlsx' }, - { label: 'CSV', value: 'csv' } - ]; - - const datasourceOptions = [ - { label: '营销系统', value: 'marketing' }, - { label: '易码通', value: 'ymt' } - ]; - - const orderTypeOptionsFor = (ds) => { - if (ds === 'ymt') { - return [ - { label: '直充卡密', value: 2 }, - { label: '立减金', value: 3 }, - { label: '红包', value: 1 } - ]; - } - return [ - { label: '直充卡密', value: 1 }, - { label: '立减金', value: 2 }, - { label: '红包', value: 3 } - ]; - }; - - const dsLabel = (v) => { - if (v === 'marketing') return '营销系统'; - if (v === 'ymt') return '易码通'; - return v || ''; - }; - - const DEFAULT_FIELDS = { - marketing: 'order_number,creator,out_trade_no,type,status,contract_price,num,total,pay_amount,create_time', - ymt: 'order_number,creator,out_trade_no,type,status,contract_price,num,pay_amount,create_time' - }; - - // ==================== 树形选择器 ==================== + 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' @@ -377,872 +105,564 @@ const app = createApp({ 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 findNodePath = (nodes, targetKey, currentPath = []) => { - for (const node of nodes) { - const newPath = [...currentPath, node.value]; - if (node.value === targetKey) { - return newPath; - } - if (node.children && node.children.length > 0) { - const found = findNodePath(node.children, targetKey, newPath); - if (found) return found; - } - } - return null; - }; - - // 查找节点(用于判断是否是叶子节点) - const findNode = (nodes, targetKey) => { - for (const node of nodes) { - if (node.value === targetKey) { - return node; - } - if (node.children) { - const found = findNode(node.children, targetKey); - if (found) return found; - } - } - return null; - }; - - // 将选中的节点转换为路径数组格式 - const paths = checkedKeys - .filter(key => { - // 只保留叶子节点(字段节点) - const node = findNode(treeData, key); - return node && (!node.children || node.children.length === 0); - }) - .map(key => { - // 查找节点的完整路径 - const path = findNodePath(treeData, key); - return path || [key]; - }) - .filter(path => path.length >= 2); // 确保路径至少包含表和字段 - - // 更新表单字段选择(使用路径数组格式) + 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.warn('setTreeChecked: 树或值无效', { kind, tree: !!tree, values }); - return; - } - + if (!tree || !values || !Array.isArray(values)) return; + const treeData = kind === 'create' ? fieldTreeData.value : editFieldTreeData.value; - - // 递归查找节点,根据路径数组找到对应的 value - const findNodeByPath = (nodes, path, index = 0) => { - if (!nodes || index >= path.length) return null; - - const target = path[index]; - for (const node of nodes) { - if (node.value === target) { - if (index === path.length - 1) { - // 找到叶子节点 - return node.value; - } else if (node.children && node.children.length > 0) { - // 继续查找子节点 - const found = findNodeByPath(node.children, path, index + 1); - if (found) return found; - } - } - } - return null; - }; - - // 将路径数组转换为节点 key 值 - const keys = values.map(v => { - if (Array.isArray(v)) { - // 路径数组格式,查找对应的节点 value - const nodeValue = findNodeByPath(treeData, v); - if (nodeValue) return nodeValue; - // 如果找不到,尝试使用路径的最后一部分(字段名) - return v[v.length - 1]; - } else if (typeof v === 'string') { - // 已经是字符串格式,可能是 'table.field' 或直接是字段名 - return v; - } - return null; - }).filter(Boolean); - + const keys = TreeUtils.pathsToNodeKeys(values, treeData); + Vue.nextTick(() => { setTimeout(() => { try { if (keys.length > 0 && tree) { tree.setCheckedKeys(keys); } - } catch (e) { - console.warn('设置树形选择器选中状态失败:', e); + } catch (error) { + console.warn('设置树形选择器选中状态失败:', error); } }, CONSTANTS.TREE_RENDER_DELAY); }); }; - - // 监听字段选择变化,同步到树形选择器 + + // 监听字段选择变化 Vue.watch(() => state.form.fieldsSel, (newVal) => { - if (newVal && Array.isArray(newVal) && newVal.length > 0) { - Vue.nextTick(() => { - setTreeChecked('create', newVal); - }); + if (newVal?.length > 0) { + Vue.nextTick(() => setTreeChecked('create', newVal)); } }); - + Vue.watch(() => state.edit.fieldsSel, (newVal) => { - if (newVal && Array.isArray(newVal) && newVal.length > 0) { - Vue.nextTick(() => { - setTreeChecked('edit', newVal); - }); + if (newVal?.length > 0) { + Vue.nextTick(() => setTreeChecked('edit', newVal)); } }); - - // 废弃的函数已移除,如需调用请检查代码 - + // ==================== 表单验证规则 ==================== - const createRules = { - name: [{ required: true, message: '请输入模板名称', trigger: 'blur' }], - datasource: [{ required: true, message: '请选择数据源', trigger: 'change' }], - main_table: [{ required: true, message: '请选择导出场景', trigger: 'change' }], - orderType: [{ required: true, message: '请选择订单类型', trigger: 'change' }], - fieldsSel: [{ - validator: (_rule, val, cb) => { - if (Array.isArray(val) && val.length > 0) { - cb(); - } else { - cb(new Error('请至少选择一个字段')); - } - }, - trigger: 'change' - }], - file_format: [{ required: true, message: '请选择输出格式', trigger: 'change' }], - visibility: [{ required: true, message: '请选择可见性', trigger: 'change' }] - }; - - const editRules = { - name: [{ required: true, message: '请输入模板名称', trigger: 'blur' }], - orderType: [{ required: true, message: '请选择订单类型', trigger: 'change' }], - fieldsSel: [{ - validator: (_rule, val, cb) => { - if (Array.isArray(val) && val.length > 0) { - cb(); - } else { - cb(new Error('请至少选择一个字段')); - } - }, - trigger: 'change' - }] - }; - - const exportRules = { - tplId: [{ required: true, message: '请选择模板', trigger: 'change' }], - dateRange: [{ - validator: (_r, v, cb) => { - if (Array.isArray(v) && v.length === 2) { - cb(); - } else { - cb(new Error('请选择时间范围')); - } - }, - trigger: 'change' - }] - }; - + const createRules = ValidationRules.createTemplateRules(); + const editRules = ValidationRules.createEditRules(); + const exportRules = ValidationRules.createExportRules(); + // ==================== 表单引用 ==================== const createFormRef = Vue.ref(null); - const exportFormRef = 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 { - const res = await fetch(API_BASE + '/api/creators'); - if (!res.ok) { - throw new Error(`加载创建者列表失败: ${res.status}`); - } - const data = await res.json(); - const arr = Array.isArray(data?.data) ? data.data : (Array.isArray(data) ? data : []); - creatorOptions.value = arr.map(it => ({ label: it.name || String(it.id), value: Number(it.id) })); - } catch (e) { - console.error('加载创建者列表失败:', e); + creatorOptions.value = await Api.fetchCreators(); + } catch (error) { + console.error('加载创建者列表失败:', error); creatorOptions.value = []; } }; - + + /** + * 加载易码通用户列表 + */ const loadYmtCreators = async () => { try { - const uid = getUserId(); - const url = API_BASE + '/api/ymt/users?limit=2000' + (uid ? ('&q=' + encodeURIComponent(uid)) : ''); - const res = await fetch(url); - if (!res.ok) { - throw new Error(`加载易码通用户列表失败: ${res.status}`); - } - const data = await res.json(); - const arr = Array.isArray(data?.data) ? data.data : (Array.isArray(data) ? data : []); - ymtCreatorOptions.value = arr.map(it => ({ label: it.name || String(it.id), value: Number(it.id) })); - } catch (e) { - console.error('加载易码通用户列表失败:', e); + const userId = Api.getUserId(); + ymtCreatorOptions.value = await Api.fetchYmtUsers(userId || undefined); + } catch (error) { + console.error('加载易码通用户列表失败:', error); ymtCreatorOptions.value = []; } }; - + + /** + * 加载分销商列表 + */ const loadResellers = async () => { - const ids = Array.isArray(state.exportForm.creatorIds) ? state.exportForm.creatorIds : []; - if (!ids.length) { + const ids = state.exportForm.creatorIds; + if (!ids?.length) { resellerOptions.value = []; return; } try { - const res = await fetch(API_BASE + '/api/resellers?creator=' + ids.join(',')); - if (!res.ok) { - throw new Error(`加载分销商列表失败: ${res.status}`); - } - const data = await res.json(); - const arr = Array.isArray(data?.data) ? data.data : (Array.isArray(data) ? data : []); - resellerOptions.value = arr.map(it => ({ label: (it.name || '') + (it.name ? '' : ''), value: Number(it.id) })); - } catch (e) { - console.error('加载分销商列表失败:', e); + resellerOptions.value = await Api.fetchResellers(ids); + } catch (error) { + console.error('加载分销商列表失败:', error); resellerOptions.value = []; } }; - + + /** + * 加载易码通客户列表 + */ const loadYmtMerchants = async () => { - const uid = state.exportForm.ymtCreatorId; - if (!uid) { + const userId = state.exportForm.ymtCreatorId; + if (!userId) { ymtMerchantOptions.value = []; return; } try { - const qs = new URLSearchParams(); - qs.set('user_id', String(uid)); - qs.set('limit', '2000'); - const res = await fetch(API_BASE + '/api/ymt/merchants?' + qs.toString()); - if (!res.ok) { - throw new Error(`加载客户列表失败: ${res.status}`); - } - const data = await res.json(); - const arr = Array.isArray(data?.data) ? data.data : (Array.isArray(data) ? data : []); - ymtMerchantOptions.value = arr.map(it => ({ label: `${it.id} - ${it.name || ''}`, value: Number(it.id) })); - } catch (e) { - console.error('加载客户列表失败:', e); + ymtMerchantOptions.value = await Api.fetchYmtMerchants(userId); + } catch (error) { + console.error('加载客户列表失败:', error); ymtMerchantOptions.value = []; } }; - + + /** + * 加载易码通活动列表 + */ const loadYmtActivities = async () => { - const mid = state.exportForm.ymtMerchantId; - if (!mid) { + const merchantId = state.exportForm.ymtMerchantId; + if (!merchantId) { ymtActivityOptions.value = []; return; } try { - const qs = new URLSearchParams(); - qs.set('merchant_id', String(mid)); - qs.set('limit', '2000'); - const res = await fetch(API_BASE + '/api/ymt/activities?' + qs.toString()); - if (!res.ok) { - throw new Error(`加载活动列表失败: ${res.status}`); - } - const data = await res.json(); - const arr = Array.isArray(data?.data) ? data.data : (Array.isArray(data) ? data : []); - ymtActivityOptions.value = arr.map(it => ({ label: `${it.id} - ${it.name || ''}`, value: Number(it.id) })); - } catch (e) { - console.error('加载活动列表失败:', e); + ymtActivityOptions.value = await Api.fetchYmtActivities(merchantId); + } catch (error) { + console.error('加载活动列表失败:', error); ymtActivityOptions.value = []; } }; - + + /** + * 加载计划列表 + */ const loadPlans = async () => { - const rid = state.exportForm.resellerId; - if (!rid) { + const resellerId = state.exportForm.resellerId; + if (!resellerId) { planOptions.value = []; return; } try { - const qs = new URLSearchParams(); - qs.set('reseller', String(rid)); - const res = await fetch(API_BASE + '/api/plans?' + qs.toString()); - if (!res.ok) { - throw new Error(`加载计划列表失败: ${res.status}`); - } - const data = await res.json(); - const arr = Array.isArray(data?.data) ? data.data : (Array.isArray(data) ? data : []); - planOptions.value = arr.map(it => ({ label: `${it.id} - ${it.title || ''}`, value: Number(it.id) })); - } catch (e) { - console.error('加载计划列表失败:', e); + planOptions.value = await Api.fetchPlans(resellerId); + } catch (error) { + console.error('加载计划列表失败:', error); planOptions.value = []; } }; - - // ==================== 计算属性 ==================== - const hasCreators = Vue.computed(() => Array.isArray(state.exportForm.creatorIds) && state.exportForm.creatorIds.length > 0); + + // ==================== 导出相关计算属性 ==================== + 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 f = state.exportTpl && state.exportTpl.filters; - if (!f) return null; - if (f.type_eq != null) return Number(f.type_eq); - if (Array.isArray(f.type_in) && f.type_in.length === 1) return Number(f.type_in[0]); + 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 f = state.exportTpl && state.exportTpl.filters; - if (!f) return []; - if (Array.isArray(f.type_in) && f.type_in.length) return f.type_in.map(n => Number(n)); - if (f.type_eq != null) return [Number(f.type_eq)]; + 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 mt = state.exportTpl && state.exportTpl.main_table; - return mt === 'order' || mt === 'order_info'; + const mainTable = state.exportTpl?.main_table; + return mainTable === 'order' || mainTable === 'order_info'; }); - - const orderTypeLabel = (ds, n) => { - if (ds === 'ymt') { - if (n === 2) return '直充卡密'; - if (n === 3) return '立减金'; - if (n === 1) return '红包'; - return ''; - } - if (n === 1) return '直充卡密'; - if (n === 2) return '立减金'; - if (n === 3) return '红包'; - return ''; - }; - - const sceneLabel = (s) => { - if (s === 'order' || s === 'order_info') return '订单'; - return s || ''; - }; - + const exportTitle = Vue.computed(() => { - let base = '执行导出'; - const mt = state.exportTpl && state.exportTpl.main_table; - if (mt) { - base += ' - ' + sceneLabel(mt); - if (mt === 'order' || mt === 'order_info') { - const list = exportTypeList.value; - const ds = state.exportTpl && state.exportTpl.datasource; - const labels = list.map(n => orderTypeLabel(ds, n)).filter(Boolean); + 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) { - base += ' - 订单类型:' + labels.join('、'); + title += ' - 订单类型:' + labels.join('、'); } } } - return base; + return title; }); - - // ==================== 字段路径处理 ==================== - const orderLeafPaths = (ds) => { - const FM = getFieldsMap(ds); - const mainTable = 'order'; - return (FM[mainTable] || []).map(f => [mainTable, f.value]); - }; - - const tableKeys = (ds) => Object.keys(getFieldsMap(ds) || {}); - const isGroupPath = (ds, path) => Array.isArray(path) && path.length >= 1 && tableKeys(ds).includes(path[path.length - 1]); - - const convertFieldsToPaths = (fields, ds, mainTable) => { - const FM = getFieldsMap(ds); - const hasField = (tbl, fld) => { - const arr = FM[tbl] || []; - return arr.some(i => i.value === fld); - }; - const actualMainTable = (ds === 'ymt' && mainTable === 'order_info') ? 'order' : mainTable; - const toPath = (tf) => { - const parts = String(tf || '').split('.'); - if (parts.length !== 2) return null; - let table = parts[0]; - const field = parts[1]; - let targetTable = table; - - if (ds === 'ymt' && table === 'order_info') { - table = 'order'; - targetTable = table; - } - - if (table === actualMainTable) { - targetTable = actualMainTable; - return [actualMainTable, field]; - } else if (table === 'order_detail') { - targetTable = 'order_detail'; - return [actualMainTable, 'order_detail', field]; - } else if (table === 'plan') { - targetTable = 'plan'; - return [actualMainTable, 'plan', field]; - } else if (table === 'key_batch') { - targetTable = 'key_batch'; - return [actualMainTable, 'plan', 'key_batch', field]; - } else if (table === 'code_batch') { - targetTable = 'code_batch'; - return [actualMainTable, 'plan', 'key_batch', 'code_batch', field]; - } else if (table === 'order_voucher') { - targetTable = 'order_voucher'; - return [actualMainTable, 'order_voucher', field]; - } else if (table === 'voucher') { - targetTable = 'voucher'; - return [actualMainTable, 'order_voucher', 'voucher', field]; - } else if (table === 'voucher_batch') { - targetTable = 'voucher_batch'; - return [actualMainTable, 'order_voucher', 'voucher', 'voucher_batch', field]; - } else if (table === 'merchant_key_send') { - targetTable = 'merchant_key_send'; - return [actualMainTable, 'merchant_key_send', field]; - } else if (table === 'order_cash') { - targetTable = 'order_cash'; - return [actualMainTable, 'order_cash', field]; - } else if (table === 'order_digit') { - targetTable = 'order_digit'; - return [actualMainTable, 'order_digit', field]; - } else if (table === 'goods_voucher_batch') { - targetTable = 'goods_voucher_batch'; - return [actualMainTable, 'goods_voucher_batch', field]; - } else if (table === 'goods_voucher_subject_config') { - targetTable = 'goods_voucher_subject_config'; - return [actualMainTable, 'goods_voucher_subject_config', field]; - } else if (table === 'merchant') { - targetTable = 'merchant'; - return [actualMainTable, 'merchant', field]; - } else if (table === 'activity') { - targetTable = 'activity'; - return [actualMainTable, 'activity', field]; - } - return null; - }; - - return fields - .map(toPath) - .filter(p => Array.isArray(p) && p.length >= 2) - .filter(path => { - const tbl = path[path.length - 2]; - const fld = path[path.length - 1]; - return hasField(tbl, fld); - }); - }; - const dedupPaths = (paths = []) => { - const seen = new Set(); - const out = []; - for (const p of paths) { - if (!Array.isArray(p)) continue; - const key = p.join('|'); - if (seen.has(key)) continue; - seen.add(key); - out.push(p); - } - return out; - }; - - const convertPathsToFields = (paths, ds, mainTable) => { - const actualMainTable = (ds === 'ymt' && mainTable === 'order_info') ? 'order' : mainTable; - const hasMainTableOnly = paths.some(p => Array.isArray(p) && p.length === 1 && p[0] === actualMainTable); - - if (hasMainTableOnly) { - return orderLeafPaths(ds).map(p => `${p[0]}.${p[1]}`); - } - - return paths.flatMap(path => { - if (!Array.isArray(path)) return []; - if (isGroupPath(ds, path)) return []; - if (path.length >= 2) { - const t = path[path.length - 2]; - const f = path[path.length - 1]; - const finalTable = (ds === 'ymt' && t === 'order') ? 'order_info' : t; - return [`${finalTable}.${f}`]; - } - return []; - }); - }; - // ==================== 模板管理 ==================== + /** + * 加载模板列表 + */ const loadTemplates = async () => { try { - const res = await fetch(API_BASE + '/api/templates' + qsUser()); - if (!res.ok) { - msg('加载模板失败', 'error'); - state.templates = []; - return; - } - const data = await res.json(); - const arr = Array.isArray(data?.data) ? data.data : (Array.isArray(data) ? data : []); - state.templates = arr; - } catch (e) { - msg('加载模板异常', 'error'); + state.templates = await Api.fetchTemplates(); + } catch (error) { + showMessage('加载模板失败', 'error'); state.templates = []; } }; - + + /** + * 创建模板 + */ const createTemplate = async () => { const formRef = createFormRef.value; - const ok = formRef ? await formRef.validate().catch(() => false) : true; - if (!ok) { - msg('请完善必填项', 'error'); + const valid = formRef ? await formRef.validate().catch(() => false) : true; + if (!valid) { + showMessage('请完善必填项', 'error'); return; } - + let fields = []; - if (state.form.fieldsSel && state.form.fieldsSel.length) { - fields = convertPathsToFields(state.form.fieldsSel, state.form.datasource, state.form.main_table); + 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 rec = recommendedMeta.value || []; - if (Array.isArray(rec) && rec.length) { - fields = rec; + const recommended = recommendedMeta.value; + if (recommended?.length) { + fields = recommended; } else { - const ds = state.form.datasource; - const defaultTable = ds === 'ymt' ? 'order_info' : 'order'; - fields = state.form.fieldsRaw.split(',').map(s => s.trim()).filter(Boolean).map(f => `${defaultTable}.${f}`); + fields = getDefaultFields(datasource); } } - + const payload = { - name: state.form.name, - datasource: state.form.datasource, - main_table: (state.form.datasource === 'ymt' ? 'order_info' : 'order'), + name, + datasource, + main_table: getMainTable(datasource), fields, - filters: { type_eq: Number(state.form.orderType) }, - file_format: state.form.file_format, - visibility: state.form.visibility, - owner_id: (getUserId() ? Number(getUserId()) : 0) + filters: { type_eq: Number(orderType) }, + file_format, + visibility, + owner_id: Api.getUserId() ? Number(Api.getUserId()) : 0 }; - - const res = await fetch(API_BASE + '/api/templates' + qsUser(), { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload) - }); - - if (res.ok) { - msg('创建成功'); + + try { + await Api.createTemplate(payload); + showMessage('创建成功'); state.createVisible = false; loadTemplates(); - } else { - msg(await res.text(), 'error'); + } catch (error) { + showMessage(error.message || '创建失败', 'error'); } }; - + + /** + * 打开编辑对话框 + * @param {Object} row - 模板行数据 + */ const openEdit = async (row) => { state.edit.id = row.id; try { - const res = await fetch(API_BASE + '/api/templates/' + row.id); - const data = await res.json(); - const tpl = data?.data || {}; - - state.edit.name = tpl.name || row.name || ''; - state.edit.datasource = tpl.datasource || row.datasource || 'marketing'; - state.edit.main_table = tpl.main_table || row.main_table || 'order'; - state.edit.file_format = tpl.file_format || row.file_format || 'xlsx'; - state.edit.visibility = tpl.visibility || row.visibility || 'private'; - - const filters = tpl.filters || {}; - if (filters && (filters.type_eq != null)) { + 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) { + } else if (Array.isArray(filters.type_in) && filters.type_in.length === 1) { state.edit.orderType = Number(filters.type_in[0]); } else { - state.edit.orderType = 1; + state.edit.orderType = getDefaultOrderType(state.edit.datasource); } - - const fields = Array.isArray(tpl.fields) ? tpl.fields : []; + + const fields = Array.isArray(template.fields) ? template.fields : []; state.editVisible = true; - + await Vue.nextTick(); - await loadFieldsMeta(state.edit.datasource, state.edit.orderType); + await loadFieldsMetadata(state.edit.datasource, state.edit.orderType); await Vue.nextTick(); - + const mainTable = state.edit.main_table || 'order'; - const paths = dedupPaths(convertFieldsToPaths(fields, state.edit.datasource, mainTable)); + 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 (_e) { + 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 = 1; + state.edit.orderType = getDefaultOrderType(state.edit.datasource); state.edit.fieldsSel = []; state.editVisible = true; } }; - + + /** + * 保存编辑 + */ const saveEdit = async () => { const formRef = editFormRef.value; - const ok = formRef ? await formRef.validate().catch(() => false) : true; - if (!ok) { - msg('请完善必填项', 'error'); + const valid = formRef ? await formRef.validate().catch(() => false) : true; + if (!valid) { + showMessage('请完善必填项', 'error'); return; } - - const id = state.edit.id; + + const { id, name, datasource, main_table, fieldsSel, visibility, file_format, orderType } = state.edit; + let fields = []; - const ds = state.edit.datasource; - const mainTable = state.edit.main_table || 'order'; - - if (state.edit.fieldsSel && state.edit.fieldsSel.length) { - fields = convertPathsToFields(state.edit.fieldsSel, ds, mainTable); + if (fieldsSel?.length) { + fields = fieldsManager.convertPathsToFields(fieldsSel, datasource, main_table || 'order'); } else { - const defaultTable = 'order'; - const def = DEFAULT_FIELDS[ds] || ''; - fields = def.split(',').map(s => s.trim()).filter(Boolean).map(f => `${defaultTable}.${f}`); + fields = getDefaultFields(datasource); } - + if (!fields.length) { - msg('请至少选择一个字段', 'error'); + showMessage('请至少选择一个字段', 'error'); return; } - + const payload = { - name: state.edit.name, - visibility: state.edit.visibility, - file_format: state.edit.file_format, + name, + visibility, + file_format, fields, - filters: { type_eq: Number(state.edit.orderType || 1) }, + filters: { type_eq: Number(orderType || 1) }, main_table: 'order' }; - + try { - const res = await fetch(API_BASE + '/api/templates/' + id, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, - body: JSON.stringify(payload) - }); - if (res.ok) { - msg('保存成功'); - state.editVisible = false; - loadTemplates(); - } else { - msg(await res.text(), 'error'); - } - } catch (e) { - msg('保存请求发生错误: ' + e.message, 'error'); - } - }; - - const removeTemplate = async (id) => { - const r = await fetch(API_BASE + '/api/templates/' + id + '?soft=1', { method: 'DELETE' }); - if (r.ok) { - msg('删除成功'); + await Api.updateTemplate(id, payload); + showMessage('保存成功'); + state.editVisible = false; loadTemplates(); - } else { - msg(await r.text(), 'error'); + } catch (error) { + showMessage(error.message || '保存失败', 'error'); } }; - - // ==================== 导出管理 ==================== - const loadTemplateDetail = async (id) => { + + /** + * 删除模板 + * @param {number} id - 模板 ID + */ + const removeTemplate = async (id) => { try { - const res = await fetch(API_BASE + '/api/templates/' + id); - if (!res.ok) { - msg('加载模板详情失败', 'error'); - state.exportTpl = { id: null, filters: {}, main_table: '', fields: [], datasource: '', file_format: '' }; - return; - } - const data = await res.json(); - const tpl = data?.data || {}; - state.exportTpl = tpl; - } catch (e) { - msg('加载模板详情异常', 'error'); - state.exportTpl = { id: null, filters: {}, main_table: '', fields: [], datasource: '', file_format: '' }; + await Api.deleteTemplate(id); + showMessage('删除成功'); + loadTemplates(); + } catch (error) { + showMessage(error.message || '删除失败', 'error'); } }; - + + // ==================== 导出任务 ==================== + /** + * 打开导出对话框 + * @param {Object} row - 模板行数据 + */ const openExport = async (row) => { state.exportForm.tplId = row.id; - await loadTemplateDetail(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 uid = getUserId(); - if (uid) { - const parts = String(uid).split(',').map(s => s.trim()).filter(Boolean); - if (parts.length > 1) { - state.exportForm.creatorIds = parts.map(n => Number(n)); - } else { - state.exportForm.creatorIds = [Number(uid)]; - } + 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 uid = getUserId(); - if (uid) { - const first = String(uid).split(',').map(s => s.trim()).filter(Boolean)[0]; + 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 (!Array.isArray(state.exportForm.dateRange) || state.exportForm.dateRange.length !== 2) { - state.exportForm.dateRange = monthRange(-1); + + if (!state.exportForm.dateRange?.length) { + state.exportForm.dateRange = getMonthRange(-1); } + state.exportVisible = true; }; - + + /** + * 提交导出任务 + */ const submitExport = async () => { const formRef = exportFormRef.value; - const ok = formRef ? await formRef.validate().catch(() => false) : true; - if (!ok) { - msg('请完善必填项', 'error'); + const valid = formRef ? await formRef.validate().catch(() => false) : true; + if (!valid) { + showMessage('请完善必填项', 'error'); return; } - + state.exportSubmitting = true; - msg('估算中', 'info'); - + showMessage('估算中', 'info'); + try { - const id = state.exportForm.tplId; + const { tplId, dateRange, datasource, file_format, planId, resellerId, voucherChannelActivityId, creatorIds, creatorIdsRaw, ymtCreatorId, ymtMerchantId, ymtActivityId } = state.exportForm; + const filters = {}; - const tVal = exportType.value; - if (tVal != null) { - filters.type_eq = Number(tVal); + const typeValue = exportType.value; + if (typeValue != null) { + filters.type_eq = Number(typeValue); } - if (Array.isArray(state.exportForm.dateRange) && state.exportForm.dateRange.length === 2) { - filters.create_time_between = [state.exportForm.dateRange[0], state.exportForm.dateRange[1]]; + + if (dateRange?.length === 2) { + filters.create_time_between = [dateRange[0], dateRange[1]]; } - - if (state.exportForm.planId) { - filters.plan_id_eq = Number(state.exportForm.planId); - } - if (state.exportForm.resellerId) { - filters.reseller_id_eq = Number(state.exportForm.resellerId); - } - if (state.exportForm.voucherChannelActivityId) { - const ds2 = state.exportForm.datasource; - const t2 = exportType.value; - if ((ds2 === 'marketing' && t2 === 2) || (ds2 === 'ymt' && t2 === 3)) { - filters.order_voucher_channel_activity_id_eq = state.exportForm.voucherChannelActivityId; + + 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 (Array.isArray(state.exportForm.creatorIds) && state.exportForm.creatorIds.length) { - filters.creator_in = state.exportForm.creatorIds.map(Number); - } else if (state.exportForm.creatorIdsRaw) { - const arr = String(state.exportForm.creatorIdsRaw).split(',').map(s => s.trim()).filter(Boolean); - if (arr.length) { - filters.creator_in = arr; + + 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); } } - - if (state.exportForm.datasource === 'ymt') { - if (String(state.exportForm.ymtCreatorId).trim()) { - filters.creator_in = [Number(state.exportForm.ymtCreatorId)]; - } - if (String(state.exportForm.ymtMerchantId).trim()) { - filters.reseller_id_eq = Number(state.exportForm.ymtMerchantId); - } - if (String(state.exportForm.ymtActivityId).trim()) { - filters.plan_id_eq = Number(state.exportForm.ymtActivityId); - } - } - + const payload = { - template_id: Number(id), + template_id: Number(tplId), requested_by: 1, permission: {}, options: {}, filters, - file_format: state.exportForm.file_format, - datasource: state.exportForm.datasource + file_format, + datasource }; - - const r = await fetch(API_BASE + '/api/exports' + qsUser(), { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload) - }); - const j = await r.json(); - const jid = j?.data?.id ?? j?.id; + + const result = await Api.createExportJob(payload); + const jobId = result?.data?.id ?? result?.id; + state.exportVisible = false; - - if (jid) { - state.jobsTplId = Number(id); + + if (jobId) { + state.jobsTplId = Number(tplId); state.jobsVisible = true; loadJobs(1); startJobsPolling(); } else { - msg('任务创建返回异常', 'error'); + showMessage('任务创建返回异常', 'error'); } + } catch (error) { + showMessage(error.message || '导出失败', 'error'); } finally { state.exportSubmitting = false; } }; - + // ==================== 任务管理 ==================== + let jobsPollTimer = null; + + /** + * 加载任务列表 + * @param {number} [page] - 页码 + */ const loadJobs = async (page) => { - if (!page) page = state.jobsPage; + page = page || state.jobsPage; try { - const qs = new URLSearchParams(); - qs.set('page', String(page)); - qs.set('page_size', String(state.jobsPageSize)); - if (state.jobsTplId) { - qs.set('template_id', String(state.jobsTplId)); - } - const res = await fetch(API_BASE + '/api/exports?' + qs.toString() + (qsUser() ? ('&' + qsUser().slice(1)) : '')); - if (!res.ok) { - throw new Error(`加载任务列表失败: ${res.status}`); - } - const data = await res.json(); - const payload = data?.data || data || {}; - const arr = Array.isArray(payload.items) ? payload.items : (Array.isArray(payload) ? payload : []); - state.jobs = arr; - state.jobsTotal = Number(payload.total || 0); - state.jobsPage = Number(payload.page || page); - } catch (e) { - console.error('加载任务列表失败:', e); + 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 = []; } }; - - let jobsPollTimer = null; - + /** - * 检查是否所有任务都已完成,若是则停止轮询 + * 检查是否所有任务都已完成 */ const checkAndStopPollingIfComplete = () => { - const hasRunningJob = state.jobs.some(job => - job.status === 'queued' || job.status === 'running' + 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 () => { @@ -1252,158 +672,130 @@ const app = createApp({ } }, 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(); }; - - const jobPercent = (row) => { - const est = Number(row.row_estimate || 0); - const done = Number(row.total_rows || 0); - - if (row.status === 'completed') return '100%'; - if (row.status === 'failed') return '失败'; - if (row.status === 'canceled') return '已取消'; - if (row.status === 'queued') return '0%'; - - if (row.status === 'running') { - const effectiveEst = est > 0 ? est : (done > 0 ? done * 2 : 0); - if (effectiveEst > 0) { - const p = Math.max(0, Math.min(100, Math.floor(done * 100 / effectiveEst))); - return p + '%'; - } - if (done > 0) return `已写${done.toLocaleString()}`; - return '评估中'; - } - - const effectiveEst = est > 0 ? est : (done > 0 ? done * 2 : 0); - if (effectiveEst > 0) { - const p = Math.max(0, Math.min(100, Math.floor(done * 100 / effectiveEst))); - return p + '%'; - } - return '评估中'; - }; - - const loadJob = async (id) => { - try { - const res = await fetch(API_BASE + '/api/exports/' + id); - if (!res.ok) { - msg('加载任务失败', 'error'); - state.job = {}; - return; - } - const data = await res.json(); - state.job = data?.data || {}; - } catch (e) { - msg('加载任务异常', 'error'); - state.job = {}; - } - }; - + + /** + * 计算任务进度 + * @param {Object} row - 任务行数据 + * @returns {string} 进度描述 + */ + const jobPercent = (row) => calculateJobProgress(row); + + /** + * 下载文件 + * @param {number} id - 任务 ID + */ const download = (id) => { - window.open(API_BASE + '/api/exports/' + id + '/download', '_blank'); + window.open(Api.getDownloadUrl(id), '_blank'); }; - + + /** + * 打开 SQL 预览 + * @param {number} id - 任务 ID + */ const openSQL = async (id) => { try { state.sqlExplainDesc = ''; - const res = await fetch(API_BASE + '/api/exports/' + id + '/sql'); - const data = await res.json(); - const s = data?.data?.final_sql || data?.final_sql || data?.data?.sql || data?.sql || ''; - state.sqlText = s; - + state.sqlText = await Api.fetchJobSql(id); + try { - const jobRes = await fetch(API_BASE + '/api/exports/' + id + (qsUser() ? qsUser() : '')); - if (jobRes.ok) { - const jobData = await jobRes.json(); - const job = jobData?.data || jobData || {}; - state.sqlExplainDesc = job?.eval_desc || job?.eval_status || ''; - } else { - const row = Array.isArray(state.jobs) ? state.jobs.find(r => Number(r.id) === Number(id)) : null; - state.sqlExplainDesc = row?.eval_desc || row?.eval_status || ''; - } - } catch (_ignore) { - const row = Array.isArray(state.jobs) ? state.jobs.find(r => Number(r.id) === Number(id)) : null; + 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 (_e) { + } catch (error) { + console.error('加载SQL失败:', error); state.sqlText = ''; state.sqlExplainDesc = ''; state.sqlVisible = false; - msg('加载SQL失败', 'error'); + showMessage('加载SQL失败', 'error'); } }; - + // ==================== 对话框尺寸管理 ==================== - const clampWidth = (w) => { - const n = Math.max(CONSTANTS.DIALOG_MIN_WIDTH, Math.min(CONSTANTS.DIALOG_MAX_WIDTH, w)); - return n + 'px'; - }; - + /** + * 调整对话框尺寸 + * @param {string} kind - 对话框类型 + * @param {number} delta - 变化量 + */ const resizeDialog = (kind, delta) => { if (kind === 'create') { - const cur = parseInt(String(state.createWidth).replace('px', '') || String(CONSTANTS.DIALOG_DEFAULT_WIDTH), 10); - const next = clampWidth(cur + delta); + 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 cur = parseInt(String(state.editWidth).replace('px', '') || String(CONSTANTS.DIALOG_EDIT_DEFAULT_WIDTH), 10); - const next = clampWidth(cur + delta); + 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 (ds) => { + // 数据源变化 + Vue.watch(() => state.form.datasource, async (datasource) => { state.form.fieldsSel = []; - state.form.main_table = (ds === 'ymt' ? 'order_info' : 'order'); - state.form.orderType = (ds === 'ymt' ? 2 : 1); - await loadFieldsMeta(ds, state.form.orderType); - const rec = await loadRecommendedFields(ds, state.form.orderType); - const recOrderFields = (rec || []).filter(k => String(k).startsWith('order.')).map(k => String(k).split('.')[1]); - state.form.fieldsRaw = (recOrderFields.length ? recOrderFields.join(',') : (DEFAULT_FIELDS[ds] || '')); + 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 = []; - const ds = state.form.datasource; - await loadFieldsMeta(ds, state.form.orderType); - const rec = await loadRecommendedFields(ds, state.form.orderType); - const recOrderFields = (rec || []).filter(k => String(k).startsWith('order.')).map(k => String(k).split('.')[1]); - state.form.fieldsRaw = (recOrderFields.length ? recOrderFields.join(',') : (DEFAULT_FIELDS[ds] || '')); + await loadFieldsMetadata(state.form.datasource, state.form.orderType); }); - - Vue.watch(() => state.edit.datasource, async (ds) => { + + // 编辑数据源变化 + Vue.watch(() => state.edit.datasource, async (datasource) => { state.edit.fieldsSel = []; - state.edit.main_table = (ds === 'ymt' ? 'order_info' : 'order'); - if (!Number(state.edit.orderType || 0)) { - state.edit.orderType = (ds === 'ymt' ? 2 : 1); + state.edit.main_table = getMainTable(datasource); + if (!state.edit.orderType) { + state.edit.orderType = getDefaultOrderType(datasource); } - await loadFieldsMeta(ds, state.edit.orderType); + await loadFieldsMetadata(datasource, state.edit.orderType); }); - + + // 编辑订单类型变化 Vue.watch(() => state.edit.orderType, async () => { state.edit.fieldsSel = []; - await loadFieldsMeta(state.edit.datasource, state.edit.orderType); + await loadFieldsMetadata(state.edit.datasource, state.edit.orderType); }); - + + // 导出筛选条件变化 Vue.watch(() => state.exportForm.creatorIds, () => { state.exportForm.resellerId = null; state.exportForm.planId = null; @@ -1411,102 +803,112 @@ const app = createApp({ 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(); - loadFieldsMeta(state.form.datasource, state.form.orderType); - - // 组件销毁时清理定时器 + loadFieldsMetadata(state.form.datasource, state.form.orderType); + + // 组件销毁时清理 Vue.onUnmounted(() => { stopJobsPolling(); }); - + // ==================== 返回 ==================== return { ...Vue.toRefs(state), + // 选项配置 visibilityOptions, formatOptions, datasourceOptions, - fieldOptionsDynamic, - editFieldOptionsDynamic, sceneOptions, editSceneOptions, + // 模板管理 loadTemplates, createTemplate, + openEdit, + saveEdit, + removeTemplate, + // 导出管理 openExport, submitExport, - loadJob, + // 任务管理 loadJobs, openJobs, closeJobs, download, openSQL, - openEdit, - saveEdit, - removeTemplate, - resizeDialog, - createRules, - exportRules, - editRules, - createFormRef, - exportFormRef, - editFormRef, - dsLabel, - orderTypeOptionsFor, - exportType, - isOrder, - exportTitle, - creatorOptions, - ymtCreatorOptions, - ymtMerchantOptions, - ymtActivityOptions, - resellerOptions, - planOptions, - hasCreators, - hasReseller, - hasPlan, - hasKeyBatch, - hasCodeBatch, jobPercent, - fmtDT, + // 对话框 + resizeDialog, + // 验证规则 + createRules, + editRules, + exportRules, + // 表单引用 + createFormRef, + editFormRef, + exportFormRef, + // 树形选择器 createFieldsTree, editFieldsTree, fieldTreeData, editFieldTreeData, onTreeCheck, setTreeChecked, + // 工具函数 + dsLabel, + orderTypeOptionsFor, + fmtDT: formatDateTime, + // 计算属性 + exportType, + isOrder, + exportTitle, hasUserId, - currentUserId + currentUserId, + hasCreators, + hasReseller, + hasPlan, + hasKeyBatch, + hasCodeBatch, + // 选项数据 + creatorOptions, + ymtCreatorOptions, + ymtMerchantOptions, + ymtActivityOptions, + resellerOptions, + planOptions }; } }); app.use(ElementPlus); app.mount('#app'); + +})(); diff --git a/web/main.refactored.js b/web/main.refactored.js deleted file mode 100644 index dc5e31a..0000000 --- a/web/main.refactored.js +++ /dev/null @@ -1,914 +0,0 @@ -/** - * 营销系统数据导出工具 - 主入口 - * @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([]); - - /** - * 加载字段元数据 - * @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; - return recommended; - } catch (error) { - console.error('加载字段元数据失败:', error); - fieldsManager.updateMetadata([], []); - metaTableLabels.value = {}; - recommendedMeta.value = []; - return []; - } - }; - - // ==================== 树形选择器数据 ==================== - const fieldTreeData = Vue.computed(() => { - return fieldsManager.buildFieldTree( - state.form.datasource, - state.form.orderType - ); - }); - - const editFieldTreeData = Vue.computed(() => { - 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)) return; - - const treeData = kind === 'create' ? fieldTreeData.value : editFieldTreeData.value; - const keys = TreeUtils.pathsToNodeKeys(values, treeData); - - Vue.nextTick(() => { - setTimeout(() => { - try { - if (keys.length > 0 && tree) { - tree.setCheckedKeys(keys); - } - } 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 { - 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'); - -})();