MarketingSystemDataExportTool/web/main.js

1139 lines
37 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 营销系统数据导出工具 - 主入口
* @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(`[设置树勾选] 调用 setCheckedKeyskeys数量: ${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');
})();