- 部门汇总全公司 5% 按每位有台账聚合行的营销经理各计一次, 团队应发小计取聚合额 + 调整净额;与月结 total_payable 对拍 (有调整单时 + adjust_net_total),分列舍入差单列 - 人员归属按行类型拆分(普通/经理个人户/仅调整单),调整单全量并入, 团队与未分配不重复;按经理筛选只汇总展示团队 - 导出改单 sheet 分块签字稿,审计覆盖取数与超限;筛选页眉 ID→名称 - 月份校验抽 core/month,报表/月结/调整单非法月份统一 400; 岗位只读选项供财务/领导筛选 - 前端修复:部门下钻带入筛选、回款多桶独立状态、打印截断判定、 summary 请求序号保护 Co-Authored-By: Claude Code <noreply@anthropic.com>
665 lines
23 KiB
Vue
665 lines
23 KiB
Vue
<script setup lang="ts">
|
||
import { computed, onMounted, reactive, ref } from 'vue'
|
||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||
|
||
import * as businessRoleApi from '@/api/businessRoles'
|
||
import * as personnelApi from '@/api/personnel'
|
||
import type { PersonnelItem } from '@/api/personnel'
|
||
import AppPagination from '@/components/table/AppPagination.vue'
|
||
import StatusTag from '@/components/table/StatusTag.vue'
|
||
import { useConfirmAction } from '@/composables/useConfirmAction'
|
||
import { usePagedList } from '@/composables/usePagedList'
|
||
import { useEditingRow } from '@/composables/useEditingRow'
|
||
import { AMOUNT_PATTERN } from '@/utils/validate'
|
||
import { handledError } from '@/utils/error'
|
||
|
||
/**
|
||
* 人员管理(需求 2.1.1:人员档案 + 辐射区域 + 拓展计划)。
|
||
*
|
||
* 与后端约定对齐:
|
||
* - 基本信息新增/编辑走弹窗(全量提交、不含状态);在职/离职切换、
|
||
* 删除均为独立操作,删除被引用时后端拒绝,此时应改为离职
|
||
* - 编辑抽屉承载子表:辐射区域与拓展计划行内增删,保存走
|
||
* PUT /{id}/regions、PUT /{id}/plans 全量替换(空列表即清空)
|
||
* - 工资标准为字符串直出(金额红线:不经 float、不 toFixed)
|
||
*/
|
||
const { confirmAndRun } = useConfirmAction()
|
||
|
||
// ==================== 列表 ====================
|
||
|
||
/** 在职/离职筛选项(与产品页「启用/禁用」语义不同,页面内定义) */
|
||
const EMPLOYMENT_OPTIONS = [
|
||
{ label: '在职', value: 1 },
|
||
{ label: '离职', value: 0 },
|
||
] as const
|
||
|
||
const filters = reactive({
|
||
keyword: '',
|
||
status: undefined as number | undefined,
|
||
business_role_id: undefined as number | undefined,
|
||
})
|
||
|
||
const { records, total, pageNum, pageSize, loading, load, search, onPageChange, onSizeChange, reloadAfterDelete } =
|
||
usePagedList<PersonnelItem, personnelApi.PersonnelQueryParams>(
|
||
personnelApi.listPersonnel,
|
||
() => ({
|
||
keyword: filters.keyword,
|
||
status: filters.status,
|
||
business_role_id: filters.business_role_id,
|
||
}),
|
||
)
|
||
|
||
function resetFilters(): void {
|
||
filters.keyword = ''
|
||
filters.status = undefined
|
||
filters.business_role_id = undefined
|
||
search()
|
||
}
|
||
|
||
// ==================== 岗位 / 经理下拉数据源 ====================
|
||
|
||
interface SelectOption {
|
||
id: number
|
||
name: string
|
||
status: number
|
||
}
|
||
|
||
const roleOptions = ref<SelectOption[]>([])
|
||
const managerOptions = ref<SelectOption[]>([])
|
||
|
||
async function loadRoleOptions(): Promise<void> {
|
||
try {
|
||
// 岗位规模有限(内置 3 个 + 自定义),选项接口一次取全
|
||
const data = await businessRoleApi.listBusinessRoleOptions()
|
||
roleOptions.value = data.map((r) => ({ id: r.id, name: r.role_name, status: r.status }))
|
||
} catch (error) {
|
||
handledError(error) // 下拉为空不阻断列表使用
|
||
}
|
||
}
|
||
|
||
async function loadManagerOptions(): Promise<void> {
|
||
try {
|
||
// 经理下拉的近似全量数据源:内部系统人员规模有限,200 条足够;
|
||
// 超出规模的极端场景由后端对经理合法性的校验兜底
|
||
const data = await personnelApi.listPersonnel({ pageNum: 1, pageSize: 200 })
|
||
managerOptions.value = data.list.map((p) => ({ id: p.id, name: p.personnel_name, status: p.status }))
|
||
} catch (error) {
|
||
handledError(error)
|
||
}
|
||
}
|
||
|
||
// ==================== 新增 / 编辑(基本信息) ====================
|
||
|
||
const dialogVisible = ref(false)
|
||
const editingId = ref<number | null>(null)
|
||
/** 单一状态源:编辑中 ⇔ editingId 非空(避免双状态漂移) */
|
||
const isEdit = computed(() => editingId.value !== null)
|
||
const formRef = ref<FormInstance>()
|
||
const submitting = ref(false)
|
||
|
||
const form = reactive({
|
||
personnel_name: '',
|
||
business_role_id: undefined as number | undefined,
|
||
manager_id: undefined as number | undefined,
|
||
phone: '',
|
||
hire_date: '',
|
||
is_probation: 0,
|
||
salary_standard: '',
|
||
remark: '',
|
||
})
|
||
|
||
/** 编辑中的行(用于停用岗位/离职经理的回显兜底) */
|
||
const editingRow = useEditingRow(records, editingId)
|
||
|
||
/** 搜索下拉显示全部岗位(含停用,便于筛出仍绑定停用岗位的人员) */
|
||
const searchRoleOptions = computed(() =>
|
||
roleOptions.value.map((r) => ({ ...r, name: r.status === 1 ? r.name : `${r.name}(已停用)` })),
|
||
)
|
||
|
||
/** 表单下拉只列启用岗位;编辑时若原岗位已停用则保留一项供回显 */
|
||
const formRoleOptions = computed(() => {
|
||
const enabled = roleOptions.value.filter((r) => r.status === 1)
|
||
const row = editingRow.value
|
||
if (row && !enabled.some((r) => r.id === row.business_role_id)) {
|
||
enabled.unshift({ id: row.business_role_id, name: `${row.role_name ?? ''}(已停用)`, status: 0 })
|
||
}
|
||
return enabled
|
||
})
|
||
|
||
/** 经理下拉:排除编辑中的人员自己(后端校验「经理不能是自己」) */
|
||
const formManagerOptions = computed(() =>
|
||
managerOptions.value.filter((m) => m.id !== editingId.value),
|
||
)
|
||
|
||
const rules = computed<FormRules>(() => ({
|
||
personnel_name: [
|
||
{ required: true, message: '请输入姓名', trigger: 'blur' },
|
||
{ max: 64, message: '姓名最长 64 个字符', trigger: 'blur' },
|
||
],
|
||
business_role_id: [{ required: true, message: '请选择岗位', trigger: 'change' }],
|
||
phone: [{ max: 20, message: '联系电话最长 20 个字符', trigger: 'blur' }],
|
||
salary_standard: [
|
||
{ pattern: AMOUNT_PATTERN, message: '工资标准须为非负数字,最多两位小数', trigger: 'blur' },
|
||
],
|
||
remark: [{ max: 255, message: '备注最长 255 个字符', trigger: 'blur' }],
|
||
}))
|
||
|
||
function openCreate(): void {
|
||
editingId.value = null
|
||
Object.assign(form, {
|
||
personnel_name: '',
|
||
business_role_id: undefined,
|
||
manager_id: undefined,
|
||
phone: '',
|
||
hire_date: '',
|
||
is_probation: 0,
|
||
salary_standard: '',
|
||
remark: '',
|
||
})
|
||
dialogVisible.value = true
|
||
formRef.value?.clearValidate()
|
||
}
|
||
|
||
function openEdit(row: PersonnelItem): void {
|
||
editingId.value = row.id
|
||
Object.assign(form, {
|
||
personnel_name: row.personnel_name,
|
||
business_role_id: row.business_role_id,
|
||
manager_id: row.manager_id ?? undefined,
|
||
phone: row.phone ?? '',
|
||
hire_date: row.hire_date ?? '',
|
||
is_probation: row.is_probation,
|
||
salary_standard: row.salary_standard ?? '',
|
||
remark: row.remark ?? '',
|
||
})
|
||
dialogVisible.value = true
|
||
formRef.value?.clearValidate()
|
||
}
|
||
|
||
async function handleSubmit(): Promise<void> {
|
||
const valid = await formRef.value?.validate().catch(() => false)
|
||
if (!valid) return
|
||
|
||
// 空串归一为 null(与后端「空串归一 NULL」口径一致);金额原串提交
|
||
const payload: personnelApi.PersonnelSaveParams = {
|
||
personnel_name: form.personnel_name.trim(),
|
||
business_role_id: form.business_role_id as number,
|
||
manager_id: form.manager_id ?? null,
|
||
phone: form.phone.trim() || null,
|
||
hire_date: form.hire_date || null,
|
||
is_probation: form.is_probation,
|
||
salary_standard: form.salary_standard.trim() || null,
|
||
remark: form.remark.trim() || null,
|
||
}
|
||
|
||
submitting.value = true
|
||
try {
|
||
if (editingId.value !== null) {
|
||
await personnelApi.updatePersonnel(editingId.value, payload)
|
||
ElMessage.success('已保存')
|
||
} else {
|
||
await personnelApi.createPersonnel(payload)
|
||
ElMessage.success('人员已创建')
|
||
}
|
||
dialogVisible.value = false
|
||
await load()
|
||
} catch (error) {
|
||
handledError(error) // 失败提示由请求拦截器统一完成
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
|
||
// ==================== 编辑抽屉:辐射区域 / 拓展计划 ====================
|
||
|
||
interface RegionDraft {
|
||
region_name: string
|
||
remark: string
|
||
}
|
||
|
||
interface PlanDraft {
|
||
plan_period: string
|
||
plan_content: string
|
||
target_desc: string
|
||
}
|
||
|
||
const drawerVisible = ref(false)
|
||
const drawerRow = ref<PersonnelItem | null>(null)
|
||
const drawerTitle = computed(() =>
|
||
drawerRow.value ? `「${drawerRow.value.personnel_name}」· 区域与拓展计划` : '区域与拓展计划',
|
||
)
|
||
const detailLoading = ref(false)
|
||
const regionSaving = ref(false)
|
||
const planSaving = ref(false)
|
||
|
||
/** 本地草稿行(全量替换:保存即以后端返回为准,无需保留行 id) */
|
||
const regionRows = ref<RegionDraft[]>([])
|
||
const planRows = ref<PlanDraft[]>([])
|
||
|
||
async function openSubTables(row: PersonnelItem): Promise<void> {
|
||
drawerRow.value = row
|
||
drawerVisible.value = true
|
||
detailLoading.value = true
|
||
regionRows.value = []
|
||
planRows.value = []
|
||
try {
|
||
const detail = await personnelApi.getPersonnelDetail(row.id)
|
||
regionRows.value = detail.regions.map((r) => ({
|
||
region_name: r.region_name,
|
||
remark: r.remark ?? '',
|
||
}))
|
||
planRows.value = detail.plans.map((p) => ({
|
||
plan_period: p.plan_period,
|
||
plan_content: p.plan_content,
|
||
target_desc: p.target_desc ?? '',
|
||
}))
|
||
} catch (error) {
|
||
handledError(error) // 失败提示由请求拦截器统一完成,抽屉显示空表
|
||
} finally {
|
||
detailLoading.value = false
|
||
}
|
||
}
|
||
|
||
function addRegionRow(): void {
|
||
regionRows.value.push({ region_name: '', remark: '' })
|
||
}
|
||
|
||
function removeRegionRow(index: number): void {
|
||
regionRows.value.splice(index, 1)
|
||
}
|
||
|
||
function addPlanRow(): void {
|
||
planRows.value.push({ plan_period: '', plan_content: '', target_desc: '' })
|
||
}
|
||
|
||
function removePlanRow(index: number): void {
|
||
planRows.value.splice(index, 1)
|
||
}
|
||
|
||
/** 保存辐射区域:全量替换,未填名称的行不能混入 */
|
||
async function saveRegions(): Promise<void> {
|
||
const row = drawerRow.value
|
||
if (!row) return
|
||
const blank = regionRows.value.findIndex((r) => !r.region_name.trim())
|
||
if (blank >= 0) {
|
||
ElMessage.warning(`第 ${blank + 1} 行的区域名称未填写`)
|
||
return
|
||
}
|
||
|
||
regionSaving.value = true
|
||
try {
|
||
await personnelApi.replacePersonnelRegions(
|
||
row.id,
|
||
regionRows.value.map((r) => ({
|
||
region_name: r.region_name.trim(),
|
||
remark: r.remark.trim() || null,
|
||
})),
|
||
)
|
||
ElMessage.success('辐射区域已保存')
|
||
} catch (error) {
|
||
handledError(error)
|
||
} finally {
|
||
regionSaving.value = false
|
||
}
|
||
}
|
||
|
||
/** 保存拓展计划:期间与内容必填,指标说明可空 */
|
||
async function savePlans(): Promise<void> {
|
||
const row = drawerRow.value
|
||
if (!row) return
|
||
const blank = planRows.value.findIndex((r) => !r.plan_period.trim() || !r.plan_content.trim())
|
||
if (blank >= 0) {
|
||
ElMessage.warning(`第 ${blank + 1} 行的计划期间或内容未填写`)
|
||
return
|
||
}
|
||
|
||
planSaving.value = true
|
||
try {
|
||
await personnelApi.replacePersonnelPlans(
|
||
row.id,
|
||
planRows.value.map((r) => ({
|
||
plan_period: r.plan_period.trim(),
|
||
plan_content: r.plan_content.trim(),
|
||
target_desc: r.target_desc.trim() || null,
|
||
})),
|
||
)
|
||
ElMessage.success('拓展计划已保存')
|
||
} catch (error) {
|
||
handledError(error)
|
||
} finally {
|
||
planSaving.value = false
|
||
}
|
||
}
|
||
|
||
// ==================== 在职切换 / 删除 ====================
|
||
|
||
async function handleToggleStatus(row: PersonnelItem): Promise<void> {
|
||
const next = row.status === 1 ? 0 : 1
|
||
const action = next === 1 ? '设为在职' : '设为离职'
|
||
await confirmAndRun({
|
||
confirmText:
|
||
next === 0
|
||
? `确定将「${row.personnel_name}」设为离职?离职人员不可再被业务与客户归属引用。`
|
||
: `确定将「${row.personnel_name}」设为在职?`,
|
||
title: '状态确认',
|
||
confirmButtonText: action,
|
||
successText: `已${action}`,
|
||
run: () => personnelApi.setPersonnelStatus(row.id, next),
|
||
after: load,
|
||
})
|
||
}
|
||
|
||
async function handleDelete(row: PersonnelItem): Promise<void> {
|
||
await confirmAndRun({
|
||
confirmText: `确定删除人员「${row.personnel_name}」?删除为物理删除、不可恢复;若该人员已被团队成员或客户归属引用将无法删除,此时应改为离职。`,
|
||
title: '删除确认',
|
||
confirmButtonText: '删除',
|
||
successText: '人员已删除',
|
||
run: () => personnelApi.deletePersonnel(row.id),
|
||
after: reloadAfterDelete,
|
||
})
|
||
}
|
||
|
||
// ==================== 初始化 ====================
|
||
|
||
onMounted(() => {
|
||
// 三个请求各自容错(load 内部已兜底),互不拖累
|
||
void load()
|
||
void loadRoleOptions()
|
||
void loadManagerOptions()
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div>
|
||
<el-card shadow="never">
|
||
<el-form class="filter-form" :inline="true" @submit.prevent>
|
||
<el-form-item label="姓名">
|
||
<el-input
|
||
v-model="filters.keyword"
|
||
placeholder="姓名,模糊匹配"
|
||
clearable
|
||
style="width: 180px"
|
||
@keyup.enter="search"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="状态">
|
||
<el-select v-model="filters.status" placeholder="全部" clearable style="width: 110px">
|
||
<el-option
|
||
v-for="item in EMPLOYMENT_OPTIONS"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="岗位">
|
||
<el-select
|
||
v-model="filters.business_role_id"
|
||
placeholder="全部"
|
||
clearable
|
||
filterable
|
||
style="width: 160px"
|
||
>
|
||
<el-option
|
||
v-for="item in searchRoleOptions"
|
||
:key="item.id"
|
||
:label="item.name"
|
||
:value="item.id"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" @click="search">查询</el-button>
|
||
<el-button @click="resetFilters">重置</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
|
||
<div class="toolbar">
|
||
<el-button type="primary" @click="openCreate">新增人员</el-button>
|
||
</div>
|
||
|
||
<el-table v-loading="loading" :data="records" stripe>
|
||
<el-table-column prop="personnel_name" label="姓名" min-width="100" />
|
||
<el-table-column prop="role_name" label="岗位" min-width="100">
|
||
<template #default="{ row }">{{ row.role_name ?? '—' }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="manager_name" label="直属经理" min-width="100">
|
||
<template #default="{ row }">{{ row.manager_name ?? '—' }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="phone" label="联系电话" min-width="120">
|
||
<template #default="{ row }">{{ row.phone ?? '—' }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="hire_date" label="入职日期" min-width="110">
|
||
<template #default="{ row }">{{ row.hire_date ?? '—' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="试用期" width="90" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag v-if="row.is_probation === 1" type="warning" size="small" disable-transitions>
|
||
试用期
|
||
</el-tag>
|
||
<span v-else class="cell-muted">正式</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="工资标准(元)" min-width="120" align="right">
|
||
<template #default="{ row }">
|
||
<!-- 字符串直出:金额红线,禁 toFixed / float 格式化 -->
|
||
<span class="cell-amount">{{ row.salary_standard ?? '—' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="状态" width="90" align="center">
|
||
<template #default="{ row }">
|
||
<StatusTag :value="row.status" active-text="在职" inactive-text="离职" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="270" fixed="right" align="center">
|
||
<template #default="{ row }">
|
||
<div class="table-actions">
|
||
<el-button link type="primary" size="small" @click="openEdit(row)">编辑</el-button>
|
||
<el-button link type="primary" size="small" @click="openSubTables(row)">
|
||
区域与计划
|
||
</el-button>
|
||
<el-button link type="warning" size="small" @click="handleToggleStatus(row)">
|
||
{{ row.status === 1 ? '设为离职' : '设为在职' }}
|
||
</el-button>
|
||
<span class="table-actions__divider" aria-hidden="true"></span>
|
||
<el-button link type="danger" size="small" @click="handleDelete(row)">删除</el-button>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<AppPagination
|
||
:total="total"
|
||
:page-num="pageNum"
|
||
:page-size="pageSize"
|
||
@page-change="onPageChange"
|
||
@size-change="onSizeChange"
|
||
/>
|
||
</el-card>
|
||
|
||
<!-- 新增 / 编辑(基本信息;不含状态与子表) -->
|
||
<el-dialog
|
||
v-model="dialogVisible"
|
||
:title="isEdit ? '编辑人员' : '新增人员'"
|
||
width="560px"
|
||
destroy-on-close
|
||
>
|
||
<el-form ref="formRef" :model="form" :rules="rules" label-width="90px">
|
||
<el-form-item label="姓名" prop="personnel_name">
|
||
<el-input v-model="form.personnel_name" placeholder="营销人员姓名" />
|
||
</el-form-item>
|
||
<el-form-item label="岗位" prop="business_role_id">
|
||
<el-select
|
||
v-model="form.business_role_id"
|
||
placeholder="选择岗位(计提公式随岗位绑定)"
|
||
filterable
|
||
style="width: 100%"
|
||
>
|
||
<el-option
|
||
v-for="item in formRoleOptions"
|
||
:key="item.id"
|
||
:label="item.name"
|
||
:value="item.id"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="直属经理" prop="manager_id">
|
||
<el-select
|
||
v-model="form.manager_id"
|
||
placeholder="不选择=无直属经理"
|
||
clearable
|
||
filterable
|
||
style="width: 100%"
|
||
>
|
||
<el-option
|
||
v-for="item in formManagerOptions"
|
||
:key="item.id"
|
||
:label="item.name"
|
||
:value="item.id"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="联系电话" prop="phone">
|
||
<el-input v-model="form.phone" maxlength="20" placeholder="选填" />
|
||
</el-form-item>
|
||
<el-form-item label="入职日期" prop="hire_date">
|
||
<el-date-picker
|
||
v-model="form.hire_date"
|
||
type="date"
|
||
value-format="YYYY-MM-DD"
|
||
placeholder="选择日期"
|
||
style="width: 100%"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="试用期" prop="is_probation">
|
||
<el-switch v-model="form.is_probation" :active-value="1" :inactive-value="0" />
|
||
<span class="switch-hint">试用期绩效照算但不发放,年累计超 18000 元部分年底一次性结算</span>
|
||
</el-form-item>
|
||
<el-form-item label="工资标准" prop="salary_standard">
|
||
<el-input v-model="form.salary_standard" placeholder="如 5000.50(月,元),选填" />
|
||
</el-form-item>
|
||
<el-form-item label="备注" prop="remark">
|
||
<el-input v-model="form.remark" type="textarea" :rows="2" maxlength="255" placeholder="选填" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="dialogVisible = false">取消</el-button>
|
||
<el-button type="primary" :loading="submitting" @click="handleSubmit">保存</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 编辑抽屉:辐射区域 / 拓展计划(行内增删,全量替换保存) -->
|
||
<el-drawer v-model="drawerVisible" :title="drawerTitle" size="720px" destroy-on-close>
|
||
<div v-loading="detailLoading">
|
||
<div class="sub-section">
|
||
<div class="sub-section-head">
|
||
<span class="sub-section-title">辐射区域</span>
|
||
<span>
|
||
<el-button size="small" @click="addRegionRow">添加区域</el-button>
|
||
<el-button
|
||
size="small"
|
||
type="primary"
|
||
:loading="regionSaving"
|
||
@click="saveRegions"
|
||
>
|
||
保存区域
|
||
</el-button>
|
||
</span>
|
||
</div>
|
||
<el-table :data="regionRows" stripe size="small">
|
||
<el-table-column label="区域名称" min-width="200">
|
||
<template #default="{ row }">
|
||
<el-input v-model="row.region_name" maxlength="128" placeholder="如 沈阳市铁西区" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="备注" min-width="220">
|
||
<template #default="{ row }">
|
||
<el-input v-model="row.remark" maxlength="255" placeholder="选填" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="80" align="center">
|
||
<template #default="{ $index }">
|
||
<el-button link type="danger" size="small" @click="removeRegionRow($index)">
|
||
删除
|
||
</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
|
||
<div class="sub-section">
|
||
<div class="sub-section-head">
|
||
<span class="sub-section-title">拓展计划</span>
|
||
<span>
|
||
<el-button size="small" @click="addPlanRow">添加计划</el-button>
|
||
<el-button size="small" type="primary" :loading="planSaving" @click="savePlans">
|
||
保存计划
|
||
</el-button>
|
||
</span>
|
||
</div>
|
||
<el-table :data="planRows" stripe size="small">
|
||
<el-table-column label="期间" min-width="110">
|
||
<template #default="{ row }">
|
||
<el-input v-model="row.plan_period" maxlength="32" placeholder="如 2026Q1" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="计划内容" min-width="240">
|
||
<template #default="{ row }">
|
||
<el-input v-model="row.plan_content" maxlength="500" placeholder="计划内容" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="指标说明" min-width="160">
|
||
<template #default="{ row }">
|
||
<el-input v-model="row.target_desc" maxlength="255" placeholder="选填" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="80" align="center">
|
||
<template #default="{ $index }">
|
||
<el-button link type="danger" size="small" @click="removePlanRow($index)">
|
||
删除
|
||
</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
</div>
|
||
</el-drawer>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.cell-muted {
|
||
color: var(--ys-ink-3);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.cell-amount {
|
||
font-family: 'SFMono-Regular', Consolas, 'Courier New', monospace;
|
||
font-size: 12px;
|
||
color: var(--ys-ink-2);
|
||
}
|
||
|
||
.switch-hint {
|
||
margin-left: 10px;
|
||
color: var(--ys-ink-3);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.sub-section {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.sub-section-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.sub-section-title {
|
||
font-weight: 600;
|
||
color: var(--ys-ink);
|
||
}
|
||
</style>
|