const { createApp, reactive } = Vue; const app = createApp({ setup(){ const state = reactive({ templates: [], jobs: [], jobsVisible: false, jobsTplId: null, jobsPage: 1, jobsPageSize: 10, jobsTotal: 0, sqlVisible: false, sqlText: '', 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: [], creatorRaw: '', permissionMode: 'all', file_format: 'xlsx', visibility: 'private', }, createVisible: false, editVisible: false, exportVisible: false, createWidth: (localStorage.getItem('tplDialogWidth') || '900px'), editWidth: (localStorage.getItem('tplEditDialogWidth') || '900px'), 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, productId: null, outTradeNo: '', account: '', cashActivityId: '', voucherChannelActivityId: '', voucherBatchChannelActivityId: '', outBizNo: '' }, exportTpl: { id: null, filters: {}, main_table: '', fields: [], datasource: '', file_format: '' } }) const API_BASE = '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 qsUser = ()=>{ const uid = getUserId() return uid ? ('?userId=' + encodeURIComponent(uid)) : '' } const hasUserId = Vue.computed(()=> !!getUserId()) const FIELDS_MAP = { marketing: { order: [ { value: 'order_number', label: '订单编号' }, { value: 'key', label: 'KEY' }, { value: 'creator', label: '创建者ID' }, { value: 'out_trade_no', label: '支付流水号' }, { value: 'type', label: '订单类型' }, { value: 'status', label: '订单状态' }, { value: 'account', label: '账号' }, { value: 'product_id', label: '商品ID' }, { value: 'reseller_id', label: '分销商ID' }, { value: 'plan_id', label: '计划' }, { value: 'key_batch_id', label: 'key批次' }, { value: 'code_batch_id', label: '兑换批次ID' }, { value: 'contract_price', label: '合同单价' }, { value: 'num', label: '数量' }, { value: 'total', label: '总金额' }, { value: 'pay_amount', label: '支付金额' }, { value: 'pay_type', label: '支付方式' }, { value: 'pay_status', label: '支付状态' }, { value: 'use_coupon', label: '是否使用优惠券' }, { value: 'deliver_status', label: '投递状态' }, { value: 'expire_time', label: '过期处理时间' }, { value: 'recharge_time', label: '充值时间' }, { value: 'create_time', label: '创建时间' }, { value: 'update_time', label: '更新时间' } ] , order_detail: [ { value: 'plan_title', label: '计划标题' }, { value: 'reseller_name', label: '分销商名称' }, { value: 'product_name', label: '商品名称' }, { value: 'show_url', label: '商品图片URL' }, { value: 'official_price', label: '官方价' }, { value: 'cost_price', label: '成本价' }, { value: 'create_time', label: '创建时间' }, { value: 'update_time', label: '更新时间' } ], order_cash: [ { value: 'channel', label: '渠道' }, { value: 'cash_activity_id', label: '红包批次号' }, { value: 'receive_status', label: '领取状态' }, { value: 'receive_time', label: '拆红包时间' }, { value: 'cash_packet_id', label: '红包ID' }, { value: 'cash_id', label: '红包规则ID' }, { value: 'amount', label: '红包额度' }, { value: 'status', label: '状态' }, { value: 'expire_time', label: '过期时间' }, { value: 'update_time', label: '更新时间' } ], order_voucher: [ { value: 'channel', label: '渠道' }, { value: 'channel_activity_id', label: '渠道立减金批次' }, { value: 'channel_voucher_id', label: '渠道立减金ID' }, { value: 'status', label: '状态' }, { value: 'receive_mode', label: '领取方式' }, { value: 'grant_time', label: '领取时间' }, { value: 'usage_time', label: '核销时间' }, { value: 'refund_time', label: '退款时间' }, { value: 'status_modify_time', label: '状态更新时间' }, { value: 'overdue_time', label: '过期时间' }, { value: 'refund_amount', label: '退款金额' }, { value: 'official_price', label: '官方价' }, { value: 'out_biz_no', label: '外部业务号' }, { value: 'account_no', label: '账户号' } ], plan: [ { value: 'id', label: '计划ID' }, { value: 'title', label: '计划标题' }, { value: 'status', label: '状态' }, { value: 'begin_time', label: '开始时间' }, { value: 'end_time', label: '结束时间' } ], key_batch: [ { value: 'id', label: '批次ID' }, { value: 'batch_name', label: '批次名称' }, { value: 'bind_object', label: '绑定对象' }, { value: 'quantity', label: '发放数量' }, { value: 'stock', label: '剩余库存' }, { value: 'begin_time', label: '开始时间' }, { value: 'end_time', label: '结束时间' } ], code_batch: [ { value: 'id', label: '兑换批次ID' }, { value: 'title', label: '标题' }, { value: 'status', label: '状态' }, { value: 'begin_time', label: '开始时间' }, { value: 'end_time', label: '结束时间' }, { value: 'quantity', label: '数量' }, { value: 'usage', label: '使用数' }, { value: 'stock', label: '库存' } ], voucher: [ { value: 'channel', label: '渠道' }, { value: 'channel_activity_id', label: '渠道批次号' }, { value: 'price', label: '合同单价' }, { value: 'balance', label: '剩余额度' }, { value: 'used_amount', label: '已用额度' }, { value: 'denomination', label: '面额' } ], voucher_batch: [ { value: 'channel_activity_id', label: '渠道批次号' }, { value: 'temp_no', label: '模板编号' }, { value: 'provider', label: '服务商' }, { value: 'weight', label: '权重' } ], merchant_key_send: [ { value: 'merchant_id', label: '商户ID' }, { value: 'out_biz_no', label: '商户业务号' }, { value: 'key', label: '券码' }, { value: 'status', label: '状态' }, { value: 'usage_time', label: '核销时间' }, { value: 'create_time', label: '创建时间' } ] }, ymt: { order: [ { value: 'order_number', label: '订单编号' }, { value: 'key', label: 'KEY' }, { value: 'creator', label: '创建者ID' }, { value: 'out_trade_no', label: '支付流水号' }, { value: 'type', label: '订单类型' }, { value: 'status', label: '订单状态' }, { value: 'account', label: '账号' }, { value: 'product_id', label: '商品ID' }, { value: 'reseller_id', label: '分销商ID' }, { value: 'plan_id', label: '计划' }, { value: 'key_batch_id', label: 'key批次' }, { value: 'code_batch_id', label: '兑换批次ID' }, { value: 'contract_price', label: '合同单价' }, { value: 'num', label: '数量' }, { value: 'total', label: '总金额' }, { value: 'pay_amount', label: '支付金额' }, { value: 'pay_type', label: '支付方式' }, { value: 'pay_status', label: '支付状态' }, { value: 'use_coupon', label: '是否使用优惠券' }, { value: 'deliver_status', label: '投递状态' }, { value: 'expire_time', label: '过期处理时间' }, { value: 'recharge_time', label: '充值时间' }, { value: 'create_time', label: '创建时间' }, { value: 'update_time', label: '更新时间' } ], order_detail: [ { value: 'plan_title', label: '计划标题' }, { value: 'reseller_name', label: '分销商名称' }, { value: 'product_name', label: '商品名称' }, { value: 'show_url', label: '商品图片URL' }, { value: 'official_price', label: '官方价' }, { value: 'cost_price', label: '成本价' }, { value: 'create_time', label: '创建时间' }, { value: 'update_time', label: '更新时间' } ], order_cash: [ { value: 'channel', label: '渠道' }, { value: 'cash_activity_id', label: '红包批次号' }, { value: 'receive_status', label: '领取状态' }, { value: 'receive_time', label: '拆红包时间' }, { value: 'cash_packet_id', label: '红包ID' }, { value: 'cash_id', label: '红包规则ID' }, { value: 'amount', label: '红包额度' }, { value: 'status', label: '状态' }, { value: 'expire_time', label: '过期时间' }, { value: 'update_time', label: '更新时间' } ], order_voucher: [ { value: 'channel', label: '渠道' }, { value: 'channel_activity_id', label: '渠道立减金批次' }, { value: 'channel_voucher_id', label: '渠道立减金ID' }, { value: 'status', label: '状态' }, { value: 'receive_mode', label: '领取方式' }, { value: 'grant_time', label: '领取时间' }, { value: 'usage_time', label: '核销时间' }, { value: 'refund_time', label: '退款时间' }, { value: 'status_modify_time', label: '状态更新时间' }, { value: 'overdue_time', label: '过期时间' }, { value: 'refund_amount', label: '退款金额' }, { value: 'official_price', label: '官方价' }, { value: 'out_biz_no', label: '外部业务号' }, { value: 'account_no', label: '账户号' } ], plan: [ { value: 'id', label: '计划ID' }, { value: 'title', label: '计划标题' }, { value: 'status', label: '状态' }, { value: 'begin_time', label: '开始时间' }, { value: 'end_time', label: '结束时间' } ], key_batch: [ { value: 'id', label: '批次ID' }, { value: 'batch_name', label: '批次名称' }, { value: 'bind_object', label: '绑定对象' }, { value: 'quantity', label: '发放数量' }, { value: 'stock', label: '剩余库存' }, { value: 'begin_time', label: '开始时间' }, { value: 'end_time', label: '结束时间' } ], code_batch: [ { value: 'id', label: '兑换批次ID' }, { value: 'title', label: '标题' }, { value: 'status', label: '状态' }, { value: 'begin_time', label: '开始时间' }, { value: 'end_time', label: '结束时间' }, { value: 'quantity', label: '数量' }, { value: 'usage', label: '使用数' }, { value: 'stock', label: '库存' } ], voucher: [ { value: 'channel', label: '渠道' }, { value: 'channel_activity_id', label: '渠道批次号' }, { value: 'price', label: '合同单价' }, { value: 'balance', label: '剩余额度' }, { value: 'used_amount', label: '已用额度' }, { value: 'denomination', label: '面额' } ], voucher_batch: [ { value: 'channel_activity_id', label: '渠道批次号' }, { value: 'temp_no', label: '模板编号' }, { value: 'provider', label: '服务商' }, { value: 'weight', label: '权重' } ], merchant_key_send: [ { value: 'merchant_id', label: '商户ID' }, { value: 'out_biz_no', label: '商户业务号' }, { value: 'key', label: '券码' }, { value: 'status', label: '状态' }, { value: 'usage_time', label: '核销时间' }, { value: 'create_time', label: '创建时间' } ] } } const TABLE_LABELS = { order: '订单主表', order_detail: '订单详情', order_cash: '红包订单', order_voucher: '立减金订单', plan: '活动计划', key_batch: 'key批次', code_batch: '兑换码批次', voucher: '立减金', voucher_batch: '立减金批次', merchant_key_send: '开放平台发放记录' } const fieldOptions = Vue.computed(()=>{ const ds = state.form.datasource const FM = FIELDS_MAP[ds] || {} const node = (table, children=[])=>({ value: table, label: TABLE_LABELS[table]||table, children }) const fieldsNode = (table)=> (FM[table]||[]) const orderChildrenBase = [] orderChildrenBase.push(...fieldsNode('order')) orderChildrenBase.push(node('order_detail', fieldsNode('order_detail'))) const planChildren = [] planChildren.push(...fieldsNode('plan')) planChildren.push(node('key_batch', [ ...fieldsNode('key_batch'), node('code_batch', fieldsNode('code_batch')) ])) const voucherChildren = [] voucherChildren.push(...fieldsNode('order_voucher')) voucherChildren.push(node('voucher', [ ...fieldsNode('voucher'), node('voucher_batch', fieldsNode('voucher_batch')) ])) const orderChildrenFor = (type)=>{ const ch = [...orderChildrenBase] if(type===1){ // 直充卡密:排除红包与立减金 ch.push(node('plan', planChildren)) ch.push(node('merchant_key_send', fieldsNode('merchant_key_send'))) } else if(type===2){ // 立减金:排除红包,保留立减金链 ch.push(node('order_voucher', voucherChildren)) ch.push(node('plan', planChildren)) } else if(type===3){ // 红包:仅红包链 ch.push(node('order_cash', fieldsNode('order_cash'))) ch.push(node('plan', planChildren)) } else { // 未选择类型:全部显示 ch.push(node('order_cash', fieldsNode('order_cash'))) ch.push(node('order_voucher', voucherChildren)) ch.push(node('plan', planChildren)) ch.push(node('merchant_key_send', fieldsNode('merchant_key_send'))) } return ch } const type = Number(state.form.orderType || 0) const orderNode = node('order', orderChildrenFor(type)) if(type){ return [ orderNode ] } return [ { value: 'scene_order', label: '订单数据', children: [ orderNode ] } ] }) const orderLeafPaths = (ds)=>{ const FM = FIELDS_MAP[ds] || {} const arr = (FM.order || []).map(f=>['order', f.value]) return arr } const hasOrderPath = (arr)=> Array.isArray(arr) && arr.some(p=>Array.isArray(p) && p.length===1 && p[0]==='order') const tableKeys = (ds)=> Object.keys(FIELDS_MAP[ds] || {}) const isGroupPath = (ds, path)=> Array.isArray(path) && path.length>=1 && tableKeys(ds).includes(path[path.length-1]) const msg = (t, type='success')=>ElementPlus.ElMessage({message:t,type}); const createFormRef = Vue.ref(null) const exportFormRef = Vue.ref(null) const editFormRef = Vue.ref(null) const fieldsCascader = Vue.ref(null) const editFieldsCascader = Vue.ref(null) const createCascaderRoot = Vue.ref(null) const editCascaderRoot = Vue.ref(null) const cascaderScroll = { create: [], edit: [] } const getWraps = (kind)=>{ const r = kind==='create' ? createCascaderRoot.value : editCascaderRoot.value const el = r && r.$el ? r.$el : r if(!el || typeof el.querySelectorAll !== 'function') return [] return Array.from(el.querySelectorAll('.el-cascader__panel .el-scrollbar__wrap')) } const onCascaderVisible = (kind, v)=>{ if(!v) return Vue.nextTick(()=>{ const wraps = getWraps(kind) cascaderScroll[kind] = wraps.map(w=>w.scrollTop) wraps.forEach((wrap, idx)=>{ wrap.addEventListener('scroll', (e)=>{ cascaderScroll[kind][idx] = e.target.scrollTop }, { passive: true }) }) const r = kind==='create' ? createCascaderRoot.value : editCascaderRoot.value const el = r && r.$el ? r.$el : r if(el && typeof el.querySelectorAll === 'function'){ el.querySelectorAll('.el-cascader-node').forEach(node=>{ node.addEventListener('click', ()=>{ const ws = getWraps(kind) cascaderScroll[kind] = ws.map(w=>w.scrollTop) }, { passive: true }) }) } }) } const onFieldsSelChange = (kind)=>{ const tops = cascaderScroll[kind] || [] Vue.nextTick(()=>{ const wraps = getWraps(kind) wraps.forEach((w, idx)=>{ w.scrollTop = tops[idx] || 0 }) }) } 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 editFieldOptions = Vue.computed(()=>{ const ds = state.edit.datasource const FM = FIELDS_MAP[ds] || {} const node = (table, children=[])=>({ value: table, label: TABLE_LABELS[table]||table, children }) const fieldsNode = (table)=> (FM[table]||[]) const orderChildrenBase = [] orderChildrenBase.push(...fieldsNode('order')) orderChildrenBase.push(node('order_detail', fieldsNode('order_detail'))) const planChildren = [] planChildren.push(...fieldsNode('plan')) planChildren.push(node('key_batch', [ ...fieldsNode('key_batch'), node('code_batch', fieldsNode('code_batch')) ])) const voucherChildren = [] voucherChildren.push(...fieldsNode('order_voucher')) voucherChildren.push(node('voucher', [ ...fieldsNode('voucher'), node('voucher_batch', fieldsNode('voucher_batch')) ])) const orderChildrenFor = (type)=>{ const ch = [...orderChildrenBase] if(type===1){ ch.push(node('plan', planChildren)) ch.push(node('merchant_key_send', fieldsNode('merchant_key_send'))) } else if(type===2){ ch.push(node('order_voucher', voucherChildren)) ch.push(node('plan', planChildren)) } else if(type===3){ ch.push(node('order_cash', fieldsNode('order_cash'))) ch.push(node('plan', planChildren)) } else { ch.push(node('order_cash', fieldsNode('order_cash'))) ch.push(node('order_voucher', voucherChildren)) ch.push(node('plan', planChildren)) ch.push(node('merchant_key_send', fieldsNode('merchant_key_send'))) } return ch } const type = Number(state.edit.orderType || 0) const orderNode = node('order', orderChildrenFor(type)) return [ orderNode ] }) 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 dsLabel = (v)=>{ if(v==='marketing') return '营销系统' if(v==='ymt') return '易码通' return v || '' } const creatorOptions = Vue.ref([]) const resellerOptions = Vue.ref([]) const planOptions = Vue.ref([]) const hasCreators = Vue.computed(()=> Array.isArray(state.exportForm.creatorIds) && 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 loadCreators = async ()=>{ try{ const res = await fetch(API_BASE + '/api/creators') 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){ creatorOptions.value = [] } } const loadResellers = async ()=>{ const ids = Array.isArray(state.exportForm.creatorIds) ? state.exportForm.creatorIds : [] if(!ids.length){ resellerOptions.value = []; return } try{ const res = await fetch(API_BASE + '/api/resellers?creator=' + ids.join(',')) 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){ resellerOptions.value = [] } } const loadPlans = async ()=>{ const rid = state.exportForm.resellerId if(!rid){ planOptions.value = []; return } try{ const qs = new URLSearchParams() qs.set('reseller', String(rid)) const res = await fetch(API_BASE + '/api/plans?' + qs.toString()) 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){ planOptions.value = [] } } 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]) 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)] return [] }) const isOrder = Vue.computed(()=>{ return (state.exportTpl && state.exportTpl.main_table) === 'order' }) const orderTypeLabel = (n)=>{ if(n===1) return '直充卡密' if(n===2) return '立减金' if(n===3) return '红包' return '' } const sceneLabel = (s)=>{ if(s==='order') 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'){ const list = exportTypeList.value const labels = list.map(orderTypeLabel).filter(Boolean) if(labels.length){ base += ' - 订单类型:' + labels.join('、') } } } return base }) 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 yearRange = ()=>{ const now = new Date() const start = new Date(now.getFullYear(), 0, 1, 0, 0, 0) const end = new Date(now.getFullYear(), 11, 31, 23, 59, 59) return [ fmtDT(start), fmtDT(end) ] } 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 = [] } } const loadJobs = async (page)=>{ if(!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){ state.jobs = []; return } 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){ state.jobs = [] } } let jobsPollTimer = null const startJobsPolling = ()=>{ if(jobsPollTimer) return jobsPollTimer = setInterval(()=>{ if(state.jobsVisible){ loadJobs(state.jobsPage) } }, 1000) } const stopJobsPolling = ()=>{ if(jobsPollTimer){ clearInterval(jobsPollTimer); jobsPollTimer=null } } 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'){ if(est>0){ const p = Math.max(0, Math.min(100, Math.floor(done*100/est))); return p + '%' } return '0%' } if(est>0){ const p = Math.max(0, Math.min(100, Math.floor(done*100/est))); return p + '%' } return '评估中' } const createTemplate = async ()=>{ const formRef = createFormRef.value const ok = formRef ? await formRef.validate().catch(()=>false) : true if(!ok){ msg('请完善必填项','error'); return } let fields = [] if(state.form.fieldsSel && state.form.fieldsSel.length){ const ds = state.form.datasource const hasOrderOnly = state.form.fieldsSel.some(p=>Array.isArray(p) && p.length===1 && p[0]==='order') if(hasOrderOnly){ fields = orderLeafPaths(ds).map(p=>`${p[0]}.${p[1]}`) } else { fields = state.form.fieldsSel.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] return [`${t}.${f}`] } return [] }) } } else { fields = state.form.fieldsRaw.split(',').map(s=>s.trim()).filter(Boolean) } const payload = { name: state.form.name, datasource: state.form.datasource, main_table: state.form.main_table, fields, filters: { type_eq: Number(state.form.orderType) }, file_format: state.form.file_format, visibility: state.form.visibility, owner_id: (getUserId()? Number(getUserId()) : (state.form.visibility==='public'? 0 : 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('创建成功'); state.createVisible=false; loadTemplates() } else { msg(await res.text(),'error') } } const openExport = async (row)=>{ state.exportForm.tplId = row.id await loadTemplateDetail(row.id) 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){ state.exportForm.creatorIds = [ Number(uid) ] } if(!Array.isArray(state.exportForm.dateRange) || state.exportForm.dateRange.length!==2){ state.exportForm.dateRange = yearRange() } state.exportVisible = true } const loadTemplateDetail = 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: '' } } } const submitExport = async ()=>{ const formRef = exportFormRef.value const ok = formRef ? await formRef.validate().catch(()=>false) : true if(!ok){ msg('请完善必填项','error'); return } const id = state.exportForm.tplId const filters = {} const tVal = exportType.value if(tVal != null){ filters.type_eq = Number(tVal) } if(Array.isArray(state.exportForm.dateRange) && state.exportForm.dateRange.length===2){ filters.create_time_between = [ state.exportForm.dateRange[0], state.exportForm.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.cashActivityId){ filters.order_cash_cash_activity_id_eq = state.exportForm.cashActivityId } if(state.exportForm.voucherChannelActivityId){ filters.order_voucher_channel_activity_id_eq = state.exportForm.voucherChannelActivityId } if(state.exportForm.voucherBatchChannelActivityId){ filters.voucher_batch_channel_activity_id_eq = state.exportForm.voucherBatchChannelActivityId } 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 } } const payload={template_id:Number(id),requested_by:1,permission:{},options:{},filters, file_format: state.exportForm.file_format, datasource: state.exportForm.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 state.exportVisible=false if(jid){ state.jobsTplId = Number(id) state.jobsVisible = true loadJobs(1) startJobsPolling() } else { msg('任务创建返回异常','error') } } Vue.watch(()=>state.exportForm.creatorIds, ()=>{ state.exportForm.resellerId=null; state.exportForm.planId=null; state.exportForm.keyBatchId=null; state.exportForm.codeBatchId=null; state.exportForm.productId=null; loadResellers() }) Vue.watch(()=>state.exportForm.resellerId, ()=>{ state.exportForm.planId=null; state.exportForm.keyBatchId=null; state.exportForm.codeBatchId=null; state.exportForm.productId=null; loadPlans() }) Vue.watch(()=>state.exportForm.planId, ()=>{ state.exportForm.keyBatchId=null; state.exportForm.codeBatchId=null; state.exportForm.productId=null }) Vue.watch(()=>state.exportForm.keyBatchId, ()=>{ state.exportForm.codeBatchId=null; state.exportForm.productId=null }) Vue.watch(()=>state.exportForm.codeBatchId, ()=>{ state.exportForm.productId=null }) const clampWidth = (w)=>{ const n = Math.max(500, Math.min(1400, w)) return n + 'px' } const resizeDialog = (kind, delta)=>{ if(kind==='create'){ const cur = parseInt(String(state.createWidth).replace('px','')||'900',10) const next = clampWidth(cur + delta) state.createWidth = next localStorage.setItem('tplDialogWidth', next) } else if(kind==='edit'){ const cur = parseInt(String(state.editWidth).replace('px','')||'600',10) const next = clampWidth(cur + delta) state.editWidth = next localStorage.setItem('tplEditDialogWidth', next) } } 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)){ 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 = 1 } const fields = Array.isArray(tpl.fields) ? tpl.fields : [] const toPath = (tf)=>{ const parts = String(tf||'').split('.') if(parts.length!==2) return null const table = parts[0] const field = parts[1] if(table==='order') return ['order', field] if(table==='order_detail') return ['order','order_detail',field] if(table==='plan') return ['order','plan',field] if(table==='key_batch') return ['order','plan','key_batch',field] if(table==='code_batch') return ['order','plan','key_batch','code_batch',field] if(table==='order_voucher') return ['order','order_voucher',field] if(table==='voucher') return ['order','order_voucher','voucher',field] if(table==='voucher_batch') return ['order','order_voucher','voucher','voucher_batch',field] if(table==='merchant_key_send') return ['order','merchant_key_send',field] if(table==='order_cash') return ['order','order_cash',field] return null } state.edit.fieldsSel = fields.map(toPath).filter(p=>Array.isArray(p) && p.length>=2) }catch(_e){ 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.fieldsSel = [] } state.editVisible = true } const saveEdit = async ()=>{ const formRef = editFormRef.value const ok = formRef ? await formRef.validate().catch(()=>false) : true if(!ok){ msg('请完善必填项','error'); return } const id = state.edit.id // 构建字段与过滤 let fields = [] const ds = state.edit.datasource if(state.edit.fieldsSel && state.edit.fieldsSel.length){ const hasOrderOnly = state.edit.fieldsSel.some(p=>Array.isArray(p) && p.length===1 && p[0]==='order') if(hasOrderOnly){ fields = orderLeafPaths(ds).map(p=>`${p[0]}.${p[1]}`) } else { fields = state.edit.fieldsSel.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] return [`${t}.${f}`] } return [] }) } } const filters = { type_eq: Number(state.edit.orderType || 1) } const payload = { name: state.edit.name, visibility: state.edit.visibility, file_format: state.edit.file_format, fields, filters } const res = await fetch(API_BASE + '/api/templates/'+id,{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)}) if(res.ok){ msg('保存成功'); state.editVisible=false; loadTemplates() } else { msg(await res.text(),'error') } } const removeTemplate = async (id)=>{ const r = await fetch(API_BASE + '/api/templates/'+id,{method:'DELETE'}) if(r.ok){ msg('删除成功'); loadTemplates() } else { msg(await r.text(),'error') } } 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 = {} } } const download = (id)=>{ window.open(API_BASE + '/api/exports/'+id+'/download','_blank') } const openSQL = async (id)=>{ try{ 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.sqlVisible = true }catch(_e){ state.sqlText=''; state.sqlVisible=false; msg('加载SQL失败','error') } } loadTemplates() return { ...Vue.toRefs(state), visibilityOptions, formatOptions, datasourceOptions, fieldOptions, editFieldOptions, loadTemplates, createTemplate, openExport, submitExport, loadJob, loadJobs, openJobs, closeJobs, download, openSQL, openEdit, saveEdit, removeTemplate, resizeDialog, createRules, exportRules, editRules, createFormRef, exportFormRef, editFormRef, dsLabel, exportType, isOrder, exportTitle, creatorOptions, resellerOptions, planOptions, hasCreators, hasReseller, hasPlan, hasKeyBatch, hasCodeBatch, jobPercent, fmtDT, fieldsCascader, editFieldsCascader, createCascaderRoot, editCascaderRoot, onCascaderVisible, onFieldsSelChange, hasUserId } } }) app.use(ElementPlus) app.mount('#app') 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,total,pay_amount,create_time' } Vue.watch(()=>state.form.datasource, (ds)=>{ state.form.fieldsSel = [] state.form.fieldsRaw = DEFAULT_FIELDS[ds] || '' state.form.main_table = 'order' state.form.orderType = 1 }) Vue.watch(()=>state.edit.datasource, ()=>{ state.edit.fieldsSel = [] state.edit.main_table = 'order' })