refactor(web): 重构脚本引入为模块化架构

- 移除对vue及element-plus脚本的defer属性,确保按顺序加载
- 拆分原有配置与功能脚本为多个模块文件
- 引入config、utils、api、fields、state等模块化脚本
- 替换主入口脚本为重构后的 main.refactored.js
- 优化脚本加载顺序,提高代码可维护性与扩展性
This commit is contained in:
zhouyonggao 2025-12-17 09:40:10 +08:00
parent 4f74eec055
commit 913f93fabd
7 changed files with 2580 additions and 3 deletions

View File

@ -310,8 +310,14 @@
</el-dialog>
</div>
<script src="./config.js"></script>
<script src="./vendor/vue.global.prod.js" defer></script>
<script src="./vendor/element-plus.full.min.js" defer></script>
<script src="./main.js" defer></script>
<script src="./vendor/vue.global.prod.js"></script>
<script src="./vendor/element-plus.full.min.js"></script>
<!-- 模块化架构 -->
<script src="./modules/config.js"></script>
<script src="./modules/utils.js"></script>
<script src="./modules/api.js"></script>
<script src="./modules/fields.js"></script>
<script src="./modules/state.js"></script>
<script src="./main.refactored.js" defer></script>
</body>
</html>

909
web/main.refactored.js Normal file
View File

