1396 lines
50 KiB
JavaScript
1396 lines
50 KiB
JavaScript
const { createApp, reactive } = Vue;
|
|
|
|
const app = createApp({
|
|
setup() {
|
|
// ==================== 配置和工具函数 ====================
|
|
const API_BASE = (window.__API_BASE__ && String(window.__API_BASE__).trim())
|
|
? String(window.__API_BASE__).trim()
|
|
: (typeof location !== 'undefined' ? location.origin : 'http://localhost:8077');
|
|
|
|
const getUserId = () => {
|
|
const sp = new URLSearchParams(window.location.search || '');
|
|
const v = sp.get('userId') || sp.get('userid') || sp.get('user_id');
|
|
return v && String(v).trim() ? String(v).trim() : '';
|
|
};
|
|
|
|
const getMerchantId = () => {
|
|
const sp = new URLSearchParams(window.location.search || '');
|
|
const v = sp.get('merchantId') || sp.get('merchantid') || sp.get('merchant_id');
|
|
return v && String(v).trim() ? String(v).trim() : '';
|
|
};
|
|
|
|
const qsUser = () => {
|
|
const uid = getUserId();
|
|
const mid = getMerchantId();
|
|
const parts = [];
|
|
if (uid) parts.push('userId=' + encodeURIComponent(uid));
|
|
if (mid) parts.push('merchantId=' + encodeURIComponent(mid));
|
|
return parts.length ? ('?' + parts.join('&')) : '';
|
|
};
|
|
|
|
const msg = (text, type = 'success') => ElementPlus.ElMessage({ message: text, type });
|
|
|
|
const fmtDT = (d) => {
|
|
const pad = (n) => String(n).padStart(2, '0');
|
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
};
|
|
|
|
const monthRange = (offsetMonths = -1) => {
|
|
const now = new Date();
|
|
const base = new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0);
|
|
const start = new Date(base.getFullYear(), base.getMonth() + offsetMonths, 1, 0, 0, 0);
|
|
const end = new Date(start.getFullYear(), start.getMonth() + 1, 0, 23, 59, 59);
|
|
return [fmtDT(start), fmtDT(end)];
|
|
};
|
|
|
|
// ==================== 状态管理 ====================
|
|
const state = reactive({
|
|
templates: [],
|
|
jobs: [],
|
|
jobsVisible: false,
|
|
jobsTplId: null,
|
|
jobsPage: 1,
|
|
jobsPageSize: 10,
|
|
jobsTotal: 0,
|
|
sqlVisible: false,
|
|
sqlText: '',
|
|
sqlExplainDesc: '',
|
|
job: {},
|
|
form: {
|
|
name: '',
|
|
datasource: 'marketing',
|
|
main_table: 'order',
|
|
orderType: 1,
|
|
fieldsRaw: 'order_number,creator,out_trade_no,type,status,contract_price,num,total,pay_amount,create_time',
|
|
fieldsSel: [],
|
|
file_format: 'xlsx',
|
|
visibility: 'private'
|
|
},
|
|
createVisible: false,
|
|
editVisible: false,
|
|
exportVisible: false,
|
|
exportSubmitting: false,
|
|
createWidth: localStorage.getItem('tplDialogWidth') || '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,
|
|
voucherChannelActivityId: '',
|
|
ymtCreatorId: '',
|
|
ymtMerchantId: '',
|
|
ymtActivityId: ''
|
|
},
|
|
exportTpl: {
|
|
id: null,
|
|
filters: {},
|
|
main_table: '',
|
|
fields: [],
|
|
datasource: '',
|
|
file_format: ''
|
|
}
|
|
});
|
|
|
|
// ==================== 计算属性 ====================
|
|
const hasUserId = Vue.computed(() => !!getUserId());
|
|
const currentUserId = Vue.computed(() => {
|
|
const v = getUserId();
|
|
return v ? Number(v) : null;
|
|
});
|
|
|
|
// ==================== 字段元数据管理 ====================
|
|
const metaFM = Vue.ref({});
|
|
const metaTableLabels = 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 = {};
|
|
const tblLabels = {};
|
|
tables.forEach(t => {
|
|
const arr = Array.isArray(t.fields) ? t.fields : [];
|
|
const visibleFields = arr.filter(it => !it.hidden);
|
|
m[t.table] = visibleFields.map(it => ({ value: it.field, label: it.label }));
|
|
if (t.label) tblLabels[t.table] = t.label;
|
|
});
|
|
metaFM.value = m;
|
|
metaTableLabels.value = tblLabels;
|
|
} catch (_e) {
|
|
metaFM.value = {};
|
|
metaTableLabels.value = {};
|
|
}
|
|
};
|
|
|
|
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 getFieldsMap = (ds) => metaFM.value || {};
|
|
|
|
// ==================== 表标签映射 ====================
|
|
const TABLE_LABELS = {
|
|
order: '订单主表',
|
|
order_detail: '订单详情',
|
|
order_cash: '红包订单',
|
|
order_voucher: '立减金订单',
|
|
plan: '活动计划',
|
|
key_batch: 'key批次',
|
|
code_batch: '兑换码批次',
|
|
voucher: '立减金',
|
|
voucher_batch: '立减金批次',
|
|
merchant_key_send: '开放平台发放记录',
|
|
order_digit: '直充卡密订单',
|
|
merchant: '客户',
|
|
activity: '活动',
|
|
goods_voucher_batch: '立减金批次表',
|
|
goods_voucher_subject_config: '立减金主体配置'
|
|
};
|
|
|
|
const tableLabel = (table) => metaTableLabels.value[table] || TABLE_LABELS[table] || table;
|
|
|
|
// ==================== 字段选项构建 ====================
|
|
const buildFieldNode = (table, children = []) => ({
|
|
value: table,
|
|
label: tableLabel(table),
|
|
children
|
|
});
|
|
|
|
const buildYmtOrderChildren = (FM, type) => {
|
|
const orderFields = FM['order'] || [];
|
|
const ch = [...orderFields.map(f => ({ value: f.value, label: f.label }))];
|
|
ch.push(buildFieldNode('merchant', FM['merchant'] || []));
|
|
ch.push(buildFieldNode('activity', FM['activity'] || []));
|
|
|
|
if (type === 2) {
|
|
ch.push(buildFieldNode('order_digit', FM['order_digit'] || []));
|
|
} else if (type === 3) {
|
|
ch.push(buildFieldNode('order_voucher', FM['order_voucher'] || []));
|
|
ch.push(buildFieldNode('goods_voucher_batch', FM['goods_voucher_batch'] || []));
|
|
ch.push(buildFieldNode('goods_voucher_subject_config', FM['goods_voucher_subject_config'] || []));
|
|
} else if (type === 1) {
|
|
ch.push(buildFieldNode('order_cash', FM['order_cash'] || []));
|
|
} else {
|
|
ch.push(buildFieldNode('order_voucher', FM['order_voucher'] || []));
|
|
ch.push(buildFieldNode('order_cash', FM['order_cash'] || []));
|
|
ch.push(buildFieldNode('order_digit', FM['order_digit'] || []));
|
|
ch.push(buildFieldNode('goods_voucher_batch', FM['goods_voucher_batch'] || []));
|
|
ch.push(buildFieldNode('goods_voucher_subject_config', FM['goods_voucher_subject_config'] || []));
|
|
}
|
|
return ch;
|
|
};
|
|
|
|
const buildMarketingOrderChildren = (FM, type) => {
|
|
const orderFields = FM['order'] || [];
|
|
const ch = [...orderFields.map(f => ({ value: f.value, label: f.label }))];
|
|
ch.push(buildFieldNode('order_detail', FM['order_detail'] || []));
|
|
|
|
const planChildren = [
|
|
...(FM['plan'] || []).map(f => ({ value: f.value, label: f.label })),
|
|
buildFieldNode('key_batch', [
|
|
...(FM['key_batch'] || []).map(f => ({ value: f.value, label: f.label })),
|
|
buildFieldNode('code_batch', FM['code_batch'] || [])
|
|
])
|
|
];
|
|
|
|
const voucherChildren = [
|
|
...(FM['order_voucher'] || []).map(f => ({ value: f.value, label: f.label })),
|
|
buildFieldNode('voucher', [
|
|
...(FM['voucher'] || []).map(f => ({ value: f.value, label: f.label })),
|
|
buildFieldNode('voucher_batch', FM['voucher_batch'] || [])
|
|
])
|
|
];
|
|
|
|
if (type === 1) {
|
|
ch.push(buildFieldNode('plan', planChildren));
|
|
ch.push(buildFieldNode('merchant_key_send', FM['merchant_key_send'] || []));
|
|
} else if (type === 2) {
|
|
ch.push(buildFieldNode('order_voucher', voucherChildren));
|
|
ch.push(buildFieldNode('plan', planChildren));
|
|
} else if (type === 3) {
|
|
ch.push(buildFieldNode('order_cash', FM['order_cash'] || []));
|
|
ch.push(buildFieldNode('plan', planChildren));
|
|
} else {
|
|
ch.push(buildFieldNode('order_cash', FM['order_cash'] || []));
|
|
ch.push(buildFieldNode('order_voucher', voucherChildren));
|
|
ch.push(buildFieldNode('plan', planChildren));
|
|
ch.push(buildFieldNode('merchant_key_send', FM['merchant_key_send'] || []));
|
|
}
|
|
return ch;
|
|
};
|
|
|
|
// 树形选择器数据(与级联选择器兼容)
|
|
const fieldTreeData = Vue.computed(() => {
|
|
const ds = state.form.datasource;
|
|
const FM = getFieldsMap(ds);
|
|
const type = Number(state.form.orderType || 0);
|
|
|
|
if (ds === 'ymt') {
|
|
const orderNode = buildFieldNode('order', buildYmtOrderChildren(FM, type));
|
|
return type ? [orderNode] : [{ value: 'scene_order', label: '订单数据', children: [orderNode] }];
|
|
} else {
|
|
const orderNode = buildFieldNode('order', buildMarketingOrderChildren(FM, type));
|
|
return type ? [orderNode] : [{ value: 'scene_order', label: '订单数据', children: [orderNode] }];
|
|
}
|
|
});
|
|
|
|
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 orderTypeOptionsFor = (ds) => {
|
|
if (ds === 'ymt') {
|
|
return [
|
|
{ label: '直充卡密', value: 2 },
|
|
{ label: '立减金', value: 3 },
|
|
{ label: '红包', value: 1 }
|
|
];
|
|
}
|
|
return [
|
|
{ label: '直充卡密', value: 1 },
|
|
{ label: '立减金', value: 2 },
|
|
{ label: '红包', value: 3 }
|
|
];
|
|
};
|
|
|
|
const dsLabel = (v) => {
|
|
if (v === 'marketing') return '营销系统';
|
|
if (v === 'ymt') return '易码通';
|
|
return v || '';
|
|
};
|
|
|
|
const DEFAULT_FIELDS = {
|
|
marketing: 'order_number,creator,out_trade_no,type,status,contract_price,num,total,pay_amount,create_time',
|
|
ymt: 'order_number,creator,out_trade_no,type,status,contract_price,num,pay_amount,create_time'
|
|
};
|
|
|
|
// ==================== 树形选择器 ====================
|
|
const 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;
|
|
}
|
|
};
|
|
|
|
// 设置树形选择器的选中状态
|
|
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 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];
|
|
|
|
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 payload = {
|
|
name: state.form.name,
|
|
datasource: state.form.datasource,
|
|
main_table: (state.form.datasource === 'ymt' ? 'order_info' : 'order'),
|
|
fields,
|
|
filters: { type_eq: Number(state.form.orderType) },
|
|
file_format: state.form.file_format,
|
|
visibility: state.form.visibility,
|
|
owner_id: (getUserId() ? Number(getUserId()) : 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 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: { 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');
|
|
}
|
|
};
|
|
|
|
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();
|
|
const tpl = data?.data || {};
|
|
state.exportTpl = tpl;
|
|
} catch (e) {
|
|
msg('加载模板详情异常', 'error');
|
|
state.exportTpl = { id: null, filters: {}, main_table: '', fields: [], datasource: '', file_format: '' };
|
|
}
|
|
};
|
|
|
|
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 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');
|