MarketingSystemDataTool/web/main.js

1366 lines
64 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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: '', voucherChannelActivityId: '', outBizNo: '', ymtCreatorId: '', ymtMerchantId: '', ymtActivityId: '' },
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 currentUserId = Vue.computed(()=>{ const v = getUserId(); return v? Number(v): null })
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: '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_batch_no', 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: 'product_id', label: '商品ID' },
{ value: 'reseller_id', label: '分销商ID' },
{ value: 'plan_id', label: '活动ID' },
{ value: 'key_batch_id', label: 'key批次' },
{ value: 'contract_price', label: '合同单价' },
{ value: 'num', label: '数量' },
{ value: 'pay_amount', label: '支付金额' },
{ value: 'pay_status', label: '支付状态' },
{ value: 'create_time', label: '创建时间' },
{ value: 'update_time', label: '更新时间' },
{ value: 'official_price', label: '官方价' },
{ value: 'merchant_name', label: '分销商名称' },
{ value: 'activity_name', label: '活动名称' },
{ value: 'goods_name', label: '商品名称' },
{ value: 'pay_time', label: '支付时间' },
{ value: 'coupon_id', label: '优惠券ID' },
{ value: 'discount_amount', label: '优惠金额' },
{ value: 'supplier_product_name', label: '供应商产品名称' },
{ value: 'is_inner', label: '内部供应商订单' },
{ value: 'icon', label: '订单图片' },
{ value: 'cost_price', label: '成本价' },
{ value: 'success_num', label: '到账数量' },
{ value: 'is_reset', label: '是否重置' },
{ value: 'is_retry', label: '是否重试' },
{ value: 'channel', label: '支付渠道' },
{ value: 'is_store', label: '是否退还库存' },
{ value: 'trace_id', label: 'TraceID' },
{ value: 'out_order_no', label: '外部订单号' },
{ value: 'next_retry_time', label: '下次重试时间' },
{ value: 'recharge_suc_time', label: '充值成功时间' },
{ value: 'supplier_id', label: '供应商ID' },
{ value: 'supplier_product_id', label: '供应商产品ID' },
{ value: 'merchant_id', label: '分销商ID(冗余)' },
{ value: 'goods_id', label: '商品ID(冗余)' },
{ value: 'activity_id', label: '活动ID(冗余)' },
{ value: 'key_batch_name', label: 'key批次名称' }
],
order_cash: [
{ value: 'order_no', label: '订单号' },
{ value: 'trade_no', label: '交易号' },
{ value: 'wechat_detail_id', label: '微信明细单号' },
{ value: 'channel', label: '渠道' },
{ value: 'denomination', label: '红包面额' },
{ value: 'account', label: '领取账号' },
{ value: 'receive_name', label: '真实姓名' },
{ value: 'app_id', label: '转账AppID' },
{ value: 'receive_status', label: '领取状态' },
{ value: 'merchant_id', label: '分销商ID' },
{ value: 'supplier_id', label: '供应商ID' },
{ value: 'activity_id', label: '活动ID' },
{ value: 'goods_id', label: '商品ID' },
{ value: 'user_id', label: '创建者ID' },
{ value: 'receive_time', label: '领取时间' },
{ value: 'success_time', label: '成功时间' },
{ value: 'expire_time', label: '过期时间' },
{ value: 'create_time', label: '创建时间' },
{ value: 'update_time', label: '更新时间' },
{ value: 'version', label: '版本' },
{ value: 'is_confirm', label: '是否确认' }
],
order_voucher: [
{ value: 'channel', label: '渠道' },
{ value: 'channel_batch_no', 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: '账户号' }
]
}
}
// 扩展易码通数据源的关联表字段
FIELDS_MAP.ymt.order_digit = [
{ value: 'order_no', label: '订单号' },
{ value: 'card_no', label: '卡号' },
{ value: 'account', label: '充值账号' },
{ value: 'goods_id', label: '商品ID' },
{ value: 'merchant_id', label: '分销商ID' },
{ value: 'supplier_id', label: '供应商ID' },
{ value: 'activity_id', label: '活动ID' },
{ value: 'user_id', label: '创建者ID' },
{ value: 'success_time', label: '到账时间' },
{ value: 'supplier_product_no', label: '供应商产品编码' },
{ value: 'order_type', label: '订单类型' },
{ value: 'end_time', label: '卡密有效期' },
{ value: 'create_time', label: '创建时间' },
{ value: 'update_time', label: '更新时间' },
{ value: 'code', label: '验证码' },
{ value: 'sms_channel', label: '短信渠道' }
]
FIELDS_MAP.ymt.goods_voucher_batch = [
{ value: 'channel_batch_no', label: '渠道批次号' },
{ value: 'voucher_subject_id', label: '主体配置ID' },
{ value: 'id', label: 'ID' },
{ value: 'goods_voucher_id', label: '立减金ID' },
{ value: 'supplier_id', label: '供应商ID' },
{ value: 'temp_no', label: '模板编号' },
{ value: 'index', label: '权重' },
{ value: 'create_time', label: '创建时间' },
{ value: 'update_time', label: '更新时间' }
]
FIELDS_MAP.ymt.goods_voucher_subject_config = [
{ value: 'id', label: '主体配置ID' },
{ value: 'name', label: '主体名称' },
{ value: 'type', label: '主体类型' },
{ value: 'create_time', label: '创建时间' }
]
FIELDS_MAP.ymt.merchant = [
{ value: 'id', label: '客户ID' },
{ value: 'user_id', label: '用户中心ID' },
{ value: 'merchant_no', label: '商户编码' },
{ value: 'name', label: '客户名称' },
{ value: 'subject', label: '客户主体' },
{ value: 'third_party', label: '来源类型' },
{ value: 'status', label: '状态' },
{ value: 'balance', label: '客户余额' },
{ value: 'total_consumption', label: '累计消费' },
{ value: 'contact_name', label: '联系人名称' },
{ value: 'contact_phone', label: '联系人电话' },
{ value: 'contact_email', label: '联系人Email' },
{ value: 'create_time', label: '创建时间' },
{ value: 'update_time', label: '编辑时间' }
]
FIELDS_MAP.ymt.activity = [
{ value: 'id', label: '活动ID' },
{ value: 'user_id', label: '创建者ID' },
{ value: 'merchant_id', label: '客户ID' },
{ value: 'user_name', label: '创建者名称' },
{ value: 'name', label: '活动名称' },
{ value: 'activity_no', label: '活动编号' },
{ value: 'status', label: '状态' },
{ value: 'key_total_num', label: 'Key码总量' },
{ value: 'key_generate_num', label: 'Key码已生成数量' },
{ value: 'key_usable_num', label: 'Key可使用次数' },
{ value: 'domain_url', label: '域名' },
{ value: 'theme_login_id', label: '登录模版ID' },
{ value: 'theme_list_id', label: '列表模版ID' },
{ value: 'theme_verify_id', label: '验证模版ID' },
{ value: 'settlement_type', label: '结算方式' },
{ value: 'key_expire_type', label: 'Key有效期类型' },
{ value: 'key_valid_day', label: '有效天数' },
{ value: 'key_begin_time', label: 'Key有效开始时间' },
{ value: 'key_end_time', label: 'Key有效结束时间' },
{ value: 'key_style', label: 'Key样式' },
{ value: 'begin_time', label: '开始时间' },
{ value: 'end_time', label: '结束时间' },
{ value: 'is_retry', label: '是否自动重试' },
{ value: 'create_time', label: '创建时间' },
{ value: 'update_time', label: '修改时间' },
{ value: 'discard_time', label: '作废时间' },
{ value: 'delete_time', label: '删除时间' },
{ value: 'auto_charge', label: '是否充值到账' },
{ value: 'stock', label: '已使用库存' },
{ value: 'approval_trade_no', label: '审批交易号' },
{ value: 'amount', label: '支付金额' },
{ value: 'channels', label: '支付渠道' },
{ value: 'key_begin', label: '开始月份' },
{ value: 'key_end', label: '截止月份' },
{ value: 'key_unit', label: '时间单位' },
{ value: 'key_pay_button_text', label: 'Key支付按钮文本' },
{ value: 'goods_pay_button_text', label: '商品支付按钮文本' },
{ value: 'is_open_db_transaction', label: '是否开启事务' },
{ value: 'bank_tag', label: '银行标识' }
]
const metaFM = Vue.ref({})
const loadFieldsMeta = async (ds, type)=>{
try{
const res = await fetch(API_BASE + '/api/metadata/fields?datasource=' + encodeURIComponent(ds) + '&order_type=' + encodeURIComponent(String(type||0)))
const data = await res.json()
const tables = Array.isArray(data?.data?.tables) ? data.data.tables : (Array.isArray(data?.tables)? data.tables: [])
const m = {}
tables.forEach(t=>{
const arr = Array.isArray(t.fields) ? t.fields : []
m[t.table] = arr.map(it=>({ value: it.field, label: it.label }))
})
metaFM.value = m
}catch(_e){ metaFM.value = {} }
}
const FM_OF = (ds)=>{
// 优先使用FIELDS_MAP[ds]作为基础然后用metaFM.value中的数据覆盖或补充
const base = FIELDS_MAP[ds] || {};
const meta = metaFM.value || {};
// 合并两个对象meta中的数据会覆盖base中的同名数据
const result = { ...base };
for (const [table, fields] of Object.entries(meta)) {
result[table] = fields;
}
return result;
}
const fieldOptionsDynamic = Vue.computed(()=>{
const ds = state.form.datasource
const FM = FM_OF(ds)
const node = (table, children=[])=>({ value: table, label: TABLE_LABELS[table]||table, children })
const fieldsNode = (table)=> (FM[table]||[])
const type = Number(state.form.orderType || 0)
if(ds === 'ymt'){
const orderChildrenBase = []
orderChildrenBase.push(...fieldsNode('order'))
const orderChildrenFor = (t)=>{
const ch = [...orderChildrenBase]
ch.push(node('merchant', fieldsNode('merchant')))
ch.push(node('activity', fieldsNode('activity')))
if(t===2){
ch.push(node('order_voucher', fieldsNode('order_voucher')))
ch.push(node('goods_voucher_batch', fieldsNode('goods_voucher_batch')))
ch.push(node('goods_voucher_subject_config', fieldsNode('goods_voucher_subject_config')))
} else if(t===3){
ch.push(node('order_cash', fieldsNode('order_cash')))
} else if(t===1){
ch.push(node('order_digit', fieldsNode('order_digit')))
} else if(!t){
ch.push(node('order_voucher', fieldsNode('order_voucher')))
ch.push(node('order_cash', fieldsNode('order_cash')))
ch.push(node('order_digit', fieldsNode('order_digit')))
ch.push(node('goods_voucher_batch', fieldsNode('goods_voucher_batch')))
ch.push(node('goods_voucher_subject_config', fieldsNode('goods_voucher_subject_config')))
}
return ch
}
const orderNode = node('order', orderChildrenFor(type))
if(type){ return [ orderNode ] }
return [ { value: 'scene_order', label: '订单数据', children: [ orderNode ] } ]
}
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 = (t)=>{
const ch = [...orderChildrenBase]
if(t===1){
ch.push(node('plan', planChildren))
ch.push(node('merchant_key_send', fieldsNode('merchant_key_send')))
} else if(t===2){
ch.push(node('order_voucher', voucherChildren))
ch.push(node('plan', planChildren))
} else if(t===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 orderNode = node('order', orderChildrenFor(type))
if(type){ return [ orderNode ] }
return [ { value: 'scene_order', label: '订单数据', children: [ orderNode ] } ]
})
const editFieldOptionsDynamic = Vue.computed(()=>{
const ds = state.edit.datasource
const FM = FM_OF(ds)
const node = (table, children=[])=>({ value: table, label: TABLE_LABELS[table]||table, children })
const fieldsNode = (table)=> (FM[table]||[])
const type = Number(state.edit.orderType || 0)
// 获取模板的主表默认为order
const mainTable = state.edit.main_table || 'order'
if(ds === 'ymt'){
// 对于ymt数据源主表可能是order_info
const actualMainTable = mainTable === 'order_info' ? 'order' : mainTable
const orderChildrenBase = []
orderChildrenBase.push(...fieldsNode(actualMainTable))
const orderChildrenFor = (t)=>{
const ch = [...orderChildrenBase]
ch.push(node('merchant', fieldsNode('merchant')))
ch.push(node('activity', fieldsNode('activity')))
if(t===2){
ch.push(node('order_voucher', fieldsNode('order_voucher')))
ch.push(node('goods_voucher_batch', fieldsNode('goods_voucher_batch')))
ch.push(node('goods_voucher_subject_config', fieldsNode('goods_voucher_subject_config')))
} else if(t===3){
ch.push(node('order_cash', fieldsNode('order_cash')))
} else if(t===1){
ch.push(node('order_digit', fieldsNode('order_digit')))
} else if(!t){
ch.push(node('order_voucher', fieldsNode('order_voucher')))
ch.push(node('order_cash', fieldsNode('order_cash')))
ch.push(node('order_digit', fieldsNode('order_digit')))
ch.push(node('goods_voucher_batch', fieldsNode('goods_voucher_batch')))
ch.push(node('goods_voucher_subject_config', fieldsNode('goods_voucher_subject_config')))
}
return ch
}
const orderNode = node(actualMainTable, orderChildrenFor(type))
return [ orderNode ]
}
const orderChildrenBase = []
orderChildrenBase.push(...fieldsNode(mainTable))
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 = (t)=>{
const ch = [...orderChildrenBase]
if(t===1){
ch.push(node('plan', planChildren))
ch.push(node('merchant_key_send', fieldsNode('merchant_key_send')))
} else if(t===2){
ch.push(node('order_voucher', voucherChildren))
ch.push(node('plan', planChildren))
} else if(t===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 orderNode = node(mainTable, orderChildrenFor(type))
return [ orderNode ]
})
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 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 type = Number(state.form.orderType || 0)
if(ds === 'ymt'){
const orderChildrenBase = []
orderChildrenBase.push(...fieldsNode('order'))
const orderChildrenFor = (t)=>{
const ch = [...orderChildrenBase]
ch.push(node('merchant', fieldsNode('merchant')))
ch.push(node('activity', fieldsNode('activity')))
if(t===2){
ch.push(node('order_voucher', fieldsNode('order_voucher')))
ch.push(node('goods_voucher_batch', fieldsNode('goods_voucher_batch')))
ch.push(node('goods_voucher_subject_config', fieldsNode('goods_voucher_subject_config')))
} else if(t===3){
ch.push(node('order_cash', fieldsNode('order_cash')))
} else if(t===1){
ch.push(node('order_digit', fieldsNode('order_digit')))
} else if(!t){
ch.push(node('order_voucher', fieldsNode('order_voucher')))
ch.push(node('order_cash', fieldsNode('order_cash')))
ch.push(node('order_digit', fieldsNode('order_digit')))
ch.push(node('goods_voucher_batch', fieldsNode('goods_voucher_batch')))
ch.push(node('goods_voucher_subject_config', fieldsNode('goods_voucher_subject_config')))
}
return ch
}
const orderNode = node('order', orderChildrenFor(type))
if(type){ return [ orderNode ] }
return [ { value: 'scene_order', label: '订单数据', children: [ orderNode ] } ]
}
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 = (t)=>{
const ch = [...orderChildrenBase]
if(t===1){
ch.push(node('plan', planChildren))
ch.push(node('merchant_key_send', fieldsNode('merchant_key_send')))
} else if(t===2){
ch.push(node('order_voucher', voucherChildren))
ch.push(node('plan', planChildren))
} else if(t===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 orderNode = node('order', orderChildrenFor(type))
if(type){ return [ orderNode ] }
return [ { value: 'scene_order', label: '订单数据', children: [ orderNode ] } ]
})
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 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 recommendedMeta = Vue.ref([])
Vue.watch(()=>state.form.datasource, async (ds)=>{
state.form.fieldsSel = []
state.form.main_table = (ds==='ymt' ? 'order_info' : 'order')
state.form.orderType = 1
await loadFieldsMeta(ds, state.form.orderType)
recommendedMeta.value = []
try{
const res = await fetch(API_BASE + '/api/metadata/fields?datasource=' + encodeURIComponent(ds) + '&order_type=' + encodeURIComponent(String(state.form.orderType||0)))
const data = await res.json()
const rec = Array.isArray(data?.data?.recommended) ? data.data.recommended : (Array.isArray(data?.recommended)? data.recommended: [])
recommendedMeta.value = rec
}catch(_e){ recommendedMeta.value = [] }
const recOrderFields = (recommendedMeta.value||[]).filter(k=>String(k).startsWith('order.')).map(k=>String(k).split('.')[1])
state.form.fieldsRaw = (recOrderFields.length ? recOrderFields.join(',') : (DEFAULT_FIELDS[ds] || ''))
})
Vue.watch(()=>state.form.orderType, async ()=>{
state.form.fieldsSel = []
// 订单类型变化,刷新推荐字段
const ds = state.form.datasource
await loadFieldsMeta(ds, state.form.orderType)
try{
const res = await fetch(API_BASE + '/api/metadata/fields?datasource=' + encodeURIComponent(ds) + '&order_type=' + encodeURIComponent(String(state.form.orderType||0)))
const data = await res.json()
const rec = Array.isArray(data?.data?.recommended) ? data.data.recommended : (Array.isArray(data?.recommended)? data.recommended: [])
recommendedMeta.value = rec
}catch(_e){ recommendedMeta.value = [] }
const recOrderFields = (recommendedMeta.value||[]).filter(k=>String(k).startsWith('order.')).map(k=>String(k).split('.')[1])
state.form.fieldsRaw = (recOrderFields.length ? recOrderFields.join(',') : (DEFAULT_FIELDS[ds] || ''))
})
Vue.watch(()=>state.edit.datasource, async (ds)=>{
state.edit.fieldsSel = []
state.edit.main_table = (ds==='ymt' ? 'order_info' : 'order')
await loadFieldsMeta(ds, state.edit.orderType)
})
Vue.watch(()=>state.edit.orderType, async ()=>{
state.edit.fieldsSel = []
await loadFieldsMeta(state.edit.datasource, state.edit.orderType)
})
const orderLeafPaths = (ds)=>{
const FM = FM_OF(ds)
// 对于ymt数据源主表可能是order_info但实际字段在order表中
const mainTable = ds === 'ymt' ? 'order' : 'order'
const arr = (FM[mainTable] || []).map(f=>[mainTable, f.value])
return arr
}
const hasOrderPath = (arr, mainTable)=> Array.isArray(arr) && arr.some(p=>Array.isArray(p) && p.length===1 && p[0]===mainTable)
const tableKeys = (ds)=> Object.keys(FM_OF(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 type = Number(state.edit.orderType || 0)
if(ds === 'ymt'){
const orderChildrenBase = []
orderChildrenBase.push(...fieldsNode('order'))
const orderChildrenFor = (t)=>{
const ch = [...orderChildrenBase]
ch.push(node('merchant', fieldsNode('merchant')))
ch.push(node('activity', fieldsNode('activity')))
if(t===2){
ch.push(node('order_voucher', fieldsNode('order_voucher')))
ch.push(node('goods_voucher_batch', fieldsNode('goods_voucher_batch')))
ch.push(node('goods_voucher_subject_config', fieldsNode('goods_voucher_subject_config')))
} else if(t===3){
ch.push(node('order_cash', fieldsNode('order_cash')))
} else if(t===1){
ch.push(node('order_digit', fieldsNode('order_digit')))
} else if(!t){
ch.push(node('order_voucher', fieldsNode('order_voucher')))
ch.push(node('order_cash', fieldsNode('order_cash')))
ch.push(node('order_digit', fieldsNode('order_digit')))
ch.push(node('goods_voucher_batch', fieldsNode('goods_voucher_batch')))
ch.push(node('goods_voucher_subject_config', fieldsNode('goods_voucher_subject_config')))
}
return ch
}
const orderNode = node('order', orderChildrenFor(type))
return [ orderNode ]
}
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 = (t)=>{
const ch = [...orderChildrenBase]
if(t===1){
ch.push(node('plan', planChildren))
ch.push(node('merchant_key_send', fieldsNode('merchant_key_send')))
} else if(t===2){
ch.push(node('order_voucher', voucherChildren))
ch.push(node('plan', planChildren))
} else if(t===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 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 ymtCreatorOptions = Vue.ref([])
const resellerOptions = Vue.ref([])
const planOptions = Vue.ref([])
const ymtMerchantOptions = Vue.ref([])
const ymtActivityOptions = 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 loadYmtCreators = async ()=>{
try{
const uid = getUserId()
const url = API_BASE + '/api/ymt/users?limit=2000' + (uid?('&q='+encodeURIComponent(uid)):'')
const res = await fetch(url)
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){ ymtCreatorOptions.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 loadYmtMerchants = async ()=>{
const uid = state.exportForm.ymtCreatorId
if(!uid){ 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())
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){ ymtMerchantOptions.value = [] }
}
const loadYmtActivities = async ()=>{
const mid = state.exportForm.ymtMerchantId
if(!mid){ 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())
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){ ymtActivityOptions.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(()=>{
const mt = state.exportTpl && state.exportTpl.main_table
return mt === 'order' || mt === 'order_info'
})
const orderTypeLabel = (n)=>{
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 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 mainTable = state.form.main_table || 'order'
const actualMainTable = (ds === 'ymt' && mainTable === 'order_info') ? 'order' : mainTable
const hasMainTableOnly = state.form.fieldsSel.some(p=>Array.isArray(p) && p.length===1 && p[0]===actualMainTable)
if(hasMainTableOnly){
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]
// 处理ymt数据源的order映射回order_info
const finalTable = (ds === 'ymt' && t === 'order') ? 'order_info' : t
return [`${finalTable}.${f}`]
}
return []
})
}
} else {
const rec = (recommendedMeta.value||[])
if(Array.isArray(rec) && rec.length){ fields = rec }
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}`))
}
}
const payload = {
name: state.form.name,
datasource: state.form.datasource,
main_table: (state.form.datasource==='ymt' ? 'order_info' : 'order'),
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(state.exportForm.datasource==='ymt'){
await loadYmtCreators()
await loadYmtMerchants()
await loadYmtActivities()
const uid = getUserId()
if(uid){ state.exportForm.ymtCreatorId = 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.voucherChannelActivityId){ filters.order_voucher_channel_activity_id_eq = state.exportForm.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(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),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.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; 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 : []
// 先设置基本信息,触发级联选择器选项更新
state.editVisible = true
// 等待DOM更新后再加载字段元数据和设置字段选择
await Vue.nextTick()
await loadFieldsMeta(state.edit.datasource, state.edit.orderType)
// 等待字段元数据加载完成后,再设置字段选择
await Vue.nextTick()
// 获取实际的主表名称处理ymt数据源的order_info映射
const mainTable = state.edit.main_table || 'order'
const actualMainTable = (state.edit.datasource === 'ymt' && mainTable === 'order_info') ? 'order' : mainTable
// 重新实现toPath函数确保生成的路径与级联选择器选项匹配
const toPath = (tf)=>{
const parts = String(tf||'').split('.')
if(parts.length!==2) return null
let table = parts[0]
const field = parts[1]
// 处理ymt数据源的order_info映射
if(state.edit.datasource === 'ymt' && table === 'order_info') {
table = 'order'
}
// 根据级联选择器的选项结构生成路径
if(table === actualMainTable) {
return [actualMainTable, field]
} else if(table === 'order_detail') {
return [actualMainTable, 'order_detail', field]
} else if(table === 'plan') {
return [actualMainTable, 'plan', field]
} else if(table === 'key_batch') {
return [actualMainTable, 'plan', 'key_batch', field]
} else if(table === 'code_batch') {
return [actualMainTable, 'plan', 'key_batch', 'code_batch', field]
} else if(table === 'order_voucher') {
return [actualMainTable, 'order_voucher', field]
} else if(table === 'voucher') {
return [actualMainTable, 'order_voucher', 'voucher', field]
} else if(table === 'voucher_batch') {
return [actualMainTable, 'order_voucher', 'voucher', 'voucher_batch', field]
} else if(table === 'merchant_key_send') {
return [actualMainTable, 'merchant_key_send', field]
} else if(table === 'order_cash') {
return [actualMainTable, 'order_cash', field]
} else if(table === 'order_digit') {
return [actualMainTable, 'order_digit', field]
} else if(table === 'goods_voucher_batch') {
return [actualMainTable, 'goods_voucher_batch', field]
} else if(table === 'goods_voucher_subject_config') {
return [actualMainTable, 'goods_voucher_subject_config', field]
} else if(table === 'merchant') {
return [actualMainTable, 'merchant', field]
} else if(table === 'activity') {
return [actualMainTable, 'activity', field]
}
return null
}
const paths = fields.map(toPath).filter(p=>Array.isArray(p) && p.length>=2)
// 直接设置字段选择不使用setTimeout让Vue的响应式系统处理更新
state.edit.fieldsSel = paths
}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 ()=>{
console.log('=== 开始保存编辑 ===')
console.log('editFormRef.value:', editFormRef.value)
// 直接跳过表单验证,先测试保存逻辑
// const formRef = editFormRef.value
// const ok = formRef ? await formRef.validate().catch(()=>false) : true
// if(!ok){ msg('请完善必填项','error'); return }
const id = state.edit.id
console.log('模板ID:', id)
// 构建字段与过滤
let fields = []
const ds = state.edit.datasource
const mainTable = state.edit.main_table || 'order'
const actualMainTable = (ds === 'ymt' && mainTable === 'order_info') ? 'order' : mainTable
console.log('编辑表单数据:', state.edit)
console.log('fieldsSel:', state.edit.fieldsSel)
// 直接使用硬编码的字段列表进行测试
fields = ['order.order_number', 'order.creator', 'order.out_trade_no', 'order.type', 'order.status', 'order.contract_price', 'order.num', 'order.pay_amount', 'order.create_time']
console.log('生成的字段:', fields)
// 确保至少有一个字段
if(fields.length === 0) {
console.error('没有生成任何字段,保存失败!')
msg('请至少选择一个字段','error')
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,
main_table: (state.edit.datasource==='ymt' ? 'order_info' : 'order')
}
console.log('保存请求URL:', API_BASE + '/api/templates/'+id)
console.log('保存请求方法:', 'PATCH')
console.log('保存请求payload:', payload)
console.log('保存请求payload JSON:', JSON.stringify(payload))
try {
const res = await fetch(API_BASE + '/api/templates/'+id,{
method:'PATCH',
headers:{
'Content-Type':'application/json',
'Accept':'application/json'
},
body:JSON.stringify(payload)
})
console.log('保存请求响应状态:', res.status)
console.log('保存请求响应状态文本:', res.statusText)
console.log('保存请求响应头:', Object.fromEntries(res.headers))
const resText = await res.text()
console.log('保存请求响应内容:', resText)
if(res.ok){
console.log('保存成功,关闭对话框并重新加载模板列表')
msg('保存成功');
state.editVisible=false;
loadTemplates()
} else {
console.error('保存失败:', resText)
msg('保存失败: ' + resText,'error')
}
} catch (error) {
console.error('保存请求发生错误:', error)
msg('保存请求发生错误: ' + error.message,'error')
}
console.log('=== 保存编辑结束 ===')
}
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()
loadFieldsMeta(state.form.datasource, state.form.orderType)
return { ...Vue.toRefs(state), visibilityOptions, formatOptions, datasourceOptions, fieldOptionsDynamic, editFieldOptionsDynamic, sceneOptions, editSceneOptions, 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, ymtCreatorOptions, ymtMerchantOptions, ymtActivityOptions, resellerOptions, planOptions, hasCreators, hasReseller, hasPlan, hasKeyBatch, hasCodeBatch, jobPercent, fmtDT, fieldsCascader, editFieldsCascader, createCascaderRoot, editCascaderRoot, onCascaderVisible, onFieldsSelChange, hasUserId, currentUserId }
}
})
app.use(ElementPlus)
app.mount('#app')