@ -0,0 +1,909 @@
/**
* 营销系统数据导出工具 - 主入口
* @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');

429
web/modules/api.js Normal file
View File

@ -0,0 +1,429 @@
/**
* API 服务层 - 统一处理 HTTP 请求
* @module api
*/
/**
* 获取 API 基础地址
* @returns {string} API 基础 URL
*/
const getApiBase = () => {
if (window.__API_BASE__ && String(window.__API_BASE__).trim()) {
return String(window.__API_BASE__).trim();
}
return typeof location !== 'undefined' ? location.origin : 'http://localhost:8077';
};
const API_BASE = getApiBase();
/**
* URL 参数中获取用户 ID
* @returns {string} 用户 ID不存在返回空字符串
*/
const getUserId = () => {
const params = new URLSearchParams(window.location.search || '');
const value = params.get('userId') || params.get('userid') || params.get('user_id');
return value && String(value).trim() ? String(value).trim() : '';
};
/**
* URL 参数中获取商户 ID
* @returns {string} 商户 ID不存在返回空字符串
*/
const getMerchantId = () => {
const params = new URLSearchParams(window.location.search || '');
const value = params.get('merchantId') || params.get('merchantid') || params.get('merchant_id');
return value && String(value).trim() ? String(value).trim() : '';
};
/**
* 构建用户相关的查询字符串
* @returns {string} 查询字符串 '?userId=1&merchantId=2'
*/
const buildUserQueryString = () => {
const userId = getUserId();
const merchantId = getMerchantId();
const parts = [];
if (userId) parts.push('userId=' + encodeURIComponent(userId));
if (merchantId) parts.push('merchantId=' + encodeURIComponent(merchantId));
return parts.length ? ('?' + parts.join('&')) : '';
};
/**
* 解析 API 响应数据
* @param {Object} data - 响应数据对象
* @returns {Array} 解析后的数组数据
*/
const parseArrayResponse = (data) => {
if (Array.isArray(data?.data)) return data.data;
if (Array.isArray(data)) return data;
return [];
};
/**
* 解析分页响应数据
* @param {Object} data - 响应数据对象
* @returns {{items: Array, total: number, page: number}} 分页数据
*/
const parsePaginatedResponse = (data) => {
const payload = data?.data || data || {};
return {
items: Array.isArray(payload.items) ? payload.items : (Array.isArray(payload) ? payload : []),
total: Number(payload.total || 0),
page: Number(payload.page || 1)
};
};
/**
* 通用 GET 请求
* @param {string} endpoint - API 端点
* @param {Object} [options] - 请求选项
* @param {boolean} [options.withUserQuery=false] - 是否附加用户查询参数
* @param {Object} [options.params] - URL 查询参数
* @returns {Promise<Object>} 响应数据
*/
const get = async (endpoint, options = {}) => {
const { withUserQuery = false, params = {} } = options;
let url = API_BASE + endpoint;
const queryParams = new URLSearchParams(params);
if (withUserQuery) {
const userId = getUserId();
const merchantId = getMerchantId();
if (userId) queryParams.set('userId', userId);
if (merchantId) queryParams.set('merchantId', merchantId);
}
const queryString = queryParams.toString();
if (queryString) {
url += (endpoint.includes('?') ? '&' : '?') + queryString;
}
const response = await fetch(url);
if (!response.ok) {
throw new Error(`请求失败: ${response.status} ${response.statusText}`);
}
return response.json();
};
/**
* 通用 POST 请求
* @param {string} endpoint - API 端点
* @param {Object} body - 请求体
* @param {Object} [options] - 请求选项
* @param {boolean} [options.withUserQuery=false] - 是否附加用户查询参数
* @returns {Promise<Object>} 响应数据
*/
const post = async (endpoint, body, options = {}) => {
const { withUserQuery = false } = options;
let url = API_BASE + endpoint;
if (withUserQuery) {
url += buildUserQueryString();
}
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText || `请求失败: ${response.status}`);
}
return response.json();
};
/**
* 通用 PATCH 请求
* @param {string} endpoint - API 端点
* @param {Object} body - 请求体
* @returns {Promise<Object>} 响应数据
*/
const patch = async (endpoint, body) => {
const url = API_BASE + endpoint;
const response = await fetch(url, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(body)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText || `请求失败: ${response.status}`);
}
return response.json();
};
/**
* 通用 DELETE 请求
* @param {string} endpoint - API 端点
* @param {Object} [options] - 请求选项
* @param {boolean} [options.soft=false] - 是否软删除
* @returns {Promise<Object>} 响应数据
*/
const del = async (endpoint, options = {}) => {
const { soft = false } = options;
let url = API_BASE + endpoint;
if (soft) {
url += (endpoint.includes('?') ? '&' : '?') + 'soft=1';
}
const response = await fetch(url, { method: 'DELETE' });
if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText || `请求失败: ${response.status}`);
}
return response.json();
};
// ==================== 模板相关 API ====================
/**
* 加载模板列表
* @returns {Promise<Array>} 模板数组
*/
const fetchTemplates = async () => {
const data = await get('/api/templates', { withUserQuery: true });
return parseArrayResponse(data);
};
/**
* 获取模板详情
* @param {number|string} id - 模板 ID
* @returns {Promise<Object>} 模板详情
*/
const fetchTemplateDetail = async (id) => {
const data = await get(`/api/templates/${id}`);
return data?.data || {};
};
/**
* 创建模板
* @param {Object} payload - 模板数据
* @returns {Promise<Object>} 创建结果
*/
const createTemplate = async (payload) => {
return post('/api/templates', payload, { withUserQuery: true });
};
/**
* 更新模板
* @param {number|string} id - 模板 ID
* @param {Object} payload - 更新数据
* @returns {Promise<Object>} 更新结果
*/
const updateTemplate = async (id, payload) => {
return patch(`/api/templates/${id}`, payload);
};
/**
* 删除模板软删除
* @param {number|string} id - 模板 ID
* @returns {Promise<Object>} 删除结果
*/
const deleteTemplate = async (id) => {
return del(`/api/templates/${id}`, { soft: true });
};
// ==================== 导出任务相关 API ====================
/**
* 加载导出任务列表
* @param {Object} params - 查询参数
* @param {number} params.page - 页码
* @param {number} params.pageSize - 每页数量
* @param {number} [params.templateId] - 模板 ID
* @returns {Promise<{items: Array, total: number, page: number}>} 分页数据
*/
const fetchJobs = async ({ page, pageSize, templateId }) => {
const params = {
page: String(page),
page_size: String(pageSize)
};
if (templateId) {
params.template_id = String(templateId);
}
const data = await get('/api/exports', { params, withUserQuery: true });
return parsePaginatedResponse(data);
};
/**
* 获取任务详情
* @param {number|string} id - 任务 ID
* @returns {Promise<Object>} 任务详情
*/
const fetchJobDetail = async (id) => {
const data = await get(`/api/exports/${id}`, { withUserQuery: true });
return data?.data || {};
};
/**
* 获取任务 SQL
* @param {number|string} id - 任务 ID
* @returns {Promise<string>} SQL 语句
*/
const fetchJobSql = async (id) => {
const data = await get(`/api/exports/${id}/sql`);
return data?.data?.final_sql || data?.final_sql || data?.data?.sql || data?.sql || '';
};
/**
* 创建导出任务
* @param {Object} payload - 任务数据
* @returns {Promise<Object>} 创建结果
*/
const createExportJob = async (payload) => {
return post('/api/exports', payload, { withUserQuery: true });
};
/**
* 获取下载地址
* @param {number|string} id - 任务 ID
* @returns {string} 下载 URL
*/
const getDownloadUrl = (id) => {
return `${API_BASE}/api/exports/${id}/download`;
};
// ==================== 元数据相关 API ====================
/**
* 加载字段元数据
* @param {string} datasource - 数据源
* @param {number} orderType - 订单类型
* @returns {Promise<Object>} 元数据对象 { tables, recommended }
*/
const fetchFieldsMetadata = async (datasource, orderType) => {
const params = {
datasource: datasource,
order_type: String(orderType || 0)
};
const data = await get('/api/metadata/fields', { params });
const tables = Array.isArray(data?.data?.tables)
? data.data.tables
: (Array.isArray(data?.tables) ? data.tables : []);
const recommended = Array.isArray(data?.data?.recommended)
? data.data.recommended
: (Array.isArray(data?.recommended) ? data.recommended : []);
return { tables, recommended };
};
// ==================== 创建者/分销商等选项 API ====================
/**
* 加载创建者列表营销系统
* @returns {Promise<Array<{label: string, value: number}>>} 选项数组
*/
const fetchCreators = async () => {
const data = await get('/api/creators');
const arr = parseArrayResponse(data);
return arr.map(it => ({ label: it.name || String(it.id), value: Number(it.id) }));
};
/**
* 加载易码通用户列表
* @param {string} [query] - 搜索关键词
* @returns {Promise<Array<{label: string, value: number}>>} 选项数组
*/
const fetchYmtUsers = async (query) => {
const params = { limit: '2000' };
if (query) params.q = query;
const data = await get('/api/ymt/users', { params });
const arr = parseArrayResponse(data);
return arr.map(it => ({ label: it.name || String(it.id), value: Number(it.id) }));
};
/**
* 加载分销商列表
* @param {number[]} creatorIds - 创建者 ID 数组
* @returns {Promise<Array<{label: string, value: number}>>} 选项数组
*/
const fetchResellers = async (creatorIds) => {
if (!creatorIds?.length) return [];
const data = await get('/api/resellers', { params: { creator: creatorIds.join(',') } });
const arr = parseArrayResponse(data);
return arr.map(it => ({ label: it.name || String(it.id), value: Number(it.id) }));
};
/**
* 加载计划列表
* @param {number} resellerId - 分销商 ID
* @returns {Promise<Array<{label: string, value: number}>>} 选项数组
*/
const fetchPlans = async (resellerId) => {
if (!resellerId) return [];
const data = await get('/api/plans', { params: { reseller: String(resellerId) } });
const arr = parseArrayResponse(data);
return arr.map(it => ({ label: `${it.id} - ${it.title || ''}`, value: Number(it.id) }));
};
/**
* 加载易码通客户列表
* @param {number} userId - 用户 ID
* @returns {Promise<Array<{label: string, value: number}>>} 选项数组
*/
const fetchYmtMerchants = async (userId) => {
if (!userId) return [];
const data = await get('/api/ymt/merchants', { params: { user_id: String(userId), limit: '2000' } });
const arr = parseArrayResponse(data);
return arr.map(it => ({ label: `${it.id} - ${it.name || ''}`, value: Number(it.id) }));
};
/**
* 加载易码通活动列表
* @param {number} merchantId - 客户 ID
* @returns {Promise<Array<{label: string, value: number}>>} 选项数组
*/
const fetchYmtActivities = async (merchantId) => {
if (!merchantId) return [];
const data = await get('/api/ymt/activities', { params: { merchant_id: String(merchantId), limit: '2000' } });
const arr = parseArrayResponse(data);
return arr.map(it => ({ label: `${it.id} - ${it.name || ''}`, value: Number(it.id) }));
};
// 导出模块
window.ApiService = {
// 基础方法
API_BASE,
getUserId,
getMerchantId,
buildUserQueryString,
get,
post,
patch,
del,
// 模板
fetchTemplates,
fetchTemplateDetail,
createTemplate,
updateTemplate,
deleteTemplate,
// 任务
fetchJobs,
fetchJobDetail,
fetchJobSql,
createExportJob,
getDownloadUrl,
// 元数据
fetchFieldsMetadata,
// 选项数据
fetchCreators,
fetchYmtUsers,
fetchResellers,
fetchPlans,
fetchYmtMerchants,
fetchYmtActivities
};

