refactor(web): 重构脚本引入为模块化架构
- 移除对vue及element-plus脚本的defer属性,确保按顺序加载 - 拆分原有配置与功能脚本为多个模块文件 - 引入config、utils、api、fields、state等模块化脚本 - 替换主入口脚本为重构后的 main.refactored.js - 优化脚本加载顺序,提高代码可维护性与扩展性
This commit is contained in:
parent
4f74eec055
commit
913f93fabd
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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
|
||||
};
|
||||
Loading…
Reference in New Issue