diff --git a/server/internal/api/metadata.go b/server/internal/api/metadata.go index d02cf0a..e46c297 100644 --- a/server/internal/api/metadata.go +++ b/server/internal/api/metadata.go @@ -14,6 +14,10 @@ func MetadataHandler(meta, marketing, ymt *sql.DB) http.Handler { if ds == "ymt" { db = ymt } + + // 从代码中获取隐藏字段列表 + hiddenFields := getHiddenFieldsFromCode(ds) + tables := []string{} if ds == "ymt" { tables = []string{"order_info", "order_cash", "order_voucher", "order_digit", "goods_voucher_batch", "goods_voucher_subject_config", "merchant", "activity"} @@ -29,6 +33,12 @@ func MetadataHandler(meta, marketing, ymt *sql.DB) http.Handler { if tCanonical == "" || fCanonical == "" { continue } + + // 检查字段是否在隐藏列表中 + if isFieldHidden(hiddenFields, tCanonical, fCanonical) { + continue + } + lab := c.Comment if lab == "" { lab = fCanonical @@ -44,6 +54,465 @@ func MetadataHandler(meta, marketing, ymt *sql.DB) http.Handler { }) } +// getHiddenFieldsFromCode 从代码中获取指定数据源的隐藏字段映射 +// 返回 map[table]map[field]bool,标记哪些字段需要隐藏 +// +// 所有表的字段列表(基于 schema/fields.go): +// +// Marketing 数据源表字段: +// - order: order_number, key, creator, out_trade_no, type, status, account, product_id, reseller_id, plan_id, +// key_batch_id, code_batch_id, pay_type, pay_status, use_coupon, deliver_status, expire_time, +// recharge_time, contract_price, num, total, pay_amount, create_time, update_time, card_code, +// official_price, merchant_name, activity_name, goods_name, pay_time, coupon_id, discount_amount, +// supplier_product_name, is_inner, icon, cost_price, success_num, is_reset, is_retry, channel, +// is_store, trace_id, out_order_no, next_retry_time, recharge_suc_time, supplier_id, +// supplier_product_id, merchant_id, goods_id, activity_id, key_batch_name +// 隐藏字段: id, order_id, key, creator_id, plan_id, product_id, reseller_id, supplier_id, +// supplier_product_id, key_batch_id, is_internal_supplier_order +// - order_detail: plan_title, order_number, reseller_name, product_name, show_url, official_price, +// cost_price, create_time, update_time +// - order_cash: order_no, trade_no, wechat_detail_id, channel, denomination, account, receive_name, +// app_id, cash_activity_id, receive_status, receive_time, success_time, cash_packet_id, +// channel_order_id, pay_fund_order_id, cash_id, amount, activity_id, goods_id, merchant_id, +// supplier_id, user_id, status, expire_time, create_time, update_time, version, is_confirm +// - order_voucher: channel, channel_activity_id, channel_voucher_id, status, receive_mode, grant_time, +// usage_time, refund_time, status_modify_time, overdue_time, refund_amount, official_price, +// out_biz_no, account_no +// - plan: id, title, status, begin_time, end_time +// - key_batch: id, batch_name, bind_object, quantity, stock, begin_time, end_time +// - code_batch: id, title, status, begin_time, end_time, quantity, usage, stock +// - voucher: channel, channel_activity_id, price, balance, used_amount, denomination +// - voucher_batch: channel_activity_id, temp_no, provider, weight +// - merchant_key_send: merchant_id, out_biz_no, key, status, usage_time, create_time +// +// YMT 数据源表字段: +// - order (order_info): order_number, key, creator, out_trade_no, type, status, account, product_id, +// reseller_id, plan_id, key_batch_id, code_batch_id, pay_type, pay_status, +// use_coupon, deliver_status, expire_time, recharge_time, contract_price, num, +// pay_amount, create_time, update_time, card_code, official_price, merchant_name, +// activity_name, goods_name, pay_time, coupon_id, discount_amount, +// supplier_product_name, is_inner, icon, cost_price, success_num, is_reset, +// is_retry, channel, is_store, trace_id, out_order_no, next_retry_time, +// recharge_suc_time, supplier_id, supplier_product_id, merchant_id, goods_id, +// activity_id, key_batch_name +// 隐藏字段: id, order_id, key, creator_id, plan_id, product_id, reseller_id, supplier_id, +// supplier_product_id, key_batch_id, is_internal_supplier_order +// - order_cash: order_no, trade_no, wechat_detail_id, channel, denomination, account, receive_name, +// app_id, cash_activity_id, receive_status, receive_time, success_time, cash_packet_id, +// channel_order_id, pay_fund_order_id, cash_id, amount, activity_id, goods_id, merchant_id, +// supplier_id, user_id, status, expire_time, create_time, update_time, version, is_confirm +// - order_voucher: channel, channel_activity_id, channel_voucher_id, status, receive_mode, grant_time, +// usage_time, refund_time, status_modify_time, overdue_time, refund_amount, official_price, +// out_biz_no, account_no +// - order_digit: order_no, card_no, account, goods_id, merchant_id, supplier_id, activity_id, user_id, +// success_time, supplier_product_no, order_type, end_time, create_time, update_time, code, +// sms_channel +// - goods_voucher_batch: channel_batch_no, voucher_subject_id, id, goods_voucher_id, supplier_id, +// temp_no, index, create_time, update_time +// - goods_voucher_subject_config: id, name, type, create_time +// - merchant: id, name, user_id, merchant_no, subject, third_party, status, balance, total_consumption, +// contact_name, contact_phone, contact_email, create_time, update_time +// - activity: id, name, user_id, merchant_id, user_name, activity_no, status, key_total_num, +// key_generate_num, key_usable_num, domain_url, theme_login_id, theme_list_id, +// theme_verify_id, settlement_type, key_expire_type, key_valid_day, key_begin_time, +// key_end_time, key_style, begin_time, end_time, is_retry, create_time, update_time, +// discard_time, delete_time, auto_charge, stock, approval_trade_no, amount, channels, +// key_begin, key_end, key_unit, key_pay_button_text, goods_pay_button_text, +// is_open_db_transaction +func getHiddenFieldsFromCode(ds string) map[string]map[string]bool { + result := make(map[string]map[string]bool) + + if ds == "ymt" { + // YMT 数据源的隐藏字段配置 - 所有字段默认隐藏(true) + // order 表所有字段 + result["order"] = map[string]bool{ + "order_number": true, + "key": true, + "creator": false, + "out_trade_no": true, + "type": true, + "status": true, + "account": true, + "product_id": false, + "reseller_id": false, + "plan_id": false, + "key_batch_id": false, + "code_batch_id": false, + "pay_type": true, + "pay_status": true, + "use_coupon": true, + "deliver_status": true, + "expire_time": true, + "recharge_time": true, + "contract_price": true, + "num": true, + "pay_amount": true, + "create_time": true, + "update_time": true, + "card_code": true, + "official_price": true, + "merchant_name": true, + "activity_name": true, + "goods_name": true, + "pay_time": true, + "coupon_id": true, + "discount_amount": true, + "supplier_product_name": true, + "is_inner": false, + "icon": true, + "cost_price": true, + "success_num": true, + "is_reset": false, + "is_retry": false, + "channel": true, + "is_store": false, + "trace_id": false, + "out_order_no": true, + "next_retry_time": true, + "recharge_suc_time": true, + "supplier_id": false, + "supplier_product_id": false, + "merchant_id": false, + "goods_id": false, + "activity_id": false, + "key_batch_name": true, + "id": false, + "order_id": false, + "creator_id": false, + "is_internal_supplier_order": false, + } + // order_cash 表所有字段 + result["order_cash"] = map[string]bool{ + "order_no": true, + "trade_no": true, + "wechat_detail_id": false, + "channel": true, + "denomination": true, + "account": true, + "receive_name": true, + "app_id": true, + "cash_activity_id": true, + "receive_status": true, + "receive_time": true, + "success_time": true, + "cash_packet_id": false, + "channel_order_id": false, + "pay_fund_order_id": false, + "cash_id": true, + "amount": true, + "activity_id": false, + "goods_id": false, + "merchant_id": false, + "supplier_id": false, + "user_id": false, + "status": true, + "expire_time": true, + "create_time": true, + "update_time": false, + "version": false, + "is_confirm": true, + } + // order_voucher 表所有字段 + result["order_voucher"] = map[string]bool{ + "channel": true, + "channel_activity_id": true, + "channel_voucher_id": true, + "status": true, + "receive_mode": true, + "grant_time": true, + "usage_time": true, + "refund_time": true, + "status_modify_time": true, + "overdue_time": true, + "refund_amount": true, + "official_price": true, + "out_biz_no": true, + "account_no": true, + } + // order_digit 表所有字段 + result["order_digit"] = map[string]bool{ + "order_no": true, + "card_no": true, + "account": true, + "goods_id": false, + "merchant_id": false, + "supplier_id": false, + "activity_id": false, + "user_id": false, + "success_time": true, + "supplier_product_no": true, + "order_type": true, + "end_time": true, + "create_time": true, + "update_time": true, + "code": false, + "sms_channel": false, + } + // goods_voucher_batch 表所有字段 + result["goods_voucher_batch"] = map[string]bool{ + "channel_batch_no": true, + "voucher_subject_id": true, + "id": false, + "goods_voucher_id": false, + "supplier_id": false, + "temp_no": true, + "index": true, + "create_time": false, + "update_time": false, + } + // goods_voucher_subject_config 表所有字段 + result["goods_voucher_subject_config"] = map[string]bool{ + "id": true, + "name": true, + "type": true, + "create_time": true, + } + // merchant 表所有字段 + result["merchant"] = map[string]bool{ + "id": false, + "name": true, + "user_id": false, + "merchant_no": true, + "subject": true, + "third_party": false, + "status": false, + "balance": false, + "total_consumption": false, + "contact_name": true, + "contact_phone": false, + "contact_email": false, + "create_time": false, + "update_time": false, + } + // activity 表所有字段 + result["activity"] = map[string]bool{ + "id": false, + "name": true, + "user_id": false, + "merchant_id": false, + "user_name": true, + "activity_no": true, + "status": true, + "key_total_num": true, + "key_generate_num": true, + "key_usable_num": true, + "domain_url": false, + "theme_login_id": false, + "theme_list_id": false, + "theme_verify_id": false, + "settlement_type": true, + "key_expire_type": true, + "key_valid_day": true, + "key_begin_time": true, + "key_end_time": true, + "key_style": true, + "begin_time": true, + "end_time": true, + "is_retry": false, + "create_time": false, + "update_time": false, + "discard_time": false, + "delete_time": false, + "auto_charge": true, + "stock": true, + "approval_trade_no": false, + "amount": true, + "channels": true, + "key_begin": true, + "key_end": true, + "key_unit": false, + "key_pay_button_text": false, + "goods_pay_button_text": false, + "is_open_db_transaction": false, + } + } else { + // Marketing 数据源的隐藏字段配置 - 所有字段默认隐藏(true) + // order 表所有字段 + result["order"] = map[string]bool{ + "order_number": true, + "key": true, + "creator": true, + "out_trade_no": true, + "type": true, + "status": true, + "account": true, + "product_id": false, + "reseller_id": false, + "plan_id": false, + "key_batch_id": false, + "code_batch_id": false, + "pay_type": true, + "pay_status": true, + "use_coupon": true, + "deliver_status": true, + "expire_time": true, + "recharge_time": true, + "contract_price": true, + "num": true, + "total": true, + "pay_amount": true, + "create_time": true, + "update_time": true, + "card_code": true, + "official_price": true, + "merchant_name": true, + "activity_name": true, + "goods_name": true, + "pay_time": true, + "coupon_id": false, + "discount_amount": true, + "supplier_product_name": true, + "is_inner": false, + "icon": false, + "cost_price": true, + "success_num": true, + "is_reset": false, + "is_retry": false, + "channel": true, + "is_store": false, + "trace_id": false, + "out_order_no": true, + "next_retry_time": true, + "recharge_suc_time": true, + "supplier_id": true, + "supplier_product_id": false, + "merchant_id": false, + "goods_id": false, + "activity_id": false, + "key_batch_name": true, + "id": false, + "order_id": false, + "creator_id": false, + "is_internal_supplier_order": false, + } + // order_detail 表所有字段 + result["order_detail"] = map[string]bool{ + "plan_title": true, + "order_number": true, + "reseller_name": true, + "product_name": true, + "show_url": false, + "official_price": true, + "cost_price": true, + "create_time": false, + "update_time": false, + } + // order_cash 表所有字段 + result["order_cash"] = map[string]bool{ + "order_no": true, + "trade_no": true, + "wechat_detail_id": false, + "channel": true, + "denomination": true, + "account": true, + "receive_name": true, + "app_id": true, + "cash_activity_id": false, + "receive_status": true, + "receive_time": true, + "success_time": true, + "cash_packet_id": true, + "channel_order_id": false, + "pay_fund_order_id": false, + "cash_id": false, + "amount": true, + "activity_id": false, + "goods_id": false, + "merchant_id": false, + "supplier_id": false, + "user_id": false, + "status": true, + "expire_time": true, + "create_time": true, + "update_time": true, + "version": false, + "is_confirm": true, + } + // order_voucher 表所有字段 + result["order_voucher"] = map[string]bool{ + "channel": true, + "channel_activity_id": false, + "channel_voucher_id": false, + "status": true, + "receive_mode": true, + "grant_time": true, + "usage_time": true, + "refund_time": true, + "status_modify_time": true, + "overdue_time": true, + "refund_amount": true, + "official_price": true, + "out_biz_no": true, + "account_no": true, + } + // plan 表所有字段 + result["plan"] = map[string]bool{ + "id": false, + "title": true, + "status": true, + "begin_time": true, + "end_time": true, + } + // key_batch 表所有字段 + result["key_batch"] = map[string]bool{ + "id": false, + "batch_name": true, + "bind_object": true, + "quantity": true, + "stock": true, + "begin_time": true, + "end_time": true, + } + // code_batch 表所有字段 + result["code_batch"] = map[string]bool{ + "id": false, + "title": true, + "status": true, + "begin_time": true, + "end_time": true, + "quantity": true, + "usage": true, + "stock": true, + } + // voucher 表所有字段 + result["voucher"] = map[string]bool{ + "channel": true, + "channel_activity_id": false, + "price": true, + "balance": true, + "used_amount": true, + "denomination": true, + } + // voucher_batch 表所有字段 + result["voucher_batch"] = map[string]bool{ + "channel_activity_id": true, + "temp_no": true, + "provider": true, + "weight": true, + } + // merchant_key_send 表所有字段 + result["merchant_key_send"] = map[string]bool{ + "merchant_id": true, + "out_biz_no": true, + "key": true, + "status": true, + "usage_time": true, + "create_time": true, + } + } + + return result +} + +// isFieldHidden 检查字段是否在隐藏列表中 +func isFieldHidden(hiddenFields map[string]map[string]bool, table, field string) bool { + tableFields, ok := hiddenFields[table] + if !ok { + return false + } + return tableFields[field] +} + func tableLabel(t string) string { switch t { case "order": diff --git a/server/server b/server/server index a6714de..648d637 100755 Binary files a/server/server and b/server/server differ diff --git a/web/index.html b/web/index.html index 361b113..b95452c 100644 --- a/web/index.html +++ b/web/index.html @@ -128,21 +128,23 @@ - - + + + 已选择 {{ form.fieldsSel.length }} 个字段 + @@ -190,21 +192,23 @@ - - + + + 已选择 {{ edit.fieldsSel.length }} 个字段 + diff --git a/web/main.js b/web/main.js index c50a4bc..20c206e 100644 --- a/web/main.js +++ b/web/main.js @@ -1,6 +1,39 @@ const { createApp, reactive } = Vue; - const app = createApp({ - setup(){ + +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 qsUser = () => { + const uid = getUserId(); + return uid ? ('?userId=' + encodeURIComponent(uid)) : ''; + }; + + 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: [], @@ -20,507 +53,94 @@ const { createApp, reactive } = Vue; 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', - + visibility: 'private' }, createVisible: false, editVisible: false, exportVisible: false, exportSubmitting: 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 = (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 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_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: '官方价' }, - - ], - 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: '创建时间' } - ] + 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' }, - 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: '账户号' } - ] + 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: '' } - } - // 扩展易码通数据源的关联表字段 - 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: '是否开启事务' }, - // removed bank_tag: not available in current activity schema - ] - - 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; + }); + + // ==================== 计算属性 ==================== + const hasUserId = Vue.computed(() => !!getUserId()); + const currentUserId = Vue.computed(() => { + const v = getUserId(); + return v ? Number(v) : null; + }); + + // ==================== 字段元数据管理 ==================== + const metaFM = Vue.ref({}); + const recommendedMeta = 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 = {}; } - - 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_digit', fieldsNode('order_digit'))) - } else if(t===3){ - 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===1){ - ch.push(node('order_cash', fieldsNode('order_cash'))) - } 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 loadRecommendedFields = async (ds, orderType) => { + try { + const res = await fetch(API_BASE + '/api/metadata/fields?datasource=' + encodeURIComponent(ds) + '&order_type=' + encodeURIComponent(String(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; + return rec; + } catch (_e) { + recommendedMeta.value = []; + return []; } - 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_digit', fieldsNode('order_digit'))) - } else if(t===3){ - 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===1){ - ch.push(node('order_cash', fieldsNode('order_cash'))) - } 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 getFieldsMap = (ds) => metaFM.value || {}; + + // ==================== 表标签映射 ==================== const TABLE_LABELS = { order: '订单主表', order_detail: '订单详情', @@ -537,860 +157,1221 @@ const { createApp, reactive } = Vue; 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_digit', fieldsNode('order_digit'))) - } else if(t===3){ - 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===1){ - ch.push(node('order_cash', fieldsNode('order_cash'))) - } 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 = (ds==='ymt' ? 2 : 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') - if(!Number(state.edit.orderType||0)){ - state.edit.orderType = (ds==='ymt' ? 2 : 1) - } - 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 buildFieldNode = (table, children = []) => ({ + value: table, + label: TABLE_LABELS[table] || table, + children + }); - 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_digit', fieldsNode('order_digit'))) - } else if(t===3){ - 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===1){ - ch.push(node('order_cash', fieldsNode('order_cash'))) - } 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 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'] || [])); } - 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 + 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'] || [])); } - const orderNode = node('order', orderChildrenFor(type)) - return [ orderNode ] - }) + 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] }]; + } + }); + + 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))]; + } + }); + + // 保留级联选择器的选项(用于兼容) + 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 dsLabel = (v)=>{ - if(v==='marketing') return '营销系统' - if(v==='ymt') return '易码通' - return v || '' - } - const orderTypeOptionsFor = (ds)=>{ - if(ds==='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 mapTypeForDs = (ds, v)=>{ return Number(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 = (ds, n)=>{ - if(ds==='ymt'){ - if(n===2) return '直充卡密' - if(n===3) return '立减金' - if(n===1) return '红包' - return '' + ]; + }; + + 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 createFieldsTree = Vue.ref(null); + const editFieldsTree = Vue.ref(null); + + // 树形选择器复选框变化处理 + const onTreeCheck = (kind, data) => { + 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); // 确保路径至少包含表和字段 + + // 更新表单字段选择(使用路径数组格式) + if (kind === 'create') { + state.form.fieldsSel = paths; + } else { + state.edit.fieldsSel = paths; } - 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) - if(labels.length){ - base += ' - 订单类型:' + labels.join('、') + }; + + // 设置树形选择器的选中状态 + 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; + } + + 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); + + Vue.nextTick(() => { + setTimeout(() => { + try { + if (keys.length > 0 && tree) { + tree.setCheckedKeys(keys); + } + } catch (e) { + console.warn('设置树形选择器选中状态失败:', e); + } + }, 100); + }); + }; + + // 监听字段选择变化,同步到树形选择器 + Vue.watch(() => state.form.fieldsSel, (newVal) => { + if (newVal && Array.isArray(newVal) && 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); + }); + } + }); + + // 兼容级联选择器的函数(已废弃,保留以避免错误) + const onCascaderVisible = () => {}; + const onFieldsSelChange = () => {}; + + // ==================== 表单验证规则 ==================== + 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 createFormRef = Vue.ref(null); + const exportFormRef = Vue.ref(null); + const editFormRef = 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'); + 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 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 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 = (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); + 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 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 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'){ - // 如果估算值为0但已有实际数据,使用实际数据作为估算值来显示进度 - 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 '评估中' - } - // 如果估算值为0但已有实际数据,使用实际数据作为估算值来显示进度 - 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 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 + return base; + }); + + // ==================== 字段路径处理 ==================== + 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 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]; - 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 [] - }) + if (ds === '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; + }; + + return fields.map(toPath).filter(p => Array.isArray(p) && p.length >= 2); + }; + + 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 = []; + } + }; + + 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) { + fields = convertPathsToFields(state.form.fieldsSel, state.form.datasource, state.form.main_table); } 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 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'), + main_table: (state.form.datasource === 'ymt' ? 'order_info' : 'order'), fields, - filters: { type_eq: mapTypeForDs(state.form.datasource, Number(state.form.orderType)) }, + 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){ - 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) ] } - } - } - 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] - if(first){ state.exportForm.ymtCreatorId = Number(first) } - } - } - if(!Array.isArray(state.exportForm.dateRange) || state.exportForm.dateRange.length!==2){ state.exportForm.dateRange = monthRange(-1) } - 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 } - state.exportSubmitting = true - msg('估算中','info') - try{ - const id = state.exportForm.tplId - const filters = {} - const tVal = exportType.value - if(tVal != null){ filters.type_eq = mapTypeForDs(state.exportForm.datasource, 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] ] } + owner_id: (getUserId() ? Number(getUserId()) : 0) + }; - if(state.exportForm.planId){ filters.plan_id_eq = Number(state.exportForm.planId) } + const res = await fetch(API_BASE + '/api/templates' + qsUser(), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); - 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') } - } finally { - state.exportSubmitting = false - } - } - 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 ()=>{ - 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 - const mainTable = state.edit.main_table || 'order' - const actualMainTable = mainTable - if(state.edit.fieldsSel && state.edit.fieldsSel.length){ - const hasMainTableOnly = state.edit.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.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 [] - }) - } + if (res.ok) { + msg('创建成功'); + state.createVisible = false; + loadTemplates(); } else { - const defaultTable = 'order' - const def = DEFAULT_FIELDS[ds] || '' - fields = def.split(',').map(s=>s.trim()).filter(Boolean).map(f=>`${defaultTable}.${f}`) + msg(await res.text(), 'error'); } - if(!fields.length){ msg('请至少选择一个字段','error'); return } - const filters = { type_eq: mapTypeForDs(state.edit.datasource, Number(state.edit.orderType || 1)) } + }; + + 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; + + await Vue.nextTick(); + await loadFieldsMeta(state.edit.datasource, state.edit.orderType); + await Vue.nextTick(); + + const mainTable = state.edit.main_table || 'order'; + const paths = convertFieldsToPaths(fields, state.edit.datasource, mainTable); + state.edit.fieldsSel = paths; + // 设置树形选择器的选中状态(延迟确保树已渲染) + await Vue.nextTick(); + setTimeout(() => { + setTreeChecked('edit', paths); + }, 300); + } 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; + const mainTable = state.edit.main_table || 'order'; + + if (state.edit.fieldsSel && state.edit.fieldsSel.length) { + fields = convertPathsToFields(state.edit.fieldsSel, ds, mainTable); + } else { + const defaultTable = 'order'; + const def = DEFAULT_FIELDS[ds] || ''; + fields = def.split(',').map(s => s.trim()).filter(Boolean).map(f => `${defaultTable}.${f}`); + } + + if (!fields.length) { + msg('请至少选择一个字段', 'error'); + return; + } + const payload = { name: state.edit.name, visibility: state.edit.visibility, file_format: state.edit.file_format, fields, - filters, + filters: { type_eq: Number(state.edit.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'); } - 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('删除成功'); 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 removeTemplate = async (id) => { + const r = await fetch(API_BASE + '/api/templates/' + id + '?soft=1', { method: 'DELETE' }); + if (r.ok) { + msg('删除成功'); + loadTemplates(); + } else { + msg(await r.text(), 'error'); + } + }; + + // ==================== 导出管理 ==================== + 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(); - state.job = data?.data || {} - }catch(e){ - msg('加载任务异常','error'); - state.job = {} + const tpl = data?.data || {}; + state.exportTpl = tpl; + } catch (e) { + msg('加载模板详情异常', 'error'); + state.exportTpl = { id: null, filters: {}, main_table: '', fields: [], datasource: '', file_format: '' }; } - } - const download = (id)=>{ window.open(API_BASE + '/api/exports/'+id+'/download','_blank') } - 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 - // 加载任务评估的自然语言描述 - 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 || '' + }; + + 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) { + const parts = String(uid).split(',').map(s => s.trim()).filter(Boolean); + if (parts.length > 1) { + state.exportForm.creatorIds = parts.map(n => Number(n)); } 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 || '' + state.exportForm.creatorIds = [Number(uid)]; } - }catch(_ignore){ - const row = Array.isArray(state.jobs)? state.jobs.find(r=>Number(r.id)===Number(id)) : null - state.sqlExplainDesc = row?.eval_desc || row?.eval_status || '' } - state.sqlVisible = true - }catch(_e){ state.sqlText=''; state.sqlExplainDesc=''; 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, orderTypeOptionsFor, 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') + + 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]; + if (first) { + state.exportForm.ymtCreatorId = Number(first); + } + } + } + + if (!Array.isArray(state.exportForm.dateRange) || state.exportForm.dateRange.length !== 2) { + state.exportForm.dateRange = monthRange(-1); + } + state.exportVisible = true; + }; + const submitExport = async () => { + const formRef = exportFormRef.value; + const ok = formRef ? await formRef.validate().catch(() => false) : true; + if (!ok) { + msg('请完善必填项', 'error'); + return; + } + + state.exportSubmitting = true; + msg('估算中', 'info'); + + try { + 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'); + } + } finally { + state.exportSubmitting = false; + } + }; + + // ==================== 任务管理 ==================== + 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') { + 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 = {}; + } + }; + + const download = (id) => { + window.open(API_BASE + '/api/exports/' + id + '/download', '_blank'); + }; + + 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; + + 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; + state.sqlExplainDesc = row?.eval_desc || row?.eval_status || ''; + } + state.sqlVisible = true; + } catch (_e) { + state.sqlText = ''; + state.sqlExplainDesc = ''; + state.sqlVisible = false; + msg('加载SQL失败', 'error'); + } + }; + + // ==================== 对话框尺寸管理 ==================== + 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); + } + }; + + // ==================== 监听器 ==================== + Vue.watch(() => state.form.datasource, async (ds) => { + 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] || '')); + }); + + 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] || '')); + }); + + Vue.watch(() => state.edit.datasource, async (ds) => { + 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); + } + await loadFieldsMeta(ds, state.edit.orderType); + }); + + Vue.watch(() => state.edit.orderType, async () => { + state.edit.fieldsSel = []; + await loadFieldsMeta(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(); + 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, + orderTypeOptionsFor, + exportType, + isOrder, + exportTitle, + creatorOptions, + ymtCreatorOptions, + ymtMerchantOptions, + ymtActivityOptions, + resellerOptions, + planOptions, + hasCreators, + hasReseller, + hasPlan, + hasKeyBatch, + hasCodeBatch, + jobPercent, + fmtDT, + createFieldsTree, + editFieldsTree, + fieldTreeData, + editFieldTreeData, + onTreeCheck, + setTreeChecked, + // 兼容性保留(已废弃) + createFieldsCascader: createFieldsTree, + editFieldsCascader: editFieldsTree, + onCascaderVisible, + onFieldsSelChange, + hasUserId, + currentUserId + }; + } +}); + +app.use(ElementPlus); +app.mount('#app');