321
web/modules/config.js Normal file
View File

@ -0,0 +1,321 @@
/**
* 配置模块 - 集中管理常量映射和配置
* @module config
*/
// ==================== 系统常量 ====================
/**
* 系统常量配置
*/
const CONSTANTS = {
// 轮询间隔(毫秒)
JOBS_POLL_INTERVAL: 1000,
// 树形选择器渲染延迟(毫秒)
TREE_RENDER_DELAY: 100,
// 编辑模式树渲染延迟(毫秒)
TREE_EDIT_RENDER_DELAY: 300,
// 对话框最小宽度
DIALOG_MIN_WIDTH: 500,
// 对话框最大宽度
DIALOG_MAX_WIDTH: 1400,
// 默认对话框宽度
DIALOG_DEFAULT_WIDTH: 900,
// 默认编辑对话框宽度
DIALOG_EDIT_DEFAULT_WIDTH: 900
};
// ==================== 数据源配置 ====================
/**
* 数据源定义
* @type {Object<string, {label: string, mainTable: string, orderTypes: Array}>}
*/
const DATASOURCE_CONFIG = {
marketing: {
label: '营销系统',
mainTable: 'order',
orderTypes: [
{ label: '直充卡密', value: 1 },
{ label: '立减金', value: 2 },
{ label: '红包', value: 3 }
],
defaultOrderType: 1
},
ymt: {
label: '易码通',
mainTable: 'order_info',
orderTypes: [
{ label: '直充卡密', value: 2 },
{ label: '立减金', value: 3 },
{ label: '红包', value: 1 }
],
defaultOrderType: 2
}
};
/**
* 获取数据源选项列表
* @returns {Array<{label: string, value: string}>}
*/
const getDatasourceOptions = () => {
return Object.entries(DATASOURCE_CONFIG).map(([value, config]) => ({
label: config.label,
value
}));
};
/**
* 获取数据源标签
* @param {string} datasource - 数据源标识
* @returns {string} 数据源标签
*/
const getDatasourceLabel = (datasource) => {
return DATASOURCE_CONFIG[datasource]?.label || datasource || '';
};
/**
* 获取数据源的主表名
* @param {string} datasource - 数据源标识
* @returns {string} 主表名
*/
const getMainTable = (datasource) => {
return DATASOURCE_CONFIG[datasource]?.mainTable || 'order';
};
/**
* 获取订单类型选项
* @param {string} datasource - 数据源标识
* @returns {Array<{label: string, value: number}>} 订单类型选项
*/
const getOrderTypeOptions = (datasource) => {
return DATASOURCE_CONFIG[datasource]?.orderTypes || [];
};
/**
* 获取默认订单类型
* @param {string} datasource - 数据源标识
* @returns {number} 默认订单类型
*/
const getDefaultOrderType = (datasource) => {
return DATASOURCE_CONFIG[datasource]?.defaultOrderType || 1;
};
/**
* 获取订单类型标签
* @param {string} datasource - 数据源标识
* @param {number} typeValue - 类型值
* @returns {string} 类型标签
*/
const getOrderTypeLabel = (datasource, typeValue) => {
const types = DATASOURCE_CONFIG[datasource]?.orderTypes || [];
const found = types.find(t => t.value === typeValue);
return found?.label || '';
};
// ==================== 表标签映射 ====================
/**
* 默认表标签映射当后端未返回时使用
*/
const DEFAULT_TABLE_LABELS = {
order: '订单主表',
order_info: '订单主表',
order_detail: '订单详情',
order_cash: '红包订单',
order_voucher: '立减金订单',
order_digit: '直充卡密订单',
plan: '活动计划',
key_batch: 'key批次',
code_batch: '兑换码批次',
voucher: '立减金',
voucher_batch: '立减金批次',
merchant_key_send: '开放平台发放记录',
merchant: '客户',
activity: '活动',
goods_voucher_batch: '立减金批次表',
goods_voucher_subject_config: '立减金主体配置'
};
// ==================== 表关联配置 ====================
/**
* 营销系统表关联结构
* 定义了从主表到各子表的路径关系
*/
const MARKETING_TABLE_RELATIONS = {
order: { path: [] },
order_detail: { path: ['order_detail'] },
plan: { path: ['plan'] },
key_batch: { path: ['plan', 'key_batch'] },
code_batch: { path: ['plan', 'key_batch', 'code_batch'] },
order_voucher: { path: ['order_voucher'] },
voucher: { path: ['order_voucher', 'voucher'] },
voucher_batch: { path: ['order_voucher', 'voucher', 'voucher_batch'] },
order_cash: { path: ['order_cash'] },
merchant_key_send: { path: ['merchant_key_send'] }
};
/**
* 易码通表关联结构
*/
const YMT_TABLE_RELATIONS = {
order: { path: [] },
order_info: { path: [] },
merchant: { path: ['merchant'] },
activity: { path: ['activity'] },
order_digit: { path: ['order_digit'] },
order_voucher: { path: ['order_voucher'] },
order_cash: { path: ['order_cash'] },
goods_voucher_batch: { path: ['goods_voucher_batch'] },
goods_voucher_subject_config: { path: ['goods_voucher_subject_config'] }
};
/**
* 获取表关联配置
* @param {string} datasource - 数据源标识
* @returns {Object} 表关联配置
*/
const getTableRelations = (datasource) => {
return datasource === 'ymt' ? YMT_TABLE_RELATIONS : MARKETING_TABLE_RELATIONS;
};
// ==================== 默认字段配置 ====================
/**
* 各数据源的默认字段列表
*/
const DEFAULT_FIELDS = {
marketing: [
'order.order_number',
'order.creator',
'order.out_trade_no',
'order.type',
'order.status',
'order.contract_price',
'order.num',
'order.total',
'order.pay_amount',
'order.create_time'
],
ymt: [
'order_info.order_number',
'order_info.creator',
'order_info.out_trade_no',
'order_info.type',
'order_info.status',
'order_info.contract_price',
'order_info.num',
'order_info.pay_amount',
'order_info.create_time'
]
};
/**
* 获取默认字段列表
* @param {string} datasource - 数据源标识
* @returns {string[]} 默认字段列表
*/
const getDefaultFields = (datasource) => {
return DEFAULT_FIELDS[datasource] || DEFAULT_FIELDS.marketing;
};
// ==================== 表单选项配置 ====================
/**
* 可见性选项
*/
const VISIBILITY_OPTIONS = [
{ label: '个人', value: 'private' },
{ label: '公共', value: 'public' }
];
/**
* 文件格式选项
*/
const FORMAT_OPTIONS = [
{ label: 'XLSX', value: 'xlsx' },
{ label: 'CSV', value: 'csv' }
];
/**
* 场景选项根据数据源动态生成
* @param {string} datasource - 数据源标识
* @returns {Array<{label: string, value: string}>}
*/
const getSceneOptions = (datasource) => {
const mainTable = getMainTable(datasource);
return [{ label: '订单数据', value: mainTable }];
};
/**
* 获取场景标签
* @param {string} scene - 场景值
* @returns {string} 场景标签
*/
const getSceneLabel = (scene) => {
if (scene === 'order' || scene === 'order_info') return '订单';
return scene || '';
};
// ==================== 订单类型与子表映射 ====================
/**
* 营销系统订单类型对应的子表
*/
const MARKETING_TYPE_TABLES = {
1: ['plan', 'key_batch', 'code_batch', 'merchant_key_send'], // 直充卡密
2: ['order_voucher', 'voucher', 'voucher_batch', 'plan', 'key_batch', 'code_batch'], // 立减金
3: ['order_cash', 'plan', 'key_batch', 'code_batch'] // 红包
};
/**
* 易码通订单类型对应的子表
*/
const YMT_TYPE_TABLES = {
1: ['order_cash'], // 红包
2: ['order_digit'], // 直充卡密
3: ['order_voucher', 'goods_voucher_batch', 'goods_voucher_subject_config'] // 立减金
};
/**
* 获取订单类型对应的可用子表
* @param {string} datasource - 数据源标识
* @param {number} orderType - 订单类型
* @returns {string[]} 可用子表列表
*/
const getAvailableSubTables = (datasource, orderType) => {
if (datasource === 'ymt') {
return YMT_TYPE_TABLES[orderType] || Object.values(YMT_TYPE_TABLES).flat();
}
return MARKETING_TYPE_TABLES[orderType] || Object.values(MARKETING_TYPE_TABLES).flat();
};
// 导出模块
window.AppConfig = {
CONSTANTS,
DATASOURCE_CONFIG,
DEFAULT_TABLE_LABELS,
VISIBILITY_OPTIONS,
FORMAT_OPTIONS,
DEFAULT_FIELDS,
// 数据源相关
getDatasourceOptions,
getDatasourceLabel,
getMainTable,
getOrderTypeOptions,
getDefaultOrderType,
getOrderTypeLabel,
// 表相关
getTableRelations,
getAvailableSubTables,
// 场景相关
getSceneOptions,
getSceneLabel,
// 默认值
getDefaultFields
};

