Przeglądaj źródła

feat(s8): add manual report template prefill flow

YY968XX 5 miesięcy temu
rodzic
commit
f5e8bb9414

+ 1 - 1
Web/package.json

@@ -1,7 +1,7 @@
 {
 	"name": "admin.net",
 	"type": "module",
-	"version": "2.4.131",
+	"version": "2.4.132",
 	"packageManager": "pnpm@10.32.1",
 	"lastBuildTime": "2026.03.15",
 	"description": "Admin.NET 站在巨人肩膀上的 .NET 通用权限开发框架",

+ 3 - 0
Web/src/views/aidop/s8/api/s8ReportApi.ts

@@ -15,6 +15,9 @@ export interface S8ReportCreate {
 	responsibleDeptId: number;
 	reporterId?: number | null;
 	relatedObjectCode?: string;
+	// TASK-022-MANUAL-REPORT-TEMPLATE-1:与后端 AdoS8ManualReportCreateDto 对齐,支持模板透传维度。
+	stageCode?: string;
+	orderFlowCode?: string;
 }
 
 export interface S8AttachmentCreate {

+ 186 - 6
Web/src/views/aidop/s8/config/components/WatchRuleWizardDialog.vue

@@ -2,7 +2,9 @@
 // CONFIG-WIZARD-DRAFT-FRONTEND-1:5 步业务条件向导 + 后端草稿接入。
 // 1) 报警机制 2) 业务维度 3) 监控对象/指标 4) 判定标准 5) 基础配置
 // 非 MANUAL_REPORT:先 saveDraft 落库,再 generate-rule 生成正式 watch_rule。
-// MANUAL_REPORT:跳转主动提报,不写草稿、不写规则。
+// TASK-022-MANUAL-REPORT-TEMPLATE-1:MANUAL_REPORT 改为「主动提报模板」,
+// 仍复用 ado_s8_config_draft 表 + wizard_json 字段保存模板,但不生成 watch_rule。
+// 模板供员工在主动提报页面(/aidop/s8/report)下拉选择并自动带入业务参数。
 import { computed, reactive, ref, watch } from 'vue';
 import { useRouter } from 'vue-router';
 import { ElMessage } from 'element-plus';
@@ -165,6 +167,31 @@ const form = reactive({
 const currentDraftId = ref<number | string | null>(null);
 const currentDraftCode = ref<string>('');
 const draftName = ref<string>('');
+
+// TASK-022-MANUAL-REPORT-TEMPLATE-1:MANUAL_REPORT 模板表单(不与 watch_rule 字段重叠)。
+// 默认值对应「生产主动申报」首条示范模板(R13 ④)。
+const MANUAL_REPORT_DEFAULT_DRAFT_CODE = 'MR-PROD-MANUAL-REPORT';
+const templateForm = reactive({
+	templateName: '生产主动申报',
+	stageCode: 'S6',
+	orderFlowCode: 'BODY_PRODUCTION',
+	title: '生产主动申报 - 产线异常',
+	description: '请描述异常现象、影响工序、涉及工单及初步处理建议。',
+	severity: 'FOLLOW' as 'FOLLOW' | 'SERIOUS',
+	relatedObjectCode: 'WO-DEMO-MANUAL-001',
+	objectCodeLabel: '工单号',
+});
+
+function resetTemplateForm() {
+	templateForm.templateName = '生产主动申报';
+	templateForm.stageCode = 'S6';
+	templateForm.orderFlowCode = 'BODY_PRODUCTION';
+	templateForm.title = '生产主动申报 - 产线异常';
+	templateForm.description = '请描述异常现象、影响工序、涉及工单及初步处理建议。';
+	templateForm.severity = 'FOLLOW';
+	templateForm.relatedObjectCode = 'WO-DEMO-MANUAL-001';
+	templateForm.objectCodeLabel = '工单号';
+}
 // 字典演进兜底:草稿回填后若按 objectType+objectLabel 找不到字典项,标记禁用 generate。
 const dictionaryMissing = ref(false);
 const fallbackObjectLabel = ref('');
@@ -229,6 +256,7 @@ function resetForm() {
 	dictionaryMissing.value = false;
 	fallbackObjectLabel.value = '';
 	fallbackMetricLabel.value = '';
+	resetTemplateForm();
 }
 
 watch(visible, (v) => {
@@ -465,12 +493,26 @@ function hydrateFromDraft(draft: S8WizardDraftDetail) {
 	currentDraftId.value = draft.id;
 	currentDraftCode.value = draft.draftCode;
 	draftName.value = draft.draftName ?? '';
-	let parsed: any = null;
+	let parsed: any;
 	try { parsed = JSON.parse(draft.wizardJson); } catch { parsed = null; }
 	if (!parsed || typeof parsed !== 'object') {
 		ElMessage.warning('草稿内容损坏,已重置');
 		return;
 	}
+	// TASK-022-MANUAL-REPORT-TEMPLATE-1:MANUAL_REPORT 模板独立 schema(templateKind=MANUAL_REPORT)。
+	if (parsed.templateKind === 'MANUAL_REPORT' || (draft.mechanism ?? '').toUpperCase() === 'MANUAL_REPORT') {
+		form.mechanism = 'MANUAL_REPORT';
+		if (typeof parsed.templateName === 'string') templateForm.templateName = parsed.templateName;
+		if (typeof parsed.stageCode === 'string') templateForm.stageCode = parsed.stageCode;
+		if (typeof parsed.orderFlowCode === 'string') templateForm.orderFlowCode = parsed.orderFlowCode;
+		if (typeof parsed.title === 'string') templateForm.title = parsed.title;
+		if (typeof parsed.description === 'string') templateForm.description = parsed.description;
+		if (parsed.severity === 'FOLLOW' || parsed.severity === 'SERIOUS') templateForm.severity = parsed.severity;
+		if (typeof parsed.relatedObjectCode === 'string') templateForm.relatedObjectCode = parsed.relatedObjectCode;
+		if (typeof parsed.objectCodeLabel === 'string') templateForm.objectCodeLabel = parsed.objectCodeLabel;
+		step.value = 0;
+		return;
+	}
 	if (parsed.version && parsed.version !== 1) {
 		ElMessage.warning(`草稿版本 ${parsed.version} 不被当前前端支持,请新建`);
 		return;
@@ -544,10 +586,96 @@ function buildDraftPayload(): {
 	};
 }
 
+// TASK-022-MANUAL-REPORT-TEMPLATE-1:MANUAL_REPORT 模板序列化(独立 schema)。
+function serializeManualReportTemplate(): string {
+	return JSON.stringify({
+		templateKind: 'MANUAL_REPORT',
+		templateName: templateForm.templateName,
+		title: templateForm.title,
+		description: templateForm.description,
+		sceneCode: templateForm.stageCode, // 主动提报页 sceneCode 与 stageCode 同源
+		stageCode: templateForm.stageCode,
+		orderFlowCode: templateForm.orderFlowCode,
+		severity: templateForm.severity,
+		relatedObjectCode: templateForm.relatedObjectCode,
+		objectCodeLabel: templateForm.objectCodeLabel,
+	});
+}
+
+function buildManualReportDraftPayload() {
+	const code = currentDraftCode.value && currentDraftCode.value.startsWith('MR-')
+		? currentDraftCode.value
+		: MANUAL_REPORT_DEFAULT_DRAFT_CODE;
+	return {
+		tenantId: props.tenantId ?? 1,
+		factoryId: props.factoryId ?? 1,
+		draftCode: code,
+		draftName: templateForm.templateName.trim() || '主动提报模板',
+		wizardJson: serializeManualReportTemplate(),
+		currentStep: 0,
+		mechanism: 'MANUAL_REPORT',
+		stageCode: templateForm.stageCode || null,
+		orderFlowCode: templateForm.orderFlowCode || null,
+		exceptionTypeCode: null,
+		remark: '主动提报模板,不生成自动监控规则',
+	};
+}
+
+async function saveManualReportTemplate(): Promise<boolean> {
+	if (!templateForm.templateName.trim()) {
+		ElMessage.warning('请填写模板名称');
+		return false;
+	}
+	if (!templateForm.title.trim()) {
+		ElMessage.warning('请填写默认标题');
+		return false;
+	}
+	if (!templateForm.stageCode) {
+		ElMessage.warning('请选择阶段维度');
+		return false;
+	}
+	const payload = buildManualReportDraftPayload();
+	savingDraft.value = true;
+	try {
+		if (currentDraftId.value == null) {
+			const created = await s8ConfigApi.wizardDrafts.create(payload);
+			currentDraftId.value = created.id;
+			currentDraftCode.value = created.draftCode;
+			draftName.value = created.draftName ?? payload.draftName;
+			ElMessage.success('主动提报模板已保存,可在主动提报页面选择使用');
+		} else {
+			const updated = await s8ConfigApi.wizardDrafts.update(currentDraftId.value, {
+				draftName: payload.draftName,
+				wizardJson: payload.wizardJson,
+				currentStep: payload.currentStep,
+				mechanism: payload.mechanism,
+				stageCode: payload.stageCode,
+				orderFlowCode: payload.orderFlowCode,
+				exceptionTypeCode: payload.exceptionTypeCode,
+				remark: payload.remark,
+			});
+			draftName.value = updated.draftName ?? payload.draftName;
+			ElMessage.success('主动提报模板已更新');
+		}
+		emit('draftSaved');
+		return true;
+	} catch (e: any) {
+		const msg = e?.response?.data?.message ?? e?.message ?? '保存主动提报模板失败';
+		// 友好兜底:DraftCode 唯一索引冲突
+		if (typeof msg === 'string' && msg.includes('已存在')) {
+			ElMessage.error(`主动提报模板编码 ${payload.draftCode} 已存在,请在草稿列表中编辑现有模板`);
+		} else {
+			ElMessage.error(msg);
+		}
+		return false;
+	} finally {
+		savingDraft.value = false;
+	}
+}
+
 async function saveDraft(): Promise<boolean> {
 	if (isManualReport.value) {
-		ElMessage.info('主动提报无需保存草稿');
-		return false;
+		return saveManualReportTemplate();
 	}
 	const payload = buildDraftPayload();
 	savingDraft.value = true;
@@ -686,8 +814,59 @@ function handleInlineExceptionTypeCreated(payload: { typeCode: string; typeName:
 				:closable="false"
 				style="margin-top: 16px"
 				title="主动提报无需配置自动规则"
-				description="主动提报类不依赖自动监控规则,由业务人员在主动提报页面发起。"
+				description="主动提报不会生成自动监控规则,可在此保存提报模板,供员工在主动提报页面选择使用。"
 			/>
+
+			<!-- TASK-022-MANUAL-REPORT-TEMPLATE-1:MANUAL_REPORT 模板字段表单 -->
+			<el-form
+				v-if="isManualReport"
+				label-position="top"
+				size="small"
+				style="margin-top: 16px"
+			>
+				<el-form-item label="模板名称" required>
+					<el-input v-model="templateForm.templateName" maxlength="64" show-word-limit />
+				</el-form-item>
+				<el-form-item label="阶段维度(S1-S7)" required>
+					<el-select v-model="templateForm.stageCode" clearable filterable placeholder="选择阶段维度" style="width: 100%">
+						<el-option
+							v-for="n in stageNodes"
+							:key="n.nodeCode"
+							:label="n.nodeName"
+							:value="n.nodeCode"
+							:disabled="n.disabled"
+						/>
+					</el-select>
+				</el-form-item>
+				<el-form-item label="订单流程节点(可选)">
+					<el-select v-model="templateForm.orderFlowCode" clearable filterable placeholder="选择订单流程节点(可不选)" style="width: 100%">
+						<el-option
+							v-for="n in orderFlowNodes"
+							:key="n.nodeCode"
+							:label="n.nodeName"
+							:value="n.nodeCode"
+							:disabled="n.disabled"
+						/>
+					</el-select>
+				</el-form-item>
+				<el-form-item label="默认标题模板" required>
+					<el-input v-model="templateForm.title" maxlength="128" show-word-limit />
+				</el-form-item>
+				<el-form-item label="默认描述模板">
+					<el-input v-model="templateForm.description" type="textarea" :rows="3" maxlength="500" show-word-limit />
+				</el-form-item>
+				<el-form-item label="默认严重度">
+					<el-select v-model="templateForm.severity" style="width: 240px">
+						<el-option v-for="o in SEVERITY_OPTIONS" :key="o.value" :label="o.label" :value="o.value" />
+					</el-select>
+				</el-form-item>
+				<el-form-item label="对象编码示例">
+					<el-input v-model="templateForm.relatedObjectCode" maxlength="64" show-word-limit placeholder="如 WO-DEMO-MANUAL-001" />
+				</el-form-item>
+				<el-form-item label="对象编码标签">
+					<el-input v-model="templateForm.objectCodeLabel" maxlength="32" placeholder="如 工单号 / 订单号" />
+				</el-form-item>
+			</el-form>
 		</div>
 
 		<!-- Step 1: 业务维度 -->
@@ -849,7 +1028,8 @@ function handleInlineExceptionTypeCreated(payload: { typeCode: string; typeName:
 		<template #footer>
 			<template v-if="isManualReport">
 				<el-button @click="visible = false">关闭</el-button>
-				<el-button type="primary" @click="goManualReport">前往主动提报</el-button>
+				<el-button :loading="savingDraft" type="primary" @click="saveDraft">保存模板</el-button>
+				<el-button @click="goManualReport">前往主动提报</el-button>
 			</template>
 			<template v-else>
 				<el-button v-if="step > 0" @click="prevStep">上一步</el-button>

+ 85 - 1
Web/src/views/aidop/s8/report/S8ManualReportPage.vue

@@ -7,6 +7,8 @@ import { getAPI } from '/@/utils/axios-utils';
 import { SysFileApi } from '/@/api-services';
 import AidopDemoShell from '../../components/AidopDemoShell.vue';
 import { s8ReportApi } from '../api/s8ReportApi';
+// TASK-022-MANUAL-REPORT-TEMPLATE-1:拉取「主动提报模板」(mechanism=MANUAL_REPORT 的 wizard-drafts)
+import { s8ConfigApi, type S8WizardDraftListItem } from '../api/s8ConfigApi';
 
 const router = useRouter();
 const saving = ref(false);
@@ -34,8 +36,16 @@ const form = reactive({
 	tenantId: 1,
 	factoryId: 1,
 	relatedObjectCode: '',
+	// TASK-022-MANUAL-REPORT-TEMPLATE-1:模板带入维度归属,提交时透传后端落 ado_s8_exception。
+	stageCode: '',
+	orderFlowCode: '',
 });
 
+// TASK-022-MANUAL-REPORT-TEMPLATE-1:模板下拉状态。
+const templates = ref<S8WizardDraftListItem[]>([]);
+const selectedTemplateId = ref<number | string | null>(null);
+const templateLoading = ref(false);
+
 async function loadOptions() {
 	const result = (await s8ReportApi.formOptions({ tenantId: 1, factoryId: 1 })) as Record<string, any> & {
 		defaultOccurrenceDeptId?: number | null;
@@ -60,6 +70,53 @@ async function loadOptions() {
 	if (!form.responsibleDeptId && formOptions.departments[0]) form.responsibleDeptId = formOptions.departments[0].value;
 }
 
+// TASK-022-MANUAL-REPORT-TEMPLATE-1:拉取主动提报模板列表(前端按 mechanism 过滤)。
+async function loadTemplates() {
+	templateLoading.value = true;
+	try {
+		const result = await s8ConfigApi.wizardDrafts.list({ tenantId: 1, factoryId: 1, pageSize: 200 });
+		templates.value = (result.items ?? []).filter(
+			(d) => (d.mechanism ?? '').toUpperCase() === 'MANUAL_REPORT',
+		);
+	} catch {
+		// 静默:模板拉取失败不影响手填路径
+		templates.value = [];
+	} finally {
+		templateLoading.value = false;
+	}
+}
+
+// 选择模板后载入详情,解析 wizardJson 并应用到 form。
+async function applyTemplate(id: number | string | null) {
+	selectedTemplateId.value = id;
+	if (id == null) return;
+	let detail;
+	try {
+		detail = await s8ConfigApi.wizardDrafts.detail(id);
+	} catch {
+		ElMessage.warning('主动提报模板加载失败,已保持当前表单内容');
+		return;
+	}
+	let parsed: any;
+	try {
+		parsed = JSON.parse(detail.wizardJson);
+	} catch {
+		parsed = null;
+	}
+	if (!parsed || typeof parsed !== 'object' || parsed.templateKind !== 'MANUAL_REPORT') {
+		ElMessage.warning('主动提报模板格式异常,已保持当前表单内容');
+		return;
+	}
+	if (typeof parsed.title === 'string' && parsed.title) form.title = parsed.title;
+	if (typeof parsed.sceneCode === 'string' && parsed.sceneCode) form.sceneCode = parsed.sceneCode;
+	if (parsed.severity === 'FOLLOW' || parsed.severity === 'SERIOUS') form.severity = parsed.severity;
+	if (typeof parsed.description === 'string') form.description = parsed.description;
+	if (typeof parsed.relatedObjectCode === 'string') form.relatedObjectCode = parsed.relatedObjectCode;
+	if (typeof parsed.stageCode === 'string') form.stageCode = parsed.stageCode;
+	if (typeof parsed.orderFlowCode === 'string') form.orderFlowCode = parsed.orderFlowCode;
+	ElMessage.success(`已应用模板:${parsed.templateName ?? detail.draftName ?? ''}`);
+}
+
 function onFileChange(uploadFile: UploadFile) {
 	if (uploadFile.raw) {
 		attachmentFiles.value = [...attachmentFiles.value, uploadFile.raw];
@@ -96,7 +153,14 @@ async function submit() {
 
 	saving.value = true;
 	try {
-		const res = (await s8ReportApi.create({ ...form, occurrenceDeptId, responsibleDeptId })) as { exceptionId: number };
+		const res = (await s8ReportApi.create({
+			...form,
+			occurrenceDeptId,
+			responsibleDeptId,
+			// TASK-022-MANUAL-REPORT-TEMPLATE-1:维度三字段空串归一为 undefined,避免后端把 '' 当合法 stage_code。
+			stageCode: form.stageCode || undefined,
+			orderFlowCode: form.orderFlowCode || undefined,
+		})) as { exceptionId: number };
 		if (attachmentFiles.value.length > 0) {
 			await uploadAttachments(res.exceptionId);
 		}
@@ -111,12 +175,32 @@ async function submit() {
 
 onMounted(() => {
 	void loadOptions();
+	void loadTemplates();
 });
 </script>
 
 <template>
 	<AidopDemoShell title="主动提报" subtitle="S8 异常协同 / 人工上报">
 		<el-form :model="form" label-width="120px" style="max-width: 760px">
+			<!-- TASK-022-MANUAL-REPORT-TEMPLATE-1:提报模板下拉,可选;不选则继续手填路径。 -->
+			<el-form-item label="提报模板">
+				<el-select
+					:model-value="selectedTemplateId"
+					placeholder="可选:选择主动提报模板自动带入业务参数"
+					style="width: 100%"
+					clearable
+					filterable
+					:loading="templateLoading"
+					@update:model-value="(v: number | string | null) => applyTemplate(v)"
+				>
+					<el-option
+						v-for="t in templates"
+						:key="t.id"
+						:label="t.draftName ?? t.draftCode"
+						:value="t.id"
+					/>
+				</el-select>
+			</el-form-item>
 			<el-form-item label="标题" required>
 				<el-input v-model="form.title" placeholder="简要描述异常" />
 			</el-form-item>