Auto-recalculate the final installment due amount when earlier periods change (with manual override), and sync lump-sum deadline to due date. Co-authored-by: Cursor <cursoragent@cursor.com>
1286 lines
42 KiB
Vue
1286 lines
42 KiB
Vue
<template>
|
||
<div class="sample-payment-panel">
|
||
<el-empty
|
||
v-if="!syncedPayments.length"
|
||
description="请先在「样本信息」中添加样本,再配置按样本缴费"
|
||
:image-size="48"
|
||
/>
|
||
|
||
<div
|
||
v-for="(pay, pIdx) in syncedPayments"
|
||
:key="pay.sampleCode || pIdx"
|
||
class="sample-pay-card"
|
||
>
|
||
<div class="sample-pay-card__head">
|
||
<div class="sample-pay-card__title">
|
||
{{ sampleTitle(pay) }}
|
||
<el-tag v-if="pay.sampleCode" size="small" type="info" class="ml-2">
|
||
{{ pay.sampleCode }}
|
||
</el-tag>
|
||
</div>
|
||
<el-tag :type="payModeTagType(String(pay.payMode || ''))" size="small">
|
||
{{ payModeLabel(String(pay.payMode || '')) || '—' }}
|
||
</el-tag>
|
||
</div>
|
||
|
||
<div
|
||
class="form-grid"
|
||
:class="isMultiSchedulePayMode(String(pay.payMode || '')) ? 'form-grid--col6' : 'form-grid--col4'"
|
||
>
|
||
<el-form-item label="缴费模式">
|
||
<el-select
|
||
:model-value="pay.payMode"
|
||
style="width: 100%"
|
||
@update:model-value="(v: string) => onPayModeChange(pIdx, v)"
|
||
>
|
||
<el-option
|
||
v-for="opt in payModeOptionsFor(pay)"
|
||
:key="opt.value"
|
||
:label="opt.label"
|
||
:value="opt.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="样本合同金额(元)">
|
||
<AmountInput
|
||
:model-value="pay.contractAmount"
|
||
:min="0"
|
||
style="width: 100%"
|
||
@update:model-value="(v: number | null) => onContractAmountChange(pIdx, v)"
|
||
/>
|
||
</el-form-item>
|
||
<template v-if="pay.payMode === 'dj'">
|
||
<el-form-item label="趸交截止时间">
|
||
<el-date-picker
|
||
:model-value="pay.lumpSumDeadline"
|
||
type="date"
|
||
value-format="YYYYMMDD"
|
||
format="YYYY-MM-DD"
|
||
placeholder="选择截止时间"
|
||
clearable
|
||
style="width: 100%"
|
||
@update:model-value="(v: string) => onLumpSumDeadlineChange(pIdx, v || '')"
|
||
/>
|
||
</el-form-item>
|
||
</template>
|
||
<template v-else>
|
||
<el-form-item :label="pay.payMode === 'fgd' ? '期数' : '分期期数'">
|
||
<el-input
|
||
:model-value="pay.installmentCount"
|
||
type="number"
|
||
:min="1"
|
||
style="width: 100%"
|
||
@update:model-value="(v: string) => onFqFieldInput(pIdx, 'installmentCount', v)"
|
||
@change="() => onFqFieldCommit(pIdx)"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="每期间隔(月)">
|
||
<el-input
|
||
:model-value="pay.installmentIntervalMonths"
|
||
type="number"
|
||
:min="0.1"
|
||
step="any"
|
||
style="width: 100%"
|
||
@update:model-value="(v: string) => onFqFieldInput(pIdx, 'installmentIntervalMonths', v)"
|
||
@change="() => onFqFieldCommit(pIdx)"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="分期开始时间">
|
||
<el-date-picker
|
||
:model-value="pay.installmentStartDate"
|
||
type="date"
|
||
value-format="YYYYMMDD"
|
||
format="YYYY-MM-DD"
|
||
clearable
|
||
style="width: 100%"
|
||
@update:model-value="(v: string) => onFqStartDateChange(pIdx, v)"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="提前催缴预警(天)">
|
||
<el-input
|
||
:model-value="pay.installmentAdvanceDays"
|
||
type="number"
|
||
:min="0"
|
||
:max="365"
|
||
style="width: 100%"
|
||
@update:model-value="(v: string) => onFqFieldInput(pIdx, 'installmentAdvanceDays', v)"
|
||
@change="() => onFqFieldCommit(pIdx)"
|
||
/>
|
||
</el-form-item>
|
||
</template>
|
||
</div>
|
||
|
||
<div class="section-subtitle">应缴批次</div>
|
||
<div v-if="!(pay.schedules || []).length" class="muted-hint"
|
||
>暂无批次,请完善缴费参数后自动生成</div
|
||
>
|
||
<div v-for="(sch, sIdx) in pay.schedules || []" :key="sch.id || sIdx" class="schedule-block">
|
||
<div class="schedule-block__head">
|
||
<span>第 {{ sch.scheduleNo || sIdx + 1 }} 期</span>
|
||
<span class="schedule-arrears">
|
||
已缴
|
||
{{
|
||
fmtMoney(schedulePaidArrears(Number(sch.dueAmount) || 0, sch.transfers || []).paid)
|
||
}}
|
||
· 欠缴
|
||
{{
|
||
fmtMoney(schedulePaidArrears(Number(sch.dueAmount) || 0, sch.transfers || []).arrears)
|
||
}}
|
||
</span>
|
||
<el-button type="primary" size="small" @click="addTransfer(pIdx, sIdx)">
|
||
<el-icon><Plus /></el-icon>
|
||
添加转账
|
||
</el-button>
|
||
</div>
|
||
<div class="form-grid form-grid--col4 schedule-due-edit">
|
||
<el-form-item label="应缴费金额(元)">
|
||
<AmountInput
|
||
:model-value="sch.dueAmount"
|
||
:min="0"
|
||
:disabled="isScheduleDueLocked(sch)"
|
||
style="width: 100%"
|
||
@update:model-value="(v: number | null) => patchSchedule(pIdx, sIdx, { dueAmount: v })"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="应缴费时间">
|
||
<el-date-picker
|
||
:model-value="sch.dueDate"
|
||
type="date"
|
||
value-format="YYYYMMDD"
|
||
format="YYYY-MM-DD"
|
||
placeholder="选择应缴费时间"
|
||
clearable
|
||
:disabled="isScheduleDueLocked(sch)"
|
||
style="width: 100%"
|
||
@update:model-value="(v: string) => patchSchedule(pIdx, sIdx, { dueDate: v || '' })"
|
||
/>
|
||
</el-form-item>
|
||
</div>
|
||
|
||
<el-table
|
||
:data="sch.transfers || []"
|
||
class="list-table transfer-table"
|
||
stripe
|
||
empty-text="暂无转账记录,请点击上方按钮添加"
|
||
>
|
||
<el-table-column type="index" label="#" min-width="50" align="center" />
|
||
<el-table-column label="转账方式" min-width="140" align="center">
|
||
<template #default="{ row }">
|
||
<el-select
|
||
v-model="row.transferType"
|
||
placeholder="请选择"
|
||
style="width: 100%"
|
||
:disabled="isTransferLocked(row)"
|
||
@change="emitChange"
|
||
>
|
||
<el-option
|
||
v-for="opt in transferTypeOptions"
|
||
:key="opt.value"
|
||
:label="opt.label"
|
||
:value="opt.value"
|
||
/>
|
||
</el-select>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="转账金额(元)" min-width="140" align="center">
|
||
<template #default="{ row }">
|
||
<AmountInput
|
||
v-model="row.transferAmount"
|
||
:min="0"
|
||
:disabled="isTransferLocked(row)"
|
||
style="width: 100%"
|
||
@change="emitChange"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="转账时间" min-width="190" align="center">
|
||
<template #default="{ row }">
|
||
<el-date-picker
|
||
v-model="row.transferTime"
|
||
type="datetime"
|
||
format="YYYY-MM-DD HH:mm"
|
||
value-format="YYYY-MM-DD HH:mm:ss"
|
||
clearable
|
||
placeholder="选择时间"
|
||
:disabled="isTransferLocked(row)"
|
||
style="width: 100%"
|
||
@change="emitChange"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="转账凭证" min-width="280" align="left">
|
||
<template #default="{ row, $index }">
|
||
<div class="attachment-upload">
|
||
<div class="attachment-list">
|
||
<el-upload
|
||
v-if="!isTransferLocked(row)"
|
||
:auto-upload="false"
|
||
:show-file-list="false"
|
||
:multiple="true"
|
||
accept="image/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.zip,.rar"
|
||
@change="(f: any) => handleTransferAttachmentChange(pIdx, sIdx, $index, f)"
|
||
>
|
||
<div class="upload-card">
|
||
<el-icon :size="22" color="#047857"><Plus /></el-icon>
|
||
<span class="upload-card-text">上传凭证</span>
|
||
</div>
|
||
</el-upload>
|
||
<div
|
||
v-for="(file, fIdx) in ensureTransferAttachments(row)"
|
||
:key="file.id || fIdx"
|
||
class="attachment-item"
|
||
:class="{ 'is-uploading': file._uploading }"
|
||
>
|
||
<div class="attachment-preview">
|
||
<template v-if="file.fileUrl && isImageUrl(file.fileUrl)">
|
||
<img :src="fullFileUrl(file.fileUrl)" :alt="file.fileName" />
|
||
</template>
|
||
<template v-else>
|
||
<div class="attachment-preview__icon">
|
||
<el-icon :size="22"><Document /></el-icon>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
<div class="attachment-footer">
|
||
<span class="attachment-name" :title="file.fileName">{{
|
||
file.fileName || '附件'
|
||
}}</span>
|
||
<el-icon
|
||
v-if="!isTransferLocked(row)"
|
||
class="action-btn"
|
||
title="删除"
|
||
@click="removeTransferAttachment(pIdx, sIdx, $index, fIdx)"
|
||
>
|
||
<Delete />
|
||
</el-icon>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="备注" min-width="160" align="center">
|
||
<template #default="{ row }">
|
||
<el-input
|
||
v-if="!isTransferLocked(row)"
|
||
v-model="row.remark"
|
||
maxlength="512"
|
||
clearable
|
||
placeholder="选填"
|
||
@change="emitChange"
|
||
/>
|
||
<span v-else>{{ row.remark || '—' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
label="财务备注"
|
||
min-width="140"
|
||
align="center"
|
||
show-overflow-tooltip
|
||
>
|
||
<template #default="{ row }">{{ row.financeRemark || '—' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="状态" min-width="110" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag :type="payStatusTagType(String(row.payStatus || 'dfj'))" size="small">
|
||
{{ payStatusLabel(String(row.payStatus || 'dfj')) }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" min-width="300" align="center" fixed="right">
|
||
<template #default="{ row, $index }">
|
||
<el-button
|
||
v-if="!isTransferLocked(row)"
|
||
link
|
||
type="primary"
|
||
size="small"
|
||
:loading="savingKey === transferRowKey(pIdx, sIdx, $index, row)"
|
||
@click="saveTransferRow(pIdx, sIdx, $index)"
|
||
>
|
||
<el-icon><Select /></el-icon>
|
||
保存
|
||
</el-button>
|
||
<el-button
|
||
v-if="row.id && row.payStatus === 'dfj' && protocolId"
|
||
link
|
||
type="primary"
|
||
size="small"
|
||
:loading="submittingId === row.id"
|
||
@click="submitTransfer(row)"
|
||
>
|
||
<el-icon><Check /></el-icon>
|
||
提交财务确认
|
||
</el-button>
|
||
<el-button
|
||
v-if="row.id && row.payStatus === 'jfqrz' && protocolId"
|
||
link
|
||
type="warning"
|
||
size="small"
|
||
:loading="submittingId === row.id"
|
||
@click="withdrawTransfer(row)"
|
||
>
|
||
<el-icon><RefreshLeft /></el-icon>
|
||
撤回
|
||
</el-button>
|
||
<el-button
|
||
v-if="!isTransferLocked(row)"
|
||
link
|
||
type="danger"
|
||
size="small"
|
||
@click="removeTransfer(pIdx, sIdx, $index)"
|
||
>
|
||
<el-icon><Delete /></el-icon>
|
||
删除
|
||
</el-button>
|
||
<span
|
||
v-if="isTransferLocked(row) && row.payStatus !== 'jfqrz'"
|
||
class="muted-dash"
|
||
>—</span
|
||
>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, ref, watch } from 'vue'
|
||
import { Plus, Check, Delete, Document, Select, RefreshLeft } from '@element-plus/icons-vue'
|
||
import AmountInput from '@/components/common/amount-input/index.vue'
|
||
import { formatMoneyDisplay } from '@/utils/money-format'
|
||
import {
|
||
payStatusLabel,
|
||
payStatusTagType,
|
||
loadTransferTypeDict,
|
||
getTransferTypeOptions
|
||
} from '@/utils/protocol-payment-labels'
|
||
import {
|
||
DEFAULT_INSTALLMENT_ADVANCE_DAYS,
|
||
calcFqInstallmentDueDate,
|
||
normalizeInstallmentAmounts
|
||
} from '@/utils/protocol-payment-installment'
|
||
import {
|
||
applyConsolidateRemainingToPayment,
|
||
applyExpandRemainingToInstallment,
|
||
applyLumpSumDeadlineToLastDueDate,
|
||
availableSamplePayModes,
|
||
calcLastPeriodDueAmount,
|
||
parseInstallmentRatios,
|
||
parsePreferentialPrice,
|
||
resolvePerSamplePaymentDefaults,
|
||
samplePaymentHasConfirmedTransfer,
|
||
schedulePaidArrears,
|
||
shouldAutoFillLastPeriodDue,
|
||
shouldReplaceContractWithPreferentialPrice
|
||
} from '@/utils/protocol-sample-payment'
|
||
import { isMultiSchedulePayMode } from '@/utils/protocol-payment-presets'
|
||
import { payModeLabel, payModeTagType } from '@/utils/status-tag-registry'
|
||
import {
|
||
fetchCreatePaymentTransfer,
|
||
fetchUpdatePaymentTransfer,
|
||
fetchSubmitPaymentTransferFinance,
|
||
fetchWithdrawPaymentTransfer
|
||
} from '@/api/protocol-sample-payment'
|
||
import { fetchUploadAttachment } from '@/api/business'
|
||
import { ElMessageBox } from 'element-plus'
|
||
|
||
type TransferAttachment = {
|
||
id?: string
|
||
fileName?: string
|
||
fileUrl?: string
|
||
_uploading?: boolean
|
||
}
|
||
|
||
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
|
||
|
||
function fullFileUrl(url: string) {
|
||
if (!url) return ''
|
||
if (/^https?:\/\//i.test(url)) return url
|
||
return minioBaseUrl ? `${minioBaseUrl}/${url.replace(/^\//, '')}` : url
|
||
}
|
||
|
||
function isImageUrl(url: string) {
|
||
return /\.(png|jpe?g|gif|webp|bmp|svg)(\?.*)?$/i.test(url)
|
||
}
|
||
|
||
/** 保证转账行有 attachments 数组(编辑回填或历史数据) */
|
||
function ensureTransferAttachments(tr: Record<string, any>): TransferAttachment[] {
|
||
if (Array.isArray(tr.attachments)) return tr.attachments
|
||
const urls = String(tr.transferAttachments || '')
|
||
.split(',')
|
||
.map((s: string) => s.trim())
|
||
.filter(Boolean)
|
||
tr.attachments = urls.map((fileUrl: string, i: number) => ({
|
||
id: `att_${i}`,
|
||
fileName: fileUrl.includes('/') ? fileUrl.split('/').pop() || '附件' : '附件',
|
||
fileUrl
|
||
}))
|
||
return tr.attachments
|
||
}
|
||
|
||
const props = defineProps<{
|
||
modelValue: Array<Record<string, any>>
|
||
samples: Array<Record<string, any>>
|
||
protocolId?: string
|
||
}>()
|
||
|
||
const emit = defineEmits<{
|
||
'update:modelValue': [Array<Record<string, any>>]
|
||
}>()
|
||
|
||
const transferTypeOptions = ref<Api.Dict.DictDataOption[]>([])
|
||
const submittingId = ref('')
|
||
const savingKey = ref('')
|
||
/** 按样本 key:用户手改过最后一期应缴金额则 true,前几期再变时清除 */
|
||
const lastPeriodDueOverrideByKey = ref<Record<string, boolean>>({})
|
||
|
||
async function ensureTransferTypes() {
|
||
await loadTransferTypeDict()
|
||
transferTypeOptions.value = getTransferTypeOptions()
|
||
}
|
||
ensureTransferTypes()
|
||
|
||
function fmtMoney(v: unknown): string {
|
||
if (v == null || v === '') return '—'
|
||
return '¥' + formatMoneyDisplay(v)
|
||
}
|
||
|
||
function samplePayKey(pay: Record<string, any>, pIdx: number): string {
|
||
return String(pay.sampleCode || pay.protocolSampleId || pIdx)
|
||
}
|
||
|
||
function activeScheduleIndices(schedules: Record<string, any>[]): number[] {
|
||
const idx: number[] = []
|
||
schedules.forEach((s, i) => {
|
||
if (String(s.scheduleStatus || 'open') !== 'void' && !s._deleted) idx.push(i)
|
||
})
|
||
return idx
|
||
}
|
||
|
||
/** 仅财务已确认(yjf)锁定;付款确认中仍可改,直至财务确认 */
|
||
function isTransferLocked(tr: Record<string, any>) {
|
||
return tr.payStatus === 'yjf'
|
||
}
|
||
|
||
/** 期次存在任一已财务确认转账时,应缴金额/时间只读 */
|
||
function isScheduleDueLocked(sch: Record<string, any>) {
|
||
return (sch.transfers || []).some((t: Record<string, any>) => t.payStatus === 'yjf')
|
||
}
|
||
|
||
/** 仅改单期字段,不重建整表批次;手填金额同步到 installmentRatios */
|
||
function patchSchedule(pIdx: number, sIdx: number, patch: Record<string, any>) {
|
||
const list = [...(props.modelValue || [])]
|
||
const pay = { ...list[pIdx] }
|
||
const schedules = [...(pay.schedules || [])]
|
||
schedules[sIdx] = { ...schedules[sIdx], ...patch }
|
||
const mode = String(pay.payMode || '')
|
||
const key = samplePayKey(pay, pIdx)
|
||
const activeIdx = activeScheduleIndices(schedules)
|
||
|
||
if ('dueAmount' in patch && mode === 'fq' && activeIdx.length >= 2) {
|
||
const lastIdx = activeIdx[activeIdx.length - 1]
|
||
if (sIdx === lastIdx) {
|
||
lastPeriodDueOverrideByKey.value = {
|
||
...lastPeriodDueOverrideByKey.value,
|
||
[key]: true
|
||
}
|
||
} else if (activeIdx.slice(0, -1).includes(sIdx)) {
|
||
lastPeriodDueOverrideByKey.value = {
|
||
...lastPeriodDueOverrideByKey.value,
|
||
[key]: false
|
||
}
|
||
const lastSch = schedules[lastIdx]
|
||
const canFill = shouldAutoFillLastPeriodDue({
|
||
lastManualOverride: false,
|
||
lastLocked: isScheduleDueLocked(lastSch)
|
||
})
|
||
if (canFill) {
|
||
const priors = activeIdx.slice(0, -1).map((i) => schedules[i]?.dueAmount)
|
||
const lastAmt = calcLastPeriodDueAmount(pay.contractAmount, priors)
|
||
if (lastAmt != null) {
|
||
schedules[lastIdx] = { ...schedules[lastIdx], dueAmount: lastAmt }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
pay.schedules = schedules
|
||
if ('dueAmount' in patch) {
|
||
pay.installmentRatios = schedules.map((s: Record<string, any>) => {
|
||
const n = Number(s.dueAmount)
|
||
return Number.isFinite(n) && n > 0 ? n : 0
|
||
})
|
||
}
|
||
list[pIdx] = pay
|
||
emit('update:modelValue', list)
|
||
}
|
||
|
||
function onLumpSumDeadlineChange(pIdx: number, raw: string) {
|
||
const list = [...(props.modelValue || [])]
|
||
const pay = { ...list[pIdx] }
|
||
const deadline = raw || ''
|
||
pay.lumpSumDeadline = deadline
|
||
if (String(pay.payMode || '') === 'dj') {
|
||
pay.schedules = applyLumpSumDeadlineToLastDueDate(
|
||
[...(pay.schedules || [])],
|
||
deadline,
|
||
isScheduleDueLocked
|
||
)
|
||
}
|
||
list[pIdx] = pay
|
||
emit('update:modelValue', list)
|
||
}
|
||
|
||
function sampleTitle(pay: Record<string, any>) {
|
||
const sample = (props.samples || []).find(
|
||
(s) => String(s.sampleCode || '') === String(pay.sampleCode || '')
|
||
)
|
||
return String(sample?.sampleTypeName || sample?.sampleName || '样本') || '样本'
|
||
}
|
||
|
||
function emptyDjPayment(sample: Record<string, any>): Record<string, any> {
|
||
const defaults = resolvePerSamplePaymentDefaults(sample, 'dj')
|
||
const amount = defaults.contractAmount
|
||
return {
|
||
sampleCode: String(sample.sampleCode || ''),
|
||
protocolSampleId: sample.id || '',
|
||
payMode: 'dj',
|
||
contractAmount: amount,
|
||
lumpSumDeadline: '',
|
||
installmentCount: null,
|
||
installmentIntervalMonths: null,
|
||
installmentStartDate: '',
|
||
installmentAdvanceDays: DEFAULT_INSTALLMENT_ADVANCE_DAYS,
|
||
installmentRatios: [],
|
||
schedules: amount
|
||
? [
|
||
{
|
||
scheduleNo: 1,
|
||
dueAmount: amount,
|
||
dueDate: '',
|
||
scheduleStatus: 'open',
|
||
transfers: []
|
||
}
|
||
]
|
||
: []
|
||
}
|
||
}
|
||
|
||
function paymentHasLockedTransfer(pay: Record<string, any>) {
|
||
return (pay.schedules || []).some((s: Record<string, any>) =>
|
||
(s.transfers || []).some((t: Record<string, any>) => t.payStatus === 'yjf')
|
||
)
|
||
}
|
||
|
||
/** 优惠价变更时同步合同;打开编辑不覆盖库内金额(仅合同为空时补齐) */
|
||
function applyPreferentialPriceToPayment(
|
||
pay: Record<string, any>,
|
||
sample: Record<string, any>,
|
||
force = false
|
||
): Record<string, any> {
|
||
if (paymentHasLockedTransfer(pay)) return pay
|
||
const price = parsePreferentialPrice(sample)
|
||
if (price == null) return pay
|
||
if (!force && !shouldReplaceContractWithPreferentialPrice(pay.contractAmount, price)) {
|
||
return pay
|
||
}
|
||
if (Math.abs(Number(pay.contractAmount || 0) - price) <= 0.015) return pay
|
||
const mode = String(pay.payMode || 'dj')
|
||
const next: Record<string, any> = { ...pay, contractAmount: price }
|
||
// 分期:不自动改写各期金额(由用户保证合计=新合同);趸交随合同重建应缴
|
||
if (mode === 'fq' || mode === 'fgd') {
|
||
return next
|
||
}
|
||
return rebuildSchedules(next)
|
||
}
|
||
|
||
function scheduleByNo(pay: Record<string, any>, scheduleNo: number) {
|
||
return (pay.schedules || []).find(
|
||
(s: any) => Number(s.scheduleNo || 0) === Number(scheduleNo)
|
||
)
|
||
}
|
||
|
||
function transfersForSchedule(pay: Record<string, any>, scheduleNo: number) {
|
||
// 重建批次时保留确认中/已确认笔,以及待缴费草稿
|
||
return [...(scheduleByNo(pay, scheduleNo)?.transfers || [])]
|
||
}
|
||
|
||
function rebuildSchedules(pay: Record<string, any>): Record<string, any> {
|
||
const mode = pay.payMode || 'dj'
|
||
if (mode === 'dj') {
|
||
const prev = scheduleByNo(pay, 1)
|
||
return {
|
||
...pay,
|
||
schedules: [
|
||
{
|
||
id: prev?.id,
|
||
scheduleNo: 1,
|
||
dueAmount: pay.contractAmount,
|
||
dueDate: pay.lumpSumDeadline || '',
|
||
scheduleStatus: prev?.scheduleStatus || 'open',
|
||
transfers: transfersForSchedule(pay, 1)
|
||
}
|
||
]
|
||
}
|
||
}
|
||
// fq / fgd / djfq:按期数生成批次;金额无模板时留空由用户手填
|
||
const count = Number(pay.installmentCount) || 0
|
||
const interval = Number(pay.installmentIntervalMonths) || 0
|
||
const startDate = pay.installmentStartDate || ''
|
||
const advanceDays = Number(pay.installmentAdvanceDays ?? DEFAULT_INSTALLMENT_ADVANCE_DAYS)
|
||
const rawRatios = parseInstallmentRatios(pay.installmentRatios)
|
||
const hasManualAmounts = rawRatios.some((v) => Number(v) > 0)
|
||
let amounts: Array<number | null> = []
|
||
if (count > 0) {
|
||
if (hasManualAmounts) {
|
||
amounts = normalizeInstallmentAmounts(rawRatios, pay.contractAmount, count)
|
||
} else {
|
||
amounts = Array.from({ length: count }, () => null)
|
||
}
|
||
}
|
||
if (!count || !interval || !startDate) {
|
||
return { ...pay, installmentRatios: hasManualAmounts ? amounts : [], schedules: [] }
|
||
}
|
||
return {
|
||
...pay,
|
||
installmentRatios: hasManualAmounts ? amounts : [],
|
||
schedules: Array.from({ length: count }, (_, i) => {
|
||
const scheduleNo = i + 1
|
||
const prev = scheduleByNo(pay, scheduleNo)
|
||
const prevAmt = prev?.dueAmount
|
||
const keepPrev =
|
||
!hasManualAmounts && prevAmt != null && Number(prevAmt) > 0
|
||
? Number(prevAmt)
|
||
: null
|
||
return {
|
||
id: prev?.id,
|
||
scheduleNo,
|
||
dueAmount: hasManualAmounts ? amounts[i] : keepPrev,
|
||
dueDate:
|
||
prev?.dueDate ||
|
||
calcFqInstallmentDueDate(startDate, i, interval, advanceDays) ||
|
||
'',
|
||
scheduleStatus: prev?.scheduleStatus || 'open',
|
||
transfers: transfersForSchedule(pay, scheduleNo)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
const syncedPayments = computed(() => props.modelValue || [])
|
||
|
||
function emitChange() {
|
||
emit('update:modelValue', [...(props.modelValue || [])])
|
||
}
|
||
|
||
function patchPay(pIdx: number, patch: Record<string, any>, rebuild = false) {
|
||
const list = [...(props.modelValue || [])]
|
||
let next = { ...list[pIdx], ...patch }
|
||
if (rebuild) next = rebuildSchedules(next)
|
||
list[pIdx] = next
|
||
emit('update:modelValue', list)
|
||
}
|
||
|
||
const PAY_MODE_OPTION_META: Array<{ value: string; label: string }> = [
|
||
{ value: 'dj', label: '趸交' },
|
||
{ value: 'fq', label: '分期' },
|
||
{ value: 'fgd', label: '分改趸' }
|
||
]
|
||
|
||
function payModeOptionsFor(pay: Record<string, any>) {
|
||
const allowed = new Set(availableSamplePayModes(pay))
|
||
return PAY_MODE_OPTION_META.filter((o) => allowed.has(o.value as any))
|
||
}
|
||
|
||
function activeScheduleCount(pay: Record<string, any>): number {
|
||
return (pay.schedules || []).filter(
|
||
(s: any) => String(s.scheduleStatus || 'open') !== 'void' && !s._deleted
|
||
).length
|
||
}
|
||
|
||
async function onPayModeChange(pIdx: number, mode: string) {
|
||
const list = props.modelValue || []
|
||
const prev = list[pIdx]
|
||
const prevMode = String(prev?.payMode || '')
|
||
if (!prev || mode === prevMode) return
|
||
|
||
const allowed = availableSamplePayModes(prev)
|
||
if (!allowed.includes(mode as any)) {
|
||
ElMessage.warning('当前缴费模式下不可切换为该选项')
|
||
return
|
||
}
|
||
|
||
const hasPaid = samplePaymentHasConfirmedTransfer(prev)
|
||
const schCount = activeScheduleCount(prev)
|
||
|
||
// 分期 → 趸交/分改趸:只要已有批次就并期(不依赖是否已有 yjf;无已缴时会并成 1 期)
|
||
// 此前无 yjf 时误走 rebuild,且期数仍按 6,界面几乎无变化
|
||
if (prevMode === 'fq' && (mode === 'dj' || mode === 'fgd') && schCount > 0) {
|
||
const label = mode === 'dj' ? '趸交' : '分改趸'
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
hasPaid
|
||
? `改为${label}将保留已缴期,并把后续未缴合并至当前未清期。是否继续?`
|
||
: `改为${label}将把各期未缴合并为当前未清期。是否继续?`,
|
||
`确认改为${label}`,
|
||
{ type: 'warning', confirmButtonText: '继续', cancelButtonText: '取消' }
|
||
)
|
||
} catch {
|
||
return
|
||
}
|
||
try {
|
||
const nextList = [...(props.modelValue || [])]
|
||
nextList[pIdx] = applyConsolidateRemainingToPayment({ ...nextList[pIdx] }, mode)
|
||
emit('update:modelValue', nextList)
|
||
} catch (e: any) {
|
||
ElMessage.error(e?.message || `无法改为${label}`)
|
||
}
|
||
return
|
||
}
|
||
|
||
// 有已缴:趸交/分改趸 → 分期(保留已缴,剩余按期数拆分)
|
||
if (hasPaid && (prevMode === 'dj' || prevMode === 'fgd') && mode === 'fq') {
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
'改为分期将保留已缴期,剩余金额按分期期数自动生成后续应缴批次。是否继续?',
|
||
'确认改为分期',
|
||
{ type: 'warning', confirmButtonText: '继续', cancelButtonText: '取消' }
|
||
)
|
||
} catch {
|
||
return
|
||
}
|
||
try {
|
||
const nextList = [...(props.modelValue || [])]
|
||
const cur = { ...nextList[pIdx] }
|
||
const count = Number(cur.installmentCount) || 6
|
||
const interval = Number(cur.installmentIntervalMonths) || 1
|
||
let startDate = String(cur.installmentStartDate || '').trim()
|
||
if (!startDate) {
|
||
startDate = String(cur.lumpSumDeadline || '').trim()
|
||
}
|
||
if (!startDate) {
|
||
const d = new Date()
|
||
startDate = `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, '0')}${String(d.getDate()).padStart(2, '0')}`
|
||
}
|
||
nextList[pIdx] = applyExpandRemainingToInstallment(cur, {
|
||
installmentCount: count,
|
||
installmentIntervalMonths: interval,
|
||
installmentStartDate: startDate,
|
||
installmentAdvanceDays:
|
||
cur.installmentAdvanceDays ?? DEFAULT_INSTALLMENT_ADVANCE_DAYS
|
||
})
|
||
emit('update:modelValue', nextList)
|
||
} catch (e: any) {
|
||
ElMessage.error(e?.message || '无法改为分期')
|
||
}
|
||
return
|
||
}
|
||
|
||
// 无已缴 / 无批次:自由切换并重建批次
|
||
const sample = (props.samples || []).find(
|
||
(s) => String(s.sampleCode || '') === String(prev?.sampleCode || '')
|
||
)
|
||
const presetMode = mode === 'fq' || mode === 'fgd' ? 'fq' : 'dj'
|
||
const defaults = resolvePerSamplePaymentDefaults(sample, presetMode)
|
||
const patch: Record<string, any> = {
|
||
payMode: mode,
|
||
contractAmount: defaults.contractAmount ?? prev?.contractAmount ?? null
|
||
}
|
||
if (mode === 'fq' || mode === 'fgd') {
|
||
// 建单选分改趸默认 3 期;勿用 defaults.installmentCount(||6) 盖掉
|
||
patch.installmentCount = mode === 'fgd' ? 3 : defaults.installmentCount || 6
|
||
patch.installmentIntervalMonths = 1
|
||
patch.installmentAdvanceDays = DEFAULT_INSTALLMENT_ADVANCE_DAYS
|
||
patch.installmentRatios = []
|
||
} else {
|
||
patch.installmentCount = null
|
||
patch.installmentRatios = []
|
||
}
|
||
patchPay(pIdx, patch, true)
|
||
}
|
||
|
||
function onContractAmountChange(pIdx: number, v: number | null) {
|
||
patchPay(pIdx, { contractAmount: v }, true)
|
||
}
|
||
|
||
/** 输入过程中同步受控值(不重建批次,避免中途键入被回滚) */
|
||
function onFqFieldInput(pIdx: number, key: string, raw: string) {
|
||
const num = raw === '' || raw == null ? null : Number(raw)
|
||
patchPay(pIdx, { [key]: Number.isFinite(num as number) ? num : null }, false)
|
||
}
|
||
|
||
/** 失焦/回车后再按最新参数重建应缴批次 */
|
||
function onFqFieldCommit(pIdx: number) {
|
||
patchPay(pIdx, {}, true)
|
||
}
|
||
|
||
function onFqStartDateChange(pIdx: number, v: string) {
|
||
patchPay(pIdx, { installmentStartDate: v || '' }, true)
|
||
}
|
||
|
||
function addTransfer(pIdx: number, sIdx: number) {
|
||
const list = [...(props.modelValue || [])]
|
||
const pay = { ...list[pIdx] }
|
||
const schedules = [...(pay.schedules || [])]
|
||
const sch = { ...schedules[sIdx] }
|
||
const transfers = [...(sch.transfers || [])]
|
||
transfers.push({
|
||
transferType: '',
|
||
transferAmount: null,
|
||
transferTime: '',
|
||
payStatus: 'dfj',
|
||
remark: '',
|
||
financeRemark: '',
|
||
transferAttachments: '',
|
||
attachments: [] as TransferAttachment[]
|
||
})
|
||
sch.transfers = transfers
|
||
schedules[sIdx] = sch
|
||
pay.schedules = schedules
|
||
list[pIdx] = pay
|
||
emit('update:modelValue', list)
|
||
}
|
||
|
||
function removeTransfer(pIdx: number, sIdx: number, tIdx: number) {
|
||
const list = [...(props.modelValue || [])]
|
||
const pay = { ...list[pIdx] }
|
||
const schedules = [...(pay.schedules || [])]
|
||
const sch = { ...schedules[sIdx] }
|
||
const transfers = [...(sch.transfers || [])]
|
||
transfers.splice(tIdx, 1)
|
||
sch.transfers = transfers
|
||
schedules[sIdx] = sch
|
||
pay.schedules = schedules
|
||
list[pIdx] = pay
|
||
emit('update:modelValue', list)
|
||
}
|
||
|
||
function getTransfer(pIdx: number, sIdx: number, tIdx: number) {
|
||
return props.modelValue?.[pIdx]?.schedules?.[sIdx]?.transfers?.[tIdx]
|
||
}
|
||
|
||
function transferRowKey(pIdx: number, sIdx: number, tIdx: number, row: Record<string, any>) {
|
||
return String(row.id || `${pIdx}-${sIdx}-${tIdx}`)
|
||
}
|
||
|
||
function attachmentsToUrls(tr: Record<string, any>) {
|
||
ensureTransferAttachments(tr)
|
||
return (tr.attachments || [])
|
||
.filter((f: TransferAttachment) => f?.fileUrl && !f._uploading)
|
||
.map((f: TransferAttachment) => String(f.fileUrl))
|
||
.join(',')
|
||
}
|
||
|
||
function validateTransferRow(tr: Record<string, any>) {
|
||
if (!(tr.transferType || '').trim()) {
|
||
ElMessage.warning('请选择转账方式')
|
||
return false
|
||
}
|
||
const amt = Number(tr.transferAmount)
|
||
if (!Number.isFinite(amt) || amt <= 0) {
|
||
ElMessage.warning('请填写转账金额')
|
||
return false
|
||
}
|
||
if ((tr.attachments || []).some((f: TransferAttachment) => f._uploading)) {
|
||
ElMessage.warning('凭证正在上传,请稍后再保存')
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
function applySavedTransfer(tr: Record<string, any>, data: Record<string, any>) {
|
||
if (!data) return
|
||
tr.id = data.id || tr.id
|
||
tr.payStatus = data.payStatus || tr.payStatus || 'dfj'
|
||
tr.scheduleId = data.scheduleId || tr.scheduleId
|
||
tr.samplePaymentId = data.samplePaymentId || tr.samplePaymentId
|
||
if (Array.isArray(data.attachments) && data.attachments.length) {
|
||
tr.attachments = data.attachments.map((a: Record<string, any>) => ({ ...a }))
|
||
}
|
||
if (data.transferAttachments) tr.transferAttachments = data.transferAttachments
|
||
emitChange()
|
||
}
|
||
|
||
async function saveTransferRow(pIdx: number, sIdx: number, tIdx: number) {
|
||
const tr = getTransfer(pIdx, sIdx, tIdx)
|
||
if (!tr || isTransferLocked(tr)) return
|
||
if (!props.protocolId) {
|
||
ElMessage.warning('请先保存协议后再保存转账记录')
|
||
return
|
||
}
|
||
const sch = props.modelValue?.[pIdx]?.schedules?.[sIdx]
|
||
if (!sch?.id) {
|
||
ElMessage.warning('请先保存协议后再保存转账记录')
|
||
return
|
||
}
|
||
if (!validateTransferRow(tr)) return
|
||
const key = transferRowKey(pIdx, sIdx, tIdx, tr)
|
||
savingKey.value = key
|
||
try {
|
||
const payload = {
|
||
transferType: tr.transferType,
|
||
transferAmount: tr.transferAmount,
|
||
transferTime: tr.transferTime || undefined,
|
||
transferAttachments: attachmentsToUrls(tr) || undefined,
|
||
remark: tr.remark
|
||
}
|
||
if (tr.id) {
|
||
const data: any = await fetchUpdatePaymentTransfer(String(tr.id), payload)
|
||
applySavedTransfer(tr, data || {})
|
||
ElMessage.success('转账记录已保存')
|
||
} else {
|
||
const data: any = await fetchCreatePaymentTransfer({
|
||
scheduleId: String(sch.id),
|
||
...payload
|
||
})
|
||
applySavedTransfer(tr, data || {})
|
||
}
|
||
} finally {
|
||
savingKey.value = ''
|
||
}
|
||
}
|
||
|
||
async function handleTransferAttachmentChange(
|
||
pIdx: number,
|
||
sIdx: number,
|
||
tIdx: number,
|
||
uploadFile: { raw?: File }
|
||
) {
|
||
const file = uploadFile.raw
|
||
if (!file) return
|
||
if (file.size > 20 * 1024 * 1024) {
|
||
ElMessage.warning('单文件不超过 20MB')
|
||
return
|
||
}
|
||
const tr = getTransfer(pIdx, sIdx, tIdx)
|
||
if (!tr) return
|
||
ensureTransferAttachments(tr)
|
||
const tempId = `temp_${Date.now()}_${Math.random().toString(16).slice(2)}`
|
||
tr.attachments.push({
|
||
id: tempId,
|
||
fileName: file.name,
|
||
fileUrl: '',
|
||
_uploading: true
|
||
})
|
||
emitChange()
|
||
const idx = tr.attachments.length - 1
|
||
try {
|
||
const fd = new FormData()
|
||
fd.append('folder', 'protocol-transfer')
|
||
fd.append('file', file)
|
||
const res: any = await fetchUploadAttachment(fd)
|
||
const data = res?.data || res
|
||
tr.attachments[idx] = {
|
||
id: data?.id || tempId,
|
||
fileName: data?.fileName || file.name,
|
||
fileUrl: data?.fileUrl || data?.url || '',
|
||
_uploading: false
|
||
}
|
||
emitChange()
|
||
} catch {
|
||
ElMessage.error('文件上传失败')
|
||
tr.attachments.splice(idx, 1)
|
||
emitChange()
|
||
}
|
||
}
|
||
|
||
function removeTransferAttachment(pIdx: number, sIdx: number, tIdx: number, fIdx: number) {
|
||
const tr = getTransfer(pIdx, sIdx, tIdx)
|
||
if (!tr?.attachments) return
|
||
tr.attachments.splice(fIdx, 1)
|
||
emitChange()
|
||
}
|
||
|
||
async function submitTransfer(tr: Record<string, any>) {
|
||
if (!tr.id) {
|
||
ElMessage.warning('请先保存该转账记录后再提交财务确认')
|
||
return
|
||
}
|
||
submittingId.value = String(tr.id)
|
||
try {
|
||
await fetchSubmitPaymentTransferFinance(String(tr.id))
|
||
tr.payStatus = 'jfqrz'
|
||
emitChange()
|
||
} finally {
|
||
submittingId.value = ''
|
||
}
|
||
}
|
||
|
||
async function withdrawTransfer(tr: Record<string, any>) {
|
||
if (!tr.id) return
|
||
await ElMessageBox.confirm(
|
||
'确认撤回该笔转账的财务确认申请?撤回后可修改并重新提交。',
|
||
'撤回',
|
||
{ type: 'warning' }
|
||
)
|
||
submittingId.value = String(tr.id)
|
||
try {
|
||
await fetchWithdrawPaymentTransfer(String(tr.id))
|
||
tr.payStatus = 'dfj'
|
||
emitChange()
|
||
ElMessage.success('已撤回')
|
||
} finally {
|
||
submittingId.value = ''
|
||
}
|
||
}
|
||
|
||
/** 上一轮各样本优惠价,用于判断是否强制同步合同金额 */
|
||
const lastPreferentialByCode = ref<Record<string, number | null>>({})
|
||
|
||
/** 样本增减 / 优惠价变更时对齐缴费方案(按 sampleCode) */
|
||
watch(
|
||
() =>
|
||
(props.samples || [])
|
||
.filter((s) => String(s.sampleStatus || '') !== 'yzf' && String(s.sampleCode || '').trim())
|
||
.map((s) => ({
|
||
code: String(s.sampleCode),
|
||
price: parsePreferentialPrice(s),
|
||
id: s.id || '',
|
||
sample: s
|
||
})),
|
||
(rows) => {
|
||
const existing = [...(props.modelValue || [])]
|
||
const byCode = new Map(existing.map((p) => [String(p.sampleCode || ''), p]))
|
||
const next: Array<Record<string, any>> = []
|
||
const nextPriceMap: Record<string, number | null> = {}
|
||
for (const row of rows) {
|
||
const { code, price, sample } = row
|
||
nextPriceMap[code] = price
|
||
const prev = byCode.get(code)
|
||
if (prev) {
|
||
const hadPrev = Object.prototype.hasOwnProperty.call(
|
||
lastPreferentialByCode.value,
|
||
code
|
||
)
|
||
const prevPrice = lastPreferentialByCode.value[code]
|
||
// 仅在「已跟踪过该样本且优惠价发生变化」时强制覆盖;首次对齐只替换空值/价目表默认
|
||
const priceChanged = hadPrev && price != null && prevPrice !== price
|
||
const aligned = applyPreferentialPriceToPayment(
|
||
{
|
||
...prev,
|
||
protocolSampleId: sample.id || prev.protocolSampleId || ''
|
||
},
|
||
sample,
|
||
priceChanged
|
||
)
|
||
next.push(aligned)
|
||
} else {
|
||
next.push(emptyDjPayment(sample))
|
||
}
|
||
}
|
||
lastPreferentialByCode.value = nextPriceMap
|
||
const same =
|
||
next.length === existing.length &&
|
||
next.every((p, i) => {
|
||
const e = existing[i]
|
||
if (!e) return false
|
||
return (
|
||
p.sampleCode === e.sampleCode &&
|
||
p.protocolSampleId === e.protocolSampleId &&
|
||
Number(p.contractAmount) === Number(e.contractAmount) &&
|
||
String(p.payMode || '') === String(e.payMode || '') &&
|
||
JSON.stringify(p.schedules || []) === JSON.stringify(e.schedules || [])
|
||
)
|
||
})
|
||
if (!same) emit('update:modelValue', next)
|
||
},
|
||
{ immediate: true, deep: true }
|
||
)
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.sample-payment-panel {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
}
|
||
|
||
.sample-pay-card {
|
||
padding: 14px 16px;
|
||
border: 1px solid var(--el-border-color-lighter, #e2e8f0);
|
||
border-radius: 10px;
|
||
background: #fff;
|
||
}
|
||
|
||
.sample-pay-card__head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.sample-pay-card__title {
|
||
display: flex;
|
||
align-items: center;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: var(--mint-text, #0f172a);
|
||
}
|
||
|
||
.ml-2 {
|
||
margin-left: 8px;
|
||
}
|
||
|
||
.section-subtitle {
|
||
margin: 12px 0 8px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: var(--mint-text-secondary, #475569);
|
||
}
|
||
|
||
.muted-hint {
|
||
font-size: 12px;
|
||
color: var(--mint-text-muted, #94a3b8);
|
||
}
|
||
|
||
.schedule-block {
|
||
margin-bottom: 10px;
|
||
padding: 10px 12px;
|
||
border-radius: 8px;
|
||
background: var(--mint-surface-muted, #f8fafc);
|
||
}
|
||
|
||
.schedule-block__head {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px 12px;
|
||
align-items: center;
|
||
margin-bottom: 10px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.schedule-due-edit {
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.schedule-arrears {
|
||
color: var(--mint-text-muted, #64748b);
|
||
}
|
||
|
||
.transfer-table {
|
||
width: 100%;
|
||
|
||
:deep(.el-date-editor) {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
.muted-dash {
|
||
color: var(--mint-text-muted, #94a3b8);
|
||
}
|
||
|
||
.attachment-upload {
|
||
width: 100%;
|
||
}
|
||
|
||
.attachment-list {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.upload-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 88px;
|
||
height: 72px;
|
||
cursor: pointer;
|
||
border: 1px dashed #a7f3d0;
|
||
border-radius: 8px;
|
||
background: #ecfdf5;
|
||
}
|
||
|
||
.upload-card-text {
|
||
margin-top: 4px;
|
||
font-size: 11px;
|
||
color: #047857;
|
||
}
|
||
|
||
.attachment-item {
|
||
width: 88px;
|
||
overflow: hidden;
|
||
background: #fff;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 8px;
|
||
|
||
&.is-uploading {
|
||
opacity: 0.6;
|
||
}
|
||
}
|
||
|
||
.attachment-preview {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 88px;
|
||
height: 52px;
|
||
background: #f8fafc;
|
||
|
||
img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
}
|
||
|
||
.attachment-preview__icon {
|
||
color: #64748b;
|
||
}
|
||
|
||
.attachment-footer {
|
||
display: flex;
|
||
gap: 4px;
|
||
align-items: center;
|
||
padding: 4px 6px;
|
||
}
|
||
|
||
.attachment-name {
|
||
flex: 1;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
font-size: 11px;
|
||
}
|
||
|
||
.action-btn {
|
||
flex-shrink: 0;
|
||
cursor: pointer;
|
||
color: #94a3b8;
|
||
|
||
&:hover {
|
||
color: #dc2626;
|
||
}
|
||
}
|
||
|
||
.form-grid {
|
||
display: grid;
|
||
gap: 8px 12px;
|
||
}
|
||
|
||
.form-grid--col4 {
|
||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
}
|
||
|
||
.form-grid--col6 {
|
||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||
}
|
||
|
||
@media (max-width: 1400px) {
|
||
.form-grid--col6 {
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
}
|
||
}
|
||
|
||
@media (max-width: 1100px) {
|
||
.form-grid--col4,
|
||
.form-grid--col6 {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
}
|
||
</style>
|