462
web/modules/fields.js Normal file
View File

@ -0,0 +1,462 @@
/**
* 字段处理模块 - 字段路径转换树结构构建
* @module fields
*/
// 依赖AppConfig
/**
* 字段管理器类
* 管理字段元数据树结构构建和路径转换
*/
class FieldsManager {
constructor() {
// 字段映射:{ tableName: [{ value, label }] }
this.fieldsMap = {};
// 表标签映射:{ tableName: label }
this.tableLabels = {};
// 推荐字段列表
this.recommendedFields = [];
}
/**
* 更新字段元数据
* @param {Array} tables - 表定义数组
* @param {Array} recommended - 推荐字段数组
*/
updateMetadata(tables, recommended = []) {
const map = {};
const labels = {};
tables.forEach(table => {
const fields = Array.isArray(table.fields) ? table.fields : [];
const visibleFields = fields.filter(f => !f.hidden);
map[table.table] = visibleFields.map(f => ({
value: f.field,
label: f.label
}));
if (table.label) {
labels[table.table] = table.label;
}
});
this.fieldsMap = map;
this.tableLabels = labels;
this.recommendedFields = recommended;
}
/**
* 获取表标签
* @param {string} table - 表名
* @returns {string} 表标签
*/
getTableLabel(table) {
return this.tableLabels[table]
|| window.AppConfig?.DEFAULT_TABLE_LABELS[table]
|| table;
}
/**
* 获取指定表的字段列表
* @param {string} table - 表名
* @returns {Array<{value: string, label: string}>} 字段列表
*/
getTableFields(table) {
return this.fieldsMap[table] || [];
}
/**
* 检查表中是否存在某字段
* @param {string} table - 表名
* @param {string} field - 字段名
* @returns {boolean} 是否存在
*/
hasField(table, field) {
const fields = this.fieldsMap[table] || [];
return fields.some(f => f.value === field);
}
/**
* 获取所有表名
* @returns {string[]} 表名列表
*/
getAllTables() {
return Object.keys(this.fieldsMap);
}
/**
* 构建树节点
* @param {string} table - 表名
* @param {Array} children - 子节点
* @returns {Object} 树节点
*/
buildTreeNode(table, children = []) {
return {
value: table,
label: this.getTableLabel(table),
children
};
}
/**
* 构建易码通订单子节点
* @param {number} orderType - 订单类型
* @returns {Array} 子节点数组
*/
buildYmtOrderChildren(orderType) {
const orderFields = this.getTableFields('order');
const children = orderFields.map(f => ({ value: f.value, label: f.label }));
// 添加公共子表
children.push(this.buildTreeNode('merchant', this.getTableFields('merchant')));
children.push(this.buildTreeNode('activity', this.getTableFields('activity')));
// 根据订单类型添加特定子表
if (orderType === 2) {
// 直充卡密
children.push(this.buildTreeNode('order_digit', this.getTableFields('order_digit')));
} else if (orderType === 3) {
// 立减金
children.push(this.buildTreeNode('order_voucher', this.getTableFields('order_voucher')));
children.push(this.buildTreeNode('goods_voucher_batch', this.getTableFields('goods_voucher_batch')));
children.push(this.buildTreeNode('goods_voucher_subject_config', this.getTableFields('goods_voucher_subject_config')));
} else if (orderType === 1) {
// 红包
children.push(this.buildTreeNode('order_cash', this.getTableFields('order_cash')));
} else {
// 全部类型
children.push(this.buildTreeNode('order_voucher', this.getTableFields('order_voucher')));
children.push(this.buildTreeNode('order_cash', this.getTableFields('order_cash')));
children.push(this.buildTreeNode('order_digit', this.getTableFields('order_digit')));
children.push(this.buildTreeNode('goods_voucher_batch', this.getTableFields('goods_voucher_batch')));
children.push(this.buildTreeNode('goods_voucher_subject_config', this.getTableFields('goods_voucher_subject_config')));
}
return children;
}
/**
* 构建营销系统订单子节点
* @param {number} orderType - 订单类型
* @returns {Array} 子节点数组
*/
buildMarketingOrderChildren(orderType) {
const orderFields = this.getTableFields('order');
const children = orderFields.map(f => ({ value: f.value, label: f.label }));
// 订单详情
children.push(this.buildTreeNode('order_detail', this.getTableFields('order_detail')));
// 构建计划节点(包含 key_batch -> code_batch 嵌套)
const planChildren = [
...this.getTableFields('plan').map(f => ({ value: f.value, label: f.label })),
this.buildTreeNode('key_batch', [
...this.getTableFields('key_batch').map(f => ({ value: f.value, label: f.label })),
this.buildTreeNode('code_batch', this.getTableFields('code_batch'))
])
];
// 构建立减金节点(包含 voucher -> voucher_batch 嵌套)
const voucherChildren = [
...this.getTableFields('order_voucher').map(f => ({ value: f.value, label: f.label })),
this.buildTreeNode('voucher', [
...this.getTableFields('voucher').map(f => ({ value: f.value, label: f.label })),
this.buildTreeNode('voucher_batch', this.getTableFields('voucher_batch'))
])
];
// 根据订单类型添加特定子表
if (orderType === 1) {
// 直充卡密
children.push(this.buildTreeNode('plan', planChildren));
children.push(this.buildTreeNode('merchant_key_send', this.getTableFields('merchant_key_send')));
} else if (orderType === 2) {
// 立减金
children.push(this.buildTreeNode('order_voucher', voucherChildren));
children.push(this.buildTreeNode('plan', planChildren));
} else if (orderType === 3) {
// 红包
children.push(this.buildTreeNode('order_cash', this.getTableFields('order_cash')));
children.push(this.buildTreeNode('plan', planChildren));
} else {
// 全部类型
children.push(this.buildTreeNode('order_cash', this.getTableFields('order_cash')));
children.push(this.buildTreeNode('order_voucher', voucherChildren));
children.push(this.buildTreeNode('plan', planChildren));
children.push(this.buildTreeNode('merchant_key_send', this.getTableFields('merchant_key_send')));
}
return children;
}
/**
* 构建字段选择树数据
* @param {string} datasource - 数据源
* @param {number} orderType - 订单类型
* @param {string} [mainTable] - 主表名可选用于编辑模式
* @returns {Array} 树数据
*/
buildFieldTree(datasource, orderType, mainTable) {
const type = Number(orderType || 0);
if (datasource === 'ymt') {
const actualMainTable = (mainTable === 'order_info') ? 'order' : (mainTable || 'order');
const orderNode = this.buildTreeNode(actualMainTable, this.buildYmtOrderChildren(type));
return type ? [orderNode] : [{ value: 'scene_order', label: '订单数据', children: [orderNode] }];
} else {
const tableForTree = mainTable || 'order';
const orderNode = this.buildTreeNode(tableForTree, this.buildMarketingOrderChildren(type));
return type ? [orderNode] : [{ value: 'scene_order', label: '订单数据', children: [orderNode] }];
}
}
/**
* table.field 格式转换为路径数组格式
* @param {string[]} fields - 字段数组格式为 ['table.field', ...]
* @param {string} datasource - 数据源
* @param {string} mainTable - 主表名
* @returns {Array<string[]>} 路径数组格式为 [['table', 'field'], ...]
*/
convertFieldsToPaths(fields, datasource, mainTable) {
const actualMainTable = (datasource === 'ymt' && mainTable === 'order_info') ? 'order' : mainTable;
const relations = window.AppConfig?.getTableRelations(datasource) || {};
const toPath = (tableField) => {
const parts = String(tableField || '').split('.');
if (parts.length !== 2) return null;
let table = parts[0];
const field = parts[1];
// ymt 数据源 order_info 映射为 order
if (datasource === 'ymt' && table === 'order_info') {
table = 'order';
}
// 查找表的路径关系
const relation = relations[table];
if (!relation) {
// 未知表,尝试直接返回
return [actualMainTable, table, field];
}
const pathToTable = relation.path || [];
if (pathToTable.length === 0) {
// 主表字段
return [actualMainTable, field];
} else {
// 子表字段
return [actualMainTable, ...pathToTable, field];
}
};
return fields
.map(toPath)
.filter(path => Array.isArray(path) && path.length >= 2)
.filter(path => {
// 验证字段存在
const table = path[path.length - 2];
const field = path[path.length - 1];
return this.hasField(table, field);
});
}
/**
* 将路径数组格式转换为 table.field 格式
* @param {Array<string[]>} paths - 路径数组
* @param {string} datasource - 数据源
* @param {string} mainTable - 主表名
* @returns {string[]} 字段数组格式为 ['table.field', ...]
*/
convertPathsToFields(paths, datasource, mainTable) {
const actualMainTable = (datasource === 'ymt' && mainTable === 'order_info') ? 'order' : mainTable;
const allTables = this.getAllTables();
// 检查是否只选中了主表节点(表示选择整表)
const hasMainTableOnly = paths.some(
p => Array.isArray(p) && p.length === 1 && p[0] === actualMainTable
);
if (hasMainTableOnly) {
// 返回主表所有字段
return this.getTableFields(actualMainTable).map(f => `${mainTable}.${f.value}`);
}
return paths.flatMap(path => {
if (!Array.isArray(path)) return [];
// 检查是否是组节点(表节点而非字段节点)
const lastPart = path[path.length - 1];
if (allTables.includes(lastPart)) {
// 是表节点,跳过
return [];
}
if (path.length >= 2) {
const table = path[path.length - 2];
const field = path[path.length - 1];
// ymt 数据源需要将 order 转回 order_info
const finalTable = (datasource === 'ymt' && table === 'order') ? 'order_info' : table;
return [`${finalTable}.${field}`];
}
return [];
});
}
/**
* 路径数组去重
* @param {Array<string[]>} paths - 路径数组
* @returns {Array<string[]>} 去重后的路径数组
*/
deduplicatePaths(paths = []) {
const seen = new Set();
const result = [];
for (const path of paths) {
if (!Array.isArray(path)) continue;
const key = path.join('|');
if (seen.has(key)) continue;
seen.add(key);
result.push(path);
}
return result;
}
/**
* 获取主表所有叶子字段的路径
* @param {string} datasource - 数据源
* @returns {Array<string[]>} 路径数组
*/
getMainTableLeafPaths(datasource) {
const mainTable = datasource === 'ymt' ? 'order' : 'order';
return this.getTableFields(mainTable).map(f => [mainTable, f.value]);
}
}
/**
* 树节点工具函数
*/
const TreeUtils = {
/**
* 在树中查找节点
* @param {Array} nodes - 树节点数组
* @param {string} targetKey - 目标节点 key
* @returns {Object|null} 找到的节点
*/
findNode(nodes, targetKey) {
for (const node of nodes) {
if (node.value === targetKey) {
return node;
}
if (node.children) {
const found = TreeUtils.findNode(node.children, targetKey);
if (found) return found;
}
}
return null;
},
/**
* 查找节点的完整路径
* @param {Array} nodes - 树节点数组
* @param {string} targetKey - 目标节点 key
* @param {string[]} currentPath - 当前路径
* @returns {string[]|null} 节点路径
*/
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 = TreeUtils.findNodePath(node.children, targetKey, newPath);
if (found) return found;
}
}
return null;
},
/**
* 根据路径数组查找节点 value
* @param {Array} nodes - 树节点数组
* @param {string[]} path - 路径数组
* @param {number} index - 当前索引
* @returns {string|null} 节点 value
*/
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 = TreeUtils.findNodeByPath(node.children, path, index + 1);
if (found) return found;
}
}
}
return null;
},
/**
* 判断节点是否为叶子节点
* @param {Object} node - 节点对象
* @returns {boolean} 是否为叶子节点
*/
isLeafNode(node) {
return !node.children || node.children.length === 0;
},
/**
* 将选中的 keys 转换为路径数组
* @param {string[]} checkedKeys - 选中的节点 keys
* @param {Array} treeData - 树数据
* @returns {Array<string[]>} 路径数组仅叶子节点
*/
checkedKeysToLeafPaths(checkedKeys, treeData) {
return checkedKeys
.filter(key => {
const node = TreeUtils.findNode(treeData, key);
return node && TreeUtils.isLeafNode(node);
})
.map(key => {
const path = TreeUtils.findNodePath(treeData, key);
return path || [key];
})
.filter(path => path.length >= 2);
},
/**
* 将路径数组转换为节点 keys
* @param {Array<string[]>} paths - 路径数组
* @param {Array} treeData - 树数据
* @returns {string[]} 节点 keys
*/
pathsToNodeKeys(paths, treeData) {
return paths.map(path => {
if (Array.isArray(path)) {
const nodeValue = TreeUtils.findNodeByPath(treeData, path);
if (nodeValue) return nodeValue;
// 找不到时使用路径最后一部分
return path[path.length - 1];
} else if (typeof path === 'string') {
return path;
}
return null;
}).filter(Boolean);
}
};
// 创建全局实例
const fieldsManager = new FieldsManager();
// 导出模块
window.FieldsModule = {
FieldsManager,
TreeUtils,
fieldsManager
};

