Просмотр исходного кода

feat(s8): add inline exception type creation in wizard

YY968XX 2 недель назад
Родитель
Сommit
29d8cd4e7e

+ 7 - 0
Web/src/views/aidop/s8/config/S8WatchRuleConfigPage.vue

@@ -1086,6 +1086,12 @@ function handleDraftDeleted() {
 	loadDraftCount();
 }
 
+// CONFIG-WIZARD-INLINE-CREATE-MVP-1:子组件 inline create 成功 → reload 异常类型;
+// reload 后 exceptionTypes 引用变化,子组件 watch 自动选中 pending typeCode。
+async function handleReloadExceptionTypes(_payload: { typeCode: string; severityDefault: string }) {
+	await loadExceptionTypes();
+}
+
 onMounted(() => {
 	loadRows();
 	loadExceptionTypes();
@@ -1148,6 +1154,7 @@ onDeactivated(() => stopAutoRefresh());
 			@saved="onWizardSaved"
 			@draft-saved="handleDraftSaved"
 			@generated="onWizardSaved"
+			@request-reload-exception-types="handleReloadExceptionTypes"
 		/>
 
 		<WizardDraftDrawer

+ 143 - 0
Web/src/views/aidop/s8/config/components/InlineCreateExceptionTypeDialog.vue

@@ -0,0 +1,143 @@
+<script setup lang="ts" name="aidopS8InlineCreateExceptionTypeDialog">
+// CONFIG-WIZARD-INLINE-CREATE-MVP-1:在统一配置器内 inline 新增异常类型。
+// 复用 POST /api/aidop/s8/config/exception-types,sceneCode 强制等于当前 stageCode,enabled=true。
+import { computed, reactive, ref, watch } from 'vue';
+import { ElMessage } from 'element-plus';
+import { s8ConfigApi, type S8ExceptionTypeConfigPayload, type S8ExceptionTypeConfigRow } from '../../api/s8ConfigApi';
+
+const props = defineProps<{
+	modelValue: boolean;
+	tenantId: number;
+	factoryId: number;
+	stageCode: string;
+	stageLabel?: string;
+}>();
+
+const emit = defineEmits<{
+	(e: 'update:modelValue', value: boolean): void;
+	(e: 'created', payload: { typeCode: string; typeName: string; severityDefault: string }): void;
+}>();
+
+const visible = computed({
+	get: () => props.modelValue,
+	set: (v) => emit('update:modelValue', v),
+});
+
+const SEVERITY_OPTIONS = [
+	{ value: 'FOLLOW', label: '关注' },
+	{ value: 'SERIOUS', label: '严重' },
+];
+
+const form = reactive({
+	typeName: '',
+	typeCode: '',
+	severityDefault: 'FOLLOW',
+	slaMinutes: 1440,
+	remark: '',
+});
+
+const saving = ref(false);
+
+function pad2(n: number) { return n < 10 ? `0${n}` : `${n}`; }
+function nowTimestampCompact() {
+	const d = new Date();
+	return `${d.getFullYear()}${pad2(d.getMonth() + 1)}${pad2(d.getDate())}${pad2(d.getHours())}${pad2(d.getMinutes())}${pad2(d.getSeconds())}`;
+}
+function generateTypeCode(stageCode: string): string {
+	if (!stageCode) return '';
+	const stage = stageCode.trim().toUpperCase().replace(/\s+/g, '');
+	return `CUSTOM_${stage}_${nowTimestampCompact()}`;
+}
+
+function resetForm() {
+	form.typeName = '';
+	form.typeCode = generateTypeCode(props.stageCode);
+	form.severityDefault = 'FOLLOW';
+	form.slaMinutes = 1440;
+	form.remark = '';
+}
+
+watch(visible, (v) => {
+	if (v) resetForm();
+});
+
+async function handleSave() {
+	if (!props.stageCode) { ElMessage.warning('请先选择所属阶段'); return; }
+	if (!form.typeName.trim()) { ElMessage.warning('请输入异常类型名称'); return; }
+	const code = form.typeCode.trim().toUpperCase().replace(/\s+/g, '');
+	if (!code) { ElMessage.warning('请输入内部编码'); return; }
+
+	const payload: S8ExceptionTypeConfigPayload = {
+		tenantId: props.tenantId,
+		factoryId: props.factoryId,
+		typeCode: code,
+		typeName: form.typeName.trim(),
+		sceneCode: props.stageCode,
+		severityDefault: form.severityDefault,
+		slaMinutes: Number(form.slaMinutes) || 0,
+		statsMode: 'ALL',
+		mobileVisible: true,
+		enabled: true,
+		sortNo: 900,
+		remark: form.remark.trim() || null,
+	};
+
+	saving.value = true;
+	try {
+		const created: S8ExceptionTypeConfigRow = await s8ConfigApi.exceptionTypes.create(payload);
+		ElMessage.success('异常类型已新增并选中');
+		emit('created', {
+			typeCode: created.typeCode,
+			typeName: created.typeName,
+			severityDefault: created.severityDefault || form.severityDefault,
+		});
+		visible.value = false;
+	} catch (e: any) {
+		const msg = e?.response?.data?.message ?? e?.message ?? '新增异常类型失败';
+		ElMessage.error(msg);
+	} finally {
+		saving.value = false;
+	}
+}
+</script>
+
+<template>
+	<el-dialog v-model="visible" title="新增异常类型" width="520px" :close-on-click-modal="false" destroy-on-close append-to-body>
+		<el-form label-position="top" size="small">
+			<el-form-item label="异常类型名称" required>
+				<el-input v-model="form.typeName" placeholder="如 客户加单延迟" maxlength="128" show-word-limit />
+			</el-form-item>
+			<el-form-item label="内部编码" required>
+				<el-input v-model="form.typeCode" maxlength="64" show-word-limit />
+				<span class="inline-create-hint">建议保留默认;同一工厂下不能重复</span>
+			</el-form-item>
+			<el-form-item label="所属阶段">
+				<el-input :model-value="stageLabel || stageCode || '—'" disabled />
+			</el-form-item>
+			<el-form-item label="默认严重度">
+				<el-select v-model="form.severityDefault" style="width: 100%">
+					<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="SLA 分钟">
+				<el-input-number v-model="form.slaMinutes" :min="0" :max="100000" :step="60" />
+				<span class="inline-create-hint">SLA 默认 1440(24 小时),可按业务调整</span>
+			</el-form-item>
+			<el-form-item label="备注">
+				<el-input v-model="form.remark" type="textarea" :rows="2" maxlength="500" show-word-limit />
+			</el-form-item>
+		</el-form>
+		<template #footer>
+			<el-button @click="visible = false">取消</el-button>
+			<el-button type="primary" :loading="saving" @click="handleSave">保存</el-button>
+		</template>
+	</el-dialog>
+</template>
+
+<style scoped>
+.inline-create-hint {
+	margin-left: 8px;
+	color: var(--el-text-color-secondary);
+	font-size: 12px;
+}
+</style>

+ 68 - 8
Web/src/views/aidop/s8/config/components/WatchRuleWizardDialog.vue

@@ -13,6 +13,7 @@ import {
 	type S8WatchRuleCreatePayload,
 	type S8WizardDraftDetail,
 } from '../../api/s8ConfigApi';
+import InlineCreateExceptionTypeDialog from './InlineCreateExceptionTypeDialog.vue';
 
 interface DataSourceLike {
 	id: number;
@@ -37,6 +38,7 @@ const emit = defineEmits<{
 	(e: 'saved'): void;
 	(e: 'draftSaved'): void;
 	(e: 'generated'): void;
+	(e: 'requestReloadExceptionTypes', payload: { typeCode: string; severityDefault: string }): void;
 }>();
 
 const router = useRouter();
@@ -163,6 +165,11 @@ const dictionaryMissing = ref(false);
 const fallbackObjectLabel = ref('');
 const fallbackMetricLabel = ref('');
 
+// CONFIG-WIZARD-INLINE-CREATE-MVP-1:inline 新增异常类型 dialog 状态 + 等待父组件 reload 后的回填项。
+const inlineCreateVisible = ref(false);
+const pendingTypeCode = ref<string>('');
+const pendingSeverity = ref<string>('');
+
 function pad2(n: number) { return n < 10 ? `0${n}` : `${n}`; }
 function nowTimestampCompact() {
 	const d = new Date();
@@ -263,6 +270,24 @@ watch(() => form.objectIndex, () => {
 	if (form.objectIndex >= 0) dictionaryMissing.value = false;
 });
 
+// CONFIG-WIZARD-INLINE-CREATE-MVP-1:父组件 reload 后 exceptionTypes 引用变化,若有 pending 则自动选中。
+watch(
+	() => props.exceptionTypes,
+	(list) => {
+		if (!pendingTypeCode.value) return;
+		const found = list.find((t) => t.typeCode === pendingTypeCode.value);
+		if (!found) return;
+		form.exceptionTypeCode = found.typeCode;
+		form.severity = pendingSeverity.value || found.severityDefault || form.severity;
+		pendingTypeCode.value = '';
+		pendingSeverity.value = '';
+		if (currentDraftId.value != null) {
+			ElMessage.info('异常类型已新增并选中,请保存草稿以保留本次配置');
+		}
+	},
+	{ deep: false },
+);
+
 const filteredExceptionTypes = computed(() => {
 	const all = props.exceptionTypes.filter((t) => t.enabled);
 	if (!form.stageCode) return all;
@@ -659,6 +684,21 @@ function goManualReport() {
 	visible.value = false;
 	router.push('/aidop/s8/report');
 }
+
+// CONFIG-WIZARD-INLINE-CREATE-MVP-1:打开 inline create dialog;未选 stage 时阻止。
+function openInlineCreateExceptionType() {
+	if (!form.stageCode) {
+		ElMessage.warning('请先选择所属阶段');
+		return;
+	}
+	inlineCreateVisible.value = true;
+}
+
+function handleInlineExceptionTypeCreated(payload: { typeCode: string; typeName: string; severityDefault: string }) {
+	pendingTypeCode.value = payload.typeCode;
+	pendingSeverity.value = payload.severityDefault;
+	emit('requestReloadExceptionTypes', { typeCode: payload.typeCode, severityDefault: payload.severityDefault });
+}
 </script>
 
 <template>
@@ -795,14 +835,18 @@ function goManualReport() {
 
 				<el-divider />
 				<el-form-item label="异常类型" required>
-					<el-select v-model="form.exceptionTypeCode" filterable clearable placeholder="选择异常类型" style="width: 100%">
-						<el-option
-							v-for="t in filteredExceptionTypes"
-							:key="t.typeCode"
-							:label="t.typeName"
-							:value="t.typeCode"
-						/>
-					</el-select>
+					<div class="wizard-exception-type-row">
+						<el-select v-model="form.exceptionTypeCode" filterable clearable placeholder="选择异常类型" style="flex: 1; min-width: 0">
+							<el-option
+								v-for="t in filteredExceptionTypes"
+								:key="t.typeCode"
+								:label="t.typeName"
+								:value="t.typeCode"
+							/>
+						</el-select>
+						<el-button :disabled="!form.stageCode" @click="openInlineCreateExceptionType">新增异常类型</el-button>
+					</div>
+					<span v-if="!form.stageCode" class="wizard-hint">请先在第 2 步选择阶段维度后再新增异常类型</span>
 				</el-form-item>
 
 				<el-alert type="success" :closable="false" style="margin-top: 8px">
@@ -863,6 +907,16 @@ function goManualReport() {
 			</template>
 		</template>
 	</el-dialog>
+
+	<!-- CONFIG-WIZARD-INLINE-CREATE-MVP-1:inline 新增异常类型 dialog -->
+	<InlineCreateExceptionTypeDialog
+		v-model="inlineCreateVisible"
+		:tenant-id="tenantId ?? 1"
+		:factory-id="factoryId ?? 1"
+		:stage-code="form.stageCode"
+		:stage-label="stageNodeLabel"
+		@created="handleInlineExceptionTypeCreated"
+	/>
 </template>
 
 <style scoped>
@@ -902,4 +956,10 @@ function goManualReport() {
 	color: var(--el-text-color-secondary);
 	font-size: 12px;
 }
+.wizard-exception-type-row {
+	display: flex;
+	gap: 8px;
+	width: 100%;
+	align-items: center;
+}
 </style>