915 lines
28 KiB
JavaScript
915 lines
28 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 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');
|
|
|
|
})();
|