263
web/modules/state.js Normal file
View File

@ -0,0 +1,263 @@
/**
* 状态管理模块 - 拆分状态对象提供初始化函数
* @module state
*/
/**
* 创建模板表单初始状态
* @param {string} [datasource='marketing'] - 数据源
* @returns {Object} 模板表单状态
*/
const createTemplateFormState = (datasource = 'marketing') => ({
name: '',
datasource: datasource,
main_table: datasource === 'ymt' ? 'order_info' : 'order',
orderType: datasource === 'ymt' ? 2 : 1,
fieldsRaw: '',
fieldsSel: [],
file_format: 'xlsx',
visibility: 'private'
});
/**
* 创建编辑表单初始状态
* @returns {Object} 编辑表单状态
*/
const createEditFormState = () => ({
id: null,
name: '',
datasource: 'marketing',
main_table: 'order',
orderType: 1,
fieldsSel: [],
visibility: 'private',
file_format: 'xlsx'
});
/**
* 创建导出表单初始状态
* @returns {Object} 导出表单状态
*/
const createExportFormState = () => ({
tplId: null,
datasource: 'marketing',
file_format: 'xlsx',
dateRange: [],
// 营销系统筛选条件
creatorIds: [],
creatorIdsRaw: '',
resellerId: null,
planId: null,
keyBatchId: null,
codeBatchId: null,
voucherChannelActivityId: '',
// 易码通筛选条件
ymtCreatorId: '',
ymtMerchantId: '',
ymtActivityId: ''
});
/**
* 创建导出模板状态
* @returns {Object} 导出模板状态
*/
const createExportTemplateState = () => ({
id: null,
filters: {},
main_table: '',
fields: [],
datasource: '',
file_format: ''
});
/**
* 创建任务列表状态
* @returns {Object} 任务列表状态
*/
const createJobsState = () => ({
jobs: [],
jobsVisible: false,
jobsTplId: null,
jobsPage: 1,
jobsPageSize: 10,
jobsTotal: 0
});
/**
* 创建SQL预览状态
* @returns {Object} SQL预览状态
*/
const createSqlPreviewState = () => ({
sqlVisible: false,
sqlText: '',
sqlExplainDesc: ''
});
/**
* 创建对话框状态
* @returns {Object} 对话框状态
*/
const createDialogState = () => {
const { CONSTANTS } = window.AppConfig || {};
const defaultWidth = CONSTANTS?.DIALOG_DEFAULT_WIDTH || 900;
const editDefaultWidth = CONSTANTS?.DIALOG_EDIT_DEFAULT_WIDTH || 900;
return {
createVisible: false,
editVisible: false,
exportVisible: false,
exportSubmitting: false,
createWidth: localStorage.getItem('tplDialogWidth') || (defaultWidth + 'px'),
editWidth: localStorage.getItem('tplEditDialogWidth') || (editDefaultWidth + 'px')
};
};
/**
* 创建完整的应用状态
* @returns {Object} 完整应用状态
*/
const createAppState = () => {
return {
// 模板列表
templates: [],
// 当前任务详情
job: {},
// 模板表单
form: createTemplateFormState(),
// 编辑表单
edit: createEditFormState(),
// 导出表单
exportForm: createExportFormState(),
// 导出模板详情
exportTpl: createExportTemplateState(),
// 任务列表
...createJobsState(),
// SQL 预览
...createSqlPreviewState(),
// 对话框
...createDialogState()
};
};
/**
* 重置模板表单
* @param {Object} form - 表单对象
* @param {string} [datasource='marketing'] - 数据源
*/
const resetTemplateForm = (form, datasource = 'marketing') => {
const initial = createTemplateFormState(datasource);
Object.assign(form, initial);
};
/**
* 重置编辑表单
* @param {Object} edit - 编辑表单对象
*/
const resetEditForm = (edit) => {
const initial = createEditFormState();
Object.assign(edit, initial);
};
/**
* 重置导出表单
* @param {Object} exportForm - 导出表单对象
*/
const resetExportForm = (exportForm) => {
const initial = createExportFormState();
Object.assign(exportForm, initial);
};
/**
* 表单验证规则工厂
*/
const ValidationRules = {
/**
* 创建模板表单验证规则
* @returns {Object} 验证规则
*/
createTemplateRules() {
return {
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' }]
};
},
/**
* 创建编辑表单验证规则
* @returns {Object} 验证规则
*/
createEditRules() {
return {
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'
}]
};
},
/**
* 创建导出表单验证规则
* @returns {Object} 验证规则
*/
createExportRules() {
return {
tplId: [{ required: true, message: '请选择模板', trigger: 'change' }],
dateRange: [{
validator: (_rule, val, cb) => {
if (Array.isArray(val) && val.length === 2) {
cb();
} else {
cb(new Error('请选择时间范围'));
}
},
trigger: 'change'
}]
};
}
};
// 导出模块
window.StateModule = {
createTemplateFormState,
createEditFormState,
createExportFormState,
createExportTemplateState,
createJobsState,
createSqlPreviewState,
createDialogState,
createAppState,
resetTemplateForm,
resetEditForm,
resetExportForm,
ValidationRules
};

