|
|
@@ -0,0 +1,541 @@
|
|
|
+<script setup lang="ts">
|
|
|
+/**
|
|
|
+ * 课酬方案页(views/academic/payroll/index.vue)—— 对齐契约 v1.9 两级结构(端点 58/59)。
|
|
|
+ *
|
|
|
+ * 结构(v1.9 通知 §1「页面一分为二」):
|
|
|
+ * ① 方案列表(端点 58 GET,useListPage 分页)+ 展开行显示**明细**(两级结构:
|
|
|
+ * 上半 = 方案行,下半 = details[]);
|
|
|
+ * ② 新建/编辑方案对话框:上半 = 方案表单(方案名/校区/生效日/方案级启停),
|
|
|
+ * 下半 = 明细表格(行内编辑 + 增删行);
|
|
|
+ * **保存 = 整案 + 完整 details 一起提交**(POST 新建 / PUT 整体替换明细,同事务先删后插);
|
|
|
+ * ③ 试算对话框(端点 59):只展示「命中哪一档规则」(9 字段含 4 个溯源字段),
|
|
|
+ * **不展示最终金额**(最终课酬只在下课时端点 51 产生;3112 = 方案未配置 → 引导去配置)。
|
|
|
+ *
|
|
|
+ * 纪律:
|
|
|
+ * - 两个 actived 分开:方案级 `actived`(整案停用)/ 明细级 `detailActived`(某档停用);
|
|
|
+ * - 教龄是两个输入框(下限/上限),`teacherExpMin`/`teacherExpMax` 区间;
|
|
|
+ * - `percentage` 是百分数字符串("85.0" = 85%);min/max/reward 是金额字符串;
|
|
|
+ * - 老师 `teacherId` 可空 = 通用档(按 classify/教龄 匹配);
|
|
|
+ * - 写操作恒 200 + body code,业务错误(2001/2002/3001/3112…)读 code 直显 message。
|
|
|
+ */
|
|
|
+import { listPlans, createPlan, updatePlan, quotePayroll } from '@/api/academic/payroll';
|
|
|
+import type { PayrollPlanDTO, PayrollDetailDTO, PayrollQuoteResult } from '@/api/academic/payroll';
|
|
|
+import { listCampuses } from '@/api/org/campus';
|
|
|
+import type { CampusDTO } from '@/api/org/campus';
|
|
|
+import { listMembers } from '@/api/org/member';
|
|
|
+import type { MemberDTO } from '@/api/org/member';
|
|
|
+import ListPage from '@/components/ListPage.vue';
|
|
|
+import type { ListColumn } from '@/components/list-column';
|
|
|
+import { useListPage } from '@/composables/use-list-page';
|
|
|
+import { useScope } from '@/store/scope';
|
|
|
+
|
|
|
+const scope = useScope();
|
|
|
+
|
|
|
+/** 命令式 EP(UMD 全局,http.ts 同款读法) */
|
|
|
+function ElMessage_(type: 'success' | 'warning' | 'error', text: string): void {
|
|
|
+ const M = (globalThis.ElementPlus ?? {}).ElMessage;
|
|
|
+ if (M) M({ type, message: text });
|
|
|
+ else console.warn(`[${type}]`, text);
|
|
|
+}
|
|
|
+
|
|
|
+/* ================= 选项数据(校区/老师) ================= */
|
|
|
+const campusOpts = ref<CampusDTO[]>([]);
|
|
|
+const teacherOpts = ref<MemberDTO[]>([]);
|
|
|
+const campusName = (id: number): string => (id === 0 ? '机构级' : campusOpts.value.find((c) => c.campusId === id)?.campusName ?? `#${id}`);
|
|
|
+const teacherName = (id: string | null): string => (id ? teacherOpts.value.find((t) => t.userId === id)?.realName ?? id : '通用档');
|
|
|
+
|
|
|
+onMounted(async () => {
|
|
|
+ const q = scope.toQuery();
|
|
|
+ const [c, m] = await Promise.all([
|
|
|
+ listCampuses({ ...q, pageSize: 200 }),
|
|
|
+ listMembers({ ...q, personType: 'staff', pageSize: 200 }),
|
|
|
+ ]);
|
|
|
+ if (c.code === 200) campusOpts.value = c.data?.list ?? [];
|
|
|
+ if (m.code === 200) teacherOpts.value = m.data?.list ?? [];
|
|
|
+});
|
|
|
+
|
|
|
+/* ================= 方案列表(端点 58 GET) ================= */
|
|
|
+const activedFilter = ref<number | undefined>(undefined);
|
|
|
+const { rows: plans, total, page, pageSize, loading, error, fetchList } = useListPage<PayrollPlanDTO>({
|
|
|
+ fetcher: (p, ps) => listPlans({ ...scope.toQuery(), actived: activedFilter.value, page: p, pageSize: ps }),
|
|
|
+});
|
|
|
+onMounted(() => {
|
|
|
+ void fetchList();
|
|
|
+});
|
|
|
+
|
|
|
+/** 科目分类(新契约无独立字典端点,暂静态枚举;classifyId ← 旧 t_course_classify) */
|
|
|
+const CLASSIFIES = [1, 2, 3] as const;
|
|
|
+const classifyText: Record<number, string> = { 1: '数学', 2: '语文', 3: '英语' };
|
|
|
+const tierText: Record<string, string> = {
|
|
|
+ personal_classify: '个人·科目档',
|
|
|
+ personal_grade_subject: '个人·年级学科档',
|
|
|
+ cert_general: '持证通用档',
|
|
|
+ nocert_general: '无证通用档',
|
|
|
+};
|
|
|
+const statusText: Record<number, string> = { 0: '停用', 1: '启用' };
|
|
|
+
|
|
|
+const planColumns: ListColumn<PayrollPlanDTO>[] = [
|
|
|
+ { prop: 'id', label: '方案 id', minWidth: 80 },
|
|
|
+ { prop: 'schemeName', label: '方案名', minWidth: 200 },
|
|
|
+ { prop: 'campusId', label: '校区', minWidth: 120, formatter: (r) => campusName(r.campusId) },
|
|
|
+ { prop: 'effectDate', label: '生效日', minWidth: 110 },
|
|
|
+ { prop: 'actived', label: '方案状态', minWidth: 90, formatter: (r) => statusText[r.actived] ?? String(r.actived) },
|
|
|
+ { prop: 'details', label: '明细数', minWidth: 80, formatter: (r) => String(r.details?.length ?? 0) },
|
|
|
+];
|
|
|
+function onSearch(): void {
|
|
|
+ page.value = 1;
|
|
|
+ void fetchList();
|
|
|
+}
|
|
|
+
|
|
|
+/* ================= 方案编辑(新建/整体替换) ================= */
|
|
|
+/** 明细编辑行(teacherId 用 '' 表示通用档/null,提交时转 null) */
|
|
|
+interface DetailEditRow {
|
|
|
+ detailId: number;
|
|
|
+ teacherId: string; // '' = 通用档(null)
|
|
|
+ classifyId: number;
|
|
|
+ classType: number;
|
|
|
+ gradeId: number;
|
|
|
+ subjectId: number;
|
|
|
+ teacherCert: number;
|
|
|
+ teacherExpMin: number;
|
|
|
+ teacherExpMax: number;
|
|
|
+ percentage: string; // "85.0"
|
|
|
+ min: string;
|
|
|
+ max: string;
|
|
|
+ reward: string;
|
|
|
+ displayName: string;
|
|
|
+ detailActived: number;
|
|
|
+}
|
|
|
+let detailSeq = 1000;
|
|
|
+
|
|
|
+const editVisible = ref(false);
|
|
|
+const editBusy = ref(false);
|
|
|
+const editingId = ref<number | null>(null); // null = 新建
|
|
|
+const planForm = reactive({ campusId: 0, schemeName: '', effectDate: '2026-09-01', actived: 1 });
|
|
|
+const detailRows = ref<DetailEditRow[]>([]);
|
|
|
+
|
|
|
+function blankDetail(): DetailEditRow {
|
|
|
+ detailSeq += 1;
|
|
|
+ return {
|
|
|
+ detailId: detailSeq,
|
|
|
+ teacherId: '',
|
|
|
+ classifyId: 1,
|
|
|
+ classType: 1,
|
|
|
+ gradeId: -1,
|
|
|
+ subjectId: -1,
|
|
|
+ teacherCert: 0,
|
|
|
+ teacherExpMin: 0,
|
|
|
+ teacherExpMax: 100,
|
|
|
+ percentage: '80.0',
|
|
|
+ min: '0.00',
|
|
|
+ max: '0.00',
|
|
|
+ reward: '0.00',
|
|
|
+ displayName: '',
|
|
|
+ detailActived: 1,
|
|
|
+ };
|
|
|
+}
|
|
|
+function openCreate(): void {
|
|
|
+ editingId.value = null;
|
|
|
+ planForm.campusId = scope.current.campusId || 0;
|
|
|
+ planForm.schemeName = '';
|
|
|
+ planForm.effectDate = '2026-09-01';
|
|
|
+ planForm.actived = 1;
|
|
|
+ detailRows.value = [blankDetail()];
|
|
|
+ editVisible.value = true;
|
|
|
+}
|
|
|
+function openEdit(p: PayrollPlanDTO): void {
|
|
|
+ editingId.value = p.id;
|
|
|
+ planForm.campusId = p.campusId;
|
|
|
+ planForm.schemeName = p.schemeName;
|
|
|
+ planForm.effectDate = p.effectDate;
|
|
|
+ planForm.actived = p.actived;
|
|
|
+ detailRows.value = p.details.map((d) => ({
|
|
|
+ detailId: d.detailId,
|
|
|
+ teacherId: d.teacherId ?? '',
|
|
|
+ classifyId: d.classifyId,
|
|
|
+ classType: d.classType,
|
|
|
+ gradeId: d.gradeId,
|
|
|
+ subjectId: d.subjectId,
|
|
|
+ teacherCert: d.teacherCert,
|
|
|
+ teacherExpMin: d.teacherExpMin,
|
|
|
+ teacherExpMax: d.teacherExpMax,
|
|
|
+ percentage: d.percentage,
|
|
|
+ min: d.min,
|
|
|
+ max: d.max,
|
|
|
+ reward: d.reward,
|
|
|
+ displayName: d.displayName ?? '',
|
|
|
+ detailActived: d.detailActived,
|
|
|
+ }));
|
|
|
+ editVisible.value = true;
|
|
|
+}
|
|
|
+function addDetail(): void {
|
|
|
+ detailRows.value.push(blankDetail());
|
|
|
+}
|
|
|
+function removeDetail(i: number): void {
|
|
|
+ detailRows.value.splice(i, 1);
|
|
|
+}
|
|
|
+
|
|
|
+/** 提交:方案级字段 + 完整 details → 整体替换(PUT 同事务先删后插)。 */
|
|
|
+async function savePlan(): Promise<void> {
|
|
|
+ if (!planForm.schemeName) {
|
|
|
+ ElMessage_('warning', '请填写方案名');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (detailRows.value.length === 0) {
|
|
|
+ ElMessage_('warning', '至少需要 1 条明细');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ for (const d of detailRows.value) {
|
|
|
+ if (d.teacherExpMin > d.teacherExpMax) {
|
|
|
+ ElMessage_('warning', '明细教龄下限不能大于上限');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const details: PayrollDetailDTO[] = detailRows.value.map((d) => ({
|
|
|
+ detailId: d.detailId,
|
|
|
+ teacherId: d.teacherId || null,
|
|
|
+ classifyId: d.classifyId,
|
|
|
+ classType: d.classType,
|
|
|
+ gradeId: d.gradeId,
|
|
|
+ subjectId: d.subjectId,
|
|
|
+ teacherCert: d.teacherCert,
|
|
|
+ teacherExpMin: d.teacherExpMin,
|
|
|
+ teacherExpMax: d.teacherExpMax,
|
|
|
+ percentage: d.percentage,
|
|
|
+ min: d.min,
|
|
|
+ max: d.max,
|
|
|
+ reward: d.reward,
|
|
|
+ displayName: d.displayName || null,
|
|
|
+ detailActived: d.detailActived,
|
|
|
+ }));
|
|
|
+ editBusy.value = true;
|
|
|
+ try {
|
|
|
+ const body = {
|
|
|
+ institutionId: scope.current.institutionId,
|
|
|
+ campusId: planForm.campusId,
|
|
|
+ schemeName: planForm.schemeName,
|
|
|
+ effectDate: planForm.effectDate,
|
|
|
+ actived: planForm.actived,
|
|
|
+ details,
|
|
|
+ };
|
|
|
+ const r = editingId.value === null ? await createPlan(body) : await updatePlan(editingId.value, body);
|
|
|
+ if (r.code === 200) {
|
|
|
+ ElMessage_('success', '保存成功');
|
|
|
+ editVisible.value = false;
|
|
|
+ void fetchList();
|
|
|
+ } else {
|
|
|
+ ElMessage_('warning', r.message || `保存失败(code ${r.code})`);
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ ElMessage_('error', '网络连接异常');
|
|
|
+ } finally {
|
|
|
+ editBusy.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* ================= 课酬试算(端点 59;只回规则参数,不含最终金额) ================= */
|
|
|
+const quoteVisible = ref(false);
|
|
|
+const quoteBusy = ref(false);
|
|
|
+const quoteForm = reactive({ teacherId: '', classifyId: 1, teacherExp: 0, date: '' });
|
|
|
+const quoteResult = ref<PayrollQuoteResult | null>(null);
|
|
|
+const quoteError = ref('');
|
|
|
+
|
|
|
+function openQuote(): void {
|
|
|
+ quoteResult.value = null;
|
|
|
+ quoteError.value = '';
|
|
|
+ if (teacherOpts.value.length > 0 && !quoteForm.teacherId) quoteForm.teacherId = teacherOpts.value[0].userId;
|
|
|
+ const today = new Date();
|
|
|
+ quoteForm.date = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
|
|
|
+ quoteVisible.value = true;
|
|
|
+}
|
|
|
+async function doQuote(): Promise<void> {
|
|
|
+ if (!quoteForm.teacherId) {
|
|
|
+ ElMessage_('warning', '请选择老师');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ quoteBusy.value = true;
|
|
|
+ quoteError.value = '';
|
|
|
+ quoteResult.value = null;
|
|
|
+ try {
|
|
|
+ const r = await quotePayroll({
|
|
|
+ teacherId: quoteForm.teacherId,
|
|
|
+ schoolId: scope.current.institutionId,
|
|
|
+ campusId: scope.current.campusId || 0,
|
|
|
+ classifyId: quoteForm.classifyId,
|
|
|
+ teacherExp: quoteForm.teacherExp,
|
|
|
+ date: quoteForm.date,
|
|
|
+ });
|
|
|
+ if (r.code === 200 && r.data) {
|
|
|
+ quoteResult.value = r.data;
|
|
|
+ } else if (r.code === 3112) {
|
|
|
+ // 3112:方案未配置 → 引导去配置(不得当失败 toast,是可操作的业务分支)
|
|
|
+ quoteError.value = '课酬方案未配置(3112):请先为该老师该科目配置生效方案';
|
|
|
+ } else {
|
|
|
+ quoteError.value = r.message || `试算失败(code ${r.code})`;
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ quoteError.value = '网络连接异常';
|
|
|
+ } finally {
|
|
|
+ quoteBusy.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <div class="payroll-page">
|
|
|
+ <!-- ① 方案列表(端点 58 GET;两级结构:下方区域逐方案渲染明细表) -->
|
|
|
+ <ListPage
|
|
|
+ v-model:page="page"
|
|
|
+ v-model:page-size="pageSize"
|
|
|
+ :rows="plans"
|
|
|
+ :total="total"
|
|
|
+ :loading="loading"
|
|
|
+ :error="error"
|
|
|
+ :columns="planColumns"
|
|
|
+ @search="onSearch"
|
|
|
+ >
|
|
|
+ <template #toolbar>
|
|
|
+ <el-select v-model="activedFilter" placeholder="方案状态" clearable class="toolbar-select">
|
|
|
+ <el-option :value="1" label="启用" />
|
|
|
+ <el-option :value="0" label="停用" />
|
|
|
+ </el-select>
|
|
|
+ <el-button type="primary" @click="openCreate">新建方案</el-button>
|
|
|
+ <el-button @click="openQuote">课酬试算</el-button>
|
|
|
+ </template>
|
|
|
+ </ListPage>
|
|
|
+
|
|
|
+ <!-- ①' 方案明细(v1.9 两级结构:方案行 + 明细表;编辑入口与方案一一对应) -->
|
|
|
+ <div v-for="p in plans" :key="p.id" class="payroll-page__plan">
|
|
|
+ <div class="payroll-page__plan-head">
|
|
|
+ <span class="payroll-page__plan-title">{{ p.schemeName }}({{ campusName(p.campusId) }},生效 {{ p.effectDate }},{{ statusText[p.actived] }})</span>
|
|
|
+ <el-button size="small" type="primary" @click="openEdit(p)">编辑方案(整体替换明细)</el-button>
|
|
|
+ </div>
|
|
|
+ <el-table :data="p.details" size="small" border class="payroll-page__details">
|
|
|
+ <el-table-column prop="detailId" label="明细 id" width="80" />
|
|
|
+ <el-table-column label="适用老师" min-width="120">
|
|
|
+ <template #default="{ row: d }">{{ teacherName(d.teacherId) }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="科目" width="80">
|
|
|
+ <template #default="{ row: d }">{{ classifyText[d.classifyId] ?? d.classifyId }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="年级/学科" width="110">
|
|
|
+ <template #default="{ row: d }">
|
|
|
+ {{ d.gradeId === -1 ? '不限' : `年级${d.gradeId}` }} / {{ d.subjectId === -1 ? '不限' : `学科${d.subjectId}` }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="持证" width="70">
|
|
|
+ <template #default="{ row: d }">{{ d.teacherCert === 0 ? '不限' : d.teacherCert }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="教龄(年)" width="110">
|
|
|
+ <template #default="{ row: d }">{{ d.teacherExpMin }} ~ {{ d.teacherExpMax }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="percentage" label="比例(%)" width="90" />
|
|
|
+ <el-table-column label="下限/上限" width="150">
|
|
|
+ <template #default="{ row: d }">{{ d.min }} / {{ d.max }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="reward" label="额外" width="90" />
|
|
|
+ <el-table-column prop="displayName" label="计算方式名" min-width="140" />
|
|
|
+ <el-table-column label="档位状态" width="90">
|
|
|
+ <template #default="{ row: d }">{{ d.detailActived === 1 ? '启用' : '停用' }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- ② 方案编辑对话框(上半方案表单 + 下半明细表格) -->
|
|
|
+ <el-dialog v-model="editVisible" :title="editingId === null ? '新建课酬方案' : `编辑方案 #${editingId}`" width="980px">
|
|
|
+ <el-form label-width="90px" class="payroll-page__form">
|
|
|
+ <el-row :gutter="12">
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item label="方案名">
|
|
|
+ <el-input v-model="planForm.schemeName" placeholder="如:2026 秋季课酬方案" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <el-form-item label="校区">
|
|
|
+ <el-select v-model="planForm.campusId" style="width: 100%">
|
|
|
+ <el-option :value="0" label="机构级(全校区)" />
|
|
|
+ <el-option v-for="c in campusOpts" :key="c.campusId" :value="c.campusId" :label="c.campusName" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <el-form-item label="生效日">
|
|
|
+ <el-date-picker v-model="planForm.effectDate" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="4">
|
|
|
+ <el-form-item label="方案启停">
|
|
|
+ <el-switch v-model="planForm.actived" :active-value="1" :inactive-value="0" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-form>
|
|
|
+
|
|
|
+ <div class="payroll-page__detail-head">
|
|
|
+ <span>明细(保存时整体替换;老师留空 = 通用档)</span>
|
|
|
+ <el-button size="small" type="primary" @click="addDetail">加一行</el-button>
|
|
|
+ </div>
|
|
|
+ <el-table :data="detailRows" size="small" border max-height="360">
|
|
|
+ <el-table-column label="适用老师" min-width="140">
|
|
|
+ <template #default="{ row: d }">
|
|
|
+ <el-select v-model="d.teacherId" size="small" clearable placeholder="通用档" style="width: 100%">
|
|
|
+ <el-option v-for="t in teacherOpts" :key="t.userId" :value="t.userId" :label="t.realName" />
|
|
|
+ </el-select>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="科目" width="100">
|
|
|
+ <template #default="{ row: d }">
|
|
|
+ <el-select v-model="d.classifyId" size="small" style="width: 100%">
|
|
|
+ <el-option v-for="k in CLASSIFIES" :key="k" :value="k" :label="classifyText[k]" />
|
|
|
+ </el-select>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="年级(-1不限)" width="100">
|
|
|
+ <template #default="{ row: d }">
|
|
|
+ <el-input-number v-model="d.gradeId" :min="-1" :max="12" size="small" controls-position="right" style="width: 88px" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="学科(-1不限)" width="100">
|
|
|
+ <template #default="{ row: d }">
|
|
|
+ <el-input-number v-model="d.subjectId" :min="-1" :max="12" size="small" controls-position="right" style="width: 88px" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="持证(0不限)" width="96">
|
|
|
+ <template #default="{ row: d }">
|
|
|
+ <el-input-number v-model="d.teacherCert" :min="0" :max="1" size="small" controls-position="right" style="width: 84px" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="教龄下限" width="96">
|
|
|
+ <template #default="{ row: d }">
|
|
|
+ <el-input-number v-model="d.teacherExpMin" :min="0" :step="0.1" size="small" controls-position="right" style="width: 88px" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="教龄上限" width="96">
|
|
|
+ <template #default="{ row: d }">
|
|
|
+ <el-input-number v-model="d.teacherExpMax" :min="0" :step="0.1" size="small" controls-position="right" style="width: 88px" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="比例%" width="96">
|
|
|
+ <template #default="{ row: d }">
|
|
|
+ <el-input v-model="d.percentage" size="small" placeholder="85.0" style="width: 84px" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="下限金额" width="104">
|
|
|
+ <template #default="{ row: d }">
|
|
|
+ <el-input v-model="d.min" size="small" placeholder="0.00" style="width: 92px" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="上限金额" width="104">
|
|
|
+ <template #default="{ row: d }">
|
|
|
+ <el-input v-model="d.max" size="small" placeholder="0.00" style="width: 92px" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="额外" width="96">
|
|
|
+ <template #default="{ row: d }">
|
|
|
+ <el-input v-model="d.reward" size="small" placeholder="0.00" style="width: 84px" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="计算方式名" min-width="130">
|
|
|
+ <template #default="{ row: d }">
|
|
|
+ <el-input v-model="d.displayName" size="small" placeholder="如:数学 5~15 年教龄" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="档位启停" width="80">
|
|
|
+ <template #default="{ row: d }">
|
|
|
+ <el-switch v-model="d.detailActived" :active-value="1" :inactive-value="0" size="small" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="" width="60">
|
|
|
+ <template #default="{ $index }">
|
|
|
+ <el-button size="small" type="danger" link @click="removeDetail($index)">删</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="editVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" :loading="editBusy" @click="savePlan">保存(整体替换明细)</el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- ③ 课酬试算对话框(只回规则参数,不含最终金额;3112 引导配置) -->
|
|
|
+ <el-dialog v-model="quoteVisible" title="课酬试算" width="560px">
|
|
|
+ <el-form label-width="90px" class="payroll-page__form">
|
|
|
+ <el-form-item label="老师">
|
|
|
+ <el-select v-model="quoteForm.teacherId" style="width: 240px">
|
|
|
+ <el-option v-for="t in teacherOpts" :key="t.userId" :value="t.userId" :label="t.realName" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="科目">
|
|
|
+ <el-select v-model="quoteForm.classifyId" style="width: 160px">
|
|
|
+ <el-option v-for="k in CLASSIFIES" :key="k" :value="k" :label="classifyText[k]" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="教龄(年)">
|
|
|
+ <el-input-number v-model="quoteForm.teacherExp" :min="0" :step="1" style="width: 160px" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="日期">
|
|
|
+ <el-date-picker v-model="quoteForm.date" type="date" value-format="YYYY-MM-DD" style="width: 240px" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <el-alert v-if="quoteError" :title="quoteError" type="warning" show-icon class="payroll-page__quote-err" />
|
|
|
+ <el-descriptions v-else-if="quoteResult" :column="2" border size="small" class="payroll-page__quote">
|
|
|
+ <el-descriptions-item label="命中方案">#{{ quoteResult.schemeId }}({{ quoteResult.schemeScope === 'institution' ? '机构级' : '校区级' }})</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="规则档">{{ tierText[quoteResult.ruleTier] ?? quoteResult.ruleTier }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="比例">{{ quoteResult.percentage }}%</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="额外">{{ quoteResult.reward }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="下限">{{ quoteResult.min }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="上限">{{ quoteResult.max }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="明细 id">{{ quoteResult.detailId }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="方案校区">{{ quoteResult.schemeCampusId === 0 ? '机构级' : quoteResult.schemeCampusId }}</el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+ <el-alert
|
|
|
+ v-if="quoteResult"
|
|
|
+ title="试算只返回规则参数;最终课酬在下课时(端点 51)产生,勿把本结果当工资金额展示"
|
|
|
+ type="info"
|
|
|
+ :closable="false"
|
|
|
+ class="payroll-page__quote-hint"
|
|
|
+ />
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="quoteVisible = false">关闭</el-button>
|
|
|
+ <el-button type="primary" :loading="quoteBusy" @click="doQuote">试算</el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.payroll-page {
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+.payroll-page__form {
|
|
|
+ margin-bottom: 12px;
|
|
|
+}
|
|
|
+.payroll-page__plan {
|
|
|
+ margin-top: 12px;
|
|
|
+}
|
|
|
+.payroll-page__plan-head {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 6px;
|
|
|
+}
|
|
|
+.payroll-page__plan-title {
|
|
|
+ font-size: var(--app-font-md);
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+.payroll-page__details {
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+.payroll-page__detail-head {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ font-size: var(--app-font-md);
|
|
|
+ color: var(--app-text-secondary, #8a8f9c);
|
|
|
+}
|
|
|
+.payroll-page__quote-err,
|
|
|
+.payroll-page__quote-hint {
|
|
|
+ margin-bottom: 12px;
|
|
|
+}
|
|
|
+</style>
|