MarketingSystemDataExportTool/web/main.refactored.js

910 lines
28 KiB
JavaScript

/**
* 营销系统数据导出工具 - 主入口
* @description 使用模块化架构重构,提升可读性和扩展性
*/
const { createApp, reactive } = Vue;
// ==================== 模块引用 ====================
const { CONSTANTS, getDatasourceOptions, getDatasourceLabel, getMainTable, getOrderTypeOptions, getDefaultOrderType, getSceneOptions, getSceneLabel, getOrderTypeLabel, VISIBILITY_OPTIONS, FORMAT_OPTIONS, getDefaultFields } = window.AppConfig;
const { showMessage, formatDateTime, getMonthRange, clampDialogWidth, parseWidth, calculateJobProgress } = window.AppUtils;
const { fieldsManager, TreeUtils } = window.FieldsModule;
const { createAppState, ValidationRules } = window.StateModule;
const Api = window.ApiService;
// ==================== Vue 应用 ====================
const app = createApp({
setup() {
// ==================== 状态初始化 ====================
const state = reactive(createAppState());
// ==================== 计算属性 ====================
const hasUserId = Vue.computed(() => !!Api.getUserId());
const currentUserId = Vue.computed(() => {
const userId = Api.getUserId();
return userId ? Number(userId) : null;
});
// ==================== 字段元数据管理 ====================
const metaTableLabels = Vue.ref({});
const recommendedMeta = Vue.ref([]);
/**
* 加载字段元数据
* @param {string} datasource - 数据源
* @param {number} orderType - 订单类型
* @returns {Promise<string[]>} 推荐字段列表
*/
const loadFieldsMetadata = async (datasource, orderType) => {
try {
const { tables, recommended } = await Api.fetchFieldsMetadata(datasource, orderType);
fieldsManager.updateMetadata(tables, recommended);
metaTableLabels.value = fieldsManager.tableLabels;
recommendedMeta.value = recommended;
return recommended;
} catch (error) {
console.error('加载字段元数据失败:', error);
fieldsManager.updateMetadata([], []);
metaTableLabels.value = {};
recommendedMeta.value = [];
return [];
}
};
// ==================== 树形选择器数据 ====================
const fieldTreeData = Vue.computed(() => {
return fieldsManager.buildFieldTree(
state.form.datasource,
state.form.orderType
);
});
const editFieldTreeData = Vue.computed(() => {
return fieldsManager.buildFieldTree(
state.edit.datasource,
state.edit.orderType,
state.edit.main_table
);
});
// ==================== 选项配置 ====================
const datasourceOptions = getDatasourceOptions();
const visibilityOptions = VISIBILITY_OPTIONS;
const formatOptions = FORMAT_OPTIONS;
const sceneOptions = Vue.computed(() => getSceneOptions(state.form.datasource));
const editSceneOptions = Vue.computed(() => getSceneOptions(state.edit.datasource));
/**
* 获取订单类型选项
* @param {string} datasource - 数据源
* @returns {Array} 订单类型选项
*/
const orderTypeOptionsFor = (datasource) => getOrderTypeOptions(datasource);
/**
* 获取数据源标签
* @param {string} value - 数据源值
* @returns {string} 数据源标签
*/
const dsLabel = (value) => getDatasourceLabel(value);
// ==================== 树形选择器引用 ====================
const createFieldsTree = Vue.ref(null);
const editFieldsTree = Vue.ref(null);
/**
* 树形选择器复选框变化处理
* @param {string} kind - 操作类型: 'create' | 'edit'
*/
const onTreeCheck = (kind) => {
if (kind !== 'create' && kind !== 'edit') {
console.warn('onTreeCheck: 无效的 kind 参数', kind);
return;
}
const tree = kind === 'create' ? createFieldsTree.value : editFieldsTree.value;
if (!tree) return;
const checkedKeys = tree.getCheckedKeys();
const treeData = kind === 'create' ? fieldTreeData.value : editFieldTreeData.value;
const paths = TreeUtils.checkedKeysToLeafPaths(checkedKeys, treeData);
if (kind === 'create') {
state.form.fieldsSel = paths;
} else {
state.edit.fieldsSel = paths;
}
};
/**
* 设置树形选择器选中状态
* @param {string} kind - 操作类型
* @param {Array} values - 选中值
*/
const setTreeChecked = (kind, values) => {
const tree = kind === 'create' ? createFieldsTree.value : editFieldsTree.value;
if (!tree || !values || !Array.isArray(values)) return;
const treeData = kind === 'create' ? fieldTreeData.value : editFieldTreeData.value;
const keys = TreeUtils.pathsToNodeKeys(values, treeData);
Vue.nextTick(() => {
setTimeout(() => {
try {
if (keys.length > 0 && tree) {
tree.setCheckedKeys(keys);
}
} catch (error) {
console.warn('设置树形选择器选中状态失败:', error);
}
}, CONSTANTS.TREE_RENDER_DELAY);
});
};
// 监听字段选择变化
Vue.watch(() => state.form.fieldsSel, (newVal) => {
if (newVal?.length > 0) {
Vue.nextTick(() => setTreeChecked('create', newVal));
}
});
Vue.watch(() => state.edit.fieldsSel, (newVal) => {
if (newVal?.length > 0) {
Vue.nextTick(() => setTreeChecked('edit', newVal));
}
});
// ==================== 表单验证规则 ====================
const createRules = ValidationRules.createTemplateRules();
const editRules = ValidationRules.createEditRules();
const exportRules = ValidationRules.createExportRules();
// ==================== 表单引用 ====================
const createFormRef = Vue.ref(null);
const editFormRef = Vue.ref(null);
const exportFormRef = Vue.ref(null);
// ==================== 选项数据 ====================
const creatorOptions = Vue.ref([]);
const ymtCreatorOptions = Vue.ref([]);
const resellerOptions = Vue.ref([]);
const planOptions = Vue.ref([]);
const ymtMerchantOptions = Vue.ref([]);
const ymtActivityOptions = Vue.ref([]);
/**
* 加载创建者列表
*/
const loadCreators = async () => {
try {
creatorOptions.value = await Api.fetchCreators();
} catch (error) {
console.error('加载创建者列表失败:', error);
creatorOptions.value = [];
}
};
/**
* 加载易码通用户列表
*/
const loadYmtCreators = async () => {
try {
const userId = Api.getUserId();
ymtCreatorOptions.value = await Api.fetchYmtUsers(userId || undefined);
} catch (error) {
console.error('加载易码通用户列表失败:', error);
ymtCreatorOptions.value = [];
}
};
/**
* 加载分销商列表
*/
const loadResellers = async () => {
const ids = state.exportForm.creatorIds;
if (!ids?.length) {
resellerOptions.value = [];
return;
}
try {
resellerOptions.value = await Api.fetchResellers(ids);
} catch (error) {
console.error('加载分销商列表失败:', error);
resellerOptions.value = [];
}
};
/**
* 加载易码通客户列表
*/
const loadYmtMerchants = async () => {
const userId = state.exportForm.ymtCreatorId;
if (!userId) {
ymtMerchantOptions.value = [];
return;
}
try {
ymtMerchantOptions.value = await Api.fetchYmtMerchants(userId);
} catch (error) {
console.error('加载客户列表失败:', error);
ymtMerchantOptions.value = [];
}
};
/**
* 加载易码通活动列表
*/
const loadYmtActivities = async () => {
const merchantId = state.exportForm.ymtMerchantId;
if (!merchantId) {
ymtActivityOptions.value = [];
return;
}
try {
ymtActivityOptions.value = await Api.fetchYmtActivities(merchantId);
} catch (error) {
console.error('加载活动列表失败:', error);
ymtActivityOptions.value = [];
}
};
/**
* 加载计划列表
*/
const loadPlans = async () => {
const resellerId = state.exportForm.resellerId;
if (!resellerId) {
planOptions.value = [];
return;
}
try {
planOptions.value = await Api.fetchPlans(resellerId);
} catch (error) {
console.error('加载计划列表失败:', error);
planOptions.value = [];
}
};
// ==================== 导出相关计算属性 ====================
const hasCreators = Vue.computed(() => state.exportForm.creatorIds?.length > 0);
const hasReseller = Vue.computed(() => !!state.exportForm.resellerId);
const hasPlan = Vue.computed(() => !!state.exportForm.planId);
const hasKeyBatch = Vue.computed(() => !!state.exportForm.keyBatchId);
const hasCodeBatch = Vue.computed(() => !!state.exportForm.codeBatchId);
const exportType = Vue.computed(() => {
const filters = state.exportTpl?.filters;
if (!filters) return null;
if (filters.type_eq != null) return Number(filters.type_eq);
if (Array.isArray(filters.type_in) && filters.type_in.length === 1) {
return Number(filters.type_in[0]);
}
return null;
});
const exportTypeList = Vue.computed(() => {
const filters = state.exportTpl?.filters;
if (!filters) return [];
if (Array.isArray(filters.type_in) && filters.type_in.length) {
return filters.type_in.map(Number);
}
if (filters.type_eq != null) return [Number(filters.type_eq)];
return [];
});
const isOrder = Vue.computed(() => {
const mainTable = state.exportTpl?.main_table;
return mainTable === 'order' || mainTable === 'order_info';
});
const exportTitle = Vue.computed(() => {
let title = '执行导出';
const mainTable = state.exportTpl?.main_table;
if (mainTable) {
title += ' - ' + getSceneLabel(mainTable);
if (mainTable === 'order' || mainTable === 'order_info') {
const datasource = state.exportTpl?.datasource;
const labels = exportTypeList.value
.map(type => getOrderTypeLabel(datasource, type))
.filter(Boolean);
if (labels.length) {
title += ' - 订单类型:' + labels.join('、');
}
}
}
return title;
});
// ==================== 模板管理 ====================
/**
* 加载模板列表
*/
const loadTemplates = async () => {
try {
state.templates = await Api.fetchTemplates();
} catch (error) {
showMessage('加载模板失败', 'error');
state.templates = [];
}
};
/**
* 创建模板
*/
const createTemplate = async () => {
const formRef = createFormRef.value;
const valid = formRef ? await formRef.validate().catch(() => false) : true;
if (!valid) {
showMessage('请完善必填项', 'error');
return;
}
let fields = [];
const { datasource, main_table, fieldsSel, fieldsRaw, orderType, name, file_format, visibility } = state.form;
if (fieldsSel?.length) {
fields = fieldsManager.convertPathsToFields(fieldsSel, datasource, main_table);
} else {
const recommended = recommendedMeta.value;
if (recommended?.length) {
fields = recommended;
} else {
fields = getDefaultFields(datasource);
}
}
const payload = {
name,
datasource,
main_table: getMainTable(datasource),
fields,
filters: { type_eq: Number(orderType) },
file_format,
visibility,
owner_id: Api.getUserId() ? Number(Api.getUserId()) : 0
};
try {
await Api.createTemplate(payload);
showMessage('创建成功');
state.createVisible = false;
loadTemplates();
} catch (error) {
showMessage(error.message || '创建失败', 'error');
}
};
/**
* 打开编辑对话框
* @param {Object} row - 模板行数据
*/
const openEdit = async (row) => {
state.edit.id = row.id;
try {
const template = await Api.fetchTemplateDetail(row.id);
state.edit.name = template.name || row.name || '';
state.edit.datasource = template.datasource || row.datasource || 'marketing';
state.edit.main_table = template.main_table || row.main_table || 'order';
state.edit.file_format = template.file_format || row.file_format || 'xlsx';
state.edit.visibility = template.visibility || row.visibility || 'private';
const filters = template.filters || {};
if (filters.type_eq != null) {
state.edit.orderType = Number(filters.type_eq);
} else if (Array.isArray(filters.type_in) && filters.type_in.length === 1) {
state.edit.orderType = Number(filters.type_in[0]);
} else {
state.edit.orderType = getDefaultOrderType(state.edit.datasource);
}
const fields = Array.isArray(template.fields) ? template.fields : [];
state.editVisible = true;
await Vue.nextTick();
await loadFieldsMetadata(state.edit.datasource, state.edit.orderType);
await Vue.nextTick();
const mainTable = state.edit.main_table || 'order';
const paths = fieldsManager.deduplicatePaths(
fieldsManager.convertFieldsToPaths(fields, state.edit.datasource, mainTable)
);
state.edit.fieldsSel = paths;
await Vue.nextTick();
setTimeout(() => setTreeChecked('edit', paths), CONSTANTS.TREE_EDIT_RENDER_DELAY);
} catch (error) {
console.error('加载模板详情失败:', error);
state.edit.name = row.name;
state.edit.datasource = row.datasource || 'marketing';
state.edit.main_table = row.main_table || 'order';
state.edit.file_format = row.file_format || 'xlsx';
state.edit.visibility = row.visibility || 'private';
state.edit.orderType = getDefaultOrderType(state.edit.datasource);
state.edit.fieldsSel = [];
state.editVisible = true;
}
};
/**
* 保存编辑
*/
const saveEdit = async () => {
const formRef = editFormRef.value;
const valid = formRef ? await formRef.validate().catch(() => false) : true;
if (!valid) {
showMessage('请完善必填项', 'error');
return;
}
const { id, name, datasource, main_table, fieldsSel, visibility, file_format, orderType } = state.edit;
let fields = [];
if (fieldsSel?.length) {
fields = fieldsManager.convertPathsToFields(fieldsSel, datasource, main_table || 'order');
} else {
fields = getDefaultFields(datasource);
}
if (!fields.length) {
showMessage('请至少选择一个字段', 'error');
return;
}
const payload = {
name,
visibility,
file_format,
fields,
filters: { type_eq: Number(orderType || 1) },
main_table: 'order'
};
try {
await Api.updateTemplate(id, payload);
showMessage('保存成功');
state.editVisible = false;
loadTemplates();
} catch (error) {
showMessage(error.message || '保存失败', 'error');
}
};
/**
* 删除模板
* @param {number} id - 模板 ID
*/
const removeTemplate = async (id) => {
try {
await Api.deleteTemplate(id);
showMessage('删除成功');
loadTemplates();
} catch (error) {
showMessage(error.message || '删除失败', 'error');
}
};
// ==================== 导出任务 ====================
/**
* 打开导出对话框
* @param {Object} row - 模板行数据
*/
const openExport = async (row) => {
state.exportForm.tplId = row.id;
try {
const template = await Api.fetchTemplateDetail(row.id);
state.exportTpl = template;
} catch (error) {
console.error('加载模板详情失败:', error);
state.exportTpl = { id: null, filters: {}, main_table: '', fields: [], datasource: '', file_format: '' };
}
state.exportForm.datasource = state.exportTpl.datasource || row.datasource || 'marketing';
state.exportForm.file_format = state.exportTpl.file_format || row.file_format || 'xlsx';
if (state.exportForm.datasource === 'marketing') {
loadCreators();
const userId = Api.getUserId();
if (userId) {
const parts = String(userId).split(',').map(s => s.trim()).filter(Boolean);
state.exportForm.creatorIds = parts.length > 1
? parts.map(Number)
: [Number(userId)];
}
}
if (state.exportForm.datasource === 'ymt') {
await loadYmtCreators();
await loadYmtMerchants();
await loadYmtActivities();
const userId = Api.getUserId();
if (userId) {
const first = String(userId).split(',').map(s => s.trim()).filter(Boolean)[0];
if (first) {
state.exportForm.ymtCreatorId = Number(first);
}
}
}
if (!state.exportForm.dateRange?.length) {
state.exportForm.dateRange = getMonthRange(-1);
}
state.exportVisible = true;
};
/**
* 提交导出任务
*/
const submitExport = async () => {
const formRef = exportFormRef.value;
const valid = formRef ? await formRef.validate().catch(() => false) : true;
if (!valid) {
showMessage('请完善必填项', 'error');
return;
}
state.exportSubmitting = true;
showMessage('估算中', 'info');
try {
const { tplId, dateRange, datasource, file_format, planId, resellerId, voucherChannelActivityId, creatorIds, creatorIdsRaw, ymtCreatorId, ymtMerchantId, ymtActivityId } = state.exportForm;
const filters = {};
const typeValue = exportType.value;
if (typeValue != null) {
filters.type_eq = Number(typeValue);
}
if (dateRange?.length === 2) {
filters.create_time_between = [dateRange[0], dateRange[1]];
}
if (planId) filters.plan_id_eq = Number(planId);
if (resellerId) filters.reseller_id_eq = Number(resellerId);
if (voucherChannelActivityId) {
const type = exportType.value;
if ((datasource === 'marketing' && type === 2) || (datasource === 'ymt' && type === 3)) {
filters.order_voucher_channel_activity_id_eq = voucherChannelActivityId;
}
}
if (creatorIds?.length) {
filters.creator_in = creatorIds.map(Number);
} else if (creatorIdsRaw) {
const arr = String(creatorIdsRaw).split(',').map(s => s.trim()).filter(Boolean);
if (arr.length) filters.creator_in = arr;
}
if (datasource === 'ymt') {
if (String(ymtCreatorId).trim()) {
filters.creator_in = [Number(ymtCreatorId)];
}
if (String(ymtMerchantId).trim()) {
filters.reseller_id_eq = Number(ymtMerchantId);
}
if (String(ymtActivityId).trim()) {
filters.plan_id_eq = Number(ymtActivityId);
}
}
const payload = {
template_id: Number(tplId),
requested_by: 1,
permission: {},
options: {},
filters,
file_format,
datasource
};
const result = await Api.createExportJob(payload);
const jobId = result?.data?.id ?? result?.id;
state.exportVisible = false;
if (jobId) {
state.jobsTplId = Number(tplId);
state.jobsVisible = true;
loadJobs(1);
startJobsPolling();
} else {
showMessage('任务创建返回异常', 'error');
}
} catch (error) {
showMessage(error.message || '导出失败', 'error');
} finally {
state.exportSubmitting = false;
}
};
// ==================== 任务管理 ====================
let jobsPollTimer = null;
/**
* 加载任务列表
* @param {number} [page] - 页码
*/
const loadJobs = async (page) => {
page = page || state.jobsPage;
try {
const { items, total, page: currentPage } = await Api.fetchJobs({
page,
pageSize: state.jobsPageSize,
templateId: state.jobsTplId
});
state.jobs = items;
state.jobsTotal = total;
state.jobsPage = currentPage;
} catch (error) {
console.error('加载任务列表失败:', error);
state.jobs = [];
}
};
/**
* 检查是否所有任务都已完成
*/
const checkAndStopPollingIfComplete = () => {
const hasRunningJob = state.jobs.some(
job => job.status === 'queued' || job.status === 'running'
);
if (!hasRunningJob && state.jobs.length > 0) {
stopJobsPolling();
}
};
/**
* 启动任务轮询
*/
const startJobsPolling = () => {
if (jobsPollTimer) return;
jobsPollTimer = setInterval(async () => {
if (state.jobsVisible) {
await loadJobs(state.jobsPage);
checkAndStopPollingIfComplete();
}
}, CONSTANTS.JOBS_POLL_INTERVAL);
};
/**
* 停止任务轮询
*/
const stopJobsPolling = () => {
if (jobsPollTimer) {
clearInterval(jobsPollTimer);
jobsPollTimer = null;
}
};
/**
* 打开任务列表
* @param {Object} row - 模板行数据
*/
const openJobs = (row) => {
state.jobsTplId = row.id;
state.jobsVisible = true;
loadJobs(1);
startJobsPolling();
};
/**
* 关闭任务列表
*/
const closeJobs = () => {
state.jobsVisible = false;
stopJobsPolling();
};
/**
* 计算任务进度
* @param {Object} row - 任务行数据
* @returns {string} 进度描述
*/
const jobPercent = (row) => calculateJobProgress(row);
/**
* 下载文件
* @param {number} id - 任务 ID
*/
const download = (id) => {
window.open(Api.getDownloadUrl(id), '_blank');
};
/**
* 打开 SQL 预览
* @param {number} id - 任务 ID
*/
const openSQL = async (id) => {
try {
state.sqlExplainDesc = '';
state.sqlText = await Api.fetchJobSql(id);
try {
const job = await Api.fetchJobDetail(id);
state.sqlExplainDesc = job?.eval_desc || job?.eval_status || '';
} catch {
const row = state.jobs.find(r => Number(r.id) === Number(id));
state.sqlExplainDesc = row?.eval_desc || row?.eval_status || '';
}
state.sqlVisible = true;
} catch (error) {
console.error('加载SQL失败:', error);
state.sqlText = '';
state.sqlExplainDesc = '';
state.sqlVisible = false;
showMessage('加载SQL失败', 'error');
}
};
// ==================== 对话框尺寸管理 ====================
/**
* 调整对话框尺寸
* @param {string} kind - 对话框类型
* @param {number} delta - 变化量
*/
const resizeDialog = (kind, delta) => {
if (kind === 'create') {
const current = parseWidth(state.createWidth, CONSTANTS.DIALOG_DEFAULT_WIDTH);
const next = clampDialogWidth(current + delta);
state.createWidth = next;
localStorage.setItem('tplDialogWidth', next);
} else if (kind === 'edit') {
const current = parseWidth(state.editWidth, CONSTANTS.DIALOG_EDIT_DEFAULT_WIDTH);
const next = clampDialogWidth(current + delta);
state.editWidth = next;
localStorage.setItem('tplEditDialogWidth', next);
}
};
// ==================== 监听器 ====================
// 数据源变化
Vue.watch(() => state.form.datasource, async (datasource) => {
state.form.fieldsSel = [];
state.form.main_table = getMainTable(datasource);
state.form.orderType = getDefaultOrderType(datasource);
await loadFieldsMetadata(datasource, state.form.orderType);
});
// 订单类型变化
Vue.watch(() => state.form.orderType, async () => {
state.form.fieldsSel = [];
await loadFieldsMetadata(state.form.datasource, state.form.orderType);
});
// 编辑数据源变化
Vue.watch(() => state.edit.datasource, async (datasource) => {
state.edit.fieldsSel = [];
state.edit.main_table = getMainTable(datasource);
if (!state.edit.orderType) {
state.edit.orderType = getDefaultOrderType(datasource);
}
await loadFieldsMetadata(datasource, state.edit.orderType);
});
// 编辑订单类型变化
Vue.watch(() => state.edit.orderType, async () => {
state.edit.fieldsSel = [];
await loadFieldsMetadata(state.edit.datasource, state.edit.orderType);
});
// 导出筛选条件变化
Vue.watch(() => state.exportForm.creatorIds, () => {
state.exportForm.resellerId = null;
state.exportForm.planId = null;
state.exportForm.keyBatchId = null;
state.exportForm.codeBatchId = null;
loadResellers();
});
Vue.watch(() => state.exportForm.ymtCreatorId, () => {
state.exportForm.ymtMerchantId = null;
state.exportForm.ymtActivityId = null;
loadYmtMerchants();
});
Vue.watch(() => state.exportForm.ymtMerchantId, () => {
state.exportForm.ymtActivityId = null;
loadYmtActivities();
});
Vue.watch(() => state.exportForm.resellerId, () => {
state.exportForm.planId = null;
state.exportForm.keyBatchId = null;
state.exportForm.codeBatchId = null;
loadPlans();
});
Vue.watch(() => state.exportForm.planId, () => {
state.exportForm.keyBatchId = null;
state.exportForm.codeBatchId = null;
});
Vue.watch(() => state.exportForm.keyBatchId, () => {
state.exportForm.codeBatchId = null;
});
// ==================== 初始化 ====================
loadTemplates();
loadFieldsMetadata(state.form.datasource, state.form.orderType);
// 组件销毁时清理
Vue.onUnmounted(() => {
stopJobsPolling();
});
// ==================== 返回 ====================
return {
...Vue.toRefs(state),
// 选项配置
visibilityOptions,
formatOptions,
datasourceOptions,
sceneOptions,
editSceneOptions,
// 模板管理
loadTemplates,
createTemplate,
openEdit,
saveEdit,
removeTemplate,
// 导出管理
openExport,
submitExport,
// 任务管理
loadJobs,
openJobs,
closeJobs,
download,
openSQL,
jobPercent,
// 对话框
resizeDialog,
// 验证规则
createRules,
editRules,
exportRules,
// 表单引用
createFormRef,
editFormRef,
exportFormRef,
// 树形选择器
createFieldsTree,
editFieldsTree,
fieldTreeData,
editFieldTreeData,
onTreeCheck,
setTreeChecked,
// 工具函数
dsLabel,
orderTypeOptionsFor,
fmtDT: formatDateTime,
// 计算属性
exportType,
isOrder,
exportTitle,
hasUserId,
currentUserId,
hasCreators,
hasReseller,
hasPlan,
hasKeyBatch,
hasCodeBatch,
// 选项数据
creatorOptions,
ymtCreatorOptions,
ymtMerchantOptions,
ymtActivityOptions,
resellerOptions,
planOptions
};
}
});
app.use(ElementPlus);
app.mount('#app');