#feature: update SQL generator

This commit is contained in:
2026-05-19 14:57:11 +08:00
parent 53bca7d609
commit 3c628eb391
10 changed files with 1043 additions and 165 deletions
+29 -4
View File
@@ -3,12 +3,12 @@
<!-- 页面标题 -->
<div class="mb-6">
<h1 class="text-2xl font-bold text-gray-900">生成周报</h1>
<p class="text-gray-600 mt-2">生成上周的工作周报</p>
<p class="text-gray-600 mt-2">按周选择统计范围生成对应周期的工作周报</p>
</div>
<!-- 周报生成区域 -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h2 class="text-xl font-semibold text-gray-700 mb-4">生成上周周报</h2>
<h2 class="text-xl font-semibold text-gray-700 mb-4">生成{{ selectedPeriodLabel }}周报</h2>
<div class="flex flex-wrap gap-4 mb-4">
<div class="flex-1 min-w-64">
@@ -20,6 +20,17 @@
placeholder="输入 JIRA 用户名"
>
</div>
<div class="w-40">
<label class="block text-sm font-medium text-gray-700 mb-2">统计周期</label>
<select
v-model="weeklyReport.period"
@change="resetWeeklyReportResult"
class="w-full px-3 py-2 border border-gray-300 rounded-md bg-white focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="last_week">上周</option>
<option value="this_week">本周</option>
</select>
</div>
<div class="flex items-end">
<button
@click="generateWeeklyReport"
@@ -73,6 +84,7 @@ export default {
return {
weeklyReport: {
username: '',
period: 'this_week',
loading: false,
result: '',
error: ''
@@ -80,11 +92,22 @@ export default {
}
},
computed: {
selectedPeriodLabel() {
return this.weeklyReport.period === 'this_week' ? '本周' : '上周';
}
},
async mounted() {
// 获取默认用户名
await this.loadDefaultUser();
},
methods: {
resetWeeklyReportResult() {
this.weeklyReport.result = '';
this.weeklyReport.error = '';
},
async loadDefaultUser() {
this.weeklyReport.username = resolveJiraDefaultQueryUser('');
@@ -118,7 +141,8 @@ export default {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
},
body: JSON.stringify({
username: this.weeklyReport.username
username: this.weeklyReport.username,
period: this.weeklyReport.period
})
});
@@ -163,7 +187,8 @@ export default {
}
const params = new URLSearchParams({
username: this.weeklyReport.username
username: this.weeklyReport.username,
period: this.weeklyReport.period
});
window.open(`/api/jira/weekly-report/download?${params}`, '_blank');
+227 -4
View File
@@ -33,7 +33,7 @@
<textarea
v-model="inputText"
rows="8"
placeholder="示例: X0X60F 17141\nC01008446046, 11894"
:placeholder="currentTool.placeholder"
class="w-full flex-1 min-h-[200px] px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm font-mono"
></textarea>
<div class="flex items-center justify-between mt-2">
@@ -63,7 +63,7 @@
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 flex flex-col min-h-0">
<div class="flex flex-col gap-4 h-full min-h-0">
<div>
<div v-if="showQuerySql">
<div class="flex items-center justify-between mb-2">
<h3 class="text-sm lg:text-base font-semibold text-gray-900">查询SQL</h3>
<button
@@ -83,7 +83,50 @@
></textarea>
</div>
<div class="flex flex-col flex-1 min-h-0">
<div v-if="showSplitOutput" class="flex flex-col flex-1 min-h-0">
<div class="flex items-center justify-between mb-2">
<h3 class="text-sm lg:text-base font-semibold text-gray-900">生成结果</h3>
<div v-if="stats.total" class="text-xs text-gray-500">
{{ stats.total }} 更新 {{ stats.update }}
</div>
</div>
<div class="grid grid-rows-3 gap-3 flex-1 min-h-0">
<div
v-for="section in splitOutputSections"
:key="section.key"
class="flex flex-col min-h-0 border border-gray-200 rounded-lg overflow-hidden bg-gray-50"
>
<div class="flex items-center justify-between px-3 py-2 border-b border-gray-200 bg-white">
<div class="text-sm font-semibold text-gray-900">{{ section.label }}</div>
<button
@click="copySplitOutput(section.key)"
:disabled="!splitOutputSql[section.key]"
class="px-3 py-1.5 text-xs text-gray-600 border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-60"
type="button"
>
复制{{ section.label }}
</button>
</div>
<textarea
v-model="splitOutputSql[section.key]"
readonly
class="w-full flex-1 min-h-[120px] px-3 py-2 border-0 text-sm font-mono bg-gray-50 resize-none focus:ring-0"
></textarea>
</div>
</div>
<div class="flex items-center justify-between mt-2">
<p class="text-xs text-gray-400">结果仅用于复制执行请确认无误后使用</p>
</div>
<div
v-if="copyStatus.message"
class="mt-2 text-xs"
:class="copyStatus.type === 'success' ? 'text-green-600' : 'text-red-600'"
>
{{ copyStatus.message }}
</div>
</div>
<div v-else class="flex flex-col flex-1 min-h-0">
<div class="flex items-center justify-between mb-2">
<h3 class="text-sm lg:text-base font-semibold text-gray-900">生成结果</h3>
<div v-if="stats.total" class="text-xs text-gray-500">
@@ -143,6 +186,11 @@ export default {
selectedTool: 'ob-external-id',
inputText: '',
outputSql: '',
splitOutputSql: {
sp: '',
ppCn: '',
ppUs: ''
},
querySql: '',
loading: false,
errors: [],
@@ -161,7 +209,14 @@ export default {
{
value: 'ob-external-id',
label: 'OB外部ID',
description: '每行输入 case_id 与 ob_id,系统会判断是否生成更新或插入 SQL。'
description: '每行输入 case_id 与 ob_id,系统会判断是否生成更新或插入 SQL。',
placeholder: '示例: X0X60F 17141\nC01008446046, 11894'
},
{
value: 'new-factory-return-redelivery',
label: '新区工厂退库重新出库',
description: '每行输入加工单、病例号、正确运单,只读取前三列;根据 CRM 加工单关联地址国家拆分 PP-CN / PP-US SQL。',
placeholder: '示例: M20260508006905 225KF9 SF123456789\nM20260509005949 C01005934247 UPS987654321'
}
]
}
@@ -169,12 +224,29 @@ export default {
computed: {
currentTool() {
return this.toolOptions.find((tool) => tool.value === this.selectedTool) || this.toolOptions[0];
},
showSplitOutput() {
return this.selectedTool === 'new-factory-return-redelivery';
},
showQuerySql() {
return !this.showSplitOutput;
},
splitOutputSections() {
return [
{ key: 'sp', label: 'SP' },
{ key: 'ppCn', label: 'PP-CN' },
{ key: 'ppUs', label: 'PP-US' }
];
}
},
methods: {
clearInput() {
this.inputText = '';
this.outputSql = '';
this.resetSplitOutputSql();
this.querySql = '';
this.errors = [];
this.warnings = [];
@@ -249,6 +321,7 @@ export default {
async generateSql() {
this.errors = [];
this.outputSql = '';
this.resetSplitOutputSql();
this.querySql = '';
this.warnings = [];
this.resetCopyStatus();
@@ -259,6 +332,11 @@ export default {
return;
}
if (this.selectedTool === 'new-factory-return-redelivery') {
await this.generateNewFactoryReturnRedeliverySql();
return;
}
this.errors = ['未识别的功能类型,请重新选择。'];
},
@@ -321,6 +399,113 @@ export default {
}
},
parseNewFactoryReturnRedeliveryInput() {
const lines = this.inputText.split(/\r?\n/);
const errors = [];
const rows = [];
lines.forEach((line, index) => {
const trimmed = line.trim();
if (!trimmed) {
return;
}
const parts = trimmed.split(/[\s,]+/).filter(Boolean);
if (parts.length < 3) {
errors.push(`${index + 1} 行格式不正确,请提供加工单、病例号、正确运单三列`);
return;
}
rows.push({
productionCode: parts[0],
caseCode: parts[1],
expressNo: parts[2],
lineNumber: index + 1
});
});
if (rows.length === 0 && errors.length === 0) {
errors.push('请输入至少一行加工单、病例号、正确运单。');
}
return {
rows,
errors
};
},
async generateNewFactoryReturnRedeliverySql() {
const { rows, errors } = this.parseNewFactoryReturnRedeliveryInput();
if (errors.length) {
this.errors = errors;
return;
}
this.loading = true;
try {
const productionCodes = [...new Set(rows.map((row) => row.productionCode))];
this.querySql = this.buildProductionCountriesSelect(productionCodes);
const response = await fetch('/api/sql-generator/production-countries/check', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
},
body: JSON.stringify({
production_codes: productionCodes
})
});
const data = await response.json();
if (!response.ok || !data.success) {
this.errors = [data.message || '查询 CRM 加工单国家失败,请稍后重试。'];
return;
}
const productionCountries = data.data.production_countries || {};
const missingRows = rows.filter((row) => !productionCountries[row.productionCode] || productionCountries[row.productionCode].length === 0);
if (missingRows.length) {
this.errors = missingRows.map((row) => `${row.lineNumber} 行加工单 ${row.productionCode} 未在 CRM 查询到地址国家`);
return;
}
const spSql = [];
const ppCnSql = [];
const ppUsSql = [];
rows.forEach((row) => {
const productionCode = this.escapeSqlValue(row.productionCode);
const expressNo = this.escapeSqlValue(row.expressNo);
const ppSql = this.isUsProductionCountry(productionCountries[row.productionCode]) ? ppUsSql : ppCnSql;
spSql.push(`update case_deliveries set express_no = '${expressNo}' where production_code = '${productionCode}';`);
ppSql.push(`update delivery_records set express_no = '${expressNo}' where production_code = '${productionCode}';`);
ppSql.push(`update receives set express_no = '${expressNo}' where production_code = '${productionCode}';`);
});
const sections = [
this.buildSqlSection('SP', spSql),
this.buildSqlSection('PP-CN', ppCnSql),
this.buildSqlSection('PP-US', ppUsSql)
].filter(Boolean);
this.stats.update = spSql.length + ppCnSql.length + ppUsSql.length;
this.stats.total = this.stats.update;
this.splitOutputSql = {
sp: spSql.join('\n'),
ppCn: ppCnSql.join('\n'),
ppUs: ppUsSql.join('\n')
};
this.outputSql = sections.join('\n\n');
} catch (error) {
this.errors = ['网络请求失败: ' + error.message];
} finally {
this.loading = false;
}
},
escapeSqlValue(value) {
return String(value).replace(/'/g, "''");
},
@@ -334,6 +519,32 @@ export default {
return `select * from case_extras where case_code in (${inValues})`;
},
buildProductionCountriesSelect(productionCodes) {
if (!productionCodes.length) {
return '';
}
const inValues = productionCodes.map((productionCode) => `'${this.escapeSqlValue(productionCode)}'`).join(', ');
return [
'select ep.name, epc.ea_case_id_c, epc.ea_businessorder_id_c, epc.ea_salesorder_id_c',
'from ea_production ep',
'join ea_production_cstm epc on ep.id = epc.id_c',
`where ep.deleted = 0 and ep.name in (${inValues})`
].join('\n');
},
isUsProductionCountry(countryCodes) {
return (countryCodes || []).some((countryCode) => ['US', 'GU', 'PR'].includes(String(countryCode).toUpperCase()));
},
buildSqlSection(title, sqlLines) {
if (!sqlLines.length) {
return '';
}
return [`-- ${title}`, ...sqlLines].join('\n');
},
buildDuplicateWarnings(duplicates) {
return duplicates.map((duplicate) => {
const lines = duplicate.lineNumbers.join('、');
@@ -348,6 +559,14 @@ export default {
};
},
resetSplitOutputSql() {
this.splitOutputSql = {
sp: '',
ppCn: '',
ppUs: ''
};
},
setCopyStatus(message, type) {
this.copyStatus = {
message,
@@ -433,6 +652,10 @@ export default {
await this.copyToClipboard(this.outputSql, '复制失败,请手动复制结果。', 'outputTextarea');
},
async copySplitOutput(key) {
await this.copyToClipboard(this.splitOutputSql[key], '复制失败,请手动复制结果。');
},
async copyQuery() {
await this.copyToClipboard(this.querySql, '复制失败,请手动复制查询SQL。', 'queryTextarea');
}