Files
PayrollManagementSystem/frontend/src/views/system/params/index.vue
T
liuwenxinandClaude Code 0ef3adec21 refactor(frontend): 金额正则/分页/抽屉状态/行编辑与下拉收口
- 金额形态正则收进 utils/validate.ts 单点导出(此前四页各写,精度
  调整漏改一页即「A 页拦得住 B 页放行后端 400」或历史行无法编辑)
- params 版本/历史两抽屉的手写分页五件套换 usePagedList(白得请求
  序号防竞态——快速翻页旧响应覆盖新页的手写坑不再复现)
- 业务抽屉三份 id 状态收为 drawerRowId computed(单一状态源,消除
  「标题 A 业务、留痕表 B 业务」的失同步面)
- useEditingRow(三页逐字相同的 computed)与 useDropdownOptions
  (取数失败不阻断列表的骨架)收口,映射函数由调用方给出
- e2e 冒烟:基础数据 13 步 + 业务全链路 16 步全过

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-15 10:08:24 +08:00

988 lines
31 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import * as businessRoleApi from '@/api/businessRoles'
import * as paramsApi from '@/api/params'
import type {
ParamBatch,
ParamBatchDetail,
ParamCatalogItem,
ParamDiff,
ParamHistory,
ParamPreviewResult,
} from '@/api/params'
import AppPagination from '@/components/table/AppPagination.vue'
import { useConfirmAction } from '@/composables/useConfirmAction'
import { usePagedList } from '@/composables/usePagedList'
import { handledError } from '@/utils/error'
/**
* 计提参数配置(需求 #28)。
*
* 与后端约定对齐:
* - 目录按「分组 → 参数 → 维度值」展示当前生效值;修改先进「待提交」,
* 以批次提交(一次一批,逐项留痕旧值/新值)
* - 支持立即生效或指定日期生效;未生效批次可整批撤回
* - 图表/导出:统计用 ECharts(用户指定),导出为 xlsx 文件流
*/
const { confirmAndRun } = useConfirmAction()
// ==================== 目录 ====================
const catalog = ref<ParamCatalogItem[]>([])
const catalogLoading = ref(false)
async function loadCatalog(): Promise<void> {
catalogLoading.value = true
try {
catalog.value = await paramsApi.getCatalog()
} catch (error) {
handledError(error)
} finally {
catalogLoading.value = false
}
}
/** 分组后的目录(保持后端 sort_order 顺序) */
const groupedCatalog = computed(() => {
const groups = new Map<string, ParamCatalogItem[]>()
for (const item of catalog.value) {
groups.set(item.param_group, [...(groups.get(item.param_group) ?? []), item])
}
return [...groups.entries()].map(([name, items]) => ({ name, items }))
})
/** 维度值 → 展示名(NONE 维度为空串) */
const DIMENSION_LABELS: Record<string, string> = {
'': '默认',
municipal_water: '市政水利',
rail_transit: '轨道交通',
}
function dimensionLabel(dimension: string): string {
return DIMENSION_LABELS[dimension] ?? dimension
}
/** 带单位的值展示 */
function formatValue(item: ParamCatalogItem, value: string): string {
return item.unit ? `${value} ${item.unit}` : value
}
// ==================== 修改(待提交) ====================
interface PendingChange {
item: ParamCatalogItem
dimension_value: string
old_value: string
new_value: string
}
const pending = reactive<Record<string, PendingChange>>({})
const pendingList = computed(() => Object.entries(pending).map(([key, change]) => ({ key, ...change })))
function pendingKey(paramCode: string, dimension: string): string {
return `${paramCode}|${dimension}`
}
function pendingOf(item: ParamCatalogItem, dimension: string): PendingChange | undefined {
return pending[pendingKey(item.param_code, dimension)]
}
const editVisible = ref(false)
const editTarget = ref<{ item: ParamCatalogItem; dimension: string } | null>(null)
const editValue = ref('')
/** 可选维度:该参数已有维度值(无则视为默认) */
const editDimensions = computed(() => {
if (!editTarget.value) return []
const dims = new Set<string>(editTarget.value.item.values.map((v) => v.dimension_value))
if (dims.size === 0) dims.add('')
return [...dims]
})
function currentValue(item: ParamCatalogItem, dimension: string): string | undefined {
return item.values.find((v) => v.dimension_value === dimension)?.value_text
}
function openEdit(item: ParamCatalogItem, dimension: string): void {
editTarget.value = { item, dimension }
editValue.value = currentValue(item, dimension) ?? item.default_value ?? ''
editVisible.value = true
}
function confirmEdit(): void {
if (!editTarget.value) return
const { item, dimension } = editTarget.value
const value = editValue.value.trim()
if (!value) {
ElMessage.warning('请输入新值')
return
}
const old = currentValue(item, dimension) ?? item.default_value ?? ''
if (value === old) {
ElMessage.info('新值与当前值相同,未加入待提交')
return
}
pending[pendingKey(item.param_code, dimension)] = {
item,
dimension_value: dimension,
old_value: old,
new_value: value,
}
editVisible.value = false
ElMessage.success('已加入待提交,提交前不会生效')
}
function removePending(key: string): void {
delete pending[key]
}
// ==================== 提交批次 ====================
const changeType = ref<1 | 2>(1)
const effectiveDate = ref<string>('')
const remark = ref('')
const submitting = ref(false)
const effectiveText = computed(() =>
changeType.value === 1 ? '立即生效' : `${effectiveDate.value} 起生效`,
)
async function submitAll(): Promise<void> {
if (changeType.value === 2 && !effectiveDate.value) {
ElMessage.warning('请选择生效日期')
return
}
try {
await ElMessageBox.confirm(
`共 ${pendingList.value.length} 项变更,${effectiveText.value}。提交后逐项留痕,未生效前可整批撤回。`,
'提交确认',
{ confirmButtonText: '提交', cancelButtonText: '再想想', type: 'warning' },
)
} catch {
return
}
submitting.value = true
try {
const batch = await paramsApi.submitChanges({
change_type: changeType.value,
effective_date: changeType.value === 2 ? effectiveDate.value : undefined,
remark: remark.value || undefined,
items: pendingList.value.map((p) => ({
param_code: p.item.param_code,
dimension_value: p.dimension_value,
value: p.new_value,
})),
})
ElMessage.success(`已提交,版本 ${batch.version_no}`)
Object.keys(pending).forEach((key) => delete pending[key])
remark.value = ''
await loadCatalog()
} catch (error) {
handledError(error)
} finally {
submitting.value = false
}
}
// ==================== 版本管理抽屉 ====================
const versionVisible = ref(false)
const detailCache = reactive<Record<number, ParamBatchDetail>>({})
// 分页五件套换 usePagedList:手写版没有请求序号防竞态(快速翻页时旧响应
// 后到会覆盖新页数据)与页码回退——正是该 composable 注释里写明已修过的坑
const {
records: batches,
total: batchesTotal,
pageNum: batchesPage,
pageSize: batchesPageSize,
loading: batchesLoading,
load: loadBatches,
onPageChange: onBatchesPageChange,
onSizeChange: onBatchesSizeChange,
} = usePagedList<ParamBatch, Record<string, never>>(paramsApi.listBatches, () => ({}))
async function detailOf(batch: ParamBatch): Promise<ParamBatchDetail> {
if (detailCache[batch.id]) return detailCache[batch.id]
const detail = await paramsApi.getBatchDetail(batch.id)
detailCache[batch.id] = detail
return detail
}
function changeTypeText(changeType: number): string {
return changeType === 1 ? '立即生效' : '指定日期'
}
/** 本地日期(YYYY-MM-DD):toISOString 是 UTC,北京时间 0–8 点会差一天 */
function localToday(): string {
const d = new Date()
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
}
async function handleWithdraw(batch: ParamBatch): Promise<void> {
await confirmAndRun({
confirmText: `确定撤回批次「${batch.version_no}」(${batch.item_count} 项,${batch.effective_date} 起生效)?撤回后这些变更不再生效。`,
title: '撤回确认',
confirmButtonText: '撤回',
successText: '批次已撤回',
run: () => paramsApi.withdrawBatch(batch.id),
after: () => {
loadBatches()
void loadCatalog()
},
})
}
// ---------- 对比 ----------
const compareFrom = ref('')
const compareTo = ref('')
const comparing = ref(false)
const diffResult = ref<ParamDiff[]>([])
const diffSearched = ref(false)
async function doCompare(): Promise<void> {
if (!compareFrom.value || !compareTo.value) {
ElMessage.warning('请选择两个对比日期')
return
}
comparing.value = true
try {
diffResult.value = await paramsApi.compareParams(compareFrom.value, compareTo.value)
diffSearched.value = true
} catch (error) {
handledError(error)
} finally {
comparing.value = false
}
}
// ---------- 导出 ----------
const exporting = ref(false)
async function doExport(): Promise<void> {
exporting.value = true
try {
const blob = await paramsApi.exportChanges({
start_date: compareFrom.value || undefined,
end_date: compareTo.value || undefined,
})
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `param_changes_${localToday()}.xlsx`
link.click()
URL.revokeObjectURL(url)
ElMessage.success('已导出')
} catch (error) {
handledError(error)
} finally {
exporting.value = false
}
}
function openVersionDrawer(): void {
versionVisible.value = true
batchesPage.value = 1
void loadBatches()
}
// ==================== 历史抽屉 ====================
const historyVisible = ref(false)
const historyItem = ref<ParamCatalogItem | null>(null)
// 历史抽屉分页同样收口 usePagedList;fetcher 闭包当前参数的编码
const {
records: historyRows,
total: historyTotal,
pageNum: historyPage,
pageSize: historyPageSize,
loading: historyLoading,
load: loadHistory,
onPageChange: onHistoryPageChange,
onSizeChange: onHistorySizeChange,
} = usePagedList<ParamHistory, Record<string, never>>(
(params) => paramsApi.getParamHistory(historyItem.value!.param_code, params),
() => ({}),
)
function openHistory(item: ParamCatalogItem): void {
historyItem.value = item
historyVisible.value = true
historyPage.value = 1
void loadHistory()
}
// ==================== 计算预览抽屉 ====================
const previewVisible = ref(false)
const previewLoading = ref(false)
const previewResult = ref<ParamPreviewResult | null>(null)
const roleOptions = ref<businessRoleApi.BusinessRoleItem[]>([])
const previewForm = reactive({
business_role: 'salesperson',
customer_type: 'personal',
product_category: 'municipal_water',
ship_date: '',
receipt_date: '',
revenue: '',
profit: '',
received_amount: '',
barter: false,
probation: false,
})
const PRODUCT_OPTIONS = [
{ label: '市政水利', value: 'municipal_water' },
{ label: '轨道交通', value: 'rail_transit' },
]
const CUSTOMER_OPTIONS = [
{ label: '个人户', value: 'personal' },
{ label: '企业户', value: 'enterprise' },
]
async function loadRoleOptions(): Promise<void> {
try {
const page = await businessRoleApi.listBusinessRoles({ pageNum: 1, pageSize: 100 })
roleOptions.value = page.list
} catch (error) {
handledError(error)
}
}
function openPreviewDrawer(): void {
previewVisible.value = true
if (roleOptions.value.length === 0) void loadRoleOptions()
}
async function runPreview(): Promise<void> {
if (!previewForm.ship_date || !previewForm.receipt_date) {
ElMessage.warning('请选择发货日与回款到账日')
return
}
previewLoading.value = true
try {
previewResult.value = await paramsApi.previewCalc({
business_role: previewForm.business_role || undefined,
customer_type: previewForm.customer_type || undefined,
product_category: previewForm.product_category || undefined,
ship_date: previewForm.ship_date,
receipt_date: previewForm.receipt_date,
revenue: previewForm.revenue,
profit: previewForm.profit,
received_amount: previewForm.received_amount,
barter: previewForm.barter,
probation: previewForm.probation,
})
} catch (error) {
handledError(error)
} finally {
previewLoading.value = false
}
}
// ==================== 初始化 ====================
onMounted(() => {
void loadCatalog()
})
</script>
<template>
<div>
<el-card shadow="never">
<div class="page-toolbar">
<span class="page-desc">
计提比例、系数、阈值、分成与起付比例在此统一维护;变更逐项留痕并支持按日期生效。
</span>
<span>
<el-button @click="openPreviewDrawer">计算预览</el-button>
<el-button @click="openVersionDrawer">版本管理</el-button>
<el-button @click="doExport" :loading="exporting">导出记录</el-button>
</span>
</div>
<el-collapse v-loading="catalogLoading">
<el-collapse-item v-for="group in groupedCatalog" :key="group.name" :name="group.name">
<template #title>
<span class="group-title">{{ group.name }}</span>
</template>
<table class="param-table">
<thead>
<tr>
<th style="width: 190px">参数</th>
<th>当前生效值</th>
<th style="width: 110px">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="item in group.items" :key="item.param_code">
<td>
<div class="param-name">{{ item.param_name }}</div>
<div class="param-desc">{{ item.description || item.param_code }}</div>
</td>
<td>
<template v-if="item.values.length">
<div v-for="v in item.values" :key="v.dimension_value" class="value-row">
<span class="dim-label">{{ dimensionLabel(v.dimension_value) }}</span>
<b class="num">{{ formatValue(item, v.value_text) }}</b>
<span class="eff">{{ v.effective_date }} 起</span>
<el-tag
v-if="pendingOf(item, v.dimension_value)"
size="small"
type="warning"
disable-transitions
>
待改为 {{ pendingOf(item, v.dimension_value)!.new_value }}
</el-tag>
<el-button
link
type="primary"
size="small"
@click="openEdit(item, v.dimension_value)"
>
改
</el-button>
</div>
</template>
<span v-else class="no-value">
未设置(默认 {{ item.default_value ?? '—' }})
</span>
</td>
<td>
<el-button
v-if="!item.values.length"
link
type="primary"
size="small"
@click="openEdit(item, '')"
>
设置
</el-button>
<el-button link type="primary" size="small" @click="openHistory(item)">历史</el-button>
</td>
</tr>
</tbody>
</table>
</el-collapse-item>
</el-collapse>
</el-card>
<!-- 待提交批次 -->
<el-card v-if="pendingList.length" shadow="never" class="pending-card">
<template #header>
<div class="pending-head">
<b>待提交变更({{ pendingList.length }} 项)</b>
<span class="pending-hint">提交前不会生效;提交后未生效前可整批撤回</span>
</div>
</template>
<el-table :data="pendingList" size="small">
<el-table-column label="参数" min-width="160">
<template #default="{ row }">{{ row.item.param_name }}</template>
</el-table-column>
<el-table-column label="维度" width="110">
<template #default="{ row }">{{ dimensionLabel(row.dimension_value) }}</template>
</el-table-column>
<el-table-column label="旧值" min-width="110">
<template #default="{ row }">{{ row.old_value || '—' }}</template>
</el-table-column>
<el-table-column label="新值" min-width="110">
<template #default="{ row }"><b class="num">{{ row.new_value }}</b></template>
</el-table-column>
<el-table-column label="操作" width="80">
<template #default="{ row }">
<el-button link type="danger" size="small" @click="removePending(row.key)">移除</el-button>
</template>
</el-table-column>
</el-table>
<div class="submit-row">
<el-radio-group v-model="changeType">
<el-radio-button :value="1">立即生效</el-radio-button>
<el-radio-button :value="2">指定日期生效</el-radio-button>
</el-radio-group>
<el-date-picker
v-if="changeType === 2"
v-model="effectiveDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="生效日期"
style="width: 150px"
/>
<el-input
v-model="remark"
placeholder="变更说明(如:四季度系数调整)"
style="width: 240px"
maxlength="255"
/>
<el-button type="primary" :loading="submitting" @click="submitAll">提交变更</el-button>
</div>
</el-card>
<!-- 修改参数 -->
<el-dialog v-model="editVisible" title="修改参数" width="440px" destroy-on-close>
<template v-if="editTarget">
<div class="edit-name">{{ editTarget.item.param_name }}</div>
<div class="edit-desc">{{ editTarget.item.description || editTarget.item.param_code }}</div>
<el-form label-width="80px" style="margin-top: 16px" @submit.prevent>
<el-form-item v-if="editDimensions.length > 1 || editTarget.item.dimension_type !== 'NONE'" label="维度">
<el-select v-model="editTarget.dimension" style="width: 100%">
<el-option
v-for="dim in editDimensions"
:key="dim"
:label="dimensionLabel(dim)"
:value="dim"
/>
</el-select>
</el-form-item>
<el-form-item label="当前值">
<span class="num">{{ currentValue(editTarget.item, editTarget.dimension) ?? editTarget.item.default_value ?? '—' }}</span>
<span v-if="editTarget.item.unit" style="color: var(--ys-ink-3)"> {{ editTarget.item.unit }}</span>
</el-form-item>
<el-form-item label="新值">
<el-select
v-if="editTarget.item.value_type === 'ENUM'"
v-model="editValue"
style="width: 100%"
>
<el-option v-for="opt in editTarget.item.options" :key="opt" :label="opt" :value="opt" />
</el-select>
<el-input v-else v-model="editValue" placeholder="输入新值(分数式如 4/360 原样保留)" />
</el-form-item>
</el-form>
<div class="edit-tip">
取值范围:{{ editTarget.item.value_min ?? '−' }} ~ {{ editTarget.item.value_max ?? '不限' }};
精确分数(如 4/360)原样存储、不做小数截断。
</div>
</template>
<template #footer>
<el-button @click="editVisible = false">取消</el-button>
<el-button type="primary" @click="confirmEdit">加入待提交</el-button>
</template>
</el-dialog>
<!-- 版本管理抽屉 -->
<el-drawer v-model="versionVisible" title="版本管理" size="760px">
<div class="drawer-block">
<div class="drawer-row">
<el-date-picker
v-model="compareFrom"
type="date"
value-format="YYYY-MM-DD"
placeholder="对比起始日期"
style="width: 150px"
/>
<el-date-picker
v-model="compareTo"
type="date"
value-format="YYYY-MM-DD"
placeholder="对比截止日期"
style="width: 150px"
/>
<el-button :loading="comparing" @click="doCompare">对比</el-button>
<el-button :loading="exporting" @click="doExport">导出记录</el-button>
</div>
<template v-if="diffSearched">
<div class="drawer-title">快照对比({{ compareFrom }} → {{ compareTo }})</div>
<el-table :data="diffResult" size="small" max-height="260">
<el-table-column prop="param_name" label="参数" min-width="130" />
<el-table-column label="维度" width="90">
<template #default="{ row }">{{ dimensionLabel(row.dimension_value) }}</template>
</el-table-column>
<el-table-column label="旧值" min-width="90">
<template #default="{ row }">{{ row.old_value ?? '—' }}</template>
</el-table-column>
<el-table-column label="新值" min-width="90">
<template #default="{ row }">
<span :class="{ changed: row.changed }">{{ row.new_value ?? '—' }}</span>
</template>
</el-table-column>
<el-table-column label="变化" width="70" align="center">
<template #default="{ row }">
<el-tag v-if="row.changed" size="small" type="warning" disable-transitions>变化</el-tag>
</template>
</el-table-column>
</el-table>
</template>
</div>
<div class="drawer-block">
<div class="drawer-title">变更记录</div>
<el-table v-loading="batchesLoading" :data="batches" size="small">
<el-table-column type="expand">
<template #default="{ row }">
<div class="batch-items">
<template v-if="detailCache[row.id]">
<div v-for="item in detailCache[row.id].items" :key="item.param_code + item.dimension_value" class="batch-item">
<span class="param-name">{{ item.param_name }}</span>
<span class="dim-label">{{ dimensionLabel(item.dimension_value) }}</span>
<span class="num">{{ item.old_value_text ?? '—' }} → <b>{{ item.value_text }}</b></span>
</div>
</template>
<el-button v-else link size="small" @click="detailOf(row).catch(() => {})">
加载明细…
</el-button>
</div>
</template>
</el-table-column>
<el-table-column prop="version_no" label="版本号" width="120" />
<el-table-column prop="effective_date" label="生效日期" width="110" />
<el-table-column label="方式" width="90">
<template #default="{ row }">{{ changeTypeText(row.change_type) }}</template>
</el-table-column>
<el-table-column prop="item_count" label="项数" width="60" align="center" />
<el-table-column prop="operator_name" label="操作人" width="90" />
<el-table-column label="操作" width="80">
<template #default="{ row }">
<el-button
v-if="row.effective_date > localToday()"
link
type="danger"
size="small"
@click="handleWithdraw(row)"
>
撤回
</el-button>
</template>
</el-table-column>
</el-table>
<AppPagination
:total="batchesTotal"
:page-num="batchesPage"
:page-size="batchesPageSize"
@page-change="onBatchesPageChange"
@size-change="onBatchesSizeChange"
/>
</div>
</el-drawer>
<!-- 历史抽屉 -->
<el-drawer v-model="historyVisible" :title="`参数历史 · ${historyItem?.param_name ?? ''}`" size="560px">
<el-table v-loading="historyLoading" :data="historyRows" size="small">
<el-table-column prop="effective_date" label="生效日期" width="105" />
<el-table-column label="维度" width="90">
<template #default="{ row }">{{ dimensionLabel(row.dimension_value) }}</template>
</el-table-column>
<el-table-column label="旧值 → 新值" min-width="130">
<template #default="{ row }">
<span class="num">{{ row.old_value_text ?? '—' }} → {{ row.value_text }}</span>
</template>
</el-table-column>
<el-table-column prop="version_no" label="版本" width="115" />
<el-table-column prop="operator_name" label="操作人" width="80" />
</el-table>
<AppPagination
:total="historyTotal"
:page-num="historyPage"
:page-size="historyPageSize"
@page-change="onHistoryPageChange"
@size-change="onHistorySizeChange"
/>
</el-drawer>
<!-- 计算预览抽屉 -->
<el-drawer v-model="previewVisible" title="计算预览" size="480px">
<el-form label-width="90px" @submit.prevent>
<el-form-item label="客户类型">
<el-select v-model="previewForm.customer_type" style="width: 100%">
<el-option v-for="o in CUSTOMER_OPTIONS" :key="o.value" :label="o.label" :value="o.value" />
</el-select>
</el-form-item>
<el-form-item label="岗位">
<el-select v-model="previewForm.business_role" clearable style="width: 100%">
<el-option
v-for="role in roleOptions"
:key="role.role_code"
:label="role.role_name"
:value="role.role_code"
/>
</el-select>
</el-form-item>
<el-form-item label="产品类别">
<el-select v-model="previewForm.product_category" style="width: 100%">
<el-option v-for="o in PRODUCT_OPTIONS" :key="o.value" :label="o.label" :value="o.value" />
</el-select>
</el-form-item>
<el-form-item label="发货日">
<el-date-picker
v-model="previewForm.ship_date"
type="date"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="回款到账日">
<el-date-picker
v-model="previewForm.receipt_date"
type="date"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="销售额">
<el-input v-model="previewForm.revenue" placeholder="不含运费" />
</el-form-item>
<el-form-item label="毛利润额">
<el-input v-model="previewForm.profit" placeholder="可为负" />
</el-form-item>
<el-form-item label="本笔回款">
<el-input v-model="previewForm.received_amount" />
</el-form-item>
<el-form-item label="以物抵顶">
<el-switch v-model="previewForm.barter" />
</el-form-item>
<el-form-item label="试用期">
<el-switch v-model="previewForm.probation" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="previewLoading" @click="runPreview">试 算</el-button>
</el-form-item>
</el-form>
<template v-if="previewResult">
<div class="preview-result">
<div class="pr-row big">
<span>单笔绩效</span><b class="num">{{ previewResult.performance_amount }} 元</b>
</div>
<div class="pr-row"><span>回款天数</span><b class="num">{{ previewResult.payment_days }} 天</b></div>
<div class="pr-row"><span>计提比例</span><b class="num">{{ previewResult.rate }}</b></div>
<div class="pr-row"><span>计提说明</span><b>{{ previewResult.rate_text }}</b></div>
<div class="pr-row"><span>折算毛利润额</span><b class="num">{{ previewResult.converted_profit }} 元</b></div>
<div class="pr-row"><span>毛利润率</span><b class="num">{{ previewResult.profit_rate }}</b></div>
<div class="pr-row"><span>起付判定</span><b>{{ previewResult.start_payment_ok ? '已达标' : '未达标(累计中)' }}</b></div>
<div class="pr-row"><span>支付权重</span><b class="num">R={{ previewResult.payment_weight_r }},权重 {{ previewResult.payment_weight }}</b></div>
<div class="pr-row"><span>当月可支付</span><b class="num">{{ previewResult.payable_amount }} 元</b></div>
<div v-if="previewResult.probation_note" class="pr-note">{{ previewResult.probation_note }}</div>
</div>
<div class="pr-used">
<div class="drawer-title">所用参数(按回款到账日取值)</div>
<div v-for="u in previewResult.params_used" :key="u.param_code" class="pr-used-row">
{{ u.param_code }} = {{ u.value_text }}
<span v-if="u.effective_date" class="eff">({{ u.effective_date }} 起)</span>
</div>
</div>
</template>
</el-drawer>
</div>
</template>
<style scoped>
.page-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 14px;
}
.page-desc {
font-size: 13px;
color: var(--ys-ink-2);
}
.group-title {
font-weight: 600;
color: var(--ys-ink);
}
.param-table {
width: 100%;
border-collapse: collapse;
}
.param-table th {
text-align: left;
font-size: 12px;
font-weight: 500;
letter-spacing: 0.04em;
color: var(--ys-ink-2);
padding: 6px 8px;
border-bottom: 1px solid var(--ys-line);
}
.param-table td {
padding: 10px 8px;
border-bottom: 1px solid var(--ys-line);
font-size: 13px;
vertical-align: top;
}
.param-name {
font-weight: 500;
}
.param-desc {
font-size: 12px;
color: var(--ys-ink-3);
margin-top: 2px;
}
.value-row {
display: flex;
align-items: baseline;
gap: 8px;
line-height: 1.9;
}
.dim-label {
font-size: 12px;
color: var(--ys-ink-3);
min-width: 56px;
}
.num {
font-variant-numeric: tabular-nums;
}
.eff {
font-size: 12px;
color: var(--ys-ink-3);
}
.no-value {
color: var(--ys-ink-3);
font-size: 12px;
}
.pending-card {
margin-top: 14px;
}
.pending-head {
display: flex;
align-items: baseline;
gap: 12px;
}
.pending-hint {
font-size: 12px;
color: var(--ys-ink-3);
}
.submit-row {
display: flex;
gap: 10px;
align-items: center;
margin-top: 14px;
}
.edit-name {
font-size: 15px;
font-weight: 600;
}
.edit-desc {
font-size: 12px;
color: var(--ys-ink-3);
margin-top: 4px;
}
.edit-tip {
font-size: 12px;
color: var(--ys-ink-3);
background: var(--ys-mist);
border-radius: 6px;
padding: 8px 12px;
}
.drawer-block {
margin-bottom: 24px;
}
.drawer-row {
display: flex;
gap: 10px;
align-items: center;
margin-bottom: 12px;
}
.drawer-title {
font-size: 13px;
font-weight: 600;
color: var(--ys-ink-2);
margin: 10px 0 8px;
}
.batch-items {
padding: 6px 12px;
background: var(--ys-mist);
border-radius: 6px;
}
.batch-item {
display: flex;
gap: 10px;
align-items: baseline;
font-size: 12.5px;
line-height: 2;
}
.batch-item .param-name {
min-width: 130px;
color: var(--ys-ink);
}
.changed {
color: var(--ys-indigo);
font-weight: 600;
}
.preview-result {
margin-top: 18px;
border: 1px solid var(--ys-line);
border-radius: 10px;
padding: 16px 18px;
background: linear-gradient(180deg, #ffffff, #fcfcfe);
}
.pr-row {
display: flex;
justify-content: space-between;
align-items: baseline;
padding: 6px 0;
font-size: 13px;
color: var(--ys-ink-2);
}
.pr-row.big {
border-bottom: 1px solid var(--ys-line);
margin-bottom: 6px;
padding-bottom: 10px;
}
.pr-row.big b {
font-size: 20px;
color: var(--ys-indigo);
}
.pr-row b {
color: var(--ys-ink);
font-weight: 600;
}
.pr-note {
margin-top: 10px;
font-size: 12px;
color: var(--ys-warn);
background: #fbf4e4;
border-radius: 6px;
padding: 8px 12px;
}
.pr-used {
margin-top: 16px;
}
.pr-used-row {
font-size: 12px;
color: var(--ys-ink-2);
line-height: 2;
font-variant-numeric: tabular-nums;
}
</style>