|
|
@@ -1,5 +1,17 @@
|
|
|
<template>
|
|
|
<AidopDemoShell :title="pageTitle" subtitle="S0 / Sales / 合同评审周期">
|
|
|
+ <!-- ── 同步当前配置开关(工厂级) ─────────────────────────────────── -->
|
|
|
+ <div class="sync-bar">
|
|
|
+ <span class="sync-bar__label">同步当前配置:</span>
|
|
|
+ <el-switch
|
|
|
+ v-model="syncEnabled"
|
|
|
+ :disabled="syncDisabled"
|
|
|
+ :loading="syncLoading"
|
|
|
+ @change="onSyncChange"
|
|
|
+ />
|
|
|
+ <span class="sync-bar__text">{{ syncBarText }}</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
<el-form :inline="true" :model="query" class="mb12" @submit.prevent>
|
|
|
<el-form-item label="公司">
|
|
|
<el-select v-model="query.companyRefId" clearable filterable placeholder="全部" style="width: 180px">
|
|
|
@@ -62,6 +74,51 @@
|
|
|
/>
|
|
|
</div>
|
|
|
|
|
|
+ <!-- ── 意见反馈下钻配置 ──────────────────────────────────────── -->
|
|
|
+ <section class="breakdown-section">
|
|
|
+ <div class="breakdown-section__head">
|
|
|
+ <h3 class="breakdown-section__title">意见反馈 — 部门/组 PI 配置</h3>
|
|
|
+ <div class="breakdown-section__actions">
|
|
|
+ <el-button :disabled="!query.factoryRefId" @click="loadBreakdown">刷新</el-button>
|
|
|
+ <el-button type="success" :disabled="!query.factoryRefId" @click="openBreakdownCreate">新增下钻</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="!query.factoryRefId" class="breakdown-section__hint">
|
|
|
+ 请先选择工厂后维护意见反馈下钻配置
|
|
|
+ </div>
|
|
|
+ <el-table
|
|
|
+ v-else
|
|
|
+ :data="breakdownRows"
|
|
|
+ v-loading="breakdownLoading"
|
|
|
+ border
|
|
|
+ stripe
|
|
|
+ size="small"
|
|
|
+ style="width: 100%"
|
|
|
+ >
|
|
|
+ <el-table-column prop="groupCode" label="组编码" width="160" class-name="breakdown-section__wrap-cell" />
|
|
|
+ <el-table-column prop="groupName" label="组名称" min-width="160" class-name="breakdown-section__wrap-cell" />
|
|
|
+ <el-table-column prop="stdHours" label="标准时长(h)" width="130" align="right" />
|
|
|
+ <el-table-column prop="orderNo" label="顺序" width="80" align="center" />
|
|
|
+ <el-table-column label="是否启用" width="100" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag :type="row.isActive ? 'success' : 'info'" size="small">
|
|
|
+ {{ row.isActive ? '是' : '否' }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" width="220" fixed="right" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-button link type="primary" @click="openBreakdownEdit(row)">编辑</el-button>
|
|
|
+ <el-button link :type="row.isActive ? 'warning' : 'success'" @click="breakdownToggleActive(row)">
|
|
|
+ {{ row.isActive ? '禁用' : '启用' }}
|
|
|
+ </el-button>
|
|
|
+ <el-button link type="danger" @click="onBreakdownDelete(row)">删除</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </section>
|
|
|
+
|
|
|
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="640px" destroy-on-close @closed="resetForm">
|
|
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
|
|
<el-row :gutter="16">
|
|
|
@@ -117,6 +174,40 @@
|
|
|
<el-button type="primary" :loading="saving" @click="submitForm">保存</el-button>
|
|
|
</template>
|
|
|
</el-dialog>
|
|
|
+
|
|
|
+ <!-- ── 下钻 新增/编辑 弹窗 ──────────────────────────────────── -->
|
|
|
+ <el-dialog
|
|
|
+ v-model="breakdownDialogVisible"
|
|
|
+ :title="breakdownDialogTitle"
|
|
|
+ width="560px"
|
|
|
+ destroy-on-close
|
|
|
+ @closed="resetBreakdownForm"
|
|
|
+ >
|
|
|
+ <el-form ref="breakdownFormRef" :model="breakdownForm" :rules="breakdownRules" label-width="110px">
|
|
|
+ <el-form-item label="父阶段">
|
|
|
+ <el-input :model-value="STAGE_NAME_MAP[BREAKDOWN_PARENT_STAGE_CODE]" disabled />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="组编码" prop="groupCode">
|
|
|
+ <el-input v-model="breakdownForm.groupCode" placeholder="例如:market" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="组名称" prop="groupName">
|
|
|
+ <el-input v-model="breakdownForm.groupName" placeholder="例如:市场部" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="标准时长(h)" prop="stdHours">
|
|
|
+ <el-input-number v-model="breakdownForm.stdHours" :min="0" :step="0.1" :precision="2" style="width: 100%" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="顺序">
|
|
|
+ <el-input-number v-model="breakdownForm.orderNo" :min="1" style="width: 100%" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="是否启用">
|
|
|
+ <el-switch v-model="breakdownForm.isActive" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="breakdownDialogVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" :loading="breakdownSaving" @click="submitBreakdownForm">保存</el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
</AidopDemoShell>
|
|
|
</template>
|
|
|
|
|
|
@@ -128,9 +219,13 @@ import AidopDemoShell from '../../components/AidopDemoShell.vue';
|
|
|
import {
|
|
|
loadOrgList,
|
|
|
s0ContractReviewCyclesApi,
|
|
|
+ s0ContractReviewCycleSyncFlagApi,
|
|
|
+ s0ContractReviewCycleBreakdownsApi,
|
|
|
type OrgOption,
|
|
|
type S0ContractReviewCycleRow,
|
|
|
type S0ContractReviewCycleUpsert,
|
|
|
+ type S0ContractReviewCycleBreakdownRow,
|
|
|
+ type S0ContractReviewCycleBreakdownUpsert,
|
|
|
} from '../api/s0SalesApi';
|
|
|
|
|
|
const route = useRoute();
|
|
|
@@ -165,6 +260,25 @@ const formRef = ref<FormInstance>();
|
|
|
const companyOptions = ref<OrgOption[]>([]);
|
|
|
const factoryOptions = ref<OrgOption[]>([]);
|
|
|
|
|
|
+// ── 同步开关(工厂级) ─────────────────────────────────────────────────
|
|
|
+const syncEnabled = ref(false);
|
|
|
+const syncLoading = ref(false);
|
|
|
+const syncDisabled = computed(() => !query.factoryRefId);
|
|
|
+const syncBarText = computed(() => {
|
|
|
+ if (!query.factoryRefId) return '请先选择工厂';
|
|
|
+ return syncEnabled.value ? '当前工厂配置已同步到 S8' : '当前工厂未同步';
|
|
|
+});
|
|
|
+
|
|
|
+// ── 意见反馈下钻配置 ───────────────────────────────────────────────────
|
|
|
+const BREAKDOWN_PARENT_STAGE_CODE = 'feedback';
|
|
|
+const breakdownRows = ref<S0ContractReviewCycleBreakdownRow[]>([]);
|
|
|
+const breakdownLoading = ref(false);
|
|
|
+const breakdownDialogVisible = ref(false);
|
|
|
+const breakdownDialogTitle = ref('新增下钻');
|
|
|
+const breakdownEditingId = ref<number | null>(null);
|
|
|
+const breakdownSaving = ref(false);
|
|
|
+const breakdownFormRef = ref<FormInstance>();
|
|
|
+
|
|
|
const filteredFactoryOptions = computed(() => {
|
|
|
if (!query.companyRefId) return factoryOptions.value;
|
|
|
return factoryOptions.value.filter((item) => item.pid === query.companyRefId);
|
|
|
@@ -199,6 +313,30 @@ const rules: FormRules = {
|
|
|
stageName: [{ required: true, message: '请填写阶段名称', trigger: 'blur' }],
|
|
|
};
|
|
|
|
|
|
+function emptyBreakdownForm(): S0ContractReviewCycleBreakdownUpsert {
|
|
|
+ return {
|
|
|
+ companyRefId: undefined,
|
|
|
+ factoryRefId: undefined,
|
|
|
+ domainCode: '',
|
|
|
+ parentStageCode: BREAKDOWN_PARENT_STAGE_CODE,
|
|
|
+ groupCode: '',
|
|
|
+ groupName: '',
|
|
|
+ stdHours: 0,
|
|
|
+ orderNo: 1,
|
|
|
+ isActive: true,
|
|
|
+ createUser: '',
|
|
|
+ updateUser: '',
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+const breakdownForm = reactive<S0ContractReviewCycleBreakdownUpsert>(emptyBreakdownForm());
|
|
|
+
|
|
|
+const breakdownRules: FormRules = {
|
|
|
+ groupCode: [{ required: true, message: '请填写组编码', trigger: 'blur' }],
|
|
|
+ groupName: [{ required: true, message: '请填写组名称', trigger: 'blur' }],
|
|
|
+ stdHours: [{ required: true, message: '请填写标准时长', trigger: 'change' }],
|
|
|
+};
|
|
|
+
|
|
|
function syncDomainCode() {
|
|
|
const f = factoryOptions.value.find((item) => item.id === form.factoryRefId);
|
|
|
form.domainCode = f?.code ?? '';
|
|
|
@@ -228,6 +366,15 @@ watch(
|
|
|
}
|
|
|
);
|
|
|
|
|
|
+// 工厂切换时重新读取该工厂的 sync flag 和意见反馈下钻(未选工厂时清空)
|
|
|
+watch(
|
|
|
+ () => query.factoryRefId,
|
|
|
+ () => {
|
|
|
+ void loadSyncFlag();
|
|
|
+ void loadBreakdown();
|
|
|
+ }
|
|
|
+);
|
|
|
+
|
|
|
async function loadOptions() {
|
|
|
const [companies, factories] = await Promise.all([loadOrgList('201'), loadOrgList('501')]);
|
|
|
companyOptions.value = companies;
|
|
|
@@ -338,9 +485,169 @@ function toggleActive(row: S0ContractReviewCycleRow) {
|
|
|
.catch(() => {});
|
|
|
}
|
|
|
|
|
|
+// ── 同步开关加载与切换 ────────────────────────────────────────────────
|
|
|
+
|
|
|
+async function loadSyncFlag() {
|
|
|
+ if (!query.factoryRefId) {
|
|
|
+ // 未选工厂:reset 到关闭状态;不发请求
|
|
|
+ syncEnabled.value = false;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ const data = await s0ContractReviewCycleSyncFlagApi.get({ factoryRefId: query.factoryRefId });
|
|
|
+ syncEnabled.value = data?.isSyncEnabled ?? false;
|
|
|
+ } catch {
|
|
|
+ syncEnabled.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function onSyncChange(next: boolean | string | number) {
|
|
|
+ const nextBool = next === true;
|
|
|
+ if (!query.factoryRefId || !query.companyRefId) {
|
|
|
+ // 双保险:模板已 disabled,但工厂被外部清空时回滚
|
|
|
+ syncEnabled.value = !nextBool;
|
|
|
+ ElMessage.warning('请先选择公司与工厂');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const factory = factoryOptions.value.find((item) => item.id === query.factoryRefId);
|
|
|
+ syncLoading.value = true;
|
|
|
+ try {
|
|
|
+ await s0ContractReviewCycleSyncFlagApi.update({
|
|
|
+ companyRefId: query.companyRefId,
|
|
|
+ factoryRefId: query.factoryRefId,
|
|
|
+ domainCode: factory?.code ?? undefined,
|
|
|
+ isSyncEnabled: nextBool,
|
|
|
+ });
|
|
|
+ syncEnabled.value = nextBool;
|
|
|
+ ElMessage.success(nextBool ? '已开启同步' : '已关闭同步');
|
|
|
+ } catch {
|
|
|
+ syncEnabled.value = !nextBool;
|
|
|
+ ElMessage.error('同步开关切换失败,请稍后重试');
|
|
|
+ } finally {
|
|
|
+ syncLoading.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// ── 意见反馈下钻加载与 CRUD ─────────────────────────────────────────
|
|
|
+
|
|
|
+async function loadBreakdown() {
|
|
|
+ if (!query.factoryRefId) {
|
|
|
+ breakdownRows.value = [];
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ breakdownLoading.value = true;
|
|
|
+ try {
|
|
|
+ const data = await s0ContractReviewCycleBreakdownsApi.list({
|
|
|
+ factoryRefId: query.factoryRefId,
|
|
|
+ parentStageCode: BREAKDOWN_PARENT_STAGE_CODE,
|
|
|
+ page: 1,
|
|
|
+ pageSize: 100,
|
|
|
+ });
|
|
|
+ breakdownRows.value = data.list;
|
|
|
+ } catch {
|
|
|
+ breakdownRows.value = [];
|
|
|
+ } finally {
|
|
|
+ breakdownLoading.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function resetBreakdownForm() {
|
|
|
+ breakdownEditingId.value = null;
|
|
|
+ Object.assign(breakdownForm, emptyBreakdownForm());
|
|
|
+ breakdownFormRef.value?.clearValidate();
|
|
|
+}
|
|
|
+
|
|
|
+function openBreakdownCreate() {
|
|
|
+ if (!query.factoryRefId || !query.companyRefId) {
|
|
|
+ ElMessage.warning('请先选择公司与工厂');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ resetBreakdownForm();
|
|
|
+ const factory = factoryOptions.value.find((item) => item.id === query.factoryRefId);
|
|
|
+ breakdownForm.companyRefId = query.companyRefId;
|
|
|
+ breakdownForm.factoryRefId = query.factoryRefId;
|
|
|
+ breakdownForm.domainCode = factory?.code ?? '';
|
|
|
+ breakdownForm.orderNo = breakdownRows.value.length + 1;
|
|
|
+ breakdownDialogTitle.value = '新增下钻';
|
|
|
+ breakdownDialogVisible.value = true;
|
|
|
+}
|
|
|
+
|
|
|
+function openBreakdownEdit(row: S0ContractReviewCycleBreakdownRow) {
|
|
|
+ resetBreakdownForm();
|
|
|
+ breakdownEditingId.value = row.id;
|
|
|
+ breakdownDialogTitle.value = `编辑:${row.groupName}`;
|
|
|
+ Object.assign(breakdownForm, {
|
|
|
+ companyRefId: row.companyRefId,
|
|
|
+ factoryRefId: row.factoryRefId,
|
|
|
+ domainCode: row.domainCode ?? '',
|
|
|
+ parentStageCode: row.parentStageCode,
|
|
|
+ groupCode: row.groupCode,
|
|
|
+ groupName: row.groupName,
|
|
|
+ stdHours: row.stdHours,
|
|
|
+ orderNo: row.orderNo,
|
|
|
+ isActive: row.isActive,
|
|
|
+ createUser: row.createUser ?? '',
|
|
|
+ updateUser: row.updateUser ?? '',
|
|
|
+ });
|
|
|
+ breakdownDialogVisible.value = true;
|
|
|
+}
|
|
|
+
|
|
|
+async function submitBreakdownForm() {
|
|
|
+ await breakdownFormRef.value?.validate();
|
|
|
+ breakdownSaving.value = true;
|
|
|
+ try {
|
|
|
+ const payload: S0ContractReviewCycleBreakdownUpsert = { ...breakdownForm };
|
|
|
+ if (breakdownEditingId.value !== null) {
|
|
|
+ await s0ContractReviewCycleBreakdownsApi.update(breakdownEditingId.value, payload);
|
|
|
+ ElMessage.success('已保存');
|
|
|
+ } else {
|
|
|
+ await s0ContractReviewCycleBreakdownsApi.create(payload);
|
|
|
+ ElMessage.success('已创建');
|
|
|
+ }
|
|
|
+ breakdownDialogVisible.value = false;
|
|
|
+ await loadBreakdown();
|
|
|
+ } catch {
|
|
|
+ ElMessage.error('保存下钻配置失败,请检查填写内容后重试');
|
|
|
+ } finally {
|
|
|
+ breakdownSaving.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function onBreakdownDelete(row: S0ContractReviewCycleBreakdownRow) {
|
|
|
+ ElMessageBox.confirm(`确认删除该部门/组 PI 配置吗?「${row.groupName}」`, '确认', { type: 'warning' })
|
|
|
+ .then(async () => {
|
|
|
+ try {
|
|
|
+ await s0ContractReviewCycleBreakdownsApi.delete(row.id);
|
|
|
+ ElMessage.success('已删除');
|
|
|
+ await loadBreakdown();
|
|
|
+ } catch {
|
|
|
+ ElMessage.error('删除失败,请稍后重试');
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .catch(() => {});
|
|
|
+}
|
|
|
+
|
|
|
+function breakdownToggleActive(row: S0ContractReviewCycleBreakdownRow) {
|
|
|
+ const next = !row.isActive;
|
|
|
+ const actionText = next ? '启用' : '禁用';
|
|
|
+ ElMessageBox.confirm(`确定${actionText}「${row.groupName}」?`, '确认', { type: 'warning' })
|
|
|
+ .then(async () => {
|
|
|
+ try {
|
|
|
+ await s0ContractReviewCycleBreakdownsApi.toggleEnabled(row.id, { isActive: next });
|
|
|
+ ElMessage.success(`${actionText}成功`);
|
|
|
+ await loadBreakdown();
|
|
|
+ } catch {
|
|
|
+ ElMessage.error('启停失败,请稍后重试');
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .catch(() => {});
|
|
|
+}
|
|
|
+
|
|
|
onMounted(async () => {
|
|
|
await loadOptions();
|
|
|
await loadList();
|
|
|
+ await loadSyncFlag();
|
|
|
+ await loadBreakdown();
|
|
|
});
|
|
|
</script>
|
|
|
|
|
|
@@ -356,4 +663,66 @@ onMounted(async () => {
|
|
|
display: flex;
|
|
|
justify-content: flex-end;
|
|
|
}
|
|
|
+
|
|
|
+.sync-bar {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ margin-bottom: 12px;
|
|
|
+ padding: 10px 14px;
|
|
|
+ background: var(--el-color-info-light-9);
|
|
|
+ border-left: 4px solid var(--el-color-primary);
|
|
|
+ border-radius: 4px;
|
|
|
+
|
|
|
+ &__label {
|
|
|
+ font-weight: 600;
|
|
|
+ color: var(--el-text-color-primary);
|
|
|
+ }
|
|
|
+
|
|
|
+ &__text {
|
|
|
+ color: var(--el-text-color-secondary);
|
|
|
+ font-size: 13px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.breakdown-section {
|
|
|
+ margin-top: 24px;
|
|
|
+ padding-top: 16px;
|
|
|
+ border-top: 1px dashed var(--el-border-color);
|
|
|
+
|
|
|
+ &__head {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 12px;
|
|
|
+ gap: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__title {
|
|
|
+ margin: 0;
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: var(--el-text-color-primary);
|
|
|
+ }
|
|
|
+
|
|
|
+ &__actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__hint {
|
|
|
+ padding: 24px 0;
|
|
|
+ text-align: center;
|
|
|
+ color: var(--el-text-color-secondary);
|
|
|
+ font-size: 13px;
|
|
|
+ background: var(--el-color-info-light-9);
|
|
|
+ border-radius: 4px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.breakdown-section__wrap-cell .cell) {
|
|
|
+ white-space: normal;
|
|
|
+ word-break: break-word;
|
|
|
+ line-height: 1.4;
|
|
|
+}
|
|
|
</style>
|