1139 lines
37 KiB
JavaScript
1139 lines
37 KiB
JavaScript
/**
|
||
* 营销系统数据导出工具 - 主入口
|
||
* @description 使用模块化架构重构,提升可读性和扩展性
|
||
*/
|
||
|
||
;(function() {
|
||
'use strict';
|
||
|
||
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 hasOnlyUserId = Vue.computed(() => Api.hasOnlyUserId());
|
||
// URL 中是否带有 mobile,用于营销系统订单数据创建者自动选择 + 禁用
|
||
const hasMobile = Vue.computed(() => !!Api.getMobile && !!Api.getMobile());
|
||
const currentUserId = Vue.computed(() => {
|
||
const userId = Api.getUserId();
|
||
return userId ? Number(userId) : null;
|
||
});
|
||
|
||
// ==================== 字段元数据管理 ====================
|
||
const metaTableLabels = Vue.ref({});
|
||
const recommendedMeta = Vue.ref([]);
|
||
const metadataVersion = Vue.ref(0); // 用于触发树重新计算的响应式变量
|
||
|
||
/**
|
||
* 加载字段元数据
|
||
* @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;
|
||
metadataVersion.value++; // 触发树重新计算
|
||
return recommended;
|
||
} catch (error) {
|
||
console.error('加载字段元数据失败:', error);
|
||
fieldsManager.updateMetadata([], []);
|
||
metaTableLabels.value = {};
|
||
recommendedMeta.value = [];
|
||
metadataVersion.value++; // 即使失败也触发更新
|
||
return [];
|
||
}
|
||
};
|
||
|
||
// ==================== 树形选择器数据 ====================
|
||
const fieldTreeData = Vue.computed(() => {
|
||
// 引用 metadataVersion 以确保元数据更新时重新计算
|
||
const _version = metadataVersion.value;
|
||
return fieldsManager.buildFieldTree(
|
||
state.form.datasource,
|
||
state.form.orderType
|
||
);
|
||
});
|
||
|
||
const editFieldTreeData = Vue.computed(() => {
|
||
// 引用 metadataVersion 以确保元数据更新时重新计算
|
||
const _version = metadataVersion.value;
|
||
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)) {
|
||
console.log(`[设置树勾选] 跳过: kind=${kind}, tree存在=${!!tree}, values类型=${Array.isArray(values) ? 'array' : typeof values}`);
|
||
return;
|
||
}
|
||
|
||
const treeData = kind === 'create' ? fieldTreeData.value : editFieldTreeData.value;
|
||
console.log(`[设置树勾选] kind=${kind}, values数量=${values.length}, 树数据存在=${!!treeData}`);
|
||
|
||
const keys = TreeUtils.pathsToNodeKeys(values, treeData);
|
||
console.log(`[设置树勾选] 转换后keys数量=${keys.length}`);
|
||
|
||
Vue.nextTick(() => {
|
||
setTimeout(() => {
|
||
try {
|
||
if (keys.length > 0 && tree) {
|
||
console.log(`[设置树勾选] 调用 setCheckedKeys,keys数量: ${keys.length}`);
|
||
tree.setCheckedKeys(keys);
|
||
console.log(`[设置树勾选] setCheckedKeys 调用成功`);
|
||
} else {
|
||
console.warn(`[设置树勾选] 未调用 setCheckedKeys: keys.length=${keys.length}, tree存在=${!!tree}`);
|
||
}
|
||
} 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();
|
||
|
||
// ==================== 时间快捷选项 ====================
|
||
// Element Plus datetimerange 默认时间:开始 00:00:00,结束 23:59:59(仅用于 UI 默认显示)
|
||
const dateDefaultTime = [
|
||
new Date(2000, 0, 1, 0, 0, 0),
|
||
new Date(2000, 0, 1, 23, 59, 59),
|
||
];
|
||
|
||
/**
|
||
* 禁用超过一年的日期
|
||
* @param {Date} date - 要检查的日期
|
||
* @returns {boolean} 是否禁用
|
||
*/
|
||
const disabledDate = (date) => {
|
||
if (!date) return false;
|
||
|
||
// 如果已经选择了开始时间,计算结束时间限制
|
||
if (state.exportForm.dateRange && state.exportForm.dateRange[0]) {
|
||
const startTime = new Date(state.exportForm.dateRange[0]);
|
||
const oneYearLater = new Date(startTime);
|
||
oneYearLater.setFullYear(startTime.getFullYear() + 1);
|
||
oneYearLater.setDate(oneYearLater.getDate() - 1); // 减去一天以确保不超过一年
|
||
|
||
// 只限制结束时间不能超过开始时间一年后
|
||
if (date > oneYearLater) return true;
|
||
}
|
||
|
||
// 如果已经选择了结束时间,计算开始时间限制
|
||
if (state.exportForm.dateRange && state.exportForm.dateRange[1]) {
|
||
const endTime = new Date(state.exportForm.dateRange[1]);
|
||
const oneYearEarlier = new Date(endTime);
|
||
oneYearEarlier.setFullYear(endTime.getFullYear() - 1);
|
||
oneYearEarlier.setDate(oneYearEarlier.getDate() + 1); // 加上一天
|
||
|
||
// 只限制开始时间不能早于结束时间一年前
|
||
if (date < oneYearEarlier) return true;
|
||
}
|
||
|
||
return false;
|
||
};
|
||
|
||
const dateShortcuts = [
|
||
{
|
||
text: '本日',
|
||
value: () => {
|
||
const start = new Date();
|
||
start.setHours(0, 0, 0, 0);
|
||
const end = new Date();
|
||
end.setHours(23, 59, 59, 999);
|
||
return [formatDateTime(start), formatDateTime(end)];
|
||
},
|
||
},
|
||
{
|
||
text: '本周',
|
||
value: () => {
|
||
const now = new Date();
|
||
const day = now.getDay() || 7; // 周一为一周开始
|
||
const start = new Date(now);
|
||
start.setDate(start.getDate() - day + 1);
|
||
start.setHours(0, 0, 0, 0);
|
||
const end = new Date();
|
||
end.setHours(23, 59, 59, 999);
|
||
return [formatDateTime(start), formatDateTime(end)];
|
||
},
|
||
},
|
||
{
|
||
text: '本月',
|
||
value: () => {
|
||
const now = new Date();
|
||
const start = new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0, 0);
|
||
const end = new Date(now);
|
||
end.setHours(23, 59, 59, 999);
|
||
return [formatDateTime(start), formatDateTime(end)];
|
||
},
|
||
},
|
||
{
|
||
text: '上个月',
|
||
value: () => {
|
||
const now = new Date();
|
||
const start = new Date(now.getFullYear(), now.getMonth() - 1, 1, 0, 0, 0, 0);
|
||
const end = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59, 999);
|
||
return [formatDateTime(start), formatDateTime(end)];
|
||
},
|
||
},
|
||
{
|
||
text: '前3月',
|
||
value: () => {
|
||
const now = new Date();
|
||
const end = new Date();
|
||
end.setHours(23, 59, 59, 999);
|
||
const start = new Date(now.getFullYear(), now.getMonth() - 3, now.getDate(), 0, 0, 0, 0);
|
||
return [formatDateTime(start), formatDateTime(end)];
|
||
},
|
||
},
|
||
{
|
||
text: '今年',
|
||
value: () => {
|
||
const now = new Date();
|
||
const start = new Date(now.getFullYear(), 0, 1, 0, 0, 0, 0);
|
||
const end = new Date();
|
||
end.setHours(23, 59, 59, 999);
|
||
return [formatDateTime(start), formatDateTime(end)];
|
||
},
|
||
},
|
||
];
|
||
|
||
// ==================== 表单引用 ====================
|
||
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 { creatorId, creatorIds } = state.exportForm;
|
||
let ids = [];
|
||
|
||
if (creatorId) {
|
||
ids = [Number(creatorId)];
|
||
} else if (creatorIds?.length) {
|
||
ids = creatorIds.map(Number);
|
||
}
|
||
|
||
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(() => {
|
||
const { creatorId, creatorIds } = state.exportForm;
|
||
return !!(creatorId || (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 {
|
||
if (window.ElementPlus?.ElMessageBox) {
|
||
await window.ElementPlus.ElMessageBox.confirm(
|
||
'删除后不可恢复,确认要删除该导出模板吗?',
|
||
'删除确认',
|
||
{
|
||
type: 'warning',
|
||
confirmButtonText: '确认删除',
|
||
cancelButtonText: '再想想'
|
||
}
|
||
);
|
||
} else {
|
||
const ok = window.confirm('删除后不可恢复,确认要删除该导出模板吗?');
|
||
if (!ok) return;
|
||
}
|
||
} catch {
|
||
// 用户取消
|
||
return;
|
||
}
|
||
|
||
// 第二次:真正调用后端删除
|
||
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') {
|
||
await loadCreators();
|
||
// 只要有手机号就自动匹配选中创建者
|
||
// 是否允许编辑通过 userId 是否为空决定
|
||
const mobile = Api.getMobile ? Api.getMobile() : '';
|
||
const shouldAutoSelectByMobile = !!mobile;
|
||
console.log('[openExport] marketing mobile:', mobile, 'shouldAutoSelectByMobile:', shouldAutoSelectByMobile);
|
||
if (shouldAutoSelectByMobile) {
|
||
const normalized = String(mobile).trim();
|
||
// 在 creatorOptions 中根据 mobile 精确匹配
|
||
const match = creatorOptions.value.find(opt => String(opt.mobile || '').trim() === normalized);
|
||
if (match) {
|
||
console.log('[openExport] 根据手机号匹配到创建者:', match);
|
||
state.exportForm.creatorId = match.value;
|
||
} else {
|
||
console.warn('[openExport] 未根据手机号匹配到创建者, mobile =', normalized);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (state.exportForm.datasource === 'ymt') {
|
||
await loadYmtCreators();
|
||
// 仍按 userId 自动选择易码通用户(保持原逻辑)
|
||
const userId = Api.getUserId();
|
||
const shouldAutoSelectCreator = userId && hasOnlyUserId.value;
|
||
console.log('[openExport] ymt userId:', userId, 'hasOnlyUserId:', hasOnlyUserId.value, 'shouldAutoSelectCreator:', shouldAutoSelectCreator);
|
||
if (shouldAutoSelectCreator) {
|
||
const userIdNum = Number(userId);
|
||
console.log('[openExport] 设置 ymt ymtCreatorId:', userIdNum);
|
||
// 直接设置 userId,即使创建者列表中暂时没有该选项
|
||
state.exportForm.ymtCreatorId = userIdNum;
|
||
// 加载相关的商户和活动
|
||
await loadYmtMerchants();
|
||
await loadYmtActivities();
|
||
// 等待 Vue 更新
|
||
await Vue.nextTick();
|
||
const creatorExists = ymtCreatorOptions.value.some(opt => opt.value === userIdNum);
|
||
console.log('[openExport] ymtCreatorOptions:', ymtCreatorOptions.value.length, 'creatorExists:', creatorExists);
|
||
if (!creatorExists) {
|
||
console.log(`警告: 易码通用户列表中未找到 userId=${userIdNum},但已自动设置`);
|
||
}
|
||
} else {
|
||
await loadYmtMerchants();
|
||
await loadYmtActivities();
|
||
}
|
||
}
|
||
|
||
if (!state.exportForm.dateRange?.length) {
|
||
state.exportForm.dateRange = getMonthRange(-1);
|
||
}
|
||
|
||
state.exportVisible = true;
|
||
|
||
// 对话框打开后,无需再根据 userId 处理营销系统创建者,手机号逻辑已在上方完成
|
||
await Vue.nextTick();
|
||
};
|
||
|
||
/**
|
||
* 提交导出任务
|
||
*/
|
||
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, creatorId, creatorIds, creatorIdsRaw, ymtCreatorId, ymtMerchantId, ymtActivityId } = state.exportForm;
|
||
|
||
const filters = {};
|
||
const typeValue = exportType.value;
|
||
if (typeValue != null) {
|
||
filters.type_eq = Number(typeValue);
|
||
}
|
||
|
||
const normalizeMarketingEndTime = (endStr) => {
|
||
const s = String(endStr || '').trim();
|
||
if (!s) return s;
|
||
// value-format is 'YYYY-MM-DD HH:mm:ss' so we just normalize "00:00:00" to end-of-day for marketing exports
|
||
if (s.length >= 19 && s.slice(11, 19) === '00:00:00') {
|
||
return s.slice(0, 10) + ' 23:59:59';
|
||
}
|
||
return s;
|
||
};
|
||
|
||
if (dateRange?.length === 2) {
|
||
const start = String(dateRange[0] || '').trim();
|
||
let end = String(dateRange[1] || '').trim();
|
||
if (datasource === 'marketing') {
|
||
end = normalizeMarketingEndTime(end);
|
||
}
|
||
filters.create_time_between = [start, end];
|
||
}
|
||
|
||
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 (creatorId) {
|
||
filters.creator_in = [Number(creatorId)];
|
||
} else 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.creatorId, () => {
|
||
state.exportForm.resellerId = null;
|
||
state.exportForm.planId = null;
|
||
state.exportForm.keyBatchId = null;
|
||
state.exportForm.codeBatchId = null;
|
||
loadResellers();
|
||
});
|
||
|
||
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.dateRange, (newVal) => {
|
||
// 触发日期选择器重新计算禁用状态
|
||
if (exportFormRef.value && exportFormRef.value.$el) {
|
||
// 通过强制更新组件状态来重新计算禁用日期
|
||
exportFormRef.value.$forceUpdate();
|
||
}
|
||
}, { deep: true });
|
||
|
||
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,
|
||
dateDefaultTime,
|
||
dateShortcuts,
|
||
disabledDate,
|
||
// 表单引用
|
||
createFormRef,
|
||
editFormRef,
|
||
exportFormRef,
|
||
// 树形选择器
|
||
createFieldsTree,
|
||
editFieldsTree,
|
||
fieldTreeData,
|
||
editFieldTreeData,
|
||
onTreeCheck,
|
||
setTreeChecked,
|
||
// 工具函数
|
||
dsLabel,
|
||
orderTypeOptionsFor,
|
||
fmtDT: formatDateTime,
|
||
// 计算属性
|
||
exportType,
|
||
isOrder,
|
||
exportTitle,
|
||
hasUserId,
|
||
hasOnlyUserId,
|
||
hasMobile,
|
||
currentUserId,
|
||
hasCreators,
|
||
hasReseller,
|
||
hasPlan,
|
||
hasKeyBatch,
|
||
hasCodeBatch,
|
||
// 选项数据
|
||
creatorOptions,
|
||
ymtCreatorOptions,
|
||
ymtMerchantOptions,
|
||
ymtActivityOptions,
|
||
resellerOptions,
|
||
planOptions
|
||
};
|
||
}
|
||
});
|
||
|
||
app.use(ElementPlus);
|
||
app.mount('#app');
|
||
|
||
})();
|