187
web/modules/utils.js Normal file
View File

@ -0,0 +1,187 @@
/**
* 工具函数模块 - 通用工具函数
* @module utils
*/
/**
* 显示消息提示
* @param {string} text - 消息内容
* @param {'success'|'error'|'warning'|'info'} [type='success'] - 消息类型
*/
const showMessage = (text, type = 'success') => {
if (window.ElementPlus?.ElMessage) {
window.ElementPlus.ElMessage({ message: text, type });
} else {
console.log(`[${type}] ${text}`);
}
};
/**
* 格式化日期时间
* @param {Date} date - 日期对象
* @returns {string} 格式化后的日期字符串 YYYY-MM-DD HH:mm:ss
*/
const formatDateTime = (date) => {
const pad = (n) => String(n).padStart(2, '0');
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
};
/**
* 获取月份范围
* @param {number} [offsetMonths=-1] - 月份偏移量负数表示往前推
* @returns {[string, string]} [开始时间, 结束时间]
*/
const getMonthRange = (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 [formatDateTime(start), formatDateTime(end)];
};
/**
* 计算对话框宽度限制在最小和最大值之间
* @param {number} width - 目标宽度
* @param {number} [minWidth=500] - 最小宽度
* @param {number} [maxWidth=1400] - 最大宽度
* @returns {string} 宽度字符串 '900px'
*/
const clampDialogWidth = (width, minWidth = 500, maxWidth = 1400) => {
const clamped = Math.max(minWidth, Math.min(maxWidth, width));
return clamped + 'px';
};
/**
* localStorage 获取对话框宽度
* @param {string} key - 存储键名
* @param {number} defaultWidth - 默认宽度
* @returns {string} 宽度字符串
*/
const getStoredDialogWidth = (key, defaultWidth) => {
const stored = localStorage.getItem(key);
return stored || (defaultWidth + 'px');
};
/**
* 保存对话框宽度到 localStorage
* @param {string} key - 存储键名
* @param {string} width - 宽度字符串
*/
const saveDialogWidth = (key, width) => {
localStorage.setItem(key, width);
};
/**
* 计算任务进度百分比
* @param {Object} job - 任务对象
* @returns {string} 进度描述
*/
const calculateJobProgress = (job) => {
const estimate = Number(job.row_estimate || 0);
const done = Number(job.total_rows || 0);
if (job.status === 'completed') return '100%';
if (job.status === 'failed') return '失败';
if (job.status === 'canceled') return '已取消';
if (job.status === 'queued') return '0%';
if (job.status === 'running') {
const effectiveEstimate = estimate > 0 ? estimate : (done > 0 ? done * 2 : 0);
if (effectiveEstimate > 0) {
const percent = Math.max(0, Math.min(100, Math.floor(done * 100 / effectiveEstimate)));
return percent + '%';
}
if (done > 0) return `已写${done.toLocaleString()}`;
return '评估中';
}
const effectiveEstimate = estimate > 0 ? estimate : (done > 0 ? done * 2 : 0);
if (effectiveEstimate > 0) {
const percent = Math.max(0, Math.min(100, Math.floor(done * 100 / effectiveEstimate)));
return percent + '%';
}
return '评估中';
};
/**
* 解析宽度字符串为数字
* @param {string} widthStr - 宽度字符串 '900px'
* @param {number} defaultValue - 默认值
* @returns {number} 宽度数值
*/
const parseWidth = (widthStr, defaultValue) => {
return parseInt(String(widthStr).replace('px', '') || String(defaultValue), 10);
};
/**
* 深度克隆对象
* @param {Object} obj - 要克隆的对象
* @returns {Object} 克隆后的对象
*/
const deepClone = (obj) => {
return JSON.parse(JSON.stringify(obj));
};
/**
* 安全地获取嵌套属性
* @param {Object} obj - 对象
* @param {string} path - 属性路径 'a.b.c'
* @param {*} defaultValue - 默认值
* @returns {*} 属性值或默认值
*/
const getNestedValue = (obj, path, defaultValue = undefined) => {
const keys = path.split('.');
let result = obj;
for (const key of keys) {
if (result == null) return defaultValue;
result = result[key];
}
return result !== undefined ? result : defaultValue;
};
/**
* 防抖函数
* @param {Function} fn - 要防抖的函数
* @param {number} delay - 延迟时间毫秒
* @returns {Function} 防抖后的函数
*/
const debounce = (fn, delay) => {
let timer = null;
return function (...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
};
/**
* 节流函数
* @param {Function} fn - 要节流的函数
* @param {number} limit - 限制时间毫秒
* @returns {Function} 节流后的函数
*/
const throttle = (fn, limit) => {
let inThrottle = false;
return function (...args) {
if (!inThrottle) {
fn.apply(this, args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
};
// 导出模块
window.AppUtils = {
showMessage,
formatDateTime,
getMonthRange,
clampDialogWidth,
getStoredDialogWidth,
saveDialogWidth,
calculateJobProgress,
parseWidth,
deepClone,
getNestedValue,
debounce,
